11 seo pages

This commit is contained in:
2026-07-06 21:50:34 +02:00
committed by Timo
parent 192c186027
commit 68b2ac0089
139 changed files with 30953 additions and 56321 deletions

View File

@@ -1,35 +1,41 @@
"use client";
import React from 'react';
import sanitizeHtml from 'sanitize-html';
import type { FAQItem } from "@/lib/types";
type Props = { items: FAQItem[]; title?: string };
export function FAQSection({ items, title = "Frequently Asked Questions" }: Props) {
if (!items?.length) return null;
return (
<section className="rounded-xl border border-gray-100 bg-gray-50/50 p-6 my-8">
<h2 className="text-xl font-bold text-gray-900 mb-6">{title}</h2>
<div className="space-y-4">
{items.map((f) => {
const cleanAnswer = sanitizeHtml(f.answer, {
allowedTags: ['p', 'strong', 'em', 'ul', 'ol', 'li', 'a', 'br', 'code'],
allowedAttributes: { 'a': ['href'] }
});
return (
<details key={f.question} className="group rounded-lg border border-gray-200 bg-white p-4 open:shadow-sm open:border-blue-200 transition-all">
<summary className="cursor-pointer font-semibold text-gray-800 flex justify-between items-center group-open:text-blue-700">
{f.question}
<span className="text-gray-400 group-open:rotate-180 transition-transform"></span>
</summary>
<div className="prose max-w-none mt-3 text-gray-600 border-t border-gray-100 pt-3" dangerouslySetInnerHTML={{ __html: cleanAnswer }} />
</details>
);
})}
</div>
</section>
);
}
"use client";
import React from 'react';
import sanitizeHtml from 'sanitize-html';
import type { FAQItem } from "@/lib/types";
type Props = { items: FAQItem[]; title?: string };
export function FAQSection({ items, title = "Frequently Asked Questions" }: Props) {
if (!items?.length) return null;
return (
<section className="rounded-2xl border border-slate-200/60 bg-white p-6 sm:p-8 my-12 shadow-sm">
<h2 className="text-2xl font-semibold text-slate-900 mb-8 tracking-tight">{title}</h2>
<div className="divide-y divide-slate-100">
{items.map((f) => {
const cleanAnswer = sanitizeHtml(f.answer, {
allowedTags: ['p', 'strong', 'em', 'ul', 'ol', 'li', 'a', 'br', 'code'],
allowedAttributes: { 'a': ['href'] }
});
return (
<details key={f.question} className="group py-5 first:pt-0 last:pb-0 transition-all">
<summary className="list-none [&::-webkit-details-marker]:hidden cursor-pointer font-semibold text-slate-800 flex justify-between items-center group-open:text-indigo-600 hover:text-indigo-600 transition-colors">
<span className="pr-4 leading-snug">{f.question}</span>
<span className="relative flex h-5 w-5 shrink-0 items-center justify-center text-slate-400 group-open:text-indigo-650 transition-colors">
<span className="absolute h-3.5 w-0.5 bg-current transition-transform duration-300 group-open:rotate-90" />
<span className="absolute h-0.5 w-3.5 bg-current" />
</span>
</summary>
<div
className="prose max-w-none mt-3.5 text-slate-500 text-sm leading-relaxed border-t border-slate-50 pt-3.5"
dangerouslySetInnerHTML={{ __html: cleanAnswer }}
/>
</details>
);
})}
</div>
</section>
);
}

View File

@@ -1,171 +1,171 @@
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { X } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { Card, CardContent } from '@/components/ui/Card';
import { appendRedirectParam } from '@/lib/auth-flow';
import {
getChecklistItems,
ONBOARDING_CHECKLIST_DISMISS_KEY,
ONBOARDING_DOWNLOAD_COMPLETE_EVENT,
ONBOARDING_DOWNLOAD_COMPLETE_KEY,
} from '@/lib/revops';
type ChecklistState = {
id?: string;
signupSourceSelfReported?: string | null;
primaryUseCase?: string | null;
firstQrCreatedAt?: string | null;
firstDynamicQrAt?: string | null;
firstScanAt?: string | null;
activationAt?: string | null;
onboardingCompletedAt?: string | null;
};
type OnboardingChecklistProps = {
state: ChecklistState | null;
};
function buildChecklistItems(state: ChecklistState, downloadDone: boolean) {
return getChecklistItems(state).map((item) =>
item.id === 'download'
? {
...item,
done: downloadDone || Boolean(state.firstScanAt),
}
: item
);
}
export function OnboardingChecklist({ state }: OnboardingChecklistProps) {
const [dismissed, setDismissed] = useState(false);
const [downloadDone, setDownloadDone] = useState(false);
const [storageReady, setStorageReady] = useState(false);
const stepMap: Record<string, number> = {
source: 1,
'use-case': 2,
'first-qr': 7,
'first-dynamic': 7,
download: 8,
scan: 8,
};
useEffect(() => {
setDismissed(localStorage.getItem(ONBOARDING_CHECKLIST_DISMISS_KEY) === '1');
setDownloadDone(localStorage.getItem(ONBOARDING_DOWNLOAD_COMPLETE_KEY) === '1');
setStorageReady(true);
const syncDownloadState = () => {
setDownloadDone(localStorage.getItem(ONBOARDING_DOWNLOAD_COMPLETE_KEY) === '1');
};
window.addEventListener(ONBOARDING_DOWNLOAD_COMPLETE_EVENT, syncDownloadState);
window.addEventListener('storage', syncDownloadState);
return () => {
window.removeEventListener(ONBOARDING_DOWNLOAD_COMPLETE_EVENT, syncDownloadState);
window.removeEventListener('storage', syncDownloadState);
};
}, []);
const items = state ? buildChecklistItems(state, downloadDone) : [];
const completed = items.filter((item) => item.done).length;
const allDone = items.length > 0 && completed === items.length;
useEffect(() => {
if (!storageReady || !state) {
return;
}
if (state.onboardingCompletedAt || allDone) {
localStorage.setItem(ONBOARDING_CHECKLIST_DISMISS_KEY, '1');
setDismissed(true);
}
}, [allDone, state, storageReady]);
if (!state || !storageReady || dismissed || state.onboardingCompletedAt || allDone) {
return null;
}
const progress = Math.round((completed / items.length) * 100);
return (
<Card className="overflow-hidden rounded-[28px] border border-slate-200 bg-white p-0 shadow-none">
<CardContent className="space-y-5 p-5 sm:p-6">
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="max-w-2xl">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">
{allDone ? 'Onboarding complete' : 'Onboarding progress'}
</p>
<h3 className="mt-2 text-xl font-semibold tracking-tight text-slate-950">
{allDone ? 'Your first setup is complete' : 'Finish the first-run checklist'}
</h3>
<p className="mt-2 text-sm leading-6 text-slate-600">
{allDone
? 'You created, downloaded, and tested your first code. You can keep this visible or dismiss it.'
: 'Keep this minimal checklist visible until the first QR workflow is fully done.'}
</p>
</div>
<div className="flex items-center gap-3">
<div className="rounded-full border border-primary-200 bg-primary-50 px-3 py-1 text-xs font-semibold text-primary-700">
{completed}/{items.length}
</div>
<button
type="button"
aria-label="Dismiss onboarding checklist"
className="flex h-10 w-10 items-center justify-center rounded-2xl border border-slate-200 bg-white text-slate-500 transition hover:border-slate-300 hover:text-slate-900"
onClick={() => {
localStorage.setItem(ONBOARDING_CHECKLIST_DISMISS_KEY, '1');
setDismissed(true);
}}
>
<X className="h-4 w-4" />
</button>
</div>
</div>
<div className="space-y-3">
<div className="h-1.5 overflow-hidden rounded-full bg-slate-100">
<div
className="h-full rounded-full bg-primary-600 transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
<div className="grid grid-cols-3 gap-2 sm:grid-cols-6">
{items.map((item, index) => (
<Link
key={item.id}
href={appendRedirectParam('/onboarding', '/dashboard', {
step: String(stepMap[item.id] || index + 1),
})}
className={`rounded-full border px-3 py-2 text-center text-xs font-medium transition-colors ${
item.done
? 'border-primary-600 bg-primary-600 text-white'
: 'border-slate-200 bg-slate-50 text-slate-500 hover:border-primary-200 hover:bg-primary-50 hover:text-primary-700'
}`}
>
{index + 1}. {item.label}
</Link>
))}
</div>
</div>
<div className="flex flex-col gap-3 sm:flex-row">
<Button
variant="outline"
className="h-11 rounded-2xl border-slate-300 px-5 text-sm font-semibold text-slate-700"
onClick={() => {
localStorage.setItem(ONBOARDING_CHECKLIST_DISMISS_KEY, '1');
setDismissed(true);
}}
>
{allDone ? 'Hide checklist' : 'Dismiss'}
</Button>
</div>
</CardContent>
</Card>
);
}
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { X } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { Card, CardContent } from '@/components/ui/Card';
import { appendRedirectParam } from '@/lib/auth-flow';
import {
getChecklistItems,
ONBOARDING_CHECKLIST_DISMISS_KEY,
ONBOARDING_DOWNLOAD_COMPLETE_EVENT,
ONBOARDING_DOWNLOAD_COMPLETE_KEY,
} from '@/lib/revops';
type ChecklistState = {
id?: string;
signupSourceSelfReported?: string | null;
primaryUseCase?: string | null;
firstQrCreatedAt?: string | null;
firstDynamicQrAt?: string | null;
firstScanAt?: string | null;
activationAt?: string | null;
onboardingCompletedAt?: string | null;
};
type OnboardingChecklistProps = {
state: ChecklistState | null;
};
function buildChecklistItems(state: ChecklistState, downloadDone: boolean) {
return getChecklistItems(state).map((item) =>
item.id === 'download'
? {
...item,
done: downloadDone || Boolean(state.firstScanAt),
}
: item
);
}
export function OnboardingChecklist({ state }: OnboardingChecklistProps) {
const [dismissed, setDismissed] = useState(false);
const [downloadDone, setDownloadDone] = useState(false);
const [storageReady, setStorageReady] = useState(false);
const stepMap: Record<string, number> = {
source: 1,
'use-case': 2,
'first-qr': 7,
'first-dynamic': 7,
download: 8,
scan: 8,
};
useEffect(() => {
setDismissed(localStorage.getItem(ONBOARDING_CHECKLIST_DISMISS_KEY) === '1');
setDownloadDone(localStorage.getItem(ONBOARDING_DOWNLOAD_COMPLETE_KEY) === '1');
setStorageReady(true);
const syncDownloadState = () => {
setDownloadDone(localStorage.getItem(ONBOARDING_DOWNLOAD_COMPLETE_KEY) === '1');
};
window.addEventListener(ONBOARDING_DOWNLOAD_COMPLETE_EVENT, syncDownloadState);
window.addEventListener('storage', syncDownloadState);
return () => {
window.removeEventListener(ONBOARDING_DOWNLOAD_COMPLETE_EVENT, syncDownloadState);
window.removeEventListener('storage', syncDownloadState);
};
}, []);
const items = state ? buildChecklistItems(state, downloadDone) : [];
const completed = items.filter((item) => item.done).length;
const allDone = items.length > 0 && completed === items.length;
useEffect(() => {
if (!storageReady || !state) {
return;
}
if (state.onboardingCompletedAt || allDone) {
localStorage.setItem(ONBOARDING_CHECKLIST_DISMISS_KEY, '1');
setDismissed(true);
}
}, [allDone, state, storageReady]);
if (!state || !storageReady || dismissed || state.onboardingCompletedAt || allDone) {
return null;
}
const progress = Math.round((completed / items.length) * 100);
return (
<Card className="overflow-hidden rounded-[28px] border border-slate-200 bg-white p-0 shadow-none">
<CardContent className="space-y-5 p-5 sm:p-6">
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="max-w-2xl">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">
{allDone ? 'Onboarding complete' : 'Onboarding progress'}
</p>
<h3 className="mt-2 text-xl font-semibold tracking-tight text-slate-950">
{allDone ? 'Your first setup is complete' : 'Finish the first-run checklist'}
</h3>
<p className="mt-2 text-sm leading-6 text-slate-600">
{allDone
? 'You created, downloaded, and tested your first code. You can keep this visible or dismiss it.'
: 'Keep this minimal checklist visible until the first QR workflow is fully done.'}
</p>
</div>
<div className="flex items-center gap-3">
<div className="rounded-full border border-primary-200 bg-primary-50 px-3 py-1 text-xs font-semibold text-primary-700">
{completed}/{items.length}
</div>
<button
type="button"
aria-label="Dismiss onboarding checklist"
className="flex h-10 w-10 items-center justify-center rounded-2xl border border-slate-200 bg-white text-slate-500 transition hover:border-slate-300 hover:text-slate-900"
onClick={() => {
localStorage.setItem(ONBOARDING_CHECKLIST_DISMISS_KEY, '1');
setDismissed(true);
}}
>
<X className="h-4 w-4" />
</button>
</div>
</div>
<div className="space-y-3">
<div className="h-1.5 overflow-hidden rounded-full bg-slate-100">
<div
className="h-full rounded-full bg-primary-600 transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
<div className="grid grid-cols-3 gap-2 sm:grid-cols-6">
{items.map((item, index) => (
<Link
key={item.id}
href={appendRedirectParam('/onboarding', '/dashboard', {
step: String(stepMap[item.id] || index + 1),
})}
className={`rounded-full border px-3 py-2 text-center text-xs font-medium transition-colors ${
item.done
? 'border-primary-600 bg-primary-600 text-white'
: 'border-slate-200 bg-slate-50 text-slate-500 hover:border-primary-200 hover:bg-primary-50 hover:text-primary-700'
}`}
>
{index + 1}. {item.label}
</Link>
))}
</div>
</div>
<div className="flex flex-col gap-3 sm:flex-row">
<Button
variant="outline"
className="h-11 rounded-2xl border-slate-300 px-5 text-sm font-semibold text-slate-700"
onClick={() => {
localStorage.setItem(ONBOARDING_CHECKLIST_DISMISS_KEY, '1');
setDismissed(true);
}}
>
{allDone ? 'Hide checklist' : 'Dismiss'}
</Button>
</div>
</CardContent>
</Card>
);
}

