13 Commits

Author SHA1 Message Date
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
20 changed files with 1199 additions and 148 deletions

View File

@@ -0,0 +1,318 @@
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)
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) 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) {
@@ -113,9 +128,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,7 +151,16 @@ 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()
printJob, err = repositories.PrintJobs.Create(printJob)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"data": gin.H{"error": err}})
@@ -134,6 +170,18 @@ func (rc *printController) PrintHandler(c *gin.Context) {
printJobs = append(printJobs, printJob)
}
invoice := models.Invoice{
PrintJobs: printJobs,
PricePerClick: 0.002604,
PartCosts: 0.0067,
}
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()

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{}
@@ -621,115 +616,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()})
}

17
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{}
)
@@ -67,10 +68,20 @@ func main() {
viewRoutes.GET("/cart/print", authValidator.RequireAdmin, printController.PrintCartView)
viewRoutes.POST("/print", authValidator.RequireAdmin, printController.PrintHandler)
viewRoutes.GET("/tags", authValidator.RequireAdmin, shopItemController.TagView)
viewRoutes.POST("/tags/:id", authValidator.RequireAdmin, shopItemController.TagHandler)
viewRoutes.GET("/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.RequireAdmin, shopItemController.AddTagHandler)
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("/cart", authValidator.RequireAuth, cartItemController.CartItemView)
viewRoutes.POST("/cart", authValidator.RequireAuth, cartItemController.AddItemHandler)
viewRoutes.POST("/cart/delete", authValidator.RequireAuth, cartItemController.DeleteItemHandler)

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,15 +18,36 @@ const (
LongEdge PrintOption = ""
ShortEdge PrintOption = "-o Binding=TopBinding"
CreateBooklet PrintOption = "-o Combination=Booklet -o PageSize=A5"
TriFold PrintOption = "-o Fold=TriFold"
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
}
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
InvoiceID uint
}
func GetPrintMode(mode string) PrintOption {
if mode == "LongEdge" {
return LongEdge
@@ -40,7 +64,7 @@ func GetPrintMode(mode string) PrintOption {
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")
}
@@ -49,32 +73,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)
@@ -91,3 +127,34 @@ 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)
printMode := GetPrintMode(p.ShopItem.PrintMode)
//Get actual pagecount depending on printmode
actualPageCount := pageCount
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) * 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))
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"`

View File

@@ -0,0 +1,82 @@
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)
GetById(string) (models.Invoice, error)
//GetByShopItemId(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.Omit("PrintJobs").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").Find(&invoice)
return invoice, result.Error
}
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

