Compare commits
2 Commits
434b5954c1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79e54f8ae2 | ||
|
|
6ccab068d8 |
@@ -1,34 +1,35 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(docker-compose:*)",
|
||||
"Bash(docker container prune:*)",
|
||||
"Bash(npx prisma migrate dev:*)",
|
||||
"Bash(npx prisma:*)",
|
||||
"Bash(npm run dev)",
|
||||
"Bash(timeout:*)",
|
||||
"Bash(taskkill:*)",
|
||||
"Bash(npx kill-port:*)",
|
||||
"Bash(docker compose:*)",
|
||||
"Bash(curl -I https://fonts.googleapis.com)",
|
||||
"Bash(wsl:*)",
|
||||
"Read(//c/Users/a931627/.ssh/**)",
|
||||
"Bash(ssh-keygen:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(git remote add:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git remote set-url:*)",
|
||||
"Bash(npm install:*)",
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(ls:*)",
|
||||
"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(pkill:*)",
|
||||
"Skill(shadcn-ui)",
|
||||
"Bash(find:*)",
|
||||
"Bash(ls -la \"/c/Users/User/Documents/QR-master/src/app/\\(main\\)/\\(marketing\\)/\")"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(docker-compose:*)",
|
||||
"Bash(docker container prune:*)",
|
||||
"Bash(npx prisma migrate dev:*)",
|
||||
"Bash(npx prisma:*)",
|
||||
"Bash(npm run dev)",
|
||||
"Bash(timeout:*)",
|
||||
"Bash(taskkill:*)",
|
||||
"Bash(npx kill-port:*)",
|
||||
"Bash(docker compose:*)",
|
||||
"Bash(curl -I https://fonts.googleapis.com)",
|
||||
"Bash(wsl:*)",
|
||||
"Read(//c/Users/a931627/.ssh/**)",
|
||||
"Bash(ssh-keygen:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(git remote add:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git remote set-url:*)",
|
||||
"Bash(npm install:*)",
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(ls:*)",
|
||||
"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(pkill:*)",
|
||||
"Skill(shadcn-ui)",
|
||||
"Bash(find:*)",
|
||||
"Bash(ls -la \"/c/Users/User/Documents/QR-master/src/app/\\(main\\)/\\(marketing\\)/\")",
|
||||
"Bash(npx tsc:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"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 { ScrollToTop } from '@/components/ui/ScrollToTop';
|
||||
import { FreeToolsGrid } from '@/components/marketing/FreeToolsGrid';
|
||||
import { Testimonials } from '@/components/marketing/Testimonials';
|
||||
import { getFeaturedTestimonials } from '@/lib/testimonial-data';
|
||||
import { TestimonialsCarousel } from '@/components/marketing/TestimonialsCarousel';
|
||||
import { testimonials } 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',
|
||||
@@ -44,8 +43,8 @@ export default function HomePageClient() {
|
||||
{/* Free Tools Grid */}
|
||||
<FreeToolsGrid />
|
||||
|
||||
{/* Testimonials Section */}
|
||||
<Testimonials testimonials={featuredTestimonials} />
|
||||
{/* Testimonials Carousel */}
|
||||
<TestimonialsCarousel testimonials={testimonials} />
|
||||
|
||||
<React.Fragment>
|
||||
<StaticVsDynamic t={t} />
|
||||
|
||||
@@ -93,18 +93,23 @@ export const Testimonials: React.FC<TestimonialsProps> = ({
|
||||
<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.author.role || testimonial.author.company) && (
|
||||
<div className="text-sm text-gray-700">
|
||||
{testimonial.author.role && (
|
||||
<span>{testimonial.author.role}</span>
|
||||
)}
|
||||
{testimonial.author.role && testimonial.author.company && (
|
||||
<span>, </span>
|
||||
)}
|
||||
{testimonial.author.company && (
|
||||
<span>{testimonial.author.company}</span>
|
||||
)}
|
||||
</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}
|
||||
</span>
|
||||
</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) {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Product',
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'QR Master',
|
||||
description: 'Professional QR code generator with dynamic QR codes, analytics, and customization.',
|
||||
image: `${SITE_URL}/static/og-image.png`,
|
||||
applicationCategory: 'BusinessApplication',
|
||||
operatingSystem: 'Web Browser',
|
||||
url: SITE_URL,
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
price: '0',
|
||||
|
||||
@@ -1,58 +1,212 @@
|
||||
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",
|
||||
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);
|
||||
}
|
||||
import type { Testimonial, AggregateRating } from '@/lib/types';
|
||||
export type { Testimonial, AggregateRating };
|
||||
|
||||
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",
|
||||
role: "Owner",
|
||||
location: "Texas"
|
||||
},
|
||||
date: "January 2026",
|
||||
datePublished: "2026-01-15T00:00:00Z",
|
||||
verified: true,
|
||||
featured: true,
|
||||
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"
|
||||
}
|
||||
];
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user