View File

@@ -1,27 +1,27 @@
import React from 'react';
import { Card } from '@/components/ui/Card';
import { Check, X } from 'lucide-react';
interface ComparisonItem {
label: string;
value: boolean;
text?: string;
}
interface AnswerFirstBlockProps {
whatIsIt: string; // 2 sentences: "Was ist das?"
whenToUse: string[]; // 3 Bulletpoints: "Wann brauchst du's?"
comparison: {
leftTitle: string;
rightTitle: string;
items: ComparisonItem[];
};
howTo: {
steps: string[]; // 3 Steps: "So funktioniert's"
};
className?: string; // Add className prop
}
import { Check, X, HelpCircle, ArrowRight, Play } from 'lucide-react';
interface ComparisonItem {
label: string;
value: boolean;
text?: string;
}
interface AnswerFirstBlockProps {
whatIsIt: string; // 2 sentences: "Was ist das?"
whenToUse: string[]; // 3 Bulletpoints: "Wann brauchst du's?"
comparison: {
leftTitle: string;
rightTitle: string;
items: ComparisonItem[];
};
howTo: {
steps: string[]; // 3 Steps: "So funktioniert's"
};
className?: string; // Add className prop
}
export const AnswerFirstBlock: React.FC<AnswerFirstBlockProps> = ({
whatIsIt,
whenToUse,
@@ -32,54 +32,62 @@ export const AnswerFirstBlock: React.FC<AnswerFirstBlockProps> = ({
const leftValueFor = (item: ComparisonItem) => item.text ?? 'No';
return (
<section className={`my-8 space-y-8 ${className || ''}`} aria-label="Quick answer">
<div className="prose max-w-none">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Quick Summary</h2>
<p className="text-lg text-gray-700 leading-relaxed font-medium">
<section className={`my-12 space-y-10 ${className || ''}`} aria-label="Quick answer">
<div className="max-w-4xl space-y-4">
<h2 className="text-2xl font-semibold text-slate-900 tracking-tight -tracking-[0.02em]">Quick Summary</h2>
<p className="text-lg text-slate-600 leading-relaxed font-light">
{whatIsIt}
</p>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<Card className="p-6 bg-blue-50 border-blue-100">
<h3 className="font-semibold text-lg text-blue-900 mb-4">When to use this?</h3>
<ul className="space-y-3">
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{/* When to use card */}
<Card className="p-6 bg-white border-slate-200/60 shadow-sm rounded-xl flex flex-col h-full">
<h3 className="font-semibold text-lg text-slate-900 mb-5 flex items-center gap-2">
<HelpCircle className="w-5 h-5 text-slate-400 shrink-0" />
When to use this?
</h3>
<ul className="space-y-4 flex-1">
{whenToUse.map((item, idx) => (
<li key={idx} className="flex items-start gap-3 text-blue-800">
<span className="mt-1.5 w-1.5 h-1.5 rounded-full bg-blue-500 shrink-0" />
{item}
</li>
<li key={idx} className="flex items-start gap-3.5 text-slate-600 text-sm leading-relaxed">
<span className="mt-2 w-1.5 h-1.5 rounded-full bg-slate-350 shrink-0" />
<span>{item}</span>
</li>
))}
</ul>
</Card>
<Card className="p-6 bg-white border-slate-200 shadow-sm flex flex-col">
<h3 className="font-semibold text-lg text-slate-900 mb-6 font-sans">Comparison</h3>
<div className="space-y-4 flex-1">
{/* Comparison card (Grayed-out static column, highlighted dynamic column) */}
<Card className="p-6 bg-white border-slate-200/60 shadow-sm rounded-xl flex flex-col h-full">
<h3 className="font-semibold text-lg text-slate-900 mb-5 flex items-center gap-2">
<ArrowRight className="w-5 h-5 text-slate-400 shrink-0 rotate-[-45deg]" />
Comparison
</h3>
<div className="space-y-3.5 flex-1">
{comparison.items.map((item, idx) => (
<div key={idx} className="flex flex-col gap-2 rounded-xl border border-slate-100 bg-slate-50 p-4">
<div className="font-semibold text-sm text-slate-900">
<div key={idx} className="flex flex-col gap-2 rounded-lg border border-slate-100 bg-slate-50/50 p-3">
<div className="font-bold text-[9px] text-slate-450 uppercase tracking-wider">
{item.label}
</div>
<div className="grid grid-cols-2 gap-4 text-sm mt-1">
{/* Left Side (e.g., Static Pages) */}
<div>
<div className="text-xs text-slate-500 mb-1">{comparison.leftTitle}</div>
<div className="text-slate-600 font-medium">{leftValueFor(item)}</div>
<div className="grid grid-cols-2 gap-4 text-xs mt-0.5 items-center">
{/* Left Side (Static QR - Grayed out) */}
<div className="opacity-45 text-slate-400">
<div className="text-[9px] font-medium mb-0.5 uppercase tracking-wide">{comparison.leftTitle}</div>
<div className="text-xs font-light truncate">{leftValueFor(item)}</div>
</div>
{/* Right Side (e.g., QR Master) */}
<div>
<div className="text-xs text-blue-600 mb-1 font-medium">{comparison.rightTitle}</div>
<div className="flex items-center gap-1.5 font-semibold text-slate-900">
{/* Right Side (Dynamic QR - Highlighted in color) */}
<div className="bg-indigo-500/[0.04] border border-indigo-500/10 rounded-lg p-2 flex flex-col">
<div className="text-[9px] text-indigo-500 font-bold mb-0.5 uppercase tracking-wide">{comparison.rightTitle}</div>
<div className="flex items-center gap-1 text-xs font-bold text-slate-800">
{item.value ? (
<>
<Check className="w-4 h-4 text-emerald-500" aria-hidden="true" />
<span>Included</span>
<Check className="w-3.5 h-3.5 text-emerald-600 shrink-0" aria-hidden="true" />
<span className="text-indigo-650">Supported</span>
</>
) : (
<>
<X className="w-4 h-4 text-red-500" aria-hidden="true" />
<span className="text-slate-500">Not Included</span>
<X className="w-3.5 h-3.5 text-red-400 shrink-0" aria-hidden="true" />
<span className="text-slate-500">No</span>
</>
)}
</div>
@@ -90,17 +98,21 @@ export const AnswerFirstBlock: React.FC<AnswerFirstBlockProps> = ({
</div>
</Card>
<Card className="p-6 bg-green-50 border-green-100">
<h3 className="font-semibold text-lg text-green-900 mb-4">How it works</h3>
<ol className="space-y-4">
{/* How it works card (Spacious clean index numbers) */}
<Card className="p-6 bg-white border-slate-200/60 shadow-sm rounded-xl flex flex-col h-full">
<h3 className="font-semibold text-lg text-slate-900 mb-5 flex items-center gap-2">
<Play className="w-5 h-5 text-slate-400 shrink-0" />
How it works
</h3>
<ol className="space-y-6 flex-1">
{howTo.steps.map((step, idx) => (
<li key={idx} className="flex gap-3 text-green-800">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-green-200 text-green-700 flex items-center justify-center text-sm font-bold">
{idx + 1}
</span>
<span>{step}</span>
</li>
))}
<li key={idx} className="flex items-start gap-4">
<span className="text-sm font-bold text-slate-400 select-none mt-0.5">
0{idx + 1}
</span>
<span className="text-sm text-slate-600 leading-relaxed">{step}</span>
</li>
))}
</ol>
</Card>
</div>

View File

@@ -1,115 +1,115 @@
'use client';
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '@/components/ui/Card';
interface FAQProps {
t: any;
}
export const FAQ: React.FC<FAQProps> = ({ t }) => {
const [openIndex, setOpenIndex] = useState<number | null>(null);
const defaultQuestions = [
'account',
'static_vs_dynamic',
'forever',
'file_type',
'analytics',
];
const questions = t?.faq?.questions
? Array.isArray(t.faq.questions)
? t.faq.questions
: Object.keys(t.faq.questions).length > 5
? Object.keys(t.faq.questions).slice(0, 12)
: defaultQuestions
: defaultQuestions;
return (
<section id="faq" className="py-16 bg-gray-50">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="text-center mb-12"
>
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
{t.faq.title}
</h2>
</motion.div>
<div className="max-w-3xl mx-auto space-y-4">
{questions.map((key: string, index: number) => (
<motion.div
key={key}
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<Card
className="cursor-pointer border-gray-200 hover:border-gray-300 transition-colors"
onClick={() => setOpenIndex(openIndex === index ? null : index)}
>
<div className="p-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900">
{t.faq.questions[key]?.question ||
t.faq.questions[key]?.question ||
key}
</h3>
<svg
className={`w-5 h-5 text-gray-500 transition-transform duration-300 ${openIndex === index ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
<AnimatePresence>
{openIndex === index && (
<motion.div
initial={{ height: 0, opacity: 0, marginTop: 0 }}
animate={{ height: 'auto', opacity: 1, marginTop: 16 }}
exit={{ height: 0, opacity: 0, marginTop: 0 }}
transition={{ duration: 0.3 }}
className="overflow-hidden"
>
<div className="text-gray-600">
{t.faq.questions[key]?.answer ||
t.faq.questions[key]?.answer ||
''}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</Card>
</motion.div>
))}
</div>
<div className="text-center mt-8">
<a
href="/faq"
className="text-primary-600 hover:text-primary-700 font-medium"
>
View All Questions
</a>
</div>
</div>
</section>
);
};
'use client';
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '@/components/ui/Card';
interface FAQProps {
t: any;
}
export const FAQ: React.FC<FAQProps> = ({ t }) => {
const [openIndex, setOpenIndex] = useState<number | null>(null);
const defaultQuestions = [
'account',
'static_vs_dynamic',
'forever',
'file_type',
'analytics',
];
const questions = t?.faq?.questions
? Array.isArray(t.faq.questions)
? t.faq.questions
: Object.keys(t.faq.questions).length > 5
? Object.keys(t.faq.questions).slice(0, 12)
: defaultQuestions
: defaultQuestions;
return (
<section id="faq" className="py-16 bg-gray-50">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="text-center mb-12"
>
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
{t.faq.title}
</h2>
</motion.div>
<div className="max-w-3xl mx-auto space-y-4">
{questions.map((key: string, index: number) => (
<motion.div
key={key}
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<Card
className="cursor-pointer border-gray-200 hover:border-gray-300 transition-colors"
onClick={() => setOpenIndex(openIndex === index ? null : index)}
>
<div className="p-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900">
{t.faq.questions[key]?.question ||
t.faq.questions[key]?.question ||
key}
</h3>
<svg
className={`w-5 h-5 text-gray-500 transition-transform duration-300 ${openIndex === index ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
<AnimatePresence>
{openIndex === index && (
<motion.div
initial={{ height: 0, opacity: 0, marginTop: 0 }}
animate={{ height: 'auto', opacity: 1, marginTop: 16 }}
exit={{ height: 0, opacity: 0, marginTop: 0 }}
transition={{ duration: 0.3 }}
className="overflow-hidden"
>
<div className="text-gray-600">
{t.faq.questions[key]?.answer ||
t.faq.questions[key]?.answer ||
''}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</Card>
</motion.div>
))}
</div>
<div className="text-center mt-8">
<a
href="/faq"
className="text-primary-600 hover:text-primary-700 font-medium"
>
View All Questions
</a>
</div>
</div>
</section>
);
};

View File

@@ -1,125 +1,125 @@
'use client';
import React from 'react';
import Link from 'next/link';
import { motion, Variants } from 'framer-motion';
import {
Link as LinkIcon,
User,
Mail,
Calendar,
Facebook,
Instagram,
Phone,
MessageSquare,
Type,
Music,
Twitter,
MessageCircle,
Wifi,
Youtube,
Bitcoin,
MapPin,
CreditCard,
Video,
Users,
Barcode,
Star
} from 'lucide-react';
const TOOLS = [
{ icon: LinkIcon, name: 'URL', description: 'Open any website', href: '/tools/url-qr-code', color: 'text-blue-500', bg: 'bg-blue-50' },
{ icon: User, name: 'vCard', description: 'Share contact details', href: '/tools/vcard-qr-code', color: 'text-rose-500', bg: 'bg-rose-50' },
{ icon: Type, name: 'Text', description: 'Display plain text', href: '/tools/text-qr-code', color: 'text-slate-600', bg: 'bg-slate-50' },
{ icon: Mail, name: 'Email', description: 'Send an email', href: '/tools/email-qr-code', color: 'text-red-500', bg: 'bg-red-50' },
{ icon: MessageSquare, name: 'SMS', description: 'Send a text message', href: '/tools/sms-qr-code', color: 'text-green-500', bg: 'bg-green-50' },
{ icon: Wifi, name: 'WiFi', description: 'Connect to WiFi', href: '/tools/wifi-qr-code', color: 'text-indigo-500', bg: 'bg-indigo-50' },
{ icon: Bitcoin, name: 'Crypto', description: 'Receive payments', href: '/tools/crypto-qr-code', color: 'text-orange-500', bg: 'bg-orange-50' },
{ icon: Calendar, name: 'Event', description: 'Save calendar event', href: '/tools/event-qr-code', color: 'text-violet-500', bg: 'bg-violet-50' },
{ icon: Facebook, name: 'Facebook', description: 'Open Facebook page', href: '/tools/facebook-qr-code', color: 'text-blue-600', bg: 'bg-blue-50' },
{ icon: Instagram, name: 'Instagram', description: 'Open Instagram profile', href: '/tools/instagram-qr-code', color: 'text-pink-500', bg: 'bg-pink-50' },
{ icon: Twitter, name: 'Twitter', description: 'Open Twitter profile', href: '/tools/twitter-qr-code', color: 'text-sky-500', bg: 'bg-sky-50' },
{ icon: Youtube, name: 'YouTube', description: 'Open YouTube video', href: '/tools/youtube-qr-code', color: 'text-red-600', bg: 'bg-red-50' },
{ icon: MessageCircle, name: 'WhatsApp', description: 'Send WhatsApp message', href: '/tools/whatsapp-qr-code', color: 'text-green-600', bg: 'bg-green-50' },
{ icon: Music, name: 'TikTok', description: 'Open TikTok profile', href: '/tools/tiktok-qr-code', color: 'text-slate-900', bg: 'bg-slate-100' },
{ icon: MapPin, name: 'Location', description: 'Share GPS coordinates', href: '/tools/location-qr-code', color: 'text-emerald-500', bg: 'bg-emerald-50' },
{ icon: Phone, name: 'Call', description: 'Start a phone call', href: '/tools/phone-qr-code', color: 'text-teal-500', bg: 'bg-teal-50' },
{ icon: Barcode, name: 'Barcode', description: 'Free online barcode generator', href: '/tools/barcode-generator', color: 'text-amber-600', bg: 'bg-amber-50' },
];
const containerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.05 }
}
};
const itemVariants: Variants = {
hidden: { opacity: 0, y: 16 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.4 }
}
};
export function FreeToolsGrid() {
return (
<section id="tools" className="py-24 bg-slate-50/50 border-t border-slate-100">
<style dangerouslySetInnerHTML={{__html:`
.ftg-card { transition: transform 0.22s ease-out, box-shadow 0.22s ease-out, border-color 0.22s ease-out; }
.ftg-card:hover { transform: translateY(-8px); box-shadow: rgba(83,58,253,0.2) 0px 20px 40px -12px, rgba(0,0,0,0.08) 0px 8px 16px -8px; border-color: #533afd; }
.ftg-icon { transition: transform 0.25s ease-out; }
.ftg-card:hover .ftg-icon { transform: rotate(12deg) scale(1.1); }
@media (prefers-reduced-motion: reduce) { .ftg-card, .ftg-icon { transition: none !important; } }
`}}/>
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5 }}
className="text-center mb-16"
>
<div className="flex flex-col md:flex-row items-center justify-center gap-3 mb-4">
<h2 className="text-3xl lg:text-4xl font-bold text-slate-900">More Free QR Code Tools</h2>
<div className="bg-gradient-to-r from-emerald-500 to-green-500 text-white px-3 py-1 rounded-full text-xs md:text-sm font-semibold shadow-lg shadow-emerald-500/20 flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-white opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-white"></span>
</span>
Free Forever
</div>
</div>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
Create specialized QR codes for every need. Completely free and no signup required.
</p>
</motion.div>
<motion.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-50px" }}
className="grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-6"
>
{TOOLS.map((tool) => (
<motion.div key={tool.name} variants={itemVariants}>
<Link
href={tool.href}
className="ftg-card group flex flex-col items-center p-5 md:p-6 rounded-2xl border border-[#e5edf5] bg-white"
>
<div className={`ftg-icon w-12 h-12 md:w-14 md:h-14 rounded-xl ${tool.bg} flex items-center justify-center mb-3 md:mb-4`}>
<tool.icon className={`w-6 h-6 md:w-7 md:h-7 ${tool.color}`} aria-hidden="true" />
</div>
<h3 className="text-base md:text-lg font-semibold text-[#061b31] mb-0.5">{tool.name}</h3>
<p className="text-xs md:text-sm text-[#64748d] text-center">{tool.description}</p>
</Link>
</motion.div>
))}
</motion.div>
</div>
</section>
);
}
'use client';
import React from 'react';
import Link from 'next/link';
import { motion, Variants } from 'framer-motion';
import {
Link as LinkIcon,
User,
Mail,
Calendar,
Facebook,
Instagram,
Phone,
MessageSquare,
Type,
Music,
Twitter,
MessageCircle,
Wifi,
Youtube,
Bitcoin,
MapPin,
CreditCard,
Video,
Users,
Barcode,
Star
} from 'lucide-react';
const TOOLS = [
{ icon: LinkIcon, name: 'URL', description: 'Open any website', href: '/tools/url-qr-code', color: 'text-blue-500', bg: 'bg-blue-50' },
{ icon: User, name: 'vCard', description: 'Share contact details', href: '/tools/vcard-qr-code', color: 'text-rose-500', bg: 'bg-rose-50' },
{ icon: Type, name: 'Text', description: 'Display plain text', href: '/tools/text-qr-code', color: 'text-slate-600', bg: 'bg-slate-50' },
{ icon: Mail, name: 'Email', description: 'Send an email', href: '/tools/email-qr-code', color: 'text-red-500', bg: 'bg-red-50' },
{ icon: MessageSquare, name: 'SMS', description: 'Send a text message', href: '/tools/sms-qr-code', color: 'text-green-500', bg: 'bg-green-50' },
{ icon: Wifi, name: 'WiFi', description: 'Connect to WiFi', href: '/tools/wifi-qr-code', color: 'text-indigo-500', bg: 'bg-indigo-50' },
{ icon: Bitcoin, name: 'Crypto', description: 'Receive payments', href: '/tools/crypto-qr-code', color: 'text-orange-500', bg: 'bg-orange-50' },
{ icon: Calendar, name: 'Event', description: 'Save calendar event', href: '/tools/event-qr-code', color: 'text-violet-500', bg: 'bg-violet-50' },
{ icon: Facebook, name: 'Facebook', description: 'Open Facebook page', href: '/tools/facebook-qr-code', color: 'text-blue-600', bg: 'bg-blue-50' },
{ icon: Instagram, name: 'Instagram', description: 'Open Instagram profile', href: '/tools/instagram-qr-code', color: 'text-pink-500', bg: 'bg-pink-50' },
{ icon: Twitter, name: 'Twitter', description: 'Open Twitter profile', href: '/tools/twitter-qr-code', color: 'text-sky-500', bg: 'bg-sky-50' },
{ icon: Youtube, name: 'YouTube', description: 'Open YouTube video', href: '/tools/youtube-qr-code', color: 'text-red-600', bg: 'bg-red-50' },
{ icon: MessageCircle, name: 'WhatsApp', description: 'Send WhatsApp message', href: '/tools/whatsapp-qr-code', color: 'text-green-600', bg: 'bg-green-50' },
{ icon: Music, name: 'TikTok', description: 'Open TikTok profile', href: '/tools/tiktok-qr-code', color: 'text-slate-900', bg: 'bg-slate-100' },
{ icon: MapPin, name: 'Location', description: 'Share GPS coordinates', href: '/tools/location-qr-code', color: 'text-emerald-500', bg: 'bg-emerald-50' },
{ icon: Phone, name: 'Call', description: 'Start a phone call', href: '/tools/phone-qr-code', color: 'text-teal-500', bg: 'bg-teal-50' },
{ icon: Barcode, name: 'Barcode', description: 'Free online barcode generator', href: '/tools/barcode-generator', color: 'text-amber-600', bg: 'bg-amber-50' },
];
const containerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.05 }
}
};
const itemVariants: Variants = {
hidden: { opacity: 0, y: 16 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.4 }
}
};
export function FreeToolsGrid() {
return (
<section id="tools" className="py-24 bg-slate-50/50 border-t border-slate-100">
<style dangerouslySetInnerHTML={{__html:`
.ftg-card { transition: transform 0.22s ease-out, box-shadow 0.22s ease-out, border-color 0.22s ease-out; }
.ftg-card:hover { transform: translateY(-8px); box-shadow: rgba(83,58,253,0.2) 0px 20px 40px -12px, rgba(0,0,0,0.08) 0px 8px 16px -8px; border-color: #533afd; }
.ftg-icon { transition: transform 0.25s ease-out; }
.ftg-card:hover .ftg-icon { transform: rotate(12deg) scale(1.1); }
@media (prefers-reduced-motion: reduce) { .ftg-card, .ftg-icon { transition: none !important; } }
`}}/>
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5 }}
className="text-center mb-16"
>
<div className="flex flex-col md:flex-row items-center justify-center gap-3 mb-4">
<h2 className="text-3xl lg:text-4xl font-bold text-slate-900">More Free QR Code Tools</h2>
<div className="bg-gradient-to-r from-emerald-500 to-green-500 text-white px-3 py-1 rounded-full text-xs md:text-sm font-semibold shadow-lg shadow-emerald-500/20 flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-white opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-white"></span>
</span>
Free Forever
</div>
</div>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
Create specialized QR codes for every need. Completely free and no signup required.
</p>
</motion.div>
<motion.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-50px" }}
className="grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-6"
>
{TOOLS.map((tool) => (
<motion.div key={tool.name} variants={itemVariants}>
<Link
href={tool.href}
className="ftg-card group flex flex-col items-center p-5 md:p-6 rounded-2xl border border-[#e5edf5] bg-white"
>
<div className={`ftg-icon w-12 h-12 md:w-14 md:h-14 rounded-xl ${tool.bg} flex items-center justify-center mb-3 md:mb-4`}>
<tool.icon className={`w-6 h-6 md:w-7 md:h-7 ${tool.color}`} aria-hidden="true" />
</div>
<h3 className="text-base md:text-lg font-semibold text-[#061b31] mb-0.5">{tool.name}</h3>
<p className="text-xs md:text-sm text-[#64748d] text-center">{tool.description}</p>
</Link>
</motion.div>
))}
</motion.div>
</div>
</section>
);
}

