94 Commits

Author SHA1 Message Date
bcbb091dfb add invites to admin header
All checks were successful
Go / build (push) Successful in 12m15s
2025-04-15 00:36:51 +02:00
adfb3df283 update route permissions
Some checks failed
Go / build (push) Has been cancelled
2025-04-15 00:31:01 +02:00
1525f44687 show some sections only to admin user 2025-04-15 00:26:54 +02:00
3955d8626a add/rm tokens, register with token 2025-04-15 00:12:34 +02:00
6d63e53200 Add token repositorie 2025-04-14 23:29:24 +02:00
cca0b2775c disable /register if a user exist 2025-04-14 22:55:36 +02:00
98c75c111f update css 2025-04-14 22:42:25 +02:00
b2735e178f hide elements when not logged in 2025-04-14 22:42:07 +02:00
1c9fc230b1 remove rest api routes
currently not used at all
2025-04-14 22:41:43 +02:00
6c2b3964fe rm unused code 2025-04-14 22:41:26 +02:00
19ce41aca7 update styling
All checks were successful
Go / build (push) Successful in 12m16s
2025-04-14 12:22:53 +02:00
763bb35a45 set item amount at cart
All checks were successful
Go / build (push) Successful in 12m21s
2025-04-14 11:29:27 +02:00
6130843aa7 update printername
All checks were successful
Go / build (push) Successful in 12m15s
2025-04-14 10:59:22 +02:00
667c3eba13 add image to edititem view
All checks were successful
Go / build (push) Successful in 12m18s
2025-04-14 01:36:54 +02:00
e22cc0b243 dont clear tags on edit 2025-04-14 01:36:35 +02:00
202c845bee fix #12 edit item without pdf removew preview image
All checks were successful
Go / build (push) Successful in 12m20s
2025-04-14 01:05:51 +02:00
d839416fdd colored tag item view
All checks were successful
Go / build (push) Successful in 12m14s
2025-04-14 00:42:26 +02:00
821f4e526f tag colors
Some checks failed
Go / build (push) Has been cancelled
2025-04-14 00:38:23 +02:00
9d2819cac4 hack to let tailwindcss keep all tag colors 2025-04-14 00:36:56 +02:00
fd46f35023 add tagview 2025-04-13 23:32:38 +02:00
6943e3c9b7 use simple golang workflow for now
All checks were successful
Go / build (push) Successful in 12m56s
2025-04-13 22:18:08 +02:00
a90131c8be update workflow
Some checks failed
Build / test (push) Failing after 2m4s
2025-04-13 22:10:04 +02:00
a04d057bce nixpkgs update 2025-04-13 22:09:28 +02:00
37ffeaa0f3 update workflow
Some checks failed
Build / test (push) Failing after 2m2s
2025-04-13 22:00:22 +02:00
961113ebd6 workflow update
All checks were successful
Build / test (push) Successful in 39s
2025-04-13 21:58:34 +02:00
529741e150 update workflow
Some checks failed
Build / test (push) Failing after 2m51s
2025-04-13 21:53:03 +02:00
8a7d66f815 fix #1 Cart is empty
Some checks failed
Build / test (push) Failing after 2m50s
2025-04-13 16:07:53 +02:00
fa561c921d fix #7 Order is empty bug 2025-04-13 15:43:32 +02:00
a9170b63b7 update workflow
Some checks failed
Build / test (push) Failing after 2m47s
2025-04-13 14:40:09 +02:00
f1e191a294 update workflow
Some checks failed
Build / build (push) Failing after 2m6s
2025-04-13 14:35:40 +02:00
6e14716305 update workflow
Some checks failed
Build / build (push) Failing after 0s
2025-04-13 14:34:41 +02:00
f70d053a23 update workflow
Some checks failed
Build / build (push) Failing after 23s
2025-04-13 14:31:53 +02:00
fb5091aad3 check disable sandbox
Some checks failed
Build / flake-check (push) Failing after 1m41s
2025-04-13 14:25:51 +02:00
2e82e3a8b9 fix check name
Some checks failed
Build / flake-check (push) Failing after 1m46s
2025-04-13 14:20:50 +02:00
d2d0f39e33 add default package 2025-04-13 14:20:35 +02:00
fc85113cd6 update workflow
Some checks failed
Integration Test / flake-check (push) Failing after 1m12s
2025-04-12 17:04:42 +02:00
08d79e2c38 update workflow
Some checks failed
Integration Test / flake-check (push) Failing after 1m8s
2025-04-12 17:03:00 +02:00
6a8ab81b88 try fix workflow by setting $HOME
Some checks failed
Integration Test / flake-check (push) Failing after 2m29s
2025-04-12 16:56:19 +02:00
584030431d add workflow
Some checks failed
Integration Test / flake-check (push) Failing after 2m41s
2025-04-12 15:10:52 +02:00
a8cb853c92 systemd service init folders 2025-04-12 02:25:22 +02:00
0b4439647a set working dir for systemd service 2025-04-11 23:42:18 +02:00
2c4c21bd4d fix ExecStart script 2025-04-11 22:57:07 +02:00
c4527ff228 fix typo 2025-04-11 19:51:41 +02:00
256b41c880 fix systemd PATH 2025-04-11 19:47:15 +02:00
af6787831a update systemd PATH 2025-04-11 19:42:28 +02:00
ec1a8b155a module add missing dep 2025-04-11 19:31:33 +02:00
122651677f set views by env 2025-04-11 18:17:33 +02:00
af11f88769 add check 2025-04-11 18:11:04 +02:00
0bcec807c8 add test 2025-04-11 18:10:55 +02:00
a3b066859b set static file by env 2025-04-11 18:10:37 +02:00
b349c4baba update header 2025-04-11 18:08:57 +02:00
e406eda9c9 add editorder view 2025-04-11 17:32:17 +02:00
a1fc053fa8 give go module proper name 2025-04-11 17:31:46 +02:00
1da5e3e8b4 allow printing/editing orders 2025-04-11 16:06:41 +02:00
adac366896 add orders edit view 2025-04-11 14:39:58 +02:00
b14deeb24f add order/:token/print 2025-04-11 14:17:22 +02:00
d5c3d7fe75 wip rm sessionid of cartitem after order creation 2025-04-11 14:12:52 +02:00
b55bf67e57 reenable stapling 2025-04-11 13:59:45 +02:00
d43401dc62 fix edititem 2025-04-11 13:59:36 +02:00
8f5dd27ae0 printing works for now 2025-04-11 13:12:39 +02:00
e864a75678 working printer
bypasstray leads to problems still
2025-04-10 19:38:32 +02:00
0436d3a2bd fix wrong link 2025-04-10 15:59:37 +02:00
490a9f1444 fix delete/edit item 2025-04-10 15:53:32 +02:00
fdb11bc57c add batchupload 2025-04-10 15:44:59 +02:00
f87a6352dd implement dummy printer 2025-04-10 14:26:20 +02:00
9e3a04cd78 cartItemController use sessionid again 2025-04-10 13:20:41 +02:00
f39b6205d1 add basic print controller + view 2025-04-10 12:54:03 +02:00
5a7565663d update 2025-04-10 11:52:34 +02:00
89f7c8c4bb fix logo 2025-03-25 21:00:56 +01:00
869642fa4d format 2025-03-25 20:47:05 +01:00
854573eb3a allow finalizing orders 2025-03-25 20:36:28 +01:00
30b32a571c update readme 2025-03-25 18:42:49 +01:00
22bf9d4390 update or create order 2025-03-24 01:04:01 +01:00
2fce17d528 [nix] init 2025-03-23 23:40:08 +01:00
e53796bda6 wip order handler 2025-03-21 13:52:51 +01:00
69420fab6c generate preview image from pdf if no image given 2025-03-21 13:27:13 +01:00
6cbd9f5a3b change aspect ratio preview images 2025-03-21 13:26:46 +01:00
9544849ef6 allow editing address on order summary 2025-03-21 13:26:26 +01:00
ac8b0339c3 basic order view 2025-03-07 16:16:23 +01:00
974b95b244 add order rep 2025-03-07 12:04:19 +01:00
1f411edf50 add order 2025-03-07 11:55:49 +01:00
cbaf48123f redirect cart 2025-03-06 00:05:24 +01:00
a88be8de62 work on checkout 2025-03-05 23:55:31 +01:00
639eaa04f5 calculate baseprice 2025-03-05 15:20:08 +01:00
fa214f4fdf add pdf upload 2025-03-05 15:06:28 +01:00
503ab0e9bd add basic error page 2025-03-05 12:40:09 +01:00
e54be98fe9 wip order 2025-03-05 12:37:41 +01:00
56035bb32f fix cart price calc 2025-03-05 10:41:54 +01:00
1ccfc620d0 variants 2025-03-05 10:39:58 +01:00
a65ba9c98c wip itemvariant 2025-03-04 16:42:59 +01:00
d24dfdf262 wip checkout 2025-03-04 13:28:38 +01:00
0c85ca5938 wip cart handling 2025-03-04 13:00:30 +01:00
23392cc5c1 cartItem one2one relation to shopitem 2025-03-04 11:30:26 +01:00
5e22be5074 add basic cart 2025-03-03 21:09:47 +01:00
53 changed files with 4107 additions and 521 deletions

View File

@@ -0,0 +1,17 @@
name: Go
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24.x'
- name: Install dependencies
run: go get .
- name: Build
run: go build -v ./...

View File

@@ -7,3 +7,29 @@ The zines will then be printed on demand and be send.
for payment a simple random string will be created to connect payments to orders.
This way also cash could be send like mullvad is doing it.
# Development
To get the webserver running do the following:
```bash
nix develop .#
# run the webserver
go run main.go
```
For updating tailwindcss on the fly open extra shell
```bash
nix develop .#
tailwindcss -i static/input.css -o static/output.css --watch
```
# Printer Testing
- [x] long edge + bypass front cover
- `lp -d KONICA_MINOLTA_KONICA_MINOLTA_bizhub_C258/BookletPrint -o Fold=HalfFold -o FrontCoverPage=Printed -o FrontCoverTray=BypassTray ~/proggn/malobeo/zineshop/Test-book-long-edge.pdf`
- [x] short edge + bypass front cover
- ` lp -d KONICA_MINOLTA_KONICA_MINOLTA_bizhub_C258/BookletPrint -o Fold=HalfFold -o Binding=TopBinding -o FrontCoverPage=Printed -o FrontCoverTray=BypassTray ~/proggn/malobeo/zineshop/Test-book-short-edge.pdf `
- [x] booklet + bypass front cover
- `lp -d KONICA_MINOLTA_KONICA_MINOLTA_bizhub_C258/BookletPrint -o Combination=Booklet -o PageSize=A5 -o Fold=HalfFold -o FrontCoverPage=Printed -o FrontCoverTray=BypassTray ~/proggn/malobeo/zineshop/Test.pdf`
---
- [x] okular long edge + bypass front cover
- [x] okular short edge + bypass front cover
- [ ] okular booklet + bypass front cover

View File

