fix: testimonial arrow layout/direction logic, language switcher fonts, getCleanImgSrc sanitizer
This commit is contained in:
parent
4f5cfc9f1e
commit
056f838172
@ -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>
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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">
|
||||
|
||||
11
src/app/lib/functions/client-utils.ts
Normal file
11
src/app/lib/functions/client-utils.ts
Normal 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;
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user