View File

@@ -1,227 +1,227 @@
'use client';
import React from 'react';
import Link from 'next/link';
import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card';
import { motion } from 'framer-motion';
import { Globe, User, MapPin, Phone, FileText, Ticket, Smartphone, Star } from 'lucide-react';
import { useState, useEffect } from 'react';
const PRODUCT_HUNT_URL =
'https://www.producthunt.com/products/qr-master-2?launch=qr-master-3';
const PRODUCT_HUNT_BADGE_URL =
'/producthunt-featured.svg';
const FlippingCard = ({ front, back, delay }: { front: any, back: any, delay: number }) => {
const [isFlipped, setIsFlipped] = useState(false);
useEffect(() => {
const initialTimeout = setTimeout(() => {
setIsFlipped(true);
const interval = setInterval(() => {
setIsFlipped(prev => !prev);
}, 8000);
return () => clearInterval(interval);
}, delay * 1000);
return () => clearTimeout(initialTimeout);
}, [delay]);
return (
<div className="relative h-32 w-full perspective-[1000px] group cursor-pointer">
<motion.div
animate={{ rotateY: isFlipped ? 180 : 0 }}
transition={{ duration: 0.6, type: "spring", stiffness: 260, damping: 20 }}
className="relative w-full h-full preserve-3d"
style={{ transformStyle: 'preserve-3d' }}
>
<div
className="absolute inset-0 backface-hidden"
style={{ backfaceVisibility: 'hidden', WebkitBackfaceVisibility: 'hidden' }}
>
<Card className="w-full h-full backdrop-blur-xl bg-white/70 border-white/50 shadow-xl shadow-gray-200/50 p-4 flex flex-col items-center justify-center hover:scale-105 transition-all duration-300">
<div className={`w-10 h-10 mb-3 rounded-xl ${front.color} flex items-center justify-center`}>
<front.icon className="w-5 h-5" />
</div>
<p className="font-semibold text-gray-800 text-sm">{front.title}</p>
</Card>
</div>
<div
className="absolute inset-0 backface-hidden"
style={{
backfaceVisibility: 'hidden',
WebkitBackfaceVisibility: 'hidden',
transform: 'rotateY(180deg)'
}}
>
<Card className="w-full h-full backdrop-blur-xl bg-white/80 border-white/60 shadow-xl shadow-blue-200/50 p-4 flex flex-col items-center justify-center hover:scale-105 transition-all duration-300">
<div className={`w-10 h-10 mb-3 rounded-xl ${back.color} flex items-center justify-center`}>
<back.icon className="w-5 h-5" />
</div>
<p className="font-semibold text-gray-900 text-sm">{back.title}</p>
</Card>
</div>
</motion.div>
</div>
);
};
interface HeroProps {
t: any;
headingAs?: 'h1' | 'div';
}
export const Hero: React.FC<HeroProps> = ({ t, headingAs = 'h1' }) => {
const HeadingTag = headingAs;
return (
<section className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-white to-purple-50 pt-12 pb-20">
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<style dangerouslySetInnerHTML={{__html:`
@keyframes dotFloat{0%{transform:translateY(0) translateX(0);opacity:0}20%{opacity:1}80%{opacity:1}100%{transform:translateY(calc(var(--dy)*1px)) translateX(calc(var(--dx)*1px));opacity:0}}
.hero-dot{position:absolute;border-radius:50%;animation:dotFloat linear infinite;}
@media(prefers-reduced-motion:reduce){.hero-dot{animation:none}}
`}}/>
{([
{size:4,x:15,y:80,dx:-30,dy:-120,dur:12,delay:0,color:'rgba(96,165,250,0.5)'},
{size:3,x:35,y:90,dx:20,dy:-100,dur:15,delay:-3,color:'rgba(167,139,250,0.4)'},
{size:5,x:55,y:85,dx:-10,dy:-130,dur:10,delay:-6,color:'rgba(96,165,250,0.3)'},
{size:2,x:70,y:95,dx:30,dy:-110,dur:18,delay:-2,color:'rgba(192,132,252,0.5)'},
{size:4,x:85,y:80,dx:-20,dy:-90,dur:14,delay:-8,color:'rgba(147,197,253,0.4)'},
{size:3,x:25,y:70,dx:15,dy:-140,dur:11,delay:-5,color:'rgba(216,180,254,0.4)'},
{size:6,x:60,y:75,dx:-25,dy:-100,dur:16,delay:-1,color:'rgba(96,165,250,0.25)'},
{size:2,x:45,y:88,dx:10,dy:-120,dur:13,delay:-9,color:'rgba(167,139,250,0.5)'},
{size:5,x:80,y:60,dx:-15,dy:-80,dur:20,delay:-4,color:'rgba(99,102,241,0.3)'},
{size:3,x:10,y:50,dx:25,dy:-110,dur:17,delay:-7,color:'rgba(192,132,252,0.35)'},
] as {size:number,x:number,y:number,dx:number,dy:number,dur:number,delay:number,color:string}[]).map((p,i)=>(
<div key={i} className="hero-dot" style={{
width:p.size,height:p.size,
left:`${p.x}%`,top:`${p.y}%`,
background:p.color,
'--dx':p.dx,'--dy':p.dy,
animationDuration:`${p.dur}s`,
animationDelay:`${p.delay}s`,
} as React.CSSProperties}/>
))}
</div>
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl relative z-10">
<div className="grid lg:grid-cols-2 gap-12 items-center">
{/* Left Content */}
<div className="space-y-6">
<div className="flex items-center gap-2.5">
<div className="w-6 h-0.5 bg-[#533afd] shrink-0" />
<span className="text-[10px] font-semibold tracking-[0.1em] uppercase text-[#533afd]">{t.hero.badge}</span>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="space-y-4"
>
<HeadingTag
className="font-extrabold text-[#061b31] leading-[0.92]"
style={{ fontSize: 'clamp(2.75rem, 6vw, 5rem)' }}
>
{t.hero.title}
</HeadingTag>
<p className="text-[0.9375rem] text-[#64748d] leading-[1.55] max-w-[44ch]">
{t.hero.subtitle}
</p>
<div className="flex flex-col gap-1.5 pt-1">
{t.hero.features.map((feature: string, index: number) => (
<motion.div
key={index}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 + (index * 0.1) }}
className="flex items-center gap-2"
>
<div className="w-1 h-1 rounded-full bg-[#533afd] shrink-0" />
<span className="text-[13px] text-[#273951] font-medium">{feature}</span>
</motion.div>
))}
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="flex gap-4 items-center flex-wrap pt-1"
>
<Link href="/signup">
<Button size="lg" style={{ padding: '14px 36px', fontSize: '1.0625rem', boxShadow: 'rgba(83,58,253,0.3) 0px 10px 28px -8px' }}>
{t.hero.cta_primary}
</Button>
</Link>
<Link
href="/#pricing"
style={{ fontSize: '0.875rem', fontWeight: 500, color: '#533afd', textDecoration: 'underline', textUnderlineOffset: '3px' }}
>
{t.hero.cta_secondary}
</Link>
</motion.div>
<motion.a
href={PRODUCT_HUNT_URL}
target="_blank"
rel="noopener noreferrer"
aria-label="View QR Master on Product Hunt"
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.65 }}
className="inline-flex rounded-[10px] transition-transform duration-200 hover:-translate-y-0.5 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#533afd]"
>
<img
src={PRODUCT_HUNT_BADGE_URL}
alt="QR Master - Dynamic QR codes with analytics and editable links | Product Hunt"
width="250"
height="54"
className="h-[54px] w-[250px]"
/>
</motion.a>
</div>
{/* Right Preview Widget */}
<div className="relative">
<div className="relative perspective-[1000px]">
<div className="grid grid-cols-2 gap-4">
{[
{
front: { title: 'URL/Website', color: 'bg-blue-500/10 text-blue-600', icon: Globe },
back: { title: 'PDF / Menu', color: 'bg-orange-500/10 text-orange-600', icon: FileText },
delay: 3
},
{
front: { title: 'Contact Card', color: 'bg-purple-500/10 text-purple-600', icon: User },
back: { title: 'Coupon / Deals', color: 'bg-red-500/10 text-red-600', icon: Ticket },
delay: 5
},
{
front: { title: 'Location', color: 'bg-green-500/10 text-green-600', icon: MapPin },
back: { title: 'App Store', color: 'bg-sky-500/10 text-sky-600', icon: Smartphone },
delay: 7
},
{
front: { title: 'Phone Number', color: 'bg-pink-500/10 text-pink-600', icon: Phone },
back: { title: 'Feedback', color: 'bg-yellow-500/10 text-yellow-600', icon: Star },
delay: 9
},
].map((card, index) => (
<FlippingCard key={index} {...card} />
))}
</div>
</div>
</div>
</div>
</div>
<div className="absolute bottom-0 left-0 w-full h-32 bg-gradient-to-b from-transparent to-gray-50 pointer-events-none" />
</section>
);
};
'use client';
import React from 'react';
import Link from 'next/link';
import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card';
import { motion } from 'framer-motion';
import { Globe, User, MapPin, Phone, FileText, Ticket, Smartphone, Star } from 'lucide-react';
import { useState, useEffect } from 'react';
const PRODUCT_HUNT_URL =
'https://www.producthunt.com/products/qr-master-2?launch=qr-master-3';
const PRODUCT_HUNT_BADGE_URL =
'/producthunt-featured.svg';
const FlippingCard = ({ front, back, delay }: { front: any, back: any, delay: number }) => {
const [isFlipped, setIsFlipped] = useState(false);
useEffect(() => {
const initialTimeout = setTimeout(() => {
setIsFlipped(true);
const interval = setInterval(() => {
setIsFlipped(prev => !prev);
}, 8000);
return () => clearInterval(interval);
}, delay * 1000);
return () => clearTimeout(initialTimeout);
}, [delay]);
return (
<div className="relative h-32 w-full perspective-[1000px] group cursor-pointer">
<motion.div
animate={{ rotateY: isFlipped ? 180 : 0 }}
transition={{ duration: 0.6, type: "spring", stiffness: 260, damping: 20 }}
className="relative w-full h-full preserve-3d"
style={{ transformStyle: 'preserve-3d' }}
>
<div
className="absolute inset-0 backface-hidden"
style={{ backfaceVisibility: 'hidden', WebkitBackfaceVisibility: 'hidden' }}
>
<Card className="w-full h-full backdrop-blur-xl bg-white/70 border-white/50 shadow-xl shadow-gray-200/50 p-4 flex flex-col items-center justify-center hover:scale-105 transition-all duration-300">
<div className={`w-10 h-10 mb-3 rounded-xl ${front.color} flex items-center justify-center`}>
<front.icon className="w-5 h-5" />
</div>
<p className="font-semibold text-gray-800 text-sm">{front.title}</p>
</Card>
</div>
<div
className="absolute inset-0 backface-hidden"
style={{
backfaceVisibility: 'hidden',
WebkitBackfaceVisibility: 'hidden',
transform: 'rotateY(180deg)'
}}
>
<Card className="w-full h-full backdrop-blur-xl bg-white/80 border-white/60 shadow-xl shadow-blue-200/50 p-4 flex flex-col items-center justify-center hover:scale-105 transition-all duration-300">
<div className={`w-10 h-10 mb-3 rounded-xl ${back.color} flex items-center justify-center`}>
<back.icon className="w-5 h-5" />
</div>
<p className="font-semibold text-gray-900 text-sm">{back.title}</p>
</Card>
</div>
</motion.div>
</div>
);
};
interface HeroProps {
t: any;
headingAs?: 'h1' | 'div';
}
export const Hero: React.FC<HeroProps> = ({ t, headingAs = 'h1' }) => {
const HeadingTag = headingAs;
return (
<section className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-white to-purple-50 pt-12 pb-20">
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<style dangerouslySetInnerHTML={{__html:`
@keyframes dotFloat{0%{transform:translateY(0) translateX(0);opacity:0}20%{opacity:1}80%{opacity:1}100%{transform:translateY(calc(var(--dy)*1px)) translateX(calc(var(--dx)*1px));opacity:0}}
.hero-dot{position:absolute;border-radius:50%;animation:dotFloat linear infinite;}
@media(prefers-reduced-motion:reduce){.hero-dot{animation:none}}
`}}/>
{([
{size:4,x:15,y:80,dx:-30,dy:-120,dur:12,delay:0,color:'rgba(96,165,250,0.5)'},
{size:3,x:35,y:90,dx:20,dy:-100,dur:15,delay:-3,color:'rgba(167,139,250,0.4)'},
{size:5,x:55,y:85,dx:-10,dy:-130,dur:10,delay:-6,color:'rgba(96,165,250,0.3)'},
{size:2,x:70,y:95,dx:30,dy:-110,dur:18,delay:-2,color:'rgba(192,132,252,0.5)'},
{size:4,x:85,y:80,dx:-20,dy:-90,dur:14,delay:-8,color:'rgba(147,197,253,0.4)'},
{size:3,x:25,y:70,dx:15,dy:-140,dur:11,delay:-5,color:'rgba(216,180,254,0.4)'},
{size:6,x:60,y:75,dx:-25,dy:-100,dur:16,delay:-1,color:'rgba(96,165,250,0.25)'},
{size:2,x:45,y:88,dx:10,dy:-120,dur:13,delay:-9,color:'rgba(167,139,250,0.5)'},
{size:5,x:80,y:60,dx:-15,dy:-80,dur:20,delay:-4,color:'rgba(99,102,241,0.3)'},
{size:3,x:10,y:50,dx:25,dy:-110,dur:17,delay:-7,color:'rgba(192,132,252,0.35)'},
] as {size:number,x:number,y:number,dx:number,dy:number,dur:number,delay:number,color:string}[]).map((p,i)=>(
<div key={i} className="hero-dot" style={{
width:p.size,height:p.size,
left:`${p.x}%`,top:`${p.y}%`,
background:p.color,
'--dx':p.dx,'--dy':p.dy,
animationDuration:`${p.dur}s`,
animationDelay:`${p.delay}s`,
} as React.CSSProperties}/>
))}
</div>
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl relative z-10">
<div className="grid lg:grid-cols-2 gap-12 items-center">
{/* Left Content */}
<div className="space-y-6">
<div className="flex items-center gap-2.5">
<div className="w-6 h-0.5 bg-[#533afd] shrink-0" />
<span className="text-[10px] font-semibold tracking-[0.1em] uppercase text-[#533afd]">{t.hero.badge}</span>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="space-y-4"
>
<HeadingTag
className="font-extrabold text-[#061b31] leading-[0.92]"
style={{ fontSize: 'clamp(2.75rem, 6vw, 5rem)' }}
>
{t.hero.title}
</HeadingTag>
<p className="text-[0.9375rem] text-[#64748d] leading-[1.55] max-w-[44ch]">
{t.hero.subtitle}
</p>
<div className="flex flex-col gap-1.5 pt-1">
{t.hero.features.map((feature: string, index: number) => (
<motion.div
key={index}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 + (index * 0.1) }}
className="flex items-center gap-2"
>
<div className="w-1 h-1 rounded-full bg-[#533afd] shrink-0" />
<span className="text-[13px] text-[#273951] font-medium">{feature}</span>
</motion.div>
))}
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="flex gap-4 items-center flex-wrap pt-1"
>
<Link href="/signup">
<Button size="lg" style={{ padding: '14px 36px', fontSize: '1.0625rem', boxShadow: 'rgba(83,58,253,0.3) 0px 10px 28px -8px' }}>
{t.hero.cta_primary}
</Button>
</Link>
<Link
href="/#pricing"
style={{ fontSize: '0.875rem', fontWeight: 500, color: '#533afd', textDecoration: 'underline', textUnderlineOffset: '3px' }}
>
{t.hero.cta_secondary}
</Link>
</motion.div>
<motion.a
href={PRODUCT_HUNT_URL}
target="_blank"
rel="noopener noreferrer"
aria-label="View QR Master on Product Hunt"
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.65 }}
className="inline-flex rounded-[10px] transition-transform duration-200 hover:-translate-y-0.5 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#533afd]"
>
<img
src={PRODUCT_HUNT_BADGE_URL}
alt="QR Master - Dynamic QR codes with analytics and editable links | Product Hunt"
width="250"
height="54"
className="h-[54px] w-[250px]"
/>
</motion.a>
</div>
{/* Right Preview Widget */}
<div className="relative">
<div className="relative perspective-[1000px]">
<div className="grid grid-cols-2 gap-4">
{[
{
front: { title: 'URL/Website', color: 'bg-blue-500/10 text-blue-600', icon: Globe },
back: { title: 'PDF / Menu', color: 'bg-orange-500/10 text-orange-600', icon: FileText },
delay: 3
},
{
front: { title: 'Contact Card', color: 'bg-purple-500/10 text-purple-600', icon: User },
back: { title: 'Coupon / Deals', color: 'bg-red-500/10 text-red-600', icon: Ticket },
delay: 5
},
{
front: { title: 'Location', color: 'bg-green-500/10 text-green-600', icon: MapPin },
back: { title: 'App Store', color: 'bg-sky-500/10 text-sky-600', icon: Smartphone },
delay: 7
},
{
front: { title: 'Phone Number', color: 'bg-pink-500/10 text-pink-600', icon: Phone },
back: { title: 'Feedback', color: 'bg-yellow-500/10 text-yellow-600', icon: Star },
delay: 9
},
].map((card, index) => (
<FlippingCard key={index} {...card} />
))}
</div>
</div>
</div>
</div>
</div>
<div className="absolute bottom-0 left-0 w-full h-32 bg-gradient-to-b from-transparent to-gray-50 pointer-events-none" />
</section>
);
};

