fix: hero button localization/hover, lang switcher dark mode, services grid, testimonials carousel, project modal, footer gap, back-to-top
This commit is contained in:
parent
9797b71f55
commit
470036d6b9
@ -128,9 +128,9 @@ export default function Hero({ dict }: HeroProps) {
|
||||
>
|
||||
<a
|
||||
href="#contact"
|
||||
className="group relative inline-flex items-center justify-center px-8 py-3 font-bold text-white transition-all duration-200 bg-primary rounded-full hover:bg-primary-foreground hover:text-primary active:scale-95"
|
||||
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"
|
||||
>
|
||||
<span className="relative z-10">Let's Work Together</span>
|
||||
<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>
|
||||
</a>
|
||||
</motion.div>
|
||||
|
||||
@ -7,12 +7,12 @@ import {
|
||||
ProjectResponse,
|
||||
} from "@/app/types/types";
|
||||
import { getCookie } from "cookies-next";
|
||||
import Link from "next/link";
|
||||
import React, { useEffect, useState, useMemo } from "react";
|
||||
import React, { useEffect, useState, useMemo, useCallback } from "react";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import Image from "next/image";
|
||||
import { X, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
interface MyProjectsProps {
|
||||
dict: {
|
||||
@ -35,6 +35,7 @@ export default function Projects({ dict }: MyProjectsProps) {
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
||||
const locale = getCookie("selectedLocale") || getCookie("defaultLocale");
|
||||
|
||||
useEffect(() => {
|
||||
@ -97,6 +98,29 @@ export default function Projects({ dict }: MyProjectsProps) {
|
||||
return groupedProjects[selectedCategory]?.slice(0, MAX_PROJECTS) || [];
|
||||
}, [selectedCategory, projects, groupedProjects, locale]);
|
||||
|
||||
const currentModalIndex = useMemo(
|
||||
() => filteredProjects.findIndex((p) => p.id === selectedProject?.id),
|
||||
[filteredProjects, selectedProject],
|
||||
);
|
||||
|
||||
const handlePrev = useCallback(() => {
|
||||
if (currentModalIndex > 0) {
|
||||
setSelectedProject(filteredProjects[currentModalIndex - 1]);
|
||||
}
|
||||
}, [currentModalIndex, filteredProjects]);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
if (currentModalIndex < filteredProjects.length - 1) {
|
||||
setSelectedProject(filteredProjects[currentModalIndex + 1]);
|
||||
}
|
||||
}, [currentModalIndex, filteredProjects]);
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === "Escape") setSelectedProject(null);
|
||||
if (e.key === "ArrowLeft") handlePrev();
|
||||
if (e.key === "ArrowRight") handleNext();
|
||||
}, [handlePrev, handleNext]);
|
||||
|
||||
const renderSkeletons = (count: number) => (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 w-full">
|
||||
{Array.from({ length: count }, (_, index) => (
|
||||
@ -161,7 +185,7 @@ export default function Projects({ dict }: MyProjectsProps) {
|
||||
>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{filteredProjects.length > 0
|
||||
? filteredProjects.map((project, index) => (
|
||||
? filteredProjects.map((project) => (
|
||||
<motion.div
|
||||
key={project.id}
|
||||
layout
|
||||
@ -169,9 +193,9 @@ export default function Projects({ dict }: MyProjectsProps) {
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="group relative rounded-3xl overflow-hidden glass-panel"
|
||||
onClick={() => setSelectedProject(project)}
|
||||
className="group relative rounded-3xl overflow-hidden glass-panel cursor-pointer"
|
||||
>
|
||||
{/* Project Image Gallery */}
|
||||
<div className="relative h-64 w-full overflow-hidden">
|
||||
{project.images.map((image, idx) => (
|
||||
<Image
|
||||
@ -182,26 +206,19 @@ export default function Projects({ dict }: MyProjectsProps) {
|
||||
className={`object-cover transition-transform duration-500 group-hover:scale-110 ${
|
||||
idx === 0 ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
// In a real app, we would implement a slider here
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Hover Overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-300 flex flex-col justify-end p-6 text-white">
|
||||
<span className="text-xs font-mono text-primary mb-2 uppercase tracking-widest">
|
||||
{project.category}
|
||||
</span>
|
||||
<Link
|
||||
href={process.env.NEXT_PUBLIC_BACK_URL + "/projects/" + project.id}
|
||||
target="_blank"
|
||||
className="text-lg font-bold underline underline-offset-4 hover:text-primary transition-colors"
|
||||
>
|
||||
View Project →
|
||||
</Link>
|
||||
<span className="text-lg font-bold">
|
||||
{locale === "fa" ? " مشاهده جزییات" : "View Details"} →
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Project Details */}
|
||||
<div className="p-6">
|
||||
<h5 className="text-sm font-mono text-primary mb-1">
|
||||
{project.category}
|
||||
@ -231,6 +248,89 @@ export default function Projects({ dict }: MyProjectsProps) {
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
|
||||
<AnimatePresence>
|
||||
{selectedProject && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
||||
onClick={() => setSelectedProject(null)}
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={0}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
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"
|
||||
>
|
||||
<button
|
||||
onClick={() => setSelectedProject(null)}
|
||||
className="absolute top-4 end-4 z-10 w-10 h-10 rounded-full bg-background/80 backdrop-blur-sm border border-border flex items-center justify-center text-foreground hover:text-primary transition-colors"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<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="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>
|
||||
|
||||
<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>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@ -37,11 +37,11 @@ export default function SelectLanguage({
|
||||
)}
|
||||
|
||||
<ul
|
||||
className={`absolute start-0 z-20 flex w-full flex-col items-center gap-2 rounded-lg border bg-white py-3 transition-all ${isOpen ? "top-[50px] scale-100" : "top-0 scale-0"}`}
|
||||
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"}`}
|
||||
>
|
||||
<Link
|
||||
href=""
|
||||
className="w-[70%] border-b-2 border-[rgba(0,0,0,0)] pb-1 text-center hover:cursor-pointer hover:border-[#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]"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setCookie("selectedLocale", "fa"), router.refresh();
|
||||
@ -52,7 +52,7 @@ export default function SelectLanguage({
|
||||
</Link>
|
||||
<Link
|
||||
href=""
|
||||
className="w-[70%] border-b-2 border-[rgba(0,0,0,0)] pb-1 text-center hover:cursor-pointer hover:border-[#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]"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setCookie("selectedLocale", "en"), router.refresh();
|
||||
|
||||
@ -103,9 +103,9 @@ export default function Services({ dict }: ServicesProps) {
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 w-full">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-5xl mx-auto w-full">
|
||||
{loading
|
||||
? Array.from({ length: 4 }).map((_, index) =>
|
||||
? Array.from({ length: 3 }).map((_, index) =>
|
||||
renderServiceCard(undefined, index),
|
||||
)
|
||||
: services.map((service, index) => renderServiceCard(service, index))}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { Testimonial } from "@/app/types/types";
|
||||
import React from "react";
|
||||
import React, { useState, useRef } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Quote } from "lucide-react";
|
||||
import { Quote, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
interface TestimonialsProps {
|
||||
@ -58,6 +58,19 @@ const testimonials: Testimonial[] = [
|
||||
];
|
||||
|
||||
export default function Testimonials({ dict }: TestimonialsProps) {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const maxIndex = Math.max(0, testimonials.length - 3);
|
||||
|
||||
const scrollTo = (index: number) => {
|
||||
const idx = Math.max(0, Math.min(index, maxIndex));
|
||||
setCurrentIndex(idx);
|
||||
scrollRef.current?.children[idx]?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "start" });
|
||||
};
|
||||
|
||||
const handlePrev = () => scrollTo(currentIndex - 1);
|
||||
const handleNext = () => scrollTo(currentIndex + 1);
|
||||
|
||||
return (
|
||||
<section
|
||||
className="relative flex w-full max-w-[1200px] flex-col items-center py-20"
|
||||
@ -84,52 +97,97 @@ export default function Testimonials({ dict }: TestimonialsProps) {
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">
|
||||
{testimonials.map((testimonial, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
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"
|
||||
>
|
||||
<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">
|
||||
"{testimonial.quote}"
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative w-full">
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex gap-8 overflow-x-auto snap-x snap-mandatory scrollbar-none pb-4"
|
||||
style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
|
||||
>
|
||||
{testimonials.map((testimonial, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="min-w-[100%] md:min-w-[calc(50%-1rem)] lg:min-w-[calc(33.333%-1.1rem)] snap-start flex-shrink-0"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
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"
|
||||
>
|
||||
<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">
|
||||
"{testimonial.quote}"
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<Image
|
||||
src={testimonial.image}
|
||||
alt={testimonial.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<h5 className="font-bold text-foreground leading-none mb-1">
|
||||
{testimonial.name}
|
||||
</h5>
|
||||
<p className="text-xs text-muted-foreground leading-tight">
|
||||
{testimonial.position}
|
||||
</p>
|
||||
</div>
|
||||
<div className="ms-auto bg-primary/10 text-primary text-[10px] font-bold px-2 py-1 rounded-md border border-primary/20">
|
||||
Verified
|
||||
</div>
|
||||
<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}
|
||||
alt={testimonial.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<h5 className="font-bold text-foreground leading-none mb-1 truncate">
|
||||
{testimonial.name}
|
||||
</h5>
|
||||
<p className="text-xs text-muted-foreground leading-tight truncate">
|
||||
{testimonial.position}
|
||||
</p>
|
||||
</div>
|
||||
<div className="ms-auto bg-primary/10 text-primary text-[10px] font-bold px-2 py-1 rounded-md border border-primary/20 flex-shrink-0">
|
||||
Verified
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
|
||||
{testimonials.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"
|
||||
aria-label="Previous testimonials"
|
||||
>
|
||||
<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"
|
||||
aria-label="Next testimonials"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<div className="flex items-center justify-center gap-2 mt-8">
|
||||
{Array.from({ length: maxIndex + 1 }).map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => scrollTo(i)}
|
||||
className={`w-2.5 h-2.5 rounded-full transition-all duration-300 ${
|
||||
i === currentIndex
|
||||
? "bg-primary w-6"
|
||||
: "bg-muted-foreground/30 hover:bg-muted-foreground/50"
|
||||
}`}
|
||||
aria-label={`Go to testimonial group ${i + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -36,19 +36,20 @@ export default function RootLayout({
|
||||
/>
|
||||
<div className="scroll-watcher" id="top"></div>
|
||||
<div className="absolute start-0 top-0 opacity-0" id="here-is-top"></div>
|
||||
<a
|
||||
href="/#here-is-top"
|
||||
className="scroll-to-top fixed end-5 flex h-[50px] w-[50px] items-center justify-center rounded-full border-2 bg-[rgba(0,0,0,0.05)] p-5 hover:border-[#f3a66b] sm:bottom-[100px] sm:end-10"
|
||||
id="back-to-top"
|
||||
>
|
||||
<p className="-mb-[35px] -ms-[12px] w-full text-[50px] font-bold text-[rgb(128,128,128)]">
|
||||
^
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<Header />
|
||||
<NextTopLoader color="rgb(253 111 0)" showAtBottom={false} />
|
||||
<div className="main my-10 flex w-full justify-center">{children}</div>
|
||||
<div className="main flex w-full justify-center">{children}</div>
|
||||
|
||||
<a
|
||||
href="/#here-is-top"
|
||||
className="fixed bottom-8 end-8 z-40 flex h-12 w-12 items-center justify-center rounded-full bg-primary text-white shadow-lg shadow-primary/30 transition-all duration-300 hover:scale-110 hover:shadow-xl hover:shadow-primary/40 active:scale-95"
|
||||
id="back-to-top"
|
||||
aria-label="Back to top"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"name": "Parsa Aghayi",
|
||||
"profession": "Senior Frontend Engineer & Web Architect",
|
||||
"roll": "Turning complex engineering challenges into clean, maintainable code.",
|
||||
"descriptionAboutMe": "Crafting high-performance web applications, scalable React/Next.js architectures, and seamless backend integrations. Turning complex engineering challenges into clean, maintainable code."
|
||||
"descriptionAboutMe": "Crafting high-performance web applications, scalable React/Next.js architectures, and seamless backend integrations. Turning complex engineering challenges into clean, maintainable code.",
|
||||
"cta": "Let's Work Together"
|
||||
},
|
||||
"services": {
|
||||
"title": "services",
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"name": "پارس آقایی",
|
||||
"profession": "ارشد توسعه فرانتاند و معمار راهکارهای وب",
|
||||
"roll": "تمرکز من بر خلق رابطهای کاربری بسیار بهینه و توسعه کامپوننتهای مقیاسپذیر است.",
|
||||
"descriptionAboutMe": "با بیش از ۱۰ سال تجربه در دنیای توسعه وب، مسیر کاری من ترکیبی از مهندسی دقیق فرانتاند و درک عمیق از معماری بکاند است. تخصص اصلی من در بازطراحی و Refactor کدهای پیچیده، بهینهسازی Performance پروژههای بزرگ Next.js و پیادهسازی زیرساختهای پایدار وب است."
|
||||
"descriptionAboutMe": "با بیش از ۱۰ سال تجربه در دنیای توسعه وب، مسیر کاری من ترکیبی از مهندسی دقیق فرانتاند و درک عمیق از معماری بکاند است. تخصص اصلی من در بازطراحی و Refactor کدهای پیچیده، بهینهسازی Performance پروژههای بزرگ Next.js و پیادهسازی زیرساختهای پایدار وب است.",
|
||||
"cta": "بیایید با هم کار کنیم"
|
||||
},
|
||||
"services": {
|
||||
"title": "خدمات",
|
||||
|
||||
@ -127,6 +127,7 @@ export type Dictionary = {
|
||||
profession: string;
|
||||
roll: string;
|
||||
descriptionAboutMe: string;
|
||||
cta: string;
|
||||
};
|
||||
services: {
|
||||
title: string;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user