2 Commits

7 changed files with 1662 additions and 110 deletions

View File

@@ -1,34 +1,35 @@
{ {
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(docker-compose:*)", "Bash(docker-compose:*)",
"Bash(docker container prune:*)", "Bash(docker container prune:*)",
"Bash(npx prisma migrate dev:*)", "Bash(npx prisma migrate dev:*)",
"Bash(npx prisma:*)", "Bash(npx prisma:*)",
"Bash(npm run dev)", "Bash(npm run dev)",
"Bash(timeout:*)", "Bash(timeout:*)",
"Bash(taskkill:*)", "Bash(taskkill:*)",
"Bash(npx kill-port:*)", "Bash(npx kill-port:*)",
"Bash(docker compose:*)", "Bash(docker compose:*)",
"Bash(curl -I https://fonts.googleapis.com)", "Bash(curl -I https://fonts.googleapis.com)",
"Bash(wsl:*)", "Bash(wsl:*)",
"Read(//c/Users/a931627/.ssh/**)", "Read(//c/Users/a931627/.ssh/**)",
"Bash(ssh-keygen:*)", "Bash(ssh-keygen:*)",
"Bash(cat:*)", "Bash(cat:*)",
"Bash(git remote add:*)", "Bash(git remote add:*)",
"Bash(git push:*)", "Bash(git push:*)",
"Bash(git remote set-url:*)", "Bash(git remote set-url:*)",
"Bash(npm install:*)", "Bash(npm install:*)",
"Bash(npm run build:*)", "Bash(npm run build:*)",
"Bash(ls:*)", "Bash(ls:*)",
"Bash(curl:*)", "Bash(curl:*)",
"Bash(echo \"\n\n## CSRF Debug aktiviert!\n\nBitte teste jetzt:\n1. Browser zu http://localhost:3050/create\n2. Dynamic QR Code erstellen versuchen\n3. Server-Logs zeigen jetzt [CSRF Debug] Output\n\nIch sehe dann:\n- Ob headerToken vorhanden ist\n- Ob cookieToken vorhanden ist \n- Ob sie übereinstimmen\n\n---\n\nStripe Portal 500 Error ist separates Problem:\nhttps://dashboard.stripe.com/test/settings/billing/portal\n→ Customer Portal Configuration muss erstellt werden\n\")", "Bash(echo \"\n\n## CSRF Debug aktiviert!\n\nBitte teste jetzt:\n1. Browser zu http://localhost:3050/create\n2. Dynamic QR Code erstellen versuchen\n3. Server-Logs zeigen jetzt [CSRF Debug] Output\n\nIch sehe dann:\n- Ob headerToken vorhanden ist\n- Ob cookieToken vorhanden ist \n- Ob sie übereinstimmen\n\n---\n\nStripe Portal 500 Error ist separates Problem:\nhttps://dashboard.stripe.com/test/settings/billing/portal\n→ Customer Portal Configuration muss erstellt werden\n\")",
"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": [], ],
"ask": [] "deny": [],
} "ask": []
} }
}

File diff suppressed because it is too large Load Diff

View File

@@ -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} />

View File

@@ -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>

View 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>
);
};

View File

@@ -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',

View File

@@ -1,58 +1,212 @@
export type Testimonial = { import type { Testimonial, AggregateRating } from '@/lib/types';
id: string; export type { Testimonial, AggregateRating };
rating: number;
title: string; export const testimonials: Testimonial[] = [
content: string; {
author: { id: "pottery-claudia-knuth-001",
name: string; rating: 5,
location?: string; title: "Perfect for my pottery",
company?: string; 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!",
role?: string; author: {
}; name: "Claudia",
date: string; role: "Owner",
datePublished: string; location: "Texas"
verified: boolean; },
featured: boolean; date: "January 2026",
useCase?: string; datePublished: "2026-01-15T00:00:00Z",
}; verified: true,
featured: true,
export type AggregateRating = { useCase: "pottery"
ratingValue: number; },
reviewCount: number; {
bestRating: number; id: "restaurant-thomas-002",
worstRating: number; 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.",
export const testimonials: Testimonial[] = [ author: {
{ name: "Thomas B.",
id: "pottery-claudia-knuth-001", role: "Restaurant Owner",
rating: 5, location: "Hamburg, Germany"
title: "Perfect for my pottery", },
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!", date: "February 2026",
author: { datePublished: "2026-02-08T00:00:00Z",
name: "Claudia", verified: true,
company: "Hotshpotsh", featured: true,
location: "Texas" useCase: "restaurant-menu"
}, },
date: "January 2026", {
datePublished: "2026-01-15T00:00:00Z", id: "marketing-sarah-003",
verified: true, rating: 5,
featured: true, title: "Finally measurable ROI on print campaigns",
useCase: "pottery" 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",
export function getAggregateRating(): AggregateRating { location: "Berlin, Germany"
const ratings = testimonials.map(t => t.rating); },
const avgRating = ratings.reduce((a, b) => a + b, 0) / ratings.length; date: "March 2026",
return { datePublished: "2026-03-10T00:00:00Z",
ratingValue: Number(avgRating.toFixed(1)), verified: true,
reviewCount: testimonials.length, featured: true,
bestRating: 5, useCase: "marketing-campaigns"
worstRating: 1 },
}; {
} id: "retail-jennifer-004",
rating: 5,
export function getFeaturedTestimonials(): Testimonial[] { title: "Generated 800 unique codes in under 5 minutes",
return testimonials.filter(t => t.featured); 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"
}
];
export function getAggregateRating(): AggregateRating {
const ratings = testimonials.map(t => t.rating);
const avgRating = ratings.reduce((a, b) => a + b, 0) / ratings.length;
return {
ratingValue: Number(avgRating.toFixed(1)),
reviewCount: testimonials.length,
bestRating: 5,
worstRating: 1
};
}
export function getFeaturedTestimonials(): Testimonial[] {
return testimonials.filter(t => t.featured);
}