fix: testimonial arrow layout/direction logic, language switcher fonts, getCleanImgSrc sanitizer

This commit is contained in:
parsa aghaei 2026-06-29 13:45:40 +03:30
parent 4f5cfc9f1e
commit 056f838172
4 changed files with 36 additions and 29 deletions

View File

@ -14,6 +14,8 @@ import { motion, AnimatePresence } from "framer-motion";
import Image from "next/image";
import { X, ChevronLeft, ChevronRight } from "lucide-react";
import { toPersianDigits, getCleanImgSrc } from "@/app/lib/functions/client-utils";
interface MyProjectsProps {
dict: {
website: {
@ -30,9 +32,6 @@ interface MyProjectsProps {
const MAX_PROJECTS = 6;
const MAX_CATEGORIES = 5;
const toPersianDigits = (str: string | number): string =>
String(str).replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[parseInt(d)]);
export default function Projects({ dict }: MyProjectsProps) {
const [categories, setCategories] = useState<ProjectCategory[]>([]);
const [projects, setProjects] = useState<Project[]>([]);
@ -283,20 +282,15 @@ export default function Projects({ dict }: MyProjectsProps) {
<div className="flex flex-col lg:flex-row h-full max-h-[90vh]">
{/* Left: Image */}
<div className="relative w-full lg:w-1/2 h-64 lg:h-auto min-h-[300px]">
{selectedProject.images.map((image, idx) => {
const imgSrc = image.src.startsWith("http")
? image.src
: `/images/projects/${image.src}`;
return (
<Image
key={idx}
src={imgSrc}
alt={`Image for ${selectedProject.title}`}
fill
className={`object-cover ${idx === 0 ? "opacity-100" : "opacity-0"}`}
/>
);
})}
{selectedProject.images.map((image, idx) => (
<Image
key={idx}
src={getCleanImgSrc(image.src)}
alt={`Image for ${selectedProject.title}`}
fill
className={`object-cover ${idx === 0 ? "opacity-100" : "opacity-0"}`}
/>
))}
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent" />
</div>

View File

@ -30,7 +30,7 @@ export default function SelectLanguage() {
>
<Link
href=""
className="w-[70%] border-b-2 border-[rgba(0,0,0,0)] dark:text-white text-black pb-1 text-center hover:cursor-pointer hover:border-[#FD6F00] dark:hover:text-[#FD6F00]"
className="w-[70%] border-b-2 border-[rgba(0,0,0,0)] dark:text-white text-black pb-1 text-center hover:cursor-pointer hover:border-[#FD6F00] dark:hover:text-[#FD6F00] font-[var(--font-mikhak)]"
onClick={(e) => {
e.preventDefault();
setCookie("selectedLocale", "fa"), router.refresh();
@ -41,7 +41,7 @@ export default function SelectLanguage() {
</Link>
<Link
href=""
className="w-[70%] border-b-2 border-[rgba(0,0,0,0)] dark:text-white text-black pb-1 text-center hover:cursor-pointer hover:border-[#FD6F00] dark:hover:text-[#FD6F00]"
className="w-[70%] border-b-2 border-[rgba(0,0,0,0)] dark:text-white text-black pb-1 text-center hover:cursor-pointer hover:border-[#FD6F00] dark:hover:text-[#FD6F00] font-sans"
onClick={(e) => {
e.preventDefault();
setCookie("selectedLocale", "en"), router.refresh();

View File

@ -6,6 +6,8 @@ import { Quote, ChevronLeft, ChevronRight } from "lucide-react";
import Image from "next/image";
import { getCookie } from "cookies-next";
import { getCleanImgSrc } from "@/app/lib/functions/client-utils";
interface TestimonialData {
name: string;
position: string;
@ -73,7 +75,7 @@ export default function Testimonials({ dict }: TestimonialsProps) {
</motion.p>
</div>
<div className="relative w-full">
<div className="relative w-full px-12">
<div
ref={scrollRef}
className="flex gap-8 overflow-x-auto snap-x snap-mandatory scrollbar-none pb-4"
@ -105,7 +107,7 @@ export default function Testimonials({ dict }: TestimonialsProps) {
<div className="flex items-center gap-4 mt-auto pt-6 border-t border-white/10">
<div className="relative w-12 h-12 overflow-hidden rounded-full border-2 border-primary/50 flex-shrink-0">
<Image
src={testimonial.image}
src={getCleanImgSrc(testimonial.image)}
alt={testimonial.name}
fill
className="object-cover"
@ -131,20 +133,20 @@ export default function Testimonials({ dict }: TestimonialsProps) {
{tData.length > 3 && (
<>
<button
onClick={handlePrev}
disabled={currentIndex === 0}
className="absolute -left-4 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-background border border-border shadow-lg flex items-center justify-center text-foreground hover:text-primary transition-colors disabled:opacity-30 disabled:cursor-not-allowed z-10"
onClick={isRTL ? handleNext : handlePrev}
disabled={isRTL ? currentIndex >= maxIndex : currentIndex === 0}
className="absolute left-2 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-background border border-border shadow-lg flex items-center justify-center text-foreground hover:text-primary transition-colors disabled:opacity-30 disabled:cursor-not-allowed z-20"
aria-label="Previous testimonials"
>
<ChevronLeft className={`w-5 h-5 ${isRTL ? "rotate-180" : ""}`} />
<ChevronLeft className="w-5 h-5" />
</button>
<button
onClick={handleNext}
disabled={currentIndex >= maxIndex}
className="absolute -right-4 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-background border border-border shadow-lg flex items-center justify-center text-foreground hover:text-primary transition-colors disabled:opacity-30 disabled:cursor-not-allowed z-10"
onClick={isRTL ? handlePrev : handleNext}
disabled={isRTL ? currentIndex === 0 : currentIndex >= maxIndex}
className="absolute right-2 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-background border border-border shadow-lg flex items-center justify-center text-foreground hover:text-primary transition-colors disabled:opacity-30 disabled:cursor-not-allowed z-20"
aria-label="Next testimonials"
>
<ChevronRight className={`w-5 h-5 ${isRTL ? "rotate-180" : ""}`} />
<ChevronRight className="w-5 h-5" />
</button>
<div className="flex items-center justify-center gap-2 mt-8">

View File

@ -0,0 +1,11 @@
export const toPersianDigits = (str: string | number): string =>
String(str).replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[parseInt(d)]);
export const getCleanImgSrc = (src: string): string => {
if (!src) return '/images/placeholder.jpg';
if (src.startsWith('http://') || src.startsWith('https://')) {
const urlPath = new URL(src).pathname;
return urlPath.startsWith('/storage') ? urlPath : `/images${urlPath}`;
}
return src;
};