feat: implement locale-aware routing with [locale] dynamic segments
Some checks failed
Build and Deploy / build (push) Has been cancelled
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
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useState, useRef } from "react";
|
||||
import { useTranslations, useLocale } from "next-intl";
|
||||
|
||||
const results = [
|
||||
{
|
||||
@@ -25,6 +26,7 @@ const results = [
|
||||
];
|
||||
|
||||
function BeforeAfterSlider({ result }: { result: typeof results[0] }) {
|
||||
const t = useTranslations("BeforeAfterGallery");
|
||||
const [sliderPosition, setSliderPosition] = useState(50);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -44,34 +46,30 @@ function BeforeAfterSlider({ result }: { result: typeof results[0] }) {
|
||||
|
||||
return (
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Before/After Slider */}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative aspect-[4/3] rounded-2xl overflow-hidden shadow-2xl cursor-ew-resize select-none"
|
||||
onMouseMove={handleMouseMove}
|
||||
onTouchMove={handleTouchMove}
|
||||
>
|
||||
{/* After Image */}
|
||||
<img
|
||||
src={result.afterImg}
|
||||
alt="After - Smooth skin"
|
||||
alt="After"
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
|
||||
{/* Before Image (clipped) */}
|
||||
<div
|
||||
className="absolute inset-0 overflow-hidden"
|
||||
style={{ width: `${sliderPosition}%` }}
|
||||
>
|
||||
<img
|
||||
src={result.beforeImg}
|
||||
alt="Before - Wrinkled skin"
|
||||
alt="Before"
|
||||
className="absolute inset-0 h-full object-cover"
|
||||
style={{ width: `${100 / (sliderPosition / 100)}%`, maxWidth: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Slider Handle */}
|
||||
<div
|
||||
className="absolute top-0 bottom-0 w-1 bg-white shadow-lg cursor-ew-resize"
|
||||
style={{ left: `${sliderPosition}%`, transform: 'translateX(-50%)' }}
|
||||
@@ -83,16 +81,14 @@ function BeforeAfterSlider({ result }: { result: typeof results[0] }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Labels */}
|
||||
<div className="absolute top-3 left-3 bg-black/70 text-white px-3 py-1.5 rounded-full text-xs font-medium backdrop-blur-sm">
|
||||
BEFORE
|
||||
{t("before")}
|
||||
</div>
|
||||
<div className="absolute top-3 right-3 bg-black/70 text-white px-3 py-1.5 rounded-full text-xs font-medium backdrop-blur-sm">
|
||||
AFTER
|
||||
{t("after")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timeline and Rating */}
|
||||
<div className="flex items-center justify-center gap-4 mt-4">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg className="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -113,18 +109,19 @@ function BeforeAfterSlider({ result }: { result: typeof results[0] }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Verified Badge */}
|
||||
<div className="flex items-center justify-center gap-1.5 mt-2">
|
||||
<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>
|
||||
<span className="text-xs text-green-700 font-medium">{t("verified")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BeforeAfterGallery() {
|
||||
const t = useTranslations("BeforeAfterGallery");
|
||||
const locale = useLocale();
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const goToPrev = () => {
|
||||
@@ -146,14 +143,13 @@ export default function BeforeAfterGallery() {
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
|
||||
Real Results
|
||||
{t("realResults")}
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-medium mb-4">
|
||||
See the Transformation
|
||||
{t("seeTransformation")}
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
{/* Desktop: Two transformations side by side */}
|
||||
<div className="hidden md:flex gap-6 max-w-6xl mx-auto">
|
||||
{results.map((result, index) => (
|
||||
<motion.div
|
||||
@@ -169,7 +165,6 @@ export default function BeforeAfterGallery() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Mobile: Carousel with one transformation at a time */}
|
||||
<div className="md:hidden relative max-w-md mx-auto">
|
||||
<div className="overflow-hidden">
|
||||
<motion.div
|
||||
@@ -182,28 +177,26 @@ export default function BeforeAfterGallery() {
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Carousel Navigation */}
|
||||
<button
|
||||
onClick={goToPrev}
|
||||
className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-2 w-10 h-10 bg-white rounded-full shadow-lg flex items-center justify-center"
|
||||
aria-label="Previous transformation"
|
||||
aria-label="Previous"
|
||||
>
|
||||
<svg className="w-5 h-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
onClick={goToNext}
|
||||
className="absolute right-0 top-1/2 -translate-y-1/2 translate-x-2 w-10 h-10 bg-white rounded-full shadow-lg flex items-center justify-center"
|
||||
aria-label="Next transformation"
|
||||
aria-label="Next"
|
||||
>
|
||||
<svg className="w-5 h-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Dot Indicators */}
|
||||
<div className="flex justify-center gap-2 mt-6">
|
||||
{results.map((_, index) => (
|
||||
<button
|
||||
@@ -212,13 +205,12 @@ export default function BeforeAfterGallery() {
|
||||
className={`w-2 h-2 rounded-full transition-all ${
|
||||
selectedIndex === index ? "bg-black w-4" : "bg-gray-300"
|
||||
}`}
|
||||
aria-label={`Go to transformation ${index + 1}`}
|
||||
aria-label={`Go to ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<motion.div
|
||||
className="text-center mt-12"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
@@ -227,13 +219,13 @@ export default function BeforeAfterGallery() {
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
>
|
||||
<a
|
||||
href="/products"
|
||||
href={`/${locale}/products`}
|
||||
className="inline-block px-10 py-4 bg-black text-white text-[13px] uppercase tracking-[0.15em] font-semibold hover:bg-[#333] transition-colors"
|
||||
>
|
||||
Start Your Transformation
|
||||
{t("startTransformation")}
|
||||
</a>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user