47 Commits

Author SHA1 Message Date
249cccd240 show newest invoice first, show datetime
All checks were successful
Go / build (push) Successful in 13m34s
2025-07-02 11:09:33 +02:00
db3dc9ecd1 better invoice view 2025-07-02 11:09:10 +02:00
b75c46ec2f use pdfcpu for pagecount
All checks were successful
Go / build (push) Successful in 13m36s
2025-07-02 00:50:04 +02:00
16c68cd0f8 set WasPrinted after printjob execution
All checks were successful
Go / build (push) Successful in 12m19s
2025-07-02 00:33:11 +02:00
ad5573ee2c add CoverPage hint to <select> 2025-07-02 00:28:27 +02:00
6c0440f06d add coverpage to price calculation 2025-07-02 00:28:13 +02:00
9e638dcfc2 add tested/not tested indicator on print view
All checks were successful
Go / build (push) Successful in 12m19s
2025-07-02 00:10:37 +02:00
6c59d1233f show invoice after print
All checks were successful
Go / build (push) Successful in 12m16s
2025-07-01 23:45:27 +02:00
7025f526c1 add invoice view
All checks were successful
Go / build (push) Successful in 12m14s
2025-07-01 15:30:06 +02:00
992b9c17c3 store invoices and printjobs
All checks were successful
Go / build (push) Successful in 12m19s
2025-07-01 13:38:28 +02:00
4b0649439c update PrintJob model and add Invoice 2025-07-01 13:15:02 +02:00
8ce01417e7 select papertype on print
All checks were successful
Go / build (push) Successful in 12m15s
2025-06-29 16:58:07 +02:00
8e1df934b3 WIP cost calculation 2025-06-29 16:57:51 +02:00
17a1ef0123 fix paperview
All checks were successful
Go / build (push) Successful in 12m14s
2025-06-29 15:37:10 +02:00
6330a990f5 add paper weight 2025-06-29 15:37:02 +02:00
f4faeb351d add basic paper model/view/controller
All checks were successful
Go / build (push) Successful in 12m50s
paper weight is missing
2025-06-27 17:02:57 +02:00
861b18651b add ui configurable config options 2025-06-27 16:31:37 +02:00
5f53d66bc4 add missing css shadows
All checks were successful
Go / build (push) Successful in 12m33s
2025-05-11 14:46:16 +02:00
459c873986 main view add shadow
All checks were successful
Go / build (push) Successful in 12m50s
2025-05-11 14:19:10 +02:00
ef2e6c99a7 default trifold top bind
All checks were successful
Go / build (push) Successful in 12m37s
2025-04-22 17:36:04 +02:00
e29287c29d trifold add right binding
Some checks failed
Go / build (push) Has been cancelled
not sure if this is correct for ever flyer
2025-04-22 17:19:06 +02:00
f55470636f Merge pull request 'trifold+sort+fixes' (#24) from trifold into master
All checks were successful
Go / build (push) Successful in 12m30s
Reviewed-on: #24
2025-04-21 12:46:35 +02:00
8f89c14961 issue #20 add sort, not yet in view
All checks were successful
Go / build (push) Successful in 12m10s
2025-04-21 12:11:44 +02:00
6f5c0354cc fix #23 viewing non existing item breaks view
Some checks failed
Go / build (push) Has been cancelled
2025-04-21 12:09:39 +02:00
6ef7c53001 feat #19 Flyer - Tri fold?
add trifold option to items
2025-04-21 11:33:18 +02:00
d4e7401586 move printmode up in edititem view
All checks were successful
Go / build (push) Successful in 12m22s
2025-04-16 14:00:12 +02:00
1688e61ccb add next/previous button to edititemhtml 2025-04-16 13:59:43 +02:00
861343b338 prefil prices on edititem
All checks were successful
Go / build (push) Successful in 12m32s
2025-04-16 02:10:11 +02:00
68c8654bf3 set selected printmode in edithtml 2025-04-16 01:56:16 +02:00
e62a45372f move error/success to top on edithmtl 2025-04-16 01:56:00 +02:00
d17c33f6ee set selected tags in edititem 2025-04-16 01:55:40 +02:00
ae36903e73 add cups to path
All checks were successful
Go / build (push) Successful in 12m26s
2025-04-15 16:41:23 +02:00
1a5df21fa8 lazy load images
All checks were successful
Go / build (push) Successful in 12m52s
2025-04-15 15:54:41 +02:00
9c15514758 rename to zines
All checks were successful
Go / build (push) Successful in 12m28s
2025-04-15 14:10:00 +02:00
27cf7c37cf resize preview images on upload
Some checks failed
Go / build (push) Has been cancelled
2025-04-15 13:57:28 +02:00
03f1ce361a add missing return in registerhandler
All checks were successful
Go / build (push) Successful in 12m35s
2025-04-15 10:46:46 +02:00
c55cf4480b Merge pull request 'userhandling' (#16) from userhandling into master
All checks were successful
Go / build (push) Successful in 12m35s
Reviewed-on: #16
2025-04-15 01:06:11 +02:00
bcbb091dfb add invites to admin header
All checks were successful
Go / build (push) Successful in 12m15s
2025-04-15 00:36:51 +02:00
adfb3df283 update route permissions
Some checks failed
Go / build (push) Has been cancelled
2025-04-15 00:31:01 +02:00
1525f44687 show some sections only to admin user 2025-04-15 00:26:54 +02:00
3955d8626a add/rm tokens, register with token 2025-04-15 00:12:34 +02:00
6d63e53200 Add token repositorie 2025-04-14 23:29:24 +02:00
cca0b2775c disable /register if a user exist 2025-04-14 22:55:36 +02:00
98c75c111f update css 2025-04-14 22:42:25 +02:00
b2735e178f hide elements when not logged in 2025-04-14 22:42:07 +02:00
1c9fc230b1 remove rest api routes
currently not used at all
2025-04-14 22:41:43 +02:00
6c2b3964fe rm unused code 2025-04-14 22:41:26 +02:00
41 changed files with 2206 additions and 490 deletions

View File

@@ -1,8 +1,6 @@
package controllers
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"net/http"
@@ -15,6 +13,7 @@ import (
"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 {
@@ -53,15 +52,6 @@ func getSetCookieValue(c *gin.Context, cookieName string) string {
return "" // Return empty string if cookie is not found
}
func generateSessionId(length int) string {
bytes := make([]byte, length) // 16 bytes = 128 bits
_, err := rand.Read(bytes)
if err != nil {
panic("failed to generate session ID")
}
return hex.EncodeToString(bytes)
}
func GetSessionId(ctx *gin.Context) string {
sessionId, err := ctx.Cookie("session_id")
@@ -73,17 +63,13 @@ func GetSessionId(ctx *gin.Context) string {
return responseCookie
}
sessionId = generateSessionId(16)
sessionId = utils.GenerateSessionId(16)
ctx.SetCookie("session_id", sessionId, 3600, "/", "", false, true)
}
return sessionId
}
func GenerateToken() string {
return generateSessionId(16)
}
func (rc *cartItemController) NewCartItemFromForm(ctx *gin.Context) (models.CartItem, error) {
sessionId := GetSessionId(ctx)
shopItemIdStr := ctx.PostForm("ShopItemId")
@@ -162,7 +148,7 @@ func (rc *cartItemController) NewAddressFromForm(ctx *gin.Context) (models.Addre
func (rc *cartItemController) NewOrderFromForm(ctx *gin.Context) (models.Order, error) {
sessionId := GetSessionId(ctx)
status := models.OrderStatus("AwaitingConfirmation")
token := GenerateToken()
token := utils.GenerateToken()
email := ctx.PostForm("email")
comment := ctx.PostForm("comment")
firstName := ctx.PostForm("firstName")

View File

@@ -0,0 +1,350 @@
package controllers
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"git.dynamicdiscord.de/kalipso/zineshop/models"
//"git.dynamicdiscord.de/kalipso/zineshop/services"
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
)
type ConfigController interface {
AddConfigHandler(*gin.Context)
ConfigHandler(*gin.Context)
ConfigView(*gin.Context)
GetAllPaper(*gin.Context)
PaperView(*gin.Context)
PaperHandler(*gin.Context)
AddPaperHandler(*gin.Context)
InvoiceView(*gin.Context)
InvoiceHandler(*gin.Context)
CreateTag(*gin.Context)
GetAllTags(*gin.Context)
TagView(*gin.Context)
TagHandler(*gin.Context)
AddTagHandler(*gin.Context)
}
type configController struct{}
func NewConfigController() ConfigController {
return &configController{}
}
func (rc *configController) AddConfigHandler(c *gin.Context) {
key := c.PostForm("key")
value := c.PostForm("value")
if key == "" || value == "" {
err := "Key or Value empty during config creation"
fmt.Println(err)
c.HTML(http.StatusBadRequest, "configview.html", gin.H{"error": err})
return
}
config := models.Config{
Key: key,
Value: value,
}
_, err := repositories.ConfigOptions.Create(config)
if err != nil {
data := CreateSessionData(c, gin.H{
"error": err,
"success": "",
})
c.HTML(http.StatusOK, "configview.html", data)
return
}
rc.ConfigView(c)
}
func (rc *configController) ConfigView(c *gin.Context) {
configOptions, err := repositories.ConfigOptions.GetAll()
if err != nil {
c.HTML(http.StatusBadRequest, "configview.html", gin.H{"data": gin.H{"error": err}})
}
data := CreateSessionData(c, gin.H{
"configOptions": configOptions,
})
if err != nil {
c.HTML(http.StatusBadRequest, "configview.html", data)
}
c.HTML(http.StatusOK, "configview.html", data)
}
func (rc *configController) ConfigHandler(ctx *gin.Context) {
key := ctx.PostForm("key")
value := ctx.PostForm("value")
action := ctx.PostForm("action")
config, err := repositories.ConfigOptions.GetById(ctx.Param("id"))
if err != nil {
fmt.Println(err)
ctx.HTML(http.StatusBadRequest, "configview.html", gin.H{"error": err})
return
}
if action == "update" {
config.Key = key
config.Value = value
config, err = repositories.ConfigOptions.Update(config)
if err != nil {
fmt.Println(err)
ctx.HTML(http.StatusBadRequest, "configview.html", gin.H{"error": err})
return
}
}
if action == "delete" {
repositories.ConfigOptions.DeleteById(ctx.Param("id"))
}
rc.ConfigView(ctx)
}
func (rc *configController) PaperHandler(ctx *gin.Context) {
newPaper, err := models.NewPaper(ctx)
action := ctx.PostForm("action")
if err != nil {
fmt.Println(err)
ctx.HTML(http.StatusBadRequest, "paperview.html", gin.H{"error": err})
return
}
paper, err := repositories.Papers.GetById(ctx.Param("id"))
if err != nil {
fmt.Println(err)
ctx.HTML(http.StatusBadRequest, "paperview.html", gin.H{"error": err})
return
}
if action == "update" {
paper.Name = newPaper.Name
paper.Brand = newPaper.Brand
paper.Size = newPaper.Size
paper.Weight = newPaper.Weight
paper.Price = newPaper.Price
paper, err = repositories.Papers.Update(paper)
if err != nil {
fmt.Println(err)
ctx.HTML(http.StatusBadRequest, "paperview.html", gin.H{"error": err})
return
}
}
if action == "delete" {
repositories.Papers.DeleteById(ctx.Param("id"))
}
rc.PaperView(ctx)
}
func (rc *configController) AddPaperHandler(c *gin.Context) {
paper, err := models.NewPaper(c)
if err != nil {
fmt.Println(err)
c.HTML(http.StatusBadRequest, "paperview.html", gin.H{"error": err})
return
}
_, err = repositories.Papers.Create(paper)
if err != nil {
data := CreateSessionData(c, gin.H{
"error": err,
"success": "",
})
c.HTML(http.StatusOK, "paperview.html", data)
return
}
rc.PaperView(c)
}
func (rc *configController) PaperView(c *gin.Context) {
papers, err := repositories.Papers.GetAll()
if err != nil {
c.HTML(http.StatusBadRequest, "paperview.html", gin.H{"data": gin.H{"error": err}})
}
data := CreateSessionData(c, gin.H{
"paper": papers,
})
if err != nil {
c.HTML(http.StatusBadRequest, "paperview.html", data)
}
c.HTML(http.StatusOK, "paperview.html", data)
}
func (rc *configController) GetAllPaper(c *gin.Context) {
papers, err := repositories.Papers.GetAll()
if err != nil {
ReplyError(c, fmt.Errorf("Could not query Papers"))
return
}
c.JSON(http.StatusOK, papers)
}
//////////////////////////////////////////////////////////////////////
func (rc *configController) InvoiceView(c *gin.Context) {
invoices, err := repositories.Invoices.GetAllNewestFirst()
if err != nil {
c.HTML(http.StatusBadRequest, "invoiceview.html", gin.H{"data": gin.H{"error": err}})
}
data := CreateSessionData(c, gin.H{
"invoices": invoices,
})
if err != nil {
c.HTML(http.StatusBadRequest, "invoiceview.html", data)
}
c.HTML(http.StatusOK, "invoiceview.html", data)
}
func (rc *configController) InvoiceHandler(ctx *gin.Context) {
action := ctx.PostForm("action")
if action == "delete" {
repositories.Invoices.DeleteById(ctx.Param("id"))
}
rc.InvoiceView(ctx)
}
//////////////////////////////////////////////////////////////////////
func (rc *configController) 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 *configController) 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 *configController) 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 *configController) CreateTag(c *gin.Context) {
tag, err := models.NewTagByJson(c)
if err != nil {
ReplyError(c, err)
}
_, err = repositories.Tags.Create(tag)
if err != nil {
ReplyError(c, fmt.Errorf("tag creation failed: %s", err))
return
}
//userID := user.(models.User).ID
//rc.DB.Model(&models.shopItem{}).Where("id = ?"), room.ID).Association("Admins").Append(&models.User{ID: userID})
//if result.Error != nil {
// ReplyError(c, fmt.Errorf("shopItem creation failed: %s", result.Error))
// return
//}
ReplyOK(c, fmt.Sprintf("tag '%s' was created", tag.Name))
}
func (rc *configController) GetAllTags(c *gin.Context) {
tags, err := repositories.Tags.GetAll()
if err != nil {
ReplyError(c, fmt.Errorf("Could not query Tags"))
return
}
c.JSON(http.StatusOK, tags)
}

View File

@@ -63,7 +63,14 @@ func (rc *printController) PrintCartView(c *gin.Context) {
return
}
paper, err := repositories.Papers.GetAll()
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"data": gin.H{"error": err}})
return
}
data := CreateSessionData(c, gin.H{
"paper": paper,
"cartItems": cartItems,
})
@@ -78,9 +85,16 @@ func (rc *printController) PrintOrderView(c *gin.Context) {
return
}
paper, err := repositories.Papers.GetAll()
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{
"paper": paper,
"cartItems": cartItems,
})
@@ -90,6 +104,7 @@ func (rc *printController) PrintOrderView(c *gin.Context) {
func (rc *printController) PrintHandler(c *gin.Context) {
variantIds := c.PostFormArray("variant-id[]")
variantAmounts := c.PostFormArray("variant-amount[]")
variantPapertypes := c.PostFormArray("variant-papertype[]")
variantCoverPages := c.PostFormArray("variant-coverpage[]")
if len(variantIds) != len(variantAmounts) || len(variantIds) != len(variantCoverPages) {
@@ -98,6 +113,7 @@ func (rc *printController) PrintHandler(c *gin.Context) {
}
var printJobs []models.PrintJob
priceTotal := 0.0
for idx := range variantIds {
variant, err := repositories.ShopItems.GetVariantById(variantIds[idx])
@@ -113,9 +129,21 @@ func (rc *printController) PrintHandler(c *gin.Context) {
return
}
coverPage := false
if variantCoverPages[idx] == "1" {
coverPage = true
paperType, err := repositories.Papers.GetById(fmt.Sprintf("%v", variantPapertypes[idx]))
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"data": gin.H{"error": err}})
return
}
var coverPage *models.Paper
if variantCoverPages[idx] != "0" {
coverPageTmp, err := repositories.Papers.GetById(fmt.Sprintf("%v", variantCoverPages[idx]))
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"data": gin.H{"error": err}})
return
}
coverPage = &coverPageTmp
}
variantAmount, err := strconv.Atoi(variantAmounts[idx])
@@ -124,23 +152,56 @@ func (rc *printController) PrintHandler(c *gin.Context) {
return
}
printJob, err := models.NewPrintJob(shopItem, variant, coverPage, uint(variantAmount))
fmt.Println("Printing Costs:")
printJob, err := models.NewPrintJob(shopItem, variant, paperType, coverPage, uint(variantAmount))
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"data": gin.H{"error": err}})
return
}
printJob.CalculatePrintCosts()
priceTotal += printJob.PriceTotal
printJobs = append(printJobs, printJob)
}
invoice := models.Invoice{
PrintJobs: printJobs,
PricePerClick: 0.002604,
PartCosts: 0.0067,
PriceTotal: priceTotal,
}
invoice, err := repositories.Invoices.Create(invoice)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"data": gin.H{"error": err}})
return
}
executeJobs := func() {
for _, printJob := range printJobs {
printJob.Execute()
err := printJob.Execute()
if err == nil {
return
}
printJob.ShopItem.WasPrinted = true
_, err = repositories.ShopItems.Update(printJob.ShopItem)
if err != nil {
fmt.Printf("Error: %s\n", err)
}
}
}
go executeJobs()
c.HTML(http.StatusOK, "index.html", nil)
fmt.Println(invoice)
data := CreateSessionData(c, gin.H{
"invoice": invoice,
})
c.HTML(http.StatusOK, "printstarted.html", data)
}

