diff --git a/.gitignore b/.gitignore
index 612f7c45..38b8f288 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,9 @@ vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# USDA bulk data downloads (regenerated by scripts/import-usda-nutrition.ts)
data/usda/
+# Loyalty-card barcodes (regenerated by scripts/generate-loyalty-cards.ts from env)
+static/shopping/supercard.svg
+static/shopping/cumulus.svg
src-tauri/target/
src-tauri/*.keystore
# Android: ignore build output and caches, track source files
diff --git a/package.json b/package.json
index c5eeb96f..ba112df8 100644
--- a/package.json
+++ b/package.json
@@ -1,11 +1,11 @@
{
"name": "homepage",
- "version": "1.46.28",
+ "version": "1.47.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
- "prebuild": "bash scripts/subset-emoji-font.sh && pnpm exec vite-node scripts/generate-mystery-verses.ts && pnpm exec vite-node scripts/download-models.ts",
+ "prebuild": "bash scripts/subset-emoji-font.sh && pnpm exec vite-node scripts/generate-mystery-verses.ts && pnpm exec vite-node scripts/download-models.ts && pnpm exec vite-node scripts/generate-loyalty-cards.ts",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
@@ -37,6 +37,7 @@
"@types/node": "^22.12.0",
"@types/node-cron": "^3.0.11",
"@vitest/ui": "^4.1.2",
+ "bwip-js": "^4.10.1",
"jsdom": "^27.2.0",
"svelte": "^5.55.1",
"svelte-check": "^4.4.6",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 940d3934..aa8efce3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -93,6 +93,9 @@ importers:
'@vitest/ui':
specifier: ^4.1.2
version: 4.1.2(vitest@4.1.2)
+ bwip-js:
+ specifier: ^4.10.1
+ version: 4.10.1
jsdom:
specifier: ^27.2.0
version: 27.2.0
@@ -1194,6 +1197,10 @@ packages:
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+ bwip-js@4.10.1:
+ resolution: {integrity: sha512-I/cEPiXsu7dRCp78PpVY4gdIXmbH752n8dMC+DStM77XPkrzeathdYrjnZ/i/vZPIxXTUWc+JxgJ/MvbodqPLA==}
+ hasBin: true
+
cac@7.0.0:
resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==}
engines: {node: '>=20.19.0'}
@@ -2952,6 +2959,8 @@ snapshots:
buffer-from@1.1.2:
optional: true
+ bwip-js@4.10.1: {}
+
cac@7.0.0: {}
chai@6.2.2: {}
diff --git a/scripts/generate-loyalty-cards.ts b/scripts/generate-loyalty-cards.ts
new file mode 100644
index 00000000..28ca0497
--- /dev/null
+++ b/scripts/generate-loyalty-cards.ts
@@ -0,0 +1,56 @@
+/**
+ * Build-time generation of loyalty-card barcode SVGs.
+ *
+ * Reads card numbers from env vars and writes static/shopping/supercard.svg
+ * + static/shopping/cumulus.svg. Skips cards whose env var is unset so the
+ * site still builds in environments without secrets.
+ *
+ * SHOPPING_COOP_SUPERCARD_NUMBER → Data Matrix (Coop Supercard)
+ * SHOPPING_MIGROS_CUMULUS_NUMBER → Code 128 (Migros Cumulus)
+ *
+ * Run: pnpm exec vite-node scripts/generate-loyalty-cards.ts
+ */
+import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
+import { dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { toSVG } from 'bwip-js/node';
+
+const HERE = dirname(fileURLToPath(import.meta.url));
+const OUT_DIR = resolve(HERE, '..', 'static', 'shopping');
+
+type CardSpec = {
+ envVar: string;
+ filename: string;
+ bcid: 'datamatrix' | 'code128';
+ scale: number;
+};
+
+const cards: CardSpec[] = [
+ { envVar: 'SHOPPING_COOP_SUPERCARD_NUMBER', filename: 'supercard.svg', bcid: 'datamatrix', scale: 6 },
+ { envVar: 'SHOPPING_MIGROS_CUMULUS_NUMBER', filename: 'cumulus.svg', bcid: 'code128', scale: 3 }
+];
+
+mkdirSync(OUT_DIR, { recursive: true });
+
+for (const card of cards) {
+ const value = process.env[card.envVar]?.trim();
+ const outPath = resolve(OUT_DIR, card.filename);
+
+ if (!value) {
+ try { rmSync(outPath); } catch { /* not present */ }
+ console.log(`[loyalty-cards] ${card.envVar} not set — skipped ${card.filename}`);
+ continue;
+ }
+
+ const svg = toSVG({
+ bcid: card.bcid,
+ text: value,
+ scale: card.scale,
+ includetext: false,
+ paddingwidth: 8,
+ paddingheight: 8
+ });
+
+ writeFileSync(outPath, svg, 'utf8');
+ console.log(`[loyalty-cards] wrote ${card.filename} (${card.bcid})`);
+}
diff --git a/src/lib/components/shopping/LoyaltyCards.svelte b/src/lib/components/shopping/LoyaltyCards.svelte
new file mode 100644
index 00000000..e949f1a6
--- /dev/null
+++ b/src/lib/components/shopping/LoyaltyCards.svelte
@@ -0,0 +1,162 @@
+
+
+
Keine Karten konfiguriert.
+ {/if} +