Some checks failed
Build and Deploy / build (push) Has been cancelled
WARNING: This change breaks existing SEO URLs for Serbian locale. Changes: - Migrated from separate locale folders (src/app/en/, src/app/de/, etc.) to [locale] dynamic segments (src/app/[locale]/) - Serbian is now at /sr/ instead of / (root) - English at /en/, German at /de/, French at /fr/ - All components updated to generate locale-aware links - Root / now redirects to /sr (307 temporary redirect) SEO Impact: - Previously indexed Serbian URLs (/, /products, /about, /contact) will now return 404 or redirect to /sr/* URLs - This is a breaking change for SEO - Serbian pages should ideally remain at root (/) with only non-default locales getting prefix - Consider implementing 301 redirects from old URLs to maintain search engine rankings Technical Notes: - next-intl v4 with [locale] structure requires ALL locales to have the prefix (cannot have default locale at root) - Alternative approach would be separate folder structure per locale
134 lines
4.9 KiB
TypeScript
134 lines
4.9 KiB
TypeScript
"use client";
|
|
|
|
import { motion } from "framer-motion";
|
|
import { useTranslations } from "next-intl";
|
|
|
|
interface Review {
|
|
id: number;
|
|
name: string;
|
|
location: string;
|
|
text: string;
|
|
rating: number;
|
|
}
|
|
|
|
interface ProductReviewsProps {
|
|
locale?: string;
|
|
productName?: string;
|
|
}
|
|
|
|
function ReviewCard({ review }: { review: Review }) {
|
|
return (
|
|
<div className="flex-shrink-0 w-80 bg-white p-6 rounded-2xl shadow-sm border border-[#f0ede8] mx-3">
|
|
<div className="flex items-center gap-1 mb-3">
|
|
{[1, 2, 3, 4, 5].map((star) => (
|
|
<svg key={star} className="w-4 h-4 fill-yellow-400 text-yellow-400" viewBox="0 0 24 24">
|
|
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
|
</svg>
|
|
))}
|
|
</div>
|
|
<p className="text-[#444444] text-sm leading-relaxed mb-4">"{review.text}"</p>
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-full bg-[#1a1a1a] flex items-center justify-center text-white text-sm font-medium">
|
|
{review.name.charAt(0)}
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center gap-1.5">
|
|
<p className="text-sm font-medium">{review.name}</p>
|
|
<svg className="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
<span className="text-xs text-green-700 font-medium">Verified</span>
|
|
</div>
|
|
<p className="text-xs text-[#888888]">{review.location}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function ProductReviews(_props: ProductReviewsProps) {
|
|
const t = useTranslations("ProductReviews");
|
|
const reviews = t.raw("reviews") as Review[];
|
|
|
|
return (
|
|
<section className="py-16 bg-[#faf9f7] overflow-hidden">
|
|
<div className="container mx-auto px-4 mb-8">
|
|
<motion.div
|
|
className="text-center"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.6 }}
|
|
>
|
|
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
|
|
{t("customerReviews")}
|
|
</span>
|
|
<h2 className="text-3xl md:text-4xl font-medium">
|
|
{t("whatCustomersSay")}
|
|
</h2>
|
|
|
|
<div className="flex items-center justify-center gap-4 mt-4">
|
|
<span className="text-5xl font-bold text-[#1a1a1a]">4.9</span>
|
|
<div>
|
|
<div className="flex gap-0.5">
|
|
{[1, 2, 3, 4, 5].map((star) => (
|
|
<svg key={star} className="w-5 h-5 fill-yellow-400 text-yellow-400" viewBox="0 0 24 24">
|
|
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
|
</svg>
|
|
))}
|
|
</div>
|
|
<p className="text-sm text-[#666666] mt-1">{t("basedOnReviews")}</p>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
|
|
<div className="relative">
|
|
<div className="absolute left-0 top-0 bottom-0 w-20 bg-gradient-to-r from-[#faf9f7] to-transparent z-10 pointer-events-none" />
|
|
<div className="absolute right-0 top-0 bottom-0 w-20 bg-gradient-to-l from-[#faf9f7] to-transparent z-10 pointer-events-none" />
|
|
|
|
<div className="flex overflow-hidden mb-4">
|
|
<motion.div
|
|
className="flex items-center gap-0"
|
|
animate={{
|
|
x: [0, -50 + "%"],
|
|
}}
|
|
transition={{
|
|
x: {
|
|
repeat: Infinity,
|
|
repeatType: "loop",
|
|
duration: 120,
|
|
ease: "linear",
|
|
},
|
|
}}
|
|
>
|
|
{[...reviews, ...reviews].map((review, index) => (
|
|
<ReviewCard key={`first-${index}-${review.id}`} review={review} />
|
|
))}
|
|
</motion.div>
|
|
</div>
|
|
|
|
<div className="flex overflow-hidden">
|
|
<motion.div
|
|
className="flex items-center gap-0"
|
|
animate={{
|
|
x: [-50 + "%", 0],
|
|
}}
|
|
transition={{
|
|
x: {
|
|
repeat: Infinity,
|
|
repeatType: "loop",
|
|
duration: 120,
|
|
ease: "linear",
|
|
},
|
|
}}
|
|
>
|
|
{[...reviews.slice(25), ...reviews.slice(0, 25), ...reviews.slice(25), ...reviews.slice(0, 25)].map((review, index) => (
|
|
<ReviewCard key={`second-${index}-${review.id}`} review={review} />
|
|
))}
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
} |