This commit is contained in:
Timo Knuth 2026-01-26 19:51:00 +01:00
parent 7b2788da7a
commit 76a76258e8
7 changed files with 378 additions and 333 deletions

View File

@ -1,64 +1,64 @@
# ---- deps ---- # ---- deps ----
FROM node:20-alpine AS deps FROM node:20-alpine AS deps
# Install OpenSSL for Prisma # Install OpenSSL for Prisma
RUN apk add --no-cache openssl RUN apk add --no-cache openssl
WORKDIR /app WORKDIR /app
COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* .npmrc* ./ COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* .npmrc* ./
# Copy prisma schema for postinstall script # Copy prisma schema for postinstall script
COPY prisma ./prisma COPY prisma ./prisma
RUN \ RUN \
if [ -f pnpm-lock.yaml ]; then \ if [ -f pnpm-lock.yaml ]; then \
npm i -g pnpm && pnpm i --frozen-lockfile; \ npm i -g pnpm && pnpm i --frozen-lockfile; \
elif [ -f yarn.lock ]; then \ elif [ -f yarn.lock ]; then \
yarn --frozen-lockfile; \ yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then \ elif [ -f package-lock.json ]; then \
npm ci; \ npm ci; \
else \ else \
npm install --legacy-peer-deps; \ npm install --legacy-peer-deps; \
fi fi
# ---- builder ---- # ---- builder ----
FROM node:20-alpine AS builder FROM node:20-alpine AS builder
# Install OpenSSL for Prisma # Install OpenSSL for Prisma
RUN apk add --no-cache openssl RUN apk add --no-cache openssl
WORKDIR /app WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
# Add build-time environment variables with defaults # Add build-time environment variables with defaults
ENV NEXTAUTH_URL="https://www.qrmaster.net" ENV NEXTAUTH_URL="https://www.qrmaster.net"
ENV NEXTAUTH_SECRET="build-time-secret" ENV NEXTAUTH_SECRET="build-time-secret"
ENV IP_SALT="build-time-salt" ENV IP_SALT="build-time-salt"
ENV STRIPE_SECRET_KEY="sk_test_placeholder_for_build" ENV STRIPE_SECRET_KEY="sk_test_placeholder_for_build"
ENV RESEND_API_KEY="re_placeholder_for_build" ENV RESEND_API_KEY="re_placeholder_for_build"
ENV NEXT_PUBLIC_APP_URL="https://www.qrmaster.net" ENV NEXT_PUBLIC_APP_URL="https://www.qrmaster.net"
# PostHog Analytics - REQUIRED at build time for client-side bundle # PostHog Analytics - REQUIRED at build time for client-side bundle
ENV NEXT_PUBLIC_POSTHOG_KEY="phc_97JBJVVQlqqiZuTVRHuBnnG9HasOv3GSsdeVjossizJ" ENV NEXT_PUBLIC_POSTHOG_KEY="phc_97JBJVVQlqqiZuTVRHuBnnG9HasOv3GSsdeVjossizJ"
ENV NEXT_PUBLIC_POSTHOG_HOST="https://us.i.posthog.com" ENV NEXT_PUBLIC_POSTHOG_HOST="https://us.i.posthog.com"
ENV NEXT_PUBLIC_INDEXABLE="true" ENV NEXT_PUBLIC_INDEXABLE="true"
ENV NEXT_PUBLIC_FACEBOOK_PIXEL_ID="1601718491252690" ENV NEXT_PUBLIC_FACEBOOK_PIXEL_ID="1601718491252690"
RUN npx prisma generate RUN npx prisma generate
RUN npm run build RUN npm run build
# ---- runner ---- # ---- runner ----
FROM node:20-alpine AS runner FROM node:20-alpine AS runner
# Install OpenSSL for Prisma runtime # Install OpenSSL for Prisma runtime
RUN apk add --no-cache openssl RUN apk add --no-cache openssl
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/prisma ./prisma COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public COPY --from=builder /app/public ./public
USER nextjs USER nextjs
EXPOSE 3000 EXPOSE 3000
CMD ["node", "server.js"] CMD ["node", "server.js"]

View File

@ -1 +1 @@
bb6dfaacf1ed41a880281c426c54ed7c bb6dfaacf1ed41a880281c426c54ed7c