View File

@@ -29,15 +29,10 @@ type ShopItemController interface {
AddItemHandler(*gin.Context)
AddItemsView(*gin.Context)
AddItemsHandler(*gin.Context)
CreateTag(*gin.Context)
GetAllTags(*gin.Context)
EditItemView(*gin.Context)
EditItemHandler(*gin.Context)
DeleteItemView(*gin.Context)
DeleteItemHandler(*gin.Context)
TagView(*gin.Context)
TagHandler(*gin.Context)
AddTagHandler(*gin.Context)
}
type shopItemController struct{}
@@ -68,6 +63,9 @@ func (rc *shopItemController) GetById(c *gin.Context) {
ReplyOK(c, shopItem)
}
// this currently creates quite big preview images
// workaround is running the following command in the uploads folder:
// for file in *.png; do convert "$file" -resize 35% "$file"; done
func (rc *shopItemController) NewShopItemFromForm(ctx *gin.Context) (models.ShopItem, error) {
defaultImagePath := "static/img/zine.jpg"
name := ctx.PostForm("name")
@@ -106,6 +104,14 @@ func (rc *shopItemController) NewShopItemFromForm(ctx *gin.Context) (models.Shop
if err != nil {
fmt.Println("Error during pdftoppm: ", err.Error())
}
cmd2 := exec.Command("convert", dstImage, "-resize", "35%", dstImage)
_, err = cmd2.Output()
if err != nil {
fmt.Println("Error during resizing preview image: ", err.Error())
}
}
} else {
fmt.Println(err)
@@ -155,6 +161,7 @@ func (rc *shopItemController) NewShopItemFromForm(ctx *gin.Context) (models.Shop
Pdf: dstPdf,
Variants: variants,
PrintMode: printMode,
WasPrinted: false,
}
fmt.Println("Creating Shopitem: ", shopItem)
@@ -249,14 +256,16 @@ func (rc *shopItemController) ShopItemView(c *gin.Context) {
shopItem, err := repositories.ShopItems.GetById(c.Param("id"))
if err != nil {
c.HTML(http.StatusBadRequest, "shopitem.html", gin.H{"data": gin.H{"error": err}})
c.HTML(http.StatusBadRequest, "error.html", gin.H{"data": gin.H{"error": "Item does not exist"}})
return
}
//TODO: get tags by item
tags, err := repositories.Tags.GetAll()
if err != nil {
c.HTML(http.StatusBadRequest, "shopitem.html", gin.H{"data": gin.H{"error": err}})
c.HTML(http.StatusBadRequest, "error.html", gin.H{"data": gin.H{"error": err}})
return
}
data := CreateSessionData(c, gin.H{
@@ -264,10 +273,6 @@ func (rc *shopItemController) ShopItemView(c *gin.Context) {
"tags": tags,
})
if err != nil {
c.HTML(http.StatusBadRequest, "shopitem.html", data)
}
c.HTML(http.StatusOK, "shopitem.html", data)
}
@@ -365,6 +370,13 @@ func (rc *shopItemController) AddItemsHandler(c *gin.Context) {
fmt.Println("Error during pdftoppm: ", err.Error())
}
cmd2 := exec.Command("convert", dstImage, "-resize", "35%", dstImage)
_, err = cmd2.Output()
if err != nil {
fmt.Println("Error during resizing preview image: ", err.Error())
}
category, err := models.ParseCategory("Zine")
if err != nil {
errorHandler(err)
@@ -389,6 +401,7 @@ func (rc *shopItemController) AddItemsHandler(c *gin.Context) {
Pdf: dstPdf,
Variants: variants,
PrintMode: "CreateBooklet",
WasPrinted: false,
}
_, err = repositories.ShopItems.Create(shopItem)
@@ -414,23 +427,93 @@ func (rc *shopItemController) AddItemsHandler(c *gin.Context) {
c.HTML(http.StatusOK, "batchupload.html", data)
}
func (rc *shopItemController) EditItemView(c *gin.Context) {
shopItem, err := repositories.ShopItems.GetById(c.Param("id"))
tags, err := repositories.Tags.GetAll()
func GetCheckedTags(item models.ShopItem) ([]models.CheckedTag, error) {
allTags, err := repositories.Tags.GetAll()
if err != nil {
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{"error": err})
return nil, err
}
fmt.Println(shopItem)
var tags []models.CheckedTag
for _, tag := range allTags {
tmpTag := models.CheckedTag{
Tag: tag,
Checked: "",
}
data := CreateSessionData(c, gin.H{
"error": "",
"success": "",
"shopItem": shopItem,
"tags": tags,
})
for _, itemTag := range item.Tags {
if itemTag.Name == tmpTag.Name {
tmpTag.Checked = "checked"
break
}
}
tags = append(tags, tmpTag)
}
return tags, nil
}
func (rc *shopItemController) getEditTemplatData(shopItem models.ShopItem) (gin.H, error) {
tags, err := GetCheckedTags(shopItem)
if err != nil {
return gin.H{}, err
}
priceBW := ""
priceColored := ""
for _, variant := range shopItem.Variants {
if variant.Name == "B/W" {
priceBW = fmt.Sprintf("%.2f", variant.Price)
}
if variant.Name == "Colored" {
priceColored = fmt.Sprintf("%.2f", variant.Price)
}
}
templateData := gin.H{
"error": "",
"success": "",
"shopItem": shopItem,
"tags": tags,
"priceBW": priceBW,
"priceColored": priceColored,
}
id := fmt.Sprintf("%d", shopItem.ID)
nextShopItem, err := repositories.ShopItems.GetNextOfId(id)
if err == nil {
fmt.Println("Setting nextitem")
fmt.Println(nextShopItem)
templateData["nextShopItem"] = nextShopItem
}
previousShopItem, err := repositories.ShopItems.GetPreviousOfId(id)
if err == nil {
templateData["previousShopItem"] = previousShopItem
}
return templateData, nil
}
func (rc *shopItemController) EditItemView(c *gin.Context) {
id := c.Param("id")
shopItem, err := repositories.ShopItems.GetById(id)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
}
templateData, err := rc.getEditTemplatData(shopItem)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
}
data := CreateSessionData(c, templateData)
c.HTML(http.StatusOK, "edititem.html", data)
}
@@ -456,6 +539,7 @@ func (rc *shopItemController) EditItemHandler(c *gin.Context) {
newShopItem.Variants = shopItem.Variants
newShopItem.BasePrice = shopItem.BasePrice
newShopItem.IsPublic = shopItem.IsPublic
newShopItem.WasPrinted = false
if len(shopItem.Tags) != 0 {
newShopItem.Tags = shopItem.Tags
@@ -471,30 +555,22 @@ func (rc *shopItemController) EditItemHandler(c *gin.Context) {
newShopItem.PrintMode = shopItem.PrintMode
tags, err := repositories.Tags.GetAll()
templateData, err := rc.getEditTemplatData(newShopItem)
if err != nil {
c.HTML(http.StatusBadRequest, "edititem.html", gin.H{"error": err})
return
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
}
_, err = repositories.ShopItems.Update(newShopItem)
if err != nil {
data := CreateSessionData(c, gin.H{
"error": err,
"success": "",
"tags": tags,
})
templateData["error"] = err
data := CreateSessionData(c, templateData)
c.HTML(http.StatusOK, "edititem.html", data)
return
}
data := CreateSessionData(c, gin.H{
"error": "",
"success": fmt.Sprintf("Item '%s' Updated", newShopItem.Name),
"tags": tags,
})
templateData["success"] = fmt.Sprintf("Item '%s' Updated", newShopItem.Name)
data := CreateSessionData(c, templateData)
c.HTML(http.StatusOK, "edititem.html", data)
}
@@ -543,115 +619,6 @@ func (rc *shopItemController) DeleteItemHandler(c *gin.Context) {
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) {
tag, err := models.NewTagByJson(c)
if err != nil {
ReplyError(c, err)
}
_, err = repositories.Tags.Create(tag)
if err != nil {
ReplyError(c, fmt.Errorf("shopItem creation failed: %s", err))
return
}
//userID := user.(models.User).ID
//rc.DB.Model(&models.shopItem{}).Where("id = ?"), room.ID).Association("Admins").Append(&models.User{ID: userID})
//if result.Error != nil {
// ReplyError(c, fmt.Errorf("shopItem creation failed: %s", result.Error))
// return
//}
ReplyOK(c, fmt.Sprintf("tag '%s' was created", tag.Name))
}
func (rc *shopItemController) GetAllTags(c *gin.Context) {
tags, err := repositories.Tags.GetAll()
if err != nil {
ReplyError(c, fmt.Errorf("Could not query Tags"))
return
}
c.JSON(http.StatusOK, tags)
}
func ReplyError(ctx *gin.Context, err error) {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}

View File

@@ -1,11 +1,12 @@
package controllers
import (
"errors"
"fmt"
"math/rand"
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"git.dynamicdiscord.de/kalipso/zineshop/models"
"git.dynamicdiscord.de/kalipso/zineshop/repositories"
@@ -36,7 +37,7 @@ func (uc *UserController) Register(c *gin.Context) {
return
}
_, err = services.Users.Register(body.Name, body.Email, body.Password)
_, err = services.Users.Register(body.Name, body.Email, body.Password, false)
if err != nil {
fmt.Println("Error: ", err)
@@ -140,10 +141,12 @@ func (rc *UserController) LoginHandler(c *gin.Context) {
}
func CreateSessionData(c *gin.Context, extra any) gin.H {
_, exists := c.Get("user")
user, exists := c.Get("user")
userImpl, _ := user.(models.User)
return gin.H{
"loggedIn": exists,
"isAdmin": userImpl.IsAdmin,
"data": extra,
}
}
@@ -153,11 +156,34 @@ func (rc *UserController) RegisterHandler(c *gin.Context) {
email := c.PostForm("email")
password := c.PostForm("password")
_, err := services.Users.Register(name, email, password)
//first registered user is admin
isEmpty, _ := repositories.Users.IsEmpty()
if isEmpty {
_, err := services.Users.Register(name, email, password, true)
if err != nil {
data := gin.H{
"error": "Registering Failed.",
"success": "",
}
c.HTML(http.StatusOK, "register.html", data)
return
}
if err != nil {
data := gin.H{
"error": "Registering Failed.",
"error": "",
"success": "You successfully registered as Admin. Try logging in.",
}
c.HTML(http.StatusOK, "register.html", data)
return
}
//for any other user token is required
token := c.PostForm("token")
if token == "" {
data := gin.H{
"error": "No token. No register.",
"success": "",
}
@@ -165,6 +191,43 @@ func (rc *UserController) RegisterHandler(c *gin.Context) {
return
}
tokenExists, err := repositories.Tokens.Exists(token)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
data := gin.H{
"error": err,
"success": "",
}
c.HTML(http.StatusOK, "register.html", data)
return
}
if !tokenExists {
data := gin.H{
"error": "Invalid Token.",
"success": "",
}
c.HTML(http.StatusOK, "register.html", data)
return
}
_, err = services.Users.Register(name, email, password, false)
if err != nil {
data := gin.H{
"error": "Registering Failed.",
"success": "",
}
c.HTML(http.StatusOK, "register.html", data)
return
}
err = repositories.Tokens.Delete(token)
if err != nil {
fmt.Println("Could not delete RegisterToken: ", err)
}
data := gin.H{
"error": "",
"success": "You successfully registered. Try logging in.",
@@ -174,6 +237,39 @@ func (rc *UserController) RegisterHandler(c *gin.Context) {
}
func (rc *UserController) RegisterView(c *gin.Context) {
data := gin.H{
"error": "",
"success": "",
"token": c.Param("token"),
}
c.HTML(http.StatusOK, "registertoken.html", data)
}
func (rc *UserController) InitAdmin(c *gin.Context) {
isEmpty, err := repositories.Users.IsEmpty()
if err != nil {
data := gin.H{
"error": err,
"success": "",
}
c.HTML(http.StatusInternalServerError, "error.html", data)
return
}
if !isEmpty {
data := gin.H{
"error": "Registration is closed",
"success": "",
}
c.HTML(http.StatusInternalServerError, "error.html", data)
return
}
data := gin.H{
"error": "",
"success": "",
@@ -204,9 +300,53 @@ func (rc *UserController) ResetHandler(c *gin.Context) {
c.HTML(http.StatusOK, "passwordreset.html", data)
}
func (rc *UserController) InviteView(c *gin.Context) {
tokens, _ := repositories.Tokens.GetAll()
fmt.Println(tokens)
data := gin.H{
"tokens": tokens,
}
c.HTML(http.StatusOK, "invites.html", data)
}
func (rc *UserController) InviteHandler(c *gin.Context) {
action := c.PostForm("action")
if action == "create" {
_, err := repositories.Tokens.Create()
if err != nil {
fmt.Println(err)
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return
}
}
if action == "delete" {
token := c.PostForm("token")
repositories.Tokens.Delete(token)
}
rc.InviteView(c)
}
func (rc *UserController) MainView(c *gin.Context) {
shopItems, _ := repositories.ShopItems.GetAll()
fmt.Println(len(shopItems))
itemOrder := c.Query("order")
var shopItems []models.ShopItem
if itemOrder == "newestFirst" {
shopItems, _ = repositories.ShopItems.GetAllNewestFirst()
} else if itemOrder == "oldestFirst" {
shopItems, _ = repositories.ShopItems.GetAllNewestLast()
} else if itemOrder == "az" {
shopItems, _ = repositories.ShopItems.GetAllLexicalFirst()
} else if itemOrder == "za" {
shopItems, _ = repositories.ShopItems.GetAllLexicalLast()
} else {
shopItems, _ = repositories.ShopItems.GetAllNewestFirst()
}
data := CreateSessionData(c, gin.H{
"title": "shopItem Page",
@@ -227,68 +367,6 @@ func (rc *UserController) TagView(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", data)
}
type booking struct {
Booked bool
}
type calendarbooking struct {
Time string
Bookings []booking
}
func (rc *UserController) CalendarView(c *gin.Context) {
shopItems, _ := repositories.ShopItems.GetAll()
fmt.Println(len(shopItems))
generateBookings := func(amountShopItems int) []calendarbooking {
var result []calendarbooking
time := 6
for _ = range 18 {
book := calendarbooking{
Time: fmt.Sprintf("%d:00", time),
Bookings: []booking{},
}
for _ = range amountShopItems {
book.Bookings = append(book.Bookings, booking{Booked: rand.Float32() < 0.5})
}
time += 1
result = append(result, book)
}
return result
}
bookings := gin.H{
"head": []string{
"malobeo",
"hole of fame",
"BK",
"AZ Conni",
},
"bookings": generateBookings(4),
//"bookings": []calendarbooking{
// {
// Time: "10:00",
// Bookings: []booking{
// { Booked: true },
// { Booked: false },
// },
// },
//},
}
data := CreateSessionData(c, gin.H{
"title": "shopItem Page",
"bookings": bookings,
"shopItemcount": len(bookings["head"].([]string)) + 1,
})
fmt.Println(data)
c.HTML(http.StatusOK, "calendar.html", data)
}
func (rc *UserController) Logout(c *gin.Context) {
c.SetCookie("Authorization", "", -1, "", "", false, true)
c.HTML(http.StatusOK, "index.html", gin.H{})

View File

@@ -20,6 +20,8 @@
go
gotools
poppler_utils #get first pdf page to png
cups
imagemagick
tailwindcss
];
};
@@ -71,6 +73,8 @@
environment.systemPackages = [
zineshop-pkg
pkgs.poppler_utils #get first pdf page to png
pkgs.cups
pkgs.imagemagick
];
systemd.tmpfiles.rules = [
@@ -100,7 +104,7 @@
WorkingDirectory = "/var/lib/zineshop";
ExecStart = pkgs.writeScript "start-zineshop" ''
#! ${pkgs.bash}/bin/bash
PATH="$PATH:${lib.makeBinPath [ pkgs.poppler_utils ]}"
PATH="$PATH:${lib.makeBinPath [ pkgs.poppler_utils pkgs.cups pkgs.imagemagick ]}"
${zineshop-pkg}/bin/zineshop
'';
Restart = "on-failure";

15
go.mod
View File

@@ -6,7 +6,8 @@ require (
github.com/gin-gonic/gin v1.10.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/joho/godotenv v1.5.1
golang.org/x/crypto v0.23.0
github.com/pdfcpu/pdfcpu v0.11.0
golang.org/x/crypto v0.38.0
gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.25.12
)
@@ -22,22 +23,30 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/hhrutter/lzw v1.0.0 // indirect
github.com/hhrutter/pkcs7 v0.2.0 // indirect
github.com/hhrutter/tiff v1.0.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/image v0.27.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

31
go.sum
View File

@@ -30,6 +30,12 @@ github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0=
github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo=
github.com/hhrutter/pkcs7 v0.2.0 h1:i4HN2XMbGQpZRnKBLsUwO3dSckzgX142TNqY/KfXg+I=
github.com/hhrutter/pkcs7 v0.2.0/go.mod h1:aEzKz0+ZAlz7YaEMY47jDHL14hVWD6iXt0AgqgAvWgE=
github.com/hhrutter/tiff v1.0.2 h1:7H3FQQpKu/i5WaSChoD1nnJbGx4MxU5TlNqqpxw55z8=
github.com/hhrutter/tiff v1.0.2/go.mod h1:pcOeuK5loFUE7Y/WnzGw20YxUdnqjY1P0Jlcieb/cCw=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -46,6 +52,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -53,10 +61,17 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pdfcpu/pdfcpu v0.11.0 h1:mL18Y3hSHzSezmnrzA21TqlayBOXuAx7BUzzZyroLGM=
github.com/pdfcpu/pdfcpu v0.11.0/go.mod h1:F1ca4GIVFdPtmgvIdvXAycAm88noyNxZwzr9CpTy+Mw=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -76,22 +91,26 @@ github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

90
main.go
View File

@@ -19,6 +19,7 @@ var (
userController controllers.UserController = controllers.UserController{}
cartItemController controllers.CartItemController = controllers.NewCartItemController()
printController controllers.PrintController = controllers.NewPrintController()
configController controllers.ConfigController = controllers.NewConfigController()
authValidator middlewares.AuthValidator = middlewares.AuthValidator{}
)
@@ -55,66 +56,63 @@ func main() {
server.Static("/static", os.Getenv("STATIC"))
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.GET("/", userController.MainView)
viewRoutes.GET("/shopitems/:id", shopItemController.ShopItemView)
viewRoutes.GET("/shopitems/:id/edit", authValidator.RequireAuth, shopItemController.EditItemView)
viewRoutes.POST("/shopitems/:id/edit", authValidator.RequireAuth, shopItemController.EditItemHandler)
viewRoutes.GET("/shopitems/:id/delete", authValidator.RequireAuth, shopItemController.DeleteItemView)
viewRoutes.POST("/shopitems/:id/delete", authValidator.RequireAuth, shopItemController.DeleteItemHandler)
viewRoutes.GET("/variant/:id/print", authValidator.RequireAuth, printController.PrintVariantView)
viewRoutes.GET("/cart/print", authValidator.RequireAuth, printController.PrintCartView)
viewRoutes.POST("/print", authValidator.RequireAuth, printController.PrintHandler)
viewRoutes.GET("/shopitems/:id/edit", authValidator.RequireAdmin, shopItemController.EditItemView)
viewRoutes.POST("/shopitems/:id/edit", authValidator.RequireAdmin, shopItemController.EditItemHandler)
viewRoutes.GET("/shopitems/:id/delete", authValidator.RequireAdmin, shopItemController.DeleteItemView)
viewRoutes.POST("/shopitems/:id/delete", authValidator.RequireAdmin, shopItemController.DeleteItemHandler)
viewRoutes.GET("/variant/:id/print", authValidator.RequireAdmin, printController.PrintVariantView)
viewRoutes.GET("/cart/print", authValidator.RequireAdmin, printController.PrintCartView)
viewRoutes.POST("/print", authValidator.RequireAdmin, printController.PrintHandler)
viewRoutes.GET("/tags", authValidator.RequireAuth, shopItemController.TagView)
viewRoutes.POST("/tags/:id", authValidator.RequireAuth, shopItemController.TagHandler)
viewRoutes.GET("/config", authValidator.RequireAdmin, configController.ConfigView)
viewRoutes.POST("/config/:id", authValidator.RequireAdmin, configController.ConfigHandler)
viewRoutes.POST("/config", authValidator.RequireAdmin, configController.AddConfigHandler)
viewRoutes.GET("/tags", authValidator.RequireAdmin, configController.TagView)
viewRoutes.POST("/tags/:id", authValidator.RequireAdmin, configController.TagHandler)
viewRoutes.GET("/tags/:id", userController.TagView)
viewRoutes.POST("/tags", authValidator.RequireAuth, shopItemController.AddTagHandler)
viewRoutes.GET("/cart", cartItemController.CartItemView)
viewRoutes.POST("/cart", cartItemController.AddItemHandler)
viewRoutes.POST("/cart/delete", cartItemController.DeleteItemHandler)
viewRoutes.POST("/cart/edit", cartItemController.EditItemHandler)
viewRoutes.GET("/checkout", cartItemController.CheckoutView)
viewRoutes.POST("/checkout", cartItemController.CheckoutHandler)
viewRoutes.POST("/order", cartItemController.OrderHandler)
viewRoutes.GET("/order/:token", cartItemController.OrderView)
viewRoutes.POST("/tags", authValidator.RequireAdmin, configController.AddTagHandler)
viewRoutes.GET("/paper", authValidator.RequireAdmin, configController.PaperView)
viewRoutes.POST("/paper/:id", authValidator.RequireAdmin, configController.PaperHandler)
viewRoutes.GET("/paper/:id", userController.TagView)
viewRoutes.POST("/paper", authValidator.RequireAdmin, configController.AddPaperHandler)
viewRoutes.GET("/invoice", authValidator.RequireAdmin, configController.InvoiceView)
viewRoutes.POST("/invoice/:id", authValidator.RequireAdmin, configController.InvoiceHandler)
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.RequireAuth, cartItemController.OrdersView)
viewRoutes.POST("/order/:token/edit", authValidator.RequireAuth, cartItemController.OrdersHandler)
viewRoutes.GET("/orders", authValidator.RequireAdmin, cartItemController.OrdersView)
viewRoutes.POST("/order/:token/edit", authValidator.RequireAdmin, cartItemController.OrdersHandler)
//write middleware that redirects to homescreen on register/login/reset for logged in users
viewRoutes.GET("/login", userController.LoginView)
viewRoutes.GET("/logout", userController.Logout)
viewRoutes.GET("/register", userController.RegisterView)
viewRoutes.GET("/passwordreset", userController.ResetView)
viewRoutes.GET("/additem", authValidator.RequireAuth, shopItemController.AddItemView)
viewRoutes.GET("/batchupload", authValidator.RequireAuth, shopItemController.AddItemsView)
viewRoutes.GET("/register", userController.InitAdmin)
viewRoutes.GET("/register/:token", userController.RegisterView)
viewRoutes.GET("/invites", authValidator.RequireAdmin, userController.InviteView)
viewRoutes.POST("/invites", authValidator.RequireAdmin, userController.InviteHandler)
viewRoutes.GET("/passwordreset", authValidator.RequireAuth, userController.ResetView)
viewRoutes.GET("/additem", authValidator.RequireAdmin, shopItemController.AddItemView)
viewRoutes.GET("/batchupload", authValidator.RequireAdmin, shopItemController.AddItemsView)
viewRoutes.POST("/login", userController.LoginHandler)
viewRoutes.POST("/register", userController.RegisterHandler)
viewRoutes.POST("/additem", authValidator.RequireAuth, shopItemController.AddItemHandler)
viewRoutes.POST("/batchupload", authValidator.RequireAuth, shopItemController.AddItemsHandler)
viewRoutes.POST("/passwordreset", userController.ResetHandler)
viewRoutes.POST("/additem", authValidator.RequireAdmin, shopItemController.AddItemHandler)
viewRoutes.POST("/batchupload", authValidator.RequireAdmin, shopItemController.AddItemsHandler)
viewRoutes.POST("/passwordreset", authValidator.RequireAuth, userController.ResetHandler)
}
server.Run(":" + os.Getenv("PORT"))

View File

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

11
models/config.go Normal file
View File

@@ -0,0 +1,11 @@
package models
import (
"gorm.io/gorm"
)
type Config struct {
gorm.Model
Key string `json:"key" binding:"required" gorm:"unique;not null"`
Value string `json:"value" binding:"required"`
}

83
models/paper.go Normal file
View File

@@ -0,0 +1,83 @@
package models
import (
"fmt"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"strconv"
"strings"
)
type PaperSize string
const (
A3 PaperSize = "A3"
A4 PaperSize = "A4"
A5 PaperSize = "A5"
SRA3 PaperSize = "SRA3"
)
func ParseSize(s string) (c PaperSize, err error) {
s = strings.ToUpper(s)
if s == "A3" {
return A3, nil
} else if s == "A4" {
return A4, nil
} else if s == "A5" {
return A5, nil
} else if s == "SRA3" {
return SRA3, nil
}
return c, fmt.Errorf("Cannot parse category %s", s)
}
type Paper struct {
gorm.Model
Name string `json:"name" binding:"required" gorm:"not null"`
Brand string `json:"brand" binding:"required"`
Size PaperSize `json:"size" binding:"required"`
Weight int `json:"weight" binding:"required"`
Price float64 `json:"price" binding:"required"`
}
func NewPaper(ctx *gin.Context) (Paper, error) {
name := ctx.PostForm("name")
brand := ctx.PostForm("brand")
sizeTmp := ctx.PostForm("size")
weightTmp := ctx.PostForm("weight")
priceTmp := ctx.PostForm("price")
size, err := ParseSize(sizeTmp)
if err != nil {
return Paper{}, fmt.Errorf("Couldnt parse Size")
}
weight, err := strconv.Atoi(weightTmp)
if err != nil {
return Paper{}, fmt.Errorf("Couldnt parse Weight")
}
price, err := strconv.ParseFloat(priceTmp, 64)
if err != nil {
return Paper{}, fmt.Errorf("Couldnt parse Price")
}
if name == "" || brand == "" {
return Paper{}, fmt.Errorf("Name or brand empty")
}
// Convert the price string to float64
tag := Paper{
Name: name,
Brand: brand,
Size: size,
Weight: weight,
Price: price,
}
return tag, nil
}

View File

@@ -2,6 +2,9 @@ package models
import (
"fmt"
"git.dynamicdiscord.de/kalipso/zineshop/utils"
"gorm.io/gorm"
"math"
"os/exec"
"strings"
)
@@ -15,14 +18,39 @@ const (
LongEdge PrintOption = ""
ShortEdge PrintOption = "-o Binding=TopBinding"
CreateBooklet PrintOption = "-o Combination=Booklet -o PageSize=A5"
TriFold PrintOption = "-o Fold=TriFold -o Binding=TopBinding"
)
type PrintJob struct {
type OldPrintJob struct {
Pdf string
Amount uint
Options []PrintOption
}
type Invoice struct {
gorm.Model
PrintJobs []PrintJob
PricePerClick float64
PartCosts float64
PriceTotal float64
}
type PrintJob struct {
gorm.Model
ShopItemID uint
ShopItem ShopItem
VariantID uint
Variant ItemVariant
PaperTypeId uint
PaperType Paper `gorm:"foreignKey:PaperTypeId"`
CoverPaperTypeId *uint
CoverPaperType *Paper `gorm:"foreignKey:CoverPaperTypeId"`
Amount uint
PricePerPiece float64
PriceTotal float64
InvoiceID uint
}
func GetPrintMode(mode string) PrintOption {
if mode == "LongEdge" {
return LongEdge
@@ -32,10 +60,14 @@ func GetPrintMode(mode string) PrintOption {
return ShortEdge
}
if mode == "TriFold" {
return TriFold
}
return CreateBooklet
}
func NewPrintJob(shopItem ShopItem, variant ItemVariant, coverPage bool, amount uint) (PrintJob, error) {
func NewPrintJob(shopItem ShopItem, variant ItemVariant, paperType Paper, coverPaperType *Paper, amount uint) (PrintJob, error) {
if shopItem.Pdf == "" {
return PrintJob{}, fmt.Errorf("ShopItem has no PDF assigned")
}
@@ -44,32 +76,44 @@ func NewPrintJob(shopItem ShopItem, variant ItemVariant, coverPage bool, amount
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)
result := PrintJob{
ShopItem: shopItem,
Variant: variant,
PaperType: paperType,
CoverPaperType: coverPaperType,
Amount: amount,
}
if coverPage {
result.Options = append(result.Options, CoverPage)
}
result.Options = append(result.Options, GetPrintMode(shopItem.PrintMode))
return result, nil
}
func (p *PrintJob) IsColored() bool {
return p.Variant.Name == "Colored"
}
func (p *PrintJob) GeneratePrintOptions() []PrintOption {
var result []PrintOption
if p.Variant.Name == "Colored" {
result = append(result, Colored)
}
if p.CoverPaperType != nil {
result = append(result, CoverPage)
}
result = append(result, GetPrintMode(p.ShopItem.PrintMode))
return result
}
func (p *PrintJob) Execute() error {
baseCommand := "lp -d KonicaBooklet"
baseCommand += fmt.Sprintf(" -n %v ", p.Amount)
for _, option := range p.Options {
for _, option := range p.GeneratePrintOptions() {
baseCommand += fmt.Sprintf(" %v ", option)
}
baseCommand += fmt.Sprintf(" -- %s", p.Pdf)
baseCommand += fmt.Sprintf(" -- %s", p.ShopItem.Pdf)
parts := strings.Fields(baseCommand)
@@ -86,3 +130,51 @@ func (p *PrintJob) Execute() error {
fmt.Printf("Output:\n%s\n", output)
return nil
}
func (p *PrintJob) CalculatePrintCosts() (float64, error) {
pageCount := utils.CountPagesAtPath(p.ShopItem.Pdf)
if pageCount == 0 {
fmt.Println("Pagecount of 0 - something is wrong here.")
return 0, fmt.Errorf("Cant calculate price, pdf seems to be empty")
}
printMode := GetPrintMode(p.ShopItem.PrintMode)
//Get actual pagecount depending on printmode
actualPageCount := pageCount
fmt.Println("PagCount: ", actualPageCount)
if printMode == CreateBooklet {
dividedCount := float64(pageCount) / 4.0
actualPageCount = int(math.Ceil(dividedCount))
}
if printMode == LongEdge || printMode == ShortEdge {
dividedCount := float64(pageCount) / 2.0
actualPageCount = int(math.Ceil(dividedCount))
}
PPC := 0.002604
partCost := 0.0067
if p.IsColored() {
partCost = 0.0478
}
printingCosts := float64(actualPageCount-1) * p.PaperType.Price
if p.CoverPaperType != nil {
printingCosts += p.CoverPaperType.Price
} else {
printingCosts += p.PaperType.Price
}
printingCosts += float64(actualPageCount/2) * PPC
printingCosts += partCost * float64(actualPageCount)
fmt.Printf("Printing Costs per Zine: %v\n", printingCosts)
fmt.Printf("Printing Costs Total: %v\n", printingCosts*float64(p.Amount))
p.PricePerPiece = printingCosts
p.PriceTotal = printingCosts * float64(p.Amount)
return printingCosts, nil
}

View File

@@ -43,7 +43,7 @@ type ItemVariant struct {
type ShopItem struct {
gorm.Model
Name string `json:"name" binding:"required" gorm:"unique;not null"`
Abstract string `json:"Abstract" binding:"required"`
Abstract string `json:"abstract" binding:"required"`
Description string `json:"description" binding:"required"`
Category Category `json:"category"`
Variants []ItemVariant `json:"variant"`
@@ -53,4 +53,5 @@ type ShopItem struct {
Image string
Pdf string
PrintMode string `json:"printMode" gorm:"default:CreateBooklet"`
WasPrinted bool `gorm:"default:false"`
}

View File

@@ -14,6 +14,11 @@ type Tag struct {
ShopItems []ShopItem `gorm:"many2many:item_tags;"`
}
type CheckedTag struct {
Tag
Checked string
}
func NewTag(ctx *gin.Context) (Tag, error) {
colors := []string{
"red",

View File

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

View File

@@ -0,0 +1,100 @@
package repositories
import (
"strconv"
"gorm.io/gorm"
"git.dynamicdiscord.de/kalipso/zineshop/models"
)
type InvoiceRepository interface {
Create(models.Invoice) (models.Invoice, error)
GetAll() ([]models.Invoice, error)
GetAllSorted(string) ([]models.Invoice, error)
GetAllNewestFirst() ([]models.Invoice, error)
GetAllNewestLast() ([]models.Invoice, error)
GetById(string) (models.Invoice, error)
//GetByInvoiceId(string) (models.Invoice, error)
Update(models.Invoice) (models.Invoice, error)
DeleteById(string) error
}
type GORMInvoiceRepository struct {
DB *gorm.DB
}
func NewGORMInvoiceRepository(db *gorm.DB) InvoiceRepository {
return &GORMInvoiceRepository{
DB: db,
}
}
func (t *GORMInvoiceRepository) Create(invoice models.Invoice) (models.Invoice, error) {
result := t.DB.Create(&invoice)
if result.Error != nil {
return models.Invoice{}, result.Error
}
return invoice, nil
}
func (t *GORMInvoiceRepository) GetAll() ([]models.Invoice, error) {
var invoice []models.Invoice
result := t.DB.Preload("PrintJobs.ShopItem").Preload("PrintJobs.Variant").Preload("PrintJobs.PaperType").Preload("PrintJobs.CoverPaperType").Preload("PrintJobs").Find(&invoice)
return invoice, result.Error
}
func (t *GORMInvoiceRepository) GetAllSorted(sortString string) ([]models.Invoice, error) {
var invoices []models.Invoice
result := t.DB.Preload("PrintJobs.ShopItem").Preload("PrintJobs.Variant").Preload("PrintJobs.PaperType").Preload("PrintJobs.CoverPaperType").Preload("PrintJobs").Order(sortString).Find(&invoices)
return invoices, result.Error
}
func (r *GORMInvoiceRepository) GetAllNewestFirst() ([]models.Invoice, error) {
return r.GetAllSorted("created_at desc")
}
func (r *GORMInvoiceRepository) GetAllNewestLast() ([]models.Invoice, error) {
return r.GetAllSorted("created_at asc")
}
func (t *GORMInvoiceRepository) GetById(id string) (models.Invoice, error) {
invoiceId, err := strconv.Atoi(id)
if err != nil {
return models.Invoice{}, err
}
var invoice models.Invoice
result := t.DB.Preload("PrintJobs").First(&invoice, uint(invoiceId))
if result.Error != nil {
return models.Invoice{}, result.Error
}
return invoice, nil
}
func (t *GORMInvoiceRepository) Update(invoice models.Invoice) (models.Invoice, error) {
result := t.DB.Save(&invoice)
if result.Error != nil {
return models.Invoice{}, result.Error
}
return invoice, nil
}
func (t *GORMInvoiceRepository) DeleteById(id string) error {
invoiceId, err := strconv.Atoi(id)
if err != nil {
return err
}
result := t.DB.Delete(&models.Invoice{}, invoiceId)
return result.Error
}

View File

@@ -0,0 +1,81 @@
package repositories
import (
"strconv"
"gorm.io/gorm"
"git.dynamicdiscord.de/kalipso/zineshop/models"
)
type ConfigRepository interface {
Create(models.Config) (models.Config, error)
GetAll() ([]models.Config, error)
GetById(string) (models.Config, error)
Update(models.Config) (models.Config, error)
DeleteById(string) error
}
type GORMConfigRepository struct {
DB *gorm.DB
}
func NewGORMConfigRepository(db *gorm.DB) ConfigRepository {
return &GORMConfigRepository{
DB: db,
}
}
func (t *GORMConfigRepository) Create(config models.Config) (models.Config, error) {
result := t.DB.Create(&config)
if result.Error != nil {
return models.Config{}, result.Error
}
return config, nil
}
func (t *GORMConfigRepository) GetAll() ([]models.Config, error) {
var configs []models.Config
result := t.DB.Find(&configs)
return configs, result.Error
}
func (t *GORMConfigRepository) GetById(id string) (models.Config, error) {
configId, err := strconv.Atoi(id)
if err != nil {
return models.Config{}, err
}
var config models.Config
result := t.DB.First(&config, uint(configId))
if result.Error != nil {
return models.Config{}, result.Error
}
return config, nil
}
func (t *GORMConfigRepository) Update(config models.Config) (models.Config, error) {
result := t.DB.Save(&config)
if result.Error != nil {
return models.Config{}, result.Error
}
return config, nil
}
func (t *GORMConfigRepository) DeleteById(id string) error {
configId, err := strconv.Atoi(id)
if err != nil {
return err
}
result := t.DB.Delete(&models.Config{}, configId)
return result.Error
}

View File

@@ -0,0 +1,82 @@
package repositories
import (
"strconv"
"gorm.io/gorm"
"git.dynamicdiscord.de/kalipso/zineshop/models"
)
type PaperRepository interface {
Create(models.Paper) (models.Paper, error)
GetAll() ([]models.Paper, error)
GetById(string) (models.Paper, error)
//GetByShopItemId(string) (models.Paper, error)
Update(models.Paper) (models.Paper, error)
DeleteById(string) error
}
type GORMPaperRepository struct {
DB *gorm.DB
}
func NewGORMPaperRepository(db *gorm.DB) PaperRepository {
return &GORMPaperRepository{
DB: db,
}
}
func (t *GORMPaperRepository) Create(tag models.Paper) (models.Paper, error) {
result := t.DB.Create(&tag)
if result.Error != nil {
return models.Paper{}, result.Error
}
return tag, nil
}
func (t *GORMPaperRepository) GetAll() ([]models.Paper, error) {
var tags []models.Paper
result := t.DB.Find(&tags)
return tags, result.Error
}
func (t *GORMPaperRepository) GetById(id string) (models.Paper, error) {
tagId, err := strconv.Atoi(id)
if err != nil {
return models.Paper{}, err
}
var tag models.Paper
result := t.DB.First(&tag, uint(tagId))
if result.Error != nil {
return models.Paper{}, result.Error
}
return tag, nil
}
func (t *GORMPaperRepository) Update(tag models.Paper) (models.Paper, error) {
result := t.DB.Save(&tag)
if result.Error != nil {
return models.Paper{}, result.Error
}
return tag, nil
}
func (t *GORMPaperRepository) DeleteById(id string) error {
tagId, err := strconv.Atoi(id)
if err != nil {
return err
}
result := t.DB.Delete(&models.Paper{}, tagId)
return result.Error
}

View File

@@ -0,0 +1,82 @@
package repositories
import (
"strconv"
"gorm.io/gorm"
"git.dynamicdiscord.de/kalipso/zineshop/models"
)
type PrintJobRepository interface {
Create(models.PrintJob) (models.PrintJob, error)
GetAll() ([]models.PrintJob, error)
GetById(string) (models.PrintJob, error)
//GetByShopItemId(string) (models.PrintJob, error)
Update(models.PrintJob) (models.PrintJob, error)
DeleteById(string) error
}
type GORMPrintJobRepository struct {
DB *gorm.DB
}
func NewGORMPrintJobRepository(db *gorm.DB) PrintJobRepository {
return &GORMPrintJobRepository{
DB: db,
}
}
func (t *GORMPrintJobRepository) Create(printJob models.PrintJob) (models.PrintJob, error) {
result := t.DB.Create(&printJob)
if result.Error != nil {
return models.PrintJob{}, result.Error
}
return printJob, nil
}
func (t *GORMPrintJobRepository) GetAll() ([]models.PrintJob, error) {
var printJobs []models.PrintJob
result := t.DB.Preload("ShopItem").Preload("Variant").Preload("PaperType").Preload("CoverPaperType").Find(&printJobs)
return printJobs, result.Error
}
func (t *GORMPrintJobRepository) GetById(id string) (models.PrintJob, error) {
printJobId, err := strconv.Atoi(id)
if err != nil {
return models.PrintJob{}, err
}
var printJob models.PrintJob
result := t.DB.Preload("ShopItem").Preload("Variant").Preload("PaperType").Preload("CoverPaperType").First(&printJob, uint(printJobId))
if result.Error != nil {
return models.PrintJob{}, result.Error
}
return printJob, nil
}
func (t *GORMPrintJobRepository) Update(printJob models.PrintJob) (models.PrintJob, error) {
result := t.DB.Save(&printJob)
if result.Error != nil {
return models.PrintJob{}, result.Error
}
return printJob, nil
}
func (t *GORMPrintJobRepository) DeleteById(id string) error {
printJobId, err := strconv.Atoi(id)
if err != nil {
return err
}
result := t.DB.Delete(&models.PrintJob{}, printJobId)
return result.Error
}

View File

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

View File

@@ -9,11 +9,16 @@ import (
)
var (
ShopItems ShopItemRepository
Users UserRepository
Tags TagRepository
CartItems CartItemRepository
Orders OrderRepository
ShopItems ShopItemRepository
Users UserRepository
Tags TagRepository
CartItems CartItemRepository
Orders OrderRepository
Tokens RegisterTokenRepository
ConfigOptions ConfigRepository
Papers PaperRepository
PrintJobs PrintJobRepository
Invoices InvoiceRepository
)
func InitRepositories() {
@@ -27,7 +32,12 @@ func InitRepositories() {
&models.User{},
&models.Tag{},
&models.CartItem{},
&models.Order{})
&models.Order{},
&models.Config{},
&models.Paper{},
&models.PrintJob{},
&models.Invoice{},
&models.RegisterToken{})
if err != nil {
panic("failed to migrate database")
@@ -38,4 +48,9 @@ func InitRepositories() {
Tags = NewGORMTagRepository(db)
CartItems = NewGORMCartItemRepository(db)
Orders = NewGORMOrderRepository(db)
Tokens = NewGORMRegisterTokenRepository(db)
ConfigOptions = NewGORMConfigRepository(db)
Papers = NewGORMPaperRepository(db)
PrintJobs = NewGORMPrintJobRepository(db)
Invoices = NewGORMInvoiceRepository(db)
}

View File

@@ -1,6 +1,7 @@
package repositories
import (
"fmt"
"gorm.io/gorm"
"strconv"
@@ -10,8 +11,15 @@ import (
type ShopItemRepository interface {
Create(models.ShopItem) (models.ShopItem, error)
GetAll() ([]models.ShopItem, error)
GetAllSorted(string) ([]models.ShopItem, error)
GetAllNewestFirst() ([]models.ShopItem, error)
GetAllNewestLast() ([]models.ShopItem, error)
GetAllLexicalFirst() ([]models.ShopItem, error)
GetAllLexicalLast() ([]models.ShopItem, error)
GetAllPublic() ([]models.ShopItem, error)
GetById(string) (models.ShopItem, error)
GetNextOfId(string) (models.ShopItem, error)
GetPreviousOfId(string) (models.ShopItem, error)
GetByTagId(string) ([]models.ShopItem, error)
GetVariantById(string) (models.ItemVariant, error)
Update(models.ShopItem) (models.ShopItem, error)
@@ -44,6 +52,29 @@ func (r *GORMShopItemRepository) GetAll() ([]models.ShopItem, error) {
return shopItems, result.Error
}
func (r *GORMShopItemRepository) GetAllSorted(sortString string) ([]models.ShopItem, error) {
var shopItems []models.ShopItem
result := r.DB.Preload("Tags").Preload("Variants").Order(sortString).Find(&shopItems)
return shopItems, result.Error
}
func (r *GORMShopItemRepository) GetAllNewestFirst() ([]models.ShopItem, error) {
return r.GetAllSorted("created_at desc")
}
func (r *GORMShopItemRepository) GetAllNewestLast() ([]models.ShopItem, error) {
return r.GetAllSorted("created_at asc")
}
func (r *GORMShopItemRepository) GetAllLexicalFirst() ([]models.ShopItem, error) {
return r.GetAllSorted("name asc")
}
func (r *GORMShopItemRepository) GetAllLexicalLast() ([]models.ShopItem, error) {
return r.GetAllSorted("name desc")
}
func (r *GORMShopItemRepository) GetAllPublic() ([]models.ShopItem, error) {
var shopItems []models.ShopItem
result := r.DB.Preload("Tags").Preload("Variants").Where("is_public = 1").Find(&shopItems)
@@ -68,6 +99,30 @@ func (r *GORMShopItemRepository) GetById(id string) (models.ShopItem, error) {
return shopItem, nil
}
func (r *GORMShopItemRepository) GetNextOfId(id string) (models.ShopItem, error) {
var nextItem models.ShopItem
if err := r.DB.Where("id > ?", id).Order("id asc").First(&nextItem).Error; err != nil {
if err != gorm.ErrRecordNotFound {
return models.ShopItem{}, err
} else {
return models.ShopItem{}, fmt.Errorf("No Item found")
}
}
return nextItem, nil
}
func (r *GORMShopItemRepository) GetPreviousOfId(id string) (models.ShopItem, error) {
var previousItem models.ShopItem
if err := r.DB.Where("id < ?", id).Order("id desc").First(&previousItem).Error; err != nil {
if err != gorm.ErrRecordNotFound {
return models.ShopItem{}, err
} else {
return models.ShopItem{}, fmt.Errorf("No Item found")
}
}
return previousItem, nil
}
func (r *GORMShopItemRepository) GetByTagId(id string) ([]models.ShopItem, error) {
tagId, err := strconv.Atoi(id)

View File

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

View File

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

View File

@@ -587,6 +587,14 @@ video {
grid-column: span 12 / span 12;
}
.col-span-3 {
grid-column: span 3 / span 3;
}
.-m-1\.5 {
margin: -0.375rem;
}
.m-2 {
margin: 0.5rem;
}
@@ -650,10 +658,6 @@ video {
margin-right: 1rem;
}
.ms-2 {
margin-inline-start: 0.5rem;
}
.mt-1 {
margin-top: 0.25rem;
}
@@ -678,10 +682,18 @@ video {
display: block;
}
.inline-block {
display: inline-block;
}
.flex {
display: flex;
}
.inline-flex {
display: inline-flex;
}
.table {
display: table;
}
@@ -758,6 +770,10 @@ video {
width: 100%;
}
.min-w-full {
min-width: 100%;
}
.max-w-2xl {
max-width: 42rem;
}
@@ -766,6 +782,10 @@ video {
max-width: 48rem;
}
.max-w-4xl {
max-width: 56rem;
}
.max-w-6xl {
max-width: 72rem;
}
@@ -810,12 +830,24 @@ video {
grid-template-columns: repeat(12, minmax(0, 1fr));
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.grid-cols-6 {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.flex-col {
flex-direction: column;
}
.items-start {
align-items: flex-start;
.flex-wrap {
flex-wrap: wrap;
}
.items-center {
@@ -830,6 +862,10 @@ video {
justify-content: space-between;
}
.gap-2 {
gap: 0.5rem;
}
.gap-4 {
gap: 1rem;
}
@@ -892,6 +928,10 @@ video {
border-color: rgb(229 231 235 / var(--tw-divide-opacity, 1));
}
.overflow-hidden {
overflow: hidden;
}
.overflow-x-auto {
overflow-x: auto;
}
@@ -900,6 +940,19 @@ video {
white-space: nowrap;
}
.break-normal {
overflow-wrap: normal;
word-break: normal;
}
.break-words {
overflow-wrap: break-word;
}
.break-all {
word-break: break-all;
}
.rounded {
border-radius: 0.25rem;
}
@@ -929,11 +982,6 @@ video {
border-bottom-left-radius: 0.375rem;
}
.rounded-l {
border-top-left-radius: 0.25rem;
border-bottom-left-radius: 0.25rem;
}
.border {
border-width: 1px;
}
@@ -979,6 +1027,11 @@ video {
background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1));
}
.bg-blue-500 {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1));
}
.bg-blue-600 {
--tw-bg-opacity: 1;
background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1));
@@ -1054,6 +1107,16 @@ video {
background-color: rgb(220 252 231 / var(--tw-bg-opacity, 1));
}
.bg-green-50 {
--tw-bg-opacity: 1;
background-color: rgb(240 253 244 / var(--tw-bg-opacity, 1));
}
.bg-green-500 {
--tw-bg-opacity: 1;
background-color: rgb(34 197 94 / var(--tw-bg-opacity, 1));
}
.bg-green-600 {
--tw-bg-opacity: 1;
background-color: rgb(22 163 74 / var(--tw-bg-opacity, 1));
@@ -1069,9 +1132,9 @@ video {
background-color: rgb(224 231 255 / var(--tw-bg-opacity, 1));
}
.bg-indigo-300 {
.bg-indigo-500 {
--tw-bg-opacity: 1;
background-color: rgb(165 180 252 / var(--tw-bg-opacity, 1));
background-color: rgb(99 102 241 / var(--tw-bg-opacity, 1));
}
.bg-indigo-600 {
@@ -1139,6 +1202,16 @@ video {
background-color: rgb(254 226 226 / var(--tw-bg-opacity, 1));
}
.bg-red-50 {
--tw-bg-opacity: 1;
background-color: rgb(254 242 242 / var(--tw-bg-opacity, 1));
}
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity, 1));
}
.bg-red-800 {
--tw-bg-opacity: 1;
background-color: rgb(153 27 27 / var(--tw-bg-opacity, 1));
@@ -1234,46 +1307,6 @@ video {
background-color: rgb(24 24 27 / var(--tw-bg-opacity, 1));
}
.bg-indigo-400 {
--tw-bg-opacity: 1;
background-color: rgb(129 140 248 / var(--tw-bg-opacity, 1));
}
.bg-indigo-500 {
--tw-bg-opacity: 1;
background-color: rgb(99 102 241 / var(--tw-bg-opacity, 1));
}
.bg-pink-200 {
--tw-bg-opacity: 1;
background-color: rgb(251 207 232 / var(--tw-bg-opacity, 1));
}
.bg-pink-300 {
--tw-bg-opacity: 1;
background-color: rgb(249 168 212 / var(--tw-bg-opacity, 1));
}
.bg-pink-700 {
--tw-bg-opacity: 1;
background-color: rgb(190 24 93 / var(--tw-bg-opacity, 1));
}
.bg-pink-500 {
--tw-bg-opacity: 1;
background-color: rgb(236 72 153 / var(--tw-bg-opacity, 1));
}
.bg-fuchsia-500 {
--tw-bg-opacity: 1;
background-color: rgb(217 70 239 / var(--tw-bg-opacity, 1));
}
.bg-blue-700 {
--tw-bg-opacity: 1;
background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1));
}
.fill-red-50 {
fill: #fef2f2;
}
@@ -1287,6 +1320,10 @@ video {
object-fit: cover;
}
.p-1\.5 {
padding: 0.375rem;
}
.p-2 {
padding: 0.5rem;
}
@@ -1299,10 +1336,6 @@ video {
padding: 1rem;
}
.p-8 {
padding: 2rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
@@ -1338,6 +1371,11 @@ video {
padding-bottom: 0.125rem;
}
.py-1 {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.py-1\.5 {
padding-top: 0.375rem;
padding-bottom: 0.375rem;
@@ -1368,6 +1406,11 @@ video {
padding-bottom: 6rem;
}
.py-3 {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
@@ -1402,10 +1445,6 @@ video {
padding-top: 1.25rem;
}
.pt-8 {
padding-top: 2rem;
}
.text-left {
text-align: left;
}
@@ -1418,6 +1457,18 @@ video {
text-align: right;
}
.text-start {
text-align: start;
}
.text-end {
text-align: end;
}
.align-middle {
vertical-align: middle;
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
@@ -1489,6 +1540,10 @@ video {
font-weight: 600;
}
.uppercase {
text-transform: uppercase;
}
.leading-10 {
line-height: 2.5rem;
}
@@ -1600,6 +1655,11 @@ video {
color: rgb(34 197 94 / var(--tw-text-opacity, 1));
}
.text-green-700 {
--tw-text-opacity: 1;
color: rgb(21 128 61 / var(--tw-text-opacity, 1));
}
.text-green-800 {
--tw-text-opacity: 1;
color: rgb(22 101 52 / var(--tw-text-opacity, 1));
@@ -1680,6 +1740,11 @@ video {
color: rgb(239 68 68 / var(--tw-text-opacity, 1));
}
.text-red-700 {
--tw-text-opacity: 1;
color: rgb(185 28 28 / var(--tw-text-opacity, 1));
}
.text-red-800 {
--tw-text-opacity: 1;
color: rgb(153 27 27 / var(--tw-text-opacity, 1));
@@ -1770,15 +1835,23 @@ video {
color: rgb(39 39 42 / var(--tw-text-opacity, 1));
}
.underline {
text-decoration-line: underline;
}
.antialiased {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.shadow-2xl {
--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-md {
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-sm {
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
@@ -1801,6 +1874,24 @@ video {
outline-color: #d1d5db;
}
.ring-1 {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.ring-inset {
--tw-ring-inset: inset;
}
.ring-green-600\/20 {
--tw-ring-color: rgb(22 163 74 / 0.2);
}
.ring-red-600\/10 {
--tw-ring-color: rgb(220 38 38 / 0.1);
}
.filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
@@ -1889,6 +1980,11 @@ video {
background-color: rgb(67 56 202 / var(--tw-bg-opacity, 1));
}
.hover\:bg-red-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(185 28 28 / var(--tw-bg-opacity, 1));
}
.hover\:bg-red-900:hover {
--tw-bg-opacity: 1;
background-color: rgb(127 29 29 / var(--tw-bg-opacity, 1));
@@ -1908,10 +2004,6 @@ video {
text-decoration-line: underline;
}
.hover\:no-underline:hover {
text-decoration-line: none;
}
.focus\:z-10:focus {
z-index: 10;
}
@@ -1948,12 +2040,6 @@ video {
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.focus\:ring-2:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.focus\:ring-4:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);
@@ -2065,10 +2151,6 @@ video {
max-width: 24rem;
}
.sm\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.sm\:items-center {
align-items: center;
}
@@ -2156,26 +2238,14 @@ video {
max-width: 80rem;
}
.lg\:max-w-4xl {
max-width: 56rem;
}
.lg\:max-w-6xl {
max-width: 72rem;
.lg\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.lg\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.lg\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.lg\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.lg\:p-8 {
padding: 2rem;
}
@@ -2196,27 +2266,18 @@ video {
}
@media (min-width: 1280px) {
.xl\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.xl\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.xl\:grid-cols-8 {
grid-template-columns: repeat(8, minmax(0, 1fr));
}
.xl\:gap-x-8 {
-moz-column-gap: 2rem;
column-gap: 2rem;
}
}
@media (min-width: 1536px) {
.\32xl\:grid-cols-8 {
grid-template-columns: repeat(8, minmax(0, 1fr));
}
.\32xl\:grid-cols-6 {
grid-template-columns: repeat(6, minmax(0, 1fr));
.\32xl\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.\32xl\:px-0 {
@@ -2235,6 +2296,11 @@ video {
border-color: rgb(31 41 55 / var(--tw-divide-opacity, 1));
}
.dark\:divide-neutral-700 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-opacity: 1;
border-color: rgb(64 64 64 / var(--tw-divide-opacity, 1));
}
.dark\:border-gray-600 {
--tw-border-opacity: 1;
border-color: rgb(75 85 99 / var(--tw-border-opacity, 1));
@@ -2290,6 +2356,16 @@ video {
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
}
.dark\:text-neutral-200 {
--tw-text-opacity: 1;
color: rgb(229 229 229 / var(--tw-text-opacity, 1));
}
.dark\:text-neutral-500 {
--tw-text-opacity: 1;
color: rgb(115 115 115 / var(--tw-text-opacity, 1));
}
.dark\:text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
@@ -2305,10 +2381,6 @@ video {
color: rgb(156 163 175 / var(--tw-placeholder-opacity, 1));
}
.dark\:ring-offset-gray-800 {
--tw-ring-offset-color: #1f2937;
}
.dark\:hover\:bg-gray-600:hover {
--tw-bg-opacity: 1;
background-color: rgb(75 85 99 / var(--tw-bg-opacity, 1));

32
utils/utils.go Normal file
View File

@@ -0,0 +1,32 @@
package utils
import (
"crypto/rand"
"encoding/hex"
"fmt"
"github.com/pdfcpu/pdfcpu/pkg/api"
)
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)
}
func CountPagesAtPath(path string) (pages int) {
ctx, err := api.ReadContextFile(path)
if err != nil {
fmt.Println("Error reading PDF:", err)
return
}
pages = ctx.PageCount
return
}

View File

@@ -131,6 +131,7 @@
<option selected value="CreateBooklet">Create Booklet</option>
<option value="LongEdge">Long Edge</option>
<option value="ShortEdge">Short Edge</option>
<option value="TriFold">Tri-Fold (Flyer)</option>
</select>

33
views/configview.html Normal file
View File

@@ -0,0 +1,33 @@
{{ 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 config options</h2>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
{{ range .data.configOptions }}
<form action="/config/{{ .ID }}" method="POST">
<div class="max-w-md mx-auto mt-4">
<div class="flex">
<input type="text" id="key" name="key" value="{{ .Key }}" class="flex-grow border border-gray-300 rounded-l-md p-2 focus:outline-none focus:ring focus:ring-blue-500">
<input type="text" id="value" name="value" value="{{ .Value }}" class="flex-grow border border-gray-300 rounded-l-md p-2 focus:outline-none focus:ring focus:ring-blue-500">
<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="/config" method="POST">
<div class="max-w-md mx-auto mt-4">
<div class="flex">
<input type="text" id="key" name="key" placeholder="" class="flex-grow border border-gray-300 rounded-l-md p-2 focus:outline-none focus:ring focus:ring-blue-500">
<input type="text" id="value" name="value" placeholder="" 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" . }}

View File

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

View File

@@ -1,13 +1,46 @@
{{ 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 Item</h2>
<a href="/shopitems/{{ .data.shopItem.ID }}">
<p class="mt-10 text-center text-sm/6 text-red-500">
{{ .data.error }}
</p>
<p class="mt-10 text-center text-sm/6 text-green-500">
{{ .data.success }}
</p>
<a href="/{{ .data.shopItem.Pdf }}" target="_blank">
<img src="/{{ .data.shopItem.Image }}" alt="Product Image" class="aspect-4/5 mx-auto rounded-md bg-gray-200 object-cover group-hover:opacity-75 lg:aspect-auto lg:h-80">
</a>
<div class="grid grid-cols-6 gap-4 mb-4">
<div class="col-span-3">
{{ if .data.previousShopItem }}
<form action="/shopitems/{{ .data.previousShopItem.ID }}/edit">
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Previous</button>
</form>
{{ end }}
</div>
<div class="col-span-3">
{{ if .data.nextShopItem }}
<form action="/shopitems/{{ .data.nextShopItem.ID }}/edit">
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6
font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2
focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Next</button>
</form>
{{ end }}
</div>
</div>
</div>
@@ -32,12 +65,25 @@
<textarea id="description" name="description" type="textarea" class="block w-full px-4 py-2 mt-2 text-gray-900 bg-white border border-gray-300 rounded-md dark:bg-gray-800 dark:text-gray-900 dark:border-gray-600 focus:border-blue-500 dark:focus:border-blue-500 focus:outline-none focus:ring">{{ .data.shopItem.Description}}</textarea>
</div>
<label class="block text-sm/6 font-medium text-gray-900">
Print Mode
</label>
<select name="print-mode" required class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg
focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option selected value="{{ .data.shopItem.PrintMode }}">{{ .data.shopItem.PrintMode }}</option>
<option value="CreateBooklet">CreateBooklet</option>
<option value="LongEdge">Long Edge</option>
<option value="ShortEdge">Short Edge</option>
<option value="TriFold">Tri-Fold (Flyer)</option>
</select>
<div class="mb-4">
<label class="block text-sm/6 text-gray-900">Select Categories</label>
<div class="space-y-2">
{{ range .data.tags }}
<label class="flex text-sm/6 items-center">
<input type="checkbox" class="form-checkbox h-4 w-4 text-gray-900" value="{{ .ID }}" name="tags[]">
<input type="checkbox" {{ .Checked }} class="form-checkbox h-4 w-4 text-gray-900" value="{{ .ID }}" name="tags[]">
<span class="ml-2 text-sm/6 text-gray-900">{{ .Name }}</span>
</label>
{{ end }}
@@ -65,7 +111,7 @@
<label for="variant-value1" class="block text-sm/6 font-medium text-gray-900">Price B/W</label>
</div>
<div class="mt-2">
<input type="number" name="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">
<input type="number" value="{{ .data.priceBW }}" name="variant-value[]" id="variant-value1" step="0.01" min="0.00" required class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
@@ -76,7 +122,7 @@
<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">
<input type="number" value="{{ .data.priceColored }}" name="variant-value[]" id="variant-value2" step="0.01" min="0.00" class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
</div>
</div>
@@ -127,24 +173,6 @@
</div>
</div>
<label class="block text-sm font-medium text-gray-900">
Print Mode
</label>
<select name="print-mode" required class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option selected value="CreateBooklet">Create Booklet</option>
<option value="LongEdge">Long Edge</option>
<option value="ShortEdge">Short Edge</option>
</select>
<p class="mt-10 text-center text-sm/6 text-red-500">
{{ .data.error }}
</p>
<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">Update Item</button>
</div>

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Zine Shop</title>
<title>Zines</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/output.css" rel="stylesheet">
@@ -25,7 +25,7 @@
{{ end }}
-->
</div>
{{ if .loggedIn }}
{{ if .isAdmin }}
<div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0 px-4 sm:px-8">
<a href="/additem" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">Add Item</a>
<a href="/batchupload" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300
@@ -33,13 +33,21 @@
<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="/config" 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">Config</a>
<a href="/paper" 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">Paper</a>
<a href="/invoice" 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">Invoices</a>
<a href="/logout" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-red-300 hover:bg-gray-700 hover:text-white">Logout</a>
</div>
{{ else }}
{{ end }}
{{ if .loggedIn }}
<div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0 px-4 sm:px-8">
<a href="/cart" class="rounded-md bg-gray-900 m-2 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">&#128722; Cart</a>
</div>

28
views/invites.html Normal file
View File

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

55
views/invoice.html Normal file
View File

@@ -0,0 +1,55 @@
<div class="-m-1.5 overflow-x-auto">
<div class="p-1.5 min-w-full inline-block align-middle">
<div class="overflow-hidden">
<table class="min-w-full divide-y divide-gray-200 dark:divide-neutral-700">
<thead>
<tr>
<th scope="col" class="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-neutral-500"></th>
<th scope="col" class="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-neutral-500">Name</th>
<th scope="col" class="px-6 py-3 text-end text-xs font-medium text-gray-500 uppercase dark:text-neutral-500">Paper</th>
<th scope="col" class="px-6 py-3 text-end text-xs font-medium text-gray-500 uppercase dark:text-neutral-500">Amount</th>
<th scope="col" class="px-6 py-3 text-end text-xs font-medium text-gray-500 uppercase dark:text-neutral-500">Price</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-neutral-700">
{{ range .PrintJobs }}
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-neutral-200">
<a href="/shopitems/{{ .Variant.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>
</td>
<td class="px-6 py-4 text-sm text-gray-800 dark:text-neutral-200">
<div class="text-sm break-normal">
<p>{{ .ShopItem.Name }}</p>
<p>{{ .Variant.Name }}</p>
</div>
</td>
<td class="px-6 py-4 text-sm text-gray-800 dark:text-neutral-200">
<div class="text-xs">
<p>{{ .PaperType.Brand }} - {{.PaperType.Name }}: {{ .PaperType.Size }} {{ .PaperType.Weight }}g</p>
{{ if .CoverPaperType }}
<p class="text-red-700">{{ .CoverPaperType.Brand }} - {{.CoverPaperType.Name }}: {{ .CoverPaperType.Size }} {{ .CoverPaperType.Weight }}g</p>
{{ end }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-neutral-200">{{ .Amount }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-neutral-200">{{ .PriceTotal }}</td>
</tr>
{{ end }}
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-neutral-200">
<b>TOTAL</b>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-neutral-200"></td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-neutral-200"></td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-neutral-200"></td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-neutral-200"><b>{{ .PriceTotal }}</b></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

29
views/invoiceview.html Normal file
View File

@@ -0,0 +1,29 @@
{{ 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">Invoices</h2>
</div>
{{ range .data.invoices }}
<div class="w-full grid grid-cols-1 gap-4 mx-auto p-4 m-4 flex flex-wrap border rounded shadow-md max-w-4xl">
<div class="">
<div class="font-bold text-center mb-4">
Invoice #{{ .ID }} - {{ .CreatedAt }}
</div>
</div>
<div class="flex flex-col">
{{ template "invoice.html" . }}
</div>
<div class="grid grid-cols-2 mt-4 flex flex-wrap gap-2 w-full">
<form action="/invoice/{{ .ID }}" method="POST">
<button type="submit" name="action" value="delete" class="bg-red-500 hover:bg-red-700 text-white text-sm/6 font-bold py-2 px-4 rounded">Delete</button>
</form>
</div>
</div>
{{ end }}
</div>
</div>
{{ template "footer.html" . }}

82
views/paperview.html Normal file
View File

@@ -0,0 +1,82 @@
{{ 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 Paper</h2>
</div>
<div class="mt-10 ">
<div class="w-full grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4">
{{ range .data.paper }}
<form action="/paper/{{ .ID }}" method="POST">
<div class="max-w-md mx-auto p-4 m-4 bg-gray-100 flex flex-wrap border rounded shadow-md">
<div class="font-bold text-center mb-4">
{{ .Brand }} - {{ .Name }}: {{ .Size }} {{ .Weight }}g/m2
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="variant-value1" class="block text-sm/6 font-medium text-gray-900">Name</label>
<input type="text" id="name" name="name" value="{{ .Name }}" class="w-full border border-gray-300 rounded-md p-2 focus:outline-none focus:ring focus:ring-blue-500">
</div>
<div>
<label for="variant-value1" class="block text-sm/6 font-medium text-gray-900">Brand</label>
<input type="text" id="brand" name="brand" value="{{ .Brand }}" class="w-full border border-gray-300 rounded-md p-2 focus:outline-none focus:ring focus:ring-blue-500">
</div>
<div>
<label for="variant-value1" class="block text-sm/6 font-medium text-gray-900">DIN Size</label>
<input type="text" id="size" name="size" value="{{ .Size }}" class="w-full border border-gray-300 rounded-md p-2 focus:outline-none focus:ring focus:ring-blue-500">
</div>
<div>
<label for="variant-value1" class="block text-sm/6 font-medium text-gray-900">Weight</label>
<input type="text" id="weight" name="weight" value="{{ .Weight }}" class="w-full border border-gray-300 rounded-md p-2 focus:outline-none focus:ring focus:ring-blue-500">
</div>
<div>
<label for="variant-value1" class="block text-sm/6 font-medium text-gray-900">Price per Sheet</label>
<input type="number" step="0.0001" min="0.0000" id="price" name="price" value="{{ .Price }}" class="w-full flex-grow border border-gray-300 rounded-md p-2 focus:outline-none focus:ring focus:ring-blue-500">
</div>
</div>
<div class="grid grid-cols-2 mt-4 flex flex-wrap gap-2 w-full">
<button type="submit" name="action" value="update" class="bg-blue-500 hover:bg-blue-700 text-white text-sm/6 font-bold py-2 px-4 rounded">Update</button>
<button type="submit" name="action" value="delete" class="bg-red-500 hover:bg-red-700 text-white text-sm/6 font-bold py-2 px-4 rounded">Delete</button>
</div>
</div>
</form>
{{ end }}
</div>
<form action="/paper" method="POST">
<div class="max-w-md mx-auto p-4 flex flex-wrap border rounded shadow-md">
<div class="font-bold text-center mb-4">
Add new Paper
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="variant-value1" class="block text-sm/6 font-medium text-gray-900">Name</label>
<input type="text" id="name" name="name" placeholder="name" class="w-full border border-gray-300 rounded-md p-2 focus:outline-none focus:ring focus:ring-blue-500">
</div>
<div>
<label for="variant-value1" class="block text-sm/6 font-medium text-gray-900">Brand</label>
<input type="text" id="brand" name="brand" placeholder="brand" class="w-full border border-gray-300 rounded-md p-2 focus:outline-none focus:ring focus:ring-blue-500">
</div>
<div>
<label for="variant-value1" class="block text-sm/6 font-medium text-gray-900">DIN Size</label>
<input type="text" id="size" name="size" placeholder="size" class="w-full border border-gray-300 rounded-md p-2 focus:outline-none focus:ring focus:ring-blue-500">
</div>
<div>
<label for="variant-value1" class="block text-sm/6 font-medium text-gray-900">Weight</label>
<input type="text" id="weight" name="weight" placeholder="Weight" class="w-full border border-gray-300 rounded-md p-2 focus:outline-none focus:ring focus:ring-blue-500">
</div>
<div>
<label for="variant-value1" class="block text-sm/6 font-medium text-gray-900">Price per Sheet</label>
<input type="number" step="0.0001" min="0.0000" id="price" name="price" placeholder="price per sheet"
class="w-full flex-grow border border-gray-300 rounded-md p-2 focus:outline-none focus:ring focus:ring-blue-500">
</div>
</div>
<div class="grid grid-cols-4 mt-4 flex flex-wrap gap-2 w-full">
<button type="submit" class="bg-green-500 hover:bg-green-700 text-white text-sm/6 font-bold py-2 px-4 rounded">Add</button>
</div>
</div>
</form>
</div>
{{ template "footer.html" . }}

22
views/printstarted.html Normal file
View File

@@ -0,0 +1,22 @@
{{ 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">Print Started
</h2>
<div class="w-full grid grid-cols-1 gap-4 mx-auto p-4 m-4 flex flex-wrap border rounded shadow-md">
<div class="flex flex-col">
{{ template "invoice.html" .data.invoice }}
</div>
<div class="grid grid-cols-2 mt-4 flex flex-wrap gap-2 w-full">
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{{ template "footer.html" . }}

View File

@@ -1,7 +1,7 @@
{{ 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="mx-auto max-w-4xl">
<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
@@ -15,7 +15,7 @@
<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>
Make 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.
@@ -33,14 +33,30 @@
<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>
{{ if .ShopItem.WasPrinted }}
<span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-green-600/20 ring-inset">Tested</span>
{{ else }}
<span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-red-600/10 ring-inset">NOT TESTED</span>
{{ end }}
<a href="/shopitems/{{ .ItemVariant.ShopItemID }}" class="hover:underline">{{ .ShopItem.Name }} - {{ .ItemVariant.Name }}</a>
</div>
</td>
<td class="whitespace-nowrap py-4">
<select name="variant-papertype[]" 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">
{{ range $.data.paper }}
<option value="{{ .ID }}">{{ .Brand }} - {{ .Name }}: {{ .Size }} {{ .Weight }}g/m2</option>
{{ end}}
</select>
</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>
<option selected value="0">CoverPage: None</option>
{{ range $.data.paper }}
<option value="{{ .ID }}">{{ .Brand }} - {{ .Name }}: {{ .Size }} {{ .Weight }}g/m2</option>
{{ end}}
</select>
</td>
<td class="whitespace-nowrap py-4">

59
views/registertoken.html Normal file
View File

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

View File

@@ -9,15 +9,18 @@
<img class="w-full h-full object-cover" src="/{{ .data.shopItem.Image}}" alt="Product Image">
</div>
<div class="flex -mx-2 mb-4">
{{ if .loggedIn }}
<input type="hidden" id="{{ .data.shopItem.ID}}" name="ShopItemId" value="{{ .data.shopItem.ID }}">
<div class="w-1/3 px-2">
<button type="submit" class="w-full bg-gray-900 dark:bg-gray-600 text-white py-2 px-4 rounded-lg font-bold hover:bg-gray-800 dark:hover:bg-gray-700">Add to Cart</button>
</div>
{{ end }}
<div class="w-1/3 px-2">
<button type="button" class="w-full bg-blue-900 dark:bg-gray-600 text-white py-2 px-4 rounded-lg font-bold hover:bg-gray-800 dark:hover:bg-gray-700"><a href="/{{ .data.shopItem.Pdf }}">View</a></button>
</div>
{{ if .loggedIn }}
{{ 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>
@@ -34,6 +37,7 @@
{{ .data.shopItem.Abstract }}
</p>
{{ if .loggedIn }}
<div class="flex mb-4">
<label for="ItemVariantId" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"></label>
<select name="ItemVariantId" id="ItemVariantId" required class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
@@ -43,6 +47,7 @@
{{ end }}
</select>
</div>
{{ end }}
<div class="flex mb-4">
<div class="mr-4">

View File

@@ -1,14 +1,14 @@
<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>
<h2 class="text-2xl font-bold tracking-tight text-gray-900">Available Zines</h2>
<input type="text" id="myInput" onkeyup="myFunction()" placeholder="Search for names..">
<input type="text" id="myInput" onkeyup="myFunction()" placeholder="Search for names..">
<div class="mt-6 grid grid-cols-1 gap-x-6 gap-y-10 md:grid-cols-2 xl:grid-cols-4">
{{ range .data.shopItems }}
<div class="myClass group relative">
<a href="/shopitems/{{ .ID }}">
<img src="/{{ .Image }}" alt="Product Image" class="aspect-4/5 mx-auto rounded bg-gray-200 object-cover group-hover:opacity-75 lg:aspect-auto lg:h-80">
<img loading="lazy" src="/{{ .Image }}" alt="Product Image" class="shadow-2xl aspect-4/5 mx-auto rounded bg-gray-200 object-cover group-hover:opacity-75 lg:aspect-auto lg:h-80">
</a>
<div class="mt-4 flex justify-between">
<div>
@@ -25,7 +25,9 @@
{{ end }}
</p>
</div>
<p class="text-sm font-medium text-gray-900">{{ .BasePrice }}€</p>
{{ if $.loggedIn }}
<p class="text-sm font-medium text-gray-900">{{ .BasePrice }}€</p>
{{ end }}
</div>
</div>