@@ -0,0 +1,605 @@
package controllers
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"git.dynamicdiscord.de/kalipso/zineshop/models"
//"git.dynamicdiscord.de/kalipso/zineshop/services"
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
"git.dynamicdiscord.de/kalipso/zineshop/utils"
)
type CartItemController interface {
//CRUDController
CartItemView(*gin.Context)
AddItemHandler(*gin.Context)
DeleteItemHandler(*gin.Context)
EditItemHandler(*gin.Context)
CheckoutView(*gin.Context)
CheckoutHandler(*gin.Context)
OrderView(*gin.Context)
OrderHandler(*gin.Context)
OrdersView(*gin.Context)
OrdersHandler(*gin.Context)
}
type cartItemController struct{}
func NewCartItemController() CartItemController {
return &cartItemController{}
}
// getSetCookieValue retrieves the value of a cookie from the Set-Cookie header
func getSetCookieValue(c *gin.Context, cookieName string) string {
// Check the Set-Cookie headers
cookies := c.Writer.Header()["Set-Cookie"]
for _, cookie := range cookies {
if strings.HasPrefix(cookie, cookieName+"=") {
// Extract the cookie value
parts := strings.SplitN(cookie, ";", 2)
if len(parts) > 0 {
return strings.TrimPrefix(parts[0], cookieName+"=")
}
}
}
return "" // Return empty string if cookie is not found
}
func GetSessionId(ctx *gin.Context) string {
sessionId, err := ctx.Cookie("session_id")
if err != nil {
//we need to check if we already set cookie in the response so that we dont do this multiple times
responseCookie := getSetCookieValue(ctx, "session_id")
if len(responseCookie) != 0 {
return responseCookie
}
sessionId = utils.GenerateSessionId(16)
ctx.SetCookie("session_id", sessionId, 3600, "/", "", false, true)
}
return sessionId
}
func (rc *cartItemController) NewCartItemFromForm(ctx *gin.Context) (models.CartItem, error) {
sessionId := GetSessionId(ctx)
shopItemIdStr := ctx.PostForm("ShopItemId")
shopItemId, err := strconv.Atoi(shopItemIdStr)
itemVariantIdStr := ctx.PostForm("ItemVariantId")
itemVariantId, err := strconv.Atoi(itemVariantIdStr)
if err != nil {
return models.CartItem{}, err
}
quantity := 1
shopItem, err := repositories.ShopItems.GetById(shopItemIdStr)
if err != nil {
return models.CartItem{}, err
}
itemVariant, err := repositories.ShopItems.GetVariantById(itemVariantIdStr)
cartItem := models.CartItem{
SessionId: sessionId,
ShopItemId: uint(shopItemId),
ShopItem: shopItem,
ItemVariantId: uint(itemVariantId),
ItemVariant: itemVariant,
Quantity: quantity,
}
return cartItem, nil
}
func (rc *cartItemController) NewAddressFromForm(ctx *gin.Context) (models.AddressInfo, error) {
firstName := ctx.PostForm("firstName")
lastName := ctx.PostForm("lastName")
address := ctx.PostForm("address")
postalCode := ctx.PostForm("postalCode")
city := ctx.PostForm("city")
country := ctx.PostForm("country")
if firstName == "" {
return models.AddressInfo{}, fmt.Errorf("first name missing.")
}
if lastName == "" {
return models.AddressInfo{}, fmt.Errorf("Last name missing.")
}
if address == "" {
return models.AddressInfo{}, fmt.Errorf("address missing.")
}
if postalCode == "" {
return models.AddressInfo{}, fmt.Errorf("postalCode missing.")
}
if city == "" {
return models.AddressInfo{}, fmt.Errorf("city missing.")
}
if country == "" {
return models.AddressInfo{}, fmt.Errorf("country missing.")
}
return models.AddressInfo{
FirstName: firstName,
LastName: lastName,
Address: address,
PostalCode: postalCode,
City: city,
Country: country,
}, nil
}
func (rc *cartItemController) NewOrderFromForm(ctx *gin.Context) (models.Order, error) {
sessionId := GetSessionId(ctx)
status := models.OrderStatus("AwaitingConfirmation")
token := utils.GenerateToken()
email := ctx.PostForm("email")
comment := ctx.PostForm("comment")
firstName := ctx.PostForm("firstName")
lastName := ctx.PostForm("lastName")
address := ctx.PostForm("address")
postalCode := ctx.PostForm("postalCode")
city := ctx.PostForm("city")
country := ctx.PostForm("country")
shippingStr := ctx.PostForm("shippingMethod")
//address, err := rc.NewAddressFromForm(ctx)
//if shippingStr != "pickup" {
// if err != nil {
// return models.Order{}, fmt.Errorf("Invalid address information.")
// }
//}
shipping, err := models.GetShippingMethod(shippingStr)
if err != nil {
return models.Order{}, fmt.Errorf("Invalid shipping method.")
}
cartItems, err := repositories.CartItems.GetAllBySession(sessionId)
fmt.Println(sessionId)
fmt.Println(cartItems)
if err != nil {
return models.Order{}, err
}
order := models.Order{
SessionId: sessionId,
Status: status,
Token: token,
Email: email,
Comment: comment,
FirstName: firstName,
LastName: lastName,
Address: address,
PostalCode: postalCode,
City: city,
Country: country,
Shipping: shipping.Id,
CartItems: cartItems,
}
return order, nil
}
func (rc *cartItemController) Create(c *gin.Context) {
cartItem, err := rc.NewCartItemFromForm(c)
if err != nil {
ReplyError(c, fmt.Errorf("cartItem creation failed: %s", err))
return
}
_, err = repositories.CartItems.Create(cartItem)
if err != nil {
ReplyError(c, fmt.Errorf("cartItem creation failed: %s", err))
return
}
ReplyOK(c, "cartItem was created")
}
func (rc *cartItemController) Update(c *gin.Context) {
cartItem, err := rc.NewCartItemFromForm(c)
if err != nil {
ReplyError(c, err)
return
}
_, err = repositories.CartItems.Update(cartItem)
if err != nil {
ReplyError(c, fmt.Errorf("cartItem creation failed: %s", err))
return
}
ReplyOK(c, "cartItem was updated")
}
//func (rc *cartItemController) Delete(c *gin.Context) {
// err := repositories.CartItems.DeleteBySessionId(c.Param("id"))
//
// if err != nil {
// ReplyError(c, fmt.Errorf("cartItem deletion failed: %s", err))
// return
// }
//
// ReplyOK(c, "cartItem was deleted")
//}
func (rc *cartItemController) CartItemView(c *gin.Context) {
sessionId := GetSessionId(c)
cartItems, err := repositories.CartItems.GetAllBySession(sessionId)
//cartItems, err := repositories.CartItems.GetAll()
if err != nil {
c.HTML(http.StatusBadRequest, "cart.html", gin.H{"data": gin.H{"error": err}})
}
priceTotal := 0.0
for _, cartItem := range cartItems {
priceTotal += (float64(cartItem.Quantity) * cartItem.ItemVariant.Price)
}
fmt.Println("PRICE TOTAL", priceTotal)
data := CreateSessionData(c, gin.H{
"cartItems": cartItems,
"priceTotal": fmt.Sprintf("%.2f", priceTotal), //round 2 decimals
"shipping": models.GetShippingMethods(),
})
c.HTML(http.StatusOK, "cart.html", data)
}
func (rc *cartItemController) AddItemHandler(c *gin.Context) {
cartItem, err := rc.NewCartItemFromForm(c)
if err != nil {
fmt.Println(err)
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return
}
_, err = repositories.CartItems.Create(cartItem)
if err != nil {
data := CreateSessionData(c, gin.H{
"error": err,
"success": "",
})
c.HTML(http.StatusOK, "cart.html", data)
return
}
rc.CartItemView(c)
}
func (rc *cartItemController) DeleteItemHandler(c *gin.Context) {
err := repositories.CartItems.DeleteById(c.PostForm("id"))
if err != nil {
fmt.Println(err)
data := CreateSessionData(c, gin.H{
"error": err,
"success": "",
})
c.HTML(http.StatusOK, "index.html", data)
}
c.Redirect(http.StatusFound, "/cart")
}
func (rc *cartItemController) EditItemHandler(c *gin.Context) {
cartItemId := c.PostForm("id")
cartItem, err := repositories.CartItems.GetById(cartItemId)
if err != nil {
fmt.Println(err)
c.Redirect(http.StatusFound, "/cart")
return
}
action := c.PostForm("action")
if action == "setAmount" {
amountStr := c.PostForm("amount")
amount, err := strconv.Atoi(amountStr)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return
}
if amount < 0 {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "amount cant be negative"})
return
}
if amount > 500 {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "amount cant be over 500"})
return
}
cartItem.Quantity = amount
}
if action == "increase" {
cartItem.Quantity += 1
}
if action == "decrease" {
cartItem.Quantity -= 1
if cartItem.Quantity == 0 {
cartItem.Quantity = 1
}
}
_, err = repositories.CartItems.Update(cartItem)
if err != nil {
fmt.Println(err)
}
c.Redirect(http.StatusFound, "/cart")
}
func (rc *cartItemController) CheckoutView(c *gin.Context) {
shippingMethod := c.Query("shippingMethod")
if shippingMethod == "" {
rc.CartItemView(c)
return
}
c.HTML(http.StatusOK, "checkout.html", gin.H{
"askAddress": (shippingMethod != "pickup"),
"shippingMethod": shippingMethod,
})
}
func (rc *cartItemController) CheckoutHandler(c *gin.Context) {
order, err := rc.NewOrderFromForm(c)
if err != nil {
fmt.Println(err)
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return
}
existingOrder, err := repositories.Orders.GetBySession(order.SessionId)
if errors.Is(err, gorm.ErrRecordNotFound) {
fmt.Println("Creating Order")
createdOrder, err := repositories.Orders.Create(order)
if err != nil {
data := CreateSessionData(c, gin.H{
"error": err,
"success": "",
})
c.HTML(http.StatusOK, "error.html", data)
return
}
for _, cartItem := range order.CartItems {
cartItem.OrderID = createdOrder.ID
repositories.CartItems.Update(cartItem)
}
} else if err == nil {
fmt.Println("Updating Order")
order.ID = existingOrder.ID
order.CreatedAt = existingOrder.CreatedAt
_, err := repositories.Orders.Update(order)
if err != nil {
fmt.Println(err)
}
}
shipping, err := models.GetShippingMethod(order.Shipping)
if err != nil {
data := CreateSessionData(c, gin.H{
"error": err,
"success": "",
})
c.HTML(http.StatusOK, "error.html", data)
return
}
priceProducts, priceTotal, err := order.CalculatePrices()
if err != nil {
data := CreateSessionData(c, gin.H{
"error": err,
"success": "",
})
c.HTML(http.StatusOK, "error.html", data)
return
}
data := CreateSessionData(c, gin.H{
"error": "",
"success": "",
"order": order,
"askAddress": (order.Shipping != "pickup"),
"isPreview": true,
"shipping": shipping,
"priceProducts": fmt.Sprintf("%.2f", priceProducts), //round 2 decimals
"priceTotal": fmt.Sprintf("%.2f", priceTotal), //round 2 decimals
})
fmt.Println(order)
c.HTML(http.StatusOK, "orderpreview.html", data)
}
func (rc *cartItemController) OrderView(c *gin.Context) {
orderToken := c.Param("token")
order, err := repositories.Orders.GetByToken(orderToken)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Order does not exist."})
return
}
shipping, err := models.GetShippingMethod(order.Shipping)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Could not get shipping method"})
return
}
priceProducts, priceTotal, err := order.CalculatePrices()
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Could not calculate final prices"})
return
}
fmt.Printf("Order: %v\n", order)
fmt.Printf("PriceTotal: %v\n", priceTotal)
fmt.Printf("Amount Items: %v\n", len(order.CartItems))
for _, item := range order.CartItems {
fmt.Printf("Cartitem: %v", item)
}
c.HTML(http.StatusOK, "order.html", CreateSessionData(c, gin.H{
"error": "",
"success": "",
"order": order,
"shipping": shipping,
"priceProducts": fmt.Sprintf("%.2f", priceProducts), //round 2 decimals
"priceTotal": fmt.Sprintf("%.2f", priceTotal), //round 2 decimals
}))
}
func (rc *cartItemController) OrderHandler(c *gin.Context) {
confirmation := c.PostForm("confirm-order")
if confirmation == "" {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Something went wrong, try again later"})
return
}
if confirmation != "true" {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Order was not confirmed."})
return
}
sessionId := GetSessionId(c)
order, err := repositories.Orders.GetBySession(sessionId)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Something went wrong, try again later"})
return
}
order.Status = models.AwaitingPayment
err = order.Validate()
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return
}
for idx := range order.CartItems {
order.CartItems[idx].SessionId = "0"
repositories.CartItems.Update(order.CartItems[idx])
}
_, err = repositories.Orders.Update(order)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return
}
//TODO: cartItemRepository delete all by session - otherwise items stay in cart after completing order..
c.Redirect(http.StatusFound, fmt.Sprintf("/order/%s", order.Token))
}
func (rc *cartItemController) OrdersView(c *gin.Context) {
orders, err := repositories.Orders.GetAll()
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Orders doe not exist."})
return
}
c.HTML(http.StatusOK, "editorders.html", CreateSessionData(c, gin.H{
"error": "",
"success": "",
"orders": orders,
}))
}
func (rc *cartItemController) OrdersHandler(c *gin.Context) {
token := c.Param("token")
if token == "" {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "EmptyToken"})
return
}
action := c.PostForm("action")
if action == "update" {
order, err := repositories.Orders.GetByToken(token)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "No order with given token found"})
return
}
status := c.PostForm("order-status")
//TODO validate status
if status == "" {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Invalid Order Status"})
return
}
order.Status = models.OrderStatus(status)
_, err = repositories.Orders.Update(order)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return
}
}
if action == "delete" {
fmt.Println("Deleting Order ", token)
err := repositories.Orders.DeleteByToken(token)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return
}
}
c.Redirect(http.StatusFound, "/orders")
}

View File

@@ -0,0 +1,146 @@
package controllers
import (
"fmt"
"net/http"
"strconv"
"git.dynamicdiscord.de/kalipso/zineshop/models"
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
"github.com/gin-gonic/gin"
)
type PrintController interface {
PrintVariantView(*gin.Context)
PrintCartView(*gin.Context)
PrintOrderView(*gin.Context)
PrintHandler(*gin.Context)
}
type printController struct{}
func NewPrintController() PrintController {
return &printController{}
}
func (rc *printController) PrintVariantView(c *gin.Context) {
variant, err := repositories.ShopItems.GetVariantById(c.Param("id"))
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"data": gin.H{"error": err}})
return
}
shopItem, err := repositories.ShopItems.GetById(fmt.Sprintf("%v", variant.ShopItemID))
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"data": gin.H{"error": err}})
return
}
type ShopItemVariantPair struct {
ShopItem models.ShopItem
ItemVariant models.ItemVariant
}
data := CreateSessionData(c, gin.H{
"itemVariants": []ShopItemVariantPair{
{ShopItem: shopItem, ItemVariant: variant},
},
})
fmt.Println(data)
c.HTML(http.StatusOK, "printvariant.html", data)
}
func (rc *printController) PrintCartView(c *gin.Context) {
sessionId := GetSessionId(c)
cartItems, err := repositories.CartItems.GetAllBySession(sessionId)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"data": gin.H{"error": err}})
return
}
data := CreateSessionData(c, gin.H{
"cartItems": cartItems,
})
c.HTML(http.StatusOK, "printvariant.html", data)
}
func (rc *printController) PrintOrderView(c *gin.Context) {
order, err := repositories.Orders.GetByToken(c.Param("token"))
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"data": gin.H{"error": err}})
return
}
cartItems := order.CartItems
data := CreateSessionData(c, gin.H{
"cartItems": cartItems,
})
c.HTML(http.StatusOK, "printvariant.html", data)
}
func (rc *printController) PrintHandler(c *gin.Context) {
variantIds := c.PostFormArray("variant-id[]")
variantAmounts := c.PostFormArray("variant-amount[]")
variantCoverPages := c.PostFormArray("variant-coverpage[]")
if len(variantIds) != len(variantAmounts) || len(variantIds) != len(variantCoverPages) {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"data": gin.H{"error": "Invalid arguments"}})
return
}
var printJobs []models.PrintJob
for idx := range variantIds {
variant, err := repositories.ShopItems.GetVariantById(variantIds[idx])
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"data": gin.H{"error": err}})
return
}
shopItem, err := repositories.ShopItems.GetById(fmt.Sprintf("%v", variant.ShopItemID))
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"data": gin.H{"error": err}})
return
}
coverPage := false
if variantCoverPages[idx] == "1" {
coverPage = true
}
variantAmount, err := strconv.Atoi(variantAmounts[idx])
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"data": gin.H{"error": err}})
return
}
printJob, err := models.NewPrintJob(shopItem, variant, coverPage, uint(variantAmount))
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"data": gin.H{"error": err}})
return
}
printJobs = append(printJobs, printJob)
}
executeJobs := func() {
for _, printJob := range printJobs {
printJob.Execute()
}
}
go executeJobs()
c.HTML(http.StatusOK, "index.html", nil)
}

View File

