16 Commits

Author SHA1 Message Date
977926a059 trifold add right binding
All checks were successful
Go / build (push) Successful in 12m41s
not sure if this is correct for ever flyer
2025-04-22 17:17:55 +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
11 changed files with 258 additions and 61 deletions

View File

@@ -68,6 +68,9 @@ func (rc *shopItemController) GetById(c *gin.Context) {
ReplyOK(c, shopItem)
}
// this currently creates quite big preview images
// workaround is running the following command in the uploads folder:
// for file in *.png; do convert "$file" -resize 35% "$file"; done
func (rc *shopItemController) NewShopItemFromForm(ctx *gin.Context) (models.ShopItem, error) {
defaultImagePath := "static/img/zine.jpg"
name := ctx.PostForm("name")
@@ -106,6 +109,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)
@@ -249,14 +260,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 +277,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 +374,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)
@@ -414,23 +430,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)
}
@@ -471,30 +557,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)
}

View File

@@ -188,6 +188,7 @@ func (rc *UserController) RegisterHandler(c *gin.Context) {
}
c.HTML(http.StatusOK, "register.html", data)
return
}
tokenExists, err := repositories.Tokens.Exists(token)
@@ -332,8 +333,20 @@ func (rc *UserController) InviteHandler(c *gin.Context) {
}
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",

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";

View File

@@ -15,6 +15,7 @@ const (
LongEdge PrintOption = ""
ShortEdge PrintOption = "-o Binding=TopBinding"
CreateBooklet PrintOption = "-o Combination=Booklet -o PageSize=A5"
TriFold PrintOption = "-o Fold=TriFold -o Binding=RightBinding"
)
type PrintJob struct {
@@ -32,6 +33,10 @@ func GetPrintMode(mode string) PrintOption {
return ShortEdge
}
if mode == "TriFold" {
return TriFold
}
return CreateBooklet
}

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

@@ -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

@@ -587,6 +587,10 @@ video {
grid-column: span 12 / span 12;
}
.col-span-3 {
grid-column: span 3 / span 3;
}
.m-2 {
margin: 0.5rem;
}
@@ -806,6 +810,10 @@ video {
grid-template-columns: repeat(12, minmax(0, 1fr));
}
.grid-cols-6 {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.flex-col {
flex-direction: column;
}

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>

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

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="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>