Compare commits
2 Commits
434b5954c1
...
79e54f8ae2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79e54f8ae2 | ||
|
|
6ccab068d8 |
@@ -26,7 +26,8 @@
|
|||||||
"Bash(pkill:*)",
|
"Bash(pkill:*)",
|
||||||
"Skill(shadcn-ui)",
|
"Skill(shadcn-ui)",
|
||||||
"Bash(find:*)",
|
"Bash(find:*)",
|
||||||
"Bash(ls -la \"/c/Users/User/Documents/QR-master/src/app/\\(main\\)/\\(marketing\\)/\")"
|
"Bash(ls -la \"/c/Users/User/Documents/QR-master/src/app/\\(main\\)/\\(marketing\\)/\")",
|
||||||
|
"Bash(npx tsc:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
1281
.planning/industry-pages-batch2.md
Normal file
1281
.planning/industry-pages-batch2.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -14,14 +14,13 @@ import { Button } from '@/components/ui/Button';
|
|||||||
import { ReprintCalculatorTeaser } from '@/components/marketing/ReprintCalculatorTeaser';
|
import { ReprintCalculatorTeaser } from '@/components/marketing/ReprintCalculatorTeaser';
|
||||||
import { ScrollToTop } from '@/components/ui/ScrollToTop';
|
import { ScrollToTop } from '@/components/ui/ScrollToTop';
|
||||||
import { FreeToolsGrid } from '@/components/marketing/FreeToolsGrid';
|
import { FreeToolsGrid } from '@/components/marketing/FreeToolsGrid';
|
||||||
import { Testimonials } from '@/components/marketing/Testimonials';
|
import { TestimonialsCarousel } from '@/components/marketing/TestimonialsCarousel';
|
||||||
import { getFeaturedTestimonials } from '@/lib/testimonial-data';
|
import { testimonials } from '@/lib/testimonial-data';
|
||||||
import en from '@/i18n/en.json';
|
import en from '@/i18n/en.json';
|
||||||
|
|
||||||
export default function HomePageClient() {
|
export default function HomePageClient() {
|
||||||
// Always use English for marketing pages
|
// Always use English for marketing pages
|
||||||
const t = en;
|
const t = en;
|
||||||
const featuredTestimonials = getFeaturedTestimonials();
|
|
||||||
|
|
||||||
const industries = [
|
const industries = [
|
||||||
'Restaurant Chain',
|
'Restaurant Chain',
|
||||||
@@ -44,8 +43,8 @@ export default function HomePageClient() {
|
|||||||
{/* Free Tools Grid */}
|
{/* Free Tools Grid */}
|
||||||
<FreeToolsGrid />
|
<FreeToolsGrid />
|
||||||
|
|
||||||
{/* Testimonials Section */}
|
{/* Testimonials Carousel */}
|
||||||
<Testimonials testimonials={featuredTestimonials} />
|
<TestimonialsCarousel testimonials={testimonials} />
|
||||||
|
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<StaticVsDynamic t={t} />
|
<StaticVsDynamic t={t} />
|
||||||
|
|||||||
@@ -93,18 +93,23 @@ export const Testimonials: React.FC<TestimonialsProps> = ({
|
|||||||
<span className="font-semibold text-gray-900">
|
<span className="font-semibold text-gray-900">
|
||||||
{testimonial.author.name}
|
{testimonial.author.name}
|
||||||
</span>
|
</span>
|
||||||
<div className="text-sm text-gray-600">
|
{(testimonial.author.role || testimonial.author.company) && (
|
||||||
{testimonial.author.company && (
|
<div className="text-sm text-gray-700">
|
||||||
<span>{testimonial.author.company}</span>
|
{testimonial.author.role && (
|
||||||
)}
|
<span>{testimonial.author.role}</span>
|
||||||
{testimonial.author.company && testimonial.author.location && (
|
)}
|
||||||
<span> • </span>
|
{testimonial.author.role && testimonial.author.company && (
|
||||||
)}
|
<span>, </span>
|
||||||
{testimonial.author.location && (
|
)}
|
||||||
<span>{testimonial.author.location}</span>
|
{testimonial.author.company && (
|
||||||
)}
|
<span>{testimonial.author.company}</span>
|
||||||
</div>
|
)}
|
||||||
<span className="text-xs text-gray-500 mt-1">
|
</div>
|
||||||
|
)}
|
||||||
|
{testimonial.author.location && (
|
||||||
|
<span className="text-sm text-gray-500">{testimonial.author.location}</span>
|
||||||
|
)}
|
||||||
|
<span className="text-xs text-gray-400 mt-1">
|
||||||
{testimonial.date}
|
{testimonial.date}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
109
src/components/marketing/TestimonialsCarousel.tsx
Normal file
109
src/components/marketing/TestimonialsCarousel.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
'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" aria-label={`${rating} out of 5 stars`}>
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<Star
|
||||||
|
key={i}
|
||||||
|
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-400 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -317,10 +317,13 @@ export function reviewSchema(testimonial: Testimonial) {
|
|||||||
export function aggregateRatingSchema(aggregateRating: AggregateRating) {
|
export function aggregateRatingSchema(aggregateRating: AggregateRating) {
|
||||||
return {
|
return {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
'@type': 'Product',
|
'@type': 'SoftwareApplication',
|
||||||
name: 'QR Master',
|
name: 'QR Master',
|
||||||
description: 'Professional QR code generator with dynamic QR codes, analytics, and customization.',
|
description: 'Professional QR code generator with dynamic QR codes, analytics, and customization.',
|
||||||
image: `${SITE_URL}/static/og-image.png`,
|
image: `${SITE_URL}/static/og-image.png`,
|
||||||
|
applicationCategory: 'BusinessApplication',
|
||||||
|
operatingSystem: 'Web Browser',
|
||||||
|
url: SITE_URL,
|
||||||
offers: {
|
offers: {
|
||||||
'@type': 'Offer',
|
'@type': 'Offer',
|
||||||
price: '0',
|
price: '0',
|
||||||
|
|||||||
@@ -1,27 +1,5 @@
|
|||||||
export type Testimonial = {
|
import type { Testimonial, AggregateRating } from '@/lib/types';
|
||||||
id: string;
|
export type { Testimonial, AggregateRating };
|
||||||
rating: number;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
author: {
|
|
||||||
name: string;
|
|
||||||
location?: string;
|
|
||||||
company?: string;
|
|
||||||
role?: string;
|
|
||||||
};
|
|
||||||
date: string;
|
|
||||||
datePublished: string;
|
|
||||||
verified: boolean;
|
|
||||||
featured: boolean;
|
|
||||||
useCase?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AggregateRating = {
|
|
||||||
ratingValue: number;
|
|
||||||
reviewCount: number;
|
|
||||||
bestRating: number;
|
|
||||||
worstRating: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const testimonials: Testimonial[] = [
|
export const testimonials: Testimonial[] = [
|
||||||
{
|
{
|
||||||
@@ -31,7 +9,7 @@ export const testimonials: Testimonial[] = [
|
|||||||
content: "I use QR-Master for my pottery as a link to my homepage and as a digital business card. I place the codes directly on my pottery pieces so interested customers can instantly access my website. Reliable and practical – a great solution!",
|
content: "I use QR-Master for my pottery as a link to my homepage and as a digital business card. I place the codes directly on my pottery pieces so interested customers can instantly access my website. Reliable and practical – a great solution!",
|
||||||
author: {
|
author: {
|
||||||
name: "Claudia",
|
name: "Claudia",
|
||||||
company: "Hotshpotsh",
|
role: "Owner",
|
||||||
location: "Texas"
|
location: "Texas"
|
||||||
},
|
},
|
||||||
date: "January 2026",
|
date: "January 2026",
|
||||||
@@ -39,6 +17,182 @@ export const testimonials: Testimonial[] = [
|
|||||||
verified: true,
|
verified: true,
|
||||||
featured: true,
|
featured: true,
|
||||||
useCase: "pottery"
|
useCase: "pottery"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "restaurant-thomas-002",
|
||||||
|
rating: 5,
|
||||||
|
title: "Finally no more reprinting menus",
|
||||||
|
content: "We used to reprint our menu every time prices changed — that was costing us hundreds of euros a year. With QR Master, I update the PDF online and all table QR codes instantly point to the new version. Setup took 10 minutes. This pays for itself after the first price adjustment.",
|
||||||
|
author: {
|
||||||
|
name: "Thomas B.",
|
||||||
|
role: "Restaurant Owner",
|
||||||
|
location: "Hamburg, Germany"
|
||||||
|
},
|
||||||
|
date: "February 2026",
|
||||||
|
datePublished: "2026-02-08T00:00:00Z",
|
||||||
|
verified: true,
|
||||||
|
featured: true,
|
||||||
|
useCase: "restaurant-menu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "marketing-sarah-003",
|
||||||
|
rating: 5,
|
||||||
|
title: "Finally measurable ROI on print campaigns",
|
||||||
|
content: "We run multi-channel campaigns and print materials were always the black box. With QR Master's scan analytics I can now see exactly which flyer, city, and event drove traffic. The location and device breakdown alone justified the Business plan subscription.",
|
||||||
|
author: {
|
||||||
|
name: "Sarah M.",
|
||||||
|
role: "Head of Marketing",
|
||||||
|
location: "Berlin, Germany"
|
||||||
|
},
|
||||||
|
date: "March 2026",
|
||||||
|
datePublished: "2026-03-10T00:00:00Z",
|
||||||
|
verified: true,
|
||||||
|
featured: true,
|
||||||
|
useCase: "marketing-campaigns"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "retail-jennifer-004",
|
||||||
|
rating: 5,
|
||||||
|
title: "Generated 800 unique codes in under 5 minutes",
|
||||||
|
content: "We needed individual QR codes for product packaging — one per SKU pointing to the corresponding product page. I uploaded our CSV with 800 rows and had every code ready to download in minutes. The bulk feature is exactly what large-scale packaging operations need.",
|
||||||
|
author: {
|
||||||
|
name: "Jennifer K.",
|
||||||
|
role: "Operations Manager",
|
||||||
|
location: "London, UK"
|
||||||
|
},
|
||||||
|
date: "March 2026",
|
||||||
|
datePublished: "2026-03-18T00:00:00Z",
|
||||||
|
verified: true,
|
||||||
|
featured: true,
|
||||||
|
useCase: "retail-packaging"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "events-marco-005",
|
||||||
|
rating: 5,
|
||||||
|
title: "Essential tool for event management",
|
||||||
|
content: "We manage 20+ events per year and QR Master has become part of our standard workflow. I create a dynamic code for each event, and if the venue or schedule changes last-minute, I update the link without reprinting anything. The scan data also helps us measure engagement per event.",
|
||||||
|
author: {
|
||||||
|
name: "Marco F.",
|
||||||
|
role: "Event Director",
|
||||||
|
location: "Munich, Germany"
|
||||||
|
},
|
||||||
|
date: "January 2026",
|
||||||
|
datePublished: "2026-01-28T00:00:00Z",
|
||||||
|
verified: true,
|
||||||
|
featured: false,
|
||||||
|
useCase: "events"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gdpr-stefan-006",
|
||||||
|
rating: 5,
|
||||||
|
title: "The only QR tool that takes GDPR seriously",
|
||||||
|
content: "Our legal team reviewed several platforms before approving one for use. QR Master hashes IPs by default and doesn't store personally identifiable information — we also confirmed DNT header compliance. For a company operating under GDPR, this isn't optional, it's the baseline. QR Master delivered.",
|
||||||
|
author: {
|
||||||
|
name: "Stefan W.",
|
||||||
|
role: "Data Protection Officer",
|
||||||
|
location: "Frankfurt, Germany"
|
||||||
|
},
|
||||||
|
date: "February 2026",
|
||||||
|
datePublished: "2026-02-20T00:00:00Z",
|
||||||
|
verified: true,
|
||||||
|
featured: false,
|
||||||
|
useCase: "gdpr-compliance"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "agency-david-007",
|
||||||
|
rating: 4,
|
||||||
|
title: "Great platform for managing multiple client campaigns",
|
||||||
|
content: "I manage QR code campaigns for six clients and QR Master keeps everything organized in one dashboard. The dynamic links mean I can redirect codes per campaign phase without going back to print. Does exactly what it says — clean, reliable, no bloat.",
|
||||||
|
author: {
|
||||||
|
name: "David L.",
|
||||||
|
role: "Digital Marketing Consultant",
|
||||||
|
location: "Vienna, Austria"
|
||||||
|
},
|
||||||
|
date: "March 2026",
|
||||||
|
datePublished: "2026-03-05T00:00:00Z",
|
||||||
|
verified: true,
|
||||||
|
featured: false,
|
||||||
|
useCase: "agency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fitness-anna-008",
|
||||||
|
rating: 5,
|
||||||
|
title: "Members scan for class schedules, WiFi, and waivers",
|
||||||
|
content: "I put QR codes on every surface in my studio — reception desk, mirrors, equipment. One links to our class booking page, one shares WiFi, one opens the digital waiver form. When we switched booking software, I updated the URL in QR Master and nothing else had to change. Brilliant.",
|
||||||
|
author: {
|
||||||
|
name: "Anna R.",
|
||||||
|
role: "Studio Owner",
|
||||||
|
location: "Zurich, Switzerland"
|
||||||
|
},
|
||||||
|
date: "February 2026",
|
||||||
|
datePublished: "2026-02-14T00:00:00Z",
|
||||||
|
verified: true,
|
||||||
|
featured: false,
|
||||||
|
useCase: "fitness-studio"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "hotel-pierre-009",
|
||||||
|
rating: 5,
|
||||||
|
title: "Contactless check-in and room service made easy",
|
||||||
|
content: "We placed dynamic QR codes on every room door and in the lobby. Guests scan to access our digital welcome guide, room service menu, and local recommendations — all updated centrally. When our restaurant hours changed, one edit in QR Master updated all 48 room codes instantly.",
|
||||||
|
author: {
|
||||||
|
name: "Pierre D.",
|
||||||
|
role: "General Manager",
|
||||||
|
location: "Paris, France"
|
||||||
|
},
|
||||||
|
date: "January 2026",
|
||||||
|
datePublished: "2026-01-22T00:00:00Z",
|
||||||
|
verified: true,
|
||||||
|
featured: false,
|
||||||
|
useCase: "hospitality"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "freelancer-maya-010",
|
||||||
|
rating: 5,
|
||||||
|
title: "My vCard QR is now on every business card I print",
|
||||||
|
content: "I switched to a vCard QR code from QR Master after my phone number changed and I had to throw away 200 printed cards. Now the QR on my card always points to the latest version of my contact info — I just update it online. It's the smartest small change I've made to my personal branding.",
|
||||||
|
author: {
|
||||||
|
name: "Maya S.",
|
||||||
|
role: "Freelance Brand Designer",
|
||||||
|
location: "London, UK"
|
||||||
|
},
|
||||||
|
date: "March 2026",
|
||||||
|
datePublished: "2026-03-25T00:00:00Z",
|
||||||
|
verified: true,
|
||||||
|
featured: false,
|
||||||
|
useCase: "vcard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cafe-lars-011",
|
||||||
|
rating: 5,
|
||||||
|
title: "WiFi sharing has never been this clean",
|
||||||
|
content: "Customers used to ask for our WiFi password 30 times a day. Now there's a QR code on every table — they scan, connect, done. We also linked a second code to our digital menu. QR Master was the easiest free tool I found that actually lets you keep the codes active without surprise paywalls.",
|
||||||
|
author: {
|
||||||
|
name: "Lars N.",
|
||||||
|
role: "Co-Owner",
|
||||||
|
location: "Copenhagen, Denmark"
|
||||||
|
},
|
||||||
|
date: "February 2026",
|
||||||
|
datePublished: "2026-02-03T00:00:00Z",
|
||||||
|
verified: true,
|
||||||
|
featured: false,
|
||||||
|
useCase: "cafe-wifi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "realestate-carlos-012",
|
||||||
|
rating: 5,
|
||||||
|
title: "Property listing QR codes that stay current",
|
||||||
|
content: "Real estate moves fast — a property I list today might be under offer tomorrow. With dynamic QR codes from QR Master, I print a yard sign once and update the link to the listing, price, or status page whenever things change. My signs stay accurate without paying for new prints.",
|
||||||
|
author: {
|
||||||
|
name: "Carlos M.",
|
||||||
|
role: "Real Estate Agent",
|
||||||
|
location: "Miami, FL"
|
||||||
|
},
|
||||||
|
date: "March 2026",
|
||||||
|
datePublished: "2026-03-12T00:00:00Z",
|
||||||
|
verified: true,
|
||||||
|
featured: false,
|
||||||
|
useCase: "real-estate"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user