41
scripts/validate-data.ts Normal file
View File

@ -0,0 +1,41 @@
import { blogPosts } from '../src/lib/blog-data';
import { authors } from '../src/lib/author-data';
import { getPublishedPosts, getPostsByAuthor } from '../src/lib/content';
console.log("Validating Author Data...");
authors.forEach(author => {
console.log(`Checking author: ${author.slug}`);
if (!author.name) console.error(`Error: Author ${author.slug} missing name`);
if (!author.image) console.warn(`Warning: Author ${author.slug} missing image`);
});
console.log("\nValidating Blog Data...");
blogPosts.forEach(post => {
try {
if (!post.slug) console.error("Error: Post missing slug", post);
if (!post.datePublished && !post.date) console.error(`Error: Post ${post.slug} missing date`);
const d = new Date(post.datePublished || post.date);
if (isNaN(d.getTime())) {
console.error(`Error: Post ${post.slug} has invalid date: ${post.datePublished || post.date}`);
}
} catch (e) {
console.error(`Exception checking post ${post.slug || 'unknown'}:`, e);
}
});
console.log("\nTesting Content Functions...");
try {
const published = getPublishedPosts();
console.log(`getPublishedPosts returned ${published.length} posts.`);
authors.forEach(author => {
const posts = getPostsByAuthor(author.slug);
console.log(`Author ${author.slug} has ${posts.length} posts.`);
});
} catch (e) {
console.error("Error running content functions:", e);
}
console.log("\nValidation Complete.");

View File

