81 Commits

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

View File

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

View File

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

View File

@@ -1,17 +1,19 @@
package controllers
import (
"errors"
"fmt"
"strconv"
"net/http"
"crypto/rand"
"encoding/hex"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"example.com/gin/test/models"
//"example.com/gin/test/services"
"example.com/gin/test/repositories"
"git.dynamicdiscord.de/kalipso/zineshop/models"
//"git.dynamicdiscord.de/kalipso/zineshop/services"
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
"git.dynamicdiscord.de/kalipso/zineshop/utils"
)
type CartItemController interface {
@@ -22,37 +24,46 @@ type CartItemController interface {
EditItemHandler(*gin.Context)
CheckoutView(*gin.Context)
CheckoutHandler(*gin.Context)
OrderView(*gin.Context)
OrderHandler(*gin.Context)
OrdersView(*gin.Context)
OrdersHandler(*gin.Context)
}
type cartItemController struct {}
type cartItemController struct{}
func NewCartItemController() CartItemController {
return &cartItemController{}
}
func GetShippingMethods() []models.Shipping {
return []models.Shipping{
{ Id: "germany", Name: "Germany (DHL)", Price: 3.99 },
{ Id: "international", Name: "International (DHL)", Price: 5.99 },
{ Id: "pickup", Name: "Pickup", Price: 0.00 },
// getSetCookieValue retrieves the value of a cookie from the Set-Cookie header
func getSetCookieValue(c *gin.Context, cookieName string) string {
// Check the Set-Cookie headers
cookies := c.Writer.Header()["Set-Cookie"]
for _, cookie := range cookies {
if strings.HasPrefix(cookie, cookieName+"=") {
// Extract the cookie value
parts := strings.SplitN(cookie, ";", 2)
if len(parts) > 0 {
return strings.TrimPrefix(parts[0], cookieName+"=")
}
}
}
}
func generateSessionId() string {
bytes := make([]byte, 16) // 16 bytes = 128 bits
_, err := rand.Read(bytes)
if err != nil {
panic("failed to generate session ID")
}
return hex.EncodeToString(bytes)
return "" // Return empty string if cookie is not found
}
func GetSessionId(ctx *gin.Context) string {
sessionId, err := ctx.Cookie("session_id")
if err != nil {
sessionId = generateSessionId()
//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)
}
@@ -81,17 +92,112 @@ func (rc *cartItemController) NewCartItemFromForm(ctx *gin.Context) (models.Cart
itemVariant, err := repositories.ShopItems.GetVariantById(itemVariantIdStr)
cartItem := models.CartItem{
SessionId: sessionId,
ShopItemId: uint(shopItemId),
ShopItem: shopItem,
SessionId: sessionId,
ShopItemId: uint(shopItemId),
ShopItem: shopItem,
ItemVariantId: uint(itemVariantId),
ItemVariant: itemVariant,
Quantity: quantity,
ItemVariant: itemVariant,
Quantity: quantity,
}
return cartItem, nil
}
func (rc *cartItemController) NewAddressFromForm(ctx *gin.Context) (models.AddressInfo, error) {
firstName := ctx.PostForm("firstName")
lastName := ctx.PostForm("lastName")
address := ctx.PostForm("address")
postalCode := ctx.PostForm("postalCode")
city := ctx.PostForm("city")
country := ctx.PostForm("country")
if firstName == "" {
return models.AddressInfo{}, fmt.Errorf("first name missing.")
}
if lastName == "" {
return models.AddressInfo{}, fmt.Errorf("Last name missing.")
}
if address == "" {
return models.AddressInfo{}, fmt.Errorf("address missing.")
}
if postalCode == "" {
return models.AddressInfo{}, fmt.Errorf("postalCode missing.")
}
if city == "" {
return models.AddressInfo{}, fmt.Errorf("city missing.")
}
if country == "" {
return models.AddressInfo{}, fmt.Errorf("country missing.")
}
return models.AddressInfo{
FirstName: firstName,
LastName: lastName,
Address: address,
PostalCode: postalCode,
City: city,
Country: country,
}, nil
}
func (rc *cartItemController) NewOrderFromForm(ctx *gin.Context) (models.Order, error) {
sessionId := GetSessionId(ctx)
status := models.OrderStatus("AwaitingConfirmation")
token := utils.GenerateToken()
email := ctx.PostForm("email")
comment := ctx.PostForm("comment")
firstName := ctx.PostForm("firstName")
lastName := ctx.PostForm("lastName")
address := ctx.PostForm("address")
postalCode := ctx.PostForm("postalCode")
city := ctx.PostForm("city")
country := ctx.PostForm("country")
shippingStr := ctx.PostForm("shippingMethod")
//address, err := rc.NewAddressFromForm(ctx)
//if shippingStr != "pickup" {
// if err != nil {
// return models.Order{}, fmt.Errorf("Invalid address information.")
// }
//}
shipping, err := models.GetShippingMethod(shippingStr)
if err != nil {
return models.Order{}, fmt.Errorf("Invalid shipping method.")
}
cartItems, err := repositories.CartItems.GetAllBySession(sessionId)
fmt.Println(sessionId)
fmt.Println(cartItems)
if err != nil {
return models.Order{}, err
}
order := models.Order{
SessionId: sessionId,
Status: status,
Token: token,
Email: email,
Comment: comment,
FirstName: firstName,
LastName: lastName,
Address: address,
PostalCode: postalCode,
City: city,
Country: country,
Shipping: shipping.Id,
CartItems: cartItems,
}
return order, nil
}
func (rc *cartItemController) Create(c *gin.Context) {
cartItem, err := rc.NewCartItemFromForm(c)
@@ -111,7 +217,6 @@ func (rc *cartItemController) Create(c *gin.Context) {
ReplyOK(c, "cartItem was created")
}
func (rc *cartItemController) Update(c *gin.Context) {
cartItem, err := rc.NewCartItemFromForm(c)
@@ -142,13 +247,13 @@ func (rc *cartItemController) Update(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.GetAll()
cartItems, err := repositories.CartItems.GetAllBySession(sessionId)
//cartItems, err := repositories.CartItems.GetAll()
if err != nil {
c.HTML(http.StatusBadRequest, "cart.html", gin.H{ "data": gin.H{ "error": err } })
c.HTML(http.StatusBadRequest, "cart.html", gin.H{"data": gin.H{"error": err}})
}
priceTotal := 0.0
@@ -159,9 +264,9 @@ func (rc *cartItemController) CartItemView(c *gin.Context) {
fmt.Println("PRICE TOTAL", priceTotal)
data := CreateSessionData(c, gin.H{
"cartItems": cartItems,
"cartItems": cartItems,
"priceTotal": fmt.Sprintf("%.2f", priceTotal), //round 2 decimals
"shipping": GetShippingMethods(),
"shipping": models.GetShippingMethods(),
})
c.HTML(http.StatusOK, "cart.html", data)
@@ -172,14 +277,14 @@ func (rc *cartItemController) AddItemHandler(c *gin.Context) {
if err != nil {
fmt.Println(err)
c.HTML(http.StatusBadRequest, "error.html", gin.H{ "error": err })
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return
}
_, err = repositories.CartItems.Create(cartItem)
if err != nil {
data := CreateSessionData(c, gin.H{
"error": err,
"error": err,
"success": "",
})
@@ -196,7 +301,7 @@ func (rc *cartItemController) DeleteItemHandler(c *gin.Context) {
if err != nil {
fmt.Println(err)
data := CreateSessionData(c, gin.H{
"error": err,
"error": err,
"success": "",
})
@@ -218,6 +323,27 @@ func (rc *cartItemController) EditItemHandler(c *gin.Context) {
action := c.PostForm("action")
if action == "setAmount" {
amountStr := c.PostForm("amount")
amount, err := strconv.Atoi(amountStr)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return
}
if amount < 0 {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "amount cant be negative"})
return
}
if amount > 500 {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "amount cant be over 500"})
return
}
cartItem.Quantity = amount
}
if action == "increase" {
cartItem.Quantity += 1
}
@@ -242,12 +368,238 @@ func (rc *cartItemController) EditItemHandler(c *gin.Context) {
func (rc *cartItemController) CheckoutView(c *gin.Context) {
shippingMethod := c.Query("shippingMethod")
if shippingMethod == "" {
rc.CartItemView(c)
return
}
c.HTML(http.StatusOK, "checkout.html", gin.H{
"askAddress": (shippingMethod != "pickup"),
"askAddress": (shippingMethod != "pickup"),
"shippingMethod": shippingMethod,
})
}
func (rc *cartItemController) CheckoutHandler(*gin.Context) {
func (rc *cartItemController) CheckoutHandler(c *gin.Context) {
order, err := rc.NewOrderFromForm(c)
if err != nil {
fmt.Println(err)
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return
}
existingOrder, err := repositories.Orders.GetBySession(order.SessionId)
if errors.Is(err, gorm.ErrRecordNotFound) {
fmt.Println("Creating Order")
createdOrder, err := repositories.Orders.Create(order)
if err != nil {
data := CreateSessionData(c, gin.H{
"error": err,
"success": "",
})
c.HTML(http.StatusOK, "error.html", data)
return
}
for _, cartItem := range order.CartItems {
cartItem.OrderID = createdOrder.ID
repositories.CartItems.Update(cartItem)
}
} else if err == nil {
fmt.Println("Updating Order")
order.ID = existingOrder.ID
order.CreatedAt = existingOrder.CreatedAt
_, err := repositories.Orders.Update(order)
if err != nil {
fmt.Println(err)
}
}
shipping, err := models.GetShippingMethod(order.Shipping)
if err != nil {
data := CreateSessionData(c, gin.H{
"error": err,
"success": "",
})
c.HTML(http.StatusOK, "error.html", data)
return
}
priceProducts, priceTotal, err := order.CalculatePrices()
if err != nil {
data := CreateSessionData(c, gin.H{
"error": err,
"success": "",
})
c.HTML(http.StatusOK, "error.html", data)
return
}
data := CreateSessionData(c, gin.H{
"error": "",
"success": "",
"order": order,
"askAddress": (order.Shipping != "pickup"),
"isPreview": true,
"shipping": shipping,
"priceProducts": fmt.Sprintf("%.2f", priceProducts), //round 2 decimals
"priceTotal": fmt.Sprintf("%.2f", priceTotal), //round 2 decimals
})
fmt.Println(order)
c.HTML(http.StatusOK, "orderpreview.html", data)
}
func (rc *cartItemController) OrderView(c *gin.Context) {
orderToken := c.Param("token")
order, err := repositories.Orders.GetByToken(orderToken)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Order does not exist."})
return
}
shipping, err := models.GetShippingMethod(order.Shipping)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Could not get shipping method"})
return
}
priceProducts, priceTotal, err := order.CalculatePrices()
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Could not calculate final prices"})
return
}
fmt.Printf("Order: %v\n", order)
fmt.Printf("PriceTotal: %v\n", priceTotal)
fmt.Printf("Amount Items: %v\n", len(order.CartItems))
for _, item := range order.CartItems {
fmt.Printf("Cartitem: %v", item)
}
c.HTML(http.StatusOK, "order.html", CreateSessionData(c, gin.H{
"error": "",
"success": "",
"order": order,
"shipping": shipping,
"priceProducts": fmt.Sprintf("%.2f", priceProducts), //round 2 decimals
"priceTotal": fmt.Sprintf("%.2f", priceTotal), //round 2 decimals
}))
}
func (rc *cartItemController) OrderHandler(c *gin.Context) {
confirmation := c.PostForm("confirm-order")
if confirmation == "" {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Something went wrong, try again later"})
return
}
if confirmation != "true" {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Order was not confirmed."})
return
}
sessionId := GetSessionId(c)
order, err := repositories.Orders.GetBySession(sessionId)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Something went wrong, try again later"})
return
}
order.Status = models.AwaitingPayment
err = order.Validate()
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return
}
for idx := range order.CartItems {
order.CartItems[idx].SessionId = "0"
repositories.CartItems.Update(order.CartItems[idx])
}
_, err = repositories.Orders.Update(order)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return
}
//TODO: cartItemRepository delete all by session - otherwise items stay in cart after completing order..
c.Redirect(http.StatusFound, fmt.Sprintf("/order/%s", order.Token))
}
func (rc *cartItemController) OrdersView(c *gin.Context) {
orders, err := repositories.Orders.GetAll()
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Orders doe not exist."})
return
}
c.HTML(http.StatusOK, "editorders.html", CreateSessionData(c, gin.H{
"error": "",
"success": "",
"orders": orders,
}))
}
func (rc *cartItemController) OrdersHandler(c *gin.Context) {
token := c.Param("token")
if token == "" {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "EmptyToken"})
return
}
action := c.PostForm("action")
if action == "update" {
order, err := repositories.Orders.GetByToken(token)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "No order with given token found"})
return
}
status := c.PostForm("order-status")
//TODO validate status
if status == "" {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Invalid Order Status"})
return
}
order.Status = models.OrderStatus(status)
_, err = repositories.Orders.Update(order)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return
}
}
if action == "delete" {
fmt.Println("Deleting Order ", token)
err := repositories.Orders.DeleteByToken(token)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return
}
}
c.Redirect(http.StatusFound, "/orders")
}