View File

@@ -1,476 +1,476 @@
import type { FAQItem } from "@/lib/types";
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import {
ArrowRight,
CheckCircle2,
Building2,
Settings2,
Smartphone,
BarChart3,
Link2,
} from "lucide-react";
import Breadcrumbs, { BreadcrumbItem } from "@/components/Breadcrumbs";
import SeoJsonLd from "@/components/SeoJsonLd";
import { FAQSection } from "@/components/aeo/FAQSection";
import {
MarketingPageTracker,
TrackedCtaLink,
} from "@/components/marketing/MarketingAnalytics";
import { AnswerFirstBlock } from "@/components/marketing/AnswerFirstBlock";
import { Button } from "@/components/ui/Button";
import { breadcrumbSchema, faqPageSchema } from "@/lib/schema";
type LinkCard = {
href: string;
title: string;
description: string;
};
type IndustryPageTemplateProps = {
title: string;
description: string;
eyebrow: string;
intro: string;
pageType: "commercial" | "use_case";
cluster: string;
useCase?: string;
breadcrumbs: BreadcrumbItem[];
answer: string;
whenToUse: string[];
comparisonItems: {
label: string;
value: boolean;
text?: string;
}[];
howToSteps: string[];
primaryCta: {
href: string;
label: string;
};
secondaryCta: {
href: string;
label: string;
};
workflowTitle: string;
workflowIntro: string;
workflowCards: {
title: string;
description: string;
}[];
checklistTitle: string;
checklist: string[];
supportLinks: LinkCard[];
faq: FAQItem[];
schemaData?: Record<string, unknown>[];
heroImage?: string;
heroImageAlt?: string;
statistics?: { value: string; label: string }[];
benefits?: { title: string; description: string }[];
};
export function buildIndustryMetadata({
title,
fallbackTitle,
description,
canonicalPath,
}: {
title: string;
fallbackTitle?: string;
description: string;
canonicalPath: string;
}): Metadata {
const canonical = `https://www.qrmaster.net${canonicalPath}`;
const brandSuffix = " | QR Master";
const maxTitleLength = 60;
const maxBaseTitleLength = maxTitleLength - brandSuffix.length;
const normalizedTitle = title.replace(/\s+\|\s+QR Master$/i, "").trim();
const fallback = fallbackTitle?.replace(/\s+\|\s+QR Master$/i, "").trim();
const candidates = [
normalizedTitle,
normalizedTitle.split(":")[0]?.trim(),
fallback,
].filter((candidate): candidate is string => Boolean(candidate));
const seoTitle =
candidates.find((candidate) => candidate.length <= maxBaseTitleLength) ??
normalizedTitle.slice(0, maxBaseTitleLength).replace(/\s+\S*$/, "").trim();
const fullTitle = `${seoTitle} | QR Master`;
return {
title: {
absolute: fullTitle,
},
description,
alternates: {
canonical,
languages: {
"x-default": canonical,
en: canonical,
},
},
openGraph: {
title: fullTitle,
description,
url: canonical,
type: "website",
images: ["/og-image.png"],
},
twitter: {
title: fullTitle,
description,
},
};
}
// Map a consistent icon for workflow cards depending on index
const WORKFLOW_ICONS = [Smartphone, Settings2, BarChart3, Building2];
export function IndustryPageTemplate({
title,
description,
eyebrow,
intro,
pageType,
cluster,
useCase,
breadcrumbs,
answer,
whenToUse,
comparisonItems,
howToSteps,
primaryCta,
secondaryCta,
workflowTitle,
workflowIntro,
workflowCards,
checklistTitle,
checklist,
supportLinks,
faq,
schemaData = [],
heroImage = "/hero-qr-scan-mockup.png",
heroImageAlt = "Industry example showing a QR code workflow in action",
statistics = [],
benefits = [],
}: IndustryPageTemplateProps) {
return (
<>
<SeoJsonLd
data={[...schemaData, breadcrumbSchema(breadcrumbs), faqPageSchema(faq)]}
/>
<MarketingPageTracker
pageType={pageType}
cluster={cluster}
useCase={useCase}
/>
<div className="min-h-screen bg-white text-slate-900 font-sans">
{/* --- HERO SECTION --- */}
<section className="relative pt-24 pb-20 lg:pt-36 lg:pb-32 overflow-hidden border-b border-slate-100 bg-slate-50/50">
{/* Decorative Backgrounds */}
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(to_right,#cbd5e1_1px,transparent_1px),linear-gradient(to_bottom,#cbd5e1_1px,transparent_1px)] bg-[size:4rem_4rem] [mask-image:radial-gradient(ellipse_80%_80%_at_50%_0%,#000_80%,transparent_110%)] opacity-30" />
<div className="absolute top-0 right-0 -mr-[20rem] w-[60rem] h-[60rem] bg-blue-200/40 rounded-full blur-[120px] pointer-events-none" />
<div className="relative container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="grid gap-16 lg:grid-cols-2 lg:gap-8 items-center">
{/* Left Column: Text & CTAs */}
<div className="flex flex-col justify-center text-center lg:text-left pt-10 lg:pt-0">
<Breadcrumbs
items={breadcrumbs}
className="mb-8 justify-center lg:justify-start [&_a]:text-slate-500 [&_a:hover]:text-blue-600 [&_span]:text-slate-400 [&_[aria-current=page]]:text-slate-900"
/>
<div className="inline-flex items-center gap-2 rounded-full border border-blue-200 bg-white/60 backdrop-blur-sm px-4 py-2 text-sm font-semibold text-blue-700 shadow-sm mb-8 mx-auto lg:mx-0 w-max">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500"></span>
</span>
Industry Solutions
</div>
<h1 className="max-w-3xl text-5xl font-extrabold tracking-tight text-slate-900 sm:text-6xl lg:text-7xl mb-8 leading-[1.1]">
{title.replace("QR Codes for", "")}{" "}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-indigo-600 block sm:inline">
QR Codes
</span>
</h1>
<p className="max-w-2xl text-xl leading-relaxed text-slate-600 mb-12 font-medium mx-auto lg:mx-0">
{intro}
</p>
<div className="flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-4 w-full">
<TrackedCtaLink
href={primaryCta.href}
ctaLabel={primaryCta.label}
ctaLocation="hero_primary"
pageType={pageType}
cluster={cluster}
useCase={useCase}
>
<Button
size="lg"
className="w-full sm:w-auto bg-blue-600 hover:bg-blue-700 text-white rounded-full px-8 py-7 text-lg font-bold shadow-xl shadow-blue-600/30 transition-all hover:-translate-y-1"
>
{primaryCta.label}
</Button>
</TrackedCtaLink>
<TrackedCtaLink
href={secondaryCta.href}
ctaLabel={secondaryCta.label}
ctaLocation="hero_secondary"
pageType={pageType}
cluster={cluster}
useCase={useCase}
>
<Button
variant="outline"
size="lg"
className="w-full sm:w-auto overflow-hidden bg-white border-slate-200 text-slate-700 hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900 rounded-full px-8 py-7 text-lg font-bold shadow-sm transition-all hover:-translate-y-1"
>
{secondaryCta.label}
</Button>
</TrackedCtaLink>
</div>
</div>
{/* Right Column: Abstract App Interface Mockup */}
<div className="relative mx-auto w-full max-w-lg lg:max-w-none flex justify-center lg:justify-end mt-12 lg:mt-0 pb-16 lg:pb-0">
<div className="relative w-full max-w-[440px] aspect-[4/5] sm:aspect-[4/5]">
{/* Decorative glowing blobs behind the image */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[120%] h-[120%] bg-blue-500/20 blur-[90px] rounded-full pointer-events-none" />
<div className="absolute -top-10 -right-10 w-64 h-64 bg-indigo-500/20 blur-[70px] rounded-full pointer-events-none" />
{/* Floating abstract element 1: Notification */}
<div className="absolute z-30 top-1/4 -left-6 sm:-left-12 bg-white/90 backdrop-blur-md p-4 rounded-2xl shadow-2xl shadow-slate-200/50 border border-slate-100/50 flex items-center gap-4 transition-transform hover:scale-105 duration-300 scale-90 sm:scale-100">
<div className="w-12 h-12 rounded-full bg-emerald-100 flex items-center justify-center shrink-0">
<CheckCircle2 className="w-6 h-6 text-emerald-600" />
</div>
<div>
<div className="text-sm font-black text-slate-900 tracking-tight">Active Scan!</div>
<div className="text-xs text-slate-500 font-medium">Just now</div>
</div>
</div>
{/* Floating abstract element 2: Stats */}
<div className="absolute z-30 bottom-1/4 -right-4 sm:-right-8 bg-white/90 backdrop-blur-md p-4 rounded-2xl shadow-2xl shadow-slate-200/50 border border-slate-100/50 flex items-center gap-4 transition-transform hover:scale-105 duration-300 scale-90 sm:scale-100 delay-100">
<div className="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center shrink-0">
<BarChart3 className="w-6 h-6 text-blue-600" />
</div>
<div>
<div className="text-sm font-black text-slate-900 tracking-tight">+148 Views</div>
<div className="text-xs text-slate-500 font-medium">This week</div>
</div>
</div>
{/* AI Generated Photorealistic Image */}
<div className="relative z-20 w-full h-full rounded-[2rem] sm:rounded-[2.5rem] shadow-2xl shadow-blue-900/30 border-8 border-white overflow-hidden rotate-0 sm:rotate-2 transition-transform duration-700 hover:rotate-0">
<Image
src={heroImage}
alt={heroImageAlt}
fill
priority
className="object-cover"
/>
</div>
</div>
</div>
</div>
</div>
</section>
{/* --- BENEFITS SECTION --- */}
{benefits.length > 0 && (
<section className="py-20 bg-slate-50 border-t border-slate-100 relative z-10">
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl max-w-2xl mx-auto">
Why Leading {title.replace("QR Codes for ", "").replace("QR Codes for", "")} Businesses Use QR Master
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{benefits.map((benefit, index) => (
<div key={benefit.title} className="bg-white rounded-3xl p-8 shadow-sm border border-slate-200 hover:border-blue-200 hover:shadow-lg transition-all">
<div className="w-12 h-12 rounded-full bg-indigo-50 flex items-center justify-center text-indigo-600 font-bold text-xl mb-6">
{index + 1}
</div>
<h3 className="text-xl font-bold text-slate-900 mb-4">{benefit.title}</h3>
<p className="text-slate-600 leading-relaxed">{benefit.description}</p>
</div>
))}
</div>
</div>
</section>
)}
{/* --- WORKFLOW CARDS SECTION (USE CASES) --- */}
<section className="py-12 bg-white relative z-10">
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{workflowCards.map((card, index) => {
const Icon = WORKFLOW_ICONS[index % WORKFLOW_ICONS.length];
return (
<div
key={card.title}
className="flex flex-col group rounded-[2rem] border border-slate-200 bg-white p-8 shadow-sm hover:shadow-xl transition-all duration-300 hover:-translate-y-1"
>
<div className="mb-6 flex h-14 w-14 items-center justify-center rounded-2xl bg-blue-50 text-blue-600 group-hover:scale-110 group-hover:bg-blue-600 group-hover:text-white transition-all duration-300">
<Icon className="h-7 w-7" />
</div>
<h3 className="mb-3 text-2xl font-bold text-slate-900 leading-tight">
{card.title}
</h3>
<p className="text-slate-600 leading-relaxed text-lg">
{card.description}
</p>
</div>
);
})}
</div>
</div>
</section>
{/* --- QUICK SUMMARY (ANSWER FIRST) --- */}
<section className="py-16">
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mb-8 text-center">
<h2 className="text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl">
Quick Summary
</h2>
<p className="mt-4 text-lg text-slate-600 max-w-2xl mx-auto">
{answer}
</p>
</div>
<div className="rounded-[2rem] border border-slate-100 bg-white p-2">
<AnswerFirstBlock
whatIsIt={answer}
whenToUse={whenToUse}
comparison={{
leftTitle: "Static Pages",
rightTitle: "QR Master Pages",
items: comparisonItems,
}}
howTo={{
steps: howToSteps,
}}
className="mt-0 shadow-none border-0"
/>
</div>
</div>
</section>
{/* --- IMPLEMENTATION CHECKLIST --- */}
<section className="py-20 bg-slate-50">
<div className="container mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
<div className="mx-auto max-w-2xl text-center mb-12">
<h2 className="text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl">
{checklistTitle}
</h2>
</div>
<div className="grid gap-x-8 gap-y-4 sm:grid-cols-2">
{checklist.map((item) => (
<div key={item} className="flex items-start gap-4 p-4 rounded-xl bg-white border border-slate-200 shadow-sm">
<CheckCircle2 className="h-6 w-6 shrink-0 text-blue-600 mt-0.5" />
<span className="text-lg font-medium text-slate-700">{item}</span>
</div>
))}
</div>
</div>
</section>
{/* --- RECOMMENDED TOOLS --- */}
<section className="py-24">
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="relative overflow-hidden rounded-[2.5rem] bg-slate-900 px-6 py-16 sm:px-12 sm:py-20 lg:px-16 text-center text-white shadow-2xl shadow-indigo-900/20 border border-slate-800">
<div className="absolute -top-24 -right-24 w-96 h-96 bg-blue-600/20 blur-[100px] rounded-full pointer-events-none" />
<div className="absolute -bottom-24 -left-24 w-96 h-96 bg-indigo-600/20 blur-[100px] rounded-full pointer-events-none" />
<div className="relative z-10">
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-12">
Recommended Tools
</h2>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 max-w-5xl mx-auto">
{supportLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="group flex flex-col items-center rounded-3xl bg-slate-800/50 p-8 border border-slate-700 transition-all hover:bg-white hover:border-white hover:scale-105 backdrop-blur-md shadow-xl"
>
<div className="mb-4 text-indigo-400 group-hover:text-blue-600 transition-colors">
<Link2 className="h-10 w-10" />
</div>
<div className="text-xl font-bold text-white group-hover:text-slate-900 mb-2 transition-colors">
{link.title}
</div>
<p className="mb-4 text-sm leading-relaxed text-slate-400 group-hover:text-slate-600 transition-colors">
{link.description}
</p>
<div className="text-sm font-medium text-slate-300 group-hover:text-blue-600 transition-colors">
Use Tool &rarr;
</div>
</Link>
))}
</div>
</div>
</div>
</div>
</section>
{/* --- FAQ SECTION --- */}
<section className="py-16">
<div className="container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
<div className="text-center mb-10">
<h2 className="text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl">
FAQ
</h2>
</div>
<div className="rounded-3xl bg-white p-6 sm:p-10 shadow-sm border border-slate-200">
<FAQSection items={faq} title="" />
</div>
</div>
</section>
{/* --- FINAL CTA --- */}
<section className="relative overflow-hidden bg-slate-900 border-t border-slate-800 pt-24 pb-44 text-center -mb-20">
<div className="absolute top-0 right-1/4 w-[40rem] h-[40rem] bg-blue-600/20 blur-[120px] rounded-full pointer-events-none" />
<div className="absolute bottom-0 left-1/4 w-[40rem] h-[40rem] bg-indigo-600/20 blur-[120px] rounded-full pointer-events-none" />
<div className="relative z-10 container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
<h2 className="mb-6 text-4xl font-extrabold tracking-tight text-white sm:text-5xl">
Ready to modernize your operations?
</h2>
<p className="mx-auto mb-10 text-xl text-slate-300 max-w-2xl font-medium">
Elevate your {title.replace("QR Codes for", "").trim().toLowerCase()} experience: seamless operations and enhanced engagement.
</p>
<div className="flex flex-col justify-center gap-4 sm:flex-row">
<TrackedCtaLink
href={primaryCta.href}
ctaLabel={primaryCta.label}
ctaLocation="footer_primary"
pageType={pageType}
cluster={cluster}
useCase={useCase}
>
<Button
size="lg"
className="w-full bg-blue-600 px-10 py-7 text-lg font-bold text-white hover:bg-blue-500 hover:shadow-blue-500/25 sm:w-auto rounded-full shadow-xl shadow-blue-600/20 transition-all hover:-translate-y-1 border border-blue-500"
>
{primaryCta.label}
</Button>
</TrackedCtaLink>
</div>
</div>
</section>
</div>
</>
);
}
import type { FAQItem } from "@/lib/types";
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import {
ArrowRight,
CheckCircle2,
Building2,
Settings2,
Smartphone,
BarChart3,
Link2,
} from "lucide-react";
import Breadcrumbs, { BreadcrumbItem } from "@/components/Breadcrumbs";
import SeoJsonLd from "@/components/SeoJsonLd";
import { FAQSection } from "@/components/aeo/FAQSection";
import {
MarketingPageTracker,
TrackedCtaLink,
} from "@/components/marketing/MarketingAnalytics";
import { AnswerFirstBlock } from "@/components/marketing/AnswerFirstBlock";
import { Button } from "@/components/ui/Button";
import { breadcrumbSchema, faqPageSchema } from "@/lib/schema";
type LinkCard = {
href: string;
title: string;
description: string;
};
type IndustryPageTemplateProps = {
title: string;
description: string;
eyebrow: string;
intro: string;
pageType: "commercial" | "use_case";
cluster: string;
useCase?: string;
breadcrumbs: BreadcrumbItem[];
answer: string;
whenToUse: string[];
comparisonItems: {
label: string;
value: boolean;
text?: string;
}[];
howToSteps: string[];
primaryCta: {
href: string;
label: string;
};
secondaryCta: {
href: string;
label: string;
};
workflowTitle: string;
workflowIntro: string;
workflowCards: {
title: string;
description: string;
}[];
checklistTitle: string;
checklist: string[];
supportLinks: LinkCard[];
faq: FAQItem[];
schemaData?: Record<string, unknown>[];
heroImage?: string;
heroImageAlt?: string;
statistics?: { value: string; label: string }[];
benefits?: { title: string; description: string }[];
};
export function buildIndustryMetadata({
title,
fallbackTitle,
description,
canonicalPath,
}: {
title: string;
fallbackTitle?: string;
description: string;
canonicalPath: string;
}): Metadata {
const canonical = `https://www.qrmaster.net${canonicalPath}`;
const brandSuffix = " | QR Master";
const maxTitleLength = 60;
const maxBaseTitleLength = maxTitleLength - brandSuffix.length;
const normalizedTitle = title.replace(/\s+\|\s+QR Master$/i, "").trim();
const fallback = fallbackTitle?.replace(/\s+\|\s+QR Master$/i, "").trim();
const candidates = [
normalizedTitle,
normalizedTitle.split(":")[0]?.trim(),
fallback,
].filter((candidate): candidate is string => Boolean(candidate));
const seoTitle =
candidates.find((candidate) => candidate.length <= maxBaseTitleLength) ??
normalizedTitle.slice(0, maxBaseTitleLength).replace(/\s+\S*$/, "").trim();
const fullTitle = `${seoTitle} | QR Master`;
return {
title: {
absolute: fullTitle,
},
description,
alternates: {
canonical,
languages: {
"x-default": canonical,
en: canonical,
},
},
openGraph: {
title: fullTitle,
description,
url: canonical,
type: "website",
images: ["/og-image.png"],
},
twitter: {
title: fullTitle,
description,
},
};
}
// Map a consistent icon for workflow cards depending on index
const WORKFLOW_ICONS = [Smartphone, Settings2, BarChart3, Building2];
export function IndustryPageTemplate({
title,
description,
eyebrow,
intro,
pageType,
cluster,
useCase,
breadcrumbs,
answer,
whenToUse,
comparisonItems,
howToSteps,
primaryCta,
secondaryCta,
workflowTitle,
workflowIntro,
workflowCards,
checklistTitle,
checklist,
supportLinks,
faq,
schemaData = [],
heroImage = "/hero-qr-scan-mockup.png",
heroImageAlt = "Industry example showing a QR code workflow in action",
statistics = [],
benefits = [],
}: IndustryPageTemplateProps) {
return (
<>
<SeoJsonLd
data={[...schemaData, breadcrumbSchema(breadcrumbs), faqPageSchema(faq)]}
/>
<MarketingPageTracker
pageType={pageType}
cluster={cluster}
useCase={useCase}
/>
<div className="min-h-screen bg-white text-slate-900 font-sans">
{/* --- HERO SECTION --- */}
<section className="relative pt-24 pb-20 lg:pt-36 lg:pb-32 overflow-hidden border-b border-slate-100 bg-slate-50/50">
{/* Decorative Backgrounds */}
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(to_right,#cbd5e1_1px,transparent_1px),linear-gradient(to_bottom,#cbd5e1_1px,transparent_1px)] bg-[size:4rem_4rem] [mask-image:radial-gradient(ellipse_80%_80%_at_50%_0%,#000_80%,transparent_110%)] opacity-30" />
<div className="absolute top-0 right-0 -mr-[20rem] w-[60rem] h-[60rem] bg-blue-200/40 rounded-full blur-[120px] pointer-events-none" />
<div className="relative container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="grid gap-16 lg:grid-cols-2 lg:gap-8 items-center">
{/* Left Column: Text & CTAs */}
<div className="flex flex-col justify-center text-center lg:text-left pt-10 lg:pt-0">
<Breadcrumbs
items={breadcrumbs}
className="mb-8 justify-center lg:justify-start [&_a]:text-slate-500 [&_a:hover]:text-blue-600 [&_span]:text-slate-400 [&_[aria-current=page]]:text-slate-900"
/>
<div className="inline-flex items-center gap-2 rounded-full border border-blue-200 bg-white/60 backdrop-blur-sm px-4 py-2 text-sm font-semibold text-blue-700 shadow-sm mb-8 mx-auto lg:mx-0 w-max">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500"></span>
</span>
Industry Solutions
</div>
<h1 className="max-w-3xl text-5xl font-extrabold tracking-tight text-slate-900 sm:text-6xl lg:text-7xl mb-8 leading-[1.1]">
{title.replace("QR Codes for", "")}{" "}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-indigo-600 block sm:inline">
QR Codes
</span>
</h1>
<p className="max-w-2xl text-xl leading-relaxed text-slate-600 mb-12 font-medium mx-auto lg:mx-0">
{intro}
</p>
<div className="flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-4 w-full">
<TrackedCtaLink
href={primaryCta.href}
ctaLabel={primaryCta.label}
ctaLocation="hero_primary"
pageType={pageType}
cluster={cluster}
useCase={useCase}
>
<Button
size="lg"
className="w-full sm:w-auto bg-blue-600 hover:bg-blue-700 text-white rounded-full px-8 py-7 text-lg font-bold shadow-xl shadow-blue-600/30 transition-all hover:-translate-y-1"
>
{primaryCta.label}
</Button>
</TrackedCtaLink>
<TrackedCtaLink
href={secondaryCta.href}
ctaLabel={secondaryCta.label}
ctaLocation="hero_secondary"
pageType={pageType}
cluster={cluster}
useCase={useCase}
>
<Button
variant="outline"
size="lg"
className="w-full sm:w-auto overflow-hidden bg-white border-slate-200 text-slate-700 hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900 rounded-full px-8 py-7 text-lg font-bold shadow-sm transition-all hover:-translate-y-1"
>
{secondaryCta.label}
</Button>
</TrackedCtaLink>
</div>
</div>
{/* Right Column: Abstract App Interface Mockup */}
<div className="relative mx-auto w-full max-w-lg lg:max-w-none flex justify-center lg:justify-end mt-12 lg:mt-0 pb-16 lg:pb-0">
<div className="relative w-full max-w-[440px] aspect-[4/5] sm:aspect-[4/5]">
{/* Decorative glowing blobs behind the image */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[120%] h-[120%] bg-blue-500/20 blur-[90px] rounded-full pointer-events-none" />
<div className="absolute -top-10 -right-10 w-64 h-64 bg-indigo-500/20 blur-[70px] rounded-full pointer-events-none" />
{/* Floating abstract element 1: Notification */}
<div className="absolute z-30 top-1/4 -left-6 sm:-left-12 bg-white/90 backdrop-blur-md p-4 rounded-2xl shadow-2xl shadow-slate-200/50 border border-slate-100/50 flex items-center gap-4 transition-transform hover:scale-105 duration-300 scale-90 sm:scale-100">
<div className="w-12 h-12 rounded-full bg-emerald-100 flex items-center justify-center shrink-0">
<CheckCircle2 className="w-6 h-6 text-emerald-600" />
</div>
<div>
<div className="text-sm font-black text-slate-900 tracking-tight">Active Scan!</div>
<div className="text-xs text-slate-500 font-medium">Just now</div>
</div>
</div>
{/* Floating abstract element 2: Stats */}
<div className="absolute z-30 bottom-1/4 -right-4 sm:-right-8 bg-white/90 backdrop-blur-md p-4 rounded-2xl shadow-2xl shadow-slate-200/50 border border-slate-100/50 flex items-center gap-4 transition-transform hover:scale-105 duration-300 scale-90 sm:scale-100 delay-100">
<div className="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center shrink-0">
<BarChart3 className="w-6 h-6 text-blue-600" />
</div>
<div>
<div className="text-sm font-black text-slate-900 tracking-tight">+148 Views</div>
<div className="text-xs text-slate-500 font-medium">This week</div>
</div>
</div>
{/* AI Generated Photorealistic Image */}
<div className="relative z-20 w-full h-full rounded-[2rem] sm:rounded-[2.5rem] shadow-2xl shadow-blue-900/30 border-8 border-white overflow-hidden rotate-0 sm:rotate-2 transition-transform duration-700 hover:rotate-0">
<Image
src={heroImage}
alt={heroImageAlt}
fill
priority
className="object-cover"
/>
</div>
</div>
</div>
</div>
</div>
</section>
{/* --- BENEFITS SECTION --- */}
{benefits.length > 0 && (
<section className="py-20 bg-slate-50 border-t border-slate-100 relative z-10">
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl max-w-2xl mx-auto">
Why Leading {title.replace("QR Codes for ", "").replace("QR Codes for", "")} Businesses Use QR Master
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{benefits.map((benefit, index) => (
<div key={benefit.title} className="bg-white rounded-3xl p-8 shadow-sm border border-slate-200 hover:border-blue-200 hover:shadow-lg transition-all">
<div className="w-12 h-12 rounded-full bg-indigo-50 flex items-center justify-center text-indigo-600 font-bold text-xl mb-6">
{index + 1}
</div>
<h3 className="text-xl font-bold text-slate-900 mb-4">{benefit.title}</h3>
<p className="text-slate-600 leading-relaxed">{benefit.description}</p>
</div>
))}
</div>
</div>
</section>
)}
{/* --- WORKFLOW CARDS SECTION (USE CASES) --- */}
<section className="py-12 bg-white relative z-10">
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{workflowCards.map((card, index) => {
const Icon = WORKFLOW_ICONS[index % WORKFLOW_ICONS.length];
return (
<div
key={card.title}
className="flex flex-col group rounded-[2rem] border border-slate-200 bg-white p-8 shadow-sm hover:shadow-xl transition-all duration-300 hover:-translate-y-1"
>
<div className="mb-6 flex h-14 w-14 items-center justify-center rounded-2xl bg-blue-50 text-blue-600 group-hover:scale-110 group-hover:bg-blue-600 group-hover:text-white transition-all duration-300">
<Icon className="h-7 w-7" />
</div>
<h3 className="mb-3 text-2xl font-bold text-slate-900 leading-tight">
{card.title}
</h3>
<p className="text-slate-600 leading-relaxed text-lg">
{card.description}
</p>
</div>
);
})}
</div>
</div>
</section>
{/* --- QUICK SUMMARY (ANSWER FIRST) --- */}
<section className="py-16">
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mb-8 text-center">
<h2 className="text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl">
Quick Summary
</h2>
<p className="mt-4 text-lg text-slate-600 max-w-2xl mx-auto">
{answer}
</p>
</div>
<div className="rounded-[2rem] border border-slate-100 bg-white p-2">
<AnswerFirstBlock
whatIsIt={answer}
whenToUse={whenToUse}
comparison={{
leftTitle: "Static Pages",
rightTitle: "QR Master Pages",
items: comparisonItems,
}}
howTo={{
steps: howToSteps,
}}
className="mt-0 shadow-none border-0"
/>
</div>
</div>
</section>
{/* --- IMPLEMENTATION CHECKLIST --- */}
<section className="py-20 bg-slate-50">
<div className="container mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
<div className="mx-auto max-w-2xl text-center mb-12">
<h2 className="text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl">
{checklistTitle}
</h2>
</div>
<div className="grid gap-x-8 gap-y-4 sm:grid-cols-2">
{checklist.map((item) => (
<div key={item} className="flex items-start gap-4 p-4 rounded-xl bg-white border border-slate-200 shadow-sm">
<CheckCircle2 className="h-6 w-6 shrink-0 text-blue-600 mt-0.5" />
<span className="text-lg font-medium text-slate-700">{item}</span>
</div>
))}
</div>
</div>
</section>
{/* --- RECOMMENDED TOOLS --- */}
<section className="py-24">
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="relative overflow-hidden rounded-[2.5rem] bg-slate-900 px-6 py-16 sm:px-12 sm:py-20 lg:px-16 text-center text-white shadow-2xl shadow-indigo-900/20 border border-slate-800">
<div className="absolute -top-24 -right-24 w-96 h-96 bg-blue-600/20 blur-[100px] rounded-full pointer-events-none" />
<div className="absolute -bottom-24 -left-24 w-96 h-96 bg-indigo-600/20 blur-[100px] rounded-full pointer-events-none" />
<div className="relative z-10">
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-12">
Recommended Tools
</h2>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 max-w-5xl mx-auto">
{supportLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="group flex flex-col items-center rounded-3xl bg-slate-800/50 p-8 border border-slate-700 transition-all hover:bg-white hover:border-white hover:scale-105 backdrop-blur-md shadow-xl"
>
<div className="mb-4 text-indigo-400 group-hover:text-blue-600 transition-colors">
<Link2 className="h-10 w-10" />
</div>
<div className="text-xl font-bold text-white group-hover:text-slate-900 mb-2 transition-colors">
{link.title}
</div>
<p className="mb-4 text-sm leading-relaxed text-slate-400 group-hover:text-slate-600 transition-colors">
{link.description}
</p>
<div className="text-sm font-medium text-slate-300 group-hover:text-blue-600 transition-colors">
Use Tool &rarr;
</div>
</Link>
))}
</div>
</div>
</div>
</div>
</section>
{/* --- FAQ SECTION --- */}
<section className="py-16">
<div className="container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
<div className="text-center mb-10">
<h2 className="text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl">
FAQ
</h2>
</div>
<div className="rounded-3xl bg-white p-6 sm:p-10 shadow-sm border border-slate-200">
<FAQSection items={faq} title="" />
</div>
</div>
</section>
{/* --- FINAL CTA --- */}
<section className="relative overflow-hidden bg-slate-900 border-t border-slate-800 pt-24 pb-44 text-center -mb-20">
<div className="absolute top-0 right-1/4 w-[40rem] h-[40rem] bg-blue-600/20 blur-[120px] rounded-full pointer-events-none" />
<div className="absolute bottom-0 left-1/4 w-[40rem] h-[40rem] bg-indigo-600/20 blur-[120px] rounded-full pointer-events-none" />
<div className="relative z-10 container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
<h2 className="mb-6 text-4xl font-extrabold tracking-tight text-white sm:text-5xl">
Ready to modernize your operations?
</h2>
<p className="mx-auto mb-10 text-xl text-slate-300 max-w-2xl font-medium">
Elevate your {title.replace("QR Codes for", "").trim().toLowerCase()} experience: seamless operations and enhanced engagement.
</p>
<div className="flex flex-col justify-center gap-4 sm:flex-row">
<TrackedCtaLink
href={primaryCta.href}
ctaLabel={primaryCta.label}
ctaLocation="footer_primary"
pageType={pageType}
cluster={cluster}
useCase={useCase}
>
<Button
size="lg"
className="w-full bg-blue-600 px-10 py-7 text-lg font-bold text-white hover:bg-blue-500 hover:shadow-blue-500/25 sm:w-auto rounded-full shadow-xl shadow-blue-600/20 transition-all hover:-translate-y-1 border border-blue-500"
>
{primaryCta.label}
</Button>
</TrackedCtaLink>
</div>
</div>
</section>
</div>
</>
);
}