@@ -3,19 +3,20 @@ package controllers
import (
"fmt"
"net/http"
"strconv"
"os/exec"
"path/filepath"
"strconv"
"github.com/gin-gonic/gin"
"example.com/gin/test/models"
//"example.com/gin/test/services"
"example.com/gin/test/repositories"
"git.dynamicdiscord.de/kalipso/zineshop/models"
//"git.dynamicdiscord.de/kalipso/zineshop/services"
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
)
type CRUDController interface {
Create(*gin.Context)
GetAll(*gin.Context)
GetAll(*gin.Context)
GetById(*gin.Context)
Update(*gin.Context)
Delete(*gin.Context)
@@ -26,6 +27,8 @@ type ShopItemController interface {
ShopItemView(*gin.Context)
AddItemView(*gin.Context)
AddItemHandler(*gin.Context)
AddItemsView(*gin.Context)
AddItemsHandler(*gin.Context)
CreateTag(*gin.Context)
GetAllTags(*gin.Context)
EditItemView(*gin.Context)
@@ -37,7 +40,7 @@ type ShopItemController interface {
AddTagHandler(*gin.Context)
}
type shopItemController struct {}
type shopItemController struct{}
func NewShopItemController() ShopItemController {
return &shopItemController{}
@@ -66,40 +69,96 @@ func (rc *shopItemController) GetById(c *gin.Context) {
}
func (rc *shopItemController) NewShopItemFromForm(ctx *gin.Context) (models.ShopItem, error) {
defaultImagePath := "static/img/zine.jpg"
name := ctx.PostForm("name")
abstract := ctx.PostForm("abstract")
description := ctx.PostForm("description")
priceStr := ctx.PostForm("price")
categoryStr := ctx.PostForm("category")
variantNames := ctx.PostFormArray("variant-name[]")
variantValues := ctx.PostFormArray("variant-value[]")
tagIds := ctx.PostFormArray("tags[]")
image, err := ctx.FormFile("image")
dst := "static/img/zine.jpg"
dstImage := defaultImagePath
printMode := ctx.PostForm("print-mode")
if err == nil {
dst = filepath.Join("static/uploads", image.Filename)
if err := ctx.SaveUploadedFile(image, dst); err != nil {
dstImage = filepath.Join("static/uploads", image.Filename)
if err := ctx.SaveUploadedFile(image, dstImage); err != nil {
return models.ShopItem{}, fmt.Errorf("Could not save image")
}
}
}
dstPdf := ""
pdf, err := ctx.FormFile("pdf")
if err == nil {
dstPdf = filepath.Join("static/uploads", pdf.Filename)
fmt.Println("Saving pdf at ", dstPdf)
if err := ctx.SaveUploadedFile(pdf, dstPdf); err != nil {
return models.ShopItem{}, fmt.Errorf("Could not save PDF")
}
if dstImage == defaultImagePath {
dstImage = dstPdf + ".preview.png"
cmd := exec.Command("pdftoppm", "-png", "-singlefile", dstPdf, dstPdf+".preview")
_, err := cmd.Output()
if err != nil {
fmt.Println("Error during pdftoppm: ", err.Error())
}
}
} else {
fmt.Println(err)
}
if name == "" || description == "" {
return models.ShopItem{}, fmt.Errorf("Name or description empty")
}
// Convert the price string to float64
price, err := strconv.ParseFloat(priceStr, 64)
if err != nil {
return models.ShopItem{}, fmt.Errorf("Could not parse price")
}
category, err := models.ParseCategory(categoryStr)
if err != nil {
return models.ShopItem{}, err
}
var variants []models.ItemVariant
fmt.Println("VariantNames: ", variantNames)
fmt.Println("VariantValues: ", variantValues)
if len(variantNames) != len(variantValues) {
return models.ShopItem{}, fmt.Errorf("Different number of variant names and values")
}
for idx := range variantNames {
if variantValues[idx] == "" || variantNames[idx] == "" {
continue
}
price, err := strconv.ParseFloat(variantValues[idx], 64)
if err != nil {
return models.ShopItem{}, fmt.Errorf("Could not variant parse price")
}
variants = append(variants, models.ItemVariant{
Name: variantNames[idx],
Price: price,
})
}
shopItem := models.ShopItem{
Name: name,
Abstract: abstract,
Name: name,
Abstract: abstract,
Description: description,
Price: price,
IsPublic: true,
Image: dst,
Category: category,
IsPublic: true,
BasePrice: rc.GetBasePrice(variants),
Image: dstImage,
Pdf: dstPdf,
Variants: variants,
PrintMode: printMode,
}
fmt.Println("Creating Shopitem: ", shopItem)
for _, tagId := range tagIds {
tag, err := repositories.Tags.GetById(tagId)
@@ -111,9 +170,24 @@ func (rc *shopItemController) NewShopItemFromForm(ctx *gin.Context) (models.Shop
}
return shopItem, nil
//return services.ShopItems.NewShopItem(name, abstract, description, price, tagIds)
}
func (rc *shopItemController) GetBasePrice(variants []models.ItemVariant) float64 {
result := 0.0
for idx, variant := range variants {
if idx == 0 {
result = variant.Price
continue
}
if variant.Price < result {
result = variant.Price
}
}
return result
}
func (rc *shopItemController) Create(c *gin.Context) {
shopItem, err := rc.NewShopItemFromForm(c)
@@ -133,7 +207,6 @@ func (rc *shopItemController) Create(c *gin.Context) {
ReplyOK(c, "shopItem was created")
}
func (rc *shopItemController) Update(c *gin.Context) {
shopItemId, err := strconv.Atoi(c.Param("id"))
@@ -160,6 +233,7 @@ func (rc *shopItemController) Update(c *gin.Context) {
ReplyOK(c, "shopItem was updated")
}
// TODO: delete associated cartitems
func (rc *shopItemController) Delete(c *gin.Context) {
err := repositories.ShopItems.DeleteById(c.Param("id"))
@@ -175,19 +249,19 @@ func (rc *shopItemController) ShopItemView(c *gin.Context) {
shopItem, err := repositories.ShopItems.GetById(c.Param("id"))
if err != nil {
c.HTML(http.StatusBadRequest, "shopitem.html", gin.H{ "data": gin.H{ "error": err } })
c.HTML(http.StatusBadRequest, "shopitem.html", gin.H{"data": gin.H{"error": err}})
}
//TODO: get tags by item
tags, err := repositories.Tags.GetAll()
if err != nil {
c.HTML(http.StatusBadRequest, "shopitem.html", gin.H{ "data": gin.H{ "error": err } })
c.HTML(http.StatusBadRequest, "shopitem.html", gin.H{"data": gin.H{"error": err}})
}
data := CreateSessionData(c, gin.H{
"shopItem": shopItem,
"tags": tags,
"tags": tags,
})
if err != nil {
@@ -201,113 +275,214 @@ func (rc *shopItemController) AddItemView(c *gin.Context) {
tags, err := repositories.Tags.GetAll()
if err != nil {
c.HTML(http.StatusBadRequest, "additem.html", gin.H{ "error": err })
c.HTML(http.StatusBadRequest, "additem.html", gin.H{"error": err})
}
data := CreateSessionData(c, gin.H{
"error": "",
"error": "",
"success": "",
"tags": tags,
"tags": tags,
})
c.HTML(http.StatusOK, "additem.html", data)
}
func (rc *shopItemController) AddItemHandler(c *gin.Context) {
shopItem, err := rc.NewShopItemFromForm(c)
errorHandler := func(err error, tags []models.Tag) {
data := CreateSessionData(c, gin.H{
"error": err,
"success": "",
"tags": tags,
})
if err != nil {
fmt.Println(err)
c.HTML(http.StatusBadRequest, "additem.html", gin.H{ "error": err })
return
c.HTML(http.StatusOK, "additem.html", data)
}
tags, err := repositories.Tags.GetAll()
if err != nil {
fmt.Println(err)
c.HTML(http.StatusBadRequest, "additem.html", gin.H{ "error": err })
errorHandler(err, tags)
return
}
shopItem, err := rc.NewShopItemFromForm(c)
if err != nil {
errorHandler(err, tags)
return
}
_, err = repositories.ShopItems.Create(shopItem)
if err != nil {
data := CreateSessionData(c, gin.H{
"error": err,
"success": "",
"tags": tags,
})
c.HTML(http.StatusOK, "additem.html", data)
errorHandler(err, tags)
return
}
data := CreateSessionData(c, gin.H{
"error": "",
"error": "",
"success": fmt.Sprintf("Item '%s' Registered", shopItem.Name),
"tags": tags,
"tags": tags,
})
c.HTML(http.StatusOK, "additem.html", data)
}
func (rc *shopItemController) AddItemsView(c *gin.Context) {
data := CreateSessionData(c, gin.H{})
c.HTML(http.StatusOK, "batchupload.html", data)
}
func (rc *shopItemController) AddItemsHandler(c *gin.Context) {
errorHandler := func(err error) {
data := CreateSessionData(c, gin.H{
"error": err,
})
c.HTML(http.StatusBadRequest, "batchupload.html", data)
}
form, err := c.MultipartForm()
if err != nil {
errorHandler(err)
return
}
files := form.File["pdf"]
var shopItems []models.ShopItem
for _, file := range files {
dstPdf := filepath.Join("static/uploads", file.Filename)
if err := c.SaveUploadedFile(file, dstPdf); err != nil {
errorHandler(err)
return
}
dstImage := dstPdf + ".preview.png"
cmd := exec.Command("pdftoppm", "-png", "-singlefile", dstPdf, dstPdf+".preview")
_, err := cmd.Output()
if err != nil {
fmt.Println("Error during pdftoppm: ", err.Error())
}
category, err := models.ParseCategory("Zine")
if err != nil {
errorHandler(err)
return
}
variants := []models.ItemVariant{
{
Name: "B/W",
Price: 1.0,
},
}
shopItem := models.ShopItem{
Name: file.Filename,
Abstract: file.Filename,
Description: file.Filename,
Category: category,
IsPublic: true,
BasePrice: rc.GetBasePrice(variants),
Image: dstImage,
Pdf: dstPdf,
Variants: variants,
PrintMode: "CreateBooklet",
}
_, err = repositories.ShopItems.Create(shopItem)
if err != nil {
errorHandler(err)
return
}
shopItems = append(shopItems, shopItem)
}
msg := "The Following items were registered:\n"
for _, item := range shopItems {
msg += fmt.Sprintf("%s\n", item.Name)
}
data := CreateSessionData(c, gin.H{
"error": "",
"success": msg,
})
c.HTML(http.StatusOK, "batchupload.html", data)
}
func (rc *shopItemController) EditItemView(c *gin.Context) {
shopItem, err := repositories.ShopItems.GetById(c.Param("id"))
tags, err := repositories.Tags.GetAll()
if err != nil {
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{ "error": err })
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{"error": err})
}
fmt.Println(shopItem)
data := CreateSessionData(c, gin.H{
"error": "",
"success": "",
"error": "",
"success": "",
"shopItem": shopItem,
"tags": tags,
"tags": tags,
})
c.HTML(http.StatusOK, "edititem.html", data)
}
func (rc *shopItemController) EditItemHandler(c *gin.Context) {
shopItem, err := rc.NewShopItemFromForm(c)
if err != nil {
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{ "error": err })
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{"error": err})
return
}
newShopItem, err := repositories.ShopItems.GetById(c.Param("id"))
if err != nil {
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{ "error": err })
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{"error": err})
return
}
newShopItem.Name = shopItem.Name
newShopItem.Abstract = shopItem.Abstract
newShopItem.Description = shopItem.Description
newShopItem.Price = shopItem.Price
newShopItem.Category = shopItem.Category
newShopItem.Variants = shopItem.Variants
newShopItem.BasePrice = shopItem.BasePrice
newShopItem.IsPublic = shopItem.IsPublic
newShopItem.Tags = shopItem.Tags
if len(shopItem.Tags) != 0 {
newShopItem.Tags = shopItem.Tags
}
if shopItem.Image != "static/img/zine.jpg" {
newShopItem.Image = shopItem.Image
}
if shopItem.Pdf != "" {
newShopItem.Pdf = shopItem.Pdf
}
newShopItem.PrintMode = shopItem.PrintMode
tags, err := repositories.Tags.GetAll()
if err != nil {
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{ "error": err })
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{"error": err})
return
}
_, err = repositories.ShopItems.Update(newShopItem)
if err != nil {
data := CreateSessionData(c, gin.H{
"error": err,
"error": err,
"success": "",
"tags": tags,
"tags": tags,
})
c.HTML(http.StatusOK, "edititem.html", data)
@@ -315,9 +490,9 @@ func (rc *shopItemController) EditItemHandler(c *gin.Context) {
}
data := CreateSessionData(c, gin.H{
"error": "",
"error": "",
"success": fmt.Sprintf("Item '%s' Updated", newShopItem.Name),
"tags": tags,
"tags": tags,
})
c.HTML(http.StatusOK, "edititem.html", data)
@@ -328,28 +503,27 @@ func (rc *shopItemController) DeleteItemView(c *gin.Context) {
tags, err := repositories.Tags.GetAll()
if err != nil {
c.HTML(http.StatusBadRequest, "deleteitem.html", gin.H{ "error": err })
c.HTML(http.StatusBadRequest, "deleteitem.html", gin.H{"error": err})
}
fmt.Println(shopItem)
data := CreateSessionData(c, gin.H{
"error": "",
"success": "",
"error": "",
"success": "",
"shopItem": shopItem,
"tags": tags,
"tags": tags,
})
c.HTML(http.StatusOK, "deleteitem.html", data)
}
func (rc *shopItemController) DeleteItemHandler(c *gin.Context) {
err := repositories.ShopItems.DeleteById(c.Param("id"))
if err != nil {
data := CreateSessionData(c, gin.H{
"error": err,
"error": err,
"success": "",
})
@@ -360,7 +534,7 @@ func (rc *shopItemController) DeleteItemHandler(c *gin.Context) {
fmt.Println(len(shopItems))
data := CreateSessionData(c, gin.H{
"title": "shopItem Page",
"title": "shopItem Page",
"shopItems": shopItems,
})
@@ -371,23 +545,25 @@ func (rc *shopItemController) DeleteItemHandler(c *gin.Context) {
func (rc *shopItemController) TagHandler(ctx *gin.Context) {
name := ctx.PostForm("name")
color := ctx.PostForm("color")
action := ctx.PostForm("action")
tag, err := repositories.Tags.GetById(ctx.Param("id"))
if err != nil {
fmt.Println(err)
ctx.HTML(http.StatusBadRequest, "tagview.html", gin.H{ "error": err })
ctx.HTML(http.StatusBadRequest, "tagview.html", gin.H{"error": err})
return
}
if action == "update" {
tag.Name = name
tag.Color = color
tag, err = repositories.Tags.Update(tag)
if err != nil {
fmt.Println(err)
ctx.HTML(http.StatusBadRequest, "tagview.html", gin.H{ "error": err })
ctx.HTML(http.StatusBadRequest, "tagview.html", gin.H{"error": err})
return
}
}
@@ -404,14 +580,14 @@ func (rc *shopItemController) AddTagHandler(c *gin.Context) {
if err != nil {
fmt.Println(err)
c.HTML(http.StatusBadRequest, "tagview.html", gin.H{ "error": err })
c.HTML(http.StatusBadRequest, "tagview.html", gin.H{"error": err})
return
}
_, err = repositories.Tags.Create(tag)
if err != nil {
data := CreateSessionData(c, gin.H{
"error": err,
"error": err,
"success": "",
})
@@ -426,7 +602,7 @@ func (rc *shopItemController) TagView(c *gin.Context) {
tags, err := repositories.Tags.GetAll()
if err != nil {
c.HTML(http.StatusBadRequest, "tagview.html", gin.H{ "data": gin.H{ "error": err } })
c.HTML(http.StatusBadRequest, "tagview.html", gin.H{"data": gin.H{"error": err}})
}
data := CreateSessionData(c, gin.H{
@@ -477,7 +653,7 @@ func (rc *shopItemController) GetAllTags(c *gin.Context) {
}
func ReplyError(ctx *gin.Context, err error) {
ctx.JSON(http.StatusBadRequest, gin.H{ "error": err.Error() })
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
func ReplyOK(ctx *gin.Context, message any) {

View File

@@ -1,30 +1,29 @@
package controllers
import(
import (
"errors"
"fmt"
"net/http"
"math/rand"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"example.com/gin/test/models"
"example.com/gin/test/repositories"
"example.com/gin/test/services"
)
"git.dynamicdiscord.de/kalipso/zineshop/models"
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
"git.dynamicdiscord.de/kalipso/zineshop/services"
)
type UserController struct {}
type UserController struct{}
func NewUserController() UserController {
return UserController{}
}
func (uc *UserController) Register(c *gin.Context) {
//Get the email/passwd off req body
var body struct {
Name string
Email string
Name string
Email string
Password string
}
@@ -38,7 +37,7 @@ func (uc *UserController) Register(c *gin.Context) {
return
}
_, err = services.Users.Register(body.Name, body.Email, body.Password)
_, err = services.Users.Register(body.Name, body.Email, body.Password, false)
if err != nil {
fmt.Println("Error: ", err)
@@ -53,11 +52,10 @@ func (uc *UserController) Register(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{})
}
func (uc *UserController) Login(c *gin.Context) {
//Get the email/passwd off req body
var body struct {
Email string
Email string
Password string
}
@@ -83,7 +81,7 @@ func (uc *UserController) Login(c *gin.Context) {
// send it back
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie("Authorization", tokenString, 3600 * 24, "", "", false, true)
c.SetCookie("Authorization", tokenString, 3600*24, "", "", false, true)
c.JSON(http.StatusOK, gin.H{})
}
@@ -116,7 +114,6 @@ func (rc *UserController) LoginView(c *gin.Context) {
c.HTML(http.StatusOK, "login.html", CreateSessionData(c, data))
}
func (rc *UserController) LoginHandler(c *gin.Context) {
email := c.PostForm("email")
password := c.PostForm("password")
@@ -139,17 +136,18 @@ func (rc *UserController) LoginHandler(c *gin.Context) {
// send it back
//c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie("Authorization", tokenString, 3600 * 24, "", "", false, true)
c.SetCookie("Authorization", tokenString, 3600*24, "", "", false, true)
c.HTML(http.StatusOK, "login.html", CreateSessionData(c, gin.H{}))
}
func CreateSessionData(c *gin.Context, extra any) gin.H {
_, exists := c.Get("user")
user, exists := c.Get("user")
userImpl, _ := user.(models.User)
return gin.H{
"test": "HEllo World",
"loggedIn": exists,
"data": extra,
"isAdmin": userImpl.IsAdmin,
"data": extra,
}
}
@@ -158,11 +156,45 @@ func (rc *UserController) RegisterHandler(c *gin.Context) {
email := c.PostForm("email")
password := c.PostForm("password")
_, err := services.Users.Register(name, email, password)
//first registered user is admin
isEmpty, _ := repositories.Users.IsEmpty()
if isEmpty {
_, err := services.Users.Register(name, email, password, true)
if err != nil {
data := gin.H{
"error": "Registering Failed.",
"success": "",
}
c.HTML(http.StatusOK, "register.html", data)
return
}
if err != nil {
data := gin.H{
"error": "Registering Failed.",
"error": "",
"success": "You successfully registered as Admin. Try logging in.",
}
c.HTML(http.StatusOK, "register.html", data)
return
}
//for any other user token is required
token := c.PostForm("token")
if token == "" {
data := gin.H{
"error": "No token. No register.",
"success": "",
}
c.HTML(http.StatusOK, "register.html", data)
}
tokenExists, err := repositories.Tokens.Exists(token)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
data := gin.H{
"error": err,
"success": "",
}
@@ -170,8 +202,33 @@ func (rc *UserController) RegisterHandler(c *gin.Context) {
return
}
if !tokenExists {
data := gin.H{
"error": "Invalid Token.",
"success": "",
}
c.HTML(http.StatusOK, "register.html", data)
return
}
_, err = services.Users.Register(name, email, password, false)
if err != nil {
data := gin.H{
"error": "Registering Failed.",
"success": "",
}
c.HTML(http.StatusOK, "register.html", data)
return
}
err = repositories.Tokens.Delete(token)
if err != nil {
fmt.Println("Could not delete RegisterToken: ", err)
}
data := gin.H{
"error": "",
"error": "",
"success": "You successfully registered. Try logging in.",
}
@@ -180,7 +237,40 @@ func (rc *UserController) RegisterHandler(c *gin.Context) {
func (rc *UserController) RegisterView(c *gin.Context) {
data := gin.H{
"error": "",
"error": "",
"success": "",
"token": c.Param("token"),
}
c.HTML(http.StatusOK, "registertoken.html", data)
}
func (rc *UserController) InitAdmin(c *gin.Context) {
isEmpty, err := repositories.Users.IsEmpty()
if err != nil {
data := gin.H{
"error": err,
"success": "",
}
c.HTML(http.StatusInternalServerError, "error.html", data)
return
}
if !isEmpty {
data := gin.H{
"error": "Registration is closed",
"success": "",
}
c.HTML(http.StatusInternalServerError, "error.html", data)
return
}
data := gin.H{
"error": "",
"success": "",
}
@@ -191,7 +281,7 @@ func (rc *UserController) ResetView(c *gin.Context) {
shopItems, _ := repositories.ShopItems.GetAll()
data := gin.H{
"title": "shopItem Page",
"title": "shopItem Page",
"shopItems": shopItems,
}
@@ -202,88 +292,66 @@ func (rc *UserController) ResetHandler(c *gin.Context) {
shopItems, _ := repositories.ShopItems.GetAll()
data := gin.H{
"title": "shopItem Page",
"title": "shopItem Page",
"shopItems": shopItems,
}
c.HTML(http.StatusOK, "passwordreset.html", data)
}
func (rc *UserController) InviteView(c *gin.Context) {
tokens, _ := repositories.Tokens.GetAll()
fmt.Println(tokens)
data := gin.H{
"tokens": tokens,
}
c.HTML(http.StatusOK, "invites.html", data)
}
func (rc *UserController) InviteHandler(c *gin.Context) {
action := c.PostForm("action")
if action == "create" {
_, err := repositories.Tokens.Create()
if err != nil {
fmt.Println(err)
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return
}
}
if action == "delete" {
token := c.PostForm("token")
repositories.Tokens.Delete(token)
}
rc.InviteView(c)
}
func (rc *UserController) MainView(c *gin.Context) {
shopItems, _ := repositories.ShopItems.GetAll()
fmt.Println(len(shopItems))
data := CreateSessionData(c, gin.H{
"title": "shopItem Page",
"title": "shopItem Page",
"shopItems": shopItems,
})
fmt.Println(data)
c.HTML(http.StatusOK, "index.html", data)
}
type booking struct {
Booked bool
}
type calendarbooking struct{
Time string
Bookings []booking
}
func (rc *UserController) CalendarView(c *gin.Context) {
shopItems, _ := repositories.ShopItems.GetAll()
fmt.Println(len(shopItems))
generateBookings := func(amountShopItems int) []calendarbooking {
var result []calendarbooking
time := 6;
for _ = range 18 {
book := calendarbooking{
Time: fmt.Sprintf("%d:00", time),
Bookings: []booking{},
}
for _ = range amountShopItems {
book.Bookings = append(book.Bookings, booking{ Booked: rand.Float32() < 0.5 })
}
time += 1
result = append(result, book)
}
return result
}
bookings := gin.H{
"head": []string{
"malobeo",
"hole of fame",
"BK",
"AZ Conni",
},
"bookings": generateBookings(4),
//"bookings": []calendarbooking{
// {
// Time: "10:00",
// Bookings: []booking{
// { Booked: true },
// { Booked: false },
// },
// },
//},
}
func (rc *UserController) TagView(c *gin.Context) {
shopItems, _ := repositories.ShopItems.GetByTagId(c.Param("id"))
data := CreateSessionData(c, gin.H{
"title": "shopItem Page",
"bookings": bookings,
"shopItemcount": len(bookings["head"].([]string)) + 1,
"title": "shopItem Page",
"shopItems": shopItems,
})
fmt.Println(data)
c.HTML(http.StatusOK, "calendar.html", data)
c.HTML(http.StatusOK, "index.html", data)
}
func (rc *UserController) Logout(c *gin.Context) {

61
flake.lock generated Normal file
View File

@@ -0,0 +1,61 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1744463964,
"narHash": "sha256-LWqduOgLHCFxiTNYi3Uj5Lgz0SR+Xhw3kr/3Xd0GPTM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "2631b0b7abcea6e640ce31cd78ea58910d31e650",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"utils": "utils"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

135
flake.nix Normal file
View File

@@ -0,0 +1,135 @@
{
description = "A very basic flake";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
};
inputs.utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, utils, ... }:
nixpkgs.lib.attrsets.recursiveUpdate
(utils.lib.eachSystem (utils.lib.defaultSystems) ( system:
let
pkgs = nixpkgs.legacyPackages.${system};
in rec
{
devShells.default = pkgs.mkShell {
packages = with pkgs; [
go
gotools
poppler_utils #get first pdf page to png
tailwindcss
];
};
packages.zineshop = nixpkgs.legacyPackages.x86_64-linux.buildGoModule {
pname = "zineshop";
version = "1.0";
vendorHash = "sha256-0M/xblZXVw4xIFZeDewYrFu7VGUCsPTPG13r9ZpTGJo=";
src = ./.;
postInstall = ''
cp -r views $out/
cp -r static $out/
'';
};
packages.default = packages.zineshop;
checks = let
checkArgs = {
pkgs = pkgs;
inherit self;
};
in {
zineshop = import ./test/test.nix checkArgs;
};
})) {
nixosModules.zineshop = { config, lib, pkgs, ... }:
let
cfg = config.services.zineshop;
zineshop-pkg = self.packages.x86_64-linux.zineshop;
in
{
options = {
services.zineshop = {
enable = lib.mkOption {
default = false;
type = lib.types.bool;
description = lib.mdDoc ''
Enables zineshop
'';
};
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [
zineshop-pkg
pkgs.poppler_utils #get first pdf page to png
];
systemd.tmpfiles.rules = [
"d '/var/lib/zineshop' 0750 zineshop zineshop - -"
"d '/var/lib/zineshop/views' 0750 zineshop zineshop - -"
"d '/var/lib/zineshop/static' 0750 zineshop zineshop - -"
"d '/var/lib/zineshop/static/uploads' 0750 zineshop zineshop - -"
"z '/var/lib/zineshop' 0750 zineshop zineshop - -"
"z '/var/lib/zineshop/views' 0750 zineshop zineshop - -"
"z '/var/lib/zineshop/static' 0750 zineshop zineshop - -"
"z '/var/lib/zineshop/static/uploads' 0750 zineshop zineshop - -"
];
users = {
groups.zineshop = {};
users.zineshop = {
description = "zineshop user";
group = "zineshop";
isNormalUser = true;
};
};
systemd.services.zineshop = {
description = "zineshop daemon";
serviceConfig = {
Type = "simple";
WorkingDirectory = "/var/lib/zineshop";
ExecStart = pkgs.writeScript "start-zineshop" ''
#! ${pkgs.bash}/bin/bash
PATH="$PATH:${lib.makeBinPath [ pkgs.poppler_utils ]}"
${zineshop-pkg}/bin/zineshop
'';
Restart = "on-failure";
};
environment = {
SQLITE_DB = "/var/lib/zineshop/zineshop.db";
SECRET = "secretforjwt"; #TODO: BAD!
PORT = "8080";
STATIC = "/var/lib/zineshop/static";
VIEWS = "/var/lib/zineshop/views";
};
preStart = ''
mkdir -m 0770 -p "/var/lib/zineshop"
cp -r ${zineshop-pkg}/views /var/lib/zineshop/
cp -r ${zineshop-pkg}/static /var/lib/zineshop/
chown zineshop:zineshop "/var/lib/zineshop"
'';
wantedBy = [ "default.target" ];
environment = {
USER = "zineshop";
HOME = "/var/lib/zineshop";
};
};
};
};
};
}

2
go.mod
View File

@@ -1,4 +1,4 @@
module example.com/gin/test
module git.dynamicdiscord.de/kalipso/zineshop
go 1.23.3

96
main.go
View File

@@ -1,23 +1,25 @@
package main
import(
"os"
import (
"fmt"
"io"
"net/http"
"fmt"
"os"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"example.com/gin/test/controllers"
"example.com/gin/test/repositories"
"example.com/gin/test/middlewares"
"git.dynamicdiscord.de/kalipso/zineshop/controllers"
"git.dynamicdiscord.de/kalipso/zineshop/middlewares"
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
)
var(
var (
shopItemController controllers.ShopItemController = controllers.NewShopItemController()
userController controllers.UserController = controllers.UserController{}
authValidator middlewares.AuthValidator = middlewares.AuthValidator{}
userController controllers.UserController = controllers.UserController{}
cartItemController controllers.CartItemController = controllers.NewCartItemController()
printController controllers.PrintController = controllers.NewPrintController()
authValidator middlewares.AuthValidator = middlewares.AuthValidator{}
)
func LoadEnvVariables() {
@@ -35,7 +37,7 @@ func setupLogOutput() {
func SetReply(ctx *gin.Context, err error, message any) {
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{ "error": err.Error() })
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
} else {
ctx.JSON(http.StatusOK, message)
}
@@ -46,56 +48,58 @@ func main() {
repositories.InitRepositories()
server := gin.New()
server.Use(gin.Recovery())
server.Use(gin.Logger())
server.Static("/static", "./static")
server.LoadHTMLGlob("views/*.html")
apiRoutes := server.Group("/api")
//apiRoutes.Use(middlewares.BasicAuth())
{
apiRoutes.POST("/tags", authValidator.RequireAuth, shopItemController.CreateTag)
apiRoutes.GET("/tags", authValidator.OptionalAuth, shopItemController.GetAllTags)
apiRoutes.POST("/shopitems", authValidator.RequireAuth, shopItemController.Create)
apiRoutes.GET("/shopitems", authValidator.OptionalAuth, shopItemController.GetAll)
apiRoutes.GET("/shopitems/:id", authValidator.OptionalAuth, shopItemController.GetById)
apiRoutes.PUT("/shopitems/:id", authValidator.RequireAuth, shopItemController.Update)
apiRoutes.DELETE("/shopitems/:id", authValidator.RequireAuth, shopItemController.Delete)
//apiRoutes.GET("/rooms/:id/users", authValidator.RequireAuth, authValidator.RequireRoomAdmin, shopItemController.GetUsers)
//apiRoutes.POST("/rooms/:id/users", authValidator.RequireAuth, shopItemController.AddUser)
apiRoutes.POST("/users/register", userController.Register)
apiRoutes.POST("/users/login", userController.Login)
apiRoutes.GET("/users/validate", authValidator.OptionalAuth, userController.Validate)
}
server.Static("/static", os.Getenv("STATIC"))
server.LoadHTMLGlob(fmt.Sprintf("%s/*.html", os.Getenv("VIEWS")))
viewRoutes := server.Group("/", authValidator.OptionalAuth)
{
viewRoutes.GET("/", userController.MainView)
viewRoutes.GET("/shopitems/:id", shopItemController.ShopItemView)
viewRoutes.GET("/shopitems/:id/edit", authValidator.RequireAuth, shopItemController.EditItemView)
viewRoutes.POST("/shopitems/:id/edit", authValidator.RequireAuth, shopItemController.EditItemHandler)
viewRoutes.GET("/shopitems/:id/delete", authValidator.RequireAuth, shopItemController.DeleteItemView)
viewRoutes.POST("/shopitems/:id/delete", authValidator.RequireAuth, shopItemController.DeleteItemHandler)
viewRoutes.GET("/tags", authValidator.RequireAuth, shopItemController.TagView)
viewRoutes.POST("/tags/:id", authValidator.RequireAuth, shopItemController.TagHandler)
viewRoutes.POST("/tags", authValidator.RequireAuth, shopItemController.AddTagHandler)
viewRoutes.GET("/shopitems/:id/edit", authValidator.RequireAdmin, shopItemController.EditItemView)
viewRoutes.POST("/shopitems/:id/edit", authValidator.RequireAdmin, shopItemController.EditItemHandler)
viewRoutes.GET("/shopitems/:id/delete", authValidator.RequireAdmin, shopItemController.DeleteItemView)
viewRoutes.POST("/shopitems/:id/delete", authValidator.RequireAdmin, shopItemController.DeleteItemHandler)
viewRoutes.GET("/variant/:id/print", authValidator.RequireAdmin, printController.PrintVariantView)
viewRoutes.GET("/cart/print", authValidator.RequireAdmin, printController.PrintCartView)
viewRoutes.POST("/print", authValidator.RequireAdmin, printController.PrintHandler)
viewRoutes.GET("/tags", authValidator.RequireAdmin, shopItemController.TagView)
viewRoutes.POST("/tags/:id", authValidator.RequireAdmin, shopItemController.TagHandler)
viewRoutes.GET("/tags/:id", userController.TagView)
viewRoutes.POST("/tags", authValidator.RequireAdmin, shopItemController.AddTagHandler)
viewRoutes.GET("/cart", authValidator.RequireAuth, cartItemController.CartItemView)
viewRoutes.POST("/cart", authValidator.RequireAuth, cartItemController.AddItemHandler)
viewRoutes.POST("/cart/delete", authValidator.RequireAuth, cartItemController.DeleteItemHandler)
viewRoutes.POST("/cart/edit", authValidator.RequireAuth, cartItemController.EditItemHandler)
viewRoutes.GET("/checkout", authValidator.RequireAuth, cartItemController.CheckoutView)
viewRoutes.POST("/checkout", authValidator.RequireAuth, cartItemController.CheckoutHandler)
viewRoutes.POST("/order", authValidator.RequireAuth, cartItemController.OrderHandler)
viewRoutes.GET("/order/:token", authValidator.RequireAuth, cartItemController.OrderView)
viewRoutes.GET("/order/:token/print", authValidator.RequireAuth, printController.PrintOrderView)
viewRoutes.GET("/orders", authValidator.RequireAdmin, cartItemController.OrdersView)
viewRoutes.POST("/order/:token/edit", authValidator.RequireAdmin, cartItemController.OrdersHandler)
//write middleware that redirects to homescreen on register/login/reset for logged in users
viewRoutes.GET("/login", userController.LoginView)
viewRoutes.GET("/logout", userController.Logout)
viewRoutes.GET("/register", userController.RegisterView)
viewRoutes.GET("/passwordreset", userController.ResetView)
viewRoutes.GET("/additem", authValidator.RequireAuth, shopItemController.AddItemView)
viewRoutes.GET("/register", userController.InitAdmin)
viewRoutes.GET("/register/:token", userController.RegisterView)
viewRoutes.GET("/invites", authValidator.RequireAdmin, userController.InviteView)
viewRoutes.POST("/invites", authValidator.RequireAdmin, userController.InviteHandler)
viewRoutes.GET("/passwordreset", authValidator.RequireAuth, userController.ResetView)
viewRoutes.GET("/additem", authValidator.RequireAdmin, shopItemController.AddItemView)
viewRoutes.GET("/batchupload", authValidator.RequireAdmin, shopItemController.AddItemsView)
viewRoutes.POST("/login", userController.LoginHandler)
viewRoutes.POST("/register", userController.RegisterHandler)
viewRoutes.POST("/additem", authValidator.RequireAuth, shopItemController.AddItemHandler)
viewRoutes.POST("/passwordreset", userController.ResetHandler)
viewRoutes.POST("/additem", authValidator.RequireAdmin, shopItemController.AddItemHandler)
viewRoutes.POST("/batchupload", authValidator.RequireAdmin, shopItemController.AddItemsHandler)
viewRoutes.POST("/passwordreset", authValidator.RequireAuth, userController.ResetHandler)
}
server.Run(":"+os.Getenv("PORT"))
server.Run(":" + os.Getenv("PORT"))
}

View File

@@ -1,16 +1,16 @@
package middlewares
import(
"os"
import (
"fmt"
"os"
"time"
//"strconv"
"net/http"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"net/http"
//"example.com/gin/test/models"
"example.com/gin/test/repositories"
//"git.dynamicdiscord.de/kalipso/zineshop/models"
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
)
type AuthValidator struct {
@@ -70,7 +70,7 @@ func (av *AuthValidator) RequireAuth(c *gin.Context) {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
//Check Expiration
if float64(time.Now().Unix()) > claims["exp"].(float64) {
@@ -78,7 +78,7 @@ func (av *AuthValidator) RequireAuth(c *gin.Context) {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
//Find user
user, err := repositories.Users.GetById(claims["sub"])
@@ -86,15 +86,72 @@ func (av *AuthValidator) RequireAuth(c *gin.Context) {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
//Attach to req
c.Set("user", user)
// Coninue
c.Next()
return
}
c.AbortWithStatus(http.StatusUnauthorized)
}
func (av *AuthValidator) RequireAdmin(c *gin.Context) {
// Get Cookie
tokenString, err := c.Cookie("Authorization")
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
//Validate
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
// hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
return []byte(os.Getenv("SECRET")), nil
})
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
//Check Expiration
if float64(time.Now().Unix()) > claims["exp"].(float64) {
//expired
c.AbortWithStatus(http.StatusUnauthorized)
return
}
//Find user
user, err := repositories.Users.GetById(claims["sub"])
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
if !user.IsAdmin {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
//Attach to req
c.Set("user", user)
// Coninue
c.Next()
return
}
c.AbortWithStatus(http.StatusUnauthorized)
}
@@ -119,19 +176,19 @@ func (av *AuthValidator) OptionalAuth(c *gin.Context) {
if err != nil {
return
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
if float64(time.Now().Unix()) > claims["exp"].(float64) {
return
}
//Find user
user, err := repositories.Users.GetById(claims["sub"])
if err != nil {
return
}
//Attach to req
c.Set("user", user)
}

161
models/cart.go Normal file
View File

@@ -0,0 +1,161 @@
package models
import (
"fmt"
"gorm.io/gorm"
)
type OrderStatus string
const (
AwaitingConfirmation OrderStatus = "AwaitingConfirmation"
Received OrderStatus = "Received"
AwaitingPayment OrderStatus = "AwaitingPayment"
Payed OrderStatus = "Payed"
ReadyForPickup OrderStatus = "ReadyForPickup"
Shipped OrderStatus = "Shipped"
Cancelled OrderStatus = "Cancelled"
)
type AddressInfo struct {
FirstName string `json:"firstname"`
LastName string `json:"lastname"`
Address string `json:"address"`
PostalCode string `json:"postalcode"`
City string `json:"city"`
Country string `json:"country"`
}
type Shipping struct {
Id string `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}
func GetShippingMethods() []Shipping {
return []Shipping{
{Id: "germany", Name: "Germany (DHL)", Price: 3.99},
{Id: "international", Name: "International (DHL)", Price: 5.99},
{Id: "pickup", Name: "Pickup", Price: 0.00},
}
}
func GetShippingMethod(id string) (Shipping, error) {
var shipping Shipping
found := false
for _, shippingMethod := range GetShippingMethods() {
if shippingMethod.Id == id {
shipping = shippingMethod
found = true
}
}
if !found {
return Shipping{}, fmt.Errorf("Shipping method does not exist.")
}
return shipping, nil
}
type Order struct {
gorm.Model
SessionId string `json:"sessionid" binding:"required" gorm:"not null"`
Status OrderStatus `json:"status"`
Token string `json:"token" binding:"required" gorm:"not null"`
Email string `json:"email"`
Comment string `json:"comment"`
FirstName string `json:"firstname"`
LastName string `json:"lastname"`
Address string `json:"address"`
PostalCode string `json:"postalcode"`
City string `json:"city"`
Country string `json:"country"`
Shipping string `json:"shipping"`
CartItems []CartItem `json:"cartitems" gorm:"foreignKey:OrderID"`
}
func (o *Order) Validate() error {
//TODO: validate sessionId
if o.SessionId == "" {
return fmt.Errorf("Invalid SessionId")
}
//TODO: validate token
if o.Token == "" {
return fmt.Errorf("Invalid Token")
}
if o.Status == AwaitingConfirmation {
return fmt.Errorf("Order still awaiting confirmation.")
}
if len(o.CartItems) == 0 {
return fmt.Errorf("Order is empty.")
}
shipping, err := GetShippingMethod(o.Shipping)
if err != nil {
return err
}
//for pickup no address validation is necessary
if shipping.Id == "pickup" {
return nil
}
return o.ValidateAddress()
}
func (o *Order) ValidateAddress() error {
if o.FirstName == "" {
return fmt.Errorf("Firstname missing")
}
if o.LastName == "" {
return fmt.Errorf("Lastname missing")
}
if o.Address == "" {
return fmt.Errorf("Address missing")
}
if o.PostalCode == "" {
return fmt.Errorf("Postalcode missing")
}
if o.City == "" {
return fmt.Errorf("City missing")
}
if o.Country == "" {
return fmt.Errorf("Country missing")
}
return nil
}
func (o *Order) CalculatePrices() (float64, float64, error) {
shipping, err := GetShippingMethod(o.Shipping)
if err != nil {
return 0, 0, err
}
priceProducts := 0.0
for _, cartItem := range o.CartItems {
priceProducts += (float64(cartItem.Quantity) * cartItem.ItemVariant.Price)
}
priceTotal := priceProducts + shipping.Price
return priceProducts, priceTotal, nil
}
type CartItem struct {
gorm.Model
SessionId string `json:"sessionid" binding:"required" gorm:"not null"`
ShopItemId uint
ShopItem ShopItem `json:"shopitem" gorm:"foreignKey:ShopItemId"` //gorm one2one
ItemVariantId uint
ItemVariant ItemVariant `json:"itemvariant" gorm:"foreignKey:ItemVariantId"` //gorm one2one
Quantity int `json:"quantity" binding:"required"`
OrderID uint
}

88
models/printer.go Normal file
View File

@@ -0,0 +1,88 @@
package models
import (
"fmt"
"os/exec"
"strings"
)
type PrintOption string
const (
CoverPage PrintOption = "-o FrontCoverPage=Printed -o FrontCoverTray=BypassTray"
Colored PrintOption = "-o SelectColor=Color"
Grayscale PrintOption = "-o SelectColor=Grayscale"
LongEdge PrintOption = ""
ShortEdge PrintOption = "-o Binding=TopBinding"
CreateBooklet PrintOption = "-o Combination=Booklet -o PageSize=A5"
)
type PrintJob struct {
Pdf string
Amount uint
Options []PrintOption
}
func GetPrintMode(mode string) PrintOption {
if mode == "LongEdge" {
return LongEdge
}
if mode == "ShortEdge" {
return ShortEdge
}
return CreateBooklet
}
func NewPrintJob(shopItem ShopItem, variant ItemVariant, coverPage bool, amount uint) (PrintJob, error) {
if shopItem.Pdf == "" {
return PrintJob{}, fmt.Errorf("ShopItem has no PDF assigned")
}
if amount > 100 {
return PrintJob{}, fmt.Errorf("Amount to big. This is denied for security reasons")
}
var result PrintJob
result.Pdf = shopItem.Pdf
result.Amount = amount
if variant.Name == "Colored" {
result.Options = append(result.Options, Colored)
}
if coverPage {
result.Options = append(result.Options, CoverPage)
}
result.Options = append(result.Options, GetPrintMode(shopItem.PrintMode))
return result, nil
}
func (p *PrintJob) Execute() error {
baseCommand := "lp -d KonicaBooklet"
baseCommand += fmt.Sprintf(" -n %v ", p.Amount)
for _, option := range p.Options {
baseCommand += fmt.Sprintf(" %v ", option)
}
baseCommand += fmt.Sprintf(" -- %s", p.Pdf)
parts := strings.Fields(baseCommand)
// The first part is the command, the rest are the arguments
fmt.Println(parts)
cmd := exec.Command(parts[0], parts[1:]...)
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("Error: %s\n", err)
return err
}
fmt.Printf("Output:\n%s\n", output)
return nil
}

View File

@@ -1,17 +1,56 @@
package models
import (
"fmt"
"gorm.io/gorm"
)
type ShopItem struct {
gorm.Model
Name string `json:"name" binding:"required" gorm:"unique;not null"`
Abstract string `json:"Abstract" binding:"required"`
Description string `json:"description" binding:"required"`
Price float64 `json:"price" binding:"required"`
IsPublic bool `json:"isPublic" gorm:"default:true"`
Tags []Tag `gorm:"many2many:item_tags;"`
Image string
/*
Sticker
- name, abstr, descr, price, tag
Poster
- name, abstr, descr, price bw/colored, tag
Zines
- name, abstr, descr, price bw/colored/coloredcoveronly, tag
Books
- name, abstr, descr, price, tag
*/
type Category string
const (
Zine Category = "Zine"
)
func ParseCategory(s string) (c Category, err error) {
if s == "Zine" {
return Zine, nil
}
return c, fmt.Errorf("Cannot parse category %s", s)
}
type ItemVariant struct {
gorm.Model
Name string `json:"name" gorm:"not null"`
Price float64 `json:"price" gorm:"not null"`
InStock bool `json:"inStock" gorm:"default:true"`
ShopItemID uint
}
type ShopItem struct {
gorm.Model
Name string `json:"name" binding:"required" gorm:"unique;not null"`
Abstract string `json:"Abstract" binding:"required"`
Description string `json:"description" binding:"required"`
Category Category `json:"category"`
Variants []ItemVariant `json:"variant"`
BasePrice float64 `json:"basePrice"`
IsPublic bool `json:"isPublic" gorm:"default:true"`
Tags []Tag `gorm:"many2many:item_tags;"`
Image string
Pdf string
PrintMode string `json:"printMode" gorm:"default:CreateBooklet"`
}

View File

@@ -2,22 +2,51 @@ package models
import (
"fmt"
"gorm.io/gorm"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"math/rand"
)
type Tag struct {
gorm.Model
Name string `json:"name" binding:"required" gorm:"not null"`
Name string `json:"name" binding:"required" gorm:"not null"`
Color string `json:"color" binding:"required" gorm:"default:pink"`
ShopItems []ShopItem `gorm:"many2many:item_tags;"`
}
func NewTag(ctx *gin.Context) (Tag, error) {
colors := []string{
"red",
"orange",
"amber",
"yellow",
"lime",
"green",
"emerald",
"teal",
"cyan",
"sky",
"blue",
"indigo",
"violet",
"purple",
"fuchsia",
"pink",
"rose",
"slate",
"gray",
"zinc",
"neutral",
"stone",
}
n := rand.Int() % len(colors)
name := ctx.PostForm("name")
// Convert the price string to float64
// Convert the price string to float64
tag := Tag{
Name: name,
Name: name,
Color: colors[n],
}
if name == "" {
@@ -27,14 +56,13 @@ func NewTag(ctx *gin.Context) (Tag, error) {
return tag, nil
}
func NewTagByJson(ctx *gin.Context) (Tag, error) {
var tag Tag
err := ctx.ShouldBindJSON(&tag)
if err != nil {
return Tag{}, err
}
return tag, nil
}

View File

@@ -4,9 +4,15 @@ import (
"gorm.io/gorm"
)
type RegisterToken struct {
gorm.Model
Token string `json:"token" binding:"required" gorm:"unique;not null"`
}
type User struct {
gorm.Model
Name string `json:"name" binding:"required" gorm:"unique;not null"`
Name string `json:"name" binding:"required" gorm:"unique;not null"`
Password string `json:"password" binding:"required" gorm:"not null"`
Email string `json:"email" binding:"required,email" gorm:"unique;not null"`
Email string `json:"email" binding:"required,email" gorm:"unique;not null"`
IsAdmin bool `json:"isAdmin" gorm:"default:false;not null"`
}

View File

@@ -0,0 +1,103 @@
package repositories
import (
"gorm.io/gorm"
"strconv"
"git.dynamicdiscord.de/kalipso/zineshop/models"
)
type OrderRepository interface {
Create(models.Order) (models.Order, error)
GetAll() ([]models.Order, error)
GetById(string) (models.Order, error)
GetBySession(string) (models.Order, error)
GetByToken(string) (models.Order, error)
Update(models.Order) (models.Order, error)
DeleteById(string) error
DeleteByToken(string) error
}
type GORMOrderRepository struct {
DB *gorm.DB
}
func NewGORMOrderRepository(db *gorm.DB) OrderRepository {
return &GORMOrderRepository{
DB: db,
}
}
func (r *GORMOrderRepository) Create(order models.Order) (models.Order, error) {
//Omit the shopitem so it is not created again in db leading to unique constain fails
result := r.DB.Omit("CartItems").Create(&order)
if result.Error != nil {
return models.Order{}, result.Error
}
return order, nil
}
func (r *GORMOrderRepository) GetAll() ([]models.Order, error) {
var orders []models.Order
result := r.DB.Preload("CartItems").Find(&orders)
return orders, result.Error
}
func (t *GORMOrderRepository) GetById(id string) (models.Order, error) {
orderId, err := strconv.Atoi(id)
if err != nil {
return models.Order{}, err
}
var order models.Order
result := t.DB.Preload("CartItems").First(&order, uint(orderId))
if result.Error != nil {
return models.Order{}, result.Error
}
return order, nil
}
func (r *GORMOrderRepository) GetBySession(sessionId string) (models.Order, error) {
var orders models.Order
result := r.DB.Preload("CartItems").Preload("CartItems.ShopItem").Preload("CartItems.ItemVariant").Where("session_id = ?", sessionId).First(&orders)
return orders, result.Error
}
func (r *GORMOrderRepository) GetByToken(token string) (models.Order, error) {
var orders models.Order
result := r.DB.Preload("CartItems").Preload("CartItems.ShopItem").Preload("CartItems.ItemVariant").Where("token = ?", token).First(&orders)
return orders, result.Error
}
func (r *GORMOrderRepository) Update(order models.Order) (models.Order, error) {
result := r.DB.Save(&order)
if result.Error != nil {
return models.Order{}, result.Error
}
return order, nil
}
func (r *GORMOrderRepository) DeleteById(id string) error {
orderId, err := strconv.Atoi(id)
if err != nil {
return err
}
result := r.DB.Delete(&models.Order{}, orderId)
return result.Error
}
func (r *GORMOrderRepository) DeleteByToken(token string) error {
result := r.DB.Where("token = ?", token).Delete(&models.Order{})
return result.Error
}

View File

@@ -0,0 +1,90 @@
package repositories
import(
"strconv"
"gorm.io/gorm"
"git.dynamicdiscord.de/kalipso/zineshop/models"
)
type CartItemRepository interface {
Create(models.CartItem) (models.CartItem, error)
GetAll() ([]models.CartItem, error)
GetById(string) (models.CartItem, error)
GetAllBySession(string) ([]models.CartItem, error)
Update(models.CartItem) (models.CartItem, error)
DeleteById(string) (error)
}
type GORMCartItemRepository struct {
DB *gorm.DB
}
func NewGORMCartItemRepository(db *gorm.DB) CartItemRepository {
return &GORMCartItemRepository{
DB: db,
}
}
func (r *GORMCartItemRepository) Create(cartItem models.CartItem) (models.CartItem, error) {
//Omit the shopitem so it is not created again in db leading to unique constain fails
result := r.DB.Omit("ShopItem").Create(&cartItem)
if result.Error != nil {
return models.CartItem{}, result.Error
}
return cartItem, nil
}
func (r *GORMCartItemRepository) GetAll() ([]models.CartItem, error) {
var cartItems []models.CartItem
result := r.DB.Preload("ShopItem").Preload("ItemVariant").Find(&cartItems)
return cartItems, result.Error
}
func (t *GORMCartItemRepository) GetById(id string) (models.CartItem, error) {
cartItemId, err := strconv.Atoi(id)
if err != nil {
return models.CartItem{}, err
}
var cartItem models.CartItem
result := t.DB.Preload("ShopItem").Preload("ItemVariant").First(&cartItem, uint(cartItemId))
if result.Error != nil {
return models.CartItem{}, result.Error
}
return cartItem, nil
}
func (r *GORMCartItemRepository) GetAllBySession(sessionId string) ([]models.CartItem, error) {
var cartItems []models.CartItem
result := r.DB.Preload("ShopItem").Preload("ItemVariant").Where("session_id = ?", sessionId).Find(&cartItems)
return cartItems, result.Error
}
func (r *GORMCartItemRepository) Update(cartItem models.CartItem) (models.CartItem, error) {
result := r.DB.Save(&cartItem)
if result.Error != nil {
return models.CartItem{}, result.Error
}
return cartItem, nil
}
func (r *GORMCartItemRepository) DeleteById(id string) error {
cartItemId, err := strconv.Atoi(id)
if err != nil {
return err
}
result := r.DB.Omit("ShopItem").Omit("ItemVariant").Delete(&models.CartItem{}, cartItemId)
return result.Error
}

View File

@@ -0,0 +1,86 @@
package repositories
import (
"errors"
"fmt"
"gorm.io/gorm"
"git.dynamicdiscord.de/kalipso/zineshop/models"
"git.dynamicdiscord.de/kalipso/zineshop/utils"
)
type RegisterTokenRepository interface {
Create() (models.RegisterToken, error)
GetAll() ([]models.RegisterToken, error)
Exists(string) (bool, error)
Delete(string) error
}
type GORMRegisterTokenRepository struct {
DB *gorm.DB
}
func NewGORMRegisterTokenRepository(db *gorm.DB) RegisterTokenRepository {
return &GORMRegisterTokenRepository{
DB: db,
}
}
func (t *GORMRegisterTokenRepository) Create() (models.RegisterToken, error) {
token := utils.GenerateToken()
exists, err := t.Exists(token)
if err != nil {
return models.RegisterToken{}, err
}
if exists {
return t.Create()
}
newToken := models.RegisterToken{
Token: token,
}
result := t.DB.Create(&newToken)
if result.Error != nil {
return models.RegisterToken{}, result.Error
}
return newToken, nil
}
func (t *GORMRegisterTokenRepository) GetAll() ([]models.RegisterToken, error) {
var tokens []models.RegisterToken
result := t.DB.Find(&tokens)
return tokens, result.Error
}
func (t *GORMRegisterTokenRepository) Exists(tokenString string) (bool, error) {
var token models.RegisterToken
result := t.DB.First(&token, "token = ?", tokenString)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return false, nil
}
return false, result.Error
}
return true, nil
}
func (t *GORMRegisterTokenRepository) Delete(token string) error {
result := t.DB.Where("token = ?", token).Delete(&models.RegisterToken{})
if result.Error != nil {
return result.Error
} else if result.RowsAffected == 0 {
return fmt.Errorf("Token not found, could not be deleted")
}
return nil
}

View File

@@ -1,17 +1,20 @@
package repositories
import(
"os"
"gorm.io/gorm"
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"os"
"example.com/gin/test/models"
)
"git.dynamicdiscord.de/kalipso/zineshop/models"
)
var(
var (
ShopItems ShopItemRepository
Users UserRepository
Tags TagRepository
Users UserRepository
Tags TagRepository
CartItems CartItemRepository
Orders OrderRepository
Tokens RegisterTokenRepository
)
func InitRepositories() {
@@ -20,7 +23,14 @@ func InitRepositories() {
panic("failed to connect to database")
}
err = db.AutoMigrate(&models.ShopItem{}, &models.User{}, &models.Tag{})
err = db.AutoMigrate(&models.ShopItem{},
&models.ItemVariant{},
&models.User{},
&models.Tag{},
&models.CartItem{},
&models.Order{},
&models.RegisterToken{})
if err != nil {
panic("failed to migrate database")
}
@@ -28,4 +38,7 @@ func InitRepositories() {
ShopItems = NewGORMShopItemRepository(db)
Users = NewGORMUserRepository(db)
Tags = NewGORMTagRepository(db)
CartItems = NewGORMCartItemRepository(db)
Orders = NewGORMOrderRepository(db)
Tokens = NewGORMRegisterTokenRepository(db)
}

View File

@@ -1,18 +1,19 @@
package repositories
import(
"strconv"
import (
"gorm.io/gorm"
"strconv"
"example.com/gin/test/models"
)
"git.dynamicdiscord.de/kalipso/zineshop/models"
)
type ShopItemRepository interface {
Create(models.ShopItem) (models.ShopItem, error)
Create(models.ShopItem) (models.ShopItem, error)
GetAll() ([]models.ShopItem, error)
GetAllPublic() ([]models.ShopItem, error)
GetById(string) (models.ShopItem, error)
//GetByTagId(string) ([]models.ShopItem, error)
GetByTagId(string) ([]models.ShopItem, error)
GetVariantById(string) (models.ItemVariant, error)
Update(models.ShopItem) (models.ShopItem, error)
DeleteById(string) error
}
@@ -30,7 +31,7 @@ func NewGORMShopItemRepository(db *gorm.DB) ShopItemRepository {
func (r *GORMShopItemRepository) Create(shopItem models.ShopItem) (models.ShopItem, error) {
result := r.DB.Create(&shopItem)
if result.Error != nil {
return models.ShopItem{}, result.Error
return models.ShopItem{}, result.Error
}
return shopItem, nil
@@ -38,17 +39,16 @@ func (r *GORMShopItemRepository) Create(shopItem models.ShopItem) (models.ShopIt
func (r *GORMShopItemRepository) GetAll() ([]models.ShopItem, error) {
var shopItems []models.ShopItem
result := r.DB.Preload("Tags").Find(&shopItems)
result := r.DB.Preload("Tags").Preload("Variants").Find(&shopItems)
return shopItems, result.Error
}
func (r *GORMShopItemRepository) GetAllPublic() ([]models.ShopItem, error) {
var shopItems []models.ShopItem
result := r.DB.Preload("Tags").Where("is_public = 1").Find(&shopItems)
result := r.DB.Preload("Tags").Preload("Variants").Where("is_public = 1").Find(&shopItems)
return shopItems, result.Error
}
func (r *GORMShopItemRepository) GetById(id string) (models.ShopItem, error) {
@@ -59,7 +59,7 @@ func (r *GORMShopItemRepository) GetById(id string) (models.ShopItem, error) {
}
var shopItem models.ShopItem
result := r.DB.Preload("Tags").First(&shopItem, uint(shopItemId))
result := r.DB.Preload("Tags").Preload("Variants").First(&shopItem, uint(shopItemId))
if result.Error != nil {
return models.ShopItem{}, result.Error
@@ -68,12 +68,51 @@ func (r *GORMShopItemRepository) GetById(id string) (models.ShopItem, error) {
return shopItem, nil
}
func (r *GORMShopItemRepository) GetByTagId(id string) ([]models.ShopItem, error) {
tagId, err := strconv.Atoi(id)
if err != nil {
return nil, err
}
var shopItems []models.ShopItem
result := r.DB.Joins("JOIN item_tags ON item_tags.shop_item_id = shop_items.id").Where("item_tags.tag_id = ?", tagId).Preload("Tags").Preload("Variants").Find(&shopItems)
if result.Error != nil {
return nil, result.Error
}
return shopItems, nil
}
func (r *GORMShopItemRepository) GetVariantById(id string) (models.ItemVariant, error) {
itemVariantId, err := strconv.Atoi(id)
if err != nil {
return models.ItemVariant{}, err
}
var itemVariant models.ItemVariant
result := r.DB.First(&itemVariant, uint(itemVariantId))
if result.Error != nil {
return models.ItemVariant{}, result.Error
}
return itemVariant, nil
}
func (r *GORMShopItemRepository) Update(shopItem models.ShopItem) (models.ShopItem, error) {
err := r.DB.Model(&shopItem).Association("Tags").Replace(shopItem.Tags)
if err != nil {
return models.ShopItem{}, err
}
err = r.DB.Model(&shopItem).Association("Variants").Replace(shopItem.Variants)
if err != nil {
return models.ShopItem{}, err
}
result := r.DB.Save(&shopItem)
if result.Error != nil {
return models.ShopItem{}, result.Error

View File

@@ -5,7 +5,7 @@ import(
"gorm.io/gorm"
"example.com/gin/test/models"
"git.dynamicdiscord.de/kalipso/zineshop/models"
)
type TagRepository interface {

View File

@@ -1,15 +1,16 @@
package repositories
import(
import (
"gorm.io/gorm"
"example.com/gin/test/models"
)
"git.dynamicdiscord.de/kalipso/zineshop/models"
)
type UserRepository interface {
Create(models.User) (models.User, error)
Create(models.User) (models.User, error)
GetByEmail(string) (models.User, error)
GetById(interface{}) (models.User, error)
IsEmpty() (bool, error)
}
type GORMUserRepository struct {
@@ -22,7 +23,7 @@ func NewGORMUserRepository(db *gorm.DB) UserRepository {
}
}
func (u *GORMUserRepository) Create(user models.User) (models.User, error) {
func (u *GORMUserRepository) Create(user models.User) (models.User, error) {
result := u.DB.Create(&user)
if result.Error != nil {
@@ -53,3 +54,18 @@ func (u *GORMUserRepository) GetById(id interface{}) (models.User, error) {
return user, nil
}
func (u *GORMUserRepository) IsEmpty() (bool, error) {
var user models.User
result := u.DB.First(&user)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return true, nil
} else {
return false, result.Error
}
}
return false, nil
}

View File

@@ -3,8 +3,8 @@ package services
import(
"fmt"
"example.com/gin/test/models"
"example.com/gin/test/repositories"
"git.dynamicdiscord.de/kalipso/zineshop/models"
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
)
var(
@@ -18,7 +18,7 @@ func (u *ShopItemService) NewShopItem(name string, abstract string, description
Name: name,
Abstract: abstract,
Description: description,
Price: price,
BasePrice: price,
IsPublic: true,
}

View File

@@ -1,23 +1,23 @@
package services
import(
import (
"golang.org/x/crypto/bcrypt"
"os"
"time"
"golang.org/x/crypto/bcrypt"
"github.com/golang-jwt/jwt/v5"
"example.com/gin/test/models"
"example.com/gin/test/repositories"
"git.dynamicdiscord.de/kalipso/zineshop/models"
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
)
var(
var (
Users UserService = UserService{}
)
type UserService struct {}
type UserService struct{}
func (u *UserService) Register(name string, email string, password string) (models.User, error) {
func (u *UserService) Register(name string, email string, password string, isAdmin bool) (models.User, error) {
//hash pw
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
@@ -25,7 +25,7 @@ func (u *UserService) Register(name string, email string, password string) (mode
return models.User{}, err
}
user := models.User{Name: name, Email: email, Password: string(hash)}
user := models.User{Name: name, Email: email, Password: string(hash), IsAdmin: isAdmin}
_, err = repositories.Users.Create(user)
if err != nil {
@@ -35,7 +35,7 @@ func (u *UserService) Register(name string, email string, password string) (mode
return user, nil
}
//return jwt tokenstring on success
// return jwt tokenstring on success
func (u *UserService) Login(email string, password string) (string, error) {
//lookup requested user
user, err := repositories.Users.GetByEmail(email)

BIN
static/img/logo-black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
static/img/logo-white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

File diff suppressed because it is too large Load Diff

21
test/lib.nix Normal file
View File

@@ -0,0 +1,21 @@
# tests/lib.nix
# based on https://blog.thalheim.io/2023/01/08/how-to-use-nixos-testing-framework-with-flakes/
# The first argument to this function is the test module itself
test:
# These arguments are provided by `flake.nix` on import, see checkArgs
{ pkgs, self}:
let
inherit (pkgs) lib;
# this imports the nixos library that contains our testing framework
nixos-lib = import (pkgs.path + "/nixos/lib") {};
in
(nixos-lib.runTest {
hostPkgs = pkgs;
# This speeds up the evaluation by skipping evaluating documentation (optional)
defaults.documentation.enable = lib.mkDefault false;
# This makes `self` available in the NixOS configuration of our virtual machines.
# This is useful for referencing modules or packages from your own flake
# as well as importing from other flakes.
node.specialArgs = { inherit self; };
imports = [ test ];
}).config.result

21
test/test.nix Normal file
View File

@@ -0,0 +1,21 @@
# ./tests/hello-world-server.nix
(import ./lib.nix) {
name = "from-nixos";
nodes = {
# `self` here is set by using specialArgs in `lib.nix`
node1 = { self, pkgs, ... }: {
imports = [ self.nixosModules.zineshop ];
services.zineshop.enable = true;
environment.systemPackages = [ pkgs.curl ];
};
};
testScript = ''
start_all() # wait for our service to start
node1.wait_for_unit("zineshop.service")
output = node1.succeed("curl localhost:8080")
# Check if our webserver returns the expected result
assert "Zine Shop" in output, f"'{output}' does not contain 'Zine Shop'"
'';
}

View File

@@ -1,37 +1,36 @@
package main
import (
"fmt"
"net/http"
"io/ioutil"
"fmt"
"io/ioutil"
"net/http"
)
func main() {
func testFunc() {
url := "http://localhost:8080/test"
method := "GET"
method := "GET"
client := &http.Client {
}
req, err := http.NewRequest(method, url, nil)
client := &http.Client{}
req, err := http.NewRequest(method, url, nil)
if err != nil {
fmt.Println(err)
return
}
req.Header.Add("Authorization", "Basic dXNlcjpwYXNzd29yZA==")
if err != nil {
fmt.Println(err)
return
}
req.Header.Add("Authorization", "Basic dXNlcjpwYXNzd29yZA==")
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}

19
utils/utils.go Normal file
View File

@@ -0,0 +1,19 @@
package utils
import (
"crypto/rand"
"encoding/hex"
)
func GenerateSessionId(length int) string {
bytes := make([]byte, length) // 16 bytes = 128 bits
_, err := rand.Read(bytes)
if err != nil {
panic("failed to generate session ID")
}
return hex.EncodeToString(bytes)
}
func GenerateToken() string {
return GenerateSessionId(16)
}

View File

@@ -2,7 +2,7 @@
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/circlea.png" alt="Your Company">
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Your Company">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Add an Item</h2>
</div>
@@ -55,11 +55,25 @@
-->
<div>
<input type="hidden" name="category" value="Zine" required>
<input type="hidden" id="variant-name1" name="variant-name[]" value="B/W" required>
<div class="flex items-center justify-between">
<label for="price" class="block text-sm/6 font-medium text-gray-900">Price</label>
<label for="variant-value1" class="block text-sm/6 font-medium text-gray-900">Price B/W</label>
</div>
<div class="mt-2">
<input type="number" name="price" id="price" step="0.01" min="0.00" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
<input type="number" name="variant-value[]" id="variant-value1" step="0.01" min="0.00" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<input type="hidden" id="variant-name2" name="variant-name[]" value="Colored" required>
<div class="flex items-center justify-between">
<label for="variant-value2" class="block text-sm/6 font-medium text-gray-900">Price Colored</label>
</div>
<div class="mt-2">
<input type="number" name="variant-value[]" id="variant-value2" step="0.01" min="0.00" class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
@@ -86,6 +100,42 @@
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-900">
PDF
</label>
<div class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
<div class="space-y-1 text-center">
<svg class="mx-auto h-12 w-12 text-gray-900" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<div class="flex text-sm text-gray-600">
<label for="pdf-upload" class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
<span class="">Upload a file</span>
<input id="pdf-upload" name="pdf" type="file" accept="application/pdf" class="sr-only">
</label>
<p class="pl-1 text-gray-900">or drag and drop</p>
</div>
<p class="text-xs text-gray-900">
PDF up to 50MB
</p>
</div>
</div>
</div>
<label class="block text-sm font-medium text-gray-900">
Print Mode
</label>
<select name="print-mode" required class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option selected value="CreateBooklet">Create Booklet</option>
<option value="LongEdge">Long Edge</option>
<option value="ShortEdge">Short Edge</option>
</select>
<p class="text-sm">Choose 'Create Booklet' if its just a normal PDF (not converted to booklet already)</p>
<p class="mt-10 text-center text-sm/6 text-red-500">
{{ .data.error }}
</p>
@@ -101,5 +151,4 @@
</div>
</div>
{{ template "footer.html" . }}

35
views/addtag.html Normal file
View File

@@ -0,0 +1,35 @@
{{ template "header.html" . }}
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Your Company">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Add Tag</h2>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form class="space-y-6" action="#" method="POST" enctype="multipart/form-data">
<div>
<label for="name" class="block text-sm/6 font-medium text-gray-900">Name</label>
<div class="mt-2">
<input type="text" name="name" id="name" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<p class="mt-10 text-center text-sm/6 text-red-500">
{{ .data.error }}
</p>
<p class="mt-10 text-center text-sm/6 text-green-500">
{{ .data.success }}
</p>
<div>
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Add Tag</button>
</div>
</form>
</div>
</div>
{{ template "footer.html" . }}

53
views/batchupload.html Normal file
View File

@@ -0,0 +1,53 @@
{{ template "header.html" . }}
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Your Company">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Batch Upload</h2>
Select multiple files on the upload. For each a new Shop Item will be created with the Filename as Name, Abstract
and Description.<br>
Afterwards you should edit every item and set the proper Values for that, price and so on.
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form class="space-y-6" action="/batchupload" method="POST" enctype="multipart/form-data">
<div>
<label class="block text-sm font-medium text-gray-900">
PDF
</label>
<div class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
<div class="space-y-1 text-center">
<svg class="mx-auto h-12 w-12 text-gray-900" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<div class="flex text-sm text-gray-600">
<label for="pdf-upload" class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
<span class="">Upload a file</span>
<input id="pdf-upload" name="pdf" type="file" multiple="multiple" accept="application/pdf" class="sr-only">
</label>
<p class="pl-1 text-gray-900">or drag and drop</p>
</div>
<p class="text-xs text-gray-900">
PDF up to 50MB
</p>
</div>
</div>
</div>
<p class="mt-10 text-center text-sm/6 text-red-500">
{{ .data.error }}
</p>
<p class="mt-10 text-center text-sm/6 text-green-500">
{{ .data.success }}
</p>
<div>
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Add Item</button>
</div>
</form>
</div>
</div>
{{ template "footer.html" . }}

91
views/cart.html Normal file
View File

@@ -0,0 +1,91 @@
{{ template "header.html" . }}
<section class="py-24 relative">
<div class="w-full max-w-7xl px-4 md:px-5 lg-6 mx-auto">
<h2 class="title font-manrope font-bold text-4xl leading-10 mb-8 text-center text-black">Shopping Cart
</h2>
{{ range .data.cartItems }}
<div class="rounded-3xl border-2 border-gray-200 p-4 lg:p-8 grid grid-cols-12 mb-8 max-lg:max-w-lg max-lg:mx-auto gap-y-4 ">
<div class="col-span-12 lg:col-span-2 img box">
<img src="/{{ .ShopItem.Image }}" alt="speaker image" class="max-lg:w-full lg:w-[180px] rounded-lg object-cover">
</div>
<div class="col-span-12 lg:col-span-10 detail w-full lg:pl-3">
<div class="flex items-center justify-between w-full mb-4">
<h5 class="font-manrope font-bold text-2xl leading-9 text-gray-900">{{ .ShopItem.Name }} - {{ .ItemVariant.Name}}</h5>
<form action="/cart/delete" method="POST">
<input type="hidden" id="{{ .ID }}" name="id" value="{{ .ID }}">
<button type="submit" class="rounded-full group flex items-center justify-center focus-within:outline-red-500">
<svg width="34" height="34" viewBox="0 0 34 34" fill="none"
xmlns="http://www.w3.org/2000/svg">
<circle class="fill-red-50 transition-all duration-500 group-hover:fill-red-400"
cx="17" cy="17" r="17" fill="" />
<path class="stroke-red-500 transition-all duration-500 group-hover:stroke-white"
d="M14.1673 13.5997V12.5923C14.1673 11.8968 14.7311 11.333 15.4266 11.333H18.5747C19.2702 11.333 19.834 11.8968 19.834 12.5923V13.5997M19.834 13.5997C19.834 13.5997 14.6534 13.5997 11.334 13.5997C6.90804 13.5998 27.0933 13.5998 22.6673 13.5997C21.5608 13.5997 19.834 13.5997 19.834 13.5997ZM12.4673 13.5997H21.534V18.8886C21.534 20.6695 21.534 21.5599 20.9807 22.1131C20.4275 22.6664 19.5371 22.6664 17.7562 22.6664H16.2451C14.4642 22.6664 13.5738 22.6664 13.0206 22.1131C12.4673 21.5599 12.4673 20.6695 12.4673 18.8886V13.5997Z"
stroke="#EF4444" stroke-width="1.6" stroke-linecap="round" />
</svg>
</button>
</form>
</div>
<p class="font-normal text-base leading-7 text-gray-500 mb-6">
{{ .ShopItem.Abstract }}
</p>
<form action="/cart/edit" method="POST">
<div class="flex justify-between items-center">
<input type="hidden" id="{{ .ID }}" name="id" value="{{ .ID }}">
<div class="flex items-center gap-4">
<input type="number" name="amount" value="{{ .Quantity }}" step="1" min="0" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
<button type="submit" name="action" value="setAmount"
class="block w-full bg-indigo-500 rounded-md px-3 py-1.5 text-base text-white placeholder:text-gray-400 sm:text-sm/6 duration-500 hover:bg-indigo-700">
Set Amount
</button>
</div>
<h6 class="text-indigo-600 font-manrope font-bold text-2xl leading-9 text-right">{{ .Quantity}} x {{ .ItemVariant.Price }}€</h6>
</div>
</form>
</div>
</div>
{{ end }}
<div class="flex flex-col md:flex-row items-center md:items-center justify-between lg:px-6 pb-6 max-lg:max-w-lg max-lg:mx-auto">
<h5 class="text-gray-900 font-manrope font-semibold text-2xl leading-9 w-full max-md:text-center max-md:mb-4">Subtotal</h5>
<div class="flex items-center justify-between gap-5 ">
<h6 class="font-manrope font-bold text-3xl lead-10 text-indigo-600">{{ .data.priceTotal }}€</h6>
</div>
</div>
<form action="/checkout" method="GET">
<div class="flex flex-col md:flex-row items-center md:items-center justify-between lg:px-6 pb-6 border-b border-gray-200 max-lg:max-w-lg max-lg:mx-auto">
<h2 class="text-gray-900 font-manrope font-semibold leading-9 w-full max-md:text-center max-md:mb-4">Select shipping method</h2>
<select name="shippingMethod" id="shippingMethod" required class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option selected value="">Shipping</option>
{{ range .data.shipping }}
<option value="{{ .Id }}">{{ .Name }} - {{ .Price }}€</option>
{{ end }}
</select>
</div>
<div class="max-lg:max-w-lg max-lg:mx-auto">
<p class="font-normal text-base leading-7 text-gray-500 text-center mb-5 mt-6">Shipping calculated at checkout</p>
<button type="submit"
class="rounded py-4 px-6 bg-indigo-500 text-white font-semibold text-lg w-full text-center transition-all duration-500 hover:bg-indigo-700 ">Checkout</button>
</div>
</form>
</div>
</div>
</div>
</section>
{{ template "footer.html" . }}

92
views/checkout.html Normal file
View File

@@ -0,0 +1,92 @@
{{ template "header.html" . }}
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Logo">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Checkout</h2>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form class="space-y-6" action="/checkout" method="POST" enctype="multipart/form-data">
<input type="hidden" name="shippingMethod" value="{{ .shippingMethod }}" required>
{{ if .askAddress }}
<div>
<label for="firstName" class="block text-sm/6 font-medium text-gray-900">First Name</label>
<div class="mt-2">
<input type="text" name="firstName" id="firstName" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<label for="lastName" class="block text-sm/6 font-medium text-gray-900">Last Name</label>
<div class="mt-2">
<input type="text" name="lastName" id="lastName" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<label for="Address" class="block text-sm/6 font-medium text-gray-900">Streetname and Number</label>
<div class="mt-2">
<input type="text" name="address" id="address" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<label for="postalCode" class="block text-sm/6 font-medium text-gray-900">Postal Code</label>
<div class="mt-2">
<input type="text" name="postalCode" id="postalCode" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<label for="city" class="block text-sm/6 font-medium text-gray-900">City</label>
<div class="mt-2">
<input type="text" name="city" id="city" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<label for="country" class="block text-sm/6 font-medium text-gray-900">Country</label>
<div class="mt-2">
<input type="text" name="country" id="country" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
{{ end }}
<div>
<label for="email" class="block text-sm/6 font-medium text-gray-900">E-Mail</label>
<p class="text-sm/8 font-small text-gray-600">Without E-Mail you wont receive an order confirmation.</p>
<div class="mt-2">
<input type="email" name="email" id="email" class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<label for="comment" class="block text-sm/6 font-medium text-gray-900" for="passwordConfirmation">Comment</label>
<textarea id="comment" name="comment" type="textarea" class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-300 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-500 dark:focus:border-blue-500 focus:outline-none focus:ring"></textarea>
</div>
<p class="mt-10 text-center text-sm/6 text-red-500">
{{ .data.error }}
</p>
<p class="mt-10 text-center text-sm/6 text-green-500">
{{ .data.success }}
</p>
<div>
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Preview Order</button>
</div>
</form>
</div>
</div>
{{ template "footer.html" . }}

23
views/colors.html Normal file
View File

@@ -0,0 +1,23 @@
<span class="bg-red-100 text-red-800 bg-red-900 text-red-300">FOO</span>
<span class="bg-orange-100 text-orange-800 bg-orange-900 text-orange-300">FOO</span>
<span class="bg-amber-100 text-amber-800 bg-amber-900 text-amber-300">FOO</span>
<span class="bg-yellow-100 text-yellow-800 bg-yellow-900 text-yellow-300">FOO</span>
<span class="bg-lime-100 text-lime-800 bg-lime-900 text-lime-300">FOO</span>
<span class="bg-green-100 text-green-800 bg-green-900 text-green-300">FOO</span>
<span class="bg-emerald-100 text-emerald-800 bg-emerald-900 text-emerald-300">FOO</span>
<span class="bg-teal-100 text-teal-800 bg-teal-900 text-teal-300">FOO</span>
<span class="bg-cyan-100 text-cyan-800 bg-cyan-900 text-cyan-300">FOO</span>
<span class="bg-sky-100 text-sky-800 bg-sky-900 text-sky-300">FOO</span>
<span class="bg-blue-100 text-blue-800 bg-blue-900 text-blue-300">FOO</span>
<span class="bg-indigo-100 text-indigo-800 bg-indigo-900 text-indigo-300">FOO</span>
<span class="bg-violet-100 text-violet-800 bg-violet-900 text-violet-300">FOO</span>
<span class="bg-purple-100 text-purple-800 bg-purple-900 text-purple-300">FOO</span>
<span class="bg-fuchsia-100 text-fuchsia-800 bg-fuchsia-900 text-fuchsia-300">FOO</span>
<span class="bg-pink-100 text-pink-800 bg-pink-900 text-pink-300">FOO</span>
<span class="bg-rose-100 text-rose-800 bg-rose-900 text-rose-300">FOO</span>
<span class="bg-slate-100 text-slate-800 bg-slate-900 text-slate-300">FOO</span>
<span class="bg-gray-100 text-gray-800 bg-gray-900 text-gray-300">FOO</span>
<span class="bg-zinc-100 text-zinc-800 bg-zinc-900 text-zinc-300">FOO</span>
<span class="bg-neutral-100 text-neutral-800 bg-neutral-900 text-neutral-300">FOO</span>
<span class="bg-stone-100 text-stone-800 bg-stone-900 text-stone-300">FOO</span>

View File

@@ -4,8 +4,8 @@
<div class="bg-gray-200 rounded m-4 p-4">
<h3 class="text-lg">{{ .data.shopItem.Name }}</h3>
<i class="text-xs">{{ .data.shopItem.Description }}</i>
<p class="">Price: {{ .data.shopItem.Price }}</p>
{{ if .loggedIn }}
<p class="">Price: {{ .data.shopItem.BasePrice }}</p>
{{ if .isAdmin }}
<p class="mt-10 text-center text-sm/6 text-red-500">
Do you really want to delete this item??

View File

@@ -2,13 +2,17 @@
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/circlea.png" alt="Your Company">
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Your Company">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Edit Item</h2>
<a href="/shopitems/{{ .data.shopItem.ID }}">
<img src="/{{ .data.shopItem.Image }}" alt="Product Image" class="aspect-4/5 mx-auto rounded-md bg-gray-200 object-cover group-hover:opacity-75 lg:aspect-auto lg:h-80">
</a>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form class="space-y-6" action="#" method="POST">
<form class="space-y-6" action="#" method="POST" enctype="multipart/form-data">
<div>
<label for="name" class="block text-sm/6 font-medium text-gray-900">Name</label>
<div class="mt-2">
@@ -55,14 +59,28 @@
-->
<div>
<input type="hidden" name="category" value="Zine" required>
<input type="hidden" id="variant-name1" name="variant-name[]" value="B/W" required>
<div class="flex items-center justify-between">
<label for="price" class="block text-sm/6 font-medium text-gray-900">Price</label>
<label for="variant-value1" class="block text-sm/6 font-medium text-gray-900">Price B/W</label>
</div>
<div class="mt-2">
<input type="number" name="price" id="price" value="{{ .data.shopItem.Price }}" step="0.01" min="0.00" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
<input type="number" name="variant-value[]" id="variant-value1" step="0.01" min="0.00" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<input type="hidden" id="variant-name2" name="variant-name[]" value="Colored" required>
<div class="flex items-center justify-between">
<label for="variant-value2" class="block text-sm/6 font-medium text-gray-900">Price Colored</label>
</div>
<div class="mt-2">
<input type="number" name="variant-value[]" id="variant-value2" step="0.01" min="0.00" class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-900">
Image
@@ -73,9 +91,9 @@
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<div class="flex text-sm text-gray-600">
<label for="file-upload" class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
<label for="image-upload" class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
<span class="">Upload a file</span>
<input id="file-upload" name="file-upload" type="file" class="sr-only">
<input id="image-upload" name="image" type="file" accept="image/*" class="sr-only">
</label>
<p class="pl-1 text-gray-900">or drag and drop</p>
</div>
@@ -86,6 +104,39 @@
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-900">
PDF
</label>
<div class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
<div class="space-y-1 text-center">
<svg class="mx-auto h-12 w-12 text-gray-900" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<div class="flex text-sm text-gray-600">
<label for="pdf-upload" class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
<span class="">Upload a file</span>
<input id="pdf-upload" name="pdf" type="file" accept="application/pdf" class="sr-only">
</label>
<p class="pl-1 text-gray-900">or drag and drop</p>
</div>
<p class="text-xs text-gray-900">
PDF up to 50MB
</p>
</div>
</div>
</div>
<label class="block text-sm font-medium text-gray-900">
Print Mode
</label>
<select name="print-mode" required class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option selected value="CreateBooklet">Create Booklet</option>
<option value="LongEdge">Long Edge</option>
<option value="ShortEdge">Short Edge</option>
</select>
<p class="mt-10 text-center text-sm/6 text-red-500">
{{ .data.error }}
</p>

38
views/editorders.html Normal file
View File

@@ -0,0 +1,38 @@
{{ template "header.html" . }}
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Your Company">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Edit Orders</h2>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
{{ range .data.orders }}
<form action="/order/{{ .Token }}/edit" method="POST">
<div class="max-w-md mx-auto mt-4">
<div class="flex">
<input type="text" id="name" name="name" value="{{ .Token }}" readonly="readonly" class="flex-grow border border-gray-300 rounded-md p-2 focus:outline-none focus:ring focus:ring-blue-500">
<div>
<select name="order-status" required class="bg-gray-50 border ml-4 border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option selected value="{{ .Status }}">{{ .Status }}</option>
<option value="AwaitingConfirmation">AwaitingConfirmation</option>
<option value="Received">Received</option>
<option value="AwaitingPayment">AwaitingPayment</option>
<option value="Payed">Payed</option>
<option value="ReadyForPickup">ReadyForPickup</option>
<option value="Shipped">Shipped</option>
<option value="Cancelled">Cancelled</option>
</select>
</div>
<button type="submit" name="action" value="update" class="bg-blue-600 text-white ml-4 rounded px-4 hover:bg-blue-700">Update</button>
<button type="button" class="bg-blue-600 text-white ml-4 mr-4 rounded px-4 hover:bg-blue-700"><a
href="/order/{{ .Token }}/print">Print</a></button>
<button type="button" class="bg-blue-600 text-white mr-4 rounded px-4 hover:bg-blue-700"><a
href="/order/{{ .Token }}">View</a></button>
<button type="submit" name="action" value="delete" class="bg-red-800 text-white rounded px-4 hover:bg-red-900">Delete</button>
</div>
</form>
{{ end }}
</div>
{{ template "footer.html" . }}

11
views/error.html Normal file
View File

@@ -0,0 +1,11 @@
{{ template "header.html" . }}
<p class="mt-10 text-center text-sm/6 text-red-500">
{{ .error }}
</p>
<p class="mt-10 text-center text-sm/6 text-red-500">
{{ .data.error }}
</p>
{{ template "footer.html" . }}

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>FreiRaum</title>
<title>Zine Shop</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/output.css" rel="stylesheet">
@@ -12,8 +12,8 @@
<div class="mx-auto max-w-7xl px-4 sm:px-8 lg:px-8">
<div class="relative flex h-16 items-center justify-between">
<div class="flex flex-1 items-center">
<a href="/"><div class="flex shrink-0">
<img class="h-8 w-auto" src="/static/img/circlea.png" alt="Your Company">
<a href="/"><div class="flex-shrink-0 w-full h-full">
<img class="h-8 w-auto" src="/static/img/logo-white.png" alt="Your Company">
</div></a>
<!--
{{ if .loggedIn }}
@@ -25,16 +25,25 @@
{{ end }}
-->
</div>
{{ if .loggedIn }}
{{ if .isAdmin }}
<div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0 px-4 sm:px-8">
<a href="/additem" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">Add Item</a>
<a href="/batchupload" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300
hover:bg-gray-700 hover:text-white">Batch Upload</a>
<a href="/orders" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300
hover:bg-gray-700 hover:text-white">Orders</a>
<a href="/tags" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">Tags</a>
<a href="/invites" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">Invites</a>
<a href="/cart/print" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700
hover:text-white">Print</a>
<a href="/cart" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700
hover:text-white">Cart</a>
<a href="/logout" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-red-300 hover:bg-gray-700 hover:text-white">Logout</a>
</div>
{{ else }}
{{ end }}
{{ if .loggedIn }}
<div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0 px-4 sm:px-8">
<a href="/login" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">Login</a>
<a href="/register" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">Register</a>
<a href="/cart" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">&#128722; Cart</a>
</div>
{{ end }}
</div>

28
views/invites.html Normal file
View File

@@ -0,0 +1,28 @@
{{ template "header.html" . }}
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Your Company">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Create/Delete Invites</h2>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
{{ range .tokens }}
<form action="/invites" method="POST">
<div class="max-w-md mx-auto mt-4">
<div class="flex">
<input type="text" id="token" name="token" value="{{ .Token }}" readonly="readonly" class="flex-grow border border-gray-300 rounded-md p-2 focus:outline-none focus:ring focus:ring-blue-500">
<button type="submit" name="action" value="delete" class="bg-red-800 text-white rounded px-4 hover:bg-red-900">Delete</button>
</div>
</form>
{{ end }}
<form action="/invites" method="POST">
<div class="max-w-md mx-auto mt-4">
<div class="flex">
<button type="submit" name="action" value="create" class="bg-green-600 text-white ml-4 mr-4 rounded px-4 hover:bg-green-700">Create</button>
</div>
</div>
</form>
</div>
{{ template "footer.html" . }}

View File

@@ -2,7 +2,7 @@
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="https://tailwindui.com/plus/img/logos/mark.svg?color=indigo&shade=600" alt="Your Company">
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Logo">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Login to your account</h2>
</div>

106
views/order.html Normal file
View File

@@ -0,0 +1,106 @@
{{ template "header.html" . }}
<section class="bg-white py-8 antialiased dark:bg-gray-900 md:py-16">
<div class="mx-auto max-w-screen-xl px-4 2xl:px-0">
<div class="mx-auto max-w-3xl">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white sm:text-2xl">Order summary</h2>
<dd class="mt-1 text-base font-normal text-gray-500 dark:text-gray-400">
Thanks for your order! As soon as your payment arrived we will print your Order.
</dd>
<div class="mt-6 space-y-4 border-b border-t border-gray-200 py-8 dark:border-gray-700 sm:mt-8">
<h4 class="text-lg font-semibold text-gray-900 dark:text-white">Order status: {{ .data.order.Status }}</h4>
<dl>
<dd class="mt-1 text-base font-normal text-gray-500 dark:text-gray-400">
Order Code: {{ .data.order.Token }}
</dd>
</dl>
</div>
<div class="mt-6 space-y-4 border-b border-t border-gray-200 py-8 dark:border-gray-700 sm:mt-8">
<h4 class="text-lg font-semibold text-gray-900 dark:text-white">Payment information</h4>
<dl>
<dd class="mt-1 text-base font-normal text-gray-500 dark:text-gray-400">
Either you transfer money to our bank account, or you come by and pay in cash.<br><br>
Miteinander Dresden e.V.*<br>
IBAN: DE66500310001076201001<br>
BIC: TRODDEF1 (Triodos Bank)<br>
Subject: {{ .data.order.Token }}<br>
Amount: {{ .data.priceTotal }}€
</dd>
</dl>
</div>
<div class="mt-6 space-y-4 border-b border-t border-gray-200 py-8 dark:border-gray-700 sm:mt-8">
<h4 class="text-lg font-semibold text-gray-900 dark:text-white">Delivery information</h4>
<dl>
<dd class="mt-1 text-base font-normal text-gray-500 dark:text-gray-400">
<p><b>Shipping:</b> {{ .data.shipping.Name }}</p>
{{ if .data.askAddress }}
<p><b>First Name:</b> {{ .data.order.FirstName }}</p>
<p><b>Last Name:</b> {{ .data.order.LastName }}</p>
<p><b>Address:</b> {{ .data.order.Address }}</p>
<p><b>Postal Code:</b> {{ .data.order.PostalCode }}</p>
<p><b>City:</b> {{ .data.order.City }}</p>
<p><b>Country:</b> {{ .data.order.Country }}</p>
{{ end }}
<p><b>Email:</b> {{ .data.order.Email }}</p>
<p><b>Comment:</b> {{ .data.order.Comment }}</p>
</dd>
</dl>
</div>
<div class="mt-6 sm:mt-8">
<div class="relative overflow-x-auto border-b border-gray-200 dark:border-gray-800">
<table class="w-full text-left font-medium text-gray-900 dark:text-white ">
<tbody class="divide-y divide-gray-200 dark:divide-gray-800">
{{ range .data.order.CartItems }}
<tr>
<td class="whitespace-nowrap py-4">
<div class="flex items-center gap-4">
<a href="#" class="flex items-center aspect-square w-8 h-10 shrink-0">
<img class="h-auto w-full max-h-full dark:hidden" src="/{{ .ShopItem.Image }}" alt="imac image" />
</a>
<a href="/shopitems/{{ .ShopItem.ID }}" class="hover:underline">{{ .ShopItem.Name }} - {{ .ItemVariant.Name }}</a>
</div>
</td>
<td class="p-4 text-base font-normal text-gray-900 dark:text-white">x{{ .Quantity }}</td>
<td class="p-4 text-right text-base font-bold text-gray-900 dark:text-white">{{ .ItemVariant.Price }}€</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<div class="mt-4 space-y-6">
<div class="space-y-4">
<div class="space-y-2">
<dl class="flex items-center justify-between gap-4">
<dt class="text-gray-500 dark:text-gray-400">Original price</dt>
<dd class="text-base font-medium text-gray-900 dark:text-white">{{ .data.priceProducts }}€</dd>
</dl>
<dl class="flex items-center justify-between gap-4">
<dt class="text-gray-500 dark:text-gray-400">Shipping</dt>
<dd class="text-base font-medium text-gray-900 dark:text-white">{{ .data.shipping.Price }}€</dd>
</dl>
</div>
<dl class="flex items-center justify-between gap-4 border-t border-gray-200 pt-2 dark:border-gray-700">
<dt class="text-lg font-bold text-gray-900 dark:text-white">Total</dt>
<dd class="text-lg font-bold text-gray-900 dark:text-white">{{ .data.priceTotal }}€</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</section>
{{ template "footer.html" . }}

88
views/orderpreview.html Normal file
View File

@@ -0,0 +1,88 @@
{{ template "header.html" . }}
<section class="bg-white py-8 antialiased dark:bg-gray-900 md:py-16">
<form action="/order" method="POST" class="mx-auto max-w-screen-xl px-4 2xl:px-0">
<input type="hidden" name="confirm-order" value="true" required>
<div class="mx-auto max-w-3xl">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white sm:text-2xl">Order summary</h2>
<div class="mt-6 space-y-4 border-b border-t border-gray-200 py-8 dark:border-gray-700 sm:mt-8">
<h4 class="text-lg font-semibold text-gray-900 dark:text-white">Delivery information</h4>
<dl>
<dd class="mt-1 text-base font-normal text-gray-500 dark:text-gray-400">
<p><b>Shipping:</b> {{ .data.shipping.Name }}</p>
{{ if .data.askAddress }}
<p><b>First Name:</b> {{ .data.order.FirstName }}</p>
<p><b>Last Name:</b> {{ .data.order.LastName }}</p>
<p><b>Address:</b> {{ .data.order.Address }}</p>
<p><b>Postal Code:</b> {{ .data.order.PostalCode }}</p>
<p><b>City:</b> {{ .data.order.City }}</p>
<p><b>Country:</b> {{ .data.order.Country }}</p>
{{ end }}
<p><b>Email:</b> {{ .data.order.Email }}</p>
<p><b>Comment:</b> {{ .data.order.Comment }}</p>
</dd>
</dl>
<a href="/checkout?shippingMethod={{ .data.order.Shipping }}" data-modal-target="billingInformationModal" data-modal-toggle="billingInformationModal" class="text-base font-medium text-primary-700 hover:underline dark:text-primary-500">Edit</a>
</div>
<div class="mt-6 sm:mt-8">
<div class="relative overflow-x-auto border-b border-gray-200 dark:border-gray-800">
<table class="w-full text-left font-medium text-gray-900 dark:text-white ">
<tbody class="divide-y divide-gray-200 dark:divide-gray-800">
{{ range .data.order.CartItems }}
<tr>
<td class="whitespace-nowrap py-4">
<div class="flex items-center gap-4">
<a href="#" class="flex items-center aspect-square w-8 h-10 shrink-0">
<img class="h-auto w-full max-h-full dark:hidden" src="/{{ .ShopItem.Image }}" alt="imac image" />
</a>
<a href="/shopitems/{{ .ShopItem.ID }}" class="hover:underline">{{ .ShopItem.Name }} - {{ .ItemVariant.Name }}</a>
</div>
</td>
<td class="p-4 text-base font-normal text-gray-900 dark:text-white">x{{ .Quantity }}</td>
<td class="p-4 text-right text-base font-bold text-gray-900 dark:text-white">{{ .ItemVariant.Price }}€</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<div class="mt-4 space-y-6">
<h4 class="text-xl font-semibold text-gray-900 dark:text-white">Order summary</h4>
<div class="space-y-4">
<div class="space-y-2">
<dl class="flex items-center justify-between gap-4">
<dt class="text-gray-500 dark:text-gray-400">Original price</dt>
<dd class="text-base font-medium text-gray-900 dark:text-white">{{ .data.priceProducts }}€</dd>
</dl>
<dl class="flex items-center justify-between gap-4">
<dt class="text-gray-500 dark:text-gray-400">Shipping</dt>
<dd class="text-base font-medium text-gray-900 dark:text-white">{{ .data.shipping.Price }}€</dd>
</dl>
</div>
<dl class="flex items-center justify-between gap-4 border-t border-gray-200 pt-2 dark:border-gray-700">
<dt class="text-lg font-bold text-gray-900 dark:text-white">Total</dt>
<dd class="text-lg font-bold text-gray-900 dark:text-white">{{ .data.priceTotal }}€</dd>
</dl>
</div>
<div class="gap-4 sm:flex sm:items-center">
<button type="button" class="w-full rounded-lg border border-gray-200 bg-white px-5 py-2.5 text-sm font-medium text-gray-900 hover:bg-gray-100 hover:text-primary-700 focus:z-10 focus:outline-none focus:ring-4 focus:ring-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"><a href="/">Return to Shopping</a></button>
<button type="submit" class="w-full bg-gray-900 dark:bg-gray-600 rounded-lg border border-gray-200 bg-white px-5 py-2.5 text-sm font-medium text-gray-900 hover:bg-gray-100 hover:text-primary-700 focus:z-10 focus:outline-none focus:ring-4 focus:ring-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700">Place binding order</button>
</div>
</div>
</div>
</div>
</form>
</section>
{{ template "footer.html" . }}

72
views/printvariant.html Normal file
View File

@@ -0,0 +1,72 @@
{{ template "header.html" . }}
<section class="bg-white py-8 antialiased dark:bg-gray-900 md:py-16">
<div class="mx-auto max-w-3xl">
<div class="mt-6 sm:mt-8">
<div class="relative overflow-x-auto border-b border-gray-200 dark:border-gray-800">
<h2 class="title font-manrope font-bold text-4xl leading-10 mb-8 text-center text-black">Zineshop Print
Service
</h2>
<p class="font-normal text-base leading-7 text-gray-900 text-left mb-5 mt-6">
Pressing Print will automatically print the given Zines for you.<br>
Add Zines for printing simply by adding them to the Cart
</p>
<p class="font-normal text-base leading-7 text-gray-500 text-left mb-5 mt-6">
<bold>CoverPage</bold>: If selected, the Printer will take Paper from the BypassTray for the first page. For
example you can put colored paper there to have a nice looking front page, and the rest will be normal paper.
Makue sure you put paper in that tray when selecting this option.<br><br>
Print Order: The Zines will be printed from top to bottom as seen in this list.
</p>
<form action="/print" method="POST">
<table class="w-full text-left font-medium text-gray-900 dark:text-white">
<tbody class="divide-y divide-gray-200 dark:divide-gray-800">
{{ range .data.cartItems }}
<tr>
<input type="hidden" name="variant-id[]" value="{{ .ItemVariant.ID }}" required>
<td class="whitespace-nowrap py-4">
<div class="flex items-center gap-4">
<a href="/shopitems/{{ .ItemVariant.ShopItemID }}" class="flex items-center aspect-square w-8 h-10 shrink-0">
<img class="h-auto w-full max-h-full dark:hidden" src="/{{ .ShopItem.Image }}" alt="imac image" />
</a>
<a href="/shopitems/{{ .ItemVariant.ShopItemID }}" class="hover:underline">{{ .ShopItem.Name }} - {{ .ItemVariant.Name }}</a>
</div>
</td>
<td class="whitespace-nowrap py-4">
<select name="variant-coverpage[]" required class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option selected value="0">Normal</option>
<option value="1">CoverPage</option>
</select>
</td>
<td class="whitespace-nowrap py-4">
Amount:
</td>
<td class="p-4 text-right text-base font-bold text-gray-900 dark:text-white">
<div>
<div class="mt-2">
<input type="number" name="variant-amount[]" value="{{ .Quantity }}" step="1" min="0" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
</td>
</tr>
{{ end }}
</tbody>
</table>
<div class="max-lg:max-w-lg max-lg:mx-auto">
<p class="font-normal text-base leading-7 text-gray-500 text-center mb-5 mt-6">If CoverPage selected, make sure you put paper in the BypassTray</p>
<button type="submit" class="rounded-full py-4 px-6 bg-indigo-600 text-white font-semibold text-lg w-full text-center transition-all duration-500 hover:bg-indigo-700">Print</button>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
{{ template "footer.html" . }}

View File

@@ -2,7 +2,7 @@
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="https://tailwindui.com/plus/img/logos/mark.svg?color=indigo&shade=600" alt="Your Company">
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Logo">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Register your account</h2>
</div>

59
views/registertoken.html Normal file
View File

@@ -0,0 +1,59 @@
{{ template "header.html" . }}
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Logo">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Register your account</h2>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form class="space-y-6" action="/register" method="POST">
<div>
<label for="token" class="block text-sm/6 font-medium text-gray-900">Token</label>
<div class="mt-2">
<input type="text" name="token" id="token" value="{{ .token }}" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<label for="name" class="block text-sm/6 font-medium text-gray-900">Username</label>
<div class="mt-2">
<input type="text" name="name" id="name" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<label for="email" class="block text-sm/6 font-medium text-gray-900">Email address</label>
<div class="mt-2">
<input type="email" name="email" id="email" autocomplete="email" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<div class="flex items-center justify-between">
<label for="password" class="block text-sm/6 font-medium text-gray-900">Password</label>
</div>
<div class="mt-2">
<input type="password" name="password" id="password" autocomplete="current-password" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<p class="mt-10 text-center text-sm/6 text-red-500">
{{ .error }}
</p>
<p class="mt-10 text-center text-sm/6 text-green-500">
{{ .success }}
</p>
<div>
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Register</button>
</div>
</form>
</div>
</div>
{{ template "footer.html" . }}

View File

@@ -1,24 +1,32 @@
{{ template "header.html" . }}
<div class="bg-gray-100 dark:bg-gray-800 py-8">
<form action="/cart" method="POST">
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex flex-col md:flex-row -mx-4">
<div class="md:flex-1 px-4">
<div class="h-[460px] rounded-lg bg-gray-300 dark:bg-gray-700 mb-4">
<div class="rounded-lg bg-gray-300 dark:bg-gray-700 mb-4">
<img class="w-full h-full object-cover" src="/{{ .data.shopItem.Image}}" alt="Product Image">
</div>
<div class="flex -mx-2 mb-4">
<div class="w-1/3 px-2">
<button class="w-full bg-gray-900 dark:bg-gray-600 text-white py-2 px-4 rounded-full font-bold hover:bg-gray-800 dark:hover:bg-gray-700">Add to Cart</button>
</div>
{{ if .loggedIn }}
<input type="hidden" id="{{ .data.shopItem.ID}}" name="ShopItemId" value="{{ .data.shopItem.ID }}">
<div class="w-1/3 px-2">
<a href="{{ .data.shopItem.ID }}/edit"><button class="w-full bg-blue-900 dark:bg-gray-600 text-white py-2 px-4 rounded-full font-bold hover:bg-gray-800 dark:hover:bg-gray-700">Edit</button></a>
<button type="submit" class="w-full bg-gray-900 dark:bg-gray-600 text-white py-2 px-4 rounded-lg font-bold hover:bg-gray-800 dark:hover:bg-gray-700">Add to Cart</button>
</div>
{{ end }}
<div class="w-1/3 px-2">
<button type="button" class="w-full bg-blue-900 dark:bg-gray-600 text-white py-2 px-4 rounded-lg font-bold hover:bg-gray-800 dark:hover:bg-gray-700"><a href="/{{ .data.shopItem.Pdf }}">View</a></button>
</div>
{{ if .isAdmin }}
<div class="w-1/3 px-2">
<button type="button" class="w-full bg-blue-900 dark:bg-gray-600 text-white py-2 px-4 rounded-lg font-bold hover:bg-gray-800 dark:hover:bg-gray-700"><a href="{{ .data.shopItem.ID }}/edit">Edit</a></button>
</div>
<div class="w-1/3 px-2">
<a href="{{ .data.shopItem.ID }}/delete"><button class="w-full bg-red-900 dark:bg-red-600 text-white py-2 px-4 rounded-full font-bold hover:bg-gray-800 dark:hover:bg-gray-700">Delete</button></a>
<button type="button" class="w-full bg-red-900 dark:bg-red-600 text-white py-2 px-4 rounded-lg font-bold hover:bg-gray-800 dark:hover:bg-gray-700"><a href="{{ .data.shopItem.ID }}/delete">Delete</a></button>
</div>
{{ end }}
</div>
@@ -28,21 +36,27 @@
<p class="text-gray-600 dark:text-gray-300 text-sm mb-4">
{{ .data.shopItem.Abstract }}
</p>
{{ if .loggedIn }}
<div class="flex mb-4">
<div class="mr-4">
<span class="font-bold text-gray-700 dark:text-gray-300">Price:</span>
<span class="text-gray-600 dark:text-gray-300">{{ .data.shopItem.Price }}€</span>
</div>
<label for="ItemVariantId" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"></label>
<select name="ItemVariantId" id="ItemVariantId" required class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option selected value="">Choose a variant</option>
{{ range .data.shopItem.Variants }}
<option value="{{ .ID }}">{{ .Name }} - {{ .Price }}€</option>
{{ end }}
</select>
</div>
{{ end }}
<div class="flex mb-4">
<div class="mr-4">
<span class="font-bold text-gray-700 dark:text-gray-300">Tags:</span>
<span class="text-gray-600 dark:text-gray-300">
<p class="mt-1 text-sm text-gray-500">
{{ range .data.shopItem.Tags }}
{{ .Name }}
<a href="/tags/{{ .ID }}"><span class="bg-{{ .Color }}-100 text-{{ .Color }}-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded-sm dark:bg-{{ .Color }}-900 dark:text-{{ .Color }}-300">{{ .Name }}</span></a>
{{ end }}
</span>
</p>
</div>
</div>
<!--
@@ -64,28 +78,8 @@
</div>
</div>
</div>
</form>
</div>
<!--
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<div class="bg-gray-200 rounded m-4 p-4">
<div class="relative flex rounded-lg justify-center items-center">
<img src="/{{ .data.shopItem.Image }}" alt="shopping image"
class="object-cover items-center justify-center rounded">
</div>
<h3 class="text-lg">{{ .data.shopItem.Name }}</h3>
<i class="text-xs">{{ .data.shopItem.Abstract }}</i>
<p class="text-xs">{{ .data.shopItem.Description }}</p>
<p class="">Price: {{ .data.shopItem.Price }}</p>
{{ if .loggedIn }}
<div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0 px-4 sm:px-8">
<a href="{{ .data.shopItem.ID }}/edit" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">Edit</a>
<a href="{{ .data.shopItem.ID }}/delete" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-red-300 hover:bg-gray-700 hover:text-white">Delete</a>
</div>
{{ end }}
</div>
</div>
-->
{{ template "footer.html" . }}

View File

@@ -1,27 +1,61 @@
<div class="bg-white">
<div class="mx-auto max-w-2xl px-4 py-16 sm:px-6 sm:py-24 lg:max-w-7xl lg:px-8">
<div class="mx-auto max-w-2xl px-4 py-16 sm:px-6 sm:py-24 lg:max-w-7xl">
<h2 class="text-2xl font-bold tracking-tight text-gray-900">Available Zines</h2>
<div class="mt-6 grid grid-cols-1 gap-x-6 gap-y-10 sm:grid-cols-2 lg:grid-cols-4 xl:gap-x-8">
<input type="text" id="myInput" onkeyup="myFunction()" placeholder="Search for names..">
<div class="mt-6 grid grid-cols-1 gap-x-6 gap-y-10 md:grid-cols-2 xl:grid-cols-4">
{{ range .data.shopItems }}
<div class="group relative">
<img src="/{{ .Image }}" alt="Product Image" class="aspect-square w-full rounded-md bg-gray-200 object-cover group-hover:opacity-75 lg:aspect-auto lg:h-80">
<div class="myClass group relative">
<a href="/shopitems/{{ .ID }}">
<img src="/{{ .Image }}" alt="Product Image" class="aspect-4/5 mx-auto rounded bg-gray-200 object-cover group-hover:opacity-75 lg:aspect-auto lg:h-80">
</a>
<div class="mt-4 flex justify-between">
<div>
<h3 class="text-sm text-gray-700">
<a href="/shopitems/{{ .ID }}">
<span aria-hidden="true" class="absolute inset-0"></span>
{{ .Name }}
</a>
</h3>
<!--<p class="mt-1 text-sm text-gray-500">{{ .Abstract }}</p>-->
<p class="mt-1 text-sm text-gray-500">{{ .Abstract }}</p>
<p class="mt-1 text-sm text-gray-500">
{{ range .Tags }}
<a href="/tags/{{ .ID }}"><span class="bg-{{ .Color }}-100 text-{{ .Color }}-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded-sm dark:bg-{{ .Color }}-900 dark:text-{{ .Color }}-300">{{ .Name }}</span></a>
{{ end }}
</p>
</div>
<p class="text-sm font-medium text-gray-900">{{ .Price }}€</p>
{{ if $.loggedIn }}
<p class="text-sm font-medium text-gray-900">{{ .BasePrice }}€</p>
{{ end }}
</div>
</div>
{{ end }}
<script>
function myFunction() {
// Declare variables
var input, filter, ul, li, a, i, txtValue;
input = document.getElementById('myInput');
filter = input.value.toUpperCase();
//ul = document.getElementById("myUL");
//li = ul.getElementsByTagName('li');
li = document.getElementsByClassName("myClass");
console.log(li)
// Loop through all list items, and hide those who don't match the search query
for (i = 0; i < li.length; i++) {
a = li[i].getElementsByTagName("a")[1];
txtValue = a.textContent || a.innerText;
if (txtValue.toUpperCase().indexOf(filter) > -1) {
li[i].style.display = "";
} else {
li[i].style.display = "none";
}
}
}
</script>
</div>
</div>
</div>

View File

@@ -3,7 +3,7 @@
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/circlea.png" alt="Your Company">
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Your Company">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Edit Tags</h2>
</div>
@@ -12,7 +12,36 @@
<form action="/tags/{{ .ID }}" method="POST">
<div class="max-w-md mx-auto mt-4">
<div class="flex">
<span class="bg-{{ .Color }}-100 text-{{ .Color }}-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded-sm
dark:bg-{{ .Color }}-900 dark:text-{{ .Color }}-300">Preview</span>
<input type="text" id="name" name="name" value="{{ .Name }}" class="flex-grow border border-gray-300 rounded-l-md p-2 focus:outline-none focus:ring focus:ring-blue-500">
<select name="color" required>
<option selected value="{{ .Color }}">{{ .Color }}</option>
<option value="1">Default</option>
<option value="red">red</option>
<option value="orange">orange</option>
<option value="amber">amber</option>
<option value="yellow">yellow</option>
<option value="lime">lime</option>
<option value="green">green</option>
<option value="emerald">emerald</option>
<option value="teal">teal</option>
<option value="cyan">cyan</option>
<option value="sky">sky</option>
<option value="blue">blue</option>
<option value="indigo">indigo</option>
<option value="violet">violet</option>
<option value="purple">purple</option>
<option value="fuchsia">fuchsia</option>
<option value="pink">pink</option>
<option value="rose">rose</option>
<option value="slate">slate</option>
<option value="gray">gray</option>
<option value="zinc">zinc</option>
<option value="neutral">neutral</option>
<option value="stone">stone</option>
</select>
<button type="submit" name="action" value="update" class="bg-blue-600 text-white ml-4 mr-4 rounded px-4 hover:bg-blue-700">Update</button>
<button type="submit" name="action" value="delete" class="bg-red-800 text-white rounded px-4 hover:bg-red-900">Delete</button>
</div>