View File

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

View File

@@ -3,19 +3,20 @@ package controllers
import (
"fmt"
"net/http"
"strconv"
"os/exec"
"path/filepath"
"strconv"
"github.com/gin-gonic/gin"
"example.com/gin/test/models"
//"example.com/gin/test/services"
"example.com/gin/test/repositories"
"git.dynamicdiscord.de/kalipso/zineshop/models"
//"git.dynamicdiscord.de/kalipso/zineshop/services"
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
)
type CRUDController interface {
Create(*gin.Context)
GetAll(*gin.Context)
GetAll(*gin.Context)
GetById(*gin.Context)
Update(*gin.Context)
Delete(*gin.Context)
@@ -26,6 +27,8 @@ type ShopItemController interface {
ShopItemView(*gin.Context)
AddItemView(*gin.Context)
AddItemHandler(*gin.Context)
AddItemsView(*gin.Context)
AddItemsHandler(*gin.Context)
CreateTag(*gin.Context)
GetAllTags(*gin.Context)
EditItemView(*gin.Context)
@@ -37,7 +40,7 @@ type ShopItemController interface {
AddTagHandler(*gin.Context)
}
type shopItemController struct {}
type shopItemController struct{}
func NewShopItemController() ShopItemController {
return &shopItemController{}
@@ -66,6 +69,7 @@ func (rc *shopItemController) GetById(c *gin.Context) {
}
func (rc *shopItemController) NewShopItemFromForm(ctx *gin.Context) (models.ShopItem, error) {
defaultImagePath := "static/img/zine.jpg"
name := ctx.PostForm("name")
abstract := ctx.PostForm("abstract")
description := ctx.PostForm("description")
@@ -74,13 +78,14 @@ func (rc *shopItemController) NewShopItemFromForm(ctx *gin.Context) (models.Shop
variantValues := ctx.PostFormArray("variant-value[]")
tagIds := ctx.PostFormArray("tags[]")
image, err := ctx.FormFile("image")
dstImage := "static/img/zine.jpg"
dstImage := defaultImagePath
printMode := ctx.PostForm("print-mode")
if err == nil {
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")
}
}
}
dstPdf := ""
@@ -88,9 +93,22 @@ func (rc *shopItemController) NewShopItemFromForm(ctx *gin.Context) (models.Shop
if err == nil {
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")
}
}
if dstImage == defaultImagePath {
dstImage = dstPdf + ".preview.png"
cmd := exec.Command("pdftoppm", "-png", "-singlefile", dstPdf, dstPdf+".preview")
_, err := cmd.Output()
if err != nil {
fmt.Println("Error during pdftoppm: ", err.Error())
}
}
} else {
fmt.Println(err)
}
if name == "" || description == "" {
@@ -113,31 +131,34 @@ func (rc *shopItemController) NewShopItemFromForm(ctx *gin.Context) (models.Shop
for idx := range variantNames {
if variantValues[idx] == "" || variantNames[idx] == "" {
continue
}
}
price, err := strconv.ParseFloat(variantValues[idx], 64)
if err != nil {
if err != nil {
return models.ShopItem{}, fmt.Errorf("Could not variant parse price")
}
}
variants = append(variants, models.ItemVariant{
Name: variantNames[idx],
variants = append(variants, models.ItemVariant{
Name: variantNames[idx],
Price: price,
})
}
shopItem := models.ShopItem{
Name: name,
Abstract: abstract,
Name: name,
Abstract: abstract,
Description: description,
Category: category,
IsPublic: true,
BasePrice: rc.GetBasePrice(variants),
Image: dstImage,
Pdf: dstPdf,
Variants: variants,
Category: category,
IsPublic: true,
BasePrice: rc.GetBasePrice(variants),
Image: dstImage,
Pdf: dstPdf,
Variants: variants,
PrintMode: printMode,
}
fmt.Println("Creating Shopitem: ", shopItem)
for _, tagId := range tagIds {
tag, err := repositories.Tags.GetById(tagId)
@@ -186,7 +207,6 @@ func (rc *shopItemController) Create(c *gin.Context) {
ReplyOK(c, "shopItem was created")
}
func (rc *shopItemController) Update(c *gin.Context) {
shopItemId, err := strconv.Atoi(c.Param("id"))
@@ -213,7 +233,7 @@ func (rc *shopItemController) Update(c *gin.Context) {
ReplyOK(c, "shopItem was updated")
}
//TODO: delete associated cartitems
// TODO: delete associated cartitems
func (rc *shopItemController) Delete(c *gin.Context) {
err := repositories.ShopItems.DeleteById(c.Param("id"))
@@ -229,19 +249,19 @@ func (rc *shopItemController) ShopItemView(c *gin.Context) {
shopItem, err := repositories.ShopItems.GetById(c.Param("id"))
if err != nil {
c.HTML(http.StatusBadRequest, "shopitem.html", gin.H{ "data": gin.H{ "error": err } })
c.HTML(http.StatusBadRequest, "shopitem.html", gin.H{"data": gin.H{"error": err}})
}
//TODO: get tags by item
tags, err := repositories.Tags.GetAll()
if err != nil {
c.HTML(http.StatusBadRequest, "shopitem.html", gin.H{ "data": gin.H{ "error": err } })
c.HTML(http.StatusBadRequest, "shopitem.html", gin.H{"data": gin.H{"error": err}})
}
data := CreateSessionData(c, gin.H{
"shopItem": shopItem,
"tags": tags,
"tags": tags,
})
if err != nil {
@@ -255,25 +275,24 @@ func (rc *shopItemController) AddItemView(c *gin.Context) {
tags, err := repositories.Tags.GetAll()
if err != nil {
c.HTML(http.StatusBadRequest, "additem.html", gin.H{ "error": err })
c.HTML(http.StatusBadRequest, "additem.html", gin.H{"error": err})
}
data := CreateSessionData(c, gin.H{
"error": "",
"error": "",
"success": "",
"tags": tags,
"tags": tags,
})
c.HTML(http.StatusOK, "additem.html", data)
}
func (rc *shopItemController) AddItemHandler(c *gin.Context) {
errorHandler := func(err error, tags []models.Tag) {
data := CreateSessionData(c, gin.H{
"error": err,
"error": err,
"success": "",
"tags": tags,
"tags": tags,
})
c.HTML(http.StatusOK, "additem.html", data)
@@ -298,71 +317,172 @@ func (rc *shopItemController) AddItemHandler(c *gin.Context) {
}
data := CreateSessionData(c, gin.H{
"error": "",
"error": "",
"success": fmt.Sprintf("Item '%s' Registered", shopItem.Name),
"tags": tags,
"tags": tags,
})
c.HTML(http.StatusOK, "additem.html", data)
}
func (rc *shopItemController) AddItemsView(c *gin.Context) {
data := CreateSessionData(c, gin.H{})
c.HTML(http.StatusOK, "batchupload.html", data)
}
func (rc *shopItemController) AddItemsHandler(c *gin.Context) {
errorHandler := func(err error) {
data := CreateSessionData(c, gin.H{
"error": err,
})
c.HTML(http.StatusBadRequest, "batchupload.html", data)
}
form, err := c.MultipartForm()
if err != nil {
errorHandler(err)
return
}
files := form.File["pdf"]
var shopItems []models.ShopItem
for _, file := range files {
dstPdf := filepath.Join("static/uploads", file.Filename)
if err := c.SaveUploadedFile(file, dstPdf); err != nil {
errorHandler(err)
return
}
dstImage := dstPdf + ".preview.png"
cmd := exec.Command("pdftoppm", "-png", "-singlefile", dstPdf, dstPdf+".preview")
_, err := cmd.Output()
if err != nil {
fmt.Println("Error during pdftoppm: ", err.Error())
}
category, err := models.ParseCategory("Zine")
if err != nil {
errorHandler(err)
return
}
variants := []models.ItemVariant{
{
Name: "B/W",
Price: 1.0,
},
}
shopItem := models.ShopItem{
Name: file.Filename,
Abstract: file.Filename,
Description: file.Filename,
Category: category,
IsPublic: true,
BasePrice: rc.GetBasePrice(variants),
Image: dstImage,
Pdf: dstPdf,
Variants: variants,
PrintMode: "CreateBooklet",
}
_, err = repositories.ShopItems.Create(shopItem)
if err != nil {
errorHandler(err)
return
}
shopItems = append(shopItems, shopItem)
}
msg := "The Following items were registered:\n"
for _, item := range shopItems {
msg += fmt.Sprintf("%s\n", item.Name)
}
data := CreateSessionData(c, gin.H{
"error": "",
"success": msg,
})
c.HTML(http.StatusOK, "batchupload.html", data)
}
func (rc *shopItemController) EditItemView(c *gin.Context) {
shopItem, err := repositories.ShopItems.GetById(c.Param("id"))
tags, err := repositories.Tags.GetAll()
if err != nil {
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{ "error": err })
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{"error": err})
}
fmt.Println(shopItem)
data := CreateSessionData(c, gin.H{
"error": "",
"success": "",
"error": "",
"success": "",
"shopItem": shopItem,
"tags": tags,
"tags": tags,
})
c.HTML(http.StatusOK, "edititem.html", data)
}
func (rc *shopItemController) EditItemHandler(c *gin.Context) {
shopItem, err := rc.NewShopItemFromForm(c)
if err != nil {
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{ "error": err })
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{"error": err})
return
}
newShopItem, err := repositories.ShopItems.GetById(c.Param("id"))
if err != nil {
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{ "error": err })
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{"error": err})
return
}
newShopItem.Name = shopItem.Name
newShopItem.Abstract = shopItem.Abstract
newShopItem.Description = shopItem.Description
newShopItem.Category = shopItem.Category
newShopItem.Variants = shopItem.Variants
newShopItem.BasePrice = shopItem.BasePrice
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()
if err != nil {
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{ "error": err })
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{"error": err})
return
}
_, err = repositories.ShopItems.Update(newShopItem)
if err != nil {
data := CreateSessionData(c, gin.H{
"error": err,
"error": err,
"success": "",
"tags": tags,
"tags": tags,
})
c.HTML(http.StatusOK, "edititem.html", data)
@@ -370,9 +490,9 @@ func (rc *shopItemController) EditItemHandler(c *gin.Context) {
}
data := CreateSessionData(c, gin.H{
"error": "",
"error": "",
"success": fmt.Sprintf("Item '%s' Updated", newShopItem.Name),
"tags": tags,
"tags": tags,
})
c.HTML(http.StatusOK, "edititem.html", data)
@@ -383,28 +503,27 @@ func (rc *shopItemController) DeleteItemView(c *gin.Context) {
tags, err := repositories.Tags.GetAll()
if err != nil {
c.HTML(http.StatusBadRequest, "deleteitem.html", gin.H{ "error": err })
c.HTML(http.StatusBadRequest, "deleteitem.html", gin.H{"error": err})
}
fmt.Println(shopItem)
data := CreateSessionData(c, gin.H{
"error": "",
"success": "",
"error": "",
"success": "",
"shopItem": shopItem,
"tags": tags,
"tags": tags,
})
c.HTML(http.StatusOK, "deleteitem.html", data)
}
func (rc *shopItemController) DeleteItemHandler(c *gin.Context) {
err := repositories.ShopItems.DeleteById(c.Param("id"))
if err != nil {
data := CreateSessionData(c, gin.H{
"error": err,
"error": err,
"success": "",
})
@@ -415,7 +534,7 @@ func (rc *shopItemController) DeleteItemHandler(c *gin.Context) {
fmt.Println(len(shopItems))
data := CreateSessionData(c, gin.H{
"title": "shopItem Page",
"title": "shopItem Page",
"shopItems": shopItems,
})
@@ -426,23 +545,25 @@ func (rc *shopItemController) DeleteItemHandler(c *gin.Context) {
func (rc *shopItemController) TagHandler(ctx *gin.Context) {
name := ctx.PostForm("name")
color := ctx.PostForm("color")
action := ctx.PostForm("action")
tag, err := repositories.Tags.GetById(ctx.Param("id"))
if err != nil {
fmt.Println(err)
ctx.HTML(http.StatusBadRequest, "tagview.html", gin.H{ "error": err })
ctx.HTML(http.StatusBadRequest, "tagview.html", gin.H{"error": err})
return
}
if action == "update" {
tag.Name = name
tag.Color = color
tag, err = repositories.Tags.Update(tag)
if err != nil {
fmt.Println(err)
ctx.HTML(http.StatusBadRequest, "tagview.html", gin.H{ "error": err })
ctx.HTML(http.StatusBadRequest, "tagview.html", gin.H{"error": err})
return
}
}
@@ -459,14 +580,14 @@ func (rc *shopItemController) AddTagHandler(c *gin.Context) {
if err != nil {
fmt.Println(err)
c.HTML(http.StatusBadRequest, "tagview.html", gin.H{ "error": err })
c.HTML(http.StatusBadRequest, "tagview.html", gin.H{"error": err})
return
}
_, err = repositories.Tags.Create(tag)
if err != nil {
data := CreateSessionData(c, gin.H{
"error": err,
"error": err,
"success": "",
})
@@ -481,7 +602,7 @@ func (rc *shopItemController) TagView(c *gin.Context) {
tags, err := repositories.Tags.GetAll()
if err != nil {
c.HTML(http.StatusBadRequest, "tagview.html", gin.H{ "data": gin.H{ "error": err } })
c.HTML(http.StatusBadRequest, "tagview.html", gin.H{"data": gin.H{"error": err}})
}
data := CreateSessionData(c, gin.H{
@@ -532,7 +653,7 @@ func (rc *shopItemController) GetAllTags(c *gin.Context) {
}
func ReplyError(ctx *gin.Context, err error) {
ctx.JSON(http.StatusBadRequest, gin.H{ "error": err.Error() })
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
func ReplyOK(ctx *gin.Context, message any) {

View File

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

61
flake.lock generated Normal file
View File

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

135
flake.nix Normal file
View File

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

2
go.mod
View File

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

100
main.go
View File

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

View File

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

View File

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

88
models/printer.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import(
"strconv"
"gorm.io/gorm"
"example.com/gin/test/models"
"git.dynamicdiscord.de/kalipso/zineshop/models"
)
type CartItemRepository interface {
@@ -63,7 +63,7 @@ func (t *GORMCartItemRepository) GetById(id string) (models.CartItem, error) {
func (r *GORMCartItemRepository) GetAllBySession(sessionId string) ([]models.CartItem, error) {
var cartItems []models.CartItem
result := r.DB.Preload("ShopItem").Where("session_id = ?", sessionId).Find(&cartItems)
result := r.DB.Preload("ShopItem").Preload("ItemVariant").Where("session_id = ?", sessionId).Find(&cartItems)
return cartItems, result.Error

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,8 +3,8 @@ package services
import(
"fmt"
"example.com/gin/test/models"
"example.com/gin/test/repositories"
"git.dynamicdiscord.de/kalipso/zineshop/models"
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
)
var(

View File

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -574,10 +574,6 @@ video {
position: relative;
}
.inset-0 {
inset: 0px;
}
.inset-y-0 {
top: 0px;
bottom: 0px;
@@ -634,6 +630,10 @@ video {
margin-bottom: 2rem;
}
.me-2 {
margin-inline-end: 0.5rem;
}
.ml-2 {
margin-left: 0.5rem;
}
@@ -678,6 +678,10 @@ video {
display: flex;
}
.table {
display: table;
}
.grid {
display: grid;
}
@@ -710,14 +714,18 @@ video {
height: 2rem;
}
.h-\[460px\] {
height: 460px;
.h-auto {
height: auto;
}
.h-full {
height: 100%;
}
.max-h-full {
max-height: 100%;
}
.min-h-full {
min-height: 100%;
}
@@ -726,10 +734,6 @@ video {
width: 33.333333%;
}
.w-10 {
width: 2.5rem;
}
.w-12 {
width: 3rem;
}
@@ -738,6 +742,10 @@ video {
width: 1rem;
}
.w-8 {
width: 2rem;
}
.w-auto {
width: auto;
}
@@ -750,6 +758,10 @@ video {
max-width: 42rem;
}
.max-w-3xl {
max-width: 48rem;
}
.max-w-6xl {
max-width: 72rem;
}
@@ -762,10 +774,18 @@ video {
max-width: 28rem;
}
.max-w-screen-xl {
max-width: 1280px;
}
.flex-1 {
flex: 1 1 0%;
}
.flex-shrink-0 {
flex-shrink: 0;
}
.shrink-0 {
flex-shrink: 0;
}
@@ -841,12 +861,37 @@ video {
margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
}
.space-y-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(1rem * var(--tw-space-y-reverse));
}
.space-y-6 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
}
.divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
}
.divide-gray-200 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-divide-opacity, 1));
}
.overflow-x-auto {
overflow-x: auto;
}
.whitespace-nowrap {
white-space: nowrap;
}
.rounded {
border-radius: 0.25rem;
}
@@ -855,10 +900,6 @@ video {
border-radius: 1.5rem;
}
.rounded-\[50px\] {
border-radius: 50px;
}
.rounded-full {
border-radius: 9999px;
}
@@ -871,6 +912,10 @@ video {
border-radius: 0.375rem;
}
.rounded-sm {
border-radius: 0.125rem;
}
.rounded-l-md {
border-top-left-radius: 0.375rem;
border-bottom-left-radius: 0.375rem;
@@ -888,6 +933,10 @@ video {
border-bottom-width: 1px;
}
.border-t {
border-top-width: 1px;
}
.border-dashed {
border-style: dashed;
}
@@ -902,6 +951,21 @@ video {
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
}
.bg-amber-100 {
--tw-bg-opacity: 1;
background-color: rgb(254 243 199 / var(--tw-bg-opacity, 1));
}
.bg-amber-900 {
--tw-bg-opacity: 1;
background-color: rgb(120 53 15 / var(--tw-bg-opacity, 1));
}
.bg-blue-100 {
--tw-bg-opacity: 1;
background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1));
}
.bg-blue-600 {
--tw-bg-opacity: 1;
background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1));
@@ -912,6 +976,36 @@ video {
background-color: rgb(30 58 138 / var(--tw-bg-opacity, 1));
}
.bg-cyan-100 {
--tw-bg-opacity: 1;
background-color: rgb(207 250 254 / var(--tw-bg-opacity, 1));
}
.bg-cyan-900 {
--tw-bg-opacity: 1;
background-color: rgb(22 78 99 / var(--tw-bg-opacity, 1));
}
.bg-emerald-100 {
--tw-bg-opacity: 1;
background-color: rgb(209 250 229 / var(--tw-bg-opacity, 1));
}
.bg-emerald-900 {
--tw-bg-opacity: 1;
background-color: rgb(6 78 59 / var(--tw-bg-opacity, 1));
}
.bg-fuchsia-100 {
--tw-bg-opacity: 1;
background-color: rgb(250 232 255 / var(--tw-bg-opacity, 1));
}
.bg-fuchsia-900 {
--tw-bg-opacity: 1;
background-color: rgb(112 26 117 / var(--tw-bg-opacity, 1));
}
.bg-gray-100 {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
@@ -942,16 +1036,96 @@ video {
background-color: rgb(17 24 39 / var(--tw-bg-opacity, 1));
}
.bg-green-100 {
--tw-bg-opacity: 1;
background-color: rgb(220 252 231 / var(--tw-bg-opacity, 1));
}
.bg-green-600 {
--tw-bg-opacity: 1;
background-color: rgb(22 163 74 / var(--tw-bg-opacity, 1));
}
.bg-green-900 {
--tw-bg-opacity: 1;
background-color: rgb(20 83 45 / var(--tw-bg-opacity, 1));
}
.bg-indigo-100 {
--tw-bg-opacity: 1;
background-color: rgb(224 231 255 / var(--tw-bg-opacity, 1));
}
.bg-indigo-500 {
--tw-bg-opacity: 1;
background-color: rgb(99 102 241 / var(--tw-bg-opacity, 1));
}
.bg-indigo-600 {
--tw-bg-opacity: 1;
background-color: rgb(79 70 229 / var(--tw-bg-opacity, 1));
}
.bg-indigo-900 {
--tw-bg-opacity: 1;
background-color: rgb(49 46 129 / var(--tw-bg-opacity, 1));
}
.bg-lime-100 {
--tw-bg-opacity: 1;
background-color: rgb(236 252 203 / var(--tw-bg-opacity, 1));
}
.bg-lime-900 {
--tw-bg-opacity: 1;
background-color: rgb(54 83 20 / var(--tw-bg-opacity, 1));
}
.bg-neutral-100 {
--tw-bg-opacity: 1;
background-color: rgb(245 245 245 / var(--tw-bg-opacity, 1));
}
.bg-neutral-900 {
--tw-bg-opacity: 1;
background-color: rgb(23 23 23 / var(--tw-bg-opacity, 1));
}
.bg-orange-100 {
--tw-bg-opacity: 1;
background-color: rgb(255 237 213 / var(--tw-bg-opacity, 1));
}
.bg-orange-900 {
--tw-bg-opacity: 1;
background-color: rgb(124 45 18 / var(--tw-bg-opacity, 1));
}
.bg-pink-100 {
--tw-bg-opacity: 1;
background-color: rgb(252 231 243 / var(--tw-bg-opacity, 1));
}
.bg-pink-900 {
--tw-bg-opacity: 1;
background-color: rgb(131 24 67 / var(--tw-bg-opacity, 1));
}
.bg-purple-100 {
--tw-bg-opacity: 1;
background-color: rgb(243 232 255 / var(--tw-bg-opacity, 1));
}
.bg-purple-900 {
--tw-bg-opacity: 1;
background-color: rgb(88 28 135 / var(--tw-bg-opacity, 1));
}
.bg-red-100 {
--tw-bg-opacity: 1;
background-color: rgb(254 226 226 / var(--tw-bg-opacity, 1));
}
.bg-red-800 {
--tw-bg-opacity: 1;
background-color: rgb(153 27 27 / var(--tw-bg-opacity, 1));
@@ -962,17 +1136,93 @@ video {
background-color: rgb(127 29 29 / var(--tw-bg-opacity, 1));
}
.bg-rose-100 {
--tw-bg-opacity: 1;
background-color: rgb(255 228 230 / var(--tw-bg-opacity, 1));
}
.bg-rose-900 {
--tw-bg-opacity: 1;
background-color: rgb(136 19 55 / var(--tw-bg-opacity, 1));
}
.bg-sky-100 {
--tw-bg-opacity: 1;
background-color: rgb(224 242 254 / var(--tw-bg-opacity, 1));
}
.bg-sky-900 {
--tw-bg-opacity: 1;
background-color: rgb(12 74 110 / var(--tw-bg-opacity, 1));
}
.bg-slate-100 {
--tw-bg-opacity: 1;
background-color: rgb(241 245 249 / var(--tw-bg-opacity, 1));
}
.bg-slate-900 {
--tw-bg-opacity: 1;
background-color: rgb(15 23 42 / var(--tw-bg-opacity, 1));
}
.bg-stone-100 {
--tw-bg-opacity: 1;
background-color: rgb(245 245 244 / var(--tw-bg-opacity, 1));
}
.bg-stone-900 {
--tw-bg-opacity: 1;
background-color: rgb(28 25 23 / var(--tw-bg-opacity, 1));
}
.bg-teal-100 {
--tw-bg-opacity: 1;
background-color: rgb(204 251 241 / var(--tw-bg-opacity, 1));
}
.bg-teal-900 {
--tw-bg-opacity: 1;
background-color: rgb(19 78 74 / var(--tw-bg-opacity, 1));
}
.bg-violet-100 {
--tw-bg-opacity: 1;
background-color: rgb(237 233 254 / var(--tw-bg-opacity, 1));
}
.bg-violet-900 {
--tw-bg-opacity: 1;
background-color: rgb(76 29 149 / var(--tw-bg-opacity, 1));
}
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
}
.fill-red-50 {
fill: #fef2f2;
.bg-yellow-100 {
--tw-bg-opacity: 1;
background-color: rgb(254 249 195 / var(--tw-bg-opacity, 1));
}
.stroke-gray-900 {
stroke: #111827;
.bg-yellow-900 {
--tw-bg-opacity: 1;
background-color: rgb(113 63 18 / var(--tw-bg-opacity, 1));
}
.bg-zinc-100 {
--tw-bg-opacity: 1;
background-color: rgb(244 244 245 / var(--tw-bg-opacity, 1));
}
.bg-zinc-900 {
--tw-bg-opacity: 1;
background-color: rgb(24 24 27 / var(--tw-bg-opacity, 1));
}
.fill-red-50 {
fill: #fef2f2;
}
.stroke-red-500 {
@@ -1001,6 +1251,11 @@ video {
padding-right: 0.5rem;
}
.px-2\.5 {
padding-left: 0.625rem;
padding-right: 0.625rem;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
@@ -1011,11 +1266,21 @@ video {
padding-right: 1rem;
}
.px-5 {
padding-left: 1.25rem;
padding-right: 1.25rem;
}
.px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.py-0\.5 {
padding-top: 0.125rem;
padding-bottom: 0.125rem;
}
.py-1\.5 {
padding-top: 0.375rem;
padding-bottom: 0.375rem;
@@ -1036,6 +1301,11 @@ video {
padding-bottom: 0.5rem;
}
.py-2\.5 {
padding-top: 0.625rem;
padding-bottom: 0.625rem;
}
.py-24 {
padding-top: 6rem;
padding-bottom: 6rem;
@@ -1075,6 +1345,10 @@ video {
padding-top: 1.25rem;
}
.text-left {
text-align: left;
}
.text-center {
text-align: center;
}
@@ -1123,16 +1397,21 @@ video {
line-height: 1.5rem;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.text-sm\/8 {
font-size: 0.875rem;
line-height: 2rem;
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.font-bold {
font-weight: 700;
}
@@ -1165,11 +1444,61 @@ video {
letter-spacing: -0.025em;
}
.text-amber-300 {
--tw-text-opacity: 1;
color: rgb(252 211 77 / var(--tw-text-opacity, 1));
}
.text-amber-800 {
--tw-text-opacity: 1;
color: rgb(146 64 14 / var(--tw-text-opacity, 1));
}
.text-black {
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity, 1));
}
.text-blue-300 {
--tw-text-opacity: 1;
color: rgb(147 197 253 / var(--tw-text-opacity, 1));
}
.text-blue-800 {
--tw-text-opacity: 1;
color: rgb(30 64 175 / var(--tw-text-opacity, 1));
}
.text-cyan-300 {
--tw-text-opacity: 1;
color: rgb(103 232 249 / var(--tw-text-opacity, 1));
}
.text-cyan-800 {
--tw-text-opacity: 1;
color: rgb(21 94 117 / var(--tw-text-opacity, 1));
}
.text-emerald-300 {
--tw-text-opacity: 1;
color: rgb(110 231 183 / var(--tw-text-opacity, 1));
}
.text-emerald-800 {
--tw-text-opacity: 1;
color: rgb(6 95 70 / var(--tw-text-opacity, 1));
}
.text-fuchsia-300 {
--tw-text-opacity: 1;
color: rgb(240 171 252 / var(--tw-text-opacity, 1));
}
.text-fuchsia-800 {
--tw-text-opacity: 1;
color: rgb(134 25 143 / var(--tw-text-opacity, 1));
}
.text-gray-300 {
--tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity, 1));
@@ -1200,16 +1529,86 @@ video {
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
}
.text-green-300 {
--tw-text-opacity: 1;
color: rgb(134 239 172 / var(--tw-text-opacity, 1));
}
.text-green-500 {
--tw-text-opacity: 1;
color: rgb(34 197 94 / var(--tw-text-opacity, 1));
}
.text-green-800 {
--tw-text-opacity: 1;
color: rgb(22 101 52 / var(--tw-text-opacity, 1));
}
.text-indigo-300 {
--tw-text-opacity: 1;
color: rgb(165 180 252 / var(--tw-text-opacity, 1));
}
.text-indigo-600 {
--tw-text-opacity: 1;
color: rgb(79 70 229 / var(--tw-text-opacity, 1));
}
.text-indigo-800 {
--tw-text-opacity: 1;
color: rgb(55 48 163 / var(--tw-text-opacity, 1));
}
.text-lime-300 {
--tw-text-opacity: 1;
color: rgb(190 242 100 / var(--tw-text-opacity, 1));
}
.text-lime-800 {
--tw-text-opacity: 1;
color: rgb(63 98 18 / var(--tw-text-opacity, 1));
}
.text-neutral-300 {
--tw-text-opacity: 1;
color: rgb(212 212 212 / var(--tw-text-opacity, 1));
}
.text-neutral-800 {
--tw-text-opacity: 1;
color: rgb(38 38 38 / var(--tw-text-opacity, 1));
}
.text-orange-300 {
--tw-text-opacity: 1;
color: rgb(253 186 116 / var(--tw-text-opacity, 1));
}
.text-orange-800 {
--tw-text-opacity: 1;
color: rgb(154 52 18 / var(--tw-text-opacity, 1));
}
.text-pink-300 {
--tw-text-opacity: 1;
color: rgb(249 168 212 / var(--tw-text-opacity, 1));
}
.text-pink-800 {
--tw-text-opacity: 1;
color: rgb(157 23 77 / var(--tw-text-opacity, 1));
}
.text-purple-300 {
--tw-text-opacity: 1;
color: rgb(216 180 254 / var(--tw-text-opacity, 1));
}
.text-purple-800 {
--tw-text-opacity: 1;
color: rgb(107 33 168 / var(--tw-text-opacity, 1));
}
.text-red-300 {
--tw-text-opacity: 1;
color: rgb(252 165 165 / var(--tw-text-opacity, 1));
@@ -1220,22 +1619,107 @@ video {
color: rgb(239 68 68 / var(--tw-text-opacity, 1));
}
.text-red-800 {
--tw-text-opacity: 1;
color: rgb(153 27 27 / var(--tw-text-opacity, 1));
}
.text-rose-300 {
--tw-text-opacity: 1;
color: rgb(253 164 175 / var(--tw-text-opacity, 1));
}
.text-rose-800 {
--tw-text-opacity: 1;
color: rgb(159 18 57 / var(--tw-text-opacity, 1));
}
.text-sky-300 {
--tw-text-opacity: 1;
color: rgb(125 211 252 / var(--tw-text-opacity, 1));
}
.text-sky-800 {
--tw-text-opacity: 1;
color: rgb(7 89 133 / var(--tw-text-opacity, 1));
}
.text-slate-300 {
--tw-text-opacity: 1;
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.text-slate-800 {
--tw-text-opacity: 1;
color: rgb(30 41 59 / var(--tw-text-opacity, 1));
}
.text-stone-300 {
--tw-text-opacity: 1;
color: rgb(214 211 209 / var(--tw-text-opacity, 1));
}
.text-stone-800 {
--tw-text-opacity: 1;
color: rgb(41 37 36 / var(--tw-text-opacity, 1));
}
.text-teal-300 {
--tw-text-opacity: 1;
color: rgb(94 234 212 / var(--tw-text-opacity, 1));
}
.text-teal-800 {
--tw-text-opacity: 1;
color: rgb(17 94 89 / var(--tw-text-opacity, 1));
}
.text-violet-300 {
--tw-text-opacity: 1;
color: rgb(196 181 253 / var(--tw-text-opacity, 1));
}
.text-violet-800 {
--tw-text-opacity: 1;
color: rgb(91 33 182 / var(--tw-text-opacity, 1));
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
}
.text-yellow-300 {
--tw-text-opacity: 1;
color: rgb(253 224 71 / var(--tw-text-opacity, 1));
}
.text-yellow-800 {
--tw-text-opacity: 1;
color: rgb(133 77 14 / var(--tw-text-opacity, 1));
}
.text-zinc-300 {
--tw-text-opacity: 1;
color: rgb(212 212 216 / var(--tw-text-opacity, 1));
}
.text-zinc-800 {
--tw-text-opacity: 1;
color: rgb(39 39 42 / var(--tw-text-opacity, 1));
}
.antialiased {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.shadow-sm {
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-transparent {
--tw-shadow-color: transparent;
--tw-shadow: var(--tw-shadow-colored);
}
.outline {
outline-style: solid;
}
@@ -1252,6 +1736,10 @@ video {
outline-color: #d1d5db;
}
.filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
@@ -1277,10 +1765,6 @@ video {
outline-offset: 2px;
}
.focus-within\:outline-gray-300:focus-within {
outline-color: #d1d5db;
}
.focus-within\:outline-red-500:focus-within {
outline-color: #ef4444;
}
@@ -1300,26 +1784,21 @@ video {
--tw-ring-offset-width: 2px;
}
.hover\:border-gray-300:hover {
--tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
}
.hover\:bg-blue-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1));
}
.hover\:bg-gray-100:hover {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
}
.hover\:bg-gray-400:hover {
--tw-bg-opacity: 1;
background-color: rgb(156 163 175 / var(--tw-bg-opacity, 1));
}
.hover\:bg-gray-50:hover {
--tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
}
.hover\:bg-gray-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1));
@@ -1360,9 +1839,12 @@ video {
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
}
.hover\:shadow-gray-200:hover {
--tw-shadow-color: #e5e7eb;
--tw-shadow: var(--tw-shadow-colored);
.hover\:underline:hover {
text-decoration-line: underline;
}
.focus\:z-10:focus {
z-index: 10;
}
.focus\:border-blue-500:focus {
@@ -1397,11 +1879,22 @@ video {
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.focus\:ring-4:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.focus\:ring-blue-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1));
}
.focus\:ring-gray-100:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(243 244 246 / var(--tw-ring-opacity, 1));
}
.focus-visible\:outline:focus-visible {
outline-style: solid;
}
@@ -1422,10 +1915,6 @@ video {
fill: #f87171;
}
.group:hover .group-hover\:stroke-black {
stroke: #000;
}
.group:hover .group-hover\:stroke-white {
stroke: #fff;
}
@@ -1477,10 +1966,18 @@ video {
margin-left: 1.5rem;
}
.sm\:mt-8 {
margin-top: 2rem;
}
.sm\:block {
display: block;
}
.sm\:flex {
display: flex;
}
.sm\:hidden {
display: none;
}
@@ -1493,8 +1990,8 @@ video {
max-width: 24rem;
}
.sm\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
.sm\:items-center {
align-items: center;
}
.sm\:px-6 {
@@ -1516,6 +2013,11 @@ video {
padding-right: 0px;
}
.sm\:text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
.sm\:text-sm\/6 {
font-size: 0.875rem;
line-height: 1.5rem;
@@ -1543,6 +2045,11 @@ video {
padding-left: 1.25rem;
padding-right: 1.25rem;
}
.md\:py-16 {
padding-top: 4rem;
padding-bottom: 4rem;
}
}
@media (min-width: 1024px) {
@@ -1574,10 +2081,6 @@ video {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.lg\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.lg\:p-8 {
padding: 2rem;
}
@@ -1601,19 +2104,40 @@ video {
.xl\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
.xl\:gap-x-8 {
-moz-column-gap: 2rem;
column-gap: 2rem;
@media (min-width: 1536px) {
.\32xl\:px-0 {
padding-left: 0px;
padding-right: 0px;
}
}
@media (prefers-color-scheme: dark) {
.dark\:hidden {
display: none;
}
.dark\:divide-gray-800 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-opacity: 1;
border-color: rgb(31 41 55 / var(--tw-divide-opacity, 1));
}
.dark\:border-gray-600 {
--tw-border-opacity: 1;
border-color: rgb(75 85 99 / var(--tw-border-opacity, 1));
}
.dark\:border-gray-700 {
--tw-border-opacity: 1;
border-color: rgb(55 65 81 / var(--tw-border-opacity, 1));
}
.dark\:border-gray-800 {
--tw-border-opacity: 1;
border-color: rgb(31 41 55 / var(--tw-border-opacity, 1));
}
.dark\:bg-gray-600 {
--tw-bg-opacity: 1;
background-color: rgb(75 85 99 / var(--tw-bg-opacity, 1));
@@ -1629,6 +2153,11 @@ video {
background-color: rgb(31 41 55 / var(--tw-bg-opacity, 1));
}
.dark\:bg-gray-900 {
--tw-bg-opacity: 1;
background-color: rgb(17 24 39 / var(--tw-bg-opacity, 1));
}
.dark\:bg-red-600 {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity, 1));
@@ -1639,6 +2168,11 @@ video {
color: rgb(209 213 219 / var(--tw-text-opacity, 1));
}
.dark\:text-gray-400 {
--tw-text-opacity: 1;
color: rgb(156 163 175 / var(--tw-text-opacity, 1));
}
.dark\:text-gray-900 {
--tw-text-opacity: 1;
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
@@ -1669,6 +2203,11 @@ video {
background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1));
}
.dark\:hover\:text-white:hover {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
}
.dark\:focus\:border-blue-500:focus {
--tw-border-opacity: 1;
border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
@@ -1678,4 +2217,9 @@ video {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1));
}
.dark\:focus\:ring-gray-700:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(55 65 81 / var(--tw-ring-opacity, 1));
}
}

21
test/lib.nix Normal file
View File

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

21
test/test.nix Normal file
View File

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

View File

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

19
utils/utils.go Normal file
View File

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

View File

@@ -2,7 +2,7 @@
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/circlea.png" alt="Your Company">
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Your Company">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Add an Item</h2>
</div>
@@ -123,6 +123,19 @@
</div>
</div>
<label class="block text-sm font-medium text-gray-900">
Print Mode
</label>
<select name="print-mode" required class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option selected value="CreateBooklet">Create Booklet</option>
<option value="LongEdge">Long Edge</option>
<option value="ShortEdge">Short Edge</option>
</select>
<p class="text-sm">Choose 'Create Booklet' if its just a normal PDF (not converted to booklet already)</p>
<p class="mt-10 text-center text-sm/6 text-red-500">
{{ .data.error }}
</p>

View File

@@ -2,7 +2,7 @@
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/circlea.png" alt="Your Company">
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Your Company">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Add Tag</h2>
</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>
<form action="/cart/edit" method="POST">
<div class="flex justify-between items-center">
<div class="flex items-center gap-4">
<input type="hidden" id="{{ .ID }}" name="id" value="{{ .ID }}">
<button type="submit" name="action" value="decrease"
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="M4.5 9.5H13.5" stroke="" stroke-width="1.6" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
<div class="flex items-center gap-4">
<input type="number" name="amount" value="{{ .Quantity }}" step="1" min="0" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
<button type="submit" name="action" value="setAmount"
class="block w-full bg-indigo-500 rounded-md px-3 py-1.5 text-base text-white placeholder:text-gray-400 sm:text-sm/6 duration-500 hover:bg-indigo-700">
Set Amount
</button>
<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>
<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>
</div>
<h6 class="text-indigo-600 font-manrope font-bold text-2xl leading-9 text-right">{{ .Quantity}} x {{ .ItemVariant.Price }}€</h6>
</div>
</form>
@@ -88,7 +74,7 @@
<div class="max-lg:max-w-lg max-lg:mx-auto">
<p class="font-normal text-base leading-7 text-gray-500 text-center mb-5 mt-6">Shipping calculated at checkout</p>
<button type="submit"
class="rounded-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>
</form>

View File

@@ -2,26 +2,26 @@
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/static/img/circlea.png" alt="Your Company">
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Logo">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Checkout</h2>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form class="space-y-6" action="#" method="POST" enctype="multipart/form-data">
<form class="space-y-6" action="/checkout" method="POST" enctype="multipart/form-data">
<input type="hidden" name="shippingMethod" value="{{ .shippingMethod }}" required>
{{ if .askAddress }}
<div>
<label for="name" class="block text-sm/6 font-medium text-gray-900">First Name</label>
<label for="firstName" class="block text-sm/6 font-medium text-gray-900">First Name</label>
<div class="mt-2">
<input type="text" name="firstname" id="firstname" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
<input type="text" name="firstName" id="firstName" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<label for="lastname" class="block text-sm/6 font-medium text-gray-900">Last Name</label>
<label for="lastName" class="block text-sm/6 font-medium text-gray-900">Last Name</label>
<div class="mt-2">
<input type="text" name="lastname" id="lastname" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
<input type="text" name="lastName" id="lastName" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
@@ -34,25 +34,25 @@
<div>
<label for="lastname" class="block text-sm/6 font-medium text-gray-900">Postal Code</label>
<label for="postalCode" class="block text-sm/6 font-medium text-gray-900">Postal Code</label>
<div class="mt-2">
<input type="text" name="lastname" id="lastname" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
<input type="text" name="postalCode" id="postalCode" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<label for="lastname" class="block text-sm/6 font-medium text-gray-900">City</label>
<label for="city" class="block text-sm/6 font-medium text-gray-900">City</label>
<div class="mt-2">
<input type="text" name="lastname" id="lastname" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
<input type="text" name="city" id="city" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<label for="lastname" class="block text-sm/6 font-medium text-gray-900">Country</label>
<label for="country" class="block text-sm/6 font-medium text-gray-900">Country</label>
<div class="mt-2">
<input type="text" name="lastname" id="lastname" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
<input type="text" name="country" id="country" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
@@ -60,17 +60,17 @@
<div>
<label for="lastname" class="block text-sm/6 font-medium text-gray-900">E-Mail</label>
<label for="email" class="block text-sm/6 font-medium text-gray-900">E-Mail</label>
<p class="text-sm/8 font-small text-gray-600">Without E-Mail you wont receive an order confirmation.</p>
<div class="mt-2">
<input type="email" name="lastname" id="lastname" 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="email" name="email" id="email" class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<label for="description" class="block text-sm/6 font-medium text-gray-900" for="passwordConfirmation">Comment</label>
<textarea id="description" name="description" type="textarea" class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-300 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-500 dark:focus:border-blue-500 focus:outline-none focus:ring"></textarea>
<label for="comment" class="block text-sm/6 font-medium text-gray-900" for="passwordConfirmation">Comment</label>
<textarea id="comment" name="comment" type="textarea" class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-300 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-500 dark:focus:border-blue-500 focus:outline-none focus:ring"></textarea>
</div>
<p class="mt-10 text-center text-sm/6 text-red-500">

23
views/colors.html Normal file
View File

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

View File

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

View File

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

38
views/editorders.html Normal file
View File

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

View File

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

28
views/invites.html Normal file
View File

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

View File

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

106
views/order.html Normal file
View File

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

88
views/orderpreview.html Normal file
View File

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

72
views/printvariant.html Normal file
View File

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

View File

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

59
views/registertoken.html Normal file
View File

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

View File

@@ -5,27 +5,28 @@
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex flex-col md:flex-row -mx-4">
<div class="md:flex-1 px-4">
<div class="h-[460px] rounded-lg bg-gray-300 dark:bg-gray-700 mb-4">
<div class="rounded-lg bg-gray-300 dark:bg-gray-700 mb-4">
<img class="w-full h-full object-cover" src="/{{ .data.shopItem.Image}}" alt="Product Image">
</div>
<div class="flex -mx-2 mb-4">
{{ if .loggedIn }}
<input type="hidden" id="{{ .data.shopItem.ID}}" name="ShopItemId" value="{{ .data.shopItem.ID }}">
<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 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>
</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>
<button type="button" class="w-full bg-red-900 dark:bg-red-600 text-white py-2 px-4 rounded-lg font-bold hover:bg-gray-800 dark:hover:bg-gray-700"><a href="{{ .data.shopItem.ID }}/delete">Delete</a></button>
</div>
{{ end }}
</div>
@@ -36,6 +37,7 @@
{{ .data.shopItem.Abstract }}
</p>
{{ if .loggedIn }}
<div class="flex mb-4">
<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">
@@ -45,15 +47,16 @@
{{ end }}
</select>
</div>
{{ end }}
<div class="flex mb-4">
<div class="mr-4">
<span class="font-bold text-gray-700 dark:text-gray-300">Tags:</span>
<span class="text-gray-600 dark:text-gray-300">
<p class="mt-1 text-sm text-gray-500">
{{ range .data.shopItem.Tags }}
{{ .Name }}
<a href="/tags/{{ .ID }}"><span class="bg-{{ .Color }}-100 text-{{ .Color }}-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded-sm dark:bg-{{ .Color }}-900 dark:text-{{ .Color }}-300">{{ .Name }}</span></a>
{{ end }}
</span>
</p>
</div>
</div>
<!--
@@ -79,25 +82,4 @@
</div>
<!--
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<div class="bg-gray-200 rounded m-4 p-4">
<div class="relative flex rounded-lg justify-center items-center">
<img src="/{{ .data.shopItem.Image }}" alt="shopping image"
class="object-cover items-center justify-center rounded">
</div>
<h3 class="text-lg">{{ .data.shopItem.Name }}</h3>
<i class="text-xs">{{ .data.shopItem.Abstract }}</i>
<p class="text-xs">{{ .data.shopItem.Description }}</p>
<p class="">Price: {{ .data.shopItem.Price }}</p>
{{ if .loggedIn }}
<div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0 px-4 sm:px-8">
<a href="{{ .data.shopItem.ID }}/edit" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">Edit</a>
<a href="{{ .data.shopItem.ID }}/delete" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-red-300 hover:bg-gray-700 hover:text-white">Delete</a>
</div>
{{ end }}
</div>
</div>
-->
{{ template "footer.html" . }}

View File

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

View File

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