Files
scan-receipts/scripts/generate_art_showcase.mjs
Timo 84b9987c49 Add full application: receipt scanning, auth, billing, and account deletion
Brings the working codebase (Next.js app, auth system, Stripe billing,
Docker/deploy config, tests, docs) into version control on top of the
placeholder initial commit, and adds account self-deletion (Danger Zone
in Settings, password + typed-email confirmation, cascading DB cleanup,
Stripe cancellation) per GDPR right-to-erasure.

Excludes local build caches, node_modules, and internal agent scratch
files; .gitignore hardened to keep those out going forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 20:59:04 +02:00

384 lines
16 KiB
JavaScript

import fs from "fs";
import path from "path";
import sharp from "sharp";
const showcaseDir = path.join(process.cwd(), "public", "showcase");
if (!fs.existsSync(showcaseDir)) {
fs.mkdirSync(showcaseDir, { recursive: true });
}
// 1. Synthwave / Cyberpunk Sunset (2400 x 1350 - 16:9 2K)
const synthwaveSvg = `
<svg width="2400" height="1350" viewBox="0 0 2400 1350" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="skyGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#0a051b" />
<stop offset="35%" stop-color="#1b0a3a" />
<stop offset="65%" stop-color="#4a0e4e" />
<stop offset="85%" stop-color="#a01a7d" />
<stop offset="100%" stop-color="#ff5e62" />
</linearGradient>
<linearGradient id="sunGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#ffee55" />
<stop offset="40%" stop-color="#ff5483" />
<stop offset="100%" stop-color="#8000ff" />
</linearGradient>
<linearGradient id="gridGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#ff007f" stop-opacity="0.9" />
<stop offset="100%" stop-color="#00f2fe" stop-opacity="0.1" />
</linearGradient>
<linearGradient id="mountGrad1" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#2d0b4d" />
<stop offset="100%" stop-color="#0e041d" />
</linearGradient>
<linearGradient id="mountGrad2" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#551268" />
<stop offset="100%" stop-color="#120420" />
</linearGradient>
<filter id="neonGlow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="25" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="softGlow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="60" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
<!-- Sky -->
<rect width="2400" height="1350" fill="url(#skyGrad)" />
<!-- Stars -->
${Array.from({ length: 120 })
.map(() => {
const cx = Math.floor(Math.random() * 2400);
const cy = Math.floor(Math.random() * 700);
const r = (Math.random() * 1.8 + 0.5).toFixed(1);
const op = (Math.random() * 0.7 + 0.3).toFixed(2);
return `<circle cx="${cx}" cy="${cy}" r="${r}" fill="#fff" opacity="${op}" />`;
})
.join("\n ")}
<!-- Sun Glow & Sun -->
<circle cx="1200" cy="720" r="340" fill="url(#sunGrad)" filter="url(#softGlow)" />
<!-- Sun Horizontal Blinds (Synthwave Bars) -->
${[600, 640, 675, 705, 730, 750, 765, 777]
.map((y, i) => `<rect x="800" y="${y}" width="800" height="${4 + i * 2.2}" fill="#1b0a3a" />`)
.join("\n ")}
<!-- Distant Mountains Layer 1 -->
<polygon points="0,850 280,680 520,790 850,620 1100,740 1450,590 1780,750 2050,650 2400,820 2400,1350 0,1350" fill="url(#mountGrad1)" opacity="0.95"/>
<!-- Foreground Mountains Layer 2 -->
<polygon points="0,920 380,730 750,880 1200,670 1620,870 1950,710 2400,900 2400,1350 0,1350" fill="url(#mountGrad2)" opacity="0.9"/>
<!-- Horizon Glow -->
<line x1="0" y1="850" x2="2400" y2="850" stroke="#00f2fe" stroke-width="4" filter="url(#neonGlow)" />
<!-- 3D Perspective Grid Plane -->
<rect x="0" y="850" width="2400" height="500" fill="#070210" />
<!-- Horizontal Grid Lines with exponential spacing -->
${[860, 875, 895, 920, 955, 1000, 1060, 1135, 1225, 1335]
.map((y) => `<line x1="0" y1="${y}" x2="2400" y2="${y}" stroke="url(#gridGrad)" stroke-width="${1 + (y - 850) / 100}" />`)
.join("\n ")}
<!-- Perspective Vanishing Lines radiating from center horizon -->
${Array.from({ length: 33 })
.map((_, i) => {
const bottomX = (i - 16) * 160 + 1200;
return `<line x1="1200" y1="850" x2="${bottomX}" y2="1350" stroke="#ff00a0" stroke-width="1.8" opacity="0.75" />`;
})
.join("\n ")}
<!-- Title / Logo Typography -->
<text x="1200" y="240" font-family="'Inter', 'Montserrat', 'Helvetica', sans-serif" font-size="64" font-weight="900" letter-spacing="16" fill="#ffffff" text-anchor="middle" filter="url(#neonGlow)">NEON HORIZON</text>
<text x="1200" y="295" font-family="'Courier New', monospace" font-size="20" letter-spacing="10" fill="#00f2fe" text-anchor="middle" opacity="0.9">PROCEDURAL VECTOR GRAPHICS ENGINE</text>
</svg>
`;
// 2. Premium Dark Mode Glassmorphic FinTech Dashboard (2000 x 2000 - 1:1 Square)
const glassmorphismSvg = `
<svg width="2000" height="2000" viewBox="0 0 2000 2000" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Background Gradient -->
<radialGradient id="bgDark" cx="50%" cy="30%" r="90%">
<stop offset="0%" stop-color="#14192b" />
<stop offset="50%" stop-color="#0b0e17" />
<stop offset="100%" stop-color="#05070c" />
</radialGradient>
<!-- Orb Gradients -->
<radialGradient id="orbPurple" cx="35%" cy="35%" r="65%">
<stop offset="0%" stop-color="#b55fe6" />
<stop offset="60%" stop-color="#6e24db" />
<stop offset="100%" stop-color="#2a0873" />
</radialGradient>
<radialGradient id="orbCyan" cx="30%" cy="30%" r="70%">
<stop offset="0%" stop-color="#5bf0e4" />
<stop offset="50%" stop-color="#0b9df2" />
<stop offset="100%" stop-color="#023b82" />
</radialGradient>
<linearGradient id="cardGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="rgba(255, 255, 255, 0.12)" />
<stop offset="100%" stop-color="rgba(255, 255, 255, 0.02)" />
</linearGradient>
<linearGradient id="accentGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#6366f1" />
<stop offset="50%" stop-color="#a855f7" />
<stop offset="100%" stop-color="#ec4899" />
</linearGradient>
<filter id="blurOrb" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="80" />
</filter>
<filter id="cardShadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="30" stdDeviation="40" flood-color="#000" flood-opacity="0.6"/>
</filter>
</defs>
<!-- Background -->
<rect width="2000" height="2000" fill="url(#bgDark)" />
<!-- Background Ambient Glow Orbs -->
<circle cx="450" cy="500" r="380" fill="url(#orbPurple)" filter="url(#blurOrb)" opacity="0.65" />
<circle cx="1550" cy="1400" r="440" fill="url(#orbCyan)" filter="url(#blurOrb)" opacity="0.55" />
<circle cx="1600" cy="400" r="280" fill="#ec4899" filter="url(#blurOrb)" opacity="0.35" />
<!-- Subtle Matrix Dot Grid -->
${Array.from({ length: 20 })
.map((_, row) =>
Array.from({ length: 20 })
.map((_, col) => `<circle cx="${100 + col * 95}" cy="${100 + row * 95}" r="2" fill="#ffffff" opacity="0.08" />`)
.join("")
)
.join("\n ")}
<!-- Main Central Glass Card -->
<g filter="url(#cardShadow)">
<rect x="300" y="320" width="1400" height="1360" rx="40" fill="url(#cardGrad)" stroke="rgba(255,255,255,0.22)" stroke-width="2" />
<!-- Top Bar inside Card -->
<circle cx="380" cy="400" r="12" fill="#ef4444" opacity="0.8" />
<circle cx="420" cy="400" r="12" fill="#f59e0b" opacity="0.8" />
<circle cx="460" cy="400" r="12" fill="#10b981" opacity="0.8" />
<text x="1000" y="408" font-family="'Inter', sans-serif" font-size="22" font-weight="600" fill="#a1a1aa" text-anchor="middle" letter-spacing="2">QUANTUM ANALYTICS ENTERPRISE</text>
<!-- Header Stats -->
<text x="380" y="520" font-family="'Inter', sans-serif" font-size="20" font-weight="500" fill="#94a3b8" letter-spacing="1">TOTAL REVENUE (YTD)</text>
<text x="380" y="590" font-family="'Inter', sans-serif" font-size="64" font-weight="800" fill="#ffffff" letter-spacing="-1">$4,892,340.50</text>
<!-- Growth Badge -->
<rect x="920" y="535" width="140" height="44" rx="22" fill="rgba(16, 185, 129, 0.15)" stroke="#10b981" stroke-width="1.5" />
<text x="990" y="563" font-family="'Inter', sans-serif" font-size="18" font-weight="700" fill="#10b981" text-anchor="middle">+34.8% ↑</text>
<!-- Chart Area -->
<g transform="translate(380, 680)">
<!-- Grid lines -->
<line x1="0" y1="0" x2="1240" y2="0" stroke="rgba(255,255,255,0.08)" stroke-width="1.5" stroke-dasharray="6,6" />
<line x1="0" y1="120" x2="1240" y2="120" stroke="rgba(255,255,255,0.08)" stroke-width="1.5" stroke-dasharray="6,6" />
<line x1="0" y1="240" x2="1240" y2="240" stroke="rgba(255,255,255,0.08)" stroke-width="1.5" stroke-dasharray="6,6" />
<line x1="0" y1="360" x2="1240" y2="360" stroke="rgba(255,255,255,0.08)" stroke-width="1.5" stroke-dasharray="6,6" />
<!-- Area Chart Gradient Fill -->
<defs>
<linearGradient id="areaGlow" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#8b5cf6" stop-opacity="0.45" />
<stop offset="100%" stop-color="#8b5cf6" stop-opacity="0.0" />
</linearGradient>
</defs>
<!-- Smooth Spline Path -->
<path d="M 0,320 C 150,280 250,180 400,210 C 550,240 700,80 850,110 C 1000,140 1100,30 1240,10 L 1240,400 L 0,400 Z" fill="url(#areaGlow)" />
<path d="M 0,320 C 150,280 250,180 400,210 C 550,240 700,80 850,110 C 1000,140 1100,30 1240,10" fill="none" stroke="url(#accentGrad)" stroke-width="6" stroke-linecap="round" />
<!-- Glowing Data Points -->
<circle cx="400" cy="210" r="9" fill="#8b5cf6" stroke="#fff" stroke-width="3" />
<circle cx="850" cy="110" r="9" fill="#ec4899" stroke="#fff" stroke-width="3" />
<circle cx="1240" cy="10" r="12" fill="#00f2fe" stroke="#fff" stroke-width="4" />
</g>
<!-- Sub Cards (Mini Metrics) -->
<!-- Card 1 -->
<g transform="translate(380, 1180)">
<rect width="380" height="240" rx="24" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.12)" stroke-width="1.5" />
<text x="40" y="60" font-family="'Inter', sans-serif" font-size="18" font-weight="600" fill="#94a3b8">Active Workflows</text>
<text x="40" y="130" font-family="'Inter', sans-serif" font-size="44" font-weight="800" fill="#ffffff">18,492</text>
<text x="40" y="185" font-family="'Inter', sans-serif" font-size="16" font-weight="500" fill="#38bdf8">99.98% uptime</text>
</g>
<!-- Card 2 -->
<g transform="translate(810, 1180)">
<rect width="380" height="240" rx="24" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.12)" stroke-width="1.5" />
<text x="40" y="60" font-family="'Inter', sans-serif" font-size="18" font-weight="600" fill="#94a3b8">AI Inference Latency</text>
<text x="40" y="130" font-family="'Inter', sans-serif" font-size="44" font-weight="800" fill="#ffffff">14.2 ms</text>
<text x="40" y="185" font-family="'Inter', sans-serif" font-size="16" font-weight="500" fill="#a855f7">Realtime stream</text>
</g>
<!-- Card 3 -->
<g transform="translate(1240, 1180)">
<rect width="380" height="240" rx="24" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.12)" stroke-width="1.5" />
<text x="40" y="60" font-family="'Inter', sans-serif" font-size="18" font-weight="600" fill="#94a3b8">Data Processed</text>
<text x="40" y="130" font-family="'Inter', sans-serif" font-size="44" font-weight="800" fill="#ffffff">942.6 TB</text>
<text x="40" y="185" font-family="'Inter', sans-serif" font-size="16" font-weight="500" fill="#10b981">+12.4% vs last week</text>
</g>
</g>
</svg>
`;
// 3. Sacred Geometry / Algorithmic Golden Mandala (2400 x 2400 - Ultra HD)
function generateMandalaSvg() {
const size = 2400;
const center = size / 2;
const rings = 12;
const petals = 24;
let elements = [];
for (let r = 1; r <= rings; r++) {
const radius = r * 85;
const strokeWidth = (r % 3 === 0 ? 3 : 1.2);
const opacity = (0.2 + (r / rings) * 0.7).toFixed(2);
elements.push(`<circle cx="${center}" cy="${center}" r="${radius}" fill="none" stroke="url(#goldGrad)" stroke-width="${strokeWidth}" opacity="${opacity}" />`);
}
for (let i = 0; i < petals; i++) {
const angle = (i * 360) / petals;
elements.push(`
<g transform="rotate(${angle} ${center} ${center})">
<ellipse cx="${center}" cy="${center - 380}" rx="90" ry="340" fill="none" stroke="url(#goldGrad)" stroke-width="1.8" opacity="0.6" />
<circle cx="${center}" cy="${center - 650}" r="25" fill="none" stroke="#ffd700" stroke-width="2.5" />
<line x1="${center}" y1="${center - 100}" x2="${center}" y2="${center - 950}" stroke="url(#goldGrad)" stroke-width="1" opacity="0.4" stroke-dasharray="5,8" />
<polygon points="${center},${center - 850} ${center - 30},${center - 780} ${center + 30},${center - 780}" fill="url(#goldGrad)" opacity="0.75" />
</g>
`);
}
for (let i = 0; i < 48; i++) {
const angle = (i * 360) / 48;
elements.push(`
<g transform="rotate(${angle} ${center} ${center})">
<circle cx="${center}" cy="${center - 880}" r="6" fill="#fff5cc" opacity="0.8" />
</g>
`);
}
return `
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="darkCosmos" cx="50%" cy="50%" r="70%">
<stop offset="0%" stop-color="#120c02" />
<stop offset="60%" stop-color="#070501" />
<stop offset="100%" stop-color="#000000" />
</radialGradient>
<linearGradient id="goldGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ffe259" />
<stop offset="50%" stop-color="#ffa751" />
<stop offset="100%" stop-color="#e67e22" />
</linearGradient>
<radialGradient id="centerGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#ffdd59" stop-opacity="0.8" />
<stop offset="40%" stop-color="#ff9f43" stop-opacity="0.3" />
<stop offset="100%" stop-color="#000" stop-opacity="0" />
</radialGradient>
<filter id="goldGlow" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur stdDeviation="15" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
<!-- Background -->
<rect width="${size}" height="${size}" fill="url(#darkCosmos)" />
<!-- Center Ambient Sun Glow -->
<circle cx="${center}" cy="${center}" r="550" fill="url(#centerGlow)" />
<!-- Mandala Geometry -->
<g filter="url(#goldGlow)">
${elements.join("\n")}
</g>
<!-- Center Focal Core -->
<circle cx="${center}" cy="${center}" r="60" fill="url(#goldGrad)" />
<circle cx="${center}" cy="${center}" r="80" fill="none" stroke="#fff" stroke-width="2" opacity="0.8" />
<circle cx="${center}" cy="${center}" r="110" fill="none" stroke="url(#goldGrad)" stroke-width="4" />
<text x="${center}" y="${size - 120}" font-family="'Cinzel', 'Times New Roman', serif" font-size="36" letter-spacing="18" fill="#ffd700" text-anchor="middle" opacity="0.9">SACRED ALGORITHMIC HARMONY</text>
</svg>
`;
}
async function runShowcase() {
console.log("🎨 Generating High-Resolution Showcase Art in public/showcase/...\n");
const items = [
{
filename: "01_synthwave_cyberpunk_sunset.png",
svg: synthwaveSvg,
type: "png",
},
{
filename: "02_glassmorphic_fintech_dashboard.png",
svg: glassmorphismSvg,
type: "png",
},
{
filename: "03_sacred_geometry_mandala_2400.png",
svg: generateMandalaSvg(),
type: "png",
},
];
for (const item of items) {
const destPath = path.join(showcaseDir, item.filename);
const svgBuffer = Buffer.from(item.svg);
console.log(`Rendering ${item.filename}...`);
if (item.type === "png") {
await sharp(svgBuffer)
.png({ compressionLevel: 9, adaptiveFiltering: true })
.toFile(destPath);
} else {
await sharp(svgBuffer)
.jpeg({ quality: 98, mozjpeg: true })
.toFile(destPath);
}
const stat = fs.statSync(destPath);
console.log(`✓ Created: ${item.filename} (${(stat.size / 1024).toFixed(1)} KB)`);
}
console.log("\n🚀 All showcase art generated successfully in public/showcase/!");
}
runShowcase().catch((err) => {
console.error("Error generating showcase images:", err);
process.exit(1);
});