View File

@@ -1,99 +1,99 @@
'use client';
import React, { useEffect, useRef } from 'react';
import Link from 'next/link';
import { X, Zap, BarChart2, RefreshCw, Palette } from 'lucide-react';
import { Button } from '@/components/ui/Button';
interface PostDownloadPopupProps {
open: boolean;
onClose: () => void;
}
const BENEFITS = [
{ icon: RefreshCw, text: 'Edit the link anytime — QR stays the same' },
{ icon: BarChart2, text: 'See who scans, when & where' },
{ icon: Palette, text: 'Custom colors, logo & frames' },
{ icon: Zap, text: 'Free plan included — upgrade anytime for more' },
];
const LS_KEY = 'qrm_download_popup_seen';
export function shouldShowDownloadPopup(): boolean {
try { return !localStorage.getItem(LS_KEY); } catch { return false; }
}
export function markDownloadPopupSeen(): void {
try { localStorage.setItem(LS_KEY, '1'); } catch { /* ignore */ }
}
export default function PostDownloadPopup({ open, onClose }: PostDownloadPopupProps) {
const overlayRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
markDownloadPopupSeen();
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
}, [open, onClose]);
if (!open) return null;
return (
<div
ref={overlayRef}
className="fixed inset-0 z-50 flex items-center justify-center p-4"
style={{ backgroundColor: 'rgba(15, 23, 42, 0.6)', backdropFilter: 'blur(4px)' }}
onClick={(e) => { if (e.target === overlayRef.current) onClose(); }}
>
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-md overflow-hidden animate-in fade-in zoom-in-95 duration-200">
{/* Header */}
<div className="relative bg-gradient-to-br from-[#4F46E5] to-[#7C3AED] p-6 text-white text-center">
<button
onClick={onClose}
className="absolute top-4 right-4 text-white/70 hover:text-white transition-colors"
aria-label="Close"
>
<X className="w-5 h-5" />
</button>
<div className="w-12 h-12 bg-white/20 rounded-2xl flex items-center justify-center mx-auto mb-3">
<Zap className="w-6 h-6 text-white" />
</div>
<h2 className="text-xl font-bold">Your QR code is downloading!</h2>
<p className="text-white/80 text-sm mt-1">
Want to make it smarter for free?
</p>
</div>
{/* Benefits */}
<div className="p-6 space-y-3">
{BENEFITS.map(({ icon: Icon, text }) => (
<div key={text} className="flex items-center gap-3">
<div className="w-8 h-8 rounded-xl bg-indigo-50 flex items-center justify-center shrink-0">
<Icon className="w-4 h-4 text-[#4F46E5]" />
</div>
<span className="text-sm text-slate-700">{text}</span>
</div>
))}
</div>
{/* CTAs */}
<div className="px-6 pb-6 space-y-3">
<Link href="/signup" onClick={onClose} className="block">
<Button className="w-full bg-[#4F46E5] hover:bg-[#4338CA] text-white h-12 text-base font-semibold shadow-lg">
Create Free Account
</Button>
</Link>
<button
onClick={onClose}
className="w-full text-sm text-slate-400 hover:text-slate-600 transition-colors py-1"
>
No thanks, keep it static
</button>
</div>
</div>
</div>
);
}
'use client';
import React, { useEffect, useRef } from 'react';
import Link from 'next/link';
import { X, Zap, BarChart2, RefreshCw, Palette } from 'lucide-react';
import { Button } from '@/components/ui/Button';
interface PostDownloadPopupProps {
open: boolean;
onClose: () => void;
}
const BENEFITS = [
{ icon: RefreshCw, text: 'Edit the link anytime — QR stays the same' },
{ icon: BarChart2, text: 'See who scans, when & where' },
{ icon: Palette, text: 'Custom colors, logo & frames' },
{ icon: Zap, text: 'Free plan included — upgrade anytime for more' },
];
const LS_KEY = 'qrm_download_popup_seen';
export function shouldShowDownloadPopup(): boolean {
try { return !localStorage.getItem(LS_KEY); } catch { return false; }
}
export function markDownloadPopupSeen(): void {
try { localStorage.setItem(LS_KEY, '1'); } catch { /* ignore */ }
}
export default function PostDownloadPopup({ open, onClose }: PostDownloadPopupProps) {
const overlayRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
markDownloadPopupSeen();
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
}, [open, onClose]);
if (!open) return null;
return (
<div
ref={overlayRef}
className="fixed inset-0 z-50 flex items-center justify-center p-4"
style={{ backgroundColor: 'rgba(15, 23, 42, 0.6)', backdropFilter: 'blur(4px)' }}
onClick={(e) => { if (e.target === overlayRef.current) onClose(); }}
>
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-md overflow-hidden animate-in fade-in zoom-in-95 duration-200">
{/* Header */}
<div className="relative bg-gradient-to-br from-[#4F46E5] to-[#7C3AED] p-6 text-white text-center">
<button
onClick={onClose}
className="absolute top-4 right-4 text-white/70 hover:text-white transition-colors"
aria-label="Close"
>
<X className="w-5 h-5" />
</button>
<div className="w-12 h-12 bg-white/20 rounded-2xl flex items-center justify-center mx-auto mb-3">
<Zap className="w-6 h-6 text-white" />
</div>
<h2 className="text-xl font-bold">Your QR code is downloading!</h2>
<p className="text-white/80 text-sm mt-1">
Want to make it smarter for free?
</p>
</div>
{/* Benefits */}
<div className="p-6 space-y-3">
{BENEFITS.map(({ icon: Icon, text }) => (
<div key={text} className="flex items-center gap-3">
<div className="w-8 h-8 rounded-xl bg-indigo-50 flex items-center justify-center shrink-0">
<Icon className="w-4 h-4 text-[#4F46E5]" />
</div>
<span className="text-sm text-slate-700">{text}</span>
</div>
))}
</div>
{/* CTAs */}
<div className="px-6 pb-6 space-y-3">
<Link href="/signup" onClick={onClose} className="block">
<Button className="w-full bg-[#4F46E5] hover:bg-[#4338CA] text-white h-12 text-base font-semibold shadow-lg">
Create Free Account
</Button>
</Link>
<button
onClick={onClose}
className="w-full text-sm text-slate-400 hover:text-slate-600 transition-colors py-1"
>
No thanks, keep it static
</button>
</div>
</div>
</div>
);
}

