feat: implement locale-aware routing with [locale] dynamic segments
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:
Unchained
2026-03-23 20:59:33 +02:00
parent 5bd1a0f167
commit 92b6c830e1
47 changed files with 2175 additions and 2881 deletions

View File

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