74 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
52 changed files with 2684 additions and 858 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. 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. 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

@@ -1,17 +1,19 @@
package controllers package controllers
import ( import (
"errors"
"fmt" "fmt"
"strconv"
"net/http" "net/http"
"crypto/rand" "strconv"
"encoding/hex" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm"
"example.com/gin/test/models" "git.dynamicdiscord.de/kalipso/zineshop/models"
//"example.com/gin/test/services" //"git.dynamicdiscord.de/kalipso/zineshop/services"
"example.com/gin/test/repositories" "git.dynamicdiscord.de/kalipso/zineshop/repositories"
"git.dynamicdiscord.de/kalipso/zineshop/utils"
) )
type CartItemController interface { type CartItemController interface {
@@ -24,48 +26,50 @@ type CartItemController interface {
CheckoutHandler(*gin.Context) CheckoutHandler(*gin.Context)
OrderView(*gin.Context) OrderView(*gin.Context)
OrderHandler(*gin.Context) OrderHandler(*gin.Context)
OrdersView(*gin.Context)
OrdersHandler(*gin.Context)
} }
type cartItemController struct {} type cartItemController struct{}
func NewCartItemController() CartItemController { func NewCartItemController() CartItemController {
return &cartItemController{} return &cartItemController{}
} }
// getSetCookieValue retrieves the value of a cookie from the Set-Cookie header
func GetShippingMethods() []models.Shipping { func getSetCookieValue(c *gin.Context, cookieName string) string {
return []models.Shipping{ // Check the Set-Cookie headers
{ Id: "germany", Name: "Germany (DHL)", Price: 3.99 }, cookies := c.Writer.Header()["Set-Cookie"]
{ Id: "international", Name: "International (DHL)", Price: 5.99 }, for _, cookie := range cookies {
{ Id: "pickup", Name: "Pickup", Price: 0.00 }, 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 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 GetSessionId(ctx *gin.Context) string { func GetSessionId(ctx *gin.Context) string {
sessionId, err := ctx.Cookie("session_id") sessionId, err := ctx.Cookie("session_id")
if err != nil { if err != nil {
sessionId = generateSessionId(16) //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) ctx.SetCookie("session_id", sessionId, 3600, "/", "", false, true)
} }
return sessionId return sessionId
} }
func GenerateToken() string {
return generateSessionId(8)
}
func (rc *cartItemController) NewCartItemFromForm(ctx *gin.Context) (models.CartItem, error) { func (rc *cartItemController) NewCartItemFromForm(ctx *gin.Context) (models.CartItem, error) {
sessionId := GetSessionId(ctx) sessionId := GetSessionId(ctx)
shopItemIdStr := ctx.PostForm("ShopItemId") shopItemIdStr := ctx.PostForm("ShopItemId")
@@ -88,12 +92,12 @@ func (rc *cartItemController) NewCartItemFromForm(ctx *gin.Context) (models.Cart
itemVariant, err := repositories.ShopItems.GetVariantById(itemVariantIdStr) itemVariant, err := repositories.ShopItems.GetVariantById(itemVariantIdStr)
cartItem := models.CartItem{ cartItem := models.CartItem{
SessionId: sessionId, SessionId: sessionId,
ShopItemId: uint(shopItemId), ShopItemId: uint(shopItemId),
ShopItem: shopItem, ShopItem: shopItem,
ItemVariantId: uint(itemVariantId), ItemVariantId: uint(itemVariantId),
ItemVariant: itemVariant, ItemVariant: itemVariant,
Quantity: quantity, Quantity: quantity,
} }
return cartItem, nil return cartItem, nil
@@ -132,19 +136,19 @@ func (rc *cartItemController) NewAddressFromForm(ctx *gin.Context) (models.Addre
} }
return models.AddressInfo{ return models.AddressInfo{
FirstName: firstName, FirstName: firstName,
LastName: lastName, LastName: lastName,
Address: address, Address: address,
PostalCode: postalCode, PostalCode: postalCode,
City: city, City: city,
Country: country, Country: country,
}, nil }, nil
} }
func (rc *cartItemController) NewOrderFromForm(ctx *gin.Context) (models.Order, error) { func (rc *cartItemController) NewOrderFromForm(ctx *gin.Context) (models.Order, error) {
sessionId := GetSessionId(ctx) sessionId := GetSessionId(ctx)
status := models.OrderStatus("Received") status := models.OrderStatus("AwaitingConfirmation")
token := GenerateToken() token := utils.GenerateToken()
email := ctx.PostForm("email") email := ctx.PostForm("email")
comment := ctx.PostForm("comment") comment := ctx.PostForm("comment")
firstName := ctx.PostForm("firstName") firstName := ctx.PostForm("firstName")
@@ -162,14 +166,9 @@ func (rc *cartItemController) NewOrderFromForm(ctx *gin.Context) (models.Order,
// } // }
//} //}
var shipping models.Shipping shipping, err := models.GetShippingMethod(shippingStr)
for _, shippingMethod := range GetShippingMethods() {
if shippingMethod.Id == shippingStr {
shipping = shippingMethod
}
}
if shipping == (models.Shipping{}) { if err != nil {
return models.Order{}, fmt.Errorf("Invalid shipping method.") return models.Order{}, fmt.Errorf("Invalid shipping method.")
} }
@@ -181,26 +180,25 @@ func (rc *cartItemController) NewOrderFromForm(ctx *gin.Context) (models.Order,
return models.Order{}, err return models.Order{}, err
} }
cartItem := models.Order{ order := models.Order{
SessionId: sessionId, SessionId: sessionId,
Status: status, Status: status,
Token: token, Token: token,
Email: email, Email: email,
Comment: comment, Comment: comment,
FirstName: firstName, FirstName: firstName,
LastName: lastName, LastName: lastName,
Address: address, Address: address,
PostalCode: postalCode, PostalCode: postalCode,
City: city, City: city,
Country: country, Country: country,
Shipping: shipping.Id, Shipping: shipping.Id,
CartItems: cartItems, CartItems: cartItems,
} }
return cartItem, nil return order, nil
} }
func (rc *cartItemController) Create(c *gin.Context) { func (rc *cartItemController) Create(c *gin.Context) {
cartItem, err := rc.NewCartItemFromForm(c) cartItem, err := rc.NewCartItemFromForm(c)
@@ -219,7 +217,6 @@ func (rc *cartItemController) Create(c *gin.Context) {
ReplyOK(c, "cartItem was created") ReplyOK(c, "cartItem was created")
} }
func (rc *cartItemController) Update(c *gin.Context) { func (rc *cartItemController) Update(c *gin.Context) {
cartItem, err := rc.NewCartItemFromForm(c) cartItem, err := rc.NewCartItemFromForm(c)
@@ -250,13 +247,13 @@ func (rc *cartItemController) Update(c *gin.Context) {
//} //}
func (rc *cartItemController) CartItemView(c *gin.Context) { func (rc *cartItemController) CartItemView(c *gin.Context) {
//sessionId := GetSessionId(c) sessionId := GetSessionId(c)
//cartItems, err := repositories.CartItems.GetAllBySession(sessionId) cartItems, err := repositories.CartItems.GetAllBySession(sessionId)
cartItems, err := repositories.CartItems.GetAll() //cartItems, err := repositories.CartItems.GetAll()
if err != nil { if err != nil {
c.HTML(http.StatusBadRequest, "cart.html", gin.H{ "data": gin.H{ "error": err } }) c.HTML(http.StatusBadRequest, "cart.html", gin.H{"data": gin.H{"error": err}})
} }
priceTotal := 0.0 priceTotal := 0.0
@@ -267,9 +264,9 @@ func (rc *cartItemController) CartItemView(c *gin.Context) {
fmt.Println("PRICE TOTAL", priceTotal) fmt.Println("PRICE TOTAL", priceTotal)
data := CreateSessionData(c, gin.H{ data := CreateSessionData(c, gin.H{
"cartItems": cartItems, "cartItems": cartItems,
"priceTotal": fmt.Sprintf("%.2f", priceTotal), //round 2 decimals "priceTotal": fmt.Sprintf("%.2f", priceTotal), //round 2 decimals
"shipping": GetShippingMethods(), "shipping": models.GetShippingMethods(),
}) })
c.HTML(http.StatusOK, "cart.html", data) c.HTML(http.StatusOK, "cart.html", data)
@@ -280,14 +277,14 @@ func (rc *cartItemController) AddItemHandler(c *gin.Context) {
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
c.HTML(http.StatusBadRequest, "error.html", gin.H{ "error": err }) c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return return
} }
_, err = repositories.CartItems.Create(cartItem) _, err = repositories.CartItems.Create(cartItem)
if err != nil { if err != nil {
data := CreateSessionData(c, gin.H{ data := CreateSessionData(c, gin.H{
"error": err, "error": err,
"success": "", "success": "",
}) })
@@ -304,7 +301,7 @@ func (rc *cartItemController) DeleteItemHandler(c *gin.Context) {
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
data := CreateSessionData(c, gin.H{ data := CreateSessionData(c, gin.H{
"error": err, "error": err,
"success": "", "success": "",
}) })
@@ -326,6 +323,27 @@ func (rc *cartItemController) EditItemHandler(c *gin.Context) {
action := c.PostForm("action") 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" { if action == "increase" {
cartItem.Quantity += 1 cartItem.Quantity += 1
} }
@@ -350,8 +368,13 @@ func (rc *cartItemController) EditItemHandler(c *gin.Context) {
func (rc *cartItemController) CheckoutView(c *gin.Context) { func (rc *cartItemController) CheckoutView(c *gin.Context) {
shippingMethod := c.Query("shippingMethod") shippingMethod := c.Query("shippingMethod")
if shippingMethod == "" {
rc.CartItemView(c)
return
}
c.HTML(http.StatusOK, "checkout.html", gin.H{ c.HTML(http.StatusOK, "checkout.html", gin.H{
"askAddress": (shippingMethod != "pickup"), "askAddress": (shippingMethod != "pickup"),
"shippingMethod": shippingMethod, "shippingMethod": shippingMethod,
}) })
} }
@@ -361,62 +384,222 @@ func (rc *cartItemController) CheckoutHandler(c *gin.Context) {
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
c.HTML(http.StatusBadRequest, "error.html", gin.H{ "error": err }) c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return return
} }
//TODO: should update or create here, in case user edited addressfield existingOrder, err := repositories.Orders.GetBySession(order.SessionId)
_, err = repositories.Orders.Create(order)
if err != nil {
data := CreateSessionData(c, gin.H{
"error": err,
"success": "",
})
c.HTML(http.StatusOK, "cart.html", data) if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Println("Creating Order")
} createdOrder, err := repositories.Orders.Create(order)
if err != nil {
data := CreateSessionData(c, gin.H{
"error": err,
"success": "",
})
var shipping models.Shipping c.HTML(http.StatusOK, "error.html", data)
for _, shippingMethod := range GetShippingMethods() { return
if shippingMethod.Id == order.Shipping { }
shipping = shippingMethod
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)
} }
} }
priceProducts := 0.0 shipping, err := models.GetShippingMethod(order.Shipping)
for _, cartItem := range order.CartItems { if err != nil {
priceProducts += (float64(cartItem.Quantity) * cartItem.ItemVariant.Price) data := CreateSessionData(c, gin.H{
"error": err,
"success": "",
})
c.HTML(http.StatusOK, "error.html", data)
return
} }
priceTotal := priceProducts + shipping.Price 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{ data := CreateSessionData(c, gin.H{
"error": "", "error": "",
"success": "", "success": "",
"order": order, "order": order,
"askAddress": (order.Shipping != "pickup"), "askAddress": (order.Shipping != "pickup"),
"isPreview": true, "isPreview": true,
"shipping": shipping, "shipping": shipping,
"priceProducts": fmt.Sprintf("%.2f", priceProducts), //round 2 decimals "priceProducts": fmt.Sprintf("%.2f", priceProducts), //round 2 decimals
"priceTotal": fmt.Sprintf("%.2f", priceTotal), //round 2 decimals "priceTotal": fmt.Sprintf("%.2f", priceTotal), //round 2 decimals
}) })
fmt.Println(order) fmt.Println(order)
c.HTML(http.StatusOK, "order.html", data) c.HTML(http.StatusOK, "orderpreview.html", data)
} }
func (rc *cartItemController) OrderView(c *gin.Context) { func (rc *cartItemController) OrderView(c *gin.Context) {
shippingMethod := c.Query("shippingMethod") orderToken := c.Param("token")
c.HTML(http.StatusOK, "checkout.html", gin.H{ order, err := repositories.Orders.GetByToken(orderToken)
"askAddress": (shippingMethod != "pickup"), if err != nil {
"shippingMethod": shippingMethod, 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) { func (rc *cartItemController) OrderHandler(c *gin.Context) {
//get order by session id confirmation := c.PostForm("confirm-order")
//generate token, preview payment info
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,15 +3,15 @@ package controllers
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strconv"
"path/filepath"
"os/exec" "os/exec"
"path/filepath"
"strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"example.com/gin/test/models" "git.dynamicdiscord.de/kalipso/zineshop/models"
//"example.com/gin/test/services" //"git.dynamicdiscord.de/kalipso/zineshop/services"
"example.com/gin/test/repositories" "git.dynamicdiscord.de/kalipso/zineshop/repositories"
) )
type CRUDController interface { type CRUDController interface {
@@ -27,6 +27,8 @@ type ShopItemController interface {
ShopItemView(*gin.Context) ShopItemView(*gin.Context)
AddItemView(*gin.Context) AddItemView(*gin.Context)
AddItemHandler(*gin.Context) AddItemHandler(*gin.Context)
AddItemsView(*gin.Context)
AddItemsHandler(*gin.Context)
CreateTag(*gin.Context) CreateTag(*gin.Context)
GetAllTags(*gin.Context) GetAllTags(*gin.Context)
EditItemView(*gin.Context) EditItemView(*gin.Context)
@@ -38,7 +40,7 @@ type ShopItemController interface {
AddTagHandler(*gin.Context) AddTagHandler(*gin.Context)
} }
type shopItemController struct {} type shopItemController struct{}
func NewShopItemController() ShopItemController { func NewShopItemController() ShopItemController {
return &shopItemController{} return &shopItemController{}
@@ -77,12 +79,13 @@ func (rc *shopItemController) NewShopItemFromForm(ctx *gin.Context) (models.Shop
tagIds := ctx.PostFormArray("tags[]") tagIds := ctx.PostFormArray("tags[]")
image, err := ctx.FormFile("image") image, err := ctx.FormFile("image")
dstImage := defaultImagePath dstImage := defaultImagePath
printMode := ctx.PostForm("print-mode")
if err == nil { if err == nil {
dstImage = filepath.Join("static/uploads", image.Filename) dstImage = filepath.Join("static/uploads", image.Filename)
if err := ctx.SaveUploadedFile(image, dstImage); err != nil { if err := ctx.SaveUploadedFile(image, dstImage); err != nil {
return models.ShopItem{}, fmt.Errorf("Could not save image") return models.ShopItem{}, fmt.Errorf("Could not save image")
} }
} }
dstPdf := "" dstPdf := ""
@@ -90,19 +93,22 @@ func (rc *shopItemController) NewShopItemFromForm(ctx *gin.Context) (models.Shop
if err == nil { if err == nil {
dstPdf = filepath.Join("static/uploads", pdf.Filename) dstPdf = filepath.Join("static/uploads", pdf.Filename)
if err := ctx.SaveUploadedFile(pdf, dstPdf); err != nil { fmt.Println("Saving pdf at ", dstPdf)
if err := ctx.SaveUploadedFile(pdf, dstPdf); err != nil {
return models.ShopItem{}, fmt.Errorf("Could not save PDF") return models.ShopItem{}, fmt.Errorf("Could not save PDF")
} }
if dstImage == defaultImagePath { if dstImage == defaultImagePath {
dstImage = dstPdf + ".preview.png" dstImage = dstPdf + ".preview.png"
cmd := exec.Command("pdftoppm", "-png", "-singlefile", dstPdf, dstPdf + ".preview") cmd := exec.Command("pdftoppm", "-png", "-singlefile", dstPdf, dstPdf+".preview")
_, err := cmd.Output() _, err := cmd.Output()
if err != nil { if err != nil {
fmt.Println("Error during pdftoppm: ", err.Error()) fmt.Println("Error during pdftoppm: ", err.Error())
} }
} }
} else {
fmt.Println(err)
} }
if name == "" || description == "" { if name == "" || description == "" {
@@ -128,28 +134,31 @@ func (rc *shopItemController) NewShopItemFromForm(ctx *gin.Context) (models.Shop
} }
price, err := strconv.ParseFloat(variantValues[idx], 64) price, err := strconv.ParseFloat(variantValues[idx], 64)
if err != nil { if err != nil {
return models.ShopItem{}, fmt.Errorf("Could not variant parse price") return models.ShopItem{}, fmt.Errorf("Could not variant parse price")
} }
variants = append(variants, models.ItemVariant{ variants = append(variants, models.ItemVariant{
Name: variantNames[idx], Name: variantNames[idx],
Price: price, Price: price,
}) })
} }
shopItem := models.ShopItem{ shopItem := models.ShopItem{
Name: name, Name: name,
Abstract: abstract, Abstract: abstract,
Description: description, Description: description,
Category: category, Category: category,
IsPublic: true, IsPublic: true,
BasePrice: rc.GetBasePrice(variants), BasePrice: rc.GetBasePrice(variants),
Image: dstImage, Image: dstImage,
Pdf: dstPdf, Pdf: dstPdf,
Variants: variants, Variants: variants,
PrintMode: printMode,
} }
fmt.Println("Creating Shopitem: ", shopItem)
for _, tagId := range tagIds { for _, tagId := range tagIds {
tag, err := repositories.Tags.GetById(tagId) tag, err := repositories.Tags.GetById(tagId)
@@ -198,7 +207,6 @@ func (rc *shopItemController) Create(c *gin.Context) {
ReplyOK(c, "shopItem was created") ReplyOK(c, "shopItem was created")
} }
func (rc *shopItemController) Update(c *gin.Context) { func (rc *shopItemController) Update(c *gin.Context) {
shopItemId, err := strconv.Atoi(c.Param("id")) shopItemId, err := strconv.Atoi(c.Param("id"))
@@ -225,7 +233,7 @@ func (rc *shopItemController) Update(c *gin.Context) {
ReplyOK(c, "shopItem was updated") ReplyOK(c, "shopItem was updated")
} }
//TODO: delete associated cartitems // TODO: delete associated cartitems
func (rc *shopItemController) Delete(c *gin.Context) { func (rc *shopItemController) Delete(c *gin.Context) {
err := repositories.ShopItems.DeleteById(c.Param("id")) err := repositories.ShopItems.DeleteById(c.Param("id"))
@@ -241,19 +249,19 @@ func (rc *shopItemController) ShopItemView(c *gin.Context) {
shopItem, err := repositories.ShopItems.GetById(c.Param("id")) shopItem, err := repositories.ShopItems.GetById(c.Param("id"))
if err != nil { 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 //TODO: get tags by item
tags, err := repositories.Tags.GetAll() tags, err := repositories.Tags.GetAll()
if err != nil { 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{ data := CreateSessionData(c, gin.H{
"shopItem": shopItem, "shopItem": shopItem,
"tags": tags, "tags": tags,
}) })
if err != nil { if err != nil {
@@ -267,25 +275,24 @@ func (rc *shopItemController) AddItemView(c *gin.Context) {
tags, err := repositories.Tags.GetAll() tags, err := repositories.Tags.GetAll()
if err != nil { 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{ data := CreateSessionData(c, gin.H{
"error": "", "error": "",
"success": "", "success": "",
"tags": tags, "tags": tags,
}) })
c.HTML(http.StatusOK, "additem.html", data) c.HTML(http.StatusOK, "additem.html", data)
} }
func (rc *shopItemController) AddItemHandler(c *gin.Context) { func (rc *shopItemController) AddItemHandler(c *gin.Context) {
errorHandler := func(err error, tags []models.Tag) { errorHandler := func(err error, tags []models.Tag) {
data := CreateSessionData(c, gin.H{ data := CreateSessionData(c, gin.H{
"error": err, "error": err,
"success": "", "success": "",
"tags": tags, "tags": tags,
}) })
c.HTML(http.StatusOK, "additem.html", data) c.HTML(http.StatusOK, "additem.html", data)
@@ -310,71 +317,172 @@ func (rc *shopItemController) AddItemHandler(c *gin.Context) {
} }
data := CreateSessionData(c, gin.H{ data := CreateSessionData(c, gin.H{
"error": "", "error": "",
"success": fmt.Sprintf("Item '%s' Registered", shopItem.Name), "success": fmt.Sprintf("Item '%s' Registered", shopItem.Name),
"tags": tags, "tags": tags,
}) })
c.HTML(http.StatusOK, "additem.html", data) 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) { func (rc *shopItemController) EditItemView(c *gin.Context) {
shopItem, err := repositories.ShopItems.GetById(c.Param("id")) shopItem, err := repositories.ShopItems.GetById(c.Param("id"))
tags, err := repositories.Tags.GetAll() tags, err := repositories.Tags.GetAll()
if err != nil { 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) fmt.Println(shopItem)
data := CreateSessionData(c, gin.H{ data := CreateSessionData(c, gin.H{
"error": "", "error": "",
"success": "", "success": "",
"shopItem": shopItem, "shopItem": shopItem,
"tags": tags, "tags": tags,
}) })
c.HTML(http.StatusOK, "edititem.html", data) c.HTML(http.StatusOK, "edititem.html", data)
} }
func (rc *shopItemController) EditItemHandler(c *gin.Context) { func (rc *shopItemController) EditItemHandler(c *gin.Context) {
shopItem, err := rc.NewShopItemFromForm(c) shopItem, err := rc.NewShopItemFromForm(c)
if err != nil { if err != nil {
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{ "error": err }) c.HTML(http.StatusBadRequest, "edititem.html", gin.H{"error": err})
return return
} }
newShopItem, err := repositories.ShopItems.GetById(c.Param("id")) newShopItem, err := repositories.ShopItems.GetById(c.Param("id"))
if err != nil { if err != nil {
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{ "error": err }) c.HTML(http.StatusBadRequest, "edititem.html", gin.H{"error": err})
return return
} }
newShopItem.Name = shopItem.Name newShopItem.Name = shopItem.Name
newShopItem.Abstract = shopItem.Abstract newShopItem.Abstract = shopItem.Abstract
newShopItem.Description = shopItem.Description newShopItem.Description = shopItem.Description
newShopItem.Category = shopItem.Category
newShopItem.Variants = shopItem.Variants
newShopItem.BasePrice = shopItem.BasePrice newShopItem.BasePrice = shopItem.BasePrice
newShopItem.IsPublic = shopItem.IsPublic newShopItem.IsPublic = shopItem.IsPublic
newShopItem.Tags = shopItem.Tags
newShopItem.Variants = shopItem.Variants 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() tags, err := repositories.Tags.GetAll()
if err != nil { if err != nil {
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{ "error": err }) c.HTML(http.StatusBadRequest, "edititem.html", gin.H{"error": err})
return return
} }
_, err = repositories.ShopItems.Update(newShopItem) _, err = repositories.ShopItems.Update(newShopItem)
if err != nil { if err != nil {
data := CreateSessionData(c, gin.H{ data := CreateSessionData(c, gin.H{
"error": err, "error": err,
"success": "", "success": "",
"tags": tags, "tags": tags,
}) })
c.HTML(http.StatusOK, "edititem.html", data) c.HTML(http.StatusOK, "edititem.html", data)
@@ -382,9 +490,9 @@ func (rc *shopItemController) EditItemHandler(c *gin.Context) {
} }
data := CreateSessionData(c, gin.H{ data := CreateSessionData(c, gin.H{
"error": "", "error": "",
"success": fmt.Sprintf("Item '%s' Updated", newShopItem.Name), "success": fmt.Sprintf("Item '%s' Updated", newShopItem.Name),
"tags": tags, "tags": tags,
}) })
c.HTML(http.StatusOK, "edititem.html", data) c.HTML(http.StatusOK, "edititem.html", data)
@@ -395,28 +503,27 @@ func (rc *shopItemController) DeleteItemView(c *gin.Context) {
tags, err := repositories.Tags.GetAll() tags, err := repositories.Tags.GetAll()
if err != nil { 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) fmt.Println(shopItem)
data := CreateSessionData(c, gin.H{ data := CreateSessionData(c, gin.H{
"error": "", "error": "",
"success": "", "success": "",
"shopItem": shopItem, "shopItem": shopItem,
"tags": tags, "tags": tags,
}) })
c.HTML(http.StatusOK, "deleteitem.html", data) c.HTML(http.StatusOK, "deleteitem.html", data)
} }
func (rc *shopItemController) DeleteItemHandler(c *gin.Context) { func (rc *shopItemController) DeleteItemHandler(c *gin.Context) {
err := repositories.ShopItems.DeleteById(c.Param("id")) err := repositories.ShopItems.DeleteById(c.Param("id"))
if err != nil { if err != nil {
data := CreateSessionData(c, gin.H{ data := CreateSessionData(c, gin.H{
"error": err, "error": err,
"success": "", "success": "",
}) })
@@ -427,7 +534,7 @@ func (rc *shopItemController) DeleteItemHandler(c *gin.Context) {
fmt.Println(len(shopItems)) fmt.Println(len(shopItems))
data := CreateSessionData(c, gin.H{ data := CreateSessionData(c, gin.H{
"title": "shopItem Page", "title": "shopItem Page",
"shopItems": shopItems, "shopItems": shopItems,
}) })
@@ -438,23 +545,25 @@ func (rc *shopItemController) DeleteItemHandler(c *gin.Context) {
func (rc *shopItemController) TagHandler(ctx *gin.Context) { func (rc *shopItemController) TagHandler(ctx *gin.Context) {
name := ctx.PostForm("name") name := ctx.PostForm("name")
color := ctx.PostForm("color")
action := ctx.PostForm("action") action := ctx.PostForm("action")
tag, err := repositories.Tags.GetById(ctx.Param("id")) tag, err := repositories.Tags.GetById(ctx.Param("id"))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
ctx.HTML(http.StatusBadRequest, "tagview.html", gin.H{ "error": err }) ctx.HTML(http.StatusBadRequest, "tagview.html", gin.H{"error": err})
return return
} }
if action == "update" { if action == "update" {
tag.Name = name tag.Name = name
tag.Color = color
tag, err = repositories.Tags.Update(tag) tag, err = repositories.Tags.Update(tag)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
ctx.HTML(http.StatusBadRequest, "tagview.html", gin.H{ "error": err }) ctx.HTML(http.StatusBadRequest, "tagview.html", gin.H{"error": err})
return return
} }
} }
@@ -471,14 +580,14 @@ func (rc *shopItemController) AddTagHandler(c *gin.Context) {
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
c.HTML(http.StatusBadRequest, "tagview.html", gin.H{ "error": err }) c.HTML(http.StatusBadRequest, "tagview.html", gin.H{"error": err})
return return
} }
_, err = repositories.Tags.Create(tag) _, err = repositories.Tags.Create(tag)
if err != nil { if err != nil {
data := CreateSessionData(c, gin.H{ data := CreateSessionData(c, gin.H{
"error": err, "error": err,
"success": "", "success": "",
}) })
@@ -493,7 +602,7 @@ func (rc *shopItemController) TagView(c *gin.Context) {
tags, err := repositories.Tags.GetAll() tags, err := repositories.Tags.GetAll()
if err != nil { 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{ data := CreateSessionData(c, gin.H{
@@ -544,7 +653,7 @@ func (rc *shopItemController) GetAllTags(c *gin.Context) {
} }
func ReplyError(ctx *gin.Context, err error) { 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) { func ReplyOK(ctx *gin.Context, message any) {

View File

@@ -1,30 +1,29 @@
package controllers package controllers
import( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"math/rand"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm"
"example.com/gin/test/models" "git.dynamicdiscord.de/kalipso/zineshop/models"
"example.com/gin/test/repositories" "git.dynamicdiscord.de/kalipso/zineshop/repositories"
"example.com/gin/test/services" "git.dynamicdiscord.de/kalipso/zineshop/services"
) )
type UserController struct{}
type UserController struct {}
func NewUserController() UserController { func NewUserController() UserController {
return UserController{} return UserController{}
} }
func (uc *UserController) Register(c *gin.Context) { func (uc *UserController) Register(c *gin.Context) {
//Get the email/passwd off req body //Get the email/passwd off req body
var body struct { var body struct {
Name string Name string
Email string Email string
Password string Password string
} }
@@ -38,7 +37,7 @@ func (uc *UserController) Register(c *gin.Context) {
return 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 { if err != nil {
fmt.Println("Error: ", err) fmt.Println("Error: ", err)
@@ -53,11 +52,10 @@ func (uc *UserController) Register(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{}) c.JSON(http.StatusOK, gin.H{})
} }
func (uc *UserController) Login(c *gin.Context) { func (uc *UserController) Login(c *gin.Context) {
//Get the email/passwd off req body //Get the email/passwd off req body
var body struct { var body struct {
Email string Email string
Password string Password string
} }
@@ -83,7 +81,7 @@ func (uc *UserController) Login(c *gin.Context) {
// send it back // send it back
c.SetSameSite(http.SameSiteLaxMode) 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{}) 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)) c.HTML(http.StatusOK, "login.html", CreateSessionData(c, data))
} }
func (rc *UserController) LoginHandler(c *gin.Context) { func (rc *UserController) LoginHandler(c *gin.Context) {
email := c.PostForm("email") email := c.PostForm("email")
password := c.PostForm("password") password := c.PostForm("password")
@@ -139,17 +136,18 @@ func (rc *UserController) LoginHandler(c *gin.Context) {
// send it back // send it back
//c.SetSameSite(http.SameSiteLaxMode) //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{})) c.HTML(http.StatusOK, "login.html", CreateSessionData(c, gin.H{}))
} }
func CreateSessionData(c *gin.Context, extra any) 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{ return gin.H{
"test": "HEllo World",
"loggedIn": exists, "loggedIn": exists,
"data": extra, "isAdmin": userImpl.IsAdmin,
"data": extra,
} }
} }
@@ -158,11 +156,45 @@ func (rc *UserController) RegisterHandler(c *gin.Context) {
email := c.PostForm("email") email := c.PostForm("email")
password := c.PostForm("password") 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{ 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": "", "success": "",
} }
@@ -170,8 +202,33 @@ func (rc *UserController) RegisterHandler(c *gin.Context) {
return 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{ data := gin.H{
"error": "", "error": "",
"success": "You successfully registered. Try logging in.", "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) { func (rc *UserController) RegisterView(c *gin.Context) {
data := gin.H{ 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": "", "success": "",
} }
@@ -191,7 +281,7 @@ func (rc *UserController) ResetView(c *gin.Context) {
shopItems, _ := repositories.ShopItems.GetAll() shopItems, _ := repositories.ShopItems.GetAll()
data := gin.H{ data := gin.H{
"title": "shopItem Page", "title": "shopItem Page",
"shopItems": shopItems, "shopItems": shopItems,
} }
@@ -202,88 +292,66 @@ func (rc *UserController) ResetHandler(c *gin.Context) {
shopItems, _ := repositories.ShopItems.GetAll() shopItems, _ := repositories.ShopItems.GetAll()
data := gin.H{ data := gin.H{
"title": "shopItem Page", "title": "shopItem Page",
"shopItems": shopItems, "shopItems": shopItems,
} }
c.HTML(http.StatusOK, "passwordreset.html", data) 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) { func (rc *UserController) MainView(c *gin.Context) {
shopItems, _ := repositories.ShopItems.GetAll() shopItems, _ := repositories.ShopItems.GetAll()
fmt.Println(len(shopItems)) fmt.Println(len(shopItems))
data := CreateSessionData(c, gin.H{ data := CreateSessionData(c, gin.H{
"title": "shopItem Page", "title": "shopItem Page",
"shopItems": shopItems, "shopItems": shopItems,
}) })
fmt.Println(data)
c.HTML(http.StatusOK, "index.html", data) c.HTML(http.StatusOK, "index.html", data)
} }
type booking struct { func (rc *UserController) TagView(c *gin.Context) {
Booked bool shopItems, _ := repositories.ShopItems.GetByTagId(c.Param("id"))
}
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 },
// },
// },
//},
}
data := CreateSessionData(c, gin.H{ data := CreateSessionData(c, gin.H{
"title": "shopItem Page", "title": "shopItem Page",
"bookings": bookings, "shopItems": shopItems,
"shopItemcount": len(bookings["head"].([]string)) + 1,
}) })
fmt.Println(data) c.HTML(http.StatusOK, "index.html", data)
c.HTML(http.StatusOK, "calendar.html", data)
} }
func (rc *UserController) Logout(c *gin.Context) { 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 go 1.23.3

102
main.go
View File

@@ -1,24 +1,25 @@
package main package main
import( import (
"os" "fmt"
"io" "io"
"net/http" "net/http"
"fmt" "os"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"example.com/gin/test/controllers" "git.dynamicdiscord.de/kalipso/zineshop/controllers"
"example.com/gin/test/repositories" "git.dynamicdiscord.de/kalipso/zineshop/middlewares"
"example.com/gin/test/middlewares" "git.dynamicdiscord.de/kalipso/zineshop/repositories"
) )
var( var (
shopItemController controllers.ShopItemController = controllers.NewShopItemController() shopItemController controllers.ShopItemController = controllers.NewShopItemController()
userController controllers.UserController = controllers.UserController{} userController controllers.UserController = controllers.UserController{}
cartItemController controllers.CartItemController = controllers.NewCartItemController() cartItemController controllers.CartItemController = controllers.NewCartItemController()
authValidator middlewares.AuthValidator = middlewares.AuthValidator{} printController controllers.PrintController = controllers.NewPrintController()
authValidator middlewares.AuthValidator = middlewares.AuthValidator{}
) )
func LoadEnvVariables() { func LoadEnvVariables() {
@@ -36,7 +37,7 @@ func setupLogOutput() {
func SetReply(ctx *gin.Context, err error, message any) { func SetReply(ctx *gin.Context, err error, message any) {
if err != nil { if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{ "error": err.Error() }) ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
} else { } else {
ctx.JSON(http.StatusOK, message) ctx.JSON(http.StatusOK, message)
} }
@@ -47,65 +48,58 @@ func main() {
repositories.InitRepositories() repositories.InitRepositories()
server := gin.New() server := gin.New()
server.Use(gin.Recovery()) server.Use(gin.Recovery())
server.Use(gin.Logger()) server.Use(gin.Logger())
server.Static("/static", "./static") server.Static("/static", os.Getenv("STATIC"))
server.LoadHTMLGlob("views/*.html") server.LoadHTMLGlob(fmt.Sprintf("%s/*.html", os.Getenv("VIEWS")))
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)
}
viewRoutes := server.Group("/", authValidator.OptionalAuth) viewRoutes := server.Group("/", authValidator.OptionalAuth)
{ {
viewRoutes.GET("/", userController.MainView) viewRoutes.GET("/", userController.MainView)
viewRoutes.GET("/shopitems/:id", shopItemController.ShopItemView) viewRoutes.GET("/shopitems/:id", shopItemController.ShopItemView)
viewRoutes.GET("/shopitems/:id/edit", authValidator.RequireAuth, shopItemController.EditItemView) viewRoutes.GET("/shopitems/:id/edit", authValidator.RequireAdmin, shopItemController.EditItemView)
viewRoutes.POST("/shopitems/:id/edit", authValidator.RequireAuth, shopItemController.EditItemHandler) viewRoutes.POST("/shopitems/:id/edit", authValidator.RequireAdmin, shopItemController.EditItemHandler)
viewRoutes.GET("/shopitems/:id/delete", authValidator.RequireAuth, shopItemController.DeleteItemView) viewRoutes.GET("/shopitems/:id/delete", authValidator.RequireAdmin, shopItemController.DeleteItemView)
viewRoutes.POST("/shopitems/:id/delete", authValidator.RequireAuth, shopItemController.DeleteItemHandler) viewRoutes.POST("/shopitems/:id/delete", authValidator.RequireAdmin, shopItemController.DeleteItemHandler)
viewRoutes.GET("/tags", authValidator.RequireAuth, shopItemController.TagView) viewRoutes.GET("/variant/:id/print", authValidator.RequireAdmin, printController.PrintVariantView)
viewRoutes.POST("/tags/:id", authValidator.RequireAuth, shopItemController.TagHandler) viewRoutes.GET("/cart/print", authValidator.RequireAdmin, printController.PrintCartView)
viewRoutes.POST("/tags", authValidator.RequireAuth, shopItemController.AddTagHandler) viewRoutes.POST("/print", authValidator.RequireAdmin, printController.PrintHandler)
viewRoutes.GET("/cart", cartItemController.CartItemView)
viewRoutes.POST("/cart", cartItemController.AddItemHandler) viewRoutes.GET("/tags", authValidator.RequireAdmin, shopItemController.TagView)
viewRoutes.POST("/cart/delete", cartItemController.DeleteItemHandler) viewRoutes.POST("/tags/:id", authValidator.RequireAdmin, shopItemController.TagHandler)
viewRoutes.POST("/cart/edit", cartItemController.EditItemHandler) viewRoutes.GET("/tags/:id", userController.TagView)
viewRoutes.GET("/checkout", cartItemController.CheckoutView) viewRoutes.POST("/tags", authValidator.RequireAdmin, shopItemController.AddTagHandler)
viewRoutes.POST("/checkout", cartItemController.CheckoutHandler) viewRoutes.GET("/cart", authValidator.RequireAuth, cartItemController.CartItemView)
viewRoutes.GET("/order", cartItemController.OrderView) viewRoutes.POST("/cart", authValidator.RequireAuth, cartItemController.AddItemHandler)
viewRoutes.POST("/order", cartItemController.OrderHandler) 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 //write middleware that redirects to homescreen on register/login/reset for logged in users
viewRoutes.GET("/login", userController.LoginView) viewRoutes.GET("/login", userController.LoginView)
viewRoutes.GET("/logout", userController.Logout) viewRoutes.GET("/logout", userController.Logout)
viewRoutes.GET("/register", userController.RegisterView) viewRoutes.GET("/register", userController.InitAdmin)
viewRoutes.GET("/passwordreset", userController.ResetView) viewRoutes.GET("/register/:token", userController.RegisterView)
viewRoutes.GET("/additem", authValidator.RequireAuth, shopItemController.AddItemView) 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("/login", userController.LoginHandler)
viewRoutes.POST("/register", userController.RegisterHandler) viewRoutes.POST("/register", userController.RegisterHandler)
viewRoutes.POST("/additem", authValidator.RequireAuth, shopItemController.AddItemHandler) viewRoutes.POST("/additem", authValidator.RequireAdmin, shopItemController.AddItemHandler)
viewRoutes.POST("/passwordreset", userController.ResetHandler) 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 package middlewares
import( import (
"os"
"fmt" "fmt"
"os"
"time" "time"
//"strconv" //"strconv"
"net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"net/http"
//"example.com/gin/test/models" //"git.dynamicdiscord.de/kalipso/zineshop/models"
"example.com/gin/test/repositories" "git.dynamicdiscord.de/kalipso/zineshop/repositories"
) )
type AuthValidator struct { type AuthValidator struct {
@@ -98,6 +98,63 @@ func (av *AuthValidator) RequireAuth(c *gin.Context) {
c.AbortWithStatus(http.StatusUnauthorized) 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)
}
func (av *AuthValidator) OptionalAuth(c *gin.Context) { func (av *AuthValidator) OptionalAuth(c *gin.Context) {
defer c.Next() defer c.Next()

View File

@@ -1,59 +1,161 @@
package models package models
import ( import (
"fmt"
"gorm.io/gorm" "gorm.io/gorm"
) )
type OrderStatus string type OrderStatus string
const ( const (
Received OrderStatus = "Received" AwaitingConfirmation OrderStatus = "AwaitingConfirmation"
AwaitingPayment OrderStatus = "AwaitingPayment" Received OrderStatus = "Received"
Payed OrderStatus = "Payed" AwaitingPayment OrderStatus = "AwaitingPayment"
ReadyForPickup OrderStatus = "ReadyForPickup" Payed OrderStatus = "Payed"
Shipped OrderStatus = "Shipped" ReadyForPickup OrderStatus = "ReadyForPickup"
Cancelled OrderStatus = "Cancelled" Shipped OrderStatus = "Shipped"
Cancelled OrderStatus = "Cancelled"
) )
type AddressInfo struct { type AddressInfo struct {
FirstName string `json:"firstname"` FirstName string `json:"firstname"`
LastName string `json:"lastname"` LastName string `json:"lastname"`
Address string `json:"address"` Address string `json:"address"`
PostalCode string `json:"postalcode"` PostalCode string `json:"postalcode"`
City string `json:"city"` City string `json:"city"`
Country string `json:"country"` Country string `json:"country"`
} }
type Shipping struct { type Shipping struct {
Id string `json:"id"` Id string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Price float64 `json:"price"` 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 { type Order struct {
gorm.Model gorm.Model
SessionId string `json:"sessionid" binding:"required" gorm:"not null"` SessionId string `json:"sessionid" binding:"required" gorm:"not null"`
Status OrderStatus `json:"status"` Status OrderStatus `json:"status"`
Token string `json:"token" binding:"required" gorm:"not null"` Token string `json:"token" binding:"required" gorm:"not null"`
Email string `json:"email"` Email string `json:"email"`
Comment string `json:"comment"` Comment string `json:"comment"`
FirstName string `json:"firstname"` FirstName string `json:"firstname"`
LastName string `json:"lastname"` LastName string `json:"lastname"`
Address string `json:"address"` Address string `json:"address"`
PostalCode string `json:"postalcode"` PostalCode string `json:"postalcode"`
City string `json:"city"` City string `json:"city"`
Country string `json:"country"` Country string `json:"country"`
Shipping string `json:"shipping"` Shipping string `json:"shipping"`
CartItems []CartItem `json:"cartitems" gorm:"foreignKey:OrderID"` 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 { type CartItem struct {
gorm.Model gorm.Model
SessionId string `json:"sessionid" binding:"required" gorm:"not null"` SessionId string `json:"sessionid" binding:"required" gorm:"not null"`
ShopItemId uint ShopItemId uint
ShopItem ShopItem `json:"shopitem" gorm:"foreignKey:ShopItemId"` //gorm one2one ShopItem ShopItem `json:"shopitem" gorm:"foreignKey:ShopItemId"` //gorm one2one
ItemVariantId uint ItemVariantId uint
ItemVariant ItemVariant `json:"itemvariant" gorm:"foreignKey:ItemVariantId"` //gorm one2one ItemVariant ItemVariant `json:"itemvariant" gorm:"foreignKey:ItemVariantId"` //gorm one2one
Quantity int `json:"quantity" binding:"required"` Quantity int `json:"quantity" binding:"required"`
OrderID uint 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

@@ -7,13 +7,16 @@ import (
/* /*
Sticker Sticker
- name, abstr, descr, price, tag - name, abstr, descr, price, tag
Poster Poster
- name, abstr, descr, price bw/colored, tag - name, abstr, descr, price bw/colored, tag
Zines Zines
- name, abstr, descr, price bw/colored/coloredcoveronly, tag - name, abstr, descr, price bw/colored/coloredcoveronly, tag
Books Books
- name, abstr, descr, price, tag - name, abstr, descr, price, tag
*/ */
type Category string type Category string
@@ -31,22 +34,23 @@ func ParseCategory(s string) (c Category, err error) {
type ItemVariant struct { type ItemVariant struct {
gorm.Model gorm.Model
Name string `json:"name" gorm:"not null"` Name string `json:"name" gorm:"not null"`
Price float64 `json:"price" gorm:"not null"` Price float64 `json:"price" gorm:"not null"`
InStock bool `json:"inStock" gorm:"default:true"` InStock bool `json:"inStock" gorm:"default:true"`
ShopItemID uint ShopItemID uint
} }
type ShopItem struct { type ShopItem struct {
gorm.Model gorm.Model
Name string `json:"name" binding:"required" gorm:"unique;not null"` Name string `json:"name" binding:"required" gorm:"unique;not null"`
Abstract string `json:"Abstract" binding:"required"` Abstract string `json:"Abstract" binding:"required"`
Description string `json:"description" binding:"required"` Description string `json:"description" binding:"required"`
Category Category `json:"category"` Category Category `json:"category"`
Variants []ItemVariant `json:"variant"` Variants []ItemVariant `json:"variant"`
BasePrice float64 `json:"basePrice"` BasePrice float64 `json:"basePrice"`
IsPublic bool `json:"isPublic" gorm:"default:true"` IsPublic bool `json:"isPublic" gorm:"default:true"`
Tags []Tag `gorm:"many2many:item_tags;"` Tags []Tag `gorm:"many2many:item_tags;"`
Image string Image string
Pdf string Pdf string
PrintMode string `json:"printMode" gorm:"default:CreateBooklet"`
} }

View File

@@ -2,22 +2,51 @@ package models
import ( import (
"fmt" "fmt"
"gorm.io/gorm"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm"
"math/rand"
) )
type Tag struct { type Tag struct {
gorm.Model 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;"` ShopItems []ShopItem `gorm:"many2many:item_tags;"`
} }
func NewTag(ctx *gin.Context) (Tag, error) { 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") name := ctx.PostForm("name")
// Convert the price string to float64 // Convert the price string to float64
tag := Tag{ tag := Tag{
Name: name, Name: name,
Color: colors[n],
} }
if name == "" { if name == "" {
@@ -27,7 +56,6 @@ func NewTag(ctx *gin.Context) (Tag, error) {
return tag, nil return tag, nil
} }
func NewTagByJson(ctx *gin.Context) (Tag, error) { func NewTagByJson(ctx *gin.Context) (Tag, error) {
var tag Tag var tag Tag
err := ctx.ShouldBindJSON(&tag) err := ctx.ShouldBindJSON(&tag)

View File

@@ -4,9 +4,15 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
type RegisterToken struct {
gorm.Model
Token string `json:"token" binding:"required" gorm:"unique;not null"`
}
type User struct { type User struct {
gorm.Model 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"` 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

@@ -1,19 +1,21 @@
package repositories package repositories
import( import (
"strconv"
"gorm.io/gorm" "gorm.io/gorm"
"strconv"
"example.com/gin/test/models" "git.dynamicdiscord.de/kalipso/zineshop/models"
) )
type OrderRepository interface { type OrderRepository interface {
Create(models.Order) (models.Order, error) Create(models.Order) (models.Order, error)
GetAll() ([]models.Order, error) GetAll() ([]models.Order, error)
GetById(string) (models.Order, error) GetById(string) (models.Order, error)
GetAllBySession(string) ([]models.Order, error) GetBySession(string) (models.Order, error)
GetByToken(string) (models.Order, error)
Update(models.Order) (models.Order, error) Update(models.Order) (models.Order, error)
DeleteById(string) (error) DeleteById(string) error
DeleteByToken(string) error
} }
type GORMOrderRepository struct { type GORMOrderRepository struct {
@@ -60,15 +62,21 @@ func (t *GORMOrderRepository) GetById(id string) (models.Order, error) {
return order, nil return order, nil
} }
func (r *GORMOrderRepository) GetBySession(sessionId string) (models.Order, error) {
func (r *GORMOrderRepository) GetAllBySession(sessionId string) ([]models.Order, error) { var orders models.Order
var orders []models.Order result := r.DB.Preload("CartItems").Preload("CartItems.ShopItem").Preload("CartItems.ItemVariant").Where("session_id = ?", sessionId).First(&orders)
result := r.DB.Preload("CartItems").Where("session_id = ?", sessionId).Find(&orders)
return orders, result.Error 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) { func (r *GORMOrderRepository) Update(order models.Order) (models.Order, error) {
result := r.DB.Save(&order) result := r.DB.Save(&order)
if result.Error != nil { if result.Error != nil {
@@ -88,3 +96,8 @@ func (r *GORMOrderRepository) DeleteById(id string) error {
result := r.DB.Delete(&models.Order{}, orderId) result := r.DB.Delete(&models.Order{}, orderId)
return result.Error return result.Error
} }
func (r *GORMOrderRepository) DeleteByToken(token string) error {
result := r.DB.Where("token = ?", token).Delete(&models.Order{})
return result.Error
}

View File

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

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

View File

@@ -1,10 +1,10 @@
package repositories package repositories
import( import (
"strconv"
"gorm.io/gorm" "gorm.io/gorm"
"strconv"
"example.com/gin/test/models" "git.dynamicdiscord.de/kalipso/zineshop/models"
) )
type ShopItemRepository interface { type ShopItemRepository interface {
@@ -12,8 +12,8 @@ type ShopItemRepository interface {
GetAll() ([]models.ShopItem, error) GetAll() ([]models.ShopItem, error)
GetAllPublic() ([]models.ShopItem, error) GetAllPublic() ([]models.ShopItem, error)
GetById(string) (models.ShopItem, error) GetById(string) (models.ShopItem, error)
GetByTagId(string) ([]models.ShopItem, error)
GetVariantById(string) (models.ItemVariant, error) GetVariantById(string) (models.ItemVariant, error)
//GetByTagId(string) ([]models.ShopItem, error)
Update(models.ShopItem) (models.ShopItem, error) Update(models.ShopItem) (models.ShopItem, error)
DeleteById(string) error DeleteById(string) error
} }
@@ -68,6 +68,23 @@ func (r *GORMShopItemRepository) GetById(id string) (models.ShopItem, error) {
return shopItem, nil 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) { func (r *GORMShopItemRepository) GetVariantById(id string) (models.ItemVariant, error) {
itemVariantId, err := strconv.Atoi(id) itemVariantId, err := strconv.Atoi(id)
@@ -85,7 +102,6 @@ func (r *GORMShopItemRepository) GetVariantById(id string) (models.ItemVariant,
return itemVariant, nil return itemVariant, nil
} }
func (r *GORMShopItemRepository) Update(shopItem models.ShopItem) (models.ShopItem, error) { func (r *GORMShopItemRepository) Update(shopItem models.ShopItem) (models.ShopItem, error) {
err := r.DB.Model(&shopItem).Association("Tags").Replace(shopItem.Tags) err := r.DB.Model(&shopItem).Association("Tags").Replace(shopItem.Tags)
if err != nil { if err != nil {

View File

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

View File

@@ -1,15 +1,16 @@
package repositories package repositories
import( import (
"gorm.io/gorm" "gorm.io/gorm"
"example.com/gin/test/models" "git.dynamicdiscord.de/kalipso/zineshop/models"
) )
type UserRepository interface { type UserRepository interface {
Create(models.User) (models.User, error) Create(models.User) (models.User, error)
GetByEmail(string) (models.User, error) GetByEmail(string) (models.User, error)
GetById(interface{}) (models.User, error) GetById(interface{}) (models.User, error)
IsEmpty() (bool, error)
} }
type GORMUserRepository struct { 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) result := u.DB.Create(&user)
if result.Error != nil { if result.Error != nil {
@@ -53,3 +54,18 @@ func (u *GORMUserRepository) GetById(id interface{}) (models.User, error) {
return user, nil 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( import(
"fmt" "fmt"
"example.com/gin/test/models" "git.dynamicdiscord.de/kalipso/zineshop/models"
"example.com/gin/test/repositories" "git.dynamicdiscord.de/kalipso/zineshop/repositories"
) )
var( var(

View File

@@ -1,23 +1,23 @@
package services package services
import( import (
"golang.org/x/crypto/bcrypt"
"os" "os"
"time" "time"
"golang.org/x/crypto/bcrypt"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"example.com/gin/test/models" "git.dynamicdiscord.de/kalipso/zineshop/models"
"example.com/gin/test/repositories" "git.dynamicdiscord.de/kalipso/zineshop/repositories"
) )
var( var (
Users UserService = UserService{} 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 pw
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10) 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 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) _, err = repositories.Users.Create(user)
if err != nil { if err != nil {
@@ -35,7 +35,7 @@ func (u *UserService) Register(name string, email string, password string) (mode
return user, nil return user, nil
} }
//return jwt tokenstring on success // return jwt tokenstring on success
func (u *UserService) Login(email string, password string) (string, error) { func (u *UserService) Login(email string, password string) (string, error) {
//lookup requested user //lookup requested user
user, err := repositories.Users.GetByEmail(email) 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 package main
import ( import (
"fmt" "fmt"
"net/http" "io/ioutil"
"io/ioutil" "net/http"
) )
func main() { func testFunc() {
url := "http://localhost:8080/test" url := "http://localhost:8080/test"
method := "GET" method := "GET"
client := &http.Client { client := &http.Client{}
} req, err := http.NewRequest(method, url, nil)
req, err := http.NewRequest(method, url, nil)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return return
} }
req.Header.Add("Authorization", "Basic dXNlcjpwYXNzd29yZA==") req.Header.Add("Authorization", "Basic dXNlcjpwYXNzd29yZA==")
res, err := client.Do(req) res, err := client.Do(req)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return return
} }
defer res.Body.Close() defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body) body, err := ioutil.ReadAll(res.Body)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return return
} }
fmt.Println(string(body)) 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="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"> <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> <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Add an Item</h2>
</div> </div>
@@ -123,6 +123,19 @@
</div> </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"> <p class="mt-10 text-center text-sm/6 text-red-500">
{{ .data.error }} {{ .data.error }}
</p> </p>

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="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"> <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 Tag</h2> <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Add Tag</h2>
</div> </div>

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" . }}

View File

@@ -33,28 +33,14 @@
</p> </p>
<form action="/cart/edit" method="POST"> <form action="/cart/edit" method="POST">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class="flex items-center gap-4">
<input type="hidden" id="{{ .ID }}" name="id" value="{{ .ID }}"> <input type="hidden" id="{{ .ID }}" name="id" value="{{ .ID }}">
<button type="submit" name="action" value="decrease" <div class="flex items-center gap-4">
class="group rounded-[50px] border border-gray-200 shadow-sm shadow-transparent p-2.5 flex items-center justify-center bg-white transition-all duration-500 hover:shadow-gray-200 hover:bg-gray-50 hover:border-gray-300 focus-within:outline-gray-300"> <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">
<svg class="stroke-gray-900 transition-all duration-500 group-hover:stroke-black" <button type="submit" name="action" value="setAmount"
width="18" height="19" viewBox="0 0 18 19" fill="none" 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">
xmlns="http://www.w3.org/2000/svg"> Set Amount
<path d="M4.5 9.5H13.5" stroke="" stroke-width="1.6" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</button> </button>
<p class="border border-gray-200 text-center rounded-full w-10 text-gray-900 font-semibold text-sm py-1.5 px-3 bg-gray-100 text-center">{{ .Quantity }}</p> </div>
<button type="submit" name="action" value="increase"
class="group rounded-[50px] border border-gray-200 shadow-sm shadow-transparent p-2.5 flex items-center justify-center bg-white transition-all duration-500 hover:shadow-gray-200 hover:bg-gray-50 hover:border-gray-300 focus-within:outline-gray-300">
<svg class="stroke-gray-900 transition-all duration-500 group-hover:stroke-black"
width="18" height="19" viewBox="0 0 18 19" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M3.75 9.5H14.25M9 14.75V4.25" stroke="" stroke-width="1.6"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
</div>
<h6 class="text-indigo-600 font-manrope font-bold text-2xl leading-9 text-right">{{ .Quantity}} x {{ .ItemVariant.Price }}€</h6> <h6 class="text-indigo-600 font-manrope font-bold text-2xl leading-9 text-right">{{ .Quantity}} x {{ .ItemVariant.Price }}€</h6>
</div> </div>
</form> </form>
@@ -88,7 +74,7 @@
<div class="max-lg:max-w-lg max-lg:mx-auto"> <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> <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" <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 ">Checkout</button> 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> </div>
</form> </form>

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="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"> <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="Logo">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Checkout</h2> <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Checkout</h2>
</div> </div>

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"> <div class="bg-gray-200 rounded m-4 p-4">
<h3 class="text-lg">{{ .data.shopItem.Name }}</h3> <h3 class="text-lg">{{ .data.shopItem.Name }}</h3>
<i class="text-xs">{{ .data.shopItem.Description }}</i> <i class="text-xs">{{ .data.shopItem.Description }}</i>
<p class="">Price: {{ .data.shopItem.Price }}</p> <p class="">Price: {{ .data.shopItem.BasePrice }}</p>
{{ if .loggedIn }} {{ if .isAdmin }}
<p class="mt-10 text-center text-sm/6 text-red-500"> <p class="mt-10 text-center text-sm/6 text-red-500">
Do you really want to delete this item?? 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="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"> <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> <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>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm"> <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> <div>
<label for="name" class="block text-sm/6 font-medium text-gray-900">Name</label> <label for="name" class="block text-sm/6 font-medium text-gray-900">Name</label>
<div class="mt-2"> <div class="mt-2">
@@ -55,14 +59,28 @@
--> -->
<div> <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"> <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>
<div class="mt-2"> <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> </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> <div>
<label class="block text-sm font-medium text-gray-900"> <label class="block text-sm font-medium text-gray-900">
Image 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" /> <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> </svg>
<div class="flex text-sm text-gray-600"> <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> <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> </label>
<p class="pl-1 text-gray-900">or drag and drop</p> <p class="pl-1 text-gray-900">or drag and drop</p>
</div> </div>
@@ -86,6 +104,39 @@
</div> </div>
</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"> <p class="mt-10 text-center text-sm/6 text-red-500">
{{ .data.error }} {{ .data.error }}
</p> </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" . }}

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>FreiRaum</title> <title>Zine Shop</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/output.css" rel="stylesheet"> <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="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="relative flex h-16 items-center justify-between">
<div class="flex flex-1 items-center"> <div class="flex flex-1 items-center">
<a href="/"><div class="flex shrink-0"> <a href="/"><div class="flex-shrink-0 w-full h-full">
<img class="h-8 w-auto" src="/static/img/circlea.png" alt="Your Company"> <img class="h-8 w-auto" src="/static/img/logo-white.png" alt="Your Company">
</div></a> </div></a>
<!-- <!--
{{ if .loggedIn }} {{ if .loggedIn }}
@@ -25,13 +25,23 @@
{{ end }} {{ end }}
--> -->
</div> </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"> <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="/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="/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> <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> </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"> <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="/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> <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> </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="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"> <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> <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Login to your account</h2>
</div> </div>

View File

@@ -1,9 +1,37 @@
{{ template "header.html" . }} {{ template "header.html" . }}
<section class="bg-white py-8 antialiased dark:bg-gray-900 md:py-16"> <section class="bg-white py-8 antialiased dark:bg-gray-900 md:py-16">
<form action="/checkout" method="POST" class="mx-auto max-w-screen-xl px-4 2xl:px-0"> <div class="mx-auto max-w-screen-xl px-4 2xl:px-0">
<div class="mx-auto max-w-3xl"> <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> <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"> <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> <h4 class="text-lg font-semibold text-gray-900 dark:text-white">Delivery information</h4>
@@ -23,8 +51,6 @@
<p><b>Comment:</b> {{ .data.order.Comment }}</p> <p><b>Comment:</b> {{ .data.order.Comment }}</p>
</dd> </dd>
</dl> </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>
<div class="mt-6 sm:mt-8"> <div class="mt-6 sm:mt-8">
@@ -51,7 +77,6 @@
</div> </div>
<div class="mt-4 space-y-6"> <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-4">
<div class="space-y-2"> <div class="space-y-2">
@@ -72,21 +97,10 @@
<dd class="text-lg font-bold text-gray-900 dark:text-white">{{ .data.priceTotal }}€</dd> <dd class="text-lg font-bold text-gray-900 dark:text-white">{{ .data.priceTotal }}€</dd>
</dl> </dl>
</div> </div>
<div class="flex items-start sm:items-center">
<input id="terms-checkbox-2" type="checkbox" value="" class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-primary-600 focus:ring-2 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-primary-600" />
<label for="terms-checkbox-2" class="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300"> I agree with the <a href="#" title="" class="text-primary-700 underline hover:no-underline dark:text-primary-500">Terms and Conditions</a> of use of the Flowbite marketplace </label>
</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 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> </div>
</div> </div>
</form> </div>
</section> </section>
{{ template "footer.html" . }} {{ 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="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"> <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> <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Register your account</h2>
</div> </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

@@ -9,23 +9,24 @@
<img class="w-full h-full object-cover" src="/{{ .data.shopItem.Image}}" alt="Product Image"> <img class="w-full h-full object-cover" src="/{{ .data.shopItem.Image}}" alt="Product Image">
</div> </div>
<div class="flex -mx-2 mb-4"> <div class="flex -mx-2 mb-4">
{{ if .loggedIn }}
<input type="hidden" id="{{ .data.shopItem.ID}}" name="ShopItemId" value="{{ .data.shopItem.ID }}"> <input type="hidden" id="{{ .data.shopItem.ID}}" name="ShopItemId" value="{{ .data.shopItem.ID }}">
<div class="w-1/3 px-2"> <div class="w-1/3 px-2">
<button type="submit" 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> <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>
<div class="w-1/3 px-2"> <div class="w-1/3 px-2">
<a href="/{{ .data.shopItem.Pdf }}"><button type="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">View</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>
{{ if .loggedIn }}
<div class="w-1/3 px-2">
<a href="{{ .data.shopItem.ID }}/edit"><button type="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>
</div>
<div class="w-1/3 px-2">
<a href="{{ .data.shopItem.ID }}/delete"><button type="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>
</div> </div>
{{ end }} {{ end }}
</div> </div>
@@ -36,6 +37,7 @@
{{ .data.shopItem.Abstract }} {{ .data.shopItem.Abstract }}
</p> </p>
{{ if .loggedIn }}
<div class="flex mb-4"> <div class="flex mb-4">
<label for="ItemVariantId" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"></label> <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"> <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">
@@ -45,15 +47,16 @@
{{ end }} {{ end }}
</select> </select>
</div> </div>
{{ end }}
<div class="flex mb-4"> <div class="flex mb-4">
<div class="mr-4"> <div class="mr-4">
<span class="font-bold text-gray-700 dark:text-gray-300">Tags:</span> <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 }} {{ 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 }} {{ end }}
</span> </p>
</div> </div>
</div> </div>
<!-- <!--
@@ -79,25 +82,4 @@
</div> </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" . }} {{ template "footer.html" . }}

View File

@@ -1,28 +1,61 @@
<div class="bg-white"> <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> <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 }} {{ range .data.shopItems }}
<div class="group relative"> <div class="myClass group relative">
<img src="/{{ .Image }}" alt="Product Image" class="aspect-4/5 w-full rounded-md bg-gray-200 object-cover group-hover:opacity-75 lg:aspect-auto lg:h-80"> <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 class="mt-4 flex justify-between">
<div> <div>
<h3 class="text-sm text-gray-700"> <h3 class="text-sm text-gray-700">
<a href="/shopitems/{{ .ID }}"> <a href="/shopitems/{{ .ID }}">
<span aria-hidden="true" class="absolute inset-0"></span>
{{ .Name }} {{ .Name }}
</a> </a>
</h3> </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 }}{{ .Name }} {{ end }}</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> </div>
<p class="text-sm font-medium text-gray-900">{{ .BasePrice }}€</p> {{ if $.loggedIn }}
<p class="text-sm font-medium text-gray-900">{{ .BasePrice }}€</p>
{{ end }}
</div> </div>
</div> </div>
{{ end }} {{ 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> </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="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"> <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> <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Edit Tags</h2>
</div> </div>
@@ -12,7 +12,36 @@
<form action="/tags/{{ .ID }}" method="POST"> <form action="/tags/{{ .ID }}" method="POST">
<div class="max-w-md mx-auto mt-4"> <div class="max-w-md mx-auto mt-4">
<div class="flex"> <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"> <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="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> <button type="submit" name="action" value="delete" class="bg-red-800 text-white rounded px-4 hover:bg-red-900">Delete</button>
</div> </div>