View File

@@ -1,164 +1,164 @@
'use client';
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { BillingToggle } from '@/components/ui/BillingToggle';
interface PricingProps {
t: any; // i18n translation function
}
export const Pricing: React.FC<PricingProps> = ({ t }) => {
const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month');
const plans = [
{
key: 'free',
popular: false,
},
{
key: 'pro',
popular: true,
},
{
key: 'business',
popular: false,
},
{
key: 'enterprise',
popular: false,
},
];
return (
<section id="pricing" className="py-16">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="text-center mb-12"
>
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
{t.pricing.title}
</h2>
<p className="text-xl text-gray-600">{t.pricing.subtitle}</p>
</motion.div>
<div className="flex justify-center mb-8">
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8 max-w-7xl mx-auto">
{plans.map((plan, index) => (
<motion.div
key={plan.key}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className="h-full"
>
<Card
className={`h-full flex flex-col ${
plan.popular
? 'border-primary-500 shadow-xl relative scale-105 z-10'
: 'border-gray-200 hover:border-gray-300 hover:shadow-lg transition-all'
}`}
>
{plan.popular && (
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2 w-full text-center">
<Badge variant="info" className="px-4 py-1.5 shadow-sm">
{t.pricing[plan.key].badge}
</Badge>
</div>
)}
<CardHeader className="text-center pb-8">
<CardTitle className="text-2xl mb-4">
{t.pricing[plan.key].title}
</CardTitle>
<div className="flex flex-col items-center">
<div className="flex items-baseline justify-center">
<span className="text-4xl font-bold">
{plan.key === 'free' || plan.key === 'enterprise'
? t.pricing[plan.key].price
: billingPeriod === 'month'
? t.pricing[plan.key].price
: plan.key === 'pro'
? '€90'
: '€290'}
</span>
<span className="text-gray-600 ml-2">
{plan.key === 'free' || plan.key === 'enterprise'
? t.pricing[plan.key].period
: billingPeriod === 'month'
? t.pricing[plan.key].period
: 'per year'}
</span>
</div>
{billingPeriod === 'year' &&
plan.key !== 'free' &&
plan.key !== 'enterprise' && (
<Badge variant="success" className="mt-2">
Save 16%
</Badge>
)}
</div>
</CardHeader>
<CardContent className="space-y-8 flex-1 flex flex-col">
<ul className="space-y-3 flex-1">
{t.pricing[plan.key].features.map(
(feature: string, index: number) => (
<li key={index} className="flex items-start space-x-3">
<svg
className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-gray-700">{feature}</span>
</li>
)
)}
</ul>
<div className="mt-8 pt-8 border-t border-gray-100">
{plan.key === 'enterprise' ? (
<Link href="mailto:timo@qrmaster.net">
<Button variant="outline" className="w-full" size="lg">
{t.pricing[plan.key].contact || 'Contact Us'}
</Button>
</Link>
) : (
<Link href="/signup">
<Button
variant={plan.popular ? 'primary' : 'outline'}
className="w-full"
size="lg"
>
Get Started
</Button>
</Link>
)}
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
);
};
'use client';
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { BillingToggle } from '@/components/ui/BillingToggle';
interface PricingProps {
t: any; // i18n translation function
}
export const Pricing: React.FC<PricingProps> = ({ t }) => {
const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month');
const plans = [
{
key: 'free',
popular: false,
},
{
key: 'pro',
popular: true,
},
{
key: 'business',
popular: false,
},
{
key: 'enterprise',
popular: false,
},
];
return (
<section id="pricing" className="py-16">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="text-center mb-12"
>
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
{t.pricing.title}
</h2>
<p className="text-xl text-gray-600">{t.pricing.subtitle}</p>
</motion.div>
<div className="flex justify-center mb-8">
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8 max-w-7xl mx-auto">
{plans.map((plan, index) => (
<motion.div
key={plan.key}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className="h-full"
>
<Card
className={`h-full flex flex-col ${
plan.popular
? 'border-primary-500 shadow-xl relative scale-105 z-10'
: 'border-gray-200 hover:border-gray-300 hover:shadow-lg transition-all'
}`}
>
{plan.popular && (
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2 w-full text-center">
<Badge variant="info" className="px-4 py-1.5 shadow-sm">
{t.pricing[plan.key].badge}
</Badge>
</div>
)}
<CardHeader className="text-center pb-8">
<CardTitle className="text-2xl mb-4">
{t.pricing[plan.key].title}
</CardTitle>
<div className="flex flex-col items-center">
<div className="flex items-baseline justify-center">
<span className="text-4xl font-bold">
{plan.key === 'free' || plan.key === 'enterprise'
? t.pricing[plan.key].price
: billingPeriod === 'month'
? t.pricing[plan.key].price
: plan.key === 'pro'
? '€90'
: '€290'}
</span>
<span className="text-gray-600 ml-2">
{plan.key === 'free' || plan.key === 'enterprise'
? t.pricing[plan.key].period
: billingPeriod === 'month'
? t.pricing[plan.key].period
: 'per year'}
</span>
</div>
{billingPeriod === 'year' &&
plan.key !== 'free' &&
plan.key !== 'enterprise' && (
<Badge variant="success" className="mt-2">
Save 16%
</Badge>
)}
</div>
</CardHeader>
<CardContent className="space-y-8 flex-1 flex flex-col">
<ul className="space-y-3 flex-1">
{t.pricing[plan.key].features.map(
(feature: string, index: number) => (
<li key={index} className="flex items-start space-x-3">
<svg
className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-gray-700">{feature}</span>
</li>
)
)}
</ul>
<div className="mt-8 pt-8 border-t border-gray-100">
{plan.key === 'enterprise' ? (
<Link href="mailto:timo@qrmaster.net">
<Button variant="outline" className="w-full" size="lg">
{t.pricing[plan.key].contact || 'Contact Us'}
</Button>
</Link>
) : (
<Link href="/signup">
<Button
variant={plan.popular ? 'primary' : 'outline'}
className="w-full"
size="lg"
>
Get Started
</Button>
</Link>
)}
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
);
};

View File

@@ -1,111 +1,111 @@
'use client';
import React from 'react';
import { Star, CheckCircle, ChevronRight } from 'lucide-react';
import Link from 'next/link';
import type { Testimonial } from '@/lib/types';
interface Props {
testimonials: Testimonial[];
title?: string;
subtitle?: string;
}
function StarRating({ rating }: { rating: number }) {
return (
<div className="flex gap-0.5" role="img" aria-label={`${rating} out of 5 stars`}>
{[...Array(5)].map((_, i) => (
<Star
key={i}
aria-hidden="true"
focusable="false"
className={`w-4 h-4 ${i < rating ? 'fill-yellow-400 text-yellow-400' : 'fill-gray-200 text-gray-200'}`}
/>
))}
</div>
);
}
function TestimonialCard({ testimonial }: { testimonial: Testimonial }) {
return (
<div className="w-[340px] sm:w-[380px] bg-white rounded-2xl shadow-sm border border-gray-100 p-6 flex flex-col h-full">
<div className="flex items-center justify-between mb-3">
<StarRating rating={testimonial.rating} />
{testimonial.verified && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-green-50 text-green-700 text-xs font-medium rounded-full border border-green-200">
<CheckCircle className="w-3 h-3" />
Verified
</span>
)}
</div>
<h3 className="text-sm font-semibold text-gray-900 mb-2">"{testimonial.title}"</h3>
<p className="text-gray-600 text-sm leading-relaxed flex-grow">{testimonial.content}</p>
<div className="border-t border-gray-100 pt-4 mt-4">
<span className="font-semibold text-gray-900 text-sm block">{testimonial.author.name}</span>
{(testimonial.author.role || testimonial.author.company) && (
<span className="text-xs text-gray-600 block">
{[testimonial.author.role, testimonial.author.company].filter(Boolean).join(', ')}
</span>
)}
{testimonial.author.location && (
<span className="text-xs text-gray-600 block">{testimonial.author.location}</span>
)}
</div>
</div>
);
}
export const TestimonialsCarousel: React.FC<Props> = ({
testimonials,
title = 'What Our Customers Say',
subtitle = 'Real experiences from businesses using QR Master',
}) => {
// Duplicate for seamless loop: when first set exits left, second set is identical → no visible reset
const doubled = [...testimonials, ...testimonials];
return (
<section className="py-16 bg-gray-50 overflow-hidden">
<style>{`
@keyframes marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
.marquee-track {
animation: marquee 90s linear infinite;
}
.marquee-track:hover {
animation-play-state: paused;
}
`}</style>
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl mb-10">
<div className="text-center">
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">{title}</h2>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">{subtitle}</p>
</div>
</div>
{/* Full-width overflow mask */}
<div className="w-full overflow-hidden">
<div
className="marquee-track flex gap-6"
style={{ width: 'max-content' }}
>
{doubled.map((t, idx) => (
<TestimonialCard key={`${t.id}-${idx}`} testimonial={t} />
))}
</div>
</div>
<div className="mt-8 text-center">
<Link
href="/testimonials"
className="inline-flex items-center text-blue-600 font-semibold hover:text-blue-700 transition-colors text-sm"
>
See all {testimonials.length} reviews
<ChevronRight className="w-4 h-4 ml-1" />
</Link>
</div>
</section>
);
};
'use client';
import React from 'react';
import { Star, CheckCircle, ChevronRight } from 'lucide-react';
import Link from 'next/link';
import type { Testimonial } from '@/lib/types';
interface Props {
testimonials: Testimonial[];
title?: string;
subtitle?: string;
}
function StarRating({ rating }: { rating: number }) {
return (
<div className="flex gap-0.5" role="img" aria-label={`${rating} out of 5 stars`}>
{[...Array(5)].map((_, i) => (
<Star
key={i}
aria-hidden="true"
focusable="false"
className={`w-4 h-4 ${i < rating ? 'fill-yellow-400 text-yellow-400' : 'fill-gray-200 text-gray-200'}`}
/>
))}
</div>
);
}
function TestimonialCard({ testimonial }: { testimonial: Testimonial }) {
return (
<div className="w-[340px] sm:w-[380px] bg-white rounded-2xl shadow-sm border border-gray-100 p-6 flex flex-col h-full">
<div className="flex items-center justify-between mb-3">
<StarRating rating={testimonial.rating} />
{testimonial.verified && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-green-50 text-green-700 text-xs font-medium rounded-full border border-green-200">
<CheckCircle className="w-3 h-3" />
Verified
</span>
)}
</div>
<h3 className="text-sm font-semibold text-gray-900 mb-2">"{testimonial.title}"</h3>
<p className="text-gray-600 text-sm leading-relaxed flex-grow">{testimonial.content}</p>
<div className="border-t border-gray-100 pt-4 mt-4">
<span className="font-semibold text-gray-900 text-sm block">{testimonial.author.name}</span>
{(testimonial.author.role || testimonial.author.company) && (
<span className="text-xs text-gray-600 block">
{[testimonial.author.role, testimonial.author.company].filter(Boolean).join(', ')}
</span>
)}
{testimonial.author.location && (
<span className="text-xs text-gray-600 block">{testimonial.author.location}</span>
)}
</div>
</div>
);
}
export const TestimonialsCarousel: React.FC<Props> = ({
testimonials,
title = 'What Our Customers Say',
subtitle = 'Real experiences from businesses using QR Master',
}) => {
// Duplicate for seamless loop: when first set exits left, second set is identical → no visible reset
const doubled = [...testimonials, ...testimonials];
return (
<section className="py-16 bg-gray-50 overflow-hidden">
<style>{`
@keyframes marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
.marquee-track {
animation: marquee 90s linear infinite;
}
.marquee-track:hover {
animation-play-state: paused;
}
`}</style>
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl mb-10">
<div className="text-center">
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">{title}</h2>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">{subtitle}</p>
</div>
</div>
{/* Full-width overflow mask */}
<div className="w-full overflow-hidden">
<div
className="marquee-track flex gap-6"
style={{ width: 'max-content' }}
>
{doubled.map((t, idx) => (
<TestimonialCard key={`${t.id}-${idx}`} testimonial={t} />
))}
</div>
</div>
<div className="mt-8 text-center">
<Link
href="/testimonials"
className="inline-flex items-center text-blue-600 font-semibold hover:text-blue-700 transition-colors text-sm"
>
See all {testimonials.length} reviews
<ChevronRight className="w-4 h-4 ml-1" />
</Link>
</div>
</section>
);
};

File diff suppressed because it is too large Load Diff