Compare commits
89 Commits
89f7c8c4bb
...
more-print
| Author | SHA1 | Date | |
|---|---|---|---|
|
134730396a
|
|||
|
5f53d66bc4
|
|||
|
459c873986
|
|||
|
ef2e6c99a7
|
|||
|
e29287c29d
|
|||
| f55470636f | |||
|
8f89c14961
|
|||
|
6f5c0354cc
|
|||
|
6ef7c53001
|
|||
|
d4e7401586
|
|||
|
1688e61ccb
|
|||
|
861343b338
|
|||
|
68c8654bf3
|
|||
|
e62a45372f
|
|||
|
d17c33f6ee
|
|||
|
ae36903e73
|
|||
|
1a5df21fa8
|
|||
|
9c15514758
|
|||
|
27cf7c37cf
|
|||
|
03f1ce361a
|
|||
| c55cf4480b | |||
|
bcbb091dfb
|
|||
|
adfb3df283
|
|||
|
1525f44687
|
|||
|
3955d8626a
|
|||
|
6d63e53200
|
|||
|
cca0b2775c
|
|||
|
98c75c111f
|
|||
|
b2735e178f
|
|||
|
1c9fc230b1
|
|||
|
6c2b3964fe
|
|||
|
19ce41aca7
|
|||
|
763bb35a45
|
|||
|
6130843aa7
|
|||
|
667c3eba13
|
|||
|
e22cc0b243
|
|||
|
202c845bee
|
|||
|
d839416fdd
|
|||
|
821f4e526f
|
|||
|
9d2819cac4
|
|||
|
fd46f35023
|
|||
|
6943e3c9b7
|
|||
|
a90131c8be
|
|||
|
a04d057bce
|
|||
|
37ffeaa0f3
|
|||
|
961113ebd6
|
|||
|
529741e150
|
|||
|
8a7d66f815
|
|||
|
fa561c921d
|
|||
|
a9170b63b7
|
|||
|
f1e191a294
|
|||
|
6e14716305
|
|||
|
f70d053a23
|
|||
|
fb5091aad3
|
|||
|
2e82e3a8b9
|
|||
|
d2d0f39e33
|
|||
|
fc85113cd6
|
|||
|
08d79e2c38
|
|||
|
6a8ab81b88
|
|||
|
584030431d
|
|||
|
a8cb853c92
|
|||
|
0b4439647a
|
|||
|
2c4c21bd4d
|
|||
|
c4527ff228
|
|||
|
256b41c880
|
|||
|
af6787831a
|
|||
|
ec1a8b155a
|
|||
|
122651677f
|
|||
|
af11f88769
|
|||
|
0bcec807c8
|
|||
|
a3b066859b
|
|||
|
b349c4baba
|
|||
|
e406eda9c9
|
|||
|
a1fc053fa8
|
|||
|
1da5e3e8b4
|
|||
|
adac366896
|
|||
|
b14deeb24f
|
|||
|
d5c3d7fe75
|
|||
|
b55bf67e57
|
|||
|
d43401dc62
|
|||
|
8f5dd27ae0
|
|||
|
e864a75678
|
|||
|
0436d3a2bd
|
|||
|
490a9f1444
|
|||
|
fdb11bc57c
|
|||
|
f87a6352dd
|
|||
|
9e3a04cd78
|
|||
|
f39b6205d1
|
|||
|
5a7565663d
|
17
.gitea/workflows/flake-check.yml
Normal file
17
.gitea/workflows/flake-check.yml
Normal 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 ./...
|
||||
11
README.md
11
README.md
@@ -22,3 +22,14 @@ 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
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"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 {
|
||||
@@ -26,6 +26,8 @@ type CartItemController interface {
|
||||
CheckoutHandler(*gin.Context)
|
||||
OrderView(*gin.Context)
|
||||
OrderHandler(*gin.Context)
|
||||
OrdersView(*gin.Context)
|
||||
OrdersHandler(*gin.Context)
|
||||
}
|
||||
|
||||
type cartItemController struct{}
|
||||
@@ -34,30 +36,40 @@ func NewCartItemController() CartItemController {
|
||||
return &cartItemController{}
|
||||
}
|
||||
|
||||
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")
|
||||
// getSetCookieValue retrieves the value of a cookie from the Set-Cookie header
|
||||
func getSetCookieValue(c *gin.Context, cookieName string) string {
|
||||
// Check the Set-Cookie headers
|
||||
cookies := c.Writer.Header()["Set-Cookie"]
|
||||
for _, cookie := range cookies {
|
||||
if strings.HasPrefix(cookie, cookieName+"=") {
|
||||
// Extract the cookie value
|
||||
parts := strings.SplitN(cookie, ";", 2)
|
||||
if len(parts) > 0 {
|
||||
return strings.TrimPrefix(parts[0], cookieName+"=")
|
||||
}
|
||||
}
|
||||
}
|
||||
return 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(16)
|
||||
//we need to check if we already set cookie in the response so that we dont do this multiple times
|
||||
responseCookie := getSetCookieValue(ctx, "session_id")
|
||||
|
||||
if len(responseCookie) != 0 {
|
||||
return responseCookie
|
||||
}
|
||||
|
||||
sessionId = utils.GenerateSessionId(16)
|
||||
ctx.SetCookie("session_id", sessionId, 3600, "/", "", false, true)
|
||||
}
|
||||
|
||||
return sessionId
|
||||
}
|
||||
|
||||
func GenerateToken() string {
|
||||
return generateSessionId(16)
|
||||
}
|
||||
|
||||
func (rc *cartItemController) NewCartItemFromForm(ctx *gin.Context) (models.CartItem, error) {
|
||||
sessionId := GetSessionId(ctx)
|
||||
shopItemIdStr := ctx.PostForm("ShopItemId")
|
||||
@@ -136,7 +148,7 @@ func (rc *cartItemController) NewAddressFromForm(ctx *gin.Context) (models.Addre
|
||||
func (rc *cartItemController) NewOrderFromForm(ctx *gin.Context) (models.Order, error) {
|
||||
sessionId := GetSessionId(ctx)
|
||||
status := models.OrderStatus("AwaitingConfirmation")
|
||||
token := GenerateToken()
|
||||
token := utils.GenerateToken()
|
||||
email := ctx.PostForm("email")
|
||||
comment := ctx.PostForm("comment")
|
||||
firstName := ctx.PostForm("firstName")
|
||||
@@ -168,7 +180,7 @@ func (rc *cartItemController) NewOrderFromForm(ctx *gin.Context) (models.Order,
|
||||
return models.Order{}, err
|
||||
}
|
||||
|
||||
cartItem := models.Order{
|
||||
order := models.Order{
|
||||
SessionId: sessionId,
|
||||
Status: status,
|
||||
Token: token,
|
||||
@@ -184,7 +196,7 @@ func (rc *cartItemController) NewOrderFromForm(ctx *gin.Context) (models.Order,
|
||||
CartItems: cartItems,
|
||||
}
|
||||
|
||||
return cartItem, nil
|
||||
return order, nil
|
||||
}
|
||||
|
||||
func (rc *cartItemController) Create(c *gin.Context) {
|
||||
@@ -235,10 +247,10 @@ 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}})
|
||||
@@ -311,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
|
||||
}
|
||||
@@ -358,21 +391,31 @@ func (rc *cartItemController) CheckoutHandler(c *gin.Context) {
|
||||
existingOrder, err := repositories.Orders.GetBySession(order.SessionId)
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
_, err = repositories.Orders.Create(order)
|
||||
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
|
||||
repositories.Orders.Update(order)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
data := CreateSessionData(c, gin.H{
|
||||
"error": err,
|
||||
"success": "",
|
||||
})
|
||||
|
||||
c.HTML(http.StatusOK, "cart.html", data)
|
||||
return
|
||||
_, err := repositories.Orders.Update(order)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
shipping, err := models.GetShippingMethod(order.Shipping)
|
||||
@@ -382,7 +425,7 @@ func (rc *cartItemController) CheckoutHandler(c *gin.Context) {
|
||||
"success": "",
|
||||
})
|
||||
|
||||
c.HTML(http.StatusOK, "cart.html", data)
|
||||
c.HTML(http.StatusOK, "error.html", data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -393,7 +436,7 @@ func (rc *cartItemController) CheckoutHandler(c *gin.Context) {
|
||||
"success": "",
|
||||
})
|
||||
|
||||
c.HTML(http.StatusOK, "cart.html", data)
|
||||
c.HTML(http.StatusOK, "error.html", data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -480,6 +523,11 @@ func (rc *cartItemController) OrderHandler(c *gin.Context) {
|
||||
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 {
|
||||
@@ -491,3 +539,67 @@ func (rc *cartItemController) OrderHandler(c *gin.Context) {
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
146
controllers/printController.go
Normal file
146
controllers/printController.go
Normal 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)
|
||||
}
|
||||
@@ -3,20 +3,20 @@ package controllers
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"path/filepath"
|
||||
"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)
|
||||
@@ -27,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)
|
||||
@@ -38,7 +40,7 @@ type ShopItemController interface {
|
||||
AddTagHandler(*gin.Context)
|
||||
}
|
||||
|
||||
type shopItemController struct {}
|
||||
type shopItemController struct{}
|
||||
|
||||
func NewShopItemController() ShopItemController {
|
||||
return &shopItemController{}
|
||||
@@ -66,6 +68,9 @@ func (rc *shopItemController) GetById(c *gin.Context) {
|
||||
ReplyOK(c, shopItem)
|
||||
}
|
||||
|
||||
// this currently creates quite big preview images
|
||||
// workaround is running the following command in the uploads folder:
|
||||
// for file in *.png; do convert "$file" -resize 35% "$file"; done
|
||||
func (rc *shopItemController) NewShopItemFromForm(ctx *gin.Context) (models.ShopItem, error) {
|
||||
defaultImagePath := "static/img/zine.jpg"
|
||||
name := ctx.PostForm("name")
|
||||
@@ -77,12 +82,13 @@ func (rc *shopItemController) NewShopItemFromForm(ctx *gin.Context) (models.Shop
|
||||
tagIds := ctx.PostFormArray("tags[]")
|
||||
image, err := ctx.FormFile("image")
|
||||
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 := ""
|
||||
@@ -90,19 +96,30 @@ 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")
|
||||
cmd := exec.Command("pdftoppm", "-png", "-singlefile", dstPdf, dstPdf+".preview")
|
||||
_, err := cmd.Output()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error during pdftoppm: ", err.Error())
|
||||
}
|
||||
|
||||
cmd2 := exec.Command("convert", dstImage, "-resize", "35%", dstImage)
|
||||
_, err = cmd2.Output()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error during resizing preview image: ", err.Error())
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
if name == "" || description == "" {
|
||||
@@ -125,31 +142,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)
|
||||
|
||||
@@ -198,7 +218,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"))
|
||||
|
||||
@@ -225,7 +244,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"))
|
||||
|
||||
@@ -241,25 +260,23 @@ 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, "error.html", gin.H{"data": gin.H{"error": "Item does not exist"}})
|
||||
return
|
||||
}
|
||||
|
||||
//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, "error.html", gin.H{"data": gin.H{"error": err}})
|
||||
return
|
||||
}
|
||||
|
||||
data := CreateSessionData(c, gin.H{
|
||||
"shopItem": shopItem,
|
||||
"tags": tags,
|
||||
"tags": tags,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
c.HTML(http.StatusBadRequest, "shopitem.html", data)
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "shopitem.html", data)
|
||||
}
|
||||
|
||||
@@ -267,25 +284,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)
|
||||
@@ -310,83 +326,253 @@ 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{})
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
fmt.Println(shopItem)
|
||||
|
||||
data := CreateSessionData(c, gin.H{
|
||||
"error": "",
|
||||
"success": "",
|
||||
"shopItem": shopItem,
|
||||
"tags": tags,
|
||||
})
|
||||
|
||||
c.HTML(http.StatusOK, "edititem.html", data)
|
||||
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())
|
||||
}
|
||||
|
||||
cmd2 := exec.Command("convert", dstImage, "-resize", "35%", dstImage)
|
||||
_, err = cmd2.Output()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error during resizing preview image: ", 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 GetCheckedTags(item models.ShopItem) ([]models.CheckedTag, error) {
|
||||
allTags, err := repositories.Tags.GetAll()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tags []models.CheckedTag
|
||||
for _, tag := range allTags {
|
||||
tmpTag := models.CheckedTag{
|
||||
Tag: tag,
|
||||
Checked: "",
|
||||
}
|
||||
|
||||
for _, itemTag := range item.Tags {
|
||||
if itemTag.Name == tmpTag.Name {
|
||||
tmpTag.Checked = "checked"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
tags = append(tags, tmpTag)
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (rc *shopItemController) getEditTemplatData(shopItem models.ShopItem) (gin.H, error) {
|
||||
tags, err := GetCheckedTags(shopItem)
|
||||
|
||||
if err != nil {
|
||||
return gin.H{}, err
|
||||
}
|
||||
|
||||
priceBW := ""
|
||||
priceColored := ""
|
||||
for _, variant := range shopItem.Variants {
|
||||
if variant.Name == "B/W" {
|
||||
priceBW = fmt.Sprintf("%.2f", variant.Price)
|
||||
}
|
||||
|
||||
if variant.Name == "Colored" {
|
||||
priceColored = fmt.Sprintf("%.2f", variant.Price)
|
||||
}
|
||||
}
|
||||
|
||||
templateData := gin.H{
|
||||
"error": "",
|
||||
"success": "",
|
||||
"shopItem": shopItem,
|
||||
"tags": tags,
|
||||
"priceBW": priceBW,
|
||||
"priceColored": priceColored,
|
||||
}
|
||||
|
||||
id := fmt.Sprintf("%d", shopItem.ID)
|
||||
nextShopItem, err := repositories.ShopItems.GetNextOfId(id)
|
||||
|
||||
if err == nil {
|
||||
fmt.Println("Setting nextitem")
|
||||
fmt.Println(nextShopItem)
|
||||
templateData["nextShopItem"] = nextShopItem
|
||||
}
|
||||
|
||||
previousShopItem, err := repositories.ShopItems.GetPreviousOfId(id)
|
||||
if err == nil {
|
||||
templateData["previousShopItem"] = previousShopItem
|
||||
}
|
||||
|
||||
return templateData, nil
|
||||
}
|
||||
|
||||
func (rc *shopItemController) EditItemView(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
shopItem, err := repositories.ShopItems.GetById(id)
|
||||
|
||||
if err != nil {
|
||||
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
|
||||
}
|
||||
|
||||
templateData, err := rc.getEditTemplatData(shopItem)
|
||||
|
||||
if err != nil {
|
||||
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
|
||||
}
|
||||
|
||||
data := CreateSessionData(c, templateData)
|
||||
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
|
||||
|
||||
tags, err := repositories.Tags.GetAll()
|
||||
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
|
||||
|
||||
templateData, err := rc.getEditTemplatData(newShopItem)
|
||||
|
||||
if err != nil {
|
||||
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{ "error": err })
|
||||
return
|
||||
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
|
||||
}
|
||||
|
||||
_, err = repositories.ShopItems.Update(newShopItem)
|
||||
if err != nil {
|
||||
data := CreateSessionData(c, gin.H{
|
||||
"error": err,
|
||||
"success": "",
|
||||
"tags": tags,
|
||||
})
|
||||
|
||||
templateData["error"] = err
|
||||
data := CreateSessionData(c, templateData)
|
||||
c.HTML(http.StatusOK, "edititem.html", data)
|
||||
return
|
||||
}
|
||||
|
||||
data := CreateSessionData(c, gin.H{
|
||||
"error": "",
|
||||
"success": fmt.Sprintf("Item '%s' Updated", newShopItem.Name),
|
||||
"tags": tags,
|
||||
})
|
||||
|
||||
templateData["success"] = fmt.Sprintf("Item '%s' Updated", newShopItem.Name)
|
||||
data := CreateSessionData(c, templateData)
|
||||
c.HTML(http.StatusOK, "edititem.html", data)
|
||||
}
|
||||
|
||||
@@ -395,28 +581,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": "",
|
||||
})
|
||||
|
||||
@@ -427,7 +612,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,
|
||||
})
|
||||
|
||||
@@ -438,23 +623,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
|
||||
}
|
||||
}
|
||||
@@ -471,14 +658,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": "",
|
||||
})
|
||||
|
||||
@@ -493,7 +680,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{
|
||||
@@ -544,7 +731,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) {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
|
||||
"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{}
|
||||
@@ -36,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)
|
||||
@@ -140,10 +141,12 @@ func (rc *UserController) LoginHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
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{
|
||||
"loggedIn": exists,
|
||||
"isAdmin": userImpl.IsAdmin,
|
||||
"data": extra,
|
||||
}
|
||||
}
|
||||
@@ -153,11 +156,34 @@ 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": "",
|
||||
}
|
||||
|
||||
@@ -165,6 +191,43 @@ func (rc *UserController) RegisterHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
tokenExists, err := repositories.Tokens.Exists(token)
|
||||
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
data := gin.H{
|
||||
"error": err,
|
||||
"success": "",
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "register.html", data)
|
||||
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": "",
|
||||
"success": "You successfully registered. Try logging in.",
|
||||
@@ -174,6 +237,39 @@ func (rc *UserController) RegisterHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (rc *UserController) RegisterView(c *gin.Context) {
|
||||
data := gin.H{
|
||||
"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": "",
|
||||
@@ -204,80 +300,71 @@ func (rc *UserController) ResetHandler(c *gin.Context) {
|
||||
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))
|
||||
itemOrder := c.Query("order")
|
||||
|
||||
var shopItems []models.ShopItem
|
||||
if itemOrder == "newestFirst" {
|
||||
shopItems, _ = repositories.ShopItems.GetAllNewestFirst()
|
||||
} else if itemOrder == "oldestFirst" {
|
||||
shopItems, _ = repositories.ShopItems.GetAllNewestLast()
|
||||
} else if itemOrder == "az" {
|
||||
shopItems, _ = repositories.ShopItems.GetAllLexicalFirst()
|
||||
} else if itemOrder == "za" {
|
||||
shopItems, _ = repositories.ShopItems.GetAllLexicalLast()
|
||||
} else {
|
||||
shopItems, _ = repositories.ShopItems.GetAllNewestFirst()
|
||||
}
|
||||
|
||||
data := CreateSessionData(c, gin.H{
|
||||
"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) {
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1742669843,
|
||||
"narHash": "sha256-G5n+FOXLXcRx+3hCJ6Rt6ZQyF1zqQ0DL0sWAMn2Nk0w=",
|
||||
"lastModified": 1744463964,
|
||||
"narHash": "sha256-LWqduOgLHCFxiTNYi3Uj5Lgz0SR+Xhw3kr/3Xd0GPTM=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "1e5b653dff12029333a6546c11e108ede13052eb",
|
||||
"rev": "2631b0b7abcea6e640ce31cd78ea58910d31e650",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
116
flake.nix
116
flake.nix
@@ -13,15 +13,127 @@
|
||||
(utils.lib.eachSystem (utils.lib.defaultSystems) ( system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
in rec
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
go
|
||||
gotools
|
||||
poppler_utils #get first pdf page to png
|
||||
cups
|
||||
imagemagick
|
||||
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
|
||||
pkgs.cups
|
||||
pkgs.imagemagick
|
||||
];
|
||||
|
||||
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 pkgs.cups pkgs.imagemagick ]}"
|
||||
${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
2
go.mod
@@ -1,4 +1,4 @@
|
||||
module example.com/gin/test
|
||||
module git.dynamicdiscord.de/kalipso/zineshop
|
||||
|
||||
go 1.23.3
|
||||
|
||||
|
||||
84
main.go
84
main.go
@@ -9,15 +9,16 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/joho/godotenv"
|
||||
|
||||
"example.com/gin/test/controllers"
|
||||
"example.com/gin/test/middlewares"
|
||||
"example.com/gin/test/repositories"
|
||||
"git.dynamicdiscord.de/kalipso/zineshop/controllers"
|
||||
"git.dynamicdiscord.de/kalipso/zineshop/middlewares"
|
||||
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
|
||||
)
|
||||
|
||||
var (
|
||||
shopItemController controllers.ShopItemController = controllers.NewShopItemController()
|
||||
userController controllers.UserController = controllers.UserController{}
|
||||
cartItemController controllers.CartItemController = controllers.NewCartItemController()
|
||||
printController controllers.PrintController = controllers.NewPrintController()
|
||||
authValidator middlewares.AuthValidator = middlewares.AuthValidator{}
|
||||
)
|
||||
|
||||
@@ -51,58 +52,53 @@ func main() {
|
||||
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("/order/:token", cartItemController.OrderView)
|
||||
viewRoutes.POST("/order", cartItemController.OrderHandler)
|
||||
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"))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
112
models/printer.go
Normal file
112
models/printer.go
Normal file
@@ -0,0 +1,112 @@
|
||||
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 MediaType=Thick1 -o Combination=Booklet -o PageSize=A5"
|
||||
TriFold PrintOption = "-o Fold=TriFold -o Binding=TopBinding"
|
||||
LongEdgeA5 PrintOption = "-o PageSize=A5"
|
||||
ShortEdgeA5 PrintOption = "-o Binding=TopBinding -o PageSize=A5"
|
||||
CreateBookletA5 PrintOption = "-o Combination=Booklet -o PageSize=A6"
|
||||
)
|
||||
|
||||
type PrintJob struct {
|
||||
Pdf string
|
||||
Amount uint
|
||||
Options []PrintOption
|
||||
}
|
||||
|
||||
func GetPrintMode(mode string) PrintOption {
|
||||
if mode == "CreateBooklet" {
|
||||
return CreateBooklet
|
||||
}
|
||||
|
||||
if mode == "LongEdge" {
|
||||
return LongEdge
|
||||
}
|
||||
|
||||
if mode == "ShortEdge" {
|
||||
return ShortEdge
|
||||
}
|
||||
|
||||
if mode == "CreateBookletA5" {
|
||||
return CreateBookletA5
|
||||
}
|
||||
|
||||
if mode == "LongEdgeA5" {
|
||||
return LongEdgeA5
|
||||
}
|
||||
|
||||
if mode == "ShortEdgeA5" {
|
||||
return ShortEdgeA5
|
||||
}
|
||||
|
||||
if mode == "TriFold" {
|
||||
return TriFold
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -2,22 +2,56 @@ 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;"`
|
||||
}
|
||||
|
||||
type CheckedTag struct {
|
||||
Tag
|
||||
Checked string
|
||||
}
|
||||
|
||||
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 +61,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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
"strconv"
|
||||
|
||||
"example.com/gin/test/models"
|
||||
"git.dynamicdiscord.de/kalipso/zineshop/models"
|
||||
)
|
||||
|
||||
type OrderRepository interface {
|
||||
@@ -15,6 +15,7 @@ type OrderRepository interface {
|
||||
GetByToken(string) (models.Order, error)
|
||||
Update(models.Order) (models.Order, error)
|
||||
DeleteById(string) error
|
||||
DeleteByToken(string) error
|
||||
}
|
||||
|
||||
type GORMOrderRepository struct {
|
||||
@@ -95,3 +96,8 @@ func (r *GORMOrderRepository) DeleteById(id string) error {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import(
|
||||
"strconv"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"example.com/gin/test/models"
|
||||
"git.dynamicdiscord.de/kalipso/zineshop/models"
|
||||
)
|
||||
|
||||
type CartItemRepository interface {
|
||||
|
||||
86
repositories/registerTokenRepository.go
Normal file
86
repositories/registerTokenRepository.go
Normal 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
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
"os"
|
||||
|
||||
"example.com/gin/test/models"
|
||||
"git.dynamicdiscord.de/kalipso/zineshop/models"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -14,6 +14,7 @@ var (
|
||||
Tags TagRepository
|
||||
CartItems CartItemRepository
|
||||
Orders OrderRepository
|
||||
Tokens RegisterTokenRepository
|
||||
)
|
||||
|
||||
func InitRepositories() {
|
||||
@@ -27,7 +28,8 @@ func InitRepositories() {
|
||||
&models.User{},
|
||||
&models.Tag{},
|
||||
&models.CartItem{},
|
||||
&models.Order{})
|
||||
&models.Order{},
|
||||
&models.RegisterToken{})
|
||||
|
||||
if err != nil {
|
||||
panic("failed to migrate database")
|
||||
@@ -38,4 +40,5 @@ func InitRepositories() {
|
||||
Tags = NewGORMTagRepository(db)
|
||||
CartItems = NewGORMCartItemRepository(db)
|
||||
Orders = NewGORMOrderRepository(db)
|
||||
Tokens = NewGORMRegisterTokenRepository(db)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
package repositories
|
||||
|
||||
import(
|
||||
"strconv"
|
||||
import (
|
||||
"fmt"
|
||||
"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)
|
||||
GetAllSorted(string) ([]models.ShopItem, error)
|
||||
GetAllNewestFirst() ([]models.ShopItem, error)
|
||||
GetAllNewestLast() ([]models.ShopItem, error)
|
||||
GetAllLexicalFirst() ([]models.ShopItem, error)
|
||||
GetAllLexicalLast() ([]models.ShopItem, error)
|
||||
GetAllPublic() ([]models.ShopItem, error)
|
||||
GetById(string) (models.ShopItem, error)
|
||||
GetNextOfId(string) (models.ShopItem, error)
|
||||
GetPreviousOfId(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 +39,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
|
||||
@@ -44,6 +52,29 @@ func (r *GORMShopItemRepository) GetAll() ([]models.ShopItem, error) {
|
||||
return shopItems, result.Error
|
||||
}
|
||||
|
||||
func (r *GORMShopItemRepository) GetAllSorted(sortString string) ([]models.ShopItem, error) {
|
||||
var shopItems []models.ShopItem
|
||||
result := r.DB.Preload("Tags").Preload("Variants").Order(sortString).Find(&shopItems)
|
||||
|
||||
return shopItems, result.Error
|
||||
}
|
||||
|
||||
func (r *GORMShopItemRepository) GetAllNewestFirst() ([]models.ShopItem, error) {
|
||||
return r.GetAllSorted("created_at desc")
|
||||
}
|
||||
|
||||
func (r *GORMShopItemRepository) GetAllNewestLast() ([]models.ShopItem, error) {
|
||||
return r.GetAllSorted("created_at asc")
|
||||
}
|
||||
|
||||
func (r *GORMShopItemRepository) GetAllLexicalFirst() ([]models.ShopItem, error) {
|
||||
return r.GetAllSorted("name asc")
|
||||
}
|
||||
|
||||
func (r *GORMShopItemRepository) GetAllLexicalLast() ([]models.ShopItem, error) {
|
||||
return r.GetAllSorted("name desc")
|
||||
}
|
||||
|
||||
func (r *GORMShopItemRepository) GetAllPublic() ([]models.ShopItem, error) {
|
||||
var shopItems []models.ShopItem
|
||||
result := r.DB.Preload("Tags").Preload("Variants").Where("is_public = 1").Find(&shopItems)
|
||||
@@ -68,6 +99,47 @@ func (r *GORMShopItemRepository) GetById(id string) (models.ShopItem, error) {
|
||||
return shopItem, nil
|
||||
}
|
||||
|
||||
func (r *GORMShopItemRepository) GetNextOfId(id string) (models.ShopItem, error) {
|
||||
var nextItem models.ShopItem
|
||||
if err := r.DB.Where("id > ?", id).Order("id asc").First(&nextItem).Error; err != nil {
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return models.ShopItem{}, err
|
||||
} else {
|
||||
return models.ShopItem{}, fmt.Errorf("No Item found")
|
||||
}
|
||||
}
|
||||
return nextItem, nil
|
||||
}
|
||||
|
||||
func (r *GORMShopItemRepository) GetPreviousOfId(id string) (models.ShopItem, error) {
|
||||
var previousItem models.ShopItem
|
||||
if err := r.DB.Where("id < ?", id).Order("id desc").First(&previousItem).Error; err != nil {
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return models.ShopItem{}, err
|
||||
} else {
|
||||
return models.ShopItem{}, fmt.Errorf("No Item found")
|
||||
}
|
||||
}
|
||||
return previousItem, 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 +157,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 {
|
||||
|
||||
@@ -5,7 +5,7 @@ import(
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"example.com/gin/test/models"
|
||||
"git.dynamicdiscord.de/kalipso/zineshop/models"
|
||||
)
|
||||
|
||||
type TagRepository interface {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
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
BIN
static/img/logo-white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
@@ -574,10 +574,6 @@ video {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inset-0 {
|
||||
inset: 0px;
|
||||
}
|
||||
|
||||
.inset-y-0 {
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
@@ -587,26 +583,14 @@ video {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.-bottom-\[1\.75rem\] {
|
||||
bottom: -1.75rem;
|
||||
}
|
||||
|
||||
.end-0 {
|
||||
inset-inline-end: 0px;
|
||||
}
|
||||
|
||||
.left-1\/2 {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.start-0 {
|
||||
inset-inline-start: 0px;
|
||||
}
|
||||
|
||||
.col-span-12 {
|
||||
grid-column: span 12 / span 12;
|
||||
}
|
||||
|
||||
.col-span-3 {
|
||||
grid-column: span 3 / span 3;
|
||||
}
|
||||
|
||||
.m-2 {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
@@ -630,11 +614,6 @@ video {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.-mx-5 {
|
||||
margin-left: -1.25rem;
|
||||
margin-right: -1.25rem;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
@@ -655,6 +634,10 @@ video {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.me-2 {
|
||||
margin-inline-end: 0.5rem;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
@@ -671,10 +654,6 @@ video {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.ms-2 {
|
||||
margin-inline-start: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
@@ -719,16 +698,6 @@ video {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.size-5 {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.size-6 {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.h-10 {
|
||||
height: 2.5rem;
|
||||
}
|
||||
@@ -769,10 +738,6 @@ video {
|
||||
width: 33.333333%;
|
||||
}
|
||||
|
||||
.w-10 {
|
||||
width: 2.5rem;
|
||||
}
|
||||
|
||||
.w-12 {
|
||||
width: 3rem;
|
||||
}
|
||||
@@ -793,14 +758,6 @@ video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.w-1\/4 {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.w-2\/4 {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.max-w-2xl {
|
||||
max-width: 42rem;
|
||||
}
|
||||
@@ -829,6 +786,10 @@ video {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
.flex-shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -837,11 +798,6 @@ video {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.-translate-x-1\/2 {
|
||||
--tw-translate-x: -50%;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -854,30 +810,18 @@ video {
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
.grid-cols-6 {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -894,10 +838,6 @@ video {
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.gap-x-6 {
|
||||
-moz-column-gap: 1.5rem;
|
||||
column-gap: 1.5rem;
|
||||
@@ -968,10 +908,6 @@ video {
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.rounded-\[50px\] {
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
.rounded-full {
|
||||
border-radius: 9999px;
|
||||
}
|
||||
@@ -984,6 +920,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;
|
||||
@@ -1019,6 +959,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));
|
||||
@@ -1029,6 +984,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));
|
||||
@@ -1059,16 +1044,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));
|
||||
@@ -1079,24 +1144,95 @@ 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));
|
||||
}
|
||||
|
||||
.bg-gray-600 {
|
||||
.bg-yellow-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(75 85 99 / var(--tw-bg-opacity, 1));
|
||||
background-color: rgb(254 249 195 / var(--tw-bg-opacity, 1));
|
||||
}
|
||||
|
||||
.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-gray-900 {
|
||||
stroke: #111827;
|
||||
}
|
||||
|
||||
.stroke-red-500 {
|
||||
stroke: #ef4444;
|
||||
}
|
||||
@@ -1123,6 +1259,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;
|
||||
@@ -1143,6 +1284,11 @@ video {
|
||||
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;
|
||||
@@ -1306,11 +1452,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));
|
||||
@@ -1341,16 +1537,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));
|
||||
@@ -1361,18 +1627,94 @@ 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-blue-600 {
|
||||
.text-yellow-300 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(37 99 235 / var(--tw-text-opacity, 1));
|
||||
color: rgb(253 224 71 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
.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 {
|
||||
@@ -1386,8 +1728,20 @@ video {
|
||||
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;
|
||||
.shadow-xl {
|
||||
--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px 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-2xl {
|
||||
--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||
--tw-shadow-colored: 0 25px 50px -12px 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-red-900 {
|
||||
--tw-shadow-color: #7f1d1d;
|
||||
--tw-shadow: var(--tw-shadow-colored);
|
||||
}
|
||||
|
||||
@@ -1407,6 +1761,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);
|
||||
@@ -1427,46 +1785,11 @@ video {
|
||||
color: rgb(156 163 175 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.after\:mt-4::after {
|
||||
content: var(--tw-content);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.after\:block::after {
|
||||
content: var(--tw-content);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.after\:h-1::after {
|
||||
content: var(--tw-content);
|
||||
height: 0.25rem;
|
||||
}
|
||||
|
||||
.after\:w-full::after {
|
||||
content: var(--tw-content);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.after\:rounded-lg::after {
|
||||
content: var(--tw-content);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.after\:bg-gray-200::after {
|
||||
content: var(--tw-content);
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
|
||||
}
|
||||
|
||||
.focus-within\:outline-none:focus-within {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.focus-within\:outline-gray-300:focus-within {
|
||||
outline-color: #d1d5db;
|
||||
}
|
||||
|
||||
.focus-within\:outline-red-500:focus-within {
|
||||
outline-color: #ef4444;
|
||||
}
|
||||
@@ -1486,11 +1809,6 @@ 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));
|
||||
@@ -1506,11 +1824,6 @@ video {
|
||||
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));
|
||||
@@ -1555,15 +1868,6 @@ video {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
.hover\:no-underline:hover {
|
||||
text-decoration-line: none;
|
||||
}
|
||||
|
||||
.hover\:shadow-gray-200:hover {
|
||||
--tw-shadow-color: #e5e7eb;
|
||||
--tw-shadow: var(--tw-shadow-colored);
|
||||
}
|
||||
|
||||
.focus\:z-10:focus {
|
||||
z-index: 10;
|
||||
}
|
||||
@@ -1600,12 +1904,6 @@ video {
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||
}
|
||||
|
||||
.focus\:ring-2: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(2px + 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-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);
|
||||
@@ -1642,10 +1940,6 @@ video {
|
||||
fill: #f87171;
|
||||
}
|
||||
|
||||
.group:hover .group-hover\:stroke-black {
|
||||
stroke: #000;
|
||||
}
|
||||
|
||||
.group:hover .group-hover\:stroke-white {
|
||||
stroke: #fff;
|
||||
}
|
||||
@@ -1721,10 +2015,6 @@ video {
|
||||
max-width: 24rem;
|
||||
}
|
||||
|
||||
.sm\:grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.sm\:items-center {
|
||||
align-items: center;
|
||||
}
|
||||
@@ -1816,10 +2106,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;
|
||||
}
|
||||
@@ -1843,11 +2129,6 @@ 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) {
|
||||
@@ -1937,10 +2218,6 @@ video {
|
||||
color: rgb(156 163 175 / var(--tw-placeholder-opacity, 1));
|
||||
}
|
||||
|
||||
.dark\:ring-offset-gray-800 {
|
||||
--tw-ring-offset-color: #1f2937;
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-gray-600:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(75 85 99 / var(--tw-bg-opacity, 1));
|
||||
|
||||
21
test/lib.nix
Normal file
21
test/lib.nix
Normal 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
21
test/test.nix
Normal 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'"
|
||||
'';
|
||||
}
|
||||
19
utils/utils.go
Normal file
19
utils/utils.go
Normal 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)
|
||||
}
|
||||
@@ -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,23 @@
|
||||
</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>
|
||||
<option value="CreateBookletA5">CreateBooklet A5</option>
|
||||
<option value="LongEdgeA5">Long Edge A5</option>
|
||||
<option value="ShortEdgeA5">Short Edge A5</option>
|
||||
<option value="TriFold">Tri-Fold (Flyer)</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>
|
||||
|
||||
@@ -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
53
views/batchupload.html
Normal 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" . }}
|
||||
@@ -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>
|
||||
|
||||
@@ -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="Logo">
|
||||
<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>
|
||||
|
||||
|
||||
23
views/colors.html
Normal file
23
views/colors.html
Normal 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>
|
||||
|
||||
@@ -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??
|
||||
|
||||
@@ -1,14 +1,51 @@
|
||||
{{ 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/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>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
<a href="/{{ .data.shopItem.Pdf }}" target="_blank">
|
||||
<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 class="grid grid-cols-6 gap-4 mb-4">
|
||||
<div class="col-span-3">
|
||||
{{ if .data.previousShopItem }}
|
||||
<form action="/shopitems/{{ .data.previousShopItem.ID }}/edit">
|
||||
<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">Previous</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="col-span-3">
|
||||
{{ if .data.nextShopItem }}
|
||||
<form action="/shopitems/{{ .data.nextShopItem.ID }}/edit">
|
||||
<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">Next</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</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">
|
||||
@@ -28,12 +65,28 @@
|
||||
<textarea id="description" name="description" type="textarea" class="block w-full px-4 py-2 mt-2 text-gray-900 bg-white border border-gray-300 rounded-md dark:bg-gray-800 dark:text-gray-900 dark:border-gray-600 focus:border-blue-500 dark:focus:border-blue-500 focus:outline-none focus:ring">{{ .data.shopItem.Description}}</textarea>
|
||||
</div>
|
||||
|
||||
<label class="block text-sm/6 font-medium text-gray-900">
|
||||
Print Mode
|
||||
</label>
|
||||
<select name="print-mode" required class="bg-white 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="{{ .data.shopItem.PrintMode }}">{{ .data.shopItem.PrintMode }}</option>
|
||||
<option value="CreateBooklet">CreateBooklet</option>
|
||||
<option value="LongEdge">Long Edge</option>
|
||||
<option value="ShortEdge">Short Edge</option>
|
||||
<option value="CreateBookletA5">CreateBooklet A5</option>
|
||||
<option value="LongEdgeA5">Long Edge A5</option>
|
||||
<option value="ShortEdgeA5">Short Edge A5</option>
|
||||
<option value="TriFold">Tri-Fold (Flyer)</option>
|
||||
</select>
|
||||
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm/6 text-gray-900">Select Categories</label>
|
||||
<div class="space-y-2">
|
||||
{{ range .data.tags }}
|
||||
<label class="flex text-sm/6 items-center">
|
||||
<input type="checkbox" class="form-checkbox h-4 w-4 text-gray-900" value="{{ .ID }}" name="tags[]">
|
||||
<input type="checkbox" {{ .Checked }} class="form-checkbox h-4 w-4 text-gray-900" value="{{ .ID }}" name="tags[]">
|
||||
<span class="ml-2 text-sm/6 text-gray-900">{{ .Name }}</span>
|
||||
</label>
|
||||
{{ end }}
|
||||
@@ -55,14 +108,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" value="{{ .data.priceBW }}" 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" value="{{ .data.priceColored }}" 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 +140,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,13 +153,28 @@
|
||||
</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>
|
||||
<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>
|
||||
|
||||
<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">Update Item</button>
|
||||
|
||||
38
views/editorders.html
Normal file
38
views/editorders.html
Normal 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" . }}
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>FreiRaum</title>
|
||||
<title>Zines</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">🛒 Cart</a>
|
||||
</div>
|
||||
|
||||
28
views/invites.html
Normal file
28
views/invites.html
Normal 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" . }}
|
||||
@@ -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="Logo">
|
||||
<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>
|
||||
|
||||
|
||||
88
views/orderpreview.html
Normal file
88
views/orderpreview.html
Normal 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
72
views/printvariant.html
Normal 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" . }}
|
||||
@@ -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="Logo">
|
||||
<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
59
views/registertoken.html
Normal 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" . }}
|
||||
@@ -9,23 +9,24 @@
|
||||
<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" . }}
|
||||
|
||||
@@ -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">
|
||||
<h2 class="text-2xl font-bold tracking-tight text-gray-900">Available Zines</h2>
|
||||
<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-4/5 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 loading="lazy" src="/{{ .Image }}" alt="Product Image" class="shadow-2xl 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user