press releases
This commit is contained in:
parent
be5db36b7f
commit
4dc7c29134
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"next/typescript"
|
||||
]
|
||||
}
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
"autoprefixer": "^10.4.16",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^16.1.1",
|
||||
"eslint-config-next": "16.1.5",
|
||||
"postcss": "^8.4.32",
|
||||
"prettier": "^3.1.1",
|
||||
"prisma": "^5.7.0",
|
||||
|
|
@ -2625,9 +2625,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.1.tgz",
|
||||
"integrity": "sha512-Ovb/6TuLKbE1UiPcg0p39Ke3puyTCIKN9hGbNItmpQsp+WX3qrjO3WaMVSi6JHr9X1NrmthqIguVHodMJbh/dw==",
|
||||
"version": "16.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.5.tgz",
|
||||
"integrity": "sha512-gUWcEsOl+1W7XakmouClcJ0TNFCkblvDUho31wulbDY9na0C6mGtBTSXGRU5GXJY65GjGj0zNaCD/GaBp888Mg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -6783,13 +6783,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint-config-next": {
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.1.tgz",
|
||||
"integrity": "sha512-55nTpVWm3qeuxoQKLOjQVciKZJUphKrNM0fCcQHAIOGl6VFXgaqeMfv0aKJhs7QtcnlAPhNVqsqRfRjeKBPIUA==",
|
||||
"version": "16.1.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.5.tgz",
|
||||
"integrity": "sha512-XwXyv65DC1HXI3gMxm13jvgx0IxKu6XhZhIWTfCDt4c45njHYUM2pk1Y8QXMAWMMnqPy94I2OLMmvIrNGcwLwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "16.1.1",
|
||||
"@next/eslint-plugin-next": "16.1.5",
|
||||
"eslint-import-resolver-node": "^0.3.6",
|
||||
"eslint-import-resolver-typescript": "^3.5.2",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@
|
|||
"autoprefixer": "^10.4.16",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^16.1.1",
|
||||
"eslint-config-next": "16.1.5",
|
||||
"postcss": "^8.4.32",
|
||||
"prettier": "^3.1.1",
|
||||
"prisma": "^5.7.0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 0H0V24" stroke="currentColor" stroke-width="0.5" stroke-opacity="0.1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 193 B |
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import SeoJsonLd from '@/components/SeoJsonLd';
|
||||
import { organizationSchema, websiteSchema, softwareApplicationSchema } from '@/lib/schema';
|
||||
import { organizationSchema, websiteSchema, softwareApplicationSchema, reviewSchema, aggregateRatingSchema } from '@/lib/schema';
|
||||
import { getFeaturedTestimonials, getAggregateRating } from '@/lib/testimonial-data';
|
||||
import HomePageClient from '@/components/marketing/HomePageClient';
|
||||
|
||||
function truncateAtWord(text: string, maxLength: number): string {
|
||||
|
|
@ -52,9 +53,19 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const featuredTestimonials = getFeaturedTestimonials();
|
||||
const aggregateRating = getAggregateRating();
|
||||
const reviewSchemas = featuredTestimonials.map(t => reviewSchema(t));
|
||||
|
||||
return (
|
||||
<>
|
||||
<SeoJsonLd data={[websiteSchema(), organizationSchema(), softwareApplicationSchema()]} />
|
||||
<SeoJsonLd data={[
|
||||
websiteSchema(),
|
||||
organizationSchema(),
|
||||
softwareApplicationSchema(),
|
||||
aggregateRatingSchema(aggregateRating),
|
||||
...reviewSchemas
|
||||
]} />
|
||||
|
||||
{/* Server-rendered SEO content for crawlers */}
|
||||
<div className="sr-only" aria-hidden="false">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
import React from 'react';
|
||||
import { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import SeoJsonLd from '@/components/SeoJsonLd';
|
||||
import { organizationSchema, websiteSchema } from '@/lib/schema';
|
||||
import { ChevronRight, ExternalLink, Newspaper, Award, Calendar } from 'lucide-react';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Press & News | QR Master',
|
||||
description: 'Latest news, press releases, and updates from QR Master. Stay informed about new features, company announcements, and industry insights.',
|
||||
keywords: ['qr master press', 'qr code generator news', 'company updates', 'press releases'],
|
||||
openGraph: {
|
||||
title: 'Press & News | QR Master',
|
||||
description: 'Latest news, press releases, and updates from QR Master.',
|
||||
url: 'https://www.qrmaster.net/press',
|
||||
type: 'website',
|
||||
}
|
||||
};
|
||||
|
||||
export default function PressPage() {
|
||||
const pressReleases = [
|
||||
{
|
||||
id: "launch-2026",
|
||||
title: "qrmaster.net Launches Free, Professional QR Code Generator for Global Users",
|
||||
date: "January 27, 2026",
|
||||
excerpt: "Duesseldorf-based startup unveils qrmaster.net, a comprehensive, free online QR code generator designed to serve as the ultimate bridge between the physical and digital worlds.",
|
||||
bullets: [
|
||||
"Advanced tracking capabilities with UTM builder for GA4 integration",
|
||||
"100% Free professional templates and design customization",
|
||||
"Privacy-focused architecture with no data selling",
|
||||
"Dynamic QR codes that can be edited after printing"
|
||||
],
|
||||
link: "https://www.prlog.org/13123883-qrmasternet-launches-free-professional-qr-code-generator-for-global-users.html",
|
||||
source: "PRLog"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SeoJsonLd data={[organizationSchema(), websiteSchema()]} />
|
||||
|
||||
<div className="bg-white min-h-screen">
|
||||
{/* Hero Section */}
|
||||
<section className="relative overflow-hidden bg-gradient-to-br from-gray-900 to-gray-800 text-white py-20 sm:py-24">
|
||||
<div className="absolute inset-0 bg-[url('/grid-pattern.svg')] opacity-10"></div>
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl relative z-10">
|
||||
<div className="max-w-3xl">
|
||||
<h1 className="text-4xl sm:text-5xl font-bold mb-6 tracking-tight">
|
||||
Newsroom
|
||||
</h1>
|
||||
<p className="text-xl text-gray-300 max-w-2xl leading-relaxed">
|
||||
Latest updates, press releases, and announcements from the QR Master team.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Press Releases List */}
|
||||
<section className="py-16 sm:py-24">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-5xl">
|
||||
<div className="flex items-center gap-3 mb-12">
|
||||
<Newspaper className="w-6 h-6 text-blue-600" />
|
||||
<h2 className="text-2xl font-bold text-gray-900">Latest Releases</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-12">
|
||||
{pressReleases.map((pr) => (
|
||||
<div key={pr.id} className="group relative bg-white border border-gray-100 rounded-2xl shadow-sm hover:shadow-md transition-all duration-300 overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-1 h-full bg-blue-600 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
|
||||
<div className="p-8 sm:p-10">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500 font-medium">
|
||||
<Calendar className="w-4 h-4" />
|
||||
{pr.date}
|
||||
</div>
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-50 text-blue-700">
|
||||
Press Release
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-4 group-hover:text-blue-600 transition-colors">
|
||||
<a href={pr.link} target="_blank" rel="noopener noreferrer" className="focus:outline-none">
|
||||
<span className="absolute inset-0" aria-hidden="true"></span>
|
||||
{pr.title}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<p className="text-gray-600 text-lg mb-8 leading-relaxed">
|
||||
{pr.excerpt}
|
||||
</p>
|
||||
|
||||
<div className="bg-gray-50 rounded-xl p-6 mb-8">
|
||||
<h4 className="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">Highlights</h4>
|
||||
<ul className="grid sm:grid-cols-2 gap-4">
|
||||
{pr.bullets.map((bullet, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2 text-gray-700">
|
||||
<span className="mt-1.5 w-1.5 h-1.5 rounded-full bg-blue-500 flex-shrink-0"></span>
|
||||
<span>{bullet}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mt-auto">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<span>Source:</span>
|
||||
<span className="font-semibold text-gray-900">{pr.source}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-blue-600 font-semibold group-hover:gap-2 transition-all">
|
||||
Read full release <ExternalLink className="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Media Kit CTA (Future proofing) */}
|
||||
<section className="bg-gray-50 py-16 border-t border-gray-100">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-5xl text-center">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Media Inquiries</h2>
|
||||
<p className="text-gray-600 mb-8 max-w-xl mx-auto">
|
||||
For press inquiries, assets, or interview requests from our leadership team.
|
||||
</p>
|
||||
<a href="mailto:press@qrmaster.net">
|
||||
<Button variant="outline" size="lg">
|
||||
Contact Press Team
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Metadata } from 'next';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import SeoJsonLd from '@/components/SeoJsonLd';
|
||||
import { organizationSchema, reviewSchema, aggregateRatingSchema } from '@/lib/schema';
|
||||
import { testimonials, getAggregateRating } from '@/lib/testimonial-data';
|
||||
import { Testimonials } from '@/components/marketing/Testimonials';
|
||||
import { Star } from 'lucide-react';
|
||||
|
||||
function truncateAtWord(text: string, maxLength: number): string {
|
||||
if (text.length <= maxLength) return text;
|
||||
const truncated = text.slice(0, maxLength);
|
||||
const lastSpace = truncated.lastIndexOf(' ');
|
||||
return lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated;
|
||||
}
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const title = truncateAtWord('Customer Testimonials | QR Master Reviews', 60);
|
||||
const description = truncateAtWord(
|
||||
'Read what our customers say about QR Master. Real reviews from businesses using dynamic QR codes for restaurants, pottery, retail, events, and more.',
|
||||
160
|
||||
);
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
keywords: ['qr master reviews', 'qr code testimonials', 'customer reviews', 'qr code generator reviews', 'dynamic qr code reviews'],
|
||||
alternates: {
|
||||
canonical: 'https://www.qrmaster.net/testimonials',
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: 'https://www.qrmaster.net/testimonials',
|
||||
type: 'website',
|
||||
images: [
|
||||
{
|
||||
url: 'https://www.qrmaster.net/og-image.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'QR Master Customer Testimonials',
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function TestimonialsPage() {
|
||||
const aggregateRating = getAggregateRating();
|
||||
const reviewSchemas = testimonials.map(t => reviewSchema(t));
|
||||
|
||||
return (
|
||||
<>
|
||||
<SeoJsonLd data={[
|
||||
organizationSchema(),
|
||||
aggregateRatingSchema(aggregateRating),
|
||||
...reviewSchemas
|
||||
]} />
|
||||
|
||||
<div className="bg-white">
|
||||
{/* Hero Section with Aggregate Rating */}
|
||||
<section className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-white to-purple-50 py-20 sm:py-24">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-5xl text-center">
|
||||
<h1 className="text-4xl sm:text-5xl font-bold text-gray-900 leading-tight mb-6">
|
||||
Customer <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-purple-600">Testimonials</span>
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 max-w-2xl mx-auto mb-8 leading-relaxed">
|
||||
Real experiences from businesses using QR Master to create dynamic QR codes
|
||||
</p>
|
||||
|
||||
{/* Aggregate Rating Display */}
|
||||
<div className="flex flex-col items-center justify-center gap-3 mb-10">
|
||||
<div className="flex gap-1" aria-label={`${aggregateRating.ratingValue} out of 5 stars`}>
|
||||
{[...Array(5)].map((_, index) => (
|
||||
<Star
|
||||
key={index}
|
||||
className={`w-8 h-8 ${
|
||||
index < aggregateRating.ratingValue
|
||||
? 'fill-yellow-400 text-yellow-400'
|
||||
: 'fill-gray-200 text-gray-200'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-lg text-gray-700">
|
||||
<span className="font-bold text-2xl">{aggregateRating.ratingValue}</span> out of 5 stars
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Based on {aggregateRating.reviewCount} {aggregateRating.reviewCount === 1 ? 'review' : 'reviews'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
<Link href="/signup">
|
||||
<Button size="lg" className="text-lg px-8 py-6 shadow-lg shadow-blue-500/25">
|
||||
Get Started Free
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Testimonials Grid */}
|
||||
<Testimonials
|
||||
testimonials={testimonials}
|
||||
showAll={true}
|
||||
title="What Our Customers Are Saying"
|
||||
subtitle="Discover how businesses use QR Master for their unique needs"
|
||||
/>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-20 bg-gradient-to-br from-blue-50 via-white to-purple-50">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center">
|
||||
<h2 className="text-3xl sm:text-4xl font-bold text-gray-900 mb-6">
|
||||
Ready to create your own QR codes?
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600 mb-10 max-w-2xl mx-auto">
|
||||
Join businesses using QR Master to create dynamic, trackable QR codes for their products, menus, events, and campaigns.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
<Link href="/signup">
|
||||
<Button size="lg" className="text-lg px-8 py-6">
|
||||
Start Free Today
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/pricing">
|
||||
<Button variant="outline" size="lg" className="text-lg px-8 py-6">
|
||||
View Pricing
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -175,6 +175,18 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|||
changeFrequency: 'yearly',
|
||||
priority: 0.6,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/press`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/testimonials`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.7,
|
||||
},
|
||||
|
||||
|
||||
...toolPages,
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export default function AdBanner({
|
|||
session.user.plan === 'LIFETIME'
|
||||
);
|
||||
|
||||
if (shouldExclude) return null;
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// Don't load if loading session or if user is paid
|
||||
|
|
@ -92,6 +92,8 @@ export default function AdBanner({
|
|||
// Don't render anything while session is loading
|
||||
if (status === 'loading') return null;
|
||||
|
||||
if (shouldExclude) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`ad-container flex justify-center items-center overflow-hidden transition-opacity duration-300 ${adFilled ? 'opacity-100' : 'opacity-0 h-0'} ${className}`}
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ const AIComingSoonBanner = () => {
|
|||
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-blue-100 mb-4 animate-pulse">
|
||||
<Sparkles className="w-4 h-4 text-blue-600" />
|
||||
<span className="text-sm font-medium text-blue-700">
|
||||
Coming Soon
|
||||
The Future is Here
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -14,11 +14,14 @@ import { Button } from '@/components/ui/Button';
|
|||
import { ReprintCalculatorTeaser } from '@/components/marketing/ReprintCalculatorTeaser';
|
||||
import { ScrollToTop } from '@/components/ui/ScrollToTop';
|
||||
import { FreeToolsGrid } from '@/components/marketing/FreeToolsGrid';
|
||||
import { Testimonials } from '@/components/marketing/Testimonials';
|
||||
import { getFeaturedTestimonials } from '@/lib/testimonial-data';
|
||||
import en from '@/i18n/en.json';
|
||||
|
||||
export default function HomePageClient() {
|
||||
// Always use English for marketing pages
|
||||
const t = en;
|
||||
const featuredTestimonials = getFeaturedTestimonials();
|
||||
|
||||
const industries = [
|
||||
'Restaurant Chain',
|
||||
|
|
@ -41,6 +44,9 @@ export default function HomePageClient() {
|
|||
{/* Free Tools Grid */}
|
||||
<FreeToolsGrid />
|
||||
|
||||
{/* Testimonials Section */}
|
||||
<Testimonials testimonials={featuredTestimonials} />
|
||||
|
||||
<React.Fragment>
|
||||
<StaticVsDynamic t={t} />
|
||||
<ReprintCalculatorTeaser />
|
||||
|
|
|
|||
|
|
@ -29,6 +29,15 @@ export const StatsStrip: React.FC<StatsStripProps> = ({ t }) => {
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Market Context - Citation / AEO Trust Signal */}
|
||||
<div className="mt-12 pt-8 border-t border-gray-100 text-center">
|
||||
<p className="text-sm text-gray-500 max-w-3xl mx-auto leading-relaxed">
|
||||
Market Context: The global QR code payment market is projected to grow from <span className="font-semibold text-gray-700">$15.9B (2025)</span> to <span className="font-semibold text-gray-700">$38B (2030)</span> <a href="https://finance.yahoo.com/news/analysis-global-qr-code-payments-155300360.html" target="_blank" rel="nofollow external" className="text-blue-600 hover:underline decoration-blue-200 underline-offset-2">(Yahoo Finance)</a>.
|
||||
<span className="mx-2">•</span>
|
||||
94% of marketers increased their QR code usage in 2025 <a href="https://bitly.com/blog/qr-code-statistics/" target="_blank" rel="nofollow external" className="text-blue-600 hover:underline decoration-blue-200 underline-offset-2">(Bitly)</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Star, CheckCircle } from 'lucide-react';
|
||||
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||
import type { Testimonial } from '@/lib/types';
|
||||
|
||||
interface TestimonialsProps {
|
||||
testimonials: Testimonial[];
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
showAll?: boolean;
|
||||
}
|
||||
|
||||
export const Testimonials: React.FC<TestimonialsProps> = ({
|
||||
testimonials,
|
||||
title = "What Our Customers Say",
|
||||
subtitle = "Real experiences from businesses using QR Master",
|
||||
showAll = false
|
||||
}) => {
|
||||
const displayTestimonials = showAll ? testimonials : testimonials.slice(0, 3);
|
||||
|
||||
const renderStars = (rating: number) => {
|
||||
return (
|
||||
<div className="flex gap-1" aria-label={`${rating} out of 5 stars`}>
|
||||
{[...Array(5)].map((_, index) => (
|
||||
<Star
|
||||
key={index}
|
||||
className={`w-5 h-5 ${index < rating
|
||||
? 'fill-yellow-400 text-yellow-400'
|
||||
: 'fill-gray-200 text-gray-200'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="py-16 bg-white">
|
||||
<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 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>
|
||||
</motion.div>
|
||||
|
||||
<div className={`grid gap-8 ${displayTestimonials.length === 1
|
||||
? 'grid-cols-1 max-w-2xl mx-auto'
|
||||
: displayTestimonials.length === 2
|
||||
? 'grid-cols-1 md:grid-cols-2 max-w-4xl mx-auto'
|
||||
: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3'
|
||||
}`}>
|
||||
{displayTestimonials.map((testimonial, index) => (
|
||||
<motion.div
|
||||
key={testimonial.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
>
|
||||
<Card hover className="h-full flex flex-col">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
{renderStars(testimonial.rating)}
|
||||
{testimonial.verified && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 text-green-700 text-xs font-medium rounded-full">
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
Verified
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
{testimonial.title}
|
||||
</h3>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-grow">
|
||||
<p className="text-gray-700 leading-relaxed mb-6">
|
||||
{testimonial.content}
|
||||
</p>
|
||||
<div className="border-t border-gray-200 pt-4 mt-auto">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold text-gray-900">
|
||||
{testimonial.author.name}
|
||||
</span>
|
||||
<div className="text-sm text-gray-600">
|
||||
{testimonial.author.company && (
|
||||
<span>{testimonial.author.company}</span>
|
||||
)}
|
||||
{testimonial.author.company && testimonial.author.location && (
|
||||
<span> • </span>
|
||||
)}
|
||||
{testimonial.author.location && (
|
||||
<span>{testimonial.author.location}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 mt-1">
|
||||
{testimonial.date}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
{!showAll && (
|
||||
<div className="mt-12 text-center">
|
||||
<a href="/testimonials" className="inline-flex items-center text-blue-600 font-semibold hover:text-blue-700 transition-colors">
|
||||
See all reviews
|
||||
<svg className="w-4 h-4 ml-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section >
|
||||
);
|
||||
};
|
||||
|
|
@ -44,6 +44,8 @@ export function Footer({ variant = 'marketing', t }: FooterProps) {
|
|||
<ul className={`space-y-2 ${isDashboard ? 'text-gray-500' : 'text-gray-400'}`}>
|
||||
<li><Link href="/features" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>{translations.features}</Link></li>
|
||||
<li><Link href="/about" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>About</Link></li>
|
||||
<li><Link href="/press" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Press</Link></li>
|
||||
<li><Link href="/testimonials" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Testimonials</Link></li>
|
||||
<li><Link href="/authors/timo" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Timo Knuth (Author)</Link></li>
|
||||
<li><Link href="/#pricing" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>{translations.pricing}</Link></li>
|
||||
<li><Link href="/qr-code-tracking" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>QR Analytics</Link></li>
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ export const pillarMeta: PillarMeta[] = [
|
|||
description: "Quishing prevention and safe QR rollouts.",
|
||||
quickAnswer: "Security is critical for trust. Learn how to prevent 'Quishing' (QR Phishing), validate links, and ensure your QR code campaigns remain safe for your users.",
|
||||
miniFaq: [
|
||||
{ question: "What is Quishing?", answer: "<strong>Quishing</strong> (QR Phishing) tricks users into scanning malicious QR codes that steal credentials or install malware." },
|
||||
{ question: "How to prevent QR code fraud?", answer: "Use short, branded links. Enable URL preview before redirect. Educate users to check the destination before scanning unknown codes." },
|
||||
{ question: "What is Quishing?", answer: "<strong>Quishing</strong> (QR Phishing) tricks users into scanning malicious QR codes. <cite><a href='https://www.ic3.gov' target='_blank' rel='nofollow external' class='text-blue-600 hover:underline'>(Source: FBI IC3 Warning Jan 2026)</a></cite>" },
|
||||
{ question: "How to prevent QR code fraud?", answer: "Verify the source. Malicious QR campaigns rose significantly in 2025. <cite><a href='https://bitly.com/blog/qr-code-statistics/' target='_blank' rel='nofollow external' class='text-blue-600 hover:underline'>(Source: Bitly Trends)</a></cite>" },
|
||||
{ question: "Are dynamic QR codes secure?", answer: "Yes, when hosted on trusted platforms with HTTPS, access logs, and link expiration. Avoid free generators with sketchy redirects." },
|
||||
{ question: "Can QR codes be hacked?", answer: "QR codes themselves can't be hacked, but attackers can overlay fake codes on legitimate ones. Use tamper-proof stickers and regular audits." }
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { BlogPost, AuthorProfile, PillarMeta } from "./types";
|
||||
import type { BlogPost, AuthorProfile, PillarMeta, Testimonial, AggregateRating } from "./types";
|
||||
|
||||
const SITE_URL = "https://www.qrmaster.net";
|
||||
|
||||
|
|
@ -244,3 +244,44 @@ export function articleSchema(params: {
|
|||
url: params.url,
|
||||
};
|
||||
}
|
||||
|
||||
export function reviewSchema(testimonial: Testimonial) {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Review',
|
||||
itemReviewed: {
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'QR Master',
|
||||
applicationCategory: 'BusinessApplication',
|
||||
operatingSystem: 'Web Browser'
|
||||
},
|
||||
reviewRating: {
|
||||
'@type': 'Rating',
|
||||
ratingValue: testimonial.rating,
|
||||
bestRating: 5,
|
||||
worstRating: 1
|
||||
},
|
||||
author: {
|
||||
'@type': 'Person',
|
||||
name: testimonial.author.name
|
||||
},
|
||||
datePublished: testimonial.datePublished,
|
||||
reviewBody: testimonial.content,
|
||||
headline: testimonial.title
|
||||
};
|
||||
}
|
||||
|
||||
export function aggregateRatingSchema(aggregateRating: AggregateRating) {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Product',
|
||||
name: 'QR Master',
|
||||
aggregateRating: {
|
||||
'@type': 'AggregateRating',
|
||||
ratingValue: aggregateRating.ratingValue,
|
||||
reviewCount: aggregateRating.reviewCount,
|
||||
bestRating: aggregateRating.bestRating,
|
||||
worstRating: aggregateRating.worstRating
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
export type Testimonial = {
|
||||
id: string;
|
||||
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[] = [
|
||||
{
|
||||
id: "pottery-claudia-knuth-001",
|
||||
rating: 5,
|
||||
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!",
|
||||
author: {
|
||||
name: "Claudia Knuth",
|
||||
company: "Hotshpotsh",
|
||||
location: "Texas"
|
||||
},
|
||||
date: "January 2026",
|
||||
datePublished: "2026-01-15T00:00:00Z",
|
||||
verified: true,
|
||||
featured: true,
|
||||
useCase: "pottery"
|
||||
}
|
||||
];
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
@ -65,3 +65,28 @@ export type PillarMeta = {
|
|||
miniFaq?: FAQItem[];
|
||||
order: number;
|
||||
};
|
||||
|
||||
export type Testimonial = {
|
||||
id: string;
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ export function middleware(req: NextRequest) {
|
|||
'/about',
|
||||
'/learn',
|
||||
'/authors',
|
||||
'/press',
|
||||
'/testimonials',
|
||||
];
|
||||
|
||||
// Check if path is public
|
||||
|
|
|
|||
Loading…
Reference in New Issue