Moving fast and breaking things
This is a first commit in preparation for 2.0 - now using prototempaltes™ - updated README - makefile for some nice shortcuts - add new tikettes - remove tikettes Things that don't work (yet) - authentication on the main frontend (there is none) - generating PDFs
@ -3,13 +3,13 @@ FROM alpine
|
||||
RUN apk --no-cache add python3 inkscape bash imagemagick ghostscript ttf-opensans
|
||||
RUN apk --no-cache add py3-pip && pip3 install --break-system-packages django tzdata gunicorn
|
||||
|
||||
ADD backend /root/zetikettes
|
||||
ADD backend /zetikettes
|
||||
|
||||
RUN mkdir -p /usr/share/fonts/TTF \
|
||||
&& cp /root/zetikettes/fonts/*.ttf /usr/share/fonts/TTF/ \
|
||||
&& cp /zetikettes/fonts/*.ttf /usr/share/fonts/TTF/ \
|
||||
&& fc-cache -fv
|
||||
|
||||
|
||||
# the script will look for templates in /data
|
||||
WORKDIR /root/zetikettes/zetikettes
|
||||
WORKDIR /zetikettes/zetikettes
|
||||
CMD /usr/bin/gunicorn zetikettes.wsgi -b 0.0.0.0:8000 --timeout 600 --forwarded-allow-ips="*"
|
||||
|
48
Makefile
Normal file
@ -0,0 +1,48 @@
|
||||
LIBPREFIX ?= /var/lib/zetikettes
|
||||
image-name = pol/zetikettes
|
||||
uuid = $(shell id -u):$(shell id -g)
|
||||
|
||||
.PHONY: image
|
||||
image: ## build the docker images
|
||||
docker compose build
|
||||
|
||||
.PHONY: initial-db
|
||||
initial-db: image ## create and populate a database
|
||||
mkdir -p $(LIBPREFIX)/data
|
||||
docker run --rm \
|
||||
--user $(uuid) \
|
||||
-v $(LIBPREFIX)/data:/data \
|
||||
$(image-name) \
|
||||
/zetikettes/zetikettes/manage.py migrate
|
||||
docker run --rm \
|
||||
--user $(uuid) \
|
||||
-v $(LIBPREFIX)/data:/data \
|
||||
$(image-name) \
|
||||
/zetikettes/zetikettes/manage.py loaddata initial_db
|
||||
cp templates/*.svg $(LIBPREFIX)/data/
|
||||
|
||||
.PHONY: superuser
|
||||
superuser: image ## create a superuser in the django admin
|
||||
docker run --rm \
|
||||
--user $(uuid) \
|
||||
-v $(LIBPREFIX)/data:/data \
|
||||
-it \
|
||||
$(image-name) \
|
||||
/zetikettes/zetikettes/manage.py createsuperuser
|
||||
|
||||
.PHONY: staticfiles
|
||||
staticfiles: image ## install all static files
|
||||
cp -r frontend $(LIBPREFIX)/
|
||||
docker run --rm \
|
||||
--user $(uuid) \
|
||||
-v $(LIBPREFIX):/libdir \
|
||||
-w /libdir \
|
||||
$(image-name) \
|
||||
/zetikettes/zetikettes/manage.py collectstatic
|
||||
|
||||
|
||||
.PHONY: help
|
||||
help: ## Show this help
|
||||
@echo Noteworthy targets:
|
||||
@egrep '^[a-zA-Z_-]+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
.DEFAULT_GOAL := help
|
93
README.md
@ -1,64 +1,53 @@
|
||||
zetikettes
|
||||
==========
|
||||
zetikettes 2.0
|
||||
==============
|
||||
|
||||
ouaich.
|
||||
ouaich. tavu.
|
||||
|
||||
**NOTE**: release 2.0 broke compatibility with previous "releases". Maxime has all the recent data to repopulate a new database.
|
||||
|
||||
|
||||
Initial setup
|
||||
-------------
|
||||
|
||||
### Database setup and population
|
||||
|
||||
Populate an empty database (if none already exists):
|
||||
|
||||
```
|
||||
sudo make initial-db
|
||||
```
|
||||
|
||||
This will setup an initial database under `/var/lib/zetikettes/data`
|
||||
|
||||
Optionally, you may need credentials to access the admin page:
|
||||
|
||||
```
|
||||
make superuser
|
||||
```
|
||||
|
||||
### System service
|
||||
|
||||
```
|
||||
docker build -t zetikettes .
|
||||
sudo mkdir -p /etc/docker/compose/zetikettes
|
||||
sudo cp docker-compose.yml /etc/docker/compose/zetikettes/
|
||||
sudo cp compose.yml /etc/docker/compose/zetikettes/
|
||||
sudo systemctl enable --now docker-compose@zetikettes
|
||||
```
|
||||
|
||||
If not already present in `/etc/systemd/system/`, the `docker-compose@` service file is provided.
|
||||
|
||||
### www static files
|
||||
|
||||
```
|
||||
make staticfiles
|
||||
```
|
||||
|
||||
This will install frontend and django admin static files under `/var/lib/zetikettes`
|
||||
|
||||
### Nginx
|
||||
|
||||
Example configuration is provided in `nginx_locations`.
|
||||
|
||||
Nginx is configured to:
|
||||
- redirect /zetikettes/srv/ to localhost:8000
|
||||
- redirect /zetikettes/ to /var/lib/zetikettes/static
|
||||
|
||||
Conf is in /etc/nginx/sites-available/default
|
||||
|
||||
Test
|
||||
----
|
||||
|
||||
```
|
||||
docker run --rm -it -v $PWD/templates:/data zetikettes /bin/bash /root/zetikettes/old/mkjam.sh
|
||||
```
|
||||
|
||||
This should produce a .pdf in `templates/`. Open it to check that
|
||||
layout & fonts are correct.
|
||||
|
||||
Run
|
||||
---
|
||||
|
||||
```
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
Notes for deploying
|
||||
-------------------
|
||||
|
||||
.h3 Initialize empty database
|
||||
```
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
.h3 Prepare static files
|
||||
```
|
||||
python manage.py collectstatic
|
||||
```
|
||||
The files will be in `www_static/` and need to be moved to `/var/lib/zetikettes/www_static`
|
||||
|
||||
.h3 Change host settings
|
||||
If not deploying on `aerith.ponteilla.net`, you'll need to edit `backend/zetikettes/zetikettes/settings.py` to change a couple things in there.
|
||||
|
||||
|
||||
Change available templates
|
||||
--------------------------
|
||||
|
||||
1. go to /zetikettes/admin
|
||||
1. add the newtikette
|
||||
1. still no need to restart the container (magic!)
|
||||
2. profit.
|
||||
- redirect `/zetikettes/srv/` to `localhost:8000`
|
||||
- redirect `/zetikettes/` to `/var/lib/zetikettes/frontend`
|
||||
- redirect `/zetikettes/srv/static` to `/var/lib/zetikettes/www_static`
|
||||
|
152
backend/zetikettes/initial_db.json
Normal file
@ -0,0 +1,152 @@
|
||||
[
|
||||
{
|
||||
"model": "tikette.tisub",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "dluo",
|
||||
"descritpion": "DLUO",
|
||||
"default": "décembre 1992",
|
||||
"type": "YM"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tisub",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "lot",
|
||||
"descritpion": "№ de lot",
|
||||
"default": "1234-5",
|
||||
"type": "ST"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tisub",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "qty",
|
||||
"descritpion": "Poids net (g)",
|
||||
"default": "370",
|
||||
"type": "ST"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tisub",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "teneur",
|
||||
"descritpion": "Teneur en fruits (%)",
|
||||
"default": "50",
|
||||
"type": "ST"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tisub",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "fruit",
|
||||
"descritpion": "Quantité de fruits pour 100g (g)",
|
||||
"default": "60",
|
||||
"type": "ST"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tikategory",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Pesto",
|
||||
"landscape": true,
|
||||
"prototempalte": "Pesto.svg",
|
||||
"subs": [
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tikategory",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Confiture",
|
||||
"landscape": true,
|
||||
"prototempalte": "Confiture.svg",
|
||||
"subs": [
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tikategory",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "Chocolat",
|
||||
"landscape": false,
|
||||
"prototempalte": "Chocolat.svg",
|
||||
"subs": [
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tikategory",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "Aromate",
|
||||
"landscape": false,
|
||||
"prototempalte": "Aromate.svg",
|
||||
"subs": [
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tikategory",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "Sel",
|
||||
"landscape": true,
|
||||
"prototempalte": "Sel.svg",
|
||||
"subs": [
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tikategory",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "Sirop",
|
||||
"landscape": false,
|
||||
"prototempalte": "Sirop.svg",
|
||||
"subs": [
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tikategory",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"name": "Tisane",
|
||||
"landscape": false,
|
||||
"prototempalte": "Tisane.svg",
|
||||
"subs": [
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
@ -1,8 +1,9 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Tikette, Tikategory, Tisub
|
||||
from .models import Tikette, Tizer, Tikategory, Tisub
|
||||
|
||||
admin.site.register(Tikette)
|
||||
admin.site.register(Tizer)
|
||||
admin.site.register(Tikategory)
|
||||
admin.site.register(Tisub)
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Generated by Django 4.2.2 on 2023-07-03 14:37
|
||||
# Generated by Django 5.2.4 on 2025-08-05 12:57
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -18,6 +18,7 @@ class Migration(migrations.Migration):
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('landscape', models.BooleanField()),
|
||||
('prototempalte', models.FileField(upload_to='')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'tikategoriez',
|
||||
@ -30,22 +31,41 @@ class Migration(migrations.Migration):
|
||||
('name', models.CharField(max_length=50)),
|
||||
('descritpion', models.TextField()),
|
||||
('default', models.TextField()),
|
||||
('type', models.CharField(choices=[('ST', 'Short Text'), ('LT', 'Long Text'), ('C', 'Color'), ('YM', 'Year Month'), ('B', 'Boolean')], default='ST', max_length=2)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'tisubz',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Tizer',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('email', models.CharField()),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'tizerz',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Tikette',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=100)),
|
||||
('svg', models.FileField(upload_to='')),
|
||||
('designation', models.CharField(max_length=100)),
|
||||
('ingredients', models.TextField()),
|
||||
('description', models.TextField()),
|
||||
('color', models.CharField(max_length=6)),
|
||||
('ab', models.CharField(choices=[('inline', 'Visible'), ('none', 'Invisible')], max_length=7)),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tikette.tikategory')),
|
||||
('subs', models.ManyToManyField(to='tikette.tisub')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'tikettz',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tikategory',
|
||||
name='subs',
|
||||
field=models.ManyToManyField(to='tikette.tisub'),
|
||||
),
|
||||
]
|
||||
|
@ -1,22 +0,0 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-04 14:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tikette', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='tikette',
|
||||
name='subs',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tikategory',
|
||||
name='subs',
|
||||
field=models.ManyToManyField(to='tikette.tisub'),
|
||||
),
|
||||
]
|
@ -1,9 +1,18 @@
|
||||
from django.db import models
|
||||
|
||||
class Tisub(models.Model):
|
||||
class Type(models.TextChoices):
|
||||
SHORT_TEXT = "ST"
|
||||
LONG_TEXT = "LT"
|
||||
COLOR = "C"
|
||||
YEAR_MONTH = "YM"
|
||||
BOOLEAN = "B"
|
||||
|
||||
name = models.CharField(max_length=50)
|
||||
descritpion = models.TextField()
|
||||
default = models.TextField()
|
||||
# everything is text. type is for UX (e.g. color picker)
|
||||
type = models.CharField(max_length=2, choices=Type, default=Type.SHORT_TEXT)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@ -16,6 +25,10 @@ class Tikategory(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
landscape = models.BooleanField()
|
||||
subs = models.ManyToManyField(Tisub)
|
||||
# For now we'll hardcode the following:xi
|
||||
# [designation, ingredients, description, color, AB, designation_fontsize]
|
||||
#protosubs = models.ManyToManyField(Tisub, related_name="protosubs")
|
||||
prototempalte = models.FileField()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@ -25,9 +38,19 @@ class Tikategory(models.Model):
|
||||
|
||||
|
||||
class Tikette(models.Model):
|
||||
class AbVisibility(models.TextChoices):
|
||||
VISIBLE = "inline"
|
||||
INVISIBLE = "none"
|
||||
|
||||
title = models.CharField(max_length=100)
|
||||
category = models.ForeignKey(Tikategory, on_delete=models.CASCADE)
|
||||
svg = models.FileField()
|
||||
|
||||
designation = models.CharField(max_length=100)
|
||||
ingredients = models.TextField()
|
||||
description = models.TextField()
|
||||
color = models.CharField(max_length=6)
|
||||
ab = models.CharField(max_length=7, choices=AbVisibility)
|
||||
# designation_fontsize is hardcoded to 42.6667
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
@ -35,4 +58,12 @@ class Tikette(models.Model):
|
||||
class Meta:
|
||||
verbose_name_plural = "tikettz"
|
||||
|
||||
# Create your models here.
|
||||
|
||||
class Tizer(models.Model):
|
||||
email = models.CharField()
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "tizerz"
|
||||
|
@ -10,16 +10,26 @@ from .models import Tikette, Tikategory
|
||||
|
||||
CORS={'access-control-allow-origin': '*'}
|
||||
|
||||
def index(request):
|
||||
|
||||
def get_list(request):
|
||||
tikettes = [{
|
||||
'id': x.id,
|
||||
'title': x.title,
|
||||
'category': x.category.name,
|
||||
'sticker': x.svg.name,
|
||||
'landscape': x.category.landscape,
|
||||
'subs': {x.name: x.default for x in x.category.subs.all()},
|
||||
} for x in Tikette.objects.all()]
|
||||
return JsonResponse({'status': 'ok', 'tikettes': tikettes}, headers=CORS)
|
||||
|
||||
|
||||
def get_categories(request):
|
||||
tikats = [{
|
||||
'id': x.id,
|
||||
'name': x.name,
|
||||
} for x in Tikategory.objects.all()]
|
||||
return JsonResponse({'status': 'ok', 'tikats': tikats}, headers=CORS)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def generate(request):
|
||||
if request.method != "POST":
|
||||
@ -29,3 +39,26 @@ def generate(request):
|
||||
pdfpath = stickersheet.generate(payload, out_dir=settings.TIKETTE_OUT_DIR)
|
||||
|
||||
return JsonResponse({'status': 'ok', 'file': pdfpath}, headers=CORS)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def newtikette(request):
|
||||
if request.method != "POST":
|
||||
return JsonResponse({'status': 'notok', 'message': 'this isn\'t the way'})
|
||||
|
||||
payload = json.loads(request.body)
|
||||
tikette = Tikette(**payload)
|
||||
tikette.save()
|
||||
|
||||
return JsonResponse({'status': 'ok'}, headers=CORS)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def deletetikette(request):
|
||||
if request.method != "POST":
|
||||
return JsonResponse({'status': 'notok', 'message': 'this isn\'t the way'})
|
||||
|
||||
payload = json.loads(request.body)
|
||||
Tikette.objects.get(id=payload["id"]).delete()
|
||||
|
||||
return JsonResponse({'status': 'ok'}, headers=CORS)
|
||||
|
@ -24,6 +24,9 @@ import tikette.views
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('list', tikette.views.index),
|
||||
path('', tikette.views.generate),
|
||||
path('list', tikette.views.get_list),
|
||||
path('categories', tikette.views.get_categories),
|
||||
path('generate', tikette.views.generate),
|
||||
path('newtikette', tikette.views.newtikette),
|
||||
path('deletetikette', tikette.views.deletetikette),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
@ -3,7 +3,8 @@ version: '3.1'
|
||||
services:
|
||||
|
||||
zetikettes:
|
||||
image: zetikettes
|
||||
build: .
|
||||
image: pol/zetikettes
|
||||
restart: always
|
||||
ports:
|
||||
- 127.0.0.1:8000:8000
|
14
docker-compose@.service
Normal file
@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=%i service with docker compose
|
||||
PartOf=docker.service
|
||||
After=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=true
|
||||
WorkingDirectory=/etc/docker/compose/%i
|
||||
ExecStart=/usr/bin/docker-compose up -d --remove-orphans
|
||||
ExecStop=/usr/bin/docker-compose down
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -1 +1 @@
|
||||
const backend_api = 'http://jenova.ponteilla.net:8001/'
|
||||
const backend_api = 'http://localhost:8000/'
|
||||
|
@ -3,15 +3,19 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>zetikettes 0.2</title>
|
||||
<title>Zětikwett's</title>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<!--Import Google Icon Font-->
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<!-- Compiled and minified CSS -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
|
||||
<link rel="stylesheet" href="placeholder.css">
|
||||
|
||||
<!-- Compiled and minified JavaScript -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
|
||||
|
||||
<script src="jscolor.min.js"></script>
|
||||
<script src="config.js"></script>
|
||||
<script src="zetikettes.js"></script>
|
||||
|
||||
@ -28,15 +32,70 @@ main {
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="container center orange-text">
|
||||
<h1>Zétikwett's</h1>
|
||||
</header>
|
||||
<nav class="light-blue">
|
||||
<div class="nav-wrapper container">
|
||||
<a href="#" class="brand-logo">Zětikwett's</a>
|
||||
<ul id="nav-mobile" class="right hide-on-med-and-down">
|
||||
<li>
|
||||
<a class="modal-trigger btn orange" href="#newproduct">
|
||||
<i class="material-icons left">add</i>Nouveau produit
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="container row">
|
||||
<p></p>
|
||||
<div class="col m6 offset-m3 s12">
|
||||
<ul class="collapsible" id="appbody"></ul>
|
||||
<a class="modal-trigger btn orange hide-on-large-only" href="#newproduct"><i class="material-icons left">add</i>Nouveau produit</a>
|
||||
<ul class="collapsible" id="appbody">
|
||||
<div class="shimmer">
|
||||
<li><div class="collapsible-header"><h6 class="faux-text short"></h6></div></li>
|
||||
<li><div class="collapsible-header"><h6 class="faux-text"></h6></div></li>
|
||||
<li><div class="collapsible-header"><h6 class="faux-text shorter"></h6></div></li>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Modal Structure -->
|
||||
<div id="newproduct" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="input-field">
|
||||
<input id="new-name" type="text">
|
||||
<label>Nom interne du produit</label>
|
||||
</div>
|
||||
<div class="input-field">
|
||||
<select id="new-type">
|
||||
</select>
|
||||
<label>Type de produit</label>
|
||||
</div>
|
||||
<div class="input-field">
|
||||
<input id="new-designation" type="text">
|
||||
<label>Désignation</label>
|
||||
</div>
|
||||
<div class="input-field">
|
||||
<textarea id="new-ingredients" class="materialize-textarea"></textarea>
|
||||
<label>Ingrédients</label>
|
||||
</div>
|
||||
<div class="input-field">
|
||||
<textarea id="new-description" class="materialize-textarea"></textarea>
|
||||
<label>Description</label>
|
||||
</div>
|
||||
<div class="input-field">
|
||||
<input value="#97A1CC" data-jscolor="{previewSize: 0}" id="new-color" type="text">
|
||||
<label>Couleur</label>
|
||||
</div>
|
||||
<label>
|
||||
<input type="checkbox" id="new-organic">
|
||||
<span>Bio</span>
|
||||
</label>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="modal-close waves-effect waves-green btn" id="new-add">Ajouter</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="page-footer">
|
||||
<footer class="page-footer orange">
|
||||
<div class="container row">
|
||||
<span class="col right">displayed with recycled electrons.</span>
|
||||
</div>
|
||||
|
1
frontend/jscolor.min.js
vendored
Normal file
47
frontend/placeholder.css
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Read the blog post here:
|
||||
* https://letsbuildui.dev/articles/how-to-build-a-skeleton-loading-placeholder
|
||||
*/
|
||||
.faux-text {
|
||||
background: #dddddd;
|
||||
border-radius: 4px;
|
||||
height: 20px;
|
||||
margin-bottom: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.faux-text.short {
|
||||
width: 75%;
|
||||
}
|
||||
.faux-text.shorter {
|
||||
width: 55%;
|
||||
}
|
||||
|
||||
.shimmer {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.shimmer::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 0.4) 50%,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
animation: shimmer 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
@ -7,58 +7,112 @@ const params = {
|
||||
'fruit': 'Quantité de fruits pour 100g (g)',
|
||||
}
|
||||
|
||||
var tikats;
|
||||
|
||||
function addProduct(tikette) {
|
||||
const zett = tikette;
|
||||
const appbody = $("#appbody");
|
||||
const block = $('<div class="section">');
|
||||
for (let sub in zett.subs) {
|
||||
block.append($(`<div class="input-field"><label class="active">${params[sub]}</label><input type="text" name="${sub}" value="${zett.subs[sub]}">`));
|
||||
}
|
||||
const loader = $('<div class="progress"><div class="indeterminate"></div></div>')
|
||||
.hide();
|
||||
|
||||
const action = $('<div class="section">')
|
||||
.append($('<a class="btn">generate<a>')
|
||||
.click(() => {
|
||||
const subs = block.find(':text')
|
||||
.toArray()
|
||||
.reduce((obj, el) => ({...obj, [el.name]: el.value}), {});
|
||||
const req = {
|
||||
sticker: zett.sticker,
|
||||
subs,
|
||||
landscape: zett.landscape,
|
||||
};
|
||||
|
||||
loader.show();
|
||||
$('.btn').addClass("disabled");
|
||||
|
||||
$.post(backend_api + 'generate', JSON.stringify(req))
|
||||
.then(data => {
|
||||
const pdfbtn = $(`<a class="btn" href="${backend_api}data/${data.file}" target="_blank">open pdf</a>`);
|
||||
action.append(pdfbtn);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
})
|
||||
.always(() => {
|
||||
loader.hide();
|
||||
$('.btn').removeClass('disabled');
|
||||
});
|
||||
})
|
||||
.append(loader));
|
||||
|
||||
const deleteAction = $('<a class="btn-flat grey-text"><b class="material-icons">delete</b>');
|
||||
deleteAction.click(() => {
|
||||
const req = {
|
||||
id: zett.id,
|
||||
};
|
||||
$.post(backend_api + 'deletetikette', JSON.stringify(req)).then(reload);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
appbody
|
||||
.append($('<li>')
|
||||
.append($('<div class="collapsible-header valign-wrapper">')
|
||||
.append(`<h6 class="blue-text">${zett.title}</h6>`)
|
||||
.append($('<span class="badge">')
|
||||
.append(deleteAction)))
|
||||
.append($('<div class="collapsible-body">')
|
||||
.append(block)
|
||||
.append(action)));
|
||||
}
|
||||
|
||||
function setCategories() {
|
||||
const katsel = $('#new-type');
|
||||
katsel.empty();
|
||||
|
||||
for (let kat of tikats) {
|
||||
katsel.append($(`<option value="${kat.id}">${kat.name}</option>`));
|
||||
}
|
||||
}
|
||||
|
||||
function loadAll(zetikettes) {
|
||||
const appbody = $("#appbody");
|
||||
appbody.empty();
|
||||
for (let zett of zetikettes) {
|
||||
const block = $('<div class="section">');
|
||||
for (let sub in zett.subs) {
|
||||
block.append($(`<div class="input-field"><label class="active">${params[sub]}</label><input type="text" name="${sub}" value="${zett.subs[sub]}">`));
|
||||
}
|
||||
const loader = $('<div class="progress"><div class="indeterminate"></div></div>')
|
||||
.hide();
|
||||
|
||||
const action = $('<div class="section">')
|
||||
.append($('<a class="btn">generate<a>')
|
||||
.click(() => {
|
||||
const subs = block.find(':text')
|
||||
.toArray()
|
||||
.reduce((obj, el) => ({...obj, [el.name]: el.value}), {});
|
||||
const req = {
|
||||
sticker: zett.sticker,
|
||||
subs,
|
||||
landscape: zett.landscape,
|
||||
};
|
||||
|
||||
loader.show();
|
||||
$('.btn').addClass("disabled");
|
||||
|
||||
$.post(backend_api, JSON.stringify(req))
|
||||
.then(data => {
|
||||
console.log(data);
|
||||
const pdfbtn = $(`<a class="btn" href="${backend_api}data/${data.file}" target="_blank">open pdf</a>`);
|
||||
action.append(pdfbtn);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
})
|
||||
.always(() => {
|
||||
loader.hide();
|
||||
$('.btn').removeClass('disabled');
|
||||
});
|
||||
})
|
||||
.append(loader));
|
||||
|
||||
appbody
|
||||
.append($('<li>')
|
||||
.append($(`<div class="collapsible-header"><h6 class="blue-text">${zett.title}</h6></div>`))
|
||||
.append($('<div class="collapsible-body">')
|
||||
.append(block)
|
||||
.append(action)));
|
||||
addProduct(zett);
|
||||
}
|
||||
setCategories();
|
||||
|
||||
$('.collapsible').collapsible();
|
||||
$('.modal').modal();
|
||||
$('select').formSelect();
|
||||
|
||||
konami();
|
||||
|
||||
$('#new-add').off('click').click(() => {
|
||||
const title = $("#new-name").val();
|
||||
const category_id = $("#new-type").val();
|
||||
const designation = $("#new-designation").val();
|
||||
const ingredients = $("#new-ingredients").val();
|
||||
const description = $("#new-description").val();
|
||||
const color = $("#new-color").val().substring(1);
|
||||
const ab = $("#new-organic").is(":checked") ? 'visible' : 'none';
|
||||
|
||||
const req = {
|
||||
title,
|
||||
category_id,
|
||||
designation,
|
||||
ingredients,
|
||||
description,
|
||||
color,
|
||||
ab,
|
||||
};
|
||||
$.post(backend_api + 'newtikette', JSON.stringify(req)).then(reload);
|
||||
});
|
||||
}
|
||||
|
||||
function konami() {
|
||||
@ -76,16 +130,22 @@ function konami() {
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(async () => {
|
||||
async function reload() {
|
||||
try {
|
||||
const resp = await $.ajax({
|
||||
url: backend_api + 'list',
|
||||
timeout: 1000,
|
||||
});
|
||||
tikats = (await $.ajax({
|
||||
url: backend_api + 'categories',
|
||||
timeout: 1000,
|
||||
})).tikats.sort((a, b) => a.name > b.name ? 1 : -1);
|
||||
loadAll(resp.tikettes.sort((a, b) => (a.title < b.title) ? -1 : 1));
|
||||
} catch(e) {
|
||||
const appbody = $("#appbody");
|
||||
appbody.append(`<li>Could not reach backend server`);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(reload);
|
||||
|
1373
templates/Aromate.svg
Normal file
After Width: | Height: | Size: 1.1 MiB |
1291
templates/Chocolat.svg
Normal file
After Width: | Height: | Size: 1.1 MiB |
1440
templates/Confiture.svg
Normal file
After Width: | Height: | Size: 5.3 MiB |
Before Width: | Height: | Size: 5.3 MiB After Width: | Height: | Size: 5.3 MiB |
1431
templates/Sel.svg
Normal file
After Width: | Height: | Size: 5.2 MiB |
2249
templates/Sirop.svg
Normal file
After Width: | Height: | Size: 1.2 MiB |
1392
templates/Tisane.svg
Normal file
After Width: | Height: | Size: 1.1 MiB |
@ -1,16 +0,0 @@
|
||||
[Unit]
|
||||
Description=Zetikettes backend service
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/docker run --rm --name %n \
|
||||
-p 127.0.0.1:8000:8000 \
|
||||
-v /var/lib/zetikettes/data:/data \
|
||||
zetikettes
|
||||
Restart=on-failure
|
||||
ExecStartPre=-/usr/bin/docker exec %n stop
|
||||
ExecStartPre=-/usr/bin/docker rm %n
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|