@ -81,7 +81,7 @@ export default function AuthorPage({ params }: { params: { slug: string } }) {
<div className="space-y-4"> <div className="space-y-4">
{posts.map(p => ( {posts.map(p => (
<Link key={p.slug} href={`/blog/${p.slug}`} className="block group p-6 rounded-xl border border-gray-200 bg-white hover:border-blue-200 hover:shadow-sm transition-all"> <Link key={p.slug} href={`/blog/${p.slug}`} className="block group p-6 rounded-xl border border-gray-200 bg-white hover:border-blue-200 hover:shadow-sm transition-all">
<div className="text-sm text-gray-400 mb-1">{p.date}</div> <div className="text-sm text-gray-400 mb-1">{new Date(p.datePublished || p.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</div>
<h3 className="text-xl font-bold text-gray-900 group-hover:text-blue-700 transition-colors mb-2">{p.title}</h3> <h3 className="text-xl font-bold text-gray-900 group-hover:text-blue-700 transition-colors mb-2">{p.title}</h3>
<p className="text-gray-600">{p.description}</p> <p className="text-gray-600">{p.description}</p>
</Link> </Link>

View File

@ -55,7 +55,7 @@ export default function PillarPage({ params }: { params: { pillar: PillarKey } }
<div className="grid md:grid-cols-2 gap-6"> <div className="grid md:grid-cols-2 gap-6">
{posts.map(p => ( {posts.map(p => (
<Link key={p.slug} href={`/blog/${p.slug}`} className="group block rounded-xl border border-gray-200 bg-white p-6 shadow-sm hover:shadow-md hover:border-blue-200 transition-all"> <Link key={p.slug} href={`/blog/${p.slug}`} className="group block rounded-xl border border-gray-200 bg-white p-6 shadow-sm hover:shadow-md hover:border-blue-200 transition-all">
<div className="text-xs text-gray-400 mb-2">{p.date}</div> <div className="text-xs text-gray-400 mb-2">{new Date(p.datePublished || p.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</div>
<div className="text-lg font-bold text-gray-900 mb-2 group-hover:text-blue-700">{p.title}</div> <div className="text-lg font-bold text-gray-900 mb-2 group-hover:text-blue-700">{p.title}</div>
<div className="text-sm text-gray-600 line-clamp-2">{p.description}</div> <div className="text-sm text-gray-600 line-clamp-2">{p.description}</div>
</Link> </Link>

View File

@ -10,7 +10,11 @@ export const metadata = {
export default function LearnHubPage() { export default function LearnHubPage() {
const posts = getPublishedPosts(); const posts = getPublishedPosts();
// Sort by date descending // Sort by date descending
const topLatest = [...posts].sort((a, b) => (new Date(a.datePublished).getTime() < new Date(b.datePublished).getTime() ? 1 : -1)).slice(0, 6); const topLatest = [...posts].sort((a, b) => {
const dateA = a.datePublished ? new Date(a.datePublished) : new Date(a.date);
const dateB = b.datePublished ? new Date(b.datePublished) : new Date(b.date);
return dateB.getTime() - dateA.getTime();
}).slice(0, 6);
return ( return (
<main className="container mx-auto max-w-5xl py-12 px-4 space-y-12"> <main className="container mx-auto max-w-5xl py-12 px-4 space-y-12">
@ -41,7 +45,7 @@ export default function LearnHubPage() {
<Link key={p.slug} href={`/blog/${p.slug}`} className="group block rounded-2xl border border-gray-200 bg-white p-6 shadow-sm hover:shadow-md hover:border-blue-200 transition-all"> <Link key={p.slug} href={`/blog/${p.slug}`} className="group block rounded-2xl border border-gray-200 bg-white p-6 shadow-sm hover:shadow-md hover:border-blue-200 transition-all">
<div className="flex justify-between items-center mb-3"> <div className="flex justify-between items-center mb-3">
<div className="text-xs font-semibold px-2 py-1 rounded bg-gray-100 text-gray-600">{p.pillar?.toUpperCase() || 'GUIDE'}</div> <div className="text-xs font-semibold px-2 py-1 rounded bg-gray-100 text-gray-600">{p.pillar?.toUpperCase() || 'GUIDE'}</div>
<div className="text-xs text-gray-400">{p.date}</div> <div className="text-xs text-gray-400">{new Date(p.datePublished || p.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</div>
</div> </div>
<div className="text-xl font-bold text-gray-900 mb-2 group-hover:text-blue-700 line-clamp-2">{p.title}</div> <div className="text-xl font-bold text-gray-900 mb-2 group-hover:text-blue-700 line-clamp-2">{p.title}</div>
<div className="text-gray-600 line-clamp-2">{p.description}</div> <div className="text-gray-600 line-clamp-2">{p.description}</div>

View File

@ -1,265 +1,265 @@
'use client'; 'use client';
import React, { useState, useRef } from 'react'; import React, { useState, useRef } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { QRCodeSVG } from 'qrcode.react'; import { QRCodeSVG } from 'qrcode.react';
import { import {
Phone, Phone,
Download, Download,
Check, Check,
Sparkles, Sparkles,
MessageCircle, MessageCircle,
Send Send
} from 'lucide-react'; } from 'lucide-react';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Textarea } from '@/components/ui/Textarea'; import { Textarea } from '@/components/ui/Textarea';
// Brand Colors // Brand Colors
const BRAND = { const BRAND = {
paleGrey: '#EBEBDF', paleGrey: '#EBEBDF',
richBlue: '#1A1265', richBlue: '#1A1265',
richBlueLight: '#2A2275', richBlueLight: '#2A2275',
}; };
// QR Color Options - WhatsApp Theme // QR Color Options - WhatsApp Theme
const QR_COLORS = [ const QR_COLORS = [
{ name: 'WhatsApp Grün', value: '#25D366' }, { name: 'WhatsApp Grün', value: '#25D366' },
{ name: 'Teal', value: '#128C7E' }, { name: 'Teal', value: '#128C7E' },
{ name: 'Klassisches Schwarz', value: '#000000' }, { name: 'Klassisches Schwarz', value: '#000000' },
{ name: 'Sattes Blau', value: '#1A1265' }, { name: 'Sattes Blau', value: '#1A1265' },
{ name: 'Lila', value: '#7C3AED' }, { name: 'Lila', value: '#7C3AED' },
{ name: 'Smaragd', value: '#10B981' }, { name: 'Smaragd', value: '#10B981' },
{ name: 'Rose', value: '#F43F5E' }, { name: 'Rose', value: '#F43F5E' },
]; ];
// Frame Options // Frame Options
const FRAME_OPTIONS = [ const FRAME_OPTIONS = [
{ id: 'none', label: 'Kein Rahmen' }, { id: 'none', label: 'Kein Rahmen' },
{ id: 'scanme', label: 'Scan Mich' }, { id: 'scanme', label: 'Scan Mich' },
{ id: 'chat', label: 'Chat starten' }, { id: 'chat', label: 'Chat starten' },
{ id: 'support', label: 'Support' }, { id: 'support', label: 'Support' },
]; ];
export default function WhatsAppGeneratorDE() { export default function WhatsAppGeneratorDE() {
const [phone, setPhone] = useState(''); const [phone, setPhone] = useState('');
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [qrColor, setQrColor] = useState('#25D366'); const [qrColor, setQrColor] = useState('#25D366');
const [frameType, setFrameType] = useState('none'); const [frameType, setFrameType] = useState('none');
const qrRef = useRef<HTMLDivElement>(null); const qrRef = useRef<HTMLDivElement>(null);
// WhatsApp URL: https://wa.me/number?text=message // WhatsApp URL: https://wa.me/number?text=message
const getUrl = () => { const getUrl = () => {
const cleanPhone = phone.replace(/\D/g, ''); // Remove non-digits const cleanPhone = phone.replace(/\D/g, ''); // Remove non-digits
const encodedMessage = encodeURIComponent(message); const encodedMessage = encodeURIComponent(message);
return `https://wa.me/${cleanPhone}?text=${encodedMessage}`; return `https://wa.me/${cleanPhone}?text=${encodedMessage}`;
}; };
const handleDownload = async (format: 'png' | 'svg') => { const handleDownload = async (format: 'png' | 'svg') => {
if (!qrRef.current) return; if (!qrRef.current) return;
try { try {
if (format === 'png') { if (format === 'png') {
const { toPng } = await import('html-to-image'); const { toPng } = await import('html-to-image');
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3 }); const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3 });
const link = document.createElement('a'); const link = document.createElement('a');
link.download = `whatsapp-qr-code.png`; link.download = `whatsapp-qr-code.png`;
link.href = dataUrl; link.href = dataUrl;
link.click(); link.click();
} else { } else {
const svgData = qrRef.current.querySelector('svg')?.outerHTML; const svgData = qrRef.current.querySelector('svg')?.outerHTML;
if (svgData) { if (svgData) {
const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' }); const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const link = document.createElement('a'); const link = document.createElement('a');
link.href = url; link.href = url;
link.download = `whatsapp-qr-code.svg`; link.download = `whatsapp-qr-code.svg`;
link.click(); link.click();
} }
} }
} catch (err) { } catch (err) {
console.error('Download failed', err); console.error('Download failed', err);
} }
}; };
const getFrameLabel = () => { const getFrameLabel = () => {
const frame = FRAME_OPTIONS.find(f => f.id === frameType); const frame = FRAME_OPTIONS.find(f => f.id === frameType);
return frame?.id !== 'none' ? frame?.label : null; return frame?.id !== 'none' ? frame?.label : null;
}; };
return ( return (
<div className="w-full max-w-5xl mx-auto px-4 md:px-6"> <div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */} {/* Main Generator Card */}
<div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100"> <div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100">
<div className="grid lg:grid-cols-2"> <div className="grid lg:grid-cols-2">
{/* LEFT: Input Section */} {/* LEFT: Input Section */}
<div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100"> <div className="p-6 md:p-8 lg:p-10 space-y-8 border-r border-slate-100">
{/* WhatsApp Details */} {/* WhatsApp Details */}
<div className="space-y-6"> <div className="space-y-6">
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2"> <h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
<MessageCircle className="w-5 h-5 text-[#25D366]" /> <MessageCircle className="w-5 h-5 text-[#25D366]" />
WhatsApp Details WhatsApp Details
</h2> </h2>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-2">Telefonnummer</label> <label className="block text-sm font-medium text-slate-700 mb-2">Telefonnummer</label>
<Input <Input
placeholder="4915112345678" placeholder="4915112345678"
value={phone} value={phone}
onChange={(e) => setPhone(e.target.value)} onChange={(e) => setPhone(e.target.value)}
className="h-12 text-base rounded-xl border-slate-200 focus:border-[#25D366] focus:ring-[#25D366]" className="h-12 text-base rounded-xl border-slate-200 focus:border-[#25D366] focus:ring-[#25D366]"
/> />
<p className="text-xs text-slate-600 mt-2">Mit Ländervorwahl (z.B. 49 für DE). Kein '+' Symbol.</p> <p className="text-xs text-slate-600 mt-2">Mit Ländervorwahl (z.B. 49 für DE). Kein '+' Symbol.</p>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-2">Vorgefertigte Nachricht (Optional)</label> <label className="block text-sm font-medium text-slate-700 mb-2">Vorgefertigte Nachricht (Optional)</label>
<Textarea <Textarea
placeholder="Hallo, ich habe Interesse an Ihren Dienstleistungen..." placeholder="Hallo, ich habe Interesse an Ihren Dienstleistungen..."
value={message} value={message}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setMessage(e.target.value)} onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setMessage(e.target.value)}
className="h-24 p-4 text-base rounded-xl border-slate-200 focus:border-[#25D366] focus:ring-[#25D366] resize-none" className="h-24 p-4 text-base rounded-xl border-slate-200 focus:border-[#25D366] focus:ring-[#25D366] resize-none"
/> />
</div> </div>
</div> </div>
<div className="border-t border-slate-100"></div> <div className="border-t border-slate-100"></div>
{/* Design Options */} {/* Design Options */}
<div className="space-y-6"> <div className="space-y-6">
<h2 className="text-lg font-bold text-slate-900 flex items-center gap-2"> <h2 className="text-lg font-bold text-slate-900 flex items-center gap-2">
<Sparkles className="w-5 h-5 text-[#25D366]" /> <Sparkles className="w-5 h-5 text-[#25D366]" />
Design Optionen Design Optionen
</h2> </h2>
{/* Color Picker */} {/* Color Picker */}
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-3">QR Code Farbe</label> <label className="block text-sm font-medium text-slate-700 mb-3">QR Code Farbe</label>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{QR_COLORS.map((c) => ( {QR_COLORS.map((c) => (
<button <button
key={c.name} key={c.name}
onClick={() => setQrColor(c.value)} onClick={() => setQrColor(c.value)}
className={cn( className={cn(
"w-9 h-9 rounded-full border-2 flex items-center justify-center transition-all hover:scale-110", "w-9 h-9 rounded-full border-2 flex items-center justify-center transition-all hover:scale-110",
qrColor === c.value ? "border-slate-900 ring-2 ring-offset-2 ring-slate-200" : "border-white shadow-md" qrColor === c.value ? "border-slate-900 ring-2 ring-offset-2 ring-slate-200" : "border-white shadow-md"
)} )}
style={{ backgroundColor: c.value }} style={{ backgroundColor: c.value }}
aria-label={`Wähle Farbe ${c.name}`} aria-label={`Wähle Farbe ${c.name}`}
title={c.name} title={c.name}
> >
{qrColor === c.value && <Check className="w-4 h-4 text-white" strokeWidth={3} />} {qrColor === c.value && <Check className="w-4 h-4 text-white" strokeWidth={3} />}
</button> </button>
))} ))}
</div> </div>
</div> </div>
{/* Frame Selector */} {/* Frame Selector */}
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-3">Rahmen Text</label> <label className="block text-sm font-medium text-slate-700 mb-3">Rahmen Text</label>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
{FRAME_OPTIONS.map((frame) => ( {FRAME_OPTIONS.map((frame) => (
<button <button
key={frame.id} key={frame.id}
onClick={() => setFrameType(frame.id)} onClick={() => setFrameType(frame.id)}
className={cn( className={cn(
"py-2.5 px-3 rounded-lg text-sm font-medium transition-all border", "py-2.5 px-3 rounded-lg text-sm font-medium transition-all border",
frameType === frame.id frameType === frame.id
? "bg-[#25D366] text-white border-[#25D366]" ? "bg-[#25D366] text-white border-[#25D366]"
: "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300" : "bg-slate-50 text-slate-600 border-slate-200 hover:border-slate-300"
)} )}
> >
{frame.label} {frame.label}
</button> </button>
))} ))}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* RIGHT: Preview Section */} {/* RIGHT: Preview Section */}
<div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}> <div className="p-6 md:p-8 lg:p-10 flex flex-col items-center justify-center" style={{ backgroundColor: BRAND.paleGrey }}>
{/* QR Card with Frame */} {/* QR Card with Frame */}
<div <div
ref={qrRef} ref={qrRef}
className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]" className="bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center w-full max-w-[320px]"
> >
{/* Frame Label */} {/* Frame Label */}
{getFrameLabel() && ( {getFrameLabel() && (
<div <div
className="mb-5 px-8 py-2.5 rounded-full text-white font-bold text-sm tracking-widest uppercase shadow-md" className="mb-5 px-8 py-2.5 rounded-full text-white font-bold text-sm tracking-widest uppercase shadow-md"
style={{ backgroundColor: qrColor }} style={{ backgroundColor: qrColor }}
> >
{getFrameLabel()} {getFrameLabel()}
</div> </div>
)} )}
{/* QR Code */} {/* QR Code */}
<div className="bg-white"> <div className="bg-white">
<QRCodeSVG <QRCodeSVG
value={getUrl()} value={getUrl()}
size={240} size={240}
level="M" level="M"
includeMargin={false} includeMargin={false}
fgColor={qrColor} fgColor={qrColor}
/> />
</div> </div>
{/* Info Preview */} {/* Info Preview */}
<div className="mt-6 text-center max-w-[260px]"> <div className="mt-6 text-center max-w-[260px]">
<h3 className="font-bold text-slate-900 text-lg flex items-center justify-center gap-2 truncate"> <h3 className="font-bold text-slate-900 text-lg flex items-center justify-center gap-2 truncate">
<Phone className="w-4 h-4 text-slate-400 shrink-0" /> <Phone className="w-4 h-4 text-slate-400 shrink-0" />
<span className="truncate">{phone ? `+${phone}` : 'Nummer'}</span> <span className="truncate">{phone ? `+${phone}` : 'Nummer'}</span>
</h3> </h3>
<div className="text-xs text-slate-600 mt-1">Startet WhatsApp Chat</div> <div className="text-xs text-slate-600 mt-1">Startet WhatsApp Chat</div>
</div> </div>
</div> </div>
{/* Download Buttons */} {/* Download Buttons */}
<div className="flex items-center gap-3 mt-8"> <div className="flex items-center gap-3 mt-8">
<Button <Button
onClick={() => handleDownload('png')} onClick={() => handleDownload('png')}
className="bg-[#25D366] hover:bg-[#128C7E] text-white shadow-lg" className="bg-[#25D366] hover:bg-[#128C7E] text-white shadow-lg"
> >
<Download className="w-4 h-4 mr-2" /> <Download className="w-4 h-4 mr-2" />
PNG Downloaden PNG Downloaden
</Button> </Button>
<Button <Button
onClick={() => handleDownload('svg')} onClick={() => handleDownload('svg')}
variant="outline" variant="outline"
className="border-slate-300 hover:bg-white" className="border-slate-300 hover:bg-white"
> >
<Download className="w-4 h-4 mr-2" /> <Download className="w-4 h-4 mr-2" />
SVG SVG
</Button> </Button>
</div> </div>
<p className="text-xs text-slate-600 mt-4 text-center"> <p className="text-xs text-slate-600 mt-4 text-center">
Der Scan startet sofort einen Chat mit dieser Nummer. Der Scan startet sofort einen Chat mit dieser Nummer.
</p> </p>
</div> </div>
</div> </div>
</div> </div>
{/* Upsell Banner */} {/* Upsell Banner */}
<div className="mt-8 bg-gradient-to-r from-[#128C7E] to-[#25D366] rounded-2xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4"> <div className="mt-8 bg-gradient-to-r from-[#128C7E] to-[#25D366] rounded-2xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="text-white text-center sm:text-left"> <div className="text-white text-center sm:text-left">
<h3 className="font-bold text-lg">WhatsApp für Business nutzen?</h3> <h3 className="font-bold text-lg">WhatsApp für Business nutzen?</h3>
<p className="text-white/80 text-sm mt-1"> <p className="text-white/80 text-sm mt-1">
Analysieren Sie Kundenkontakte mit unseren Pro-Statistiken für dynamische QR Codes. Analysieren Sie Kundenkontakte mit unseren Pro-Statistiken für dynamische QR Codes.
</p> </p>
</div> </div>
<Link href="/signup"> <Link href="/signup">
<Button className="bg-white text-[#128C7E] hover:bg-slate-100 shrink-0 shadow-lg"> <Button className="bg-white text-[#128C7E] hover:bg-slate-100 shrink-0 shadow-lg">
Jetzt Business-Analysen starten Jetzt Business-Analysen starten
</Button> </Button>
</Link> </Link>
</div> </div>
</div> </div>
); );
} }