11 seo pages
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 →
|
||||
</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 →
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user