/restaurants
This commit is contained in:
@@ -19,3 +19,6 @@ Best early TikTok angles from the supplied account analysis:
|
||||
|
||||
TikTok content should lead with a concrete business mistake or relatable QR frustration, then connect the fix to dynamic QR codes and QR Master.
|
||||
|
||||
## Outreach A/B Test Notes
|
||||
|
||||
- Variant B (`old_qr_links_outdated_destination`, subject `old qr links`) produced a paid user. Treat B as a proven paid-user signal when deciding future lead batch messaging.
|
||||
|
||||
BIN
public/meta-restaurant-dashboard-ad.png
Normal file
BIN
public/meta-restaurant-dashboard-ad.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
BIN
public/restaurants-hero-og.jpg
Normal file
BIN
public/restaurants-hero-og.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
BIN
public/restaurants-hero-wide.png
Normal file
BIN
public/restaurants-hero-wide.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
BIN
public/restaurants-hero-wide.webp
Normal file
BIN
public/restaurants-hero-wide.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
@@ -200,10 +200,10 @@ export default function AppLayout({
|
||||
</aside>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="lg:ml-64">
|
||||
{/* Top bar */}
|
||||
<header className="bg-white border-b border-gray-200">
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<div className="lg:ml-64">
|
||||
{/* Top bar */}
|
||||
<header className="bg-white border-b border-gray-200">
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<button
|
||||
className="lg:hidden"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
@@ -213,24 +213,24 @@ export default function AppLayout({
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div className="flex items-center space-x-4 ml-auto">
|
||||
{/* User Menu */}
|
||||
<Dropdown
|
||||
align="right"
|
||||
trigger={
|
||||
<button className="flex items-center space-x-2 text-gray-700 hover:text-gray-900">
|
||||
<div className="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
|
||||
<span className="text-sm font-medium text-primary-600">
|
||||
{getUserInitials()}
|
||||
</span>
|
||||
</div>
|
||||
<span className="hidden md:block font-medium">
|
||||
{getDisplayName()}
|
||||
</span>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="flex items-center space-x-4 ml-auto">
|
||||
{/* User Menu */}
|
||||
<Dropdown
|
||||
align="right"
|
||||
trigger={
|
||||
<button className="flex items-center space-x-2 text-gray-700 hover:text-gray-900">
|
||||
<div className="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
|
||||
<span className="text-sm font-medium text-primary-600">
|
||||
{getUserInitials()}
|
||||
</span>
|
||||
</div>
|
||||
<span className="hidden md:block font-medium">
|
||||
{getDisplayName()}
|
||||
</span>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<DropdownItem onClick={handleSignOut}>
|
||||
@@ -242,9 +242,9 @@ export default function AppLayout({
|
||||
</header>
|
||||
|
||||
{/* Page content */}
|
||||
<main className="p-6">
|
||||
{children}
|
||||
</main>
|
||||
<main className="p-6">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<Footer variant="dashboard" />
|
||||
|
||||
@@ -86,10 +86,11 @@ export default function MarketingLayout({
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><Link href="/pricing">{t.nav.pricing}</Link></li>
|
||||
<li><Link href="/blog">{t.nav.blog}</Link></li>
|
||||
<li><Link href="/learn">{t.nav.learn}</Link></li>
|
||||
<li><Link href="/use-cases">Use Cases</Link></li>
|
||||
<li><Link href="/faq">{t.nav.faq}</Link></li>
|
||||
<li><Link href="/blog">{t.nav.blog}</Link></li>
|
||||
<li><Link href="/learn">{t.nav.learn}</Link></li>
|
||||
<li><Link href="/use-cases">Use Cases</Link></li>
|
||||
<li><Link href="/restaurants">Restaurant Menu QR Codes</Link></li>
|
||||
<li><Link href="/faq">{t.nav.faq}</Link></li>
|
||||
<li><Link href="/about">{t.nav.about}</Link></li>
|
||||
<li><Link href="/contact">{t.nav.contact}</Link></li>
|
||||
<li><Link href="/login">{t.nav.login}</Link></li>
|
||||
|
||||
872
src/app/(main)/(marketing)/restaurants/page.tsx
Normal file
872
src/app/(main)/(marketing)/restaurants/page.tsx
Normal file
@@ -0,0 +1,872 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ArrowRight,
|
||||
BarChart3,
|
||||
Calculator,
|
||||
Check,
|
||||
Clock3,
|
||||
FileText,
|
||||
Link2,
|
||||
Printer,
|
||||
QrCode,
|
||||
RefreshCw,
|
||||
ShieldCheck,
|
||||
} from "lucide-react";
|
||||
|
||||
import Breadcrumbs from "@/components/Breadcrumbs";
|
||||
import SeoJsonLd from "@/components/SeoJsonLd";
|
||||
import {
|
||||
MarketingPageTracker,
|
||||
TrackedCtaLink,
|
||||
} from "@/components/marketing/MarketingAnalytics";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { breadcrumbSchema, faqPageSchema, softwareApplicationSchema } from "@/lib/schema";
|
||||
|
||||
const SITE_URL = "https://www.qrmaster.net";
|
||||
const PAGE_URL = `${SITE_URL}/restaurants`;
|
||||
const HERO_IMAGE = "/restaurants-hero-wide.webp";
|
||||
const OG_IMAGE = "/restaurants-hero-og.jpg";
|
||||
const CAMPAIGN_SIGNUP =
|
||||
"/signup?utm_source=meta&utm_medium=paid_social&utm_campaign=restaurant_menu_landing&utm_content=restaurants_page_cta";
|
||||
const FAQ = [
|
||||
{
|
||||
question: "What is a dynamic QR code for restaurant menus?",
|
||||
answer:
|
||||
"A dynamic QR code lets a restaurant keep the same printed QR code while changing the destination behind it. You can update a menu PDF, menu page, or ordering link without replacing table tents, flyers, or window signs.",
|
||||
},
|
||||
{
|
||||
question: "Can I change menu prices after the QR code is printed?",
|
||||
answer:
|
||||
"Yes. With QR Master, the printed QR code points to a managed redirect. When prices, dishes, or PDFs change, you update the destination in the dashboard and keep the same printed code.",
|
||||
},
|
||||
{
|
||||
question: "Is QR Master useful for small restaurants?",
|
||||
answer:
|
||||
"Yes. Small restaurants can start with a free account, create a dynamic menu QR code, and use scan analytics to see whether guests actually use the menu link.",
|
||||
},
|
||||
{
|
||||
question: "Do I need to reprint my menu QR code every time the PDF changes?",
|
||||
answer:
|
||||
"No. If the code is dynamic, the printed QR code can stay on the table while the destination changes online.",
|
||||
},
|
||||
{
|
||||
question: "Should a restaurant menu QR code be static or dynamic?",
|
||||
answer:
|
||||
"A static QR code is acceptable only when the destination will never change. Restaurants usually need a dynamic QR code because menus, prices, PDFs, opening hours, and ordering links change after print.",
|
||||
},
|
||||
{
|
||||
question: "Can I use a QR code for a menu PDF?",
|
||||
answer:
|
||||
"Yes. You can point a dynamic QR code to a menu PDF and replace that PDF later. The QR code on the table can stay the same while the file or destination is updated in QR Master.",
|
||||
},
|
||||
{
|
||||
question: "Can I track scans from table tents, flyers, and window signs separately?",
|
||||
answer:
|
||||
"Yes. Use separate dynamic QR codes or tagged destination URLs for each placement. That lets you compare tables, flyers, receipts, window signs, and campaign materials instead of treating every scan as the same source.",
|
||||
},
|
||||
{
|
||||
question: "Does a restaurant menu QR code need a landing page?",
|
||||
answer:
|
||||
"If the QR code is for guests at the table, it can open the menu directly. If the QR code is used in ads or flyers, a focused landing page often works better because it can explain the offer before asking visitors to sign up or order.",
|
||||
},
|
||||
{
|
||||
question: "Can I add UTM parameters to restaurant QR codes?",
|
||||
answer:
|
||||
"Yes. UTM parameters are useful when you want to measure traffic from different printed placements or Meta ad campaigns. QR Master can point dynamic codes to tagged URLs so analytics tools can separate sources and campaigns.",
|
||||
},
|
||||
{
|
||||
question: "Is scan analytics privacy-friendly for restaurant guests?",
|
||||
answer:
|
||||
"QR Master is designed for privacy-conscious scan analytics. Restaurant teams can see practical scan context such as timing and device patterns without turning a menu QR code into intrusive guest tracking.",
|
||||
},
|
||||
{
|
||||
question: "What should a restaurant put on a QR code table tent?",
|
||||
answer:
|
||||
"Use short, direct wording such as Scan for our menu, View today's menu, or Order from your table. Keep the QR code large enough to scan, leave quiet space around it, and test it from normal table distance before printing.",
|
||||
},
|
||||
{
|
||||
question: "How much can a dynamic menu QR code save on reprints?",
|
||||
answer:
|
||||
"It depends on your print volume and how often the menu changes. As a simple example, 30 table tents at EUR 2.50 each reprinted twice per year equals EUR 150 before flyers, window signs, design time, or staff coordination are included.",
|
||||
},
|
||||
];
|
||||
|
||||
const relatedResources = [
|
||||
{
|
||||
href: "/reprint-calculator",
|
||||
title: "QR code reprint calculator",
|
||||
text: "Estimate how much print work changes when you stop replacing QR materials.",
|
||||
},
|
||||
{
|
||||
href: "/dynamic-qr-code-generator",
|
||||
title: "Dynamic QR code generator",
|
||||
text: "Create editable QR codes for links, PDFs, campaigns, and menu updates.",
|
||||
},
|
||||
{
|
||||
href: "/qr-code-tracking",
|
||||
title: "QR code tracking",
|
||||
text: "Compare scans from table tents, flyers, receipts, and window signs.",
|
||||
},
|
||||
{
|
||||
href: "/qr-code-print-size-guide",
|
||||
title: "QR code print size guide",
|
||||
text: "Pick a practical size before sending table cards or menus to print.",
|
||||
},
|
||||
];
|
||||
|
||||
const schemaData = [
|
||||
breadcrumbSchema([
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Restaurants", url: "/restaurants" },
|
||||
]),
|
||||
faqPageSchema(FAQ),
|
||||
softwareApplicationSchema(),
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
"@id": `${PAGE_URL}#webpage`,
|
||||
url: PAGE_URL,
|
||||
name: "Restaurant Menu QR Codes",
|
||||
description:
|
||||
"Create a dynamic restaurant menu QR code that can be updated after print, with scan analytics and a free start.",
|
||||
inLanguage: "en",
|
||||
isPartOf: {
|
||||
"@id": `${SITE_URL}/#website`,
|
||||
},
|
||||
about: [
|
||||
"dynamic QR codes",
|
||||
"restaurant menu QR codes",
|
||||
"QR code analytics",
|
||||
"menu PDF updates",
|
||||
"QR code reprint savings",
|
||||
],
|
||||
significantLink: relatedResources.map((resource) => `${SITE_URL}${resource.href}`),
|
||||
primaryImageOfPage: {
|
||||
"@type": "ImageObject",
|
||||
url: `${SITE_URL}${HERO_IMAGE}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "HowTo",
|
||||
name: "How to create a restaurant menu QR code that can be updated",
|
||||
description:
|
||||
"Create one dynamic QR code, print it on your table materials, then update the menu destination whenever prices or dishes change.",
|
||||
step: [
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
position: 1,
|
||||
name: "Create a dynamic QR code",
|
||||
text: "Create a QR Master account and choose a dynamic QR code for your restaurant menu.",
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
position: 2,
|
||||
name: "Add your menu destination",
|
||||
text: "Link the QR code to a menu PDF, menu page, ordering link, or hosted restaurant menu.",
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
position: 3,
|
||||
name: "Print the QR code once",
|
||||
text: "Place the same QR code on table tents, printed menus, flyers, window signs, or receipts.",
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
position: 4,
|
||||
name: "Update the destination later",
|
||||
text: "Change the destination in QR Master when prices, dishes, opening hours, or menu files change.",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
absolute: "Restaurant Menu QR Codes | QR Master",
|
||||
},
|
||||
description:
|
||||
"Create one restaurant menu QR code, update the destination after print, and track scans. Built for menus, table tents, flyers, and price changes.",
|
||||
alternates: {
|
||||
canonical: PAGE_URL,
|
||||
languages: {
|
||||
"x-default": PAGE_URL,
|
||||
en: PAGE_URL,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: "Restaurant Menu QR Codes | QR Master",
|
||||
description:
|
||||
"Update restaurant menu links, PDFs, and prices without reprinting your QR code.",
|
||||
url: PAGE_URL,
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: `${SITE_URL}${OG_IMAGE}`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "Restaurant owner using QR Master to update a menu QR code",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Restaurant Menu QR Codes | QR Master",
|
||||
description:
|
||||
"Update restaurant menu links, PDFs, and prices without reprinting your QR code.",
|
||||
images: [`${SITE_URL}${OG_IMAGE}`],
|
||||
},
|
||||
};
|
||||
|
||||
const proofPoints = [
|
||||
"Change menu PDFs after print",
|
||||
"Use one QR code across tables and flyers",
|
||||
"Track scans without exposing guest data",
|
||||
];
|
||||
|
||||
const comparisonRows = [
|
||||
{
|
||||
label: "Menu price changes",
|
||||
static: "Reprint table materials",
|
||||
dynamic: "Update the destination online",
|
||||
},
|
||||
{
|
||||
label: "Seasonal dishes",
|
||||
static: "Old QR code points to old menu",
|
||||
dynamic: "Same QR code points to the new menu",
|
||||
},
|
||||
{
|
||||
label: "Scan analytics",
|
||||
static: "No reliable usage data",
|
||||
dynamic: "See scans, device types, and timing",
|
||||
},
|
||||
{
|
||||
label: "Campaign tracking",
|
||||
static: "Hard to compare placements",
|
||||
dynamic: "Use tagged links for tables, flyers, and windows",
|
||||
},
|
||||
];
|
||||
|
||||
const steps = [
|
||||
{
|
||||
icon: QrCode,
|
||||
title: "Create one dynamic code",
|
||||
text: "Start with a menu QR code that points through QR Master, not directly to a file you cannot change later.",
|
||||
},
|
||||
{
|
||||
icon: FileText,
|
||||
title: "Connect your menu",
|
||||
text: "Use a PDF, website menu, ordering page, or any link your guests should open from the table.",
|
||||
},
|
||||
{
|
||||
icon: RefreshCw,
|
||||
title: "Update when prices change",
|
||||
text: "Switch the destination from your dashboard while the printed QR code stays where it is.",
|
||||
},
|
||||
];
|
||||
|
||||
const useCases = [
|
||||
"Table tents and counter displays",
|
||||
"Printed menus with a digital backup",
|
||||
"Window signs for after-hours browsing",
|
||||
"Flyers for lunch specials or events",
|
||||
"Receipts with feedback and review links",
|
||||
"Seasonal menu PDFs",
|
||||
];
|
||||
|
||||
const reprintExampleRows = [
|
||||
{
|
||||
label: "Table tents",
|
||||
math: "30 pieces x EUR 2.50 x 2 menu changes",
|
||||
value: "EUR 150",
|
||||
},
|
||||
{
|
||||
label: "Lunch flyers",
|
||||
math: "500 pieces x EUR 0.20 x 1 outdated link",
|
||||
value: "EUR 100",
|
||||
},
|
||||
{
|
||||
label: "Window and counter signs",
|
||||
math: "6 pieces x EUR 8.00 x 1 update",
|
||||
value: "EUR 48",
|
||||
},
|
||||
];
|
||||
|
||||
const restaurantScenarios = [
|
||||
{
|
||||
type: "Small cafe",
|
||||
profile: "12 tables, seasonal drinks, one counter sign, and a simple PDF menu.",
|
||||
printRisk:
|
||||
"A coffee price update or new brunch menu can make table cards outdated before the next print run.",
|
||||
qrMasterFit:
|
||||
"Use one dynamic menu QR code for table cards and update the PDF when prices or specials change.",
|
||||
},
|
||||
{
|
||||
type: "Independent restaurant",
|
||||
profile: "30 tables, lunch flyers, takeaway inserts, and a menu that changes several times per year.",
|
||||
printRisk:
|
||||
"Printed flyers and table tents can send guests to old menu files, old prices, or inactive ordering links.",
|
||||
qrMasterFit:
|
||||
"Keep separate dynamic QR codes for tables, flyers, and takeaway materials so scan analytics show which placement works.",
|
||||
},
|
||||
{
|
||||
type: "Multi-location group",
|
||||
profile: "Several locations, local menus, shared brand materials, and recurring print coordination.",
|
||||
printRisk:
|
||||
"One changed PDF or location-specific menu can trigger edits across many printed assets.",
|
||||
qrMasterFit:
|
||||
"Manage destinations centrally while each location keeps its printed QR materials stable.",
|
||||
},
|
||||
];
|
||||
|
||||
const whyItWorks = [
|
||||
{
|
||||
title: "Editable destinations reduce print dependency",
|
||||
text: "A dynamic QR code separates the printed code from the menu destination. That is why a PDF, menu page, or ordering link can change without replacing the physical table card.",
|
||||
href: "/dynamic-qr-code-generator",
|
||||
linkLabel: "Dynamic QR codes",
|
||||
},
|
||||
{
|
||||
title: "Tracking separates placements",
|
||||
text: "Restaurants can use different dynamic codes or tagged URLs for tables, flyers, receipts, and window signs. That makes scan data useful for decisions instead of blending every scan into one number.",
|
||||
href: "/qr-code-tracking",
|
||||
linkLabel: "QR code tracking",
|
||||
},
|
||||
{
|
||||
title: "Print sizing protects the guest experience",
|
||||
text: "The code still has to scan reliably from the table. Correct print size, quiet space, contrast, and test scans matter before sending table tents or menus to print.",
|
||||
href: "/qr-code-print-size-guide",
|
||||
linkLabel: "Print size guide",
|
||||
},
|
||||
];
|
||||
|
||||
function SectionHeading({
|
||||
eyebrow,
|
||||
title,
|
||||
text,
|
||||
inverse = false,
|
||||
}: {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
text: string;
|
||||
inverse?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="max-w-3xl">
|
||||
<p
|
||||
className={`mb-3 text-xs font-semibold uppercase tracking-[0.22em] ${
|
||||
inverse ? "text-blue-300" : "text-blue-700"
|
||||
}`}
|
||||
>
|
||||
{eyebrow}
|
||||
</p>
|
||||
<h2
|
||||
className={`text-3xl font-semibold tracking-tight sm:text-4xl ${
|
||||
inverse ? "text-white" : "text-slate-950"
|
||||
}`}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<p
|
||||
className={`mt-4 text-base leading-7 sm:text-lg ${
|
||||
inverse ? "text-slate-300" : "text-slate-600"
|
||||
}`}
|
||||
>
|
||||
{text}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RestaurantsPage() {
|
||||
return (
|
||||
<>
|
||||
<SeoJsonLd data={schemaData} />
|
||||
<MarketingPageTracker
|
||||
pageType="commercial"
|
||||
cluster="restaurants"
|
||||
useCase="restaurant-menu"
|
||||
/>
|
||||
|
||||
<main className="bg-[#fbfcff] text-slate-950">
|
||||
<section className="overflow-hidden border-b border-slate-200 bg-[linear-gradient(135deg,#f8fbff_0%,#edf4ff_48%,#fff7ed_100%)]">
|
||||
<div className="mx-auto grid max-w-7xl gap-4 px-4 pb-8 pt-5 sm:px-6 lg:grid-cols-[minmax(0,0.92fr)_minmax(500px,1.08fr)] lg:items-start lg:px-8">
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Restaurants", url: "/restaurants" },
|
||||
]}
|
||||
className="lg:col-span-2 [&_a]:text-slate-500 [&_a:hover]:text-slate-950 [&_span]:text-slate-500 [&_[aria-current=page]]:text-slate-950"
|
||||
/>
|
||||
|
||||
<div className="max-w-3xl pt-0 lg:pt-1">
|
||||
<p className="mb-4 inline-flex rounded-md border border-blue-200 bg-white/80 px-3 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-blue-700 shadow-sm">
|
||||
Dynamic menu QR codes for restaurants
|
||||
</p>
|
||||
<h1 className="max-w-3xl text-4xl font-semibold leading-[1.02] tracking-tight text-slate-950 sm:text-5xl lg:text-[4.15rem] xl:text-[4.65rem]">
|
||||
Update your menu QR code without reprinting.
|
||||
</h1>
|
||||
<p className="mt-3 max-w-2xl text-base leading-7 text-slate-700 sm:text-lg">
|
||||
Keep one printed QR code on the table. Change prices, menu PDFs,
|
||||
and ordering links from QR Master when your restaurant changes.
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-3 sm:flex-row">
|
||||
<TrackedCtaLink
|
||||
href={CAMPAIGN_SIGNUP}
|
||||
ctaLabel="Create free menu QR code"
|
||||
ctaLocation="restaurants_hero_primary"
|
||||
pageType="commercial"
|
||||
cluster="restaurants"
|
||||
useCase="restaurant-menu"
|
||||
>
|
||||
<Button className="w-full rounded-md bg-blue-600 px-6 py-3 text-base font-semibold text-white hover:bg-blue-700 sm:w-auto">
|
||||
Create free menu QR code
|
||||
</Button>
|
||||
</TrackedCtaLink>
|
||||
<TrackedCtaLink
|
||||
href="/reprint-calculator"
|
||||
ctaLabel="Calculate reprint savings"
|
||||
ctaLocation="restaurants_hero_secondary"
|
||||
pageType="commercial"
|
||||
cluster="restaurants"
|
||||
useCase="restaurant-menu"
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full rounded-md border-blue-200 bg-white/80 px-6 py-3 text-base text-blue-700 hover:bg-white sm:w-auto"
|
||||
>
|
||||
Calculate reprint savings
|
||||
</Button>
|
||||
</TrackedCtaLink>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="relative pt-0 lg:pt-4">
|
||||
<div className="absolute -inset-4 rounded-[2rem] bg-blue-200/25 blur-2xl" />
|
||||
<div className="relative overflow-hidden rounded-xl border border-white/80 bg-white p-2 shadow-[0_28px_80px_-45px_rgba(15,23,42,0.45)]">
|
||||
<Image
|
||||
src={HERO_IMAGE}
|
||||
alt="Restaurant owner reviewing menu updates with QR Master dashboard"
|
||||
width={1920}
|
||||
height={1080}
|
||||
priority
|
||||
sizes="(min-width: 1024px) 52vw, 100vw"
|
||||
className="aspect-video w-full rounded-lg object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative mt-4 grid gap-2 text-sm text-slate-700 sm:grid-cols-3">
|
||||
{proofPoints.map((point) => (
|
||||
<div
|
||||
key={point}
|
||||
className="flex items-start gap-2 rounded-md border border-blue-100 bg-white/82 px-3 py-2 shadow-sm"
|
||||
>
|
||||
<Check className="mt-0.5 h-4 w-4 shrink-0 text-blue-700" />
|
||||
<span>{point}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-b border-slate-200 bg-white">
|
||||
<div className="mx-auto grid max-w-7xl gap-8 px-4 py-12 sm:px-6 lg:grid-cols-[0.72fr_1fr] lg:px-8">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-blue-700">
|
||||
Direct answer
|
||||
</p>
|
||||
<h2 className="mt-3 text-2xl font-semibold tracking-tight text-slate-950">
|
||||
What is a restaurant menu QR code?
|
||||
</h2>
|
||||
</div>
|
||||
<p className="max-w-3xl text-xl leading-9 text-slate-700">
|
||||
A restaurant menu QR code lets guests open your digital menu from
|
||||
a table, flyer, or window sign. A dynamic menu QR code is better
|
||||
for restaurants because the printed code stays the same while the
|
||||
menu PDF, menu page, or ordering link can change later.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-7xl px-4 py-20 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
eyebrow="Why dynamic matters"
|
||||
title="Menus change faster than printed materials."
|
||||
text="Static QR codes are fine for links that never move. Restaurants need more room to adapt: prices change, dishes sell out, PDFs are replaced, and delivery links move."
|
||||
/>
|
||||
|
||||
<div className="mt-10 overflow-hidden rounded-lg border border-slate-200 bg-white shadow-[0_24px_54px_-44px_rgba(15,23,42,0.45)]">
|
||||
<div className="hidden grid-cols-[1fr_1fr_1fr] border-b border-slate-200 bg-slate-50 text-sm font-semibold text-slate-700 md:grid">
|
||||
<div className="p-4">Restaurant change</div>
|
||||
<div className="border-l border-slate-200 p-4">Static QR code</div>
|
||||
<div className="border-l border-slate-200 p-4">QR Master dynamic code</div>
|
||||
</div>
|
||||
{comparisonRows.map((row) => (
|
||||
<div
|
||||
key={row.label}
|
||||
className="grid grid-cols-1 border-b border-slate-200 last:border-b-0 md:grid-cols-[1fr_1fr_1fr]"
|
||||
>
|
||||
<div className="bg-white p-4 font-medium text-slate-950">
|
||||
{row.label}
|
||||
</div>
|
||||
<div className="border-t border-slate-200 p-4 text-slate-600 md:border-l md:border-t-0">
|
||||
{row.static}
|
||||
</div>
|
||||
<div className="border-t border-slate-200 p-4 text-slate-950 md:border-l md:border-t-0">
|
||||
<span className="inline-flex items-start gap-2">
|
||||
<Check className="mt-1 h-4 w-4 shrink-0 text-green-600" />
|
||||
{row.dynamic}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-y border-blue-100 bg-white">
|
||||
<div className="mx-auto grid max-w-7xl gap-10 px-4 py-16 sm:px-6 lg:grid-cols-[0.85fr_1.15fr] lg:px-8">
|
||||
<div>
|
||||
<SectionHeading
|
||||
eyebrow="Example reprint math"
|
||||
title="A few small menu changes can become a real print bill."
|
||||
text="This is an illustrative restaurant scenario, not a guaranteed saving. The useful point is simple: every QR code destination change that stays digital removes one print-dependent fix."
|
||||
/>
|
||||
<TrackedCtaLink
|
||||
href="/reprint-calculator"
|
||||
ctaLabel="Open reprint calculator"
|
||||
ctaLocation="restaurants_reprint_example"
|
||||
pageType="commercial"
|
||||
cluster="restaurants"
|
||||
useCase="restaurant-menu"
|
||||
>
|
||||
<Button className="mt-7 rounded-md bg-blue-600 px-6 py-3 text-white hover:bg-blue-700">
|
||||
Open reprint calculator
|
||||
</Button>
|
||||
</TrackedCtaLink>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-slate-200 bg-slate-50 p-5 shadow-[0_22px_58px_-44px_rgba(15,23,42,0.45)]">
|
||||
<div className="flex items-center gap-3 border-b border-slate-200 pb-4">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-md bg-blue-100 text-blue-700">
|
||||
<Calculator className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-950">
|
||||
Sample yearly avoidable reprint cost
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600">
|
||||
Restaurant with table tents, flyers, and a few fixed signs.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-slate-200">
|
||||
{reprintExampleRows.map((row) => (
|
||||
<div
|
||||
key={row.label}
|
||||
className="grid gap-2 py-4 sm:grid-cols-[0.8fr_1.35fr_0.5fr] sm:items-center"
|
||||
>
|
||||
<div className="font-medium text-slate-950">{row.label}</div>
|
||||
<div className="text-sm text-slate-600">{row.math}</div>
|
||||
<div className="font-semibold text-slate-950 sm:text-right">
|
||||
{row.value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 rounded-md border border-blue-200 bg-white p-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="font-semibold text-slate-950">
|
||||
Example total
|
||||
</span>
|
||||
<span className="text-2xl font-semibold text-blue-700">
|
||||
EUR 298/year
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-3 text-sm leading-6 text-slate-600">
|
||||
A dynamic restaurant QR code does not remove printing
|
||||
entirely. It prevents a menu PDF, price change, or ordering
|
||||
link change from forcing another print run.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-7xl px-4 py-20 sm:px-6 lg:px-8">
|
||||
<div className="mb-10">
|
||||
<SectionHeading
|
||||
eyebrow="Restaurant scenarios"
|
||||
title="The same QR code workflow scales from cafe to multi-location group."
|
||||
text="These examples show where dynamic menu QR codes usually create the most practical value: menu updates, placement tracking, and less coordination around printed materials."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-5 lg:grid-cols-3">
|
||||
{restaurantScenarios.map((scenario) => (
|
||||
<article
|
||||
key={scenario.type}
|
||||
className="rounded-lg border border-slate-200 bg-white p-6 shadow-[0_20px_54px_-42px_rgba(15,23,42,0.45)]"
|
||||
>
|
||||
<h3 className="text-xl font-semibold text-slate-950">
|
||||
{scenario.type}
|
||||
</h3>
|
||||
<p className="mt-3 text-sm leading-6 text-slate-600">
|
||||
{scenario.profile}
|
||||
</p>
|
||||
|
||||
<div className="mt-6 space-y-5">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">
|
||||
Print risk
|
||||
</p>
|
||||
<p className="mt-2 leading-7 text-slate-700">
|
||||
{scenario.printRisk}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-blue-700">
|
||||
QR Master fit
|
||||
</p>
|
||||
<p className="mt-2 leading-7 text-slate-700">
|
||||
{scenario.qrMasterFit}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-y border-blue-100 bg-[linear-gradient(135deg,#eef5ff_0%,#ffffff_48%,#fff4e6_100%)] py-20">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
eyebrow="Workflow"
|
||||
title="Print once. Update whenever the menu changes."
|
||||
text="The QR code on the table should not be the fragile part of your restaurant workflow. Keep print stable and move the flexible work into the dashboard."
|
||||
/>
|
||||
|
||||
<div className="mt-12 grid gap-5 md:grid-cols-3">
|
||||
{steps.map((step, index) => (
|
||||
<div
|
||||
key={step.title}
|
||||
className="rounded-lg border border-blue-100 bg-white/86 p-6 shadow-[0_18px_48px_-36px_rgba(37,99,235,0.55)]"
|
||||
>
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-md bg-blue-50 text-blue-700">
|
||||
<step.icon className="h-6 w-6" />
|
||||
</div>
|
||||
<span className="font-mono text-sm text-blue-500">
|
||||
0{index + 1}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-slate-950">
|
||||
{step.title}
|
||||
</h3>
|
||||
<p className="mt-4 leading-7 text-slate-600">{step.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto grid max-w-7xl gap-12 px-4 py-20 sm:px-6 lg:grid-cols-[0.9fr_1.1fr] lg:px-8">
|
||||
<div>
|
||||
<SectionHeading
|
||||
eyebrow="For restaurant operators"
|
||||
title="Built for the places where QR codes actually live."
|
||||
text="A restaurant QR code is not only a link. It is part of the table, the menu, the receipt, and the guest experience."
|
||||
/>
|
||||
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
|
||||
<TrackedCtaLink
|
||||
href={CAMPAIGN_SIGNUP}
|
||||
ctaLabel="Start with a free dynamic code"
|
||||
ctaLocation="restaurants_mid_primary"
|
||||
pageType="commercial"
|
||||
cluster="restaurants"
|
||||
useCase="restaurant-menu"
|
||||
>
|
||||
<Button className="rounded-md bg-blue-600 px-6 py-3 text-white hover:bg-blue-700">
|
||||
Start with a free dynamic code
|
||||
</Button>
|
||||
</TrackedCtaLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{useCases.map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
className="flex items-start gap-3 rounded-lg border border-slate-200 bg-white p-4"
|
||||
>
|
||||
<QrCode className="mt-1 h-5 w-5 shrink-0 text-blue-700" />
|
||||
<span className="leading-7 text-slate-700">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-y border-slate-200 bg-white">
|
||||
<div className="mx-auto grid max-w-7xl gap-10 px-4 py-16 sm:px-6 lg:grid-cols-4 lg:px-8">
|
||||
{[
|
||||
{
|
||||
icon: Printer,
|
||||
label: "Reprint control",
|
||||
text: "Avoid replacing table materials just because the destination changed.",
|
||||
},
|
||||
{
|
||||
icon: Link2,
|
||||
label: "Editable destination",
|
||||
text: "Swap PDFs, menu pages, ordering links, and campaign URLs.",
|
||||
},
|
||||
{
|
||||
icon: BarChart3,
|
||||
label: "Scan analytics",
|
||||
text: "See whether guests scan, when they scan, and which devices they use.",
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
label: "Privacy aware",
|
||||
text: "QR Master is built around privacy-conscious analytics for business use.",
|
||||
},
|
||||
].map((item) => (
|
||||
<div key={item.label}>
|
||||
<item.icon className="h-7 w-7 text-blue-700" />
|
||||
<h3 className="mt-5 text-lg font-semibold text-slate-950">
|
||||
{item.label}
|
||||
</h3>
|
||||
<p className="mt-3 text-sm leading-6 text-slate-600">
|
||||
{item.text}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-7xl px-4 py-20 sm:px-6 lg:px-8">
|
||||
<div className="mb-10 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<SectionHeading
|
||||
eyebrow="Related workflows"
|
||||
title="Go deeper before you print."
|
||||
text="These QR Master guides connect the restaurant page to the practical jobs behind it: editable codes, tracking, print sizing, and reprint planning."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{relatedResources.map((resource) => (
|
||||
<Link
|
||||
key={resource.href}
|
||||
href={resource.href}
|
||||
className="group rounded-lg border border-slate-200 bg-white p-5 shadow-sm transition hover:-translate-y-0.5 hover:border-blue-200 hover:shadow-[0_18px_40px_-34px_rgba(37,99,235,0.65)]"
|
||||
>
|
||||
<span className="flex items-center justify-between gap-4 text-base font-semibold text-slate-950">
|
||||
{resource.title}
|
||||
<ArrowRight className="h-4 w-4 shrink-0 text-slate-400 transition group-hover:translate-x-1 group-hover:text-blue-700" />
|
||||
</span>
|
||||
<span className="mt-3 block text-sm leading-6 text-slate-600">
|
||||
{resource.text}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-y border-blue-100 bg-[linear-gradient(135deg,#f7fbff_0%,#ffffff_52%,#fff7ed_100%)]">
|
||||
<div className="mx-auto grid max-w-7xl gap-8 px-4 py-16 sm:px-6 lg:grid-cols-[0.7fr_1.3fr] lg:px-8">
|
||||
<div>
|
||||
<SectionHeading
|
||||
eyebrow="Why this works"
|
||||
title="Dynamic QR codes turn a print problem into a dashboard update."
|
||||
text="For restaurants, the QR code is usually printed before the menu stops changing. The strongest workflow keeps the physical code stable and moves updates, tracking, and testing into QR Master."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{whyItWorks.map((item) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className="rounded-lg border border-blue-100 bg-white/88 p-5 shadow-sm"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-slate-950">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="mt-3 leading-7 text-slate-600">{item.text}</p>
|
||||
<Link
|
||||
href={item.href}
|
||||
className="mt-4 inline-flex items-center gap-2 text-sm font-semibold text-blue-700 hover:text-blue-800"
|
||||
>
|
||||
{item.linkLabel}
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-5xl px-4 py-20 sm:px-6 lg:px-8">
|
||||
<div className="mb-10 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||
<SectionHeading
|
||||
eyebrow="Questions"
|
||||
title="Restaurant menu QR code FAQ"
|
||||
text="Short answers for the decisions restaurant owners usually need to make before printing QR materials."
|
||||
/>
|
||||
<div className="flex items-center gap-2 text-sm text-slate-500">
|
||||
<Clock3 className="h-4 w-4" />
|
||||
<span>Last updated April 30, 2026</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-slate-200 rounded-lg border border-slate-200 bg-white">
|
||||
{FAQ.map((item) => (
|
||||
<details key={item.question} className="group p-5 open:bg-slate-50">
|
||||
<summary className="flex cursor-pointer list-none items-center justify-between gap-6 text-base font-semibold text-slate-950">
|
||||
{item.question}
|
||||
<ArrowRight className="h-4 w-4 shrink-0 text-slate-400 transition-transform group-open:rotate-90" />
|
||||
</summary>
|
||||
<p className="mt-4 max-w-3xl leading-7 text-slate-600">
|
||||
{item.answer}
|
||||
</p>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-y border-blue-100 bg-[linear-gradient(135deg,#f7fbff_0%,#ffffff_55%,#fff7ed_100%)] py-16">
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-8 px-4 sm:px-6 lg:flex-row lg:items-end lg:justify-between lg:px-8">
|
||||
<div className="max-w-3xl">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-blue-700">
|
||||
Ready for the next menu change
|
||||
</p>
|
||||
<h2 className="mt-4 text-3xl font-semibold tracking-tight text-slate-950 sm:text-5xl">
|
||||
Keep the QR code. Change the menu.
|
||||
</h2>
|
||||
<p className="mt-5 text-lg leading-8 text-slate-600">
|
||||
Create a free QR Master account and test the restaurant menu
|
||||
workflow before your next print run.
|
||||
</p>
|
||||
</div>
|
||||
<TrackedCtaLink
|
||||
href={CAMPAIGN_SIGNUP}
|
||||
ctaLabel="Create free menu QR code"
|
||||
ctaLocation="restaurants_footer_primary"
|
||||
pageType="commercial"
|
||||
cluster="restaurants"
|
||||
useCase="restaurant-menu"
|
||||
>
|
||||
<Button className="w-full rounded-md bg-blue-600 px-6 py-3 text-base font-semibold text-white shadow-lg shadow-blue-200 hover:bg-blue-700 sm:w-auto">
|
||||
Create free menu QR code
|
||||
</Button>
|
||||
</TrackedCtaLink>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -183,6 +183,12 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/restaurants`,
|
||||
lastModified: new Date('2026-04-30'),
|
||||
changeFrequency: 'weekly',
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/qr-code-erstellen`,
|
||||
lastModified: new Date(),
|
||||
|
||||
@@ -13,21 +13,36 @@ import {
|
||||
ONBOARDING_DOWNLOAD_COMPLETE_KEY,
|
||||
} from '@/lib/revops';
|
||||
|
||||
type OnboardingChecklistProps = {
|
||||
state: {
|
||||
id?: string;
|
||||
signupSourceSelfReported?: string | null;
|
||||
primaryUseCase?: string | null;
|
||||
firstQrCreatedAt?: string | null;
|
||||
firstDynamicQrAt?: string | null;
|
||||
firstScanAt?: string | null;
|
||||
activationAt?: string | null;
|
||||
} | null;
|
||||
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,
|
||||
@@ -41,6 +56,7 @@ export function OnboardingChecklist({ state }: OnboardingChecklistProps) {
|
||||
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');
|
||||
@@ -55,21 +71,26 @@ export function OnboardingChecklist({ state }: OnboardingChecklistProps) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!state || dismissed) {
|
||||
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 items = getChecklistItems(state).map((item) =>
|
||||
item.id === 'download'
|
||||
? {
|
||||
...item,
|
||||
done: downloadDone || Boolean(state.firstScanAt),
|
||||
}
|
||||
: item
|
||||
);
|
||||
const completed = items.filter((item) => item.done).length;
|
||||
const progress = Math.round((completed / items.length) * 100);
|
||||
const allDone = completed === items.length;
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden rounded-[28px] border border-slate-200 bg-white p-0 shadow-none">
|
||||
|
||||
@@ -67,8 +67,9 @@ export function middleware(req: NextRequest) {
|
||||
'/login',
|
||||
'/signup',
|
||||
'/privacy',
|
||||
'/newsletter',
|
||||
'/tools',
|
||||
'/newsletter',
|
||||
'/restaurants',
|
||||
'/tools',
|
||||
'/features',
|
||||
// '/guide', // Redirected to /learn/*
|
||||
'/qr-code-erstellen',
|
||||
|
||||
Reference in New Issue
Block a user