@@ -9,12 +9,16 @@ import (
)
var (
ShopItems ShopItemRepository
Users UserRepository
Tags TagRepository
CartItems CartItemRepository
Orders OrderRepository
Tokens RegisterTokenRepository
ShopItems ShopItemRepository
Users UserRepository
Tags TagRepository
CartItems CartItemRepository
Orders OrderRepository
Tokens RegisterTokenRepository
ConfigOptions ConfigRepository
Papers PaperRepository
PrintJobs PrintJobRepository
Invoices InvoiceRepository
)
func InitRepositories() {
@@ -29,6 +33,10 @@ func InitRepositories() {
&models.Tag{},
&models.CartItem{},
&models.Order{},
&models.Config{},
&models.Paper{},
&models.PrintJob{},
&models.Invoice{},
&models.RegisterToken{})
if err != nil {
@@ -41,4 +49,8 @@ func InitRepositories() {
CartItems = NewGORMCartItemRepository(db)
Orders = NewGORMOrderRepository(db)
Tokens = NewGORMRegisterTokenRepository(db)
ConfigOptions = NewGORMConfigRepository(db)
Papers = NewGORMPaperRepository(db)
PrintJobs = NewGORMPrintJobRepository(db)
Invoices = NewGORMInvoiceRepository(db)
}

View File

@@ -674,6 +674,10 @@ video {
margin-top: 1.5rem;
}
.mb-3 {
margin-bottom: 0.75rem;
}
.block {
display: block;
}
@@ -810,14 +814,34 @@ 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));
}
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.flex-col {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.content-stretch {
align-content: stretch;
}
.items-center {
align-items: center;
}
@@ -830,6 +854,10 @@ video {
justify-content: space-between;
}
.gap-2 {
gap: 0.5rem;
}
.gap-4 {
gap: 1rem;
}
@@ -974,6 +1002,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));
@@ -1049,6 +1082,11 @@ video {
background-color: rgb(220 252 231 / 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));
@@ -1134,6 +1172,11 @@ video {
background-color: rgb(254 226 226 / 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));
@@ -1229,6 +1272,16 @@ video {
background-color: rgb(24 24 27 / var(--tw-bg-opacity, 1));
}
.bg-gray-700 {
--tw-bg-opacity: 1;
background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1));
}
.bg-gray-500 {
--tw-bg-opacity: 1;
background-color: rgb(107 114 128 / var(--tw-bg-opacity, 1));
}
.fill-red-50 {
fill: #fef2f2;
}
@@ -1329,6 +1382,16 @@ video {
padding-bottom: 2rem;
}
.py-20 {
padding-top: 5rem;
padding-bottom: 5rem;
}
.px-8 {
padding-left: 2rem;
padding-right: 2rem;
}
.pb-3 {
padding-bottom: 0.75rem;
}
@@ -1353,6 +1416,10 @@ video {
padding-top: 1.25rem;
}
.pb-4 {
padding-bottom: 1rem;
}
.text-left {
text-align: left;
}
@@ -1717,17 +1784,39 @@ 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-sm {
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-xl {
--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-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);
}
.outline {
outline-style: solid;
}
@@ -1832,6 +1921,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));
@@ -1998,6 +2092,10 @@ video {
max-width: 24rem;
}
.sm\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.sm\:items-center {
align-items: center;
}
@@ -2041,6 +2139,10 @@ video {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.md\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.md\:flex-row {
flex-direction: row;
}
@@ -2089,6 +2191,14 @@ video {
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;
}
@@ -2112,9 +2222,17 @@ video {
.xl\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.xl\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (min-width: 1536px) {
.\32xl\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.\32xl\:px-0 {
padding-left: 0px;
padding-right: 0px;

View File

@@ -1,8 +1,11 @@
package utils
import (
"bufio"
"crypto/rand"
"encoding/hex"
"io"
"os"
)
func GenerateSessionId(length int) string {
@@ -17,3 +20,41 @@ func GenerateSessionId(length int) string {
func GenerateToken() string {
return GenerateSessionId(16)
}
const match = "/Page\x00"
// Pages reads the given io.ByteReader until EOF is reached, returning the
// number of pages encountered.
func Pages(reader io.ByteReader) (pages int) {
i := 0
for {
b, err := reader.ReadByte()
if err != nil {
return
}
check:
switch match[i] {
case 0:
if !(b >= 'A' && b <= 'Z' || b >= 'a' && b <= 'z') {
pages++
}
i = 0
goto check
case b:
i++
default:
i = 0
}
}
}
// PagesAtPath opens a PDF file at the given file path, returning the number
// of pages found.
func CountPagesAtPath(path string) (pages int) {
if reader, err := os.Open(path); err == nil {
reader.Chdir()
pages = Pages(bufio.NewReader(reader))
reader.Close()
}
return
}

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

@@ -38,6 +38,10 @@
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="/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>
{{ end }}

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

View File

@@ -37,10 +37,20 @@
</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">None</option>
{{ range $.data.paper }}
<option value="{{ .ID }}">{{ .Brand }} - {{ .Name }}: {{ .Size }} {{ .Weight }}g/m2</option>
{{ end}}
</select>
</td>
<td class="whitespace-nowrap py-4">

View File

@@ -8,7 +8,7 @@
<div class="myClass group relative">
<a href="/shopitems/{{ .ID }}">
<img loading="lazy" 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>