Compare commits
97 Commits
803535daba
...
userhandli
| Author | SHA1 | Date | |
|---|---|---|---|
|
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
|
|||
|
89f7c8c4bb
|
|||
|
869642fa4d
|
|||
|
854573eb3a
|
|||
|
30b32a571c
|
|||
|
22bf9d4390
|
|||
|
2fce17d528
|
|||
|
e53796bda6
|
|||
|
69420fab6c
|
|||
|
6cbd9f5a3b
|
|||
|
9544849ef6
|
|||
| ac8b0339c3 | |||
| 974b95b244 | |||
| 1f411edf50 | |||
| cbaf48123f | |||
| a88be8de62 | |||
| 639eaa04f5 | |||
| fa214f4fdf | |||
| 503ab0e9bd | |||
| e54be98fe9 | |||
| 56035bb32f | |||
| 1ccfc620d0 | |||
| a65ba9c98c | |||
| d24dfdf262 | |||
| 0c85ca5938 | |||
| 23392cc5c1 | |||
| 5e22be5074 | |||
| 1dc10d61bc | |||
| 07ee8a004c | |||
| 4e296ca445 |
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 ./...
|
||||||
26
README.md
26
README.md
@@ -7,3 +7,29 @@ The zines will then be printed on demand and be send.
|
|||||||
for payment a simple random string will be created to connect payments to orders.
|
for payment a simple random string will be created to connect payments to orders.
|
||||||
This way also cash could be send like mullvad is doing it.
|
This way also cash could be send like mullvad is doing it.
|
||||||
|
|
||||||
|
# Development
|
||||||
|
To get the webserver running do the following:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix develop .#
|
||||||
|
# run the webserver
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
For updating tailwindcss on the fly open extra shell
|
||||||
|
```bash
|
||||||
|
nix develop .#
|
||||||
|
tailwindcss -i static/input.css -o static/output.css --watch
|
||||||
|
```
|
||||||
|
|
||||||
|
# Printer Testing
|
||||||
|
- [x] long edge + bypass front cover
|
||||||
|
- `lp -d KONICA_MINOLTA_KONICA_MINOLTA_bizhub_C258/BookletPrint -o Fold=HalfFold -o FrontCoverPage=Printed -o FrontCoverTray=BypassTray ~/proggn/malobeo/zineshop/Test-book-long-edge.pdf`
|
||||||
|
- [x] short edge + bypass front cover
|
||||||
|
- ` lp -d KONICA_MINOLTA_KONICA_MINOLTA_bizhub_C258/BookletPrint -o Fold=HalfFold -o Binding=TopBinding -o FrontCoverPage=Printed -o FrontCoverTray=BypassTray ~/proggn/malobeo/zineshop/Test-book-short-edge.pdf `
|
||||||
|
- [x] booklet + bypass front cover
|
||||||
|
- `lp -d KONICA_MINOLTA_KONICA_MINOLTA_bizhub_C258/BookletPrint -o Combination=Booklet -o PageSize=A5 -o Fold=HalfFold -o FrontCoverPage=Printed -o FrontCoverTray=BypassTray ~/proggn/malobeo/zineshop/Test.pdf`
|
||||||
|
---
|
||||||
|
- [x] okular long edge + bypass front cover
|
||||||
|
- [x] okular short edge + bypass front cover
|
||||||
|
- [ ] okular booklet + bypass front cover
|
||||||
|
|||||||
605
controllers/cartItemController.go
Normal file
605
controllers/cartItemController.go
Normal file
@@ -0,0 +1,605 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"git.dynamicdiscord.de/kalipso/zineshop/models"
|
||||||
|
//"git.dynamicdiscord.de/kalipso/zineshop/services"
|
||||||
|
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
|
||||||
|
"git.dynamicdiscord.de/kalipso/zineshop/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CartItemController interface {
|
||||||
|
//CRUDController
|
||||||
|
CartItemView(*gin.Context)
|
||||||
|
AddItemHandler(*gin.Context)
|
||||||
|
DeleteItemHandler(*gin.Context)
|
||||||
|
EditItemHandler(*gin.Context)
|
||||||
|
CheckoutView(*gin.Context)
|
||||||
|
CheckoutHandler(*gin.Context)
|
||||||
|
OrderView(*gin.Context)
|
||||||
|
OrderHandler(*gin.Context)
|
||||||
|
OrdersView(*gin.Context)
|
||||||
|
OrdersHandler(*gin.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
type cartItemController struct{}
|
||||||
|
|
||||||
|
func NewCartItemController() CartItemController {
|
||||||
|
return &cartItemController{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSetCookieValue retrieves the value of a cookie from the Set-Cookie header
|
||||||
|
func getSetCookieValue(c *gin.Context, cookieName string) string {
|
||||||
|
// Check the Set-Cookie headers
|
||||||
|
cookies := c.Writer.Header()["Set-Cookie"]
|
||||||
|
for _, cookie := range cookies {
|
||||||
|
if strings.HasPrefix(cookie, cookieName+"=") {
|
||||||
|
// Extract the cookie value
|
||||||
|
parts := strings.SplitN(cookie, ";", 2)
|
||||||
|
if len(parts) > 0 {
|
||||||
|
return strings.TrimPrefix(parts[0], cookieName+"=")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "" // Return empty string if cookie is not found
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSessionId(ctx *gin.Context) string {
|
||||||
|
sessionId, err := ctx.Cookie("session_id")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
//we need to check if we already set cookie in the response so that we dont do this multiple times
|
||||||
|
responseCookie := getSetCookieValue(ctx, "session_id")
|
||||||
|
|
||||||
|
if len(responseCookie) != 0 {
|
||||||
|
return responseCookie
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionId = utils.GenerateSessionId(16)
|
||||||
|
ctx.SetCookie("session_id", sessionId, 3600, "/", "", false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessionId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *cartItemController) NewCartItemFromForm(ctx *gin.Context) (models.CartItem, error) {
|
||||||
|
sessionId := GetSessionId(ctx)
|
||||||
|
shopItemIdStr := ctx.PostForm("ShopItemId")
|
||||||
|
shopItemId, err := strconv.Atoi(shopItemIdStr)
|
||||||
|
itemVariantIdStr := ctx.PostForm("ItemVariantId")
|
||||||
|
itemVariantId, err := strconv.Atoi(itemVariantIdStr)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return models.CartItem{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
quantity := 1
|
||||||
|
|
||||||
|
shopItem, err := repositories.ShopItems.GetById(shopItemIdStr)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return models.CartItem{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
itemVariant, err := repositories.ShopItems.GetVariantById(itemVariantIdStr)
|
||||||
|
|
||||||
|
cartItem := models.CartItem{
|
||||||
|
SessionId: sessionId,
|
||||||
|
ShopItemId: uint(shopItemId),
|
||||||
|
ShopItem: shopItem,
|
||||||
|
ItemVariantId: uint(itemVariantId),
|
||||||
|
ItemVariant: itemVariant,
|
||||||
|
Quantity: quantity,
|
||||||
|
}
|
||||||
|
|
||||||
|
return cartItem, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *cartItemController) NewAddressFromForm(ctx *gin.Context) (models.AddressInfo, error) {
|
||||||
|
firstName := ctx.PostForm("firstName")
|
||||||
|
lastName := ctx.PostForm("lastName")
|
||||||
|
address := ctx.PostForm("address")
|
||||||
|
postalCode := ctx.PostForm("postalCode")
|
||||||
|
city := ctx.PostForm("city")
|
||||||
|
country := ctx.PostForm("country")
|
||||||
|
|
||||||
|
if firstName == "" {
|
||||||
|
return models.AddressInfo{}, fmt.Errorf("first name missing.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastName == "" {
|
||||||
|
return models.AddressInfo{}, fmt.Errorf("Last name missing.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if address == "" {
|
||||||
|
return models.AddressInfo{}, fmt.Errorf("address missing.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if postalCode == "" {
|
||||||
|
return models.AddressInfo{}, fmt.Errorf("postalCode missing.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if city == "" {
|
||||||
|
return models.AddressInfo{}, fmt.Errorf("city missing.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if country == "" {
|
||||||
|
return models.AddressInfo{}, fmt.Errorf("country missing.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return models.AddressInfo{
|
||||||
|
FirstName: firstName,
|
||||||
|
LastName: lastName,
|
||||||
|
Address: address,
|
||||||
|
PostalCode: postalCode,
|
||||||
|
City: city,
|
||||||
|
Country: country,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *cartItemController) NewOrderFromForm(ctx *gin.Context) (models.Order, error) {
|
||||||
|
sessionId := GetSessionId(ctx)
|
||||||
|
status := models.OrderStatus("AwaitingConfirmation")
|
||||||
|
token := utils.GenerateToken()
|
||||||
|
email := ctx.PostForm("email")
|
||||||
|
comment := ctx.PostForm("comment")
|
||||||
|
firstName := ctx.PostForm("firstName")
|
||||||
|
lastName := ctx.PostForm("lastName")
|
||||||
|
address := ctx.PostForm("address")
|
||||||
|
postalCode := ctx.PostForm("postalCode")
|
||||||
|
city := ctx.PostForm("city")
|
||||||
|
country := ctx.PostForm("country")
|
||||||
|
shippingStr := ctx.PostForm("shippingMethod")
|
||||||
|
|
||||||
|
//address, err := rc.NewAddressFromForm(ctx)
|
||||||
|
//if shippingStr != "pickup" {
|
||||||
|
// if err != nil {
|
||||||
|
// return models.Order{}, fmt.Errorf("Invalid address information.")
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
shipping, err := models.GetShippingMethod(shippingStr)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return models.Order{}, fmt.Errorf("Invalid shipping method.")
|
||||||
|
}
|
||||||
|
|
||||||
|
cartItems, err := repositories.CartItems.GetAllBySession(sessionId)
|
||||||
|
fmt.Println(sessionId)
|
||||||
|
fmt.Println(cartItems)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return models.Order{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
order := models.Order{
|
||||||
|
SessionId: sessionId,
|
||||||
|
Status: status,
|
||||||
|
Token: token,
|
||||||
|
Email: email,
|
||||||
|
Comment: comment,
|
||||||
|
FirstName: firstName,
|
||||||
|
LastName: lastName,
|
||||||
|
Address: address,
|
||||||
|
PostalCode: postalCode,
|
||||||
|
City: city,
|
||||||
|
Country: country,
|
||||||
|
Shipping: shipping.Id,
|
||||||
|
CartItems: cartItems,
|
||||||
|
}
|
||||||
|
|
||||||
|
return order, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *cartItemController) Create(c *gin.Context) {
|
||||||
|
cartItem, err := rc.NewCartItemFromForm(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ReplyError(c, fmt.Errorf("cartItem creation failed: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = repositories.CartItems.Create(cartItem)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ReplyError(c, fmt.Errorf("cartItem creation failed: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ReplyOK(c, "cartItem was created")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *cartItemController) Update(c *gin.Context) {
|
||||||
|
cartItem, err := rc.NewCartItemFromForm(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ReplyError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = repositories.CartItems.Update(cartItem)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ReplyError(c, fmt.Errorf("cartItem creation failed: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ReplyOK(c, "cartItem was updated")
|
||||||
|
}
|
||||||
|
|
||||||
|
//func (rc *cartItemController) Delete(c *gin.Context) {
|
||||||
|
// err := repositories.CartItems.DeleteBySessionId(c.Param("id"))
|
||||||
|
//
|
||||||
|
// if err != nil {
|
||||||
|
// ReplyError(c, fmt.Errorf("cartItem deletion failed: %s", err))
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// ReplyOK(c, "cartItem was deleted")
|
||||||
|
//}
|
||||||
|
|
||||||
|
func (rc *cartItemController) CartItemView(c *gin.Context) {
|
||||||
|
sessionId := GetSessionId(c)
|
||||||
|
|
||||||
|
cartItems, err := repositories.CartItems.GetAllBySession(sessionId)
|
||||||
|
//cartItems, err := repositories.CartItems.GetAll()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(http.StatusBadRequest, "cart.html", gin.H{"data": gin.H{"error": err}})
|
||||||
|
}
|
||||||
|
|
||||||
|
priceTotal := 0.0
|
||||||
|
for _, cartItem := range cartItems {
|
||||||
|
priceTotal += (float64(cartItem.Quantity) * cartItem.ItemVariant.Price)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("PRICE TOTAL", priceTotal)
|
||||||
|
|
||||||
|
data := CreateSessionData(c, gin.H{
|
||||||
|
"cartItems": cartItems,
|
||||||
|
"priceTotal": fmt.Sprintf("%.2f", priceTotal), //round 2 decimals
|
||||||
|
"shipping": models.GetShippingMethods(),
|
||||||
|
})
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "cart.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *cartItemController) AddItemHandler(c *gin.Context) {
|
||||||
|
cartItem, err := rc.NewCartItemFromForm(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = repositories.CartItems.Create(cartItem)
|
||||||
|
if err != nil {
|
||||||
|
data := CreateSessionData(c, gin.H{
|
||||||
|
"error": err,
|
||||||
|
"success": "",
|
||||||
|
})
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "cart.html", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.CartItemView(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *cartItemController) DeleteItemHandler(c *gin.Context) {
|
||||||
|
err := repositories.CartItems.DeleteById(c.PostForm("id"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
data := CreateSessionData(c, gin.H{
|
||||||
|
"error": err,
|
||||||
|
"success": "",
|
||||||
|
})
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "index.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusFound, "/cart")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *cartItemController) EditItemHandler(c *gin.Context) {
|
||||||
|
cartItemId := c.PostForm("id")
|
||||||
|
cartItem, err := repositories.CartItems.GetById(cartItemId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
c.Redirect(http.StatusFound, "/cart")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action := c.PostForm("action")
|
||||||
|
|
||||||
|
if action == "setAmount" {
|
||||||
|
amountStr := c.PostForm("amount")
|
||||||
|
amount, err := strconv.Atoi(amountStr)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if amount < 0 {
|
||||||
|
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "amount cant be negative"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if amount > 500 {
|
||||||
|
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "amount cant be over 500"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cartItem.Quantity = amount
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == "increase" {
|
||||||
|
cartItem.Quantity += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == "decrease" {
|
||||||
|
cartItem.Quantity -= 1
|
||||||
|
|
||||||
|
if cartItem.Quantity == 0 {
|
||||||
|
cartItem.Quantity = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = repositories.CartItems.Update(cartItem)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusFound, "/cart")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *cartItemController) CheckoutView(c *gin.Context) {
|
||||||
|
shippingMethod := c.Query("shippingMethod")
|
||||||
|
|
||||||
|
if shippingMethod == "" {
|
||||||
|
rc.CartItemView(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "checkout.html", gin.H{
|
||||||
|
"askAddress": (shippingMethod != "pickup"),
|
||||||
|
"shippingMethod": shippingMethod,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *cartItemController) CheckoutHandler(c *gin.Context) {
|
||||||
|
order, err := rc.NewOrderFromForm(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
existingOrder, err := repositories.Orders.GetBySession(order.SessionId)
|
||||||
|
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
fmt.Println("Creating Order")
|
||||||
|
createdOrder, err := repositories.Orders.Create(order)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
data := CreateSessionData(c, gin.H{
|
||||||
|
"error": err,
|
||||||
|
"success": "",
|
||||||
|
})
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "error.html", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cartItem := range order.CartItems {
|
||||||
|
cartItem.OrderID = createdOrder.ID
|
||||||
|
repositories.CartItems.Update(cartItem)
|
||||||
|
}
|
||||||
|
} else if err == nil {
|
||||||
|
fmt.Println("Updating Order")
|
||||||
|
order.ID = existingOrder.ID
|
||||||
|
order.CreatedAt = existingOrder.CreatedAt
|
||||||
|
_, err := repositories.Orders.Update(order)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shipping, err := models.GetShippingMethod(order.Shipping)
|
||||||
|
if err != nil {
|
||||||
|
data := CreateSessionData(c, gin.H{
|
||||||
|
"error": err,
|
||||||
|
"success": "",
|
||||||
|
})
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "error.html", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
priceProducts, priceTotal, err := order.CalculatePrices()
|
||||||
|
if err != nil {
|
||||||
|
data := CreateSessionData(c, gin.H{
|
||||||
|
"error": err,
|
||||||
|
"success": "",
|
||||||
|
})
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "error.html", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := CreateSessionData(c, gin.H{
|
||||||
|
"error": "",
|
||||||
|
"success": "",
|
||||||
|
"order": order,
|
||||||
|
"askAddress": (order.Shipping != "pickup"),
|
||||||
|
"isPreview": true,
|
||||||
|
"shipping": shipping,
|
||||||
|
"priceProducts": fmt.Sprintf("%.2f", priceProducts), //round 2 decimals
|
||||||
|
"priceTotal": fmt.Sprintf("%.2f", priceTotal), //round 2 decimals
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt.Println(order)
|
||||||
|
c.HTML(http.StatusOK, "orderpreview.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *cartItemController) OrderView(c *gin.Context) {
|
||||||
|
orderToken := c.Param("token")
|
||||||
|
|
||||||
|
order, err := repositories.Orders.GetByToken(orderToken)
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Order does not exist."})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shipping, err := models.GetShippingMethod(order.Shipping)
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Could not get shipping method"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
priceProducts, priceTotal, err := order.CalculatePrices()
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Could not calculate final prices"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Order: %v\n", order)
|
||||||
|
fmt.Printf("PriceTotal: %v\n", priceTotal)
|
||||||
|
fmt.Printf("Amount Items: %v\n", len(order.CartItems))
|
||||||
|
|
||||||
|
for _, item := range order.CartItems {
|
||||||
|
fmt.Printf("Cartitem: %v", item)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "order.html", CreateSessionData(c, gin.H{
|
||||||
|
"error": "",
|
||||||
|
"success": "",
|
||||||
|
"order": order,
|
||||||
|
"shipping": shipping,
|
||||||
|
"priceProducts": fmt.Sprintf("%.2f", priceProducts), //round 2 decimals
|
||||||
|
"priceTotal": fmt.Sprintf("%.2f", priceTotal), //round 2 decimals
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *cartItemController) OrderHandler(c *gin.Context) {
|
||||||
|
confirmation := c.PostForm("confirm-order")
|
||||||
|
|
||||||
|
if confirmation == "" {
|
||||||
|
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Something went wrong, try again later"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if confirmation != "true" {
|
||||||
|
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Order was not confirmed."})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionId := GetSessionId(c)
|
||||||
|
order, err := repositories.Orders.GetBySession(sessionId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Something went wrong, try again later"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
order.Status = models.AwaitingPayment
|
||||||
|
|
||||||
|
err = order.Validate()
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := range order.CartItems {
|
||||||
|
order.CartItems[idx].SessionId = "0"
|
||||||
|
repositories.CartItems.Update(order.CartItems[idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = repositories.Orders.Update(order)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: cartItemRepository delete all by session - otherwise items stay in cart after completing order..
|
||||||
|
|
||||||
|
c.Redirect(http.StatusFound, fmt.Sprintf("/order/%s", order.Token))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *cartItemController) OrdersView(c *gin.Context) {
|
||||||
|
orders, err := repositories.Orders.GetAll()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Orders doe not exist."})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "editorders.html", CreateSessionData(c, gin.H{
|
||||||
|
"error": "",
|
||||||
|
"success": "",
|
||||||
|
"orders": orders,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *cartItemController) OrdersHandler(c *gin.Context) {
|
||||||
|
token := c.Param("token")
|
||||||
|
|
||||||
|
if token == "" {
|
||||||
|
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "EmptyToken"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action := c.PostForm("action")
|
||||||
|
|
||||||
|
if action == "update" {
|
||||||
|
order, err := repositories.Orders.GetByToken(token)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "No order with given token found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status := c.PostForm("order-status")
|
||||||
|
|
||||||
|
//TODO validate status
|
||||||
|
if status == "" {
|
||||||
|
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Invalid Order Status"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
order.Status = models.OrderStatus(status)
|
||||||
|
|
||||||
|
_, err = repositories.Orders.Update(order)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == "delete" {
|
||||||
|
fmt.Println("Deleting Order ", token)
|
||||||
|
err := repositories.Orders.DeleteByToken(token)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusFound, "/orders")
|
||||||
|
}
|
||||||
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,14 +3,15 @@ package controllers
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"example.com/gin/test/models"
|
"git.dynamicdiscord.de/kalipso/zineshop/models"
|
||||||
//"example.com/gin/test/services"
|
//"git.dynamicdiscord.de/kalipso/zineshop/services"
|
||||||
"example.com/gin/test/repositories"
|
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CRUDController interface {
|
type CRUDController interface {
|
||||||
@@ -26,15 +27,20 @@ type ShopItemController interface {
|
|||||||
ShopItemView(*gin.Context)
|
ShopItemView(*gin.Context)
|
||||||
AddItemView(*gin.Context)
|
AddItemView(*gin.Context)
|
||||||
AddItemHandler(*gin.Context)
|
AddItemHandler(*gin.Context)
|
||||||
|
AddItemsView(*gin.Context)
|
||||||
|
AddItemsHandler(*gin.Context)
|
||||||
CreateTag(*gin.Context)
|
CreateTag(*gin.Context)
|
||||||
GetAllTags(*gin.Context)
|
GetAllTags(*gin.Context)
|
||||||
EditItemView(*gin.Context)
|
EditItemView(*gin.Context)
|
||||||
EditItemHandler(*gin.Context)
|
EditItemHandler(*gin.Context)
|
||||||
DeleteItemView(*gin.Context)
|
DeleteItemView(*gin.Context)
|
||||||
DeleteItemHandler(*gin.Context)
|
DeleteItemHandler(*gin.Context)
|
||||||
|
TagView(*gin.Context)
|
||||||
|
TagHandler(*gin.Context)
|
||||||
|
AddTagHandler(*gin.Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
type shopItemController struct {}
|
type shopItemController struct{}
|
||||||
|
|
||||||
func NewShopItemController() ShopItemController {
|
func NewShopItemController() ShopItemController {
|
||||||
return &shopItemController{}
|
return &shopItemController{}
|
||||||
@@ -63,40 +69,96 @@ func (rc *shopItemController) GetById(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rc *shopItemController) NewShopItemFromForm(ctx *gin.Context) (models.ShopItem, error) {
|
func (rc *shopItemController) NewShopItemFromForm(ctx *gin.Context) (models.ShopItem, error) {
|
||||||
|
defaultImagePath := "static/img/zine.jpg"
|
||||||
name := ctx.PostForm("name")
|
name := ctx.PostForm("name")
|
||||||
abstract := ctx.PostForm("abstract")
|
abstract := ctx.PostForm("abstract")
|
||||||
description := ctx.PostForm("description")
|
description := ctx.PostForm("description")
|
||||||
priceStr := ctx.PostForm("price")
|
categoryStr := ctx.PostForm("category")
|
||||||
|
variantNames := ctx.PostFormArray("variant-name[]")
|
||||||
|
variantValues := ctx.PostFormArray("variant-value[]")
|
||||||
tagIds := ctx.PostFormArray("tags[]")
|
tagIds := ctx.PostFormArray("tags[]")
|
||||||
image, err := ctx.FormFile("image")
|
image, err := ctx.FormFile("image")
|
||||||
dst := "static/img/zine.jpg"
|
dstImage := defaultImagePath
|
||||||
|
printMode := ctx.PostForm("print-mode")
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
dst = filepath.Join("static/uploads", image.Filename)
|
dstImage = filepath.Join("static/uploads", image.Filename)
|
||||||
if err := ctx.SaveUploadedFile(image, dst); err != nil {
|
if err := ctx.SaveUploadedFile(image, dstImage); err != nil {
|
||||||
return models.ShopItem{}, fmt.Errorf("Could not save image")
|
return models.ShopItem{}, fmt.Errorf("Could not save image")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dstPdf := ""
|
||||||
|
pdf, err := ctx.FormFile("pdf")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
dstPdf = filepath.Join("static/uploads", pdf.Filename)
|
||||||
|
fmt.Println("Saving pdf at ", dstPdf)
|
||||||
|
if err := ctx.SaveUploadedFile(pdf, dstPdf); err != nil {
|
||||||
|
return models.ShopItem{}, fmt.Errorf("Could not save PDF")
|
||||||
|
}
|
||||||
|
|
||||||
|
if dstImage == defaultImagePath {
|
||||||
|
dstImage = dstPdf + ".preview.png"
|
||||||
|
cmd := exec.Command("pdftoppm", "-png", "-singlefile", dstPdf, dstPdf+".preview")
|
||||||
|
_, err := cmd.Output()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error during pdftoppm: ", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if name == "" || description == "" {
|
if name == "" || description == "" {
|
||||||
return models.ShopItem{}, fmt.Errorf("Name or description empty")
|
return models.ShopItem{}, fmt.Errorf("Name or description empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the price string to float64
|
category, err := models.ParseCategory(categoryStr)
|
||||||
price, err := strconv.ParseFloat(priceStr, 64)
|
if err != nil {
|
||||||
if err != nil {
|
return models.ShopItem{}, err
|
||||||
return models.ShopItem{}, fmt.Errorf("Could not parse price")
|
}
|
||||||
}
|
|
||||||
|
var variants []models.ItemVariant
|
||||||
|
|
||||||
|
fmt.Println("VariantNames: ", variantNames)
|
||||||
|
fmt.Println("VariantValues: ", variantValues)
|
||||||
|
if len(variantNames) != len(variantValues) {
|
||||||
|
return models.ShopItem{}, fmt.Errorf("Different number of variant names and values")
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := range variantNames {
|
||||||
|
if variantValues[idx] == "" || variantNames[idx] == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
price, err := strconv.ParseFloat(variantValues[idx], 64)
|
||||||
|
if err != nil {
|
||||||
|
return models.ShopItem{}, fmt.Errorf("Could not variant parse price")
|
||||||
|
}
|
||||||
|
|
||||||
|
variants = append(variants, models.ItemVariant{
|
||||||
|
Name: variantNames[idx],
|
||||||
|
Price: price,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
shopItem := models.ShopItem{
|
shopItem := models.ShopItem{
|
||||||
Name: name,
|
Name: name,
|
||||||
Abstract: abstract,
|
Abstract: abstract,
|
||||||
Description: description,
|
Description: description,
|
||||||
Price: price,
|
Category: category,
|
||||||
IsPublic: true,
|
IsPublic: true,
|
||||||
Image: dst,
|
BasePrice: rc.GetBasePrice(variants),
|
||||||
|
Image: dstImage,
|
||||||
|
Pdf: dstPdf,
|
||||||
|
Variants: variants,
|
||||||
|
PrintMode: printMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Println("Creating Shopitem: ", shopItem)
|
||||||
|
|
||||||
for _, tagId := range tagIds {
|
for _, tagId := range tagIds {
|
||||||
tag, err := repositories.Tags.GetById(tagId)
|
tag, err := repositories.Tags.GetById(tagId)
|
||||||
|
|
||||||
@@ -108,9 +170,24 @@ func (rc *shopItemController) NewShopItemFromForm(ctx *gin.Context) (models.Shop
|
|||||||
}
|
}
|
||||||
|
|
||||||
return shopItem, nil
|
return shopItem, nil
|
||||||
//return services.ShopItems.NewShopItem(name, abstract, description, price, tagIds)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rc *shopItemController) GetBasePrice(variants []models.ItemVariant) float64 {
|
||||||
|
result := 0.0
|
||||||
|
|
||||||
|
for idx, variant := range variants {
|
||||||
|
if idx == 0 {
|
||||||
|
result = variant.Price
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if variant.Price < result {
|
||||||
|
result = variant.Price
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func (rc *shopItemController) Create(c *gin.Context) {
|
func (rc *shopItemController) Create(c *gin.Context) {
|
||||||
shopItem, err := rc.NewShopItemFromForm(c)
|
shopItem, err := rc.NewShopItemFromForm(c)
|
||||||
@@ -130,7 +207,6 @@ func (rc *shopItemController) Create(c *gin.Context) {
|
|||||||
ReplyOK(c, "shopItem was created")
|
ReplyOK(c, "shopItem was created")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (rc *shopItemController) Update(c *gin.Context) {
|
func (rc *shopItemController) Update(c *gin.Context) {
|
||||||
shopItemId, err := strconv.Atoi(c.Param("id"))
|
shopItemId, err := strconv.Atoi(c.Param("id"))
|
||||||
|
|
||||||
@@ -157,6 +233,7 @@ func (rc *shopItemController) Update(c *gin.Context) {
|
|||||||
ReplyOK(c, "shopItem was updated")
|
ReplyOK(c, "shopItem was updated")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: delete associated cartitems
|
||||||
func (rc *shopItemController) Delete(c *gin.Context) {
|
func (rc *shopItemController) Delete(c *gin.Context) {
|
||||||
err := repositories.ShopItems.DeleteById(c.Param("id"))
|
err := repositories.ShopItems.DeleteById(c.Param("id"))
|
||||||
|
|
||||||
@@ -172,19 +249,19 @@ func (rc *shopItemController) ShopItemView(c *gin.Context) {
|
|||||||
shopItem, err := repositories.ShopItems.GetById(c.Param("id"))
|
shopItem, err := repositories.ShopItems.GetById(c.Param("id"))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.HTML(http.StatusBadRequest, "shopitem.html", gin.H{ "data": gin.H{ "error": err } })
|
c.HTML(http.StatusBadRequest, "shopitem.html", gin.H{"data": gin.H{"error": err}})
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: get tags by item
|
//TODO: get tags by item
|
||||||
tags, err := repositories.Tags.GetAll()
|
tags, err := repositories.Tags.GetAll()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.HTML(http.StatusBadRequest, "shopitem.html", gin.H{ "data": gin.H{ "error": err } })
|
c.HTML(http.StatusBadRequest, "shopitem.html", gin.H{"data": gin.H{"error": err}})
|
||||||
}
|
}
|
||||||
|
|
||||||
data := CreateSessionData(c, gin.H{
|
data := CreateSessionData(c, gin.H{
|
||||||
"shopItem": shopItem,
|
"shopItem": shopItem,
|
||||||
"tags": tags,
|
"tags": tags,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -198,113 +275,214 @@ func (rc *shopItemController) AddItemView(c *gin.Context) {
|
|||||||
tags, err := repositories.Tags.GetAll()
|
tags, err := repositories.Tags.GetAll()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.HTML(http.StatusBadRequest, "additem.html", gin.H{ "error": err })
|
c.HTML(http.StatusBadRequest, "additem.html", gin.H{"error": err})
|
||||||
}
|
}
|
||||||
|
|
||||||
data := CreateSessionData(c, gin.H{
|
data := CreateSessionData(c, gin.H{
|
||||||
"error": "",
|
"error": "",
|
||||||
"success": "",
|
"success": "",
|
||||||
"tags": tags,
|
"tags": tags,
|
||||||
})
|
})
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "additem.html", data)
|
c.HTML(http.StatusOK, "additem.html", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (rc *shopItemController) AddItemHandler(c *gin.Context) {
|
func (rc *shopItemController) AddItemHandler(c *gin.Context) {
|
||||||
shopItem, err := rc.NewShopItemFromForm(c)
|
errorHandler := func(err error, tags []models.Tag) {
|
||||||
|
data := CreateSessionData(c, gin.H{
|
||||||
|
"error": err,
|
||||||
|
"success": "",
|
||||||
|
"tags": tags,
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
c.HTML(http.StatusOK, "additem.html", data)
|
||||||
fmt.Println(err)
|
|
||||||
c.HTML(http.StatusBadRequest, "additem.html", gin.H{ "error": err })
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tags, err := repositories.Tags.GetAll()
|
tags, err := repositories.Tags.GetAll()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
errorHandler(err, tags)
|
||||||
c.HTML(http.StatusBadRequest, "additem.html", gin.H{ "error": err })
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shopItem, err := rc.NewShopItemFromForm(c)
|
||||||
|
if err != nil {
|
||||||
|
errorHandler(err, tags)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = repositories.ShopItems.Create(shopItem)
|
_, err = repositories.ShopItems.Create(shopItem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
data := CreateSessionData(c, gin.H{
|
errorHandler(err, tags)
|
||||||
"error": err,
|
|
||||||
"success": "",
|
|
||||||
"tags": tags,
|
|
||||||
})
|
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "additem.html", data)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data := CreateSessionData(c, gin.H{
|
data := CreateSessionData(c, gin.H{
|
||||||
"error": "",
|
"error": "",
|
||||||
"success": fmt.Sprintf("Item '%s' Registered", shopItem.Name),
|
"success": fmt.Sprintf("Item '%s' Registered", shopItem.Name),
|
||||||
"tags": tags,
|
"tags": tags,
|
||||||
})
|
})
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "additem.html", data)
|
c.HTML(http.StatusOK, "additem.html", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rc *shopItemController) AddItemsView(c *gin.Context) {
|
||||||
|
data := CreateSessionData(c, gin.H{})
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "batchupload.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *shopItemController) AddItemsHandler(c *gin.Context) {
|
||||||
|
errorHandler := func(err error) {
|
||||||
|
data := CreateSessionData(c, gin.H{
|
||||||
|
"error": err,
|
||||||
|
})
|
||||||
|
c.HTML(http.StatusBadRequest, "batchupload.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
form, err := c.MultipartForm()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
errorHandler(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files := form.File["pdf"]
|
||||||
|
|
||||||
|
var shopItems []models.ShopItem
|
||||||
|
for _, file := range files {
|
||||||
|
dstPdf := filepath.Join("static/uploads", file.Filename)
|
||||||
|
|
||||||
|
if err := c.SaveUploadedFile(file, dstPdf); err != nil {
|
||||||
|
errorHandler(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dstImage := dstPdf + ".preview.png"
|
||||||
|
cmd := exec.Command("pdftoppm", "-png", "-singlefile", dstPdf, dstPdf+".preview")
|
||||||
|
_, err := cmd.Output()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error during pdftoppm: ", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
category, err := models.ParseCategory("Zine")
|
||||||
|
if err != nil {
|
||||||
|
errorHandler(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
variants := []models.ItemVariant{
|
||||||
|
{
|
||||||
|
Name: "B/W",
|
||||||
|
Price: 1.0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
shopItem := models.ShopItem{
|
||||||
|
Name: file.Filename,
|
||||||
|
Abstract: file.Filename,
|
||||||
|
Description: file.Filename,
|
||||||
|
Category: category,
|
||||||
|
IsPublic: true,
|
||||||
|
BasePrice: rc.GetBasePrice(variants),
|
||||||
|
Image: dstImage,
|
||||||
|
Pdf: dstPdf,
|
||||||
|
Variants: variants,
|
||||||
|
PrintMode: "CreateBooklet",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = repositories.ShopItems.Create(shopItem)
|
||||||
|
if err != nil {
|
||||||
|
errorHandler(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shopItems = append(shopItems, shopItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := "The Following items were registered:\n"
|
||||||
|
|
||||||
|
for _, item := range shopItems {
|
||||||
|
msg += fmt.Sprintf("%s\n", item.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := CreateSessionData(c, gin.H{
|
||||||
|
"error": "",
|
||||||
|
"success": msg,
|
||||||
|
})
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "batchupload.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
func (rc *shopItemController) EditItemView(c *gin.Context) {
|
func (rc *shopItemController) EditItemView(c *gin.Context) {
|
||||||
shopItem, err := repositories.ShopItems.GetById(c.Param("id"))
|
shopItem, err := repositories.ShopItems.GetById(c.Param("id"))
|
||||||
tags, err := repositories.Tags.GetAll()
|
tags, err := repositories.Tags.GetAll()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{ "error": err })
|
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{"error": err})
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(shopItem)
|
fmt.Println(shopItem)
|
||||||
|
|
||||||
data := CreateSessionData(c, gin.H{
|
data := CreateSessionData(c, gin.H{
|
||||||
"error": "",
|
"error": "",
|
||||||
"success": "",
|
"success": "",
|
||||||
"shopItem": shopItem,
|
"shopItem": shopItem,
|
||||||
"tags": tags,
|
"tags": tags,
|
||||||
})
|
})
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "edititem.html", data)
|
c.HTML(http.StatusOK, "edititem.html", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (rc *shopItemController) EditItemHandler(c *gin.Context) {
|
func (rc *shopItemController) EditItemHandler(c *gin.Context) {
|
||||||
shopItem, err := rc.NewShopItemFromForm(c)
|
shopItem, err := rc.NewShopItemFromForm(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{ "error": err })
|
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{"error": err})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newShopItem, err := repositories.ShopItems.GetById(c.Param("id"))
|
newShopItem, err := repositories.ShopItems.GetById(c.Param("id"))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{ "error": err })
|
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{"error": err})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newShopItem.Name = shopItem.Name
|
newShopItem.Name = shopItem.Name
|
||||||
newShopItem.Abstract = shopItem.Abstract
|
newShopItem.Abstract = shopItem.Abstract
|
||||||
newShopItem.Description = shopItem.Description
|
newShopItem.Description = shopItem.Description
|
||||||
newShopItem.Price = shopItem.Price
|
newShopItem.Category = shopItem.Category
|
||||||
|
newShopItem.Variants = shopItem.Variants
|
||||||
|
newShopItem.BasePrice = shopItem.BasePrice
|
||||||
newShopItem.IsPublic = shopItem.IsPublic
|
newShopItem.IsPublic = shopItem.IsPublic
|
||||||
newShopItem.Tags = shopItem.Tags
|
|
||||||
|
if len(shopItem.Tags) != 0 {
|
||||||
|
newShopItem.Tags = shopItem.Tags
|
||||||
|
}
|
||||||
|
|
||||||
|
if shopItem.Image != "static/img/zine.jpg" {
|
||||||
|
newShopItem.Image = shopItem.Image
|
||||||
|
}
|
||||||
|
|
||||||
|
if shopItem.Pdf != "" {
|
||||||
|
newShopItem.Pdf = shopItem.Pdf
|
||||||
|
}
|
||||||
|
|
||||||
|
newShopItem.PrintMode = shopItem.PrintMode
|
||||||
|
|
||||||
tags, err := repositories.Tags.GetAll()
|
tags, err := repositories.Tags.GetAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{ "error": err })
|
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{"error": err})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = repositories.ShopItems.Update(newShopItem)
|
_, err = repositories.ShopItems.Update(newShopItem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
data := CreateSessionData(c, gin.H{
|
data := CreateSessionData(c, gin.H{
|
||||||
"error": err,
|
"error": err,
|
||||||
"success": "",
|
"success": "",
|
||||||
"tags": tags,
|
"tags": tags,
|
||||||
})
|
})
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "edititem.html", data)
|
c.HTML(http.StatusOK, "edititem.html", data)
|
||||||
@@ -312,9 +490,9 @@ func (rc *shopItemController) EditItemHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data := CreateSessionData(c, gin.H{
|
data := CreateSessionData(c, gin.H{
|
||||||
"error": "",
|
"error": "",
|
||||||
"success": fmt.Sprintf("Item '%s' Updated", newShopItem.Name),
|
"success": fmt.Sprintf("Item '%s' Updated", newShopItem.Name),
|
||||||
"tags": tags,
|
"tags": tags,
|
||||||
})
|
})
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "edititem.html", data)
|
c.HTML(http.StatusOK, "edititem.html", data)
|
||||||
@@ -325,28 +503,27 @@ func (rc *shopItemController) DeleteItemView(c *gin.Context) {
|
|||||||
tags, err := repositories.Tags.GetAll()
|
tags, err := repositories.Tags.GetAll()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.HTML(http.StatusBadRequest, "deleteitem.html", gin.H{ "error": err })
|
c.HTML(http.StatusBadRequest, "deleteitem.html", gin.H{"error": err})
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(shopItem)
|
fmt.Println(shopItem)
|
||||||
|
|
||||||
data := CreateSessionData(c, gin.H{
|
data := CreateSessionData(c, gin.H{
|
||||||
"error": "",
|
"error": "",
|
||||||
"success": "",
|
"success": "",
|
||||||
"shopItem": shopItem,
|
"shopItem": shopItem,
|
||||||
"tags": tags,
|
"tags": tags,
|
||||||
})
|
})
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "deleteitem.html", data)
|
c.HTML(http.StatusOK, "deleteitem.html", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (rc *shopItemController) DeleteItemHandler(c *gin.Context) {
|
func (rc *shopItemController) DeleteItemHandler(c *gin.Context) {
|
||||||
err := repositories.ShopItems.DeleteById(c.Param("id"))
|
err := repositories.ShopItems.DeleteById(c.Param("id"))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
data := CreateSessionData(c, gin.H{
|
data := CreateSessionData(c, gin.H{
|
||||||
"error": err,
|
"error": err,
|
||||||
"success": "",
|
"success": "",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -357,7 +534,7 @@ func (rc *shopItemController) DeleteItemHandler(c *gin.Context) {
|
|||||||
fmt.Println(len(shopItems))
|
fmt.Println(len(shopItems))
|
||||||
|
|
||||||
data := CreateSessionData(c, gin.H{
|
data := CreateSessionData(c, gin.H{
|
||||||
"title": "shopItem Page",
|
"title": "shopItem Page",
|
||||||
"shopItems": shopItems,
|
"shopItems": shopItems,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -366,6 +543,79 @@ func (rc *shopItemController) DeleteItemHandler(c *gin.Context) {
|
|||||||
c.HTML(http.StatusOK, "index.html", data)
|
c.HTML(http.StatusOK, "index.html", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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})
|
||||||
|
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})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == "delete" {
|
||||||
|
repositories.Tags.DeleteById(ctx.Param("id"))
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.TagView(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *shopItemController) AddTagHandler(c *gin.Context) {
|
||||||
|
tag, err := models.NewTag(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(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,
|
||||||
|
"success": "",
|
||||||
|
})
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "tagview.html", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.TagView(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
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}})
|
||||||
|
}
|
||||||
|
|
||||||
|
data := CreateSessionData(c, gin.H{
|
||||||
|
"tags": tags,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(http.StatusBadRequest, "tagview.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "tagview.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
func (rc *shopItemController) CreateTag(c *gin.Context) {
|
func (rc *shopItemController) CreateTag(c *gin.Context) {
|
||||||
tag, err := models.NewTagByJson(c)
|
tag, err := models.NewTagByJson(c)
|
||||||
|
|
||||||
@@ -403,7 +653,7 @@ func (rc *shopItemController) GetAllTags(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ReplyError(ctx *gin.Context, err error) {
|
func ReplyError(ctx *gin.Context, err error) {
|
||||||
ctx.JSON(http.StatusBadRequest, gin.H{ "error": err.Error() })
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReplyOK(ctx *gin.Context, message any) {
|
func ReplyOK(ctx *gin.Context, message any) {
|
||||||
|
|||||||
@@ -1,30 +1,29 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import(
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"math/rand"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"example.com/gin/test/models"
|
"git.dynamicdiscord.de/kalipso/zineshop/models"
|
||||||
"example.com/gin/test/repositories"
|
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
|
||||||
"example.com/gin/test/services"
|
"git.dynamicdiscord.de/kalipso/zineshop/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type UserController struct{}
|
||||||
type UserController struct {}
|
|
||||||
|
|
||||||
func NewUserController() UserController {
|
func NewUserController() UserController {
|
||||||
return UserController{}
|
return UserController{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (uc *UserController) Register(c *gin.Context) {
|
func (uc *UserController) Register(c *gin.Context) {
|
||||||
//Get the email/passwd off req body
|
//Get the email/passwd off req body
|
||||||
var body struct {
|
var body struct {
|
||||||
Name string
|
Name string
|
||||||
Email string
|
Email string
|
||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +37,7 @@ func (uc *UserController) Register(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = services.Users.Register(body.Name, body.Email, body.Password)
|
_, err = services.Users.Register(body.Name, body.Email, body.Password, false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error: ", err)
|
fmt.Println("Error: ", err)
|
||||||
@@ -53,11 +52,10 @@ func (uc *UserController) Register(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{})
|
c.JSON(http.StatusOK, gin.H{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (uc *UserController) Login(c *gin.Context) {
|
func (uc *UserController) Login(c *gin.Context) {
|
||||||
//Get the email/passwd off req body
|
//Get the email/passwd off req body
|
||||||
var body struct {
|
var body struct {
|
||||||
Email string
|
Email string
|
||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +81,7 @@ func (uc *UserController) Login(c *gin.Context) {
|
|||||||
|
|
||||||
// send it back
|
// send it back
|
||||||
c.SetSameSite(http.SameSiteLaxMode)
|
c.SetSameSite(http.SameSiteLaxMode)
|
||||||
c.SetCookie("Authorization", tokenString, 3600 * 24, "", "", false, true)
|
c.SetCookie("Authorization", tokenString, 3600*24, "", "", false, true)
|
||||||
c.JSON(http.StatusOK, gin.H{})
|
c.JSON(http.StatusOK, gin.H{})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +114,6 @@ func (rc *UserController) LoginView(c *gin.Context) {
|
|||||||
c.HTML(http.StatusOK, "login.html", CreateSessionData(c, data))
|
c.HTML(http.StatusOK, "login.html", CreateSessionData(c, data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (rc *UserController) LoginHandler(c *gin.Context) {
|
func (rc *UserController) LoginHandler(c *gin.Context) {
|
||||||
email := c.PostForm("email")
|
email := c.PostForm("email")
|
||||||
password := c.PostForm("password")
|
password := c.PostForm("password")
|
||||||
@@ -139,17 +136,18 @@ func (rc *UserController) LoginHandler(c *gin.Context) {
|
|||||||
|
|
||||||
// send it back
|
// send it back
|
||||||
//c.SetSameSite(http.SameSiteLaxMode)
|
//c.SetSameSite(http.SameSiteLaxMode)
|
||||||
c.SetCookie("Authorization", tokenString, 3600 * 24, "", "", false, true)
|
c.SetCookie("Authorization", tokenString, 3600*24, "", "", false, true)
|
||||||
c.HTML(http.StatusOK, "login.html", CreateSessionData(c, gin.H{}))
|
c.HTML(http.StatusOK, "login.html", CreateSessionData(c, gin.H{}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateSessionData(c *gin.Context, extra any) gin.H {
|
func CreateSessionData(c *gin.Context, extra any) gin.H {
|
||||||
_, exists := c.Get("user")
|
user, exists := c.Get("user")
|
||||||
|
userImpl, _ := user.(models.User)
|
||||||
|
|
||||||
return gin.H{
|
return gin.H{
|
||||||
"test": "HEllo World",
|
|
||||||
"loggedIn": exists,
|
"loggedIn": exists,
|
||||||
"data": extra,
|
"isAdmin": userImpl.IsAdmin,
|
||||||
|
"data": extra,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,11 +156,45 @@ func (rc *UserController) RegisterHandler(c *gin.Context) {
|
|||||||
email := c.PostForm("email")
|
email := c.PostForm("email")
|
||||||
password := c.PostForm("password")
|
password := c.PostForm("password")
|
||||||
|
|
||||||
_, err := services.Users.Register(name, email, password)
|
//first registered user is admin
|
||||||
|
isEmpty, _ := repositories.Users.IsEmpty()
|
||||||
|
if isEmpty {
|
||||||
|
_, err := services.Users.Register(name, email, password, true)
|
||||||
|
if err != nil {
|
||||||
|
data := gin.H{
|
||||||
|
"error": "Registering Failed.",
|
||||||
|
"success": "",
|
||||||
|
}
|
||||||
|
c.HTML(http.StatusOK, "register.html", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
data := gin.H{
|
data := gin.H{
|
||||||
"error": "Registering Failed.",
|
"error": "",
|
||||||
|
"success": "You successfully registered as Admin. Try logging in.",
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "register.html", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//for any other user token is required
|
||||||
|
token := c.PostForm("token")
|
||||||
|
|
||||||
|
if token == "" {
|
||||||
|
data := gin.H{
|
||||||
|
"error": "No token. No register.",
|
||||||
|
"success": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "register.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenExists, err := repositories.Tokens.Exists(token)
|
||||||
|
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
data := gin.H{
|
||||||
|
"error": err,
|
||||||
"success": "",
|
"success": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,8 +202,33 @@ func (rc *UserController) RegisterHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !tokenExists {
|
||||||
|
data := gin.H{
|
||||||
|
"error": "Invalid Token.",
|
||||||
|
"success": "",
|
||||||
|
}
|
||||||
|
c.HTML(http.StatusOK, "register.html", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = services.Users.Register(name, email, password, false)
|
||||||
|
if err != nil {
|
||||||
|
data := gin.H{
|
||||||
|
"error": "Registering Failed.",
|
||||||
|
"success": "",
|
||||||
|
}
|
||||||
|
c.HTML(http.StatusOK, "register.html", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = repositories.Tokens.Delete(token)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Could not delete RegisterToken: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
data := gin.H{
|
data := gin.H{
|
||||||
"error": "",
|
"error": "",
|
||||||
"success": "You successfully registered. Try logging in.",
|
"success": "You successfully registered. Try logging in.",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +237,40 @@ func (rc *UserController) RegisterHandler(c *gin.Context) {
|
|||||||
|
|
||||||
func (rc *UserController) RegisterView(c *gin.Context) {
|
func (rc *UserController) RegisterView(c *gin.Context) {
|
||||||
data := gin.H{
|
data := gin.H{
|
||||||
"error": "",
|
"error": "",
|
||||||
|
"success": "",
|
||||||
|
"token": c.Param("token"),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "registertoken.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *UserController) InitAdmin(c *gin.Context) {
|
||||||
|
isEmpty, err := repositories.Users.IsEmpty()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
data := gin.H{
|
||||||
|
"error": err,
|
||||||
|
"success": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusInternalServerError, "error.html", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isEmpty {
|
||||||
|
data := gin.H{
|
||||||
|
"error": "Registration is closed",
|
||||||
|
"success": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusInternalServerError, "error.html", data)
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data := gin.H{
|
||||||
|
"error": "",
|
||||||
"success": "",
|
"success": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +281,7 @@ func (rc *UserController) ResetView(c *gin.Context) {
|
|||||||
shopItems, _ := repositories.ShopItems.GetAll()
|
shopItems, _ := repositories.ShopItems.GetAll()
|
||||||
|
|
||||||
data := gin.H{
|
data := gin.H{
|
||||||
"title": "shopItem Page",
|
"title": "shopItem Page",
|
||||||
"shopItems": shopItems,
|
"shopItems": shopItems,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,88 +292,66 @@ func (rc *UserController) ResetHandler(c *gin.Context) {
|
|||||||
shopItems, _ := repositories.ShopItems.GetAll()
|
shopItems, _ := repositories.ShopItems.GetAll()
|
||||||
|
|
||||||
data := gin.H{
|
data := gin.H{
|
||||||
"title": "shopItem Page",
|
"title": "shopItem Page",
|
||||||
"shopItems": shopItems,
|
"shopItems": shopItems,
|
||||||
}
|
}
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "passwordreset.html", data)
|
c.HTML(http.StatusOK, "passwordreset.html", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rc *UserController) InviteView(c *gin.Context) {
|
||||||
|
tokens, _ := repositories.Tokens.GetAll()
|
||||||
|
fmt.Println(tokens)
|
||||||
|
|
||||||
|
data := gin.H{
|
||||||
|
"tokens": tokens,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "invites.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *UserController) InviteHandler(c *gin.Context) {
|
||||||
|
action := c.PostForm("action")
|
||||||
|
|
||||||
|
if action == "create" {
|
||||||
|
_, err := repositories.Tokens.Create()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == "delete" {
|
||||||
|
token := c.PostForm("token")
|
||||||
|
repositories.Tokens.Delete(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.InviteView(c)
|
||||||
|
}
|
||||||
|
|
||||||
func (rc *UserController) MainView(c *gin.Context) {
|
func (rc *UserController) MainView(c *gin.Context) {
|
||||||
shopItems, _ := repositories.ShopItems.GetAll()
|
shopItems, _ := repositories.ShopItems.GetAll()
|
||||||
fmt.Println(len(shopItems))
|
fmt.Println(len(shopItems))
|
||||||
|
|
||||||
data := CreateSessionData(c, gin.H{
|
data := CreateSessionData(c, gin.H{
|
||||||
"title": "shopItem Page",
|
"title": "shopItem Page",
|
||||||
"shopItems": shopItems,
|
"shopItems": shopItems,
|
||||||
})
|
})
|
||||||
|
|
||||||
fmt.Println(data)
|
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "index.html", data)
|
c.HTML(http.StatusOK, "index.html", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
type booking struct {
|
func (rc *UserController) TagView(c *gin.Context) {
|
||||||
Booked bool
|
shopItems, _ := repositories.ShopItems.GetByTagId(c.Param("id"))
|
||||||
}
|
|
||||||
|
|
||||||
type calendarbooking struct{
|
|
||||||
Time string
|
|
||||||
Bookings []booking
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *UserController) CalendarView(c *gin.Context) {
|
|
||||||
shopItems, _ := repositories.ShopItems.GetAll()
|
|
||||||
fmt.Println(len(shopItems))
|
|
||||||
|
|
||||||
|
|
||||||
generateBookings := func(amountShopItems int) []calendarbooking {
|
|
||||||
var result []calendarbooking
|
|
||||||
time := 6;
|
|
||||||
for _ = range 18 {
|
|
||||||
book := calendarbooking{
|
|
||||||
Time: fmt.Sprintf("%d:00", time),
|
|
||||||
Bookings: []booking{},
|
|
||||||
}
|
|
||||||
for _ = range amountShopItems {
|
|
||||||
book.Bookings = append(book.Bookings, booking{ Booked: rand.Float32() < 0.5 })
|
|
||||||
}
|
|
||||||
|
|
||||||
time += 1
|
|
||||||
result = append(result, book)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
bookings := gin.H{
|
|
||||||
"head": []string{
|
|
||||||
"malobeo",
|
|
||||||
"hole of fame",
|
|
||||||
"BK",
|
|
||||||
"AZ Conni",
|
|
||||||
},
|
|
||||||
"bookings": generateBookings(4),
|
|
||||||
//"bookings": []calendarbooking{
|
|
||||||
// {
|
|
||||||
// Time: "10:00",
|
|
||||||
// Bookings: []booking{
|
|
||||||
// { Booked: true },
|
|
||||||
// { Booked: false },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
//},
|
|
||||||
}
|
|
||||||
|
|
||||||
data := CreateSessionData(c, gin.H{
|
data := CreateSessionData(c, gin.H{
|
||||||
"title": "shopItem Page",
|
"title": "shopItem Page",
|
||||||
"bookings": bookings,
|
"shopItems": shopItems,
|
||||||
"shopItemcount": len(bookings["head"].([]string)) + 1,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
fmt.Println(data)
|
c.HTML(http.StatusOK, "index.html", data)
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "calendar.html", data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *UserController) Logout(c *gin.Context) {
|
func (rc *UserController) Logout(c *gin.Context) {
|
||||||
|
|||||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1744463964,
|
||||||
|
"narHash": "sha256-LWqduOgLHCFxiTNYi3Uj5Lgz0SR+Xhw3kr/3Xd0GPTM=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "2631b0b7abcea6e640ce31cd78ea58910d31e650",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"utils": "utils"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
135
flake.nix
Normal file
135
flake.nix
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
description = "A very basic flake";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
inputs.utils.url = "github:numtide/flake-utils";
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, utils, ... }:
|
||||||
|
|
||||||
|
nixpkgs.lib.attrsets.recursiveUpdate
|
||||||
|
(utils.lib.eachSystem (utils.lib.defaultSystems) ( system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
in rec
|
||||||
|
{
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
go
|
||||||
|
gotools
|
||||||
|
poppler_utils #get first pdf page to png
|
||||||
|
tailwindcss
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
packages.zineshop = nixpkgs.legacyPackages.x86_64-linux.buildGoModule {
|
||||||
|
pname = "zineshop";
|
||||||
|
version = "1.0";
|
||||||
|
vendorHash = "sha256-0M/xblZXVw4xIFZeDewYrFu7VGUCsPTPG13r9ZpTGJo=";
|
||||||
|
src = ./.;
|
||||||
|
|
||||||
|
postInstall = ''
|
||||||
|
cp -r views $out/
|
||||||
|
cp -r static $out/
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
packages.default = packages.zineshop;
|
||||||
|
|
||||||
|
checks = let
|
||||||
|
checkArgs = {
|
||||||
|
pkgs = pkgs;
|
||||||
|
inherit self;
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
zineshop = import ./test/test.nix checkArgs;
|
||||||
|
};
|
||||||
|
|
||||||
|
})) {
|
||||||
|
|
||||||
|
nixosModules.zineshop = { config, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
cfg = config.services.zineshop;
|
||||||
|
zineshop-pkg = self.packages.x86_64-linux.zineshop;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
services.zineshop = {
|
||||||
|
enable = lib.mkOption {
|
||||||
|
default = false;
|
||||||
|
type = lib.types.bool;
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
Enables zineshop
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
environment.systemPackages = [
|
||||||
|
zineshop-pkg
|
||||||
|
pkgs.poppler_utils #get first pdf page to png
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d '/var/lib/zineshop' 0750 zineshop zineshop - -"
|
||||||
|
"d '/var/lib/zineshop/views' 0750 zineshop zineshop - -"
|
||||||
|
"d '/var/lib/zineshop/static' 0750 zineshop zineshop - -"
|
||||||
|
"d '/var/lib/zineshop/static/uploads' 0750 zineshop zineshop - -"
|
||||||
|
"z '/var/lib/zineshop' 0750 zineshop zineshop - -"
|
||||||
|
"z '/var/lib/zineshop/views' 0750 zineshop zineshop - -"
|
||||||
|
"z '/var/lib/zineshop/static' 0750 zineshop zineshop - -"
|
||||||
|
"z '/var/lib/zineshop/static/uploads' 0750 zineshop zineshop - -"
|
||||||
|
];
|
||||||
|
|
||||||
|
users = {
|
||||||
|
groups.zineshop = {};
|
||||||
|
users.zineshop = {
|
||||||
|
description = "zineshop user";
|
||||||
|
group = "zineshop";
|
||||||
|
isNormalUser = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.zineshop = {
|
||||||
|
description = "zineshop daemon";
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
WorkingDirectory = "/var/lib/zineshop";
|
||||||
|
ExecStart = pkgs.writeScript "start-zineshop" ''
|
||||||
|
#! ${pkgs.bash}/bin/bash
|
||||||
|
PATH="$PATH:${lib.makeBinPath [ pkgs.poppler_utils ]}"
|
||||||
|
${zineshop-pkg}/bin/zineshop
|
||||||
|
'';
|
||||||
|
Restart = "on-failure";
|
||||||
|
};
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
SQLITE_DB = "/var/lib/zineshop/zineshop.db";
|
||||||
|
SECRET = "secretforjwt"; #TODO: BAD!
|
||||||
|
PORT = "8080";
|
||||||
|
STATIC = "/var/lib/zineshop/static";
|
||||||
|
VIEWS = "/var/lib/zineshop/views";
|
||||||
|
};
|
||||||
|
|
||||||
|
preStart = ''
|
||||||
|
mkdir -m 0770 -p "/var/lib/zineshop"
|
||||||
|
cp -r ${zineshop-pkg}/views /var/lib/zineshop/
|
||||||
|
cp -r ${zineshop-pkg}/static /var/lib/zineshop/
|
||||||
|
chown zineshop:zineshop "/var/lib/zineshop"
|
||||||
|
'';
|
||||||
|
|
||||||
|
wantedBy = [ "default.target" ];
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
USER = "zineshop";
|
||||||
|
HOME = "/var/lib/zineshop";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
2
go.mod
2
go.mod
@@ -1,4 +1,4 @@
|
|||||||
module example.com/gin/test
|
module git.dynamicdiscord.de/kalipso/zineshop
|
||||||
|
|
||||||
go 1.23.3
|
go 1.23.3
|
||||||
|
|
||||||
|
|||||||
93
main.go
93
main.go
@@ -1,23 +1,25 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import(
|
import (
|
||||||
"os"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"fmt"
|
"os"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
|
|
||||||
"example.com/gin/test/controllers"
|
"git.dynamicdiscord.de/kalipso/zineshop/controllers"
|
||||||
"example.com/gin/test/repositories"
|
"git.dynamicdiscord.de/kalipso/zineshop/middlewares"
|
||||||
"example.com/gin/test/middlewares"
|
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
|
||||||
)
|
)
|
||||||
|
|
||||||
var(
|
var (
|
||||||
shopItemController controllers.ShopItemController = controllers.NewShopItemController()
|
shopItemController controllers.ShopItemController = controllers.NewShopItemController()
|
||||||
userController controllers.UserController = controllers.UserController{}
|
userController controllers.UserController = controllers.UserController{}
|
||||||
authValidator middlewares.AuthValidator = middlewares.AuthValidator{}
|
cartItemController controllers.CartItemController = controllers.NewCartItemController()
|
||||||
|
printController controllers.PrintController = controllers.NewPrintController()
|
||||||
|
authValidator middlewares.AuthValidator = middlewares.AuthValidator{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func LoadEnvVariables() {
|
func LoadEnvVariables() {
|
||||||
@@ -35,7 +37,7 @@ func setupLogOutput() {
|
|||||||
|
|
||||||
func SetReply(ctx *gin.Context, err error, message any) {
|
func SetReply(ctx *gin.Context, err error, message any) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.JSON(http.StatusBadRequest, gin.H{ "error": err.Error() })
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
} else {
|
} else {
|
||||||
ctx.JSON(http.StatusOK, message)
|
ctx.JSON(http.StatusOK, message)
|
||||||
}
|
}
|
||||||
@@ -46,53 +48,58 @@ func main() {
|
|||||||
|
|
||||||
repositories.InitRepositories()
|
repositories.InitRepositories()
|
||||||
|
|
||||||
|
|
||||||
server := gin.New()
|
server := gin.New()
|
||||||
server.Use(gin.Recovery())
|
server.Use(gin.Recovery())
|
||||||
server.Use(gin.Logger())
|
server.Use(gin.Logger())
|
||||||
|
|
||||||
server.Static("/static", "./static")
|
server.Static("/static", os.Getenv("STATIC"))
|
||||||
server.LoadHTMLGlob("views/*.html")
|
server.LoadHTMLGlob(fmt.Sprintf("%s/*.html", os.Getenv("VIEWS")))
|
||||||
|
|
||||||
apiRoutes := server.Group("/api")
|
|
||||||
//apiRoutes.Use(middlewares.BasicAuth())
|
|
||||||
{
|
|
||||||
apiRoutes.POST("/tags", authValidator.RequireAuth, shopItemController.CreateTag)
|
|
||||||
apiRoutes.GET("/tags", authValidator.OptionalAuth, shopItemController.GetAllTags)
|
|
||||||
apiRoutes.POST("/shopitems", authValidator.RequireAuth, shopItemController.Create)
|
|
||||||
apiRoutes.GET("/shopitems", authValidator.OptionalAuth, shopItemController.GetAll)
|
|
||||||
apiRoutes.GET("/shopitems/:id", authValidator.OptionalAuth, shopItemController.GetById)
|
|
||||||
apiRoutes.PUT("/shopitems/:id", authValidator.RequireAuth, shopItemController.Update)
|
|
||||||
apiRoutes.DELETE("/shopitems/:id", authValidator.RequireAuth, shopItemController.Delete)
|
|
||||||
|
|
||||||
//apiRoutes.GET("/rooms/:id/users", authValidator.RequireAuth, authValidator.RequireRoomAdmin, shopItemController.GetUsers)
|
|
||||||
//apiRoutes.POST("/rooms/:id/users", authValidator.RequireAuth, shopItemController.AddUser)
|
|
||||||
|
|
||||||
apiRoutes.POST("/users/register", userController.Register)
|
|
||||||
apiRoutes.POST("/users/login", userController.Login)
|
|
||||||
apiRoutes.GET("/users/validate", authValidator.OptionalAuth, userController.Validate)
|
|
||||||
}
|
|
||||||
|
|
||||||
viewRoutes := server.Group("/", authValidator.OptionalAuth)
|
viewRoutes := server.Group("/", authValidator.OptionalAuth)
|
||||||
{
|
{
|
||||||
viewRoutes.GET("/", userController.MainView)
|
viewRoutes.GET("/", userController.MainView)
|
||||||
viewRoutes.GET("/shopitems/:id", shopItemController.ShopItemView)
|
viewRoutes.GET("/shopitems/:id", shopItemController.ShopItemView)
|
||||||
viewRoutes.GET("/shopitems/:id/edit", authValidator.RequireAuth, shopItemController.EditItemView)
|
viewRoutes.GET("/shopitems/:id/edit", authValidator.RequireAdmin, shopItemController.EditItemView)
|
||||||
viewRoutes.GET("/shopitems/:id/delete", authValidator.RequireAuth, shopItemController.DeleteItemView)
|
viewRoutes.POST("/shopitems/:id/edit", authValidator.RequireAdmin, shopItemController.EditItemHandler)
|
||||||
viewRoutes.POST("/shopitems/:id/edit", authValidator.RequireAuth, shopItemController.EditItemHandler)
|
viewRoutes.GET("/shopitems/:id/delete", authValidator.RequireAdmin, shopItemController.DeleteItemView)
|
||||||
viewRoutes.POST("/shopitems/:id/delete", authValidator.RequireAuth, shopItemController.DeleteItemHandler)
|
viewRoutes.POST("/shopitems/:id/delete", authValidator.RequireAdmin, shopItemController.DeleteItemHandler)
|
||||||
|
viewRoutes.GET("/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
|
//write middleware that redirects to homescreen on register/login/reset for logged in users
|
||||||
viewRoutes.GET("/login", userController.LoginView)
|
viewRoutes.GET("/login", userController.LoginView)
|
||||||
viewRoutes.GET("/logout", userController.Logout)
|
viewRoutes.GET("/logout", userController.Logout)
|
||||||
viewRoutes.GET("/register", userController.RegisterView)
|
viewRoutes.GET("/register", userController.InitAdmin)
|
||||||
viewRoutes.GET("/passwordreset", userController.ResetView)
|
viewRoutes.GET("/register/:token", userController.RegisterView)
|
||||||
viewRoutes.GET("/additem", authValidator.RequireAuth, shopItemController.AddItemView)
|
viewRoutes.GET("/invites", authValidator.RequireAdmin, userController.InviteView)
|
||||||
|
viewRoutes.POST("/invites", authValidator.RequireAdmin, userController.InviteHandler)
|
||||||
|
viewRoutes.GET("/passwordreset", authValidator.RequireAuth, userController.ResetView)
|
||||||
|
viewRoutes.GET("/additem", authValidator.RequireAdmin, shopItemController.AddItemView)
|
||||||
|
viewRoutes.GET("/batchupload", authValidator.RequireAdmin, shopItemController.AddItemsView)
|
||||||
viewRoutes.POST("/login", userController.LoginHandler)
|
viewRoutes.POST("/login", userController.LoginHandler)
|
||||||
viewRoutes.POST("/register", userController.RegisterHandler)
|
viewRoutes.POST("/register", userController.RegisterHandler)
|
||||||
viewRoutes.POST("/additem", authValidator.RequireAuth, shopItemController.AddItemHandler)
|
viewRoutes.POST("/additem", authValidator.RequireAdmin, shopItemController.AddItemHandler)
|
||||||
viewRoutes.POST("/passwordreset", userController.ResetHandler)
|
viewRoutes.POST("/batchupload", authValidator.RequireAdmin, shopItemController.AddItemsHandler)
|
||||||
|
viewRoutes.POST("/passwordreset", authValidator.RequireAuth, userController.ResetHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
server.Run(":" + os.Getenv("PORT"))
|
||||||
server.Run(":"+os.Getenv("PORT"))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
package middlewares
|
package middlewares
|
||||||
|
|
||||||
import(
|
import (
|
||||||
"os"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
//"strconv"
|
//"strconv"
|
||||||
"net/http"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
//"example.com/gin/test/models"
|
//"git.dynamicdiscord.de/kalipso/zineshop/models"
|
||||||
"example.com/gin/test/repositories"
|
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthValidator struct {
|
type AuthValidator struct {
|
||||||
@@ -98,6 +98,63 @@ func (av *AuthValidator) RequireAuth(c *gin.Context) {
|
|||||||
c.AbortWithStatus(http.StatusUnauthorized)
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (av *AuthValidator) RequireAdmin(c *gin.Context) {
|
||||||
|
// Get Cookie
|
||||||
|
tokenString, err := c.Cookie("Authorization")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Validate
|
||||||
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
// Don't forget to validate the alg is what you expect:
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
|
||||||
|
return []byte(os.Getenv("SECRET")), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims, ok := token.Claims.(jwt.MapClaims); ok {
|
||||||
|
//Check Expiration
|
||||||
|
if float64(time.Now().Unix()) > claims["exp"].(float64) {
|
||||||
|
//expired
|
||||||
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Find user
|
||||||
|
user, err := repositories.Users.GetById(claims["sub"])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.IsAdmin {
|
||||||
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Attach to req
|
||||||
|
c.Set("user", user)
|
||||||
|
|
||||||
|
// Coninue
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
func (av *AuthValidator) OptionalAuth(c *gin.Context) {
|
func (av *AuthValidator) OptionalAuth(c *gin.Context) {
|
||||||
defer c.Next()
|
defer c.Next()
|
||||||
|
|
||||||
|
|||||||
161
models/cart.go
Normal file
161
models/cart.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AwaitingConfirmation OrderStatus = "AwaitingConfirmation"
|
||||||
|
Received OrderStatus = "Received"
|
||||||
|
AwaitingPayment OrderStatus = "AwaitingPayment"
|
||||||
|
Payed OrderStatus = "Payed"
|
||||||
|
ReadyForPickup OrderStatus = "ReadyForPickup"
|
||||||
|
Shipped OrderStatus = "Shipped"
|
||||||
|
Cancelled OrderStatus = "Cancelled"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AddressInfo struct {
|
||||||
|
FirstName string `json:"firstname"`
|
||||||
|
LastName string `json:"lastname"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
PostalCode string `json:"postalcode"`
|
||||||
|
City string `json:"city"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Shipping struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetShippingMethods() []Shipping {
|
||||||
|
return []Shipping{
|
||||||
|
{Id: "germany", Name: "Germany (DHL)", Price: 3.99},
|
||||||
|
{Id: "international", Name: "International (DHL)", Price: 5.99},
|
||||||
|
{Id: "pickup", Name: "Pickup", Price: 0.00},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetShippingMethod(id string) (Shipping, error) {
|
||||||
|
var shipping Shipping
|
||||||
|
found := false
|
||||||
|
for _, shippingMethod := range GetShippingMethods() {
|
||||||
|
if shippingMethod.Id == id {
|
||||||
|
shipping = shippingMethod
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return Shipping{}, fmt.Errorf("Shipping method does not exist.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return shipping, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Order struct {
|
||||||
|
gorm.Model
|
||||||
|
SessionId string `json:"sessionid" binding:"required" gorm:"not null"`
|
||||||
|
Status OrderStatus `json:"status"`
|
||||||
|
Token string `json:"token" binding:"required" gorm:"not null"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
FirstName string `json:"firstname"`
|
||||||
|
LastName string `json:"lastname"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
PostalCode string `json:"postalcode"`
|
||||||
|
City string `json:"city"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
Shipping string `json:"shipping"`
|
||||||
|
CartItems []CartItem `json:"cartitems" gorm:"foreignKey:OrderID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Order) Validate() error {
|
||||||
|
//TODO: validate sessionId
|
||||||
|
if o.SessionId == "" {
|
||||||
|
return fmt.Errorf("Invalid SessionId")
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: validate token
|
||||||
|
if o.Token == "" {
|
||||||
|
return fmt.Errorf("Invalid Token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.Status == AwaitingConfirmation {
|
||||||
|
return fmt.Errorf("Order still awaiting confirmation.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(o.CartItems) == 0 {
|
||||||
|
return fmt.Errorf("Order is empty.")
|
||||||
|
}
|
||||||
|
|
||||||
|
shipping, err := GetShippingMethod(o.Shipping)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//for pickup no address validation is necessary
|
||||||
|
if shipping.Id == "pickup" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return o.ValidateAddress()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Order) ValidateAddress() error {
|
||||||
|
if o.FirstName == "" {
|
||||||
|
return fmt.Errorf("Firstname missing")
|
||||||
|
}
|
||||||
|
if o.LastName == "" {
|
||||||
|
return fmt.Errorf("Lastname missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.Address == "" {
|
||||||
|
return fmt.Errorf("Address missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.PostalCode == "" {
|
||||||
|
return fmt.Errorf("Postalcode missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.City == "" {
|
||||||
|
return fmt.Errorf("City missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.Country == "" {
|
||||||
|
return fmt.Errorf("Country missing")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Order) CalculatePrices() (float64, float64, error) {
|
||||||
|
shipping, err := GetShippingMethod(o.Shipping)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
priceProducts := 0.0
|
||||||
|
for _, cartItem := range o.CartItems {
|
||||||
|
priceProducts += (float64(cartItem.Quantity) * cartItem.ItemVariant.Price)
|
||||||
|
}
|
||||||
|
|
||||||
|
priceTotal := priceProducts + shipping.Price
|
||||||
|
|
||||||
|
return priceProducts, priceTotal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CartItem struct {
|
||||||
|
gorm.Model
|
||||||
|
SessionId string `json:"sessionid" binding:"required" gorm:"not null"`
|
||||||
|
ShopItemId uint
|
||||||
|
ShopItem ShopItem `json:"shopitem" gorm:"foreignKey:ShopItemId"` //gorm one2one
|
||||||
|
ItemVariantId uint
|
||||||
|
ItemVariant ItemVariant `json:"itemvariant" gorm:"foreignKey:ItemVariantId"` //gorm one2one
|
||||||
|
Quantity int `json:"quantity" binding:"required"`
|
||||||
|
OrderID uint
|
||||||
|
}
|
||||||
88
models/printer.go
Normal file
88
models/printer.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PrintOption string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CoverPage PrintOption = "-o FrontCoverPage=Printed -o FrontCoverTray=BypassTray"
|
||||||
|
Colored PrintOption = "-o SelectColor=Color"
|
||||||
|
Grayscale PrintOption = "-o SelectColor=Grayscale"
|
||||||
|
LongEdge PrintOption = ""
|
||||||
|
ShortEdge PrintOption = "-o Binding=TopBinding"
|
||||||
|
CreateBooklet PrintOption = "-o Combination=Booklet -o PageSize=A5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PrintJob struct {
|
||||||
|
Pdf string
|
||||||
|
Amount uint
|
||||||
|
Options []PrintOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPrintMode(mode string) PrintOption {
|
||||||
|
if mode == "LongEdge" {
|
||||||
|
return LongEdge
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode == "ShortEdge" {
|
||||||
|
return ShortEdge
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateBooklet
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPrintJob(shopItem ShopItem, variant ItemVariant, coverPage bool, amount uint) (PrintJob, error) {
|
||||||
|
if shopItem.Pdf == "" {
|
||||||
|
return PrintJob{}, fmt.Errorf("ShopItem has no PDF assigned")
|
||||||
|
}
|
||||||
|
|
||||||
|
if amount > 100 {
|
||||||
|
return PrintJob{}, fmt.Errorf("Amount to big. This is denied for security reasons")
|
||||||
|
}
|
||||||
|
|
||||||
|
var result PrintJob
|
||||||
|
result.Pdf = shopItem.Pdf
|
||||||
|
result.Amount = amount
|
||||||
|
|
||||||
|
if variant.Name == "Colored" {
|
||||||
|
result.Options = append(result.Options, Colored)
|
||||||
|
}
|
||||||
|
|
||||||
|
if coverPage {
|
||||||
|
result.Options = append(result.Options, CoverPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Options = append(result.Options, GetPrintMode(shopItem.PrintMode))
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PrintJob) Execute() error {
|
||||||
|
baseCommand := "lp -d KonicaBooklet"
|
||||||
|
baseCommand += fmt.Sprintf(" -n %v ", p.Amount)
|
||||||
|
|
||||||
|
for _, option := range p.Options {
|
||||||
|
baseCommand += fmt.Sprintf(" %v ", option)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseCommand += fmt.Sprintf(" -- %s", p.Pdf)
|
||||||
|
|
||||||
|
parts := strings.Fields(baseCommand)
|
||||||
|
|
||||||
|
// The first part is the command, the rest are the arguments
|
||||||
|
fmt.Println(parts)
|
||||||
|
cmd := exec.Command(parts[0], parts[1:]...)
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error: %s\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Output:\n%s\n", output)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,17 +1,56 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ShopItem struct {
|
/*
|
||||||
gorm.Model
|
Sticker
|
||||||
Name string `json:"name" binding:"required" gorm:"unique;not null"`
|
- name, abstr, descr, price, tag
|
||||||
Abstract string `json:"Abstract" binding:"required"`
|
|
||||||
Description string `json:"description" binding:"required"`
|
Poster
|
||||||
Price float64 `json:"price" binding:"required"`
|
- name, abstr, descr, price bw/colored, tag
|
||||||
IsPublic bool `json:"isPublic" gorm:"default:true"`
|
|
||||||
Tags []Tag `gorm:"many2many:item_tags;"`
|
Zines
|
||||||
Image string
|
- name, abstr, descr, price bw/colored/coloredcoveronly, tag
|
||||||
|
|
||||||
|
Books
|
||||||
|
- name, abstr, descr, price, tag
|
||||||
|
*/
|
||||||
|
type Category string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Zine Category = "Zine"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseCategory(s string) (c Category, err error) {
|
||||||
|
if s == "Zine" {
|
||||||
|
return Zine, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, fmt.Errorf("Cannot parse category %s", s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ItemVariant struct {
|
||||||
|
gorm.Model
|
||||||
|
Name string `json:"name" gorm:"not null"`
|
||||||
|
Price float64 `json:"price" gorm:"not null"`
|
||||||
|
InStock bool `json:"inStock" gorm:"default:true"`
|
||||||
|
ShopItemID uint
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShopItem struct {
|
||||||
|
gorm.Model
|
||||||
|
Name string `json:"name" binding:"required" gorm:"unique;not null"`
|
||||||
|
Abstract string `json:"Abstract" binding:"required"`
|
||||||
|
Description string `json:"description" binding:"required"`
|
||||||
|
Category Category `json:"category"`
|
||||||
|
Variants []ItemVariant `json:"variant"`
|
||||||
|
BasePrice float64 `json:"basePrice"`
|
||||||
|
IsPublic bool `json:"isPublic" gorm:"default:true"`
|
||||||
|
Tags []Tag `gorm:"many2many:item_tags;"`
|
||||||
|
Image string
|
||||||
|
Pdf string
|
||||||
|
PrintMode string `json:"printMode" gorm:"default:CreateBooklet"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,22 +2,51 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"gorm.io/gorm"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"math/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Tag struct {
|
type Tag struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Name string `json:"name" binding:"required" gorm:"unique;not null"`
|
Name string `json:"name" binding:"required" gorm:"not null"`
|
||||||
|
Color string `json:"color" binding:"required" gorm:"default:pink"`
|
||||||
ShopItems []ShopItem `gorm:"many2many:item_tags;"`
|
ShopItems []ShopItem `gorm:"many2many:item_tags;"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTag(ctx *gin.Context) (Tag, error) {
|
func NewTag(ctx *gin.Context) (Tag, error) {
|
||||||
|
colors := []string{
|
||||||
|
"red",
|
||||||
|
"orange",
|
||||||
|
"amber",
|
||||||
|
"yellow",
|
||||||
|
"lime",
|
||||||
|
"green",
|
||||||
|
"emerald",
|
||||||
|
"teal",
|
||||||
|
"cyan",
|
||||||
|
"sky",
|
||||||
|
"blue",
|
||||||
|
"indigo",
|
||||||
|
"violet",
|
||||||
|
"purple",
|
||||||
|
"fuchsia",
|
||||||
|
"pink",
|
||||||
|
"rose",
|
||||||
|
"slate",
|
||||||
|
"gray",
|
||||||
|
"zinc",
|
||||||
|
"neutral",
|
||||||
|
"stone",
|
||||||
|
}
|
||||||
|
n := rand.Int() % len(colors)
|
||||||
|
|
||||||
name := ctx.PostForm("name")
|
name := ctx.PostForm("name")
|
||||||
|
|
||||||
// Convert the price string to float64
|
// Convert the price string to float64
|
||||||
tag := Tag{
|
tag := Tag{
|
||||||
Name: name,
|
Name: name,
|
||||||
|
Color: colors[n],
|
||||||
}
|
}
|
||||||
|
|
||||||
if name == "" {
|
if name == "" {
|
||||||
@@ -27,7 +56,6 @@ func NewTag(ctx *gin.Context) (Tag, error) {
|
|||||||
return tag, nil
|
return tag, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func NewTagByJson(ctx *gin.Context) (Tag, error) {
|
func NewTagByJson(ctx *gin.Context) (Tag, error) {
|
||||||
var tag Tag
|
var tag Tag
|
||||||
err := ctx.ShouldBindJSON(&tag)
|
err := ctx.ShouldBindJSON(&tag)
|
||||||
|
|||||||
@@ -4,9 +4,15 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type RegisterToken struct {
|
||||||
|
gorm.Model
|
||||||
|
Token string `json:"token" binding:"required" gorm:"unique;not null"`
|
||||||
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Name string `json:"name" binding:"required" gorm:"unique;not null"`
|
Name string `json:"name" binding:"required" gorm:"unique;not null"`
|
||||||
Password string `json:"password" binding:"required" gorm:"not null"`
|
Password string `json:"password" binding:"required" gorm:"not null"`
|
||||||
Email string `json:"email" binding:"required,email" gorm:"unique;not null"`
|
Email string `json:"email" binding:"required,email" gorm:"unique;not null"`
|
||||||
|
IsAdmin bool `json:"isAdmin" gorm:"default:false;not null"`
|
||||||
}
|
}
|
||||||
|
|||||||
103
repositories/OrderRepository.go
Normal file
103
repositories/OrderRepository.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.dynamicdiscord.de/kalipso/zineshop/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderRepository interface {
|
||||||
|
Create(models.Order) (models.Order, error)
|
||||||
|
GetAll() ([]models.Order, error)
|
||||||
|
GetById(string) (models.Order, error)
|
||||||
|
GetBySession(string) (models.Order, error)
|
||||||
|
GetByToken(string) (models.Order, error)
|
||||||
|
Update(models.Order) (models.Order, error)
|
||||||
|
DeleteById(string) error
|
||||||
|
DeleteByToken(string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type GORMOrderRepository struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGORMOrderRepository(db *gorm.DB) OrderRepository {
|
||||||
|
return &GORMOrderRepository{
|
||||||
|
DB: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GORMOrderRepository) Create(order models.Order) (models.Order, error) {
|
||||||
|
//Omit the shopitem so it is not created again in db leading to unique constain fails
|
||||||
|
result := r.DB.Omit("CartItems").Create(&order)
|
||||||
|
if result.Error != nil {
|
||||||
|
return models.Order{}, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return order, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GORMOrderRepository) GetAll() ([]models.Order, error) {
|
||||||
|
var orders []models.Order
|
||||||
|
result := r.DB.Preload("CartItems").Find(&orders)
|
||||||
|
|
||||||
|
return orders, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *GORMOrderRepository) GetById(id string) (models.Order, error) {
|
||||||
|
orderId, err := strconv.Atoi(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return models.Order{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var order models.Order
|
||||||
|
result := t.DB.Preload("CartItems").First(&order, uint(orderId))
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return models.Order{}, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return order, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GORMOrderRepository) GetBySession(sessionId string) (models.Order, error) {
|
||||||
|
var orders models.Order
|
||||||
|
result := r.DB.Preload("CartItems").Preload("CartItems.ShopItem").Preload("CartItems.ItemVariant").Where("session_id = ?", sessionId).First(&orders)
|
||||||
|
|
||||||
|
return orders, result.Error
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GORMOrderRepository) GetByToken(token string) (models.Order, error) {
|
||||||
|
var orders models.Order
|
||||||
|
result := r.DB.Preload("CartItems").Preload("CartItems.ShopItem").Preload("CartItems.ItemVariant").Where("token = ?", token).First(&orders)
|
||||||
|
|
||||||
|
return orders, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GORMOrderRepository) Update(order models.Order) (models.Order, error) {
|
||||||
|
result := r.DB.Save(&order)
|
||||||
|
if result.Error != nil {
|
||||||
|
return models.Order{}, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return order, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GORMOrderRepository) DeleteById(id string) error {
|
||||||
|
orderId, err := strconv.Atoi(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := r.DB.Delete(&models.Order{}, orderId)
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GORMOrderRepository) DeleteByToken(token string) error {
|
||||||
|
result := r.DB.Where("token = ?", token).Delete(&models.Order{})
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
90
repositories/cartItemRepository.go
Normal file
90
repositories/cartItemRepository.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import(
|
||||||
|
"strconv"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"git.dynamicdiscord.de/kalipso/zineshop/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CartItemRepository interface {
|
||||||
|
Create(models.CartItem) (models.CartItem, error)
|
||||||
|
GetAll() ([]models.CartItem, error)
|
||||||
|
GetById(string) (models.CartItem, error)
|
||||||
|
GetAllBySession(string) ([]models.CartItem, error)
|
||||||
|
Update(models.CartItem) (models.CartItem, error)
|
||||||
|
DeleteById(string) (error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type GORMCartItemRepository struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGORMCartItemRepository(db *gorm.DB) CartItemRepository {
|
||||||
|
return &GORMCartItemRepository{
|
||||||
|
DB: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GORMCartItemRepository) Create(cartItem models.CartItem) (models.CartItem, error) {
|
||||||
|
//Omit the shopitem so it is not created again in db leading to unique constain fails
|
||||||
|
result := r.DB.Omit("ShopItem").Create(&cartItem)
|
||||||
|
if result.Error != nil {
|
||||||
|
return models.CartItem{}, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return cartItem, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GORMCartItemRepository) GetAll() ([]models.CartItem, error) {
|
||||||
|
var cartItems []models.CartItem
|
||||||
|
result := r.DB.Preload("ShopItem").Preload("ItemVariant").Find(&cartItems)
|
||||||
|
|
||||||
|
return cartItems, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *GORMCartItemRepository) GetById(id string) (models.CartItem, error) {
|
||||||
|
cartItemId, err := strconv.Atoi(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return models.CartItem{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cartItem models.CartItem
|
||||||
|
result := t.DB.Preload("ShopItem").Preload("ItemVariant").First(&cartItem, uint(cartItemId))
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return models.CartItem{}, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return cartItem, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (r *GORMCartItemRepository) GetAllBySession(sessionId string) ([]models.CartItem, error) {
|
||||||
|
var cartItems []models.CartItem
|
||||||
|
result := r.DB.Preload("ShopItem").Preload("ItemVariant").Where("session_id = ?", sessionId).Find(&cartItems)
|
||||||
|
|
||||||
|
return cartItems, result.Error
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GORMCartItemRepository) Update(cartItem models.CartItem) (models.CartItem, error) {
|
||||||
|
result := r.DB.Save(&cartItem)
|
||||||
|
if result.Error != nil {
|
||||||
|
return models.CartItem{}, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return cartItem, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GORMCartItemRepository) DeleteById(id string) error {
|
||||||
|
cartItemId, err := strconv.Atoi(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := r.DB.Omit("ShopItem").Omit("ItemVariant").Delete(&models.CartItem{}, cartItemId)
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
package repositories
|
package repositories
|
||||||
|
|
||||||
import(
|
import (
|
||||||
"os"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"os"
|
||||||
|
|
||||||
"example.com/gin/test/models"
|
"git.dynamicdiscord.de/kalipso/zineshop/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
var(
|
var (
|
||||||
ShopItems ShopItemRepository
|
ShopItems ShopItemRepository
|
||||||
Users UserRepository
|
Users UserRepository
|
||||||
Tags TagRepository
|
Tags TagRepository
|
||||||
|
CartItems CartItemRepository
|
||||||
|
Orders OrderRepository
|
||||||
|
Tokens RegisterTokenRepository
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitRepositories() {
|
func InitRepositories() {
|
||||||
@@ -20,7 +23,14 @@ func InitRepositories() {
|
|||||||
panic("failed to connect to database")
|
panic("failed to connect to database")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = db.AutoMigrate(&models.ShopItem{}, &models.User{}, &models.Tag{})
|
err = db.AutoMigrate(&models.ShopItem{},
|
||||||
|
&models.ItemVariant{},
|
||||||
|
&models.User{},
|
||||||
|
&models.Tag{},
|
||||||
|
&models.CartItem{},
|
||||||
|
&models.Order{},
|
||||||
|
&models.RegisterToken{})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("failed to migrate database")
|
panic("failed to migrate database")
|
||||||
}
|
}
|
||||||
@@ -28,4 +38,7 @@ func InitRepositories() {
|
|||||||
ShopItems = NewGORMShopItemRepository(db)
|
ShopItems = NewGORMShopItemRepository(db)
|
||||||
Users = NewGORMUserRepository(db)
|
Users = NewGORMUserRepository(db)
|
||||||
Tags = NewGORMTagRepository(db)
|
Tags = NewGORMTagRepository(db)
|
||||||
|
CartItems = NewGORMCartItemRepository(db)
|
||||||
|
Orders = NewGORMOrderRepository(db)
|
||||||
|
Tokens = NewGORMRegisterTokenRepository(db)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package repositories
|
package repositories
|
||||||
|
|
||||||
import(
|
import (
|
||||||
"strconv"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"example.com/gin/test/models"
|
"git.dynamicdiscord.de/kalipso/zineshop/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ShopItemRepository interface {
|
type ShopItemRepository interface {
|
||||||
@@ -12,7 +12,8 @@ type ShopItemRepository interface {
|
|||||||
GetAll() ([]models.ShopItem, error)
|
GetAll() ([]models.ShopItem, error)
|
||||||
GetAllPublic() ([]models.ShopItem, error)
|
GetAllPublic() ([]models.ShopItem, error)
|
||||||
GetById(string) (models.ShopItem, error)
|
GetById(string) (models.ShopItem, error)
|
||||||
//GetByTagId(string) ([]models.ShopItem, error)
|
GetByTagId(string) ([]models.ShopItem, error)
|
||||||
|
GetVariantById(string) (models.ItemVariant, error)
|
||||||
Update(models.ShopItem) (models.ShopItem, error)
|
Update(models.ShopItem) (models.ShopItem, error)
|
||||||
DeleteById(string) error
|
DeleteById(string) error
|
||||||
}
|
}
|
||||||
@@ -38,17 +39,16 @@ func (r *GORMShopItemRepository) Create(shopItem models.ShopItem) (models.ShopIt
|
|||||||
|
|
||||||
func (r *GORMShopItemRepository) GetAll() ([]models.ShopItem, error) {
|
func (r *GORMShopItemRepository) GetAll() ([]models.ShopItem, error) {
|
||||||
var shopItems []models.ShopItem
|
var shopItems []models.ShopItem
|
||||||
result := r.DB.Preload("Tags").Find(&shopItems)
|
result := r.DB.Preload("Tags").Preload("Variants").Find(&shopItems)
|
||||||
|
|
||||||
return shopItems, result.Error
|
return shopItems, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *GORMShopItemRepository) GetAllPublic() ([]models.ShopItem, error) {
|
func (r *GORMShopItemRepository) GetAllPublic() ([]models.ShopItem, error) {
|
||||||
var shopItems []models.ShopItem
|
var shopItems []models.ShopItem
|
||||||
result := r.DB.Preload("Tags").Where("is_public = 1").Find(&shopItems)
|
result := r.DB.Preload("Tags").Preload("Variants").Where("is_public = 1").Find(&shopItems)
|
||||||
|
|
||||||
return shopItems, result.Error
|
return shopItems, result.Error
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *GORMShopItemRepository) GetById(id string) (models.ShopItem, error) {
|
func (r *GORMShopItemRepository) GetById(id string) (models.ShopItem, error) {
|
||||||
@@ -59,7 +59,7 @@ func (r *GORMShopItemRepository) GetById(id string) (models.ShopItem, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var shopItem models.ShopItem
|
var shopItem models.ShopItem
|
||||||
result := r.DB.Preload("Tags").First(&shopItem, uint(shopItemId))
|
result := r.DB.Preload("Tags").Preload("Variants").First(&shopItem, uint(shopItemId))
|
||||||
|
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return models.ShopItem{}, result.Error
|
return models.ShopItem{}, result.Error
|
||||||
@@ -68,7 +68,51 @@ func (r *GORMShopItemRepository) GetById(id string) (models.ShopItem, error) {
|
|||||||
return shopItem, nil
|
return shopItem, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *GORMShopItemRepository) GetByTagId(id string) ([]models.ShopItem, error) {
|
||||||
|
tagId, err := strconv.Atoi(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var shopItems []models.ShopItem
|
||||||
|
result := r.DB.Joins("JOIN item_tags ON item_tags.shop_item_id = shop_items.id").Where("item_tags.tag_id = ?", tagId).Preload("Tags").Preload("Variants").Find(&shopItems)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return shopItems, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GORMShopItemRepository) GetVariantById(id string) (models.ItemVariant, error) {
|
||||||
|
itemVariantId, err := strconv.Atoi(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return models.ItemVariant{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemVariant models.ItemVariant
|
||||||
|
result := r.DB.First(&itemVariant, uint(itemVariantId))
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return models.ItemVariant{}, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemVariant, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *GORMShopItemRepository) Update(shopItem models.ShopItem) (models.ShopItem, error) {
|
func (r *GORMShopItemRepository) Update(shopItem models.ShopItem) (models.ShopItem, error) {
|
||||||
|
err := r.DB.Model(&shopItem).Association("Tags").Replace(shopItem.Tags)
|
||||||
|
if err != nil {
|
||||||
|
return models.ShopItem{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.DB.Model(&shopItem).Association("Variants").Replace(shopItem.Variants)
|
||||||
|
if err != nil {
|
||||||
|
return models.ShopItem{}, err
|
||||||
|
}
|
||||||
|
|
||||||
result := r.DB.Save(&shopItem)
|
result := r.DB.Save(&shopItem)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return models.ShopItem{}, result.Error
|
return models.ShopItem{}, result.Error
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import(
|
|||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"example.com/gin/test/models"
|
"git.dynamicdiscord.de/kalipso/zineshop/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TagRepository interface {
|
type TagRepository interface {
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
package repositories
|
package repositories
|
||||||
|
|
||||||
import(
|
import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"example.com/gin/test/models"
|
"git.dynamicdiscord.de/kalipso/zineshop/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserRepository interface {
|
type UserRepository interface {
|
||||||
Create(models.User) (models.User, error)
|
Create(models.User) (models.User, error)
|
||||||
GetByEmail(string) (models.User, error)
|
GetByEmail(string) (models.User, error)
|
||||||
GetById(interface{}) (models.User, error)
|
GetById(interface{}) (models.User, error)
|
||||||
|
IsEmpty() (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type GORMUserRepository struct {
|
type GORMUserRepository struct {
|
||||||
@@ -22,7 +23,7 @@ func NewGORMUserRepository(db *gorm.DB) UserRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *GORMUserRepository) Create(user models.User) (models.User, error) {
|
func (u *GORMUserRepository) Create(user models.User) (models.User, error) {
|
||||||
result := u.DB.Create(&user)
|
result := u.DB.Create(&user)
|
||||||
|
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
@@ -53,3 +54,18 @@ func (u *GORMUserRepository) GetById(id interface{}) (models.User, error) {
|
|||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *GORMUserRepository) IsEmpty() (bool, error) {
|
||||||
|
var user models.User
|
||||||
|
result := u.DB.First(&user)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
if result.Error == gorm.ErrRecordNotFound {
|
||||||
|
return true, nil
|
||||||
|
} else {
|
||||||
|
return false, result.Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package services
|
|||||||
import(
|
import(
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"example.com/gin/test/models"
|
"git.dynamicdiscord.de/kalipso/zineshop/models"
|
||||||
"example.com/gin/test/repositories"
|
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
|
||||||
)
|
)
|
||||||
|
|
||||||
var(
|
var(
|
||||||
@@ -18,7 +18,7 @@ func (u *ShopItemService) NewShopItem(name string, abstract string, description
|
|||||||
Name: name,
|
Name: name,
|
||||||
Abstract: abstract,
|
Abstract: abstract,
|
||||||
Description: description,
|
Description: description,
|
||||||
Price: price,
|
BasePrice: price,
|
||||||
IsPublic: true,
|
IsPublic: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import(
|
import (
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
|
||||||
"example.com/gin/test/models"
|
"git.dynamicdiscord.de/kalipso/zineshop/models"
|
||||||
"example.com/gin/test/repositories"
|
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
|
||||||
)
|
)
|
||||||
|
|
||||||
var(
|
var (
|
||||||
Users UserService = UserService{}
|
Users UserService = UserService{}
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserService struct {}
|
type UserService struct{}
|
||||||
|
|
||||||
func (u *UserService) Register(name string, email string, password string) (models.User, error) {
|
func (u *UserService) Register(name string, email string, password string, isAdmin bool) (models.User, error) {
|
||||||
//hash pw
|
//hash pw
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ func (u *UserService) Register(name string, email string, password string) (mode
|
|||||||
return models.User{}, err
|
return models.User{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
user := models.User{Name: name, Email: email, Password: string(hash)}
|
user := models.User{Name: name, Email: email, Password: string(hash), IsAdmin: isAdmin}
|
||||||
_, err = repositories.Users.Create(user)
|
_, err = repositories.Users.Create(user)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -35,7 +35,7 @@ func (u *UserService) Register(name string, email string, password string) (mode
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//return jwt tokenstring on success
|
// return jwt tokenstring on success
|
||||||
func (u *UserService) Login(email string, password string) (string, error) {
|
func (u *UserService) Login(email string, password string) (string, error) {
|
||||||
//lookup requested user
|
//lookup requested user
|
||||||
user, err := repositories.Users.GetByEmail(email)
|
user, err := repositories.Users.GetByEmail(email)
|
||||||
|
|||||||
BIN
static/img/logo-black.png
Normal file
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 |
1052
static/output.css
1052
static/output.css
File diff suppressed because it is too large
Load Diff
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'"
|
||||||
|
'';
|
||||||
|
}
|
||||||
49
tester.go
49
tester.go
@@ -1,37 +1,36 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"io/ioutil"
|
||||||
"io/ioutil"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func testFunc() {
|
||||||
|
|
||||||
url := "http://localhost:8080/test"
|
url := "http://localhost:8080/test"
|
||||||
method := "GET"
|
method := "GET"
|
||||||
|
|
||||||
client := &http.Client {
|
client := &http.Client{}
|
||||||
}
|
req, err := http.NewRequest(method, url, nil)
|
||||||
req, err := http.NewRequest(method, url, nil)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.Header.Add("Authorization", "Basic dXNlcjpwYXNzd29yZA==")
|
req.Header.Add("Authorization", "Basic dXNlcjpwYXNzd29yZA==")
|
||||||
|
|
||||||
res, err := client.Do(req)
|
res, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
body, err := ioutil.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Println(string(body))
|
fmt.Println(string(body))
|
||||||
}
|
}
|
||||||
|
|||||||
19
utils/utils.go
Normal file
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="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
|
||||||
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
<img class="mx-auto h-10 w-auto" src="/static/img/circlea.png" alt="Your Company">
|
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Your Company">
|
||||||
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Add an Item</h2>
|
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Add an Item</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -55,11 +55,25 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
<input type="hidden" name="category" value="Zine" required>
|
||||||
|
<input type="hidden" id="variant-name1" name="variant-name[]" value="B/W" required>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<label for="price" class="block text-sm/6 font-medium text-gray-900">Price</label>
|
<label for="variant-value1" class="block text-sm/6 font-medium text-gray-900">Price B/W</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<input type="number" name="price" id="price" step="0.01" min="0.00" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
|
<input type="number" name="variant-value[]" id="variant-value1" step="0.01" min="0.00" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input type="hidden" id="variant-name2" name="variant-name[]" value="Colored" required>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label for="variant-value2" class="block text-sm/6 font-medium text-gray-900">Price Colored</label>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input type="number" name="variant-value[]" id="variant-value2" step="0.01" min="0.00" class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -86,6 +100,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-900">
|
||||||
|
PDF
|
||||||
|
</label>
|
||||||
|
<div class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
|
||||||
|
<div class="space-y-1 text-center">
|
||||||
|
<svg class="mx-auto h-12 w-12 text-gray-900" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
|
||||||
|
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
<div class="flex text-sm text-gray-600">
|
||||||
|
<label for="pdf-upload" class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
|
||||||
|
<span class="">Upload a file</span>
|
||||||
|
<input id="pdf-upload" name="pdf" type="file" accept="application/pdf" class="sr-only">
|
||||||
|
</label>
|
||||||
|
<p class="pl-1 text-gray-900">or drag and drop</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-900">
|
||||||
|
PDF up to 50MB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<label class="block text-sm font-medium text-gray-900">
|
||||||
|
Print Mode
|
||||||
|
</label>
|
||||||
|
<select name="print-mode" required class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option selected value="CreateBooklet">Create Booklet</option>
|
||||||
|
<option value="LongEdge">Long Edge</option>
|
||||||
|
<option value="ShortEdge">Short Edge</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
|
||||||
|
<p class="text-sm">Choose 'Create Booklet' if its just a normal PDF (not converted to booklet already)</p>
|
||||||
|
|
||||||
<p class="mt-10 text-center text-sm/6 text-red-500">
|
<p class="mt-10 text-center text-sm/6 text-red-500">
|
||||||
{{ .data.error }}
|
{{ .data.error }}
|
||||||
</p>
|
</p>
|
||||||
@@ -101,5 +151,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{{ template "footer.html" . }}
|
{{ template "footer.html" . }}
|
||||||
|
|||||||
35
views/addtag.html
Normal file
35
views/addtag.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{{ template "header.html" . }}
|
||||||
|
|
||||||
|
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
|
||||||
|
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
|
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Your Company">
|
||||||
|
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Add Tag</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
|
<form class="space-y-6" action="#" method="POST" enctype="multipart/form-data">
|
||||||
|
<div>
|
||||||
|
<label for="name" class="block text-sm/6 font-medium text-gray-900">Name</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input type="text" name="name" id="name" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-10 text-center text-sm/6 text-red-500">
|
||||||
|
{{ .data.error }}
|
||||||
|
</p>
|
||||||
|
<p class="mt-10 text-center text-sm/6 text-green-500">
|
||||||
|
{{ .data.success }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Add Tag</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{{ template "footer.html" . }}
|
||||||
53
views/batchupload.html
Normal file
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" . }}
|
||||||
91
views/cart.html
Normal file
91
views/cart.html
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
{{ template "header.html" . }}
|
||||||
|
|
||||||
|
<section class="py-24 relative">
|
||||||
|
<div class="w-full max-w-7xl px-4 md:px-5 lg-6 mx-auto">
|
||||||
|
<h2 class="title font-manrope font-bold text-4xl leading-10 mb-8 text-center text-black">Shopping Cart
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{{ range .data.cartItems }}
|
||||||
|
|
||||||
|
<div class="rounded-3xl border-2 border-gray-200 p-4 lg:p-8 grid grid-cols-12 mb-8 max-lg:max-w-lg max-lg:mx-auto gap-y-4 ">
|
||||||
|
<div class="col-span-12 lg:col-span-2 img box">
|
||||||
|
<img src="/{{ .ShopItem.Image }}" alt="speaker image" class="max-lg:w-full lg:w-[180px] rounded-lg object-cover">
|
||||||
|
</div>
|
||||||
|
<div class="col-span-12 lg:col-span-10 detail w-full lg:pl-3">
|
||||||
|
<div class="flex items-center justify-between w-full mb-4">
|
||||||
|
<h5 class="font-manrope font-bold text-2xl leading-9 text-gray-900">{{ .ShopItem.Name }} - {{ .ItemVariant.Name}}</h5>
|
||||||
|
<form action="/cart/delete" method="POST">
|
||||||
|
<input type="hidden" id="{{ .ID }}" name="id" value="{{ .ID }}">
|
||||||
|
<button type="submit" class="rounded-full group flex items-center justify-center focus-within:outline-red-500">
|
||||||
|
<svg width="34" height="34" viewBox="0 0 34 34" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle class="fill-red-50 transition-all duration-500 group-hover:fill-red-400"
|
||||||
|
cx="17" cy="17" r="17" fill="" />
|
||||||
|
<path class="stroke-red-500 transition-all duration-500 group-hover:stroke-white"
|
||||||
|
d="M14.1673 13.5997V12.5923C14.1673 11.8968 14.7311 11.333 15.4266 11.333H18.5747C19.2702 11.333 19.834 11.8968 19.834 12.5923V13.5997M19.834 13.5997C19.834 13.5997 14.6534 13.5997 11.334 13.5997C6.90804 13.5998 27.0933 13.5998 22.6673 13.5997C21.5608 13.5997 19.834 13.5997 19.834 13.5997ZM12.4673 13.5997H21.534V18.8886C21.534 20.6695 21.534 21.5599 20.9807 22.1131C20.4275 22.6664 19.5371 22.6664 17.7562 22.6664H16.2451C14.4642 22.6664 13.5738 22.6664 13.0206 22.1131C12.4673 21.5599 12.4673 20.6695 12.4673 18.8886V13.5997Z"
|
||||||
|
stroke="#EF4444" stroke-width="1.6" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<p class="font-normal text-base leading-7 text-gray-500 mb-6">
|
||||||
|
{{ .ShopItem.Abstract }}
|
||||||
|
</p>
|
||||||
|
<form action="/cart/edit" method="POST">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<input type="hidden" id="{{ .ID }}" name="id" value="{{ .ID }}">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<input type="number" name="amount" value="{{ .Quantity }}" step="1" min="0" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
|
||||||
|
<button type="submit" name="action" value="setAmount"
|
||||||
|
class="block w-full bg-indigo-500 rounded-md px-3 py-1.5 text-base text-white placeholder:text-gray-400 sm:text-sm/6 duration-500 hover:bg-indigo-700">
|
||||||
|
Set Amount
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<h6 class="text-indigo-600 font-manrope font-bold text-2xl leading-9 text-right">{{ .Quantity}} x {{ .ItemVariant.Price }}€</h6>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex flex-col md:flex-row items-center md:items-center justify-between lg:px-6 pb-6 max-lg:max-w-lg max-lg:mx-auto">
|
||||||
|
<h5 class="text-gray-900 font-manrope font-semibold text-2xl leading-9 w-full max-md:text-center max-md:mb-4">Subtotal</h5>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between gap-5 ">
|
||||||
|
<h6 class="font-manrope font-bold text-3xl lead-10 text-indigo-600">{{ .data.priceTotal }}€</h6>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<form action="/checkout" method="GET">
|
||||||
|
<div class="flex flex-col md:flex-row items-center md:items-center justify-between lg:px-6 pb-6 border-b border-gray-200 max-lg:max-w-lg max-lg:mx-auto">
|
||||||
|
<h2 class="text-gray-900 font-manrope font-semibold leading-9 w-full max-md:text-center max-md:mb-4">Select shipping method</h2>
|
||||||
|
<select name="shippingMethod" id="shippingMethod" required class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option selected value="">Shipping</option>
|
||||||
|
{{ range .data.shipping }}
|
||||||
|
<option value="{{ .Id }}">{{ .Name }} - {{ .Price }}€</option>
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="max-lg:max-w-lg max-lg:mx-auto">
|
||||||
|
<p class="font-normal text-base leading-7 text-gray-500 text-center mb-5 mt-6">Shipping calculated at checkout</p>
|
||||||
|
<button type="submit"
|
||||||
|
class="rounded py-4 px-6 bg-indigo-500 text-white font-semibold text-lg w-full text-center transition-all duration-500 hover:bg-indigo-700 ">Checkout</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{{ template "footer.html" . }}
|
||||||
92
views/checkout.html
Normal file
92
views/checkout.html
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
{{ template "header.html" . }}
|
||||||
|
|
||||||
|
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
|
||||||
|
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
|
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Logo">
|
||||||
|
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Checkout</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
|
<form class="space-y-6" action="/checkout" method="POST" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="shippingMethod" value="{{ .shippingMethod }}" required>
|
||||||
|
{{ if .askAddress }}
|
||||||
|
<div>
|
||||||
|
<label for="firstName" class="block text-sm/6 font-medium text-gray-900">First Name</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input type="text" name="firstName" id="firstName" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="lastName" class="block text-sm/6 font-medium text-gray-900">Last Name</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input type="text" name="lastName" id="lastName" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="Address" class="block text-sm/6 font-medium text-gray-900">Streetname and Number</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input type="text" name="address" id="address" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="postalCode" class="block text-sm/6 font-medium text-gray-900">Postal Code</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input type="text" name="postalCode" id="postalCode" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="city" class="block text-sm/6 font-medium text-gray-900">City</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input type="text" name="city" id="city" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="country" class="block text-sm/6 font-medium text-gray-900">Country</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input type="text" name="country" id="country" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block text-sm/6 font-medium text-gray-900">E-Mail</label>
|
||||||
|
<p class="text-sm/8 font-small text-gray-600">Without E-Mail you wont receive an order confirmation.</p>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input type="email" name="email" id="email" class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="comment" class="block text-sm/6 font-medium text-gray-900" for="passwordConfirmation">Comment</label>
|
||||||
|
<textarea id="comment" name="comment" type="textarea" class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-300 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-500 dark:focus:border-blue-500 focus:outline-none focus:ring"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-10 text-center text-sm/6 text-red-500">
|
||||||
|
{{ .data.error }}
|
||||||
|
</p>
|
||||||
|
<p class="mt-10 text-center text-sm/6 text-green-500">
|
||||||
|
{{ .data.success }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Preview Order</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{{ template "footer.html" . }}
|
||||||
23
views/colors.html
Normal file
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">
|
<div class="bg-gray-200 rounded m-4 p-4">
|
||||||
<h3 class="text-lg">{{ .data.shopItem.Name }}</h3>
|
<h3 class="text-lg">{{ .data.shopItem.Name }}</h3>
|
||||||
<i class="text-xs">{{ .data.shopItem.Description }}</i>
|
<i class="text-xs">{{ .data.shopItem.Description }}</i>
|
||||||
<p class="">Price: {{ .data.shopItem.Price }}</p>
|
<p class="">Price: {{ .data.shopItem.BasePrice }}</p>
|
||||||
{{ if .loggedIn }}
|
{{ if .isAdmin }}
|
||||||
|
|
||||||
<p class="mt-10 text-center text-sm/6 text-red-500">
|
<p class="mt-10 text-center text-sm/6 text-red-500">
|
||||||
Do you really want to delete this item??
|
Do you really want to delete this item??
|
||||||
|
|||||||
@@ -2,13 +2,17 @@
|
|||||||
|
|
||||||
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
|
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
|
||||||
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
<img class="mx-auto h-10 w-auto" src="/static/img/circlea.png" alt="Your Company">
|
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Your Company">
|
||||||
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Edit Item</h2>
|
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Edit Item</h2>
|
||||||
|
|
||||||
|
<a href="/shopitems/{{ .data.shopItem.ID }}">
|
||||||
|
<img src="/{{ .data.shopItem.Image }}" alt="Product Image" class="aspect-4/5 mx-auto rounded-md bg-gray-200 object-cover group-hover:opacity-75 lg:aspect-auto lg:h-80">
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
<form class="space-y-6" action="#" method="POST">
|
<form class="space-y-6" action="#" method="POST" enctype="multipart/form-data">
|
||||||
<div>
|
<div>
|
||||||
<label for="name" class="block text-sm/6 font-medium text-gray-900">Name</label>
|
<label for="name" class="block text-sm/6 font-medium text-gray-900">Name</label>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
@@ -55,14 +59,28 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
<input type="hidden" name="category" value="Zine" required>
|
||||||
|
<input type="hidden" id="variant-name1" name="variant-name[]" value="B/W" required>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<label for="price" class="block text-sm/6 font-medium text-gray-900">Price</label>
|
<label for="variant-value1" class="block text-sm/6 font-medium text-gray-900">Price B/W</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<input type="number" name="price" id="price" value="{{ .data.shopItem.Price }}" step="0.01" min="0.00" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
|
<input type="number" name="variant-value[]" id="variant-value1" step="0.01" min="0.00" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input type="hidden" id="variant-name2" name="variant-name[]" value="Colored" required>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label for="variant-value2" class="block text-sm/6 font-medium text-gray-900">Price Colored</label>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input type="number" name="variant-value[]" id="variant-value2" step="0.01" min="0.00" class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-900">
|
<label class="block text-sm font-medium text-gray-900">
|
||||||
Image
|
Image
|
||||||
@@ -73,9 +91,9 @@
|
|||||||
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
<div class="flex text-sm text-gray-600">
|
<div class="flex text-sm text-gray-600">
|
||||||
<label for="file-upload" class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
|
<label for="image-upload" class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
|
||||||
<span class="">Upload a file</span>
|
<span class="">Upload a file</span>
|
||||||
<input id="file-upload" name="file-upload" type="file" class="sr-only">
|
<input id="image-upload" name="image" type="file" accept="image/*" class="sr-only">
|
||||||
</label>
|
</label>
|
||||||
<p class="pl-1 text-gray-900">or drag and drop</p>
|
<p class="pl-1 text-gray-900">or drag and drop</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,6 +104,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-900">
|
||||||
|
PDF
|
||||||
|
</label>
|
||||||
|
<div class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
|
||||||
|
<div class="space-y-1 text-center">
|
||||||
|
<svg class="mx-auto h-12 w-12 text-gray-900" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
|
||||||
|
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
<div class="flex text-sm text-gray-600">
|
||||||
|
<label for="pdf-upload" class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
|
||||||
|
<span class="">Upload a file</span>
|
||||||
|
<input id="pdf-upload" name="pdf" type="file" accept="application/pdf" class="sr-only">
|
||||||
|
</label>
|
||||||
|
<p class="pl-1 text-gray-900">or drag and drop</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-900">
|
||||||
|
PDF up to 50MB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="block text-sm font-medium text-gray-900">
|
||||||
|
Print Mode
|
||||||
|
</label>
|
||||||
|
<select name="print-mode" required class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||||
|
<option selected value="CreateBooklet">Create Booklet</option>
|
||||||
|
<option value="LongEdge">Long Edge</option>
|
||||||
|
<option value="ShortEdge">Short Edge</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
|
||||||
<p class="mt-10 text-center text-sm/6 text-red-500">
|
<p class="mt-10 text-center text-sm/6 text-red-500">
|
||||||
{{ .data.error }}
|
{{ .data.error }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
38
views/editorders.html
Normal file
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" . }}
|
||||||
11
views/error.html
Normal file
11
views/error.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{{ template "header.html" . }}
|
||||||
|
|
||||||
|
<p class="mt-10 text-center text-sm/6 text-red-500">
|
||||||
|
{{ .error }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="mt-10 text-center text-sm/6 text-red-500">
|
||||||
|
{{ .data.error }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{ template "footer.html" . }}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>FreiRaum</title>
|
<title>Zine Shop</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link href="/static/output.css" rel="stylesheet">
|
<link href="/static/output.css" rel="stylesheet">
|
||||||
@@ -12,8 +12,8 @@
|
|||||||
<div class="mx-auto max-w-7xl px-4 sm:px-8 lg:px-8">
|
<div class="mx-auto max-w-7xl px-4 sm:px-8 lg:px-8">
|
||||||
<div class="relative flex h-16 items-center justify-between">
|
<div class="relative flex h-16 items-center justify-between">
|
||||||
<div class="flex flex-1 items-center">
|
<div class="flex flex-1 items-center">
|
||||||
<a href="/"><div class="flex shrink-0">
|
<a href="/"><div class="flex-shrink-0 w-full h-full">
|
||||||
<img class="h-8 w-auto" src="/static/img/circlea.png" alt="Your Company">
|
<img class="h-8 w-auto" src="/static/img/logo-white.png" alt="Your Company">
|
||||||
</div></a>
|
</div></a>
|
||||||
<!--
|
<!--
|
||||||
{{ if .loggedIn }}
|
{{ if .loggedIn }}
|
||||||
@@ -25,15 +25,25 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
-->
|
-->
|
||||||
</div>
|
</div>
|
||||||
{{ if .loggedIn }}
|
{{ if .isAdmin }}
|
||||||
<div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0 px-4 sm:px-8">
|
<div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0 px-4 sm:px-8">
|
||||||
<a href="/additem" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">Add Item</a>
|
<a href="/additem" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">Add Item</a>
|
||||||
|
<a href="/batchupload" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300
|
||||||
|
hover:bg-gray-700 hover:text-white">Batch Upload</a>
|
||||||
|
<a href="/orders" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300
|
||||||
|
hover:bg-gray-700 hover:text-white">Orders</a>
|
||||||
|
<a href="/tags" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">Tags</a>
|
||||||
|
<a href="/invites" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">Invites</a>
|
||||||
|
<a href="/cart/print" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700
|
||||||
|
hover:text-white">Print</a>
|
||||||
|
<a href="/cart" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700
|
||||||
|
hover:text-white">Cart</a>
|
||||||
<a href="/logout" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-red-300 hover:bg-gray-700 hover:text-white">Logout</a>
|
<a href="/logout" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-red-300 hover:bg-gray-700 hover:text-white">Logout</a>
|
||||||
</div>
|
</div>
|
||||||
{{ else }}
|
{{ end }}
|
||||||
|
{{ if .loggedIn }}
|
||||||
<div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0 px-4 sm:px-8">
|
<div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0 px-4 sm:px-8">
|
||||||
<a href="/login" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">Login</a>
|
<a href="/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="/register" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">Register</a>
|
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</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="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
|
||||||
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
<img class="mx-auto h-10 w-auto" src="https://tailwindui.com/plus/img/logos/mark.svg?color=indigo&shade=600" alt="Your Company">
|
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Logo">
|
||||||
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Login to your account</h2>
|
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Login to your account</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
106
views/order.html
Normal file
106
views/order.html
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{{ template "header.html" . }}
|
||||||
|
|
||||||
|
<section class="bg-white py-8 antialiased dark:bg-gray-900 md:py-16">
|
||||||
|
<div class="mx-auto max-w-screen-xl px-4 2xl:px-0">
|
||||||
|
<div class="mx-auto max-w-3xl">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white sm:text-2xl">Order summary</h2>
|
||||||
|
<dd class="mt-1 text-base font-normal text-gray-500 dark:text-gray-400">
|
||||||
|
Thanks for your order! As soon as your payment arrived we will print your Order.
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<div class="mt-6 space-y-4 border-b border-t border-gray-200 py-8 dark:border-gray-700 sm:mt-8">
|
||||||
|
<h4 class="text-lg font-semibold text-gray-900 dark:text-white">Order status: {{ .data.order.Status }}</h4>
|
||||||
|
<dl>
|
||||||
|
<dd class="mt-1 text-base font-normal text-gray-500 dark:text-gray-400">
|
||||||
|
Order Code: {{ .data.order.Token }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 space-y-4 border-b border-t border-gray-200 py-8 dark:border-gray-700 sm:mt-8">
|
||||||
|
<h4 class="text-lg font-semibold text-gray-900 dark:text-white">Payment information</h4>
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
<dd class="mt-1 text-base font-normal text-gray-500 dark:text-gray-400">
|
||||||
|
Either you transfer money to our bank account, or you come by and pay in cash.<br><br>
|
||||||
|
|
||||||
|
Miteinander Dresden e.V.*<br>
|
||||||
|
IBAN: DE66500310001076201001<br>
|
||||||
|
BIC: TRODDEF1 (Triodos Bank)<br>
|
||||||
|
Subject: {{ .data.order.Token }}<br>
|
||||||
|
Amount: {{ .data.priceTotal }}€
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 space-y-4 border-b border-t border-gray-200 py-8 dark:border-gray-700 sm:mt-8">
|
||||||
|
<h4 class="text-lg font-semibold text-gray-900 dark:text-white">Delivery information</h4>
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
<dd class="mt-1 text-base font-normal text-gray-500 dark:text-gray-400">
|
||||||
|
<p><b>Shipping:</b> {{ .data.shipping.Name }}</p>
|
||||||
|
{{ if .data.askAddress }}
|
||||||
|
<p><b>First Name:</b> {{ .data.order.FirstName }}</p>
|
||||||
|
<p><b>Last Name:</b> {{ .data.order.LastName }}</p>
|
||||||
|
<p><b>Address:</b> {{ .data.order.Address }}</p>
|
||||||
|
<p><b>Postal Code:</b> {{ .data.order.PostalCode }}</p>
|
||||||
|
<p><b>City:</b> {{ .data.order.City }}</p>
|
||||||
|
<p><b>Country:</b> {{ .data.order.Country }}</p>
|
||||||
|
{{ end }}
|
||||||
|
<p><b>Email:</b> {{ .data.order.Email }}</p>
|
||||||
|
<p><b>Comment:</b> {{ .data.order.Comment }}</p>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 sm:mt-8">
|
||||||
|
<div class="relative overflow-x-auto border-b border-gray-200 dark:border-gray-800">
|
||||||
|
<table class="w-full text-left font-medium text-gray-900 dark:text-white ">
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-800">
|
||||||
|
{{ range .data.order.CartItems }}
|
||||||
|
<tr>
|
||||||
|
<td class="whitespace-nowrap py-4">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<a href="#" class="flex items-center aspect-square w-8 h-10 shrink-0">
|
||||||
|
<img class="h-auto w-full max-h-full dark:hidden" src="/{{ .ShopItem.Image }}" alt="imac image" />
|
||||||
|
</a>
|
||||||
|
<a href="/shopitems/{{ .ShopItem.ID }}" class="hover:underline">{{ .ShopItem.Name }} - {{ .ItemVariant.Name }}</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="p-4 text-base font-normal text-gray-900 dark:text-white">x{{ .Quantity }}</td>
|
||||||
|
<td class="p-4 text-right text-base font-bold text-gray-900 dark:text-white">{{ .ItemVariant.Price }}€</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 space-y-6">
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<dl class="flex items-center justify-between gap-4">
|
||||||
|
<dt class="text-gray-500 dark:text-gray-400">Original price</dt>
|
||||||
|
<dd class="text-base font-medium text-gray-900 dark:text-white">{{ .data.priceProducts }}€</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<dl class="flex items-center justify-between gap-4">
|
||||||
|
<dt class="text-gray-500 dark:text-gray-400">Shipping</dt>
|
||||||
|
<dd class="text-base font-medium text-gray-900 dark:text-white">{{ .data.shipping.Price }}€</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="flex items-center justify-between gap-4 border-t border-gray-200 pt-2 dark:border-gray-700">
|
||||||
|
<dt class="text-lg font-bold text-gray-900 dark:text-white">Total</dt>
|
||||||
|
<dd class="text-lg font-bold text-gray-900 dark:text-white">{{ .data.priceTotal }}€</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{{ template "footer.html" . }}
|
||||||
88
views/orderpreview.html
Normal file
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="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
|
||||||
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
<img class="mx-auto h-10 w-auto" src="https://tailwindui.com/plus/img/logos/mark.svg?color=indigo&shade=600" alt="Your Company">
|
<img class="mx-auto h-10 w-auto" src="/static/img/logo-black.png" alt="Logo">
|
||||||
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Register your account</h2>
|
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Register your account</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
59
views/registertoken.html
Normal file
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" . }}
|
||||||
@@ -1,23 +1,85 @@
|
|||||||
{{ template "header.html" . }}
|
{{ template "header.html" . }}
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
<div class="bg-gray-100 dark:bg-gray-800 py-8">
|
||||||
<div class="bg-gray-200 rounded m-4 p-4">
|
<form action="/cart" method="POST">
|
||||||
<div class="relative w-full md:w-48 mb-4 flex rounded-lg justify-center items-center">
|
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<img src="/{{ .data.shopItem.Image }}" alt="shopping image"
|
<div class="flex flex-col md:flex-row -mx-4">
|
||||||
class="object-cover w-full h-48 md:h-full rounded">
|
<div class="md:flex-1 px-4">
|
||||||
</div>
|
<div class="rounded-lg bg-gray-300 dark:bg-gray-700 mb-4">
|
||||||
<h3 class="text-lg">{{ .data.shopItem.Name }}</h3>
|
<img class="w-full h-full object-cover" src="/{{ .data.shopItem.Image}}" alt="Product Image">
|
||||||
<i class="text-xs">{{ .data.shopItem.Abstract }}</i>
|
</div>
|
||||||
<p class="text-xs">{{ .data.shopItem.Description }}</p>
|
<div class="flex -mx-2 mb-4">
|
||||||
<p class="">Price: {{ .data.shopItem.Price }}</p>
|
|
||||||
{{ if .loggedIn }}
|
{{ 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">
|
<input type="hidden" id="{{ .data.shopItem.ID}}" name="ShopItemId" value="{{ .data.shopItem.ID }}">
|
||||||
<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>
|
<div class="w-1/3 px-2">
|
||||||
<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>
|
<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>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div class="md:flex-1 px-4">
|
||||||
|
<h2 class="text-2xl font-bold text-gray-800 dark:text-white mb-2">{{ .data.shopItem.Name }}</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 text-sm mb-4">
|
||||||
|
{{ .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">
|
||||||
|
<option selected value="">Choose a variant</option>
|
||||||
|
{{ range .data.shopItem.Variants }}
|
||||||
|
<option value="{{ .ID }}">{{ .Name }} - {{ .Price }}€</option>
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<div class="flex mb-4">
|
||||||
|
<div class="mr-4">
|
||||||
|
<span class="font-bold text-gray-700 dark:text-gray-300">Tags:</span>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
{{ range .data.shopItem.Tags }}
|
||||||
|
<a href="/tags/{{ .ID }}"><span class="bg-{{ .Color }}-100 text-{{ .Color }}-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded-sm dark:bg-{{ .Color }}-900 dark:text-{{ .Color }}-300">{{ .Name }}</span></a>
|
||||||
|
{{ end }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
|
<div class="mb-4">
|
||||||
|
<span class="font-bold text-gray-700 dark:text-gray-300">Select Size:</span>
|
||||||
|
<div class="flex items-center mt-2">
|
||||||
|
<button class="bg-gray-300 dark:bg-gray-700 text-gray-700 dark:text-white py-2 px-4 rounded-full font-bold mr-2 hover:bg-gray-400 dark:hover:bg-gray-600">Black/White</button>
|
||||||
|
<button class="bg-gray-300 dark:bg-gray-700 text-gray-700 dark:text-white py-2 px-4 rounded-full font-bold mr-2 hover:bg-gray-400 dark:hover:bg-gray-600">Colored</button>
|
||||||
|
<button class="bg-gray-300 dark:bg-gray-700 text-gray-700 dark:text-white py-2 px-4 rounded-full font-bold mr-2 hover:bg-gray-400 dark:hover:bg-gray-600">Colored Covering Page</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
<div>
|
||||||
|
<span class="font-bold text-gray-700 dark:text-gray-300">Product Description:</span>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 text-sm mt-2">
|
||||||
|
{{ .data.shopItem.Description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{{ template "footer.html" . }}
|
{{ template "footer.html" . }}
|
||||||
|
|||||||
@@ -1,17 +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">
|
||||||
|
<h2 class="text-2xl font-bold tracking-tight text-gray-900">Available Zines</h2>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
<input type="text" id="myInput" onkeyup="myFunction()" placeholder="Search for names..">
|
||||||
|
<div class="mt-6 grid grid-cols-1 gap-x-6 gap-y-10 md:grid-cols-2 xl:grid-cols-4">
|
||||||
{{ range .data.shopItems }}
|
{{ range .data.shopItems }}
|
||||||
<a href="/shopitems/{{ .ID }}">
|
|
||||||
<div class="bg-gray-200 rounded m-4 p-4">
|
|
||||||
|
|
||||||
<div class="relative w-full md:w-48 mb-4 flex rounded-lg justify-center items-center">
|
<div class="myClass group relative">
|
||||||
<img src="/{{ .Image }}" alt="shopping image"
|
<a href="/shopitems/{{ .ID }}">
|
||||||
class="object-cover w-full h-48 md:h-full rounded">
|
<img src="/{{ .Image }}" alt="Product Image" class="aspect-4/5 mx-auto rounded bg-gray-200 object-cover group-hover:opacity-75 lg:aspect-auto lg:h-80">
|
||||||
</div>
|
</a>
|
||||||
<h3 class="text-lg">{{ .Name }}</h3>
|
<div class="mt-4 flex justify-between">
|
||||||
<i class="text-xs">{{ .Abstract }}</i>
|
<div>
|
||||||
<p class="">Price: {{ .Price }}</pÖ>
|
<h3 class="text-sm text-gray-700">
|
||||||
|
<a href="/shopitems/{{ .ID }}">
|
||||||
|
{{ .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 }}
|
||||||
|
<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>
|
||||||
|
{{ if $.loggedIn }}
|
||||||
|
<p class="text-sm font-medium text-gray-900">{{ .BasePrice }}€</p>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
|
||||||
|
<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>
|
||||||
|
|||||||
59
views/tagview.html
Normal file
59
views/tagview.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="Your Company">
|
||||||
|
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Edit Tags</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
|
{{ range .data.tags }}
|
||||||
|
<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>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
||||||
|
<form action="/tags" method="POST">
|
||||||
|
<div class="max-w-md mx-auto mt-4">
|
||||||
|
<div class="flex">
|
||||||
|
<input type="text" id="name" name="name" placeholder="add new tag" class="flex-grow border border-gray-300 rounded-l-md p-2 focus:outline-none focus:ring focus:ring-blue-500">
|
||||||
|
<button type="submit" class="bg-green-600 text-white ml-4 mr-4 rounded px-4 hover:bg-green-700">Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{ template "footer.html" . }}
|
||||||
Reference in New Issue
Block a user