fix: sticky header blur on scroll, testimonials carousel 6 items + RTL arrows, project modal dual-column, smooth scroll, hero btn no-wrap

This commit is contained in:
parsa aghaei 2026-06-29 09:32:40 +03:30
parent dfedd18f66
commit 7dc79b3151
7 changed files with 179 additions and 115 deletions

View File

@ -1,29 +1,51 @@
import React from "react";
"use client";
import React, { useEffect, useState } from "react";
import BurgerButton from "../burger-button/burgerButton";
import Link from "next/link";
import SelectLanguage from "../selectLanguage/selectLanguage";
import { ThemeToggle } from "../theme-toggle/theme-toggle";
import { cookies } from "next/headers";
import { getCookie } from "cookies-next";
import { getDictionary } from "@/app/lib/functions/dictionaries";
import { getDefaultLocale } from "@/app/lib/functions/utils";
import { motion } from "framer-motion";
export default async function Header() {
const dict = await getDictionary(await getDefaultLocale());
export default function Header() {
const [dict, setDict] = useState<any>(null);
const [scrolled, setScrolled] = useState(false);
const navLinks = [
{ href: "/#home", label: dict.website.header.Home },
{ href: "/#about-me", label: dict.website.header.AboutMe },
{ href: "/#services", label: dict.website.header.Services },
{ href: "/#projects", label: dict.website.header.Projects },
{ href: "/#testimonials", label: dict.website.header.Testimonials },
{ href: "/#contact", label: dict.website.header.Contact },
{ href: "/blog", label: dict.website.header.Blog },
];
useEffect(() => {
const loadDict = async () => {
const selectedLocale = getCookie("selectedLocale");
const defaultLocale = getCookie("defaultLocale");
const d = await getDictionary((selectedLocale || defaultLocale || "en") as any);
setDict(d);
};
loadDict();
const handleScroll = () => setScrolled(window.scrollY > 50);
window.addEventListener("scroll", handleScroll, { passive: true });
handleScroll();
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const navLinks = dict
? [
{ href: "/#home", label: dict.website.header.Home },
{ href: "/#about-me", label: dict.website.header.AboutMe },
{ href: "/#services", label: dict.website.header.Services },
{ href: "/#projects", label: dict.website.header.Projects },
{ href: "/#testimonials", label: dict.website.header.Testimonials },
{ href: "/#contact", label: dict.website.header.Contact },
{ href: "/blog", label: dict.website.header.Blog },
]
: [];
return (
<header
className="fixed top-0 left-0 z-50 flex h-20 w-full items-center justify-between px-6 py-4 transition-all duration-300 xl:px-12"
className={`fixed top-0 left-0 z-50 flex h-20 w-full items-center justify-between px-6 py-4 transition-all duration-300 xl:px-12 ${
scrolled
? "backdrop-blur-md bg-white/70 dark:bg-zinc-950/70 border-b border-zinc-200/50 dark:border-zinc-800/50 shadow-sm"
: "bg-transparent"
}`}
id="header"
>
<div className="flex items-center gap-4">
@ -32,7 +54,7 @@ export default async function Header() {
<img src="/images/logo.svg" className="w-full h-full object-cover" alt="logo" />
</div>
<span className="font-mono font-bold text-xl md:text-2xl text-primary tracking-tighter uppercase italic">
{dict.website.header.Name}
{dict?.website.header.Name}
</span>
</Link>
</div>
@ -51,10 +73,7 @@ export default async function Header() {
))}
</ul>
<div className="flex items-center gap-2 border-s border-border ps-4">
<SelectLanguage
selectedLocale={(await cookies()).get("selectedLocale")}
defaultLocale={(await cookies()).get("defaultLocale")}
/>
<SelectLanguage />
<ThemeToggle />
</div>
</nav>
@ -66,9 +85,9 @@ export default async function Header() {
target="_blank"
className="hidden sm:flex items-center justify-center whitespace-nowrap rounded-full bg-primary px-5 py-2 text-sm font-semibold text-primary-foreground shadow-[0_0_20px_rgba(253,111,0,0.3)] transition-all duration-300 hover:shadow-[0_0_30px_rgba(253,111,0,0.5)] hover:scale-105 active:scale-95"
>
{dict.website.header.Button}
{dict?.website.header.Button}
</a>
<BurgerButton dict={dict} />
{dict && <BurgerButton dict={dict} />}
</div>
</header>
);

View File

@ -128,7 +128,7 @@ export default function Hero({ dict }: HeroProps) {
>
<a
href="#contact"
className="group relative inline-flex items-center justify-center whitespace-nowrap px-8 py-3 font-bold text-white transition-all duration-200 bg-primary rounded-full hover:brightness-110 hover:scale-105 active:scale-95"
className="group relative inline-flex items-center justify-center whitespace-nowrap px-4 sm:px-8 py-3 font-bold text-white transition-all duration-200 bg-primary rounded-full hover:brightness-110 hover:scale-105 active:scale-95"
>
<span className="relative z-10">{dict.website.homePage.top.cta}</span>
<div className="absolute inset-0 rounded-full bg-primary blur-lg opacity-50 group-hover:opacity-100 transition-opacity"></div>

View File

@ -36,7 +36,8 @@ export default function Projects({ dict }: MyProjectsProps) {
const [selectedCategory, setSelectedCategory] = useState<string>("");
const [loading, setLoading] = useState(true);
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
const locale = getCookie("selectedLocale") || getCookie("defaultLocale");
const locale = (getCookie("selectedLocale") || getCookie("defaultLocale") || "en") as string;
const isRTL = locale === "fa";
useEffect(() => {
const initialCategory = locale === "fa" ? "جدیدترین‌ها" : "Newest";
@ -214,7 +215,7 @@ export default function Projects({ dict }: MyProjectsProps) {
{project.category}
</span>
<span className="text-lg font-bold">
{locale === "fa" ? " مشاهده جزییات" : "View Details"}
{isRTL ? "مشاهده جزییات" : "View Details"}
</span>
</div>
</div>
@ -239,10 +240,10 @@ export default function Projects({ dict }: MyProjectsProps) {
</svg>
</div>
<h3 className="text-xl font-bold text-foreground mb-2">
{locale === "fa" ? "پروژه‌ای یافت نشد" : "No projects found"}
{isRTL ? "پروژه‌ای یافت نشد" : "No projects found"}
</h3>
<p className="text-muted-foreground max-w-md">
{locale === "fa" ? "در حال حاضر پروژه‌ای در این دسته‌بندی وجود ندارد. لطفاً دسته‌بندی دیگری را انتخاب کنید." : "No projects in this category yet. Please try another category."}
{isRTL ? "در حال حاضر پروژه‌ای در این دسته‌بندی وجود ندارد. لطفاً دسته‌بندی دیگری را انتخاب کنید." : "No projects in this category yet. Please try another category."}
</p>
</div>
)}
@ -266,7 +267,7 @@ export default function Projects({ dict }: MyProjectsProps) {
exit={{ opacity: 0, scale: 0.9, y: 20 }}
transition={{ duration: 0.3 }}
onClick={(e) => e.stopPropagation()}
className="relative w-full max-w-3xl max-h-[90vh] overflow-y-auto rounded-3xl bg-background border border-border shadow-2xl"
className="relative w-full max-w-5xl max-h-[90vh] rounded-3xl bg-background border border-border shadow-2xl overflow-hidden"
>
<button
onClick={() => setSelectedProject(null)}
@ -276,56 +277,77 @@ export default function Projects({ dict }: MyProjectsProps) {
<X className="w-5 h-5" />
</button>
<div className="relative h-72 md:h-96 w-full">
{selectedProject.images.map((image, idx) => (
<Image
key={idx}
src={process.env.NEXT_PUBLIC_BACK_URL + "/images/projects/" + image.src}
alt={`Image for ${selectedProject.title}`}
fill
className={`object-cover ${idx === 0 ? "opacity-100" : "opacity-0"}`}
/>
))}
</div>
<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) => (
<Image
key={idx}
src={process.env.NEXT_PUBLIC_BACK_URL + "/images/projects/" + 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>
<div className="p-8">
<span className="text-xs font-mono text-primary uppercase tracking-widest">
{selectedProject.category}
</span>
<h3 className="text-3xl font-bold text-foreground mt-2 mb-4">
{selectedProject.title}
</h3>
<p className="text-muted-foreground leading-relaxed">
{locale === "fa"
? "توضیحات کامل پروژه در این بخش نمایش داده می‌شود."
: "Full project description will be displayed here."}
</p>
</div>
{/* Right: Details */}
<div className="flex flex-col w-full lg:w-1/2 p-8 lg:p-10 overflow-hidden">
<div className="flex-grow overflow-y-auto custom-scrollbar pr-2">
<span className="text-xs font-mono text-primary uppercase tracking-widest">
{selectedProject.category}
</span>
<h3 className="text-2xl lg:text-3xl font-bold text-foreground mt-2 mb-6">
{selectedProject.title}
</h3>
<div className="flex items-center justify-between px-8 pb-8">
<button
onClick={handlePrev}
disabled={currentModalIndex <= 0}
className="flex items-center gap-2 px-4 py-2 rounded-full bg-muted text-foreground hover:text-primary transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
>
<ChevronLeft className="w-5 h-5" />
<span className="text-sm font-medium">
{locale === "fa" ? "قبلی" : "Previous"}
</span>
</button>
<span className="text-sm text-muted-foreground">
{currentModalIndex + 1} / {filteredProjects.length}
</span>
<button
onClick={handleNext}
disabled={currentModalIndex >= filteredProjects.length - 1}
className="flex items-center gap-2 px-4 py-2 rounded-full bg-muted text-foreground hover:text-primary transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
>
<span className="text-sm font-medium">
{locale === "fa" ? "بعدی" : "Next"}
</span>
<ChevronRight className="w-5 h-5" />
</button>
<div className="flex flex-wrap gap-2 mb-6">
<span className="px-3 py-1 rounded-full text-xs font-mono border border-primary/30 bg-primary/10 text-primary">
{selectedProject.category}
</span>
<span className="px-3 py-1 rounded-full text-xs font-mono border border-muted-foreground/30 bg-muted text-muted-foreground">
{isRTL ? "طراحی وب" : "Web Design"}
</span>
<span className="px-3 py-1 rounded-full text-xs font-mono border border-muted-foreground/30 bg-muted text-muted-foreground">
{isRTL ? "توسعه" : "Development"}
</span>
</div>
<p className="text-muted-foreground leading-relaxed text-sm md:text-base">
{isRTL
? "این پروژه با استفاده از آخرین تکنولوژی‌های وب توسعه یافته است. تیم ما با دقت و توجه به جزئیات، راهکاری scalable و maintainable ارائه داده است که نیازهای کسب و کار را به بهترین شکل برآورده می‌کند. از طراحی رابط کاربری گرفته تا بهینه‌سازی performance، تمامی جنبه‌های این پروژه با بالاترین استانداردها پیاده‌سازی شده است."
: "This project was built using the latest web technologies. Our team delivered a scalable and maintainable solution with meticulous attention to detail, meeting business requirements effectively. From UI/UX design to performance optimization, every aspect was implemented to the highest standards."}
</p>
</div>
{/* Navigation */}
<div className="flex items-center justify-between pt-6 mt-6 border-t border-border flex-shrink-0">
<button
onClick={isRTL ? handleNext : handlePrev}
disabled={isRTL ? currentModalIndex >= filteredProjects.length - 1 : currentModalIndex <= 0}
className="flex items-center gap-2 px-4 py-2 rounded-full bg-muted text-foreground hover:text-primary transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
>
<ChevronLeft className={`w-5 h-5 ${isRTL ? "rotate-180" : ""}`} />
<span className="text-sm font-medium">
{isRTL ? "بعدی" : "Previous"}
</span>
</button>
<span className="text-sm text-muted-foreground">
{currentModalIndex + 1} / {filteredProjects.length}
</span>
<button
onClick={isRTL ? handlePrev : handleNext}
disabled={isRTL ? currentModalIndex <= 0 : currentModalIndex >= filteredProjects.length - 1}
className="flex items-center gap-2 px-4 py-2 rounded-full bg-muted text-foreground hover:text-primary transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
>
<span className="text-sm font-medium">
{isRTL ? "قبلی" : "Next"}
</span>
<ChevronRight className={`w-5 h-5 ${isRTL ? "rotate-180" : ""}`} />
</button>
</div>
</div>
</div>
</motion.div>
</motion.div>

View File

@ -1,40 +1,29 @@
"use client";
import { setCookie } from "cookies-next";
import { LocaleCookieType } from "@/app/types/types";
import { getCookie, setCookie } from "cookies-next";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useState, useEffect } from "react";
import Link from "next/link";
interface SelectLanguageProps {
selectedLocale: LocaleCookieType | undefined;
defaultLocale: LocaleCookieType | undefined;
}
export default function SelectLanguage({
selectedLocale,
defaultLocale,
}: SelectLanguageProps) {
export default function SelectLanguage() {
const router = useRouter();
const [isOpen, setisOpen] = useState(false);
const [currentLocale, setCurrentLocale] = useState("");
useEffect(() => {
const sl = getCookie("selectedLocale");
const dl = getCookie("defaultLocale");
setCurrentLocale(sl || dl || "en");
}, []);
return (
<div className="relative flex min-w-[100px] justify-center">
{selectedLocale ? (
<p
onClick={() => setisOpen(!isOpen)}
className="mx-2 mb-[5px] cursor-pointer select-none whitespace-nowrap border-b-2 border-[#FD6F00] px-2 py-[10px] pb-[5px] text-[15px] font-[600] text-[#FD6F00] hover:text-[#FD6F00] xl:text-[20px]"
>
{selectedLocale?.value}
</p>
) : (
<p
onClick={() => setisOpen(!isOpen)}
className="mx-2 mb-[5px] cursor-pointer select-none whitespace-nowrap px-2 py-[10px] pb-[5px] text-[15px] font-[600] hover:text-[#FD6F00] xl:text-[20px]"
>
{defaultLocale?.value}
</p>
)}
<p
onClick={() => setisOpen(!isOpen)}
className="mx-2 mb-[5px] cursor-pointer select-none whitespace-nowrap border-b-2 border-[#FD6F00] px-2 py-[10px] pb-[5px] text-[15px] font-[600] text-[#FD6F00] hover:text-[#FD6F00] xl:text-[20px]"
>
{currentLocale}
</p>
<ul
className={`absolute start-0 z-20 flex w-full flex-col items-center gap-2 rounded-lg border bg-white dark:bg-zinc-900 dark:text-white text-black py-3 transition-all ${isOpen ? "top-[50px] scale-100" : "top-0 scale-0"}`}

View File

@ -1,10 +1,11 @@
"use client";
import { Testimonial } from "@/app/types/types";
import React, { useState, useRef } from "react";
import React, { useState, useRef, useEffect } from "react";
import { motion } from "framer-motion";
import { Quote, ChevronLeft, ChevronRight } from "lucide-react";
import Image from "next/image";
import { getCookie } from "cookies-next";
interface TestimonialsProps {
dict: {
@ -55,11 +56,27 @@ const testimonials: Testimonial[] = [
quote:
"It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable. The generated Lorem Ipsum is therefore always free from repetition, injected humour, or non-characteristic words etc.",
},
{
name: "Alex Chen",
position: "CTO at CloudBase Technologies",
image: "/images/user-1.svg",
quote:
"Working with this developer was an absolute pleasure. The attention to detail, code quality, and timely delivery exceeded our expectations. I highly recommend their services for any complex web development project.",
},
];
export default function Testimonials({ dict }: TestimonialsProps) {
const [currentIndex, setCurrentIndex] = useState(0);
const scrollRef = useRef<HTMLDivElement>(null);
const [locale, setLocale] = useState("en");
useEffect(() => {
const sl = getCookie("selectedLocale");
const dl = getCookie("defaultLocale");
setLocale(sl || dl || "en");
}, []);
const isRTL = locale === "fa";
const maxIndex = Math.max(0, testimonials.length - 3);
const scrollTo = (index: number) => {
@ -114,14 +131,14 @@ export default function Testimonials({ dict }: TestimonialsProps) {
transition={{ duration: 0.5, delay: index * 0.1 }}
viewport={{ once: true }}
whileHover={{ y: -10 }}
className="relative p-8 rounded-3xl glass-panel flex flex-col h-full"
className="relative p-6 md:p-8 rounded-3xl glass-panel flex flex-col h-full"
>
<div className="absolute -top-6 -start-6 w-12 h-12 bg-primary rounded-full flex items-center justify-center text-white shadow-lg">
<Quote className="w-6 h-6" />
</div>
<div className="flex-grow">
<p className="text-muted-foreground leading-relaxed mb-8 italic">
<div className="flex-grow pt-2">
<p className="text-muted-foreground leading-relaxed mb-8 italic text-sm md:text-base">
"{testimonial.quote}"
</p>
</div>
@ -155,20 +172,20 @@ export default function Testimonials({ dict }: TestimonialsProps) {
{testimonials.length > 3 && (
<>
<button
onClick={handlePrev}
disabled={currentIndex === 0}
onClick={isRTL ? handleNext : handlePrev}
disabled={isRTL ? currentIndex >= maxIndex : 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"
aria-label="Previous testimonials"
>
<ChevronLeft className="w-5 h-5" />
<ChevronLeft className={`w-5 h-5 ${isRTL ? "rotate-180" : ""}`} />
</button>
<button
onClick={handleNext}
disabled={currentIndex >= maxIndex}
onClick={isRTL ? handlePrev : handleNext}
disabled={isRTL ? currentIndex === 0 : 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"
aria-label="Next testimonials"
>
<ChevronRight className="w-5 h-5" />
<ChevronRight className={`w-5 h-5 ${isRTL ? "rotate-180" : ""}`} />
</button>
<div className="flex items-center justify-center gap-2 mt-8">

View File

@ -2,6 +2,10 @@
@tailwind components;
@tailwind utilities;
html {
scroll-behavior: smooth;
}
@layer base {
:root {
--background: 240 10% 98%;
@ -82,6 +86,20 @@
scrollbar-width: none;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: hsl(var(--muted-foreground) / 0.3);
border-radius: 999px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: hsl(var(--muted-foreground) / 0.5);
}
.mesh-gradient {
background-image:
radial-gradient(at 0% 0%, hsla(30, 80%, 50%, 0.15) 0px, transparent 50%),

View File

@ -1,5 +1,4 @@
import { Dictionary } from "@/app/types/types";
import "server-only";
const dictionaries: { [key: string]: () => Promise<Dictionary> } = {
en: () => import("../dictionaries/en.json").then((module) => module.default),