style: implement dark theme, glassmorphism, and framer-motion architecture; chore: update seeders with professional copywriting for EN and FA languages
This commit is contained in:
parent
c145467405
commit
83b5a2f22a
68
package-lock.json
generated
68
package-lock.json
generated
@ -9,7 +9,10 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookies-next": "^4.3.0",
|
"cookies-next": "^4.3.0",
|
||||||
|
"framer-motion": "^12.42.0",
|
||||||
|
"lucide-react": "^1.21.0",
|
||||||
"next": "16.2.9",
|
"next": "16.2.9",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"nextjs-toploader": "^3.7.15",
|
"nextjs-toploader": "^3.7.15",
|
||||||
"react": "19.2.7",
|
"react": "19.2.7",
|
||||||
"react-dom": "19.2.7",
|
"react-dom": "19.2.7",
|
||||||
@ -37,6 +40,10 @@
|
|||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"vitest": "^4.1.9",
|
"vitest": "^4.1.9",
|
||||||
"webpack-bundle-analyzer": "^4.10.2"
|
"webpack-bundle-analyzer": "^4.10.2"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@next/swc-linux-x64-gnu": "16.2.9",
|
||||||
|
"@next/swc-linux-x64-musl": "16.2.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@adobe/css-tools": {
|
"node_modules/@adobe/css-tools": {
|
||||||
@ -5260,6 +5267,33 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/framer-motion": {
|
||||||
|
"version": "12.42.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.0.tgz",
|
||||||
|
"integrity": "sha512-wp7EJnfWaaEScVygKv3e20udoRz+LbtxScsuTkakAxfXmt+ReC6WyPW2nINRAGvd+hG9odwcjBLyOTPjH5pBRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-dom": "^12.42.0",
|
||||||
|
"motion-utils": "^12.39.0",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fs-extra": {
|
"node_modules/fs-extra": {
|
||||||
"version": "9.1.0",
|
"version": "9.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||||
@ -7345,6 +7379,15 @@
|
|||||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "1.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.21.0.tgz",
|
||||||
|
"integrity": "sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lz-string": {
|
"node_modules/lz-string": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||||
@ -7500,6 +7543,21 @@
|
|||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/motion-dom": {
|
||||||
|
"version": "12.42.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.0.tgz",
|
||||||
|
"integrity": "sha512-M63h4n8R+quJdNhBwuLlgxM+OLYa9+I/T2pzDRboB9fLXRdbou+Gw7Zury+SkpaCyACP1JHSjHgZ1EgTkBr30w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-utils": "^12.39.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/motion-utils": {
|
||||||
|
"version": "12.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz",
|
||||||
|
"integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/mrmime": {
|
"node_modules/mrmime": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
|
||||||
@ -7606,6 +7664,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-themes": {
|
||||||
|
"version": "0.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
|
||||||
|
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/next/node_modules/@img/sharp-darwin-arm64": {
|
"node_modules/next/node_modules/@img/sharp-darwin-arm64": {
|
||||||
"version": "0.34.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||||
|
|||||||
13
package.json
13
package.json
@ -15,7 +15,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookies-next": "^4.3.0",
|
"cookies-next": "^4.3.0",
|
||||||
|
"framer-motion": "^12.42.0",
|
||||||
|
"lucide-react": "^1.21.0",
|
||||||
"next": "16.2.9",
|
"next": "16.2.9",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"nextjs-toploader": "^3.7.15",
|
"nextjs-toploader": "^3.7.15",
|
||||||
"react": "19.2.7",
|
"react": "19.2.7",
|
||||||
"react-dom": "19.2.7",
|
"react-dom": "19.2.7",
|
||||||
@ -25,23 +28,23 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next/bundle-analyzer": "16.2.9",
|
"@next/bundle-analyzer": "16.2.9",
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
"@types/negotiator": "^0.6.3",
|
"@types/negotiator": "^0.6.3",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "19.2.17",
|
"@types/react": "19.2.17",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^6.0.3",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cypress": "^15.17.0",
|
"cypress": "^15.17.0",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"jsdom": "^29.1.1",
|
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
|
||||||
"@testing-library/react": "^16.3.2",
|
|
||||||
"@vitejs/plugin-react": "^6.0.3",
|
|
||||||
"vitest": "^4.1.9",
|
|
||||||
"eslint-config-next": "16.2.9",
|
"eslint-config-next": "16.2.9",
|
||||||
|
"jsdom": "^29.1.1",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.6",
|
"prettier-plugin-tailwindcss": "^0.6.6",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
|
"vitest": "^4.1.9",
|
||||||
"webpack-bundle-analyzer": "^4.10.2"
|
"webpack-bundle-analyzer": "^4.10.2"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import Image from "next/image";
|
|
||||||
import Testimonials from "../components/testimonials/testimonials";
|
import Testimonials from "../components/testimonials/testimonials";
|
||||||
import Contact from "../components/contact/contact";
|
import Contact from "../components/contact/contact";
|
||||||
import AboutMe from "../components/aboutMe/aboutMe";
|
import AboutMe from "../components/aboutMe/aboutMe";
|
||||||
import Services from "../components/services/services";
|
import Services from "../components/services/services";
|
||||||
import MyProjects from "../components/projects/projects";
|
import MyProjects from "../components/projects/projects";
|
||||||
|
import Hero from "../components/hero/hero";
|
||||||
import { getDictionary } from "@/app/lib/functions/dictionaries";
|
import { getDictionary } from "@/app/lib/functions/dictionaries";
|
||||||
import { getDefaultLocale } from "@/app/lib/functions/utils";
|
import { getDefaultLocale } from "@/app/lib/functions/utils";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
@ -23,79 +23,16 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const dict = await getDictionary(await getDefaultLocale());
|
const dict = await getDictionary(await getDefaultLocale());
|
||||||
const socialLinks = [
|
|
||||||
{ href: "#", src: "/images/icons/facebook.svg", alt: "facebook" },
|
|
||||||
{ href: "#", src: "/images/icons/twitter.svg", alt: "twitter" },
|
|
||||||
{ href: "#", src: "/images/icons/instagram.svg", alt: "instagram" },
|
|
||||||
{ href: "#", src: "/images/icons/linkedin.svg", alt: "linkedin" },
|
|
||||||
];
|
|
||||||
return (
|
return (
|
||||||
<div className="my-15 flex w-full max-w-[1420px] flex-col items-center gap-[50px]">
|
<div className="mesh-gradient min-h-screen w-full flex flex-col items-center">
|
||||||
{/* Home */}
|
<div className="flex w-full max-w-[1420px] flex-col items-center gap-[100px] py-20 px-6">
|
||||||
<div
|
<Hero dict={dict} />
|
||||||
className="xl:gap-15 flex w-full flex-wrap justify-center gap-10 pt-[120px] lg:flex-nowrap 2xl:gap-20"
|
<AboutMe />
|
||||||
id="home"
|
<Services dict={dict} />
|
||||||
>
|
<MyProjects dict={dict} />
|
||||||
{/* start */}
|
<Testimonials dict={dict} />
|
||||||
<div className="flex w-full max-w-[500px] flex-wrap justify-center lg:w-[45%] xl:w-[50%]">
|
<Contact dict={dict} />
|
||||||
<div className="aboutme-image">
|
|
||||||
<Image
|
|
||||||
src="/images/profile.png"
|
|
||||||
alt="Profile picture"
|
|
||||||
width={500} // specify dimensions for next/image optimization
|
|
||||||
height={500}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* socials */}
|
|
||||||
<div className="mt-10 flex w-full justify-center">
|
|
||||||
<div className="flex items-center gap-[25px]">
|
|
||||||
{socialLinks.map(({ href, src, alt }) => (
|
|
||||||
<a key={alt} href={href} rel="noopener noreferrer">
|
|
||||||
<Image
|
|
||||||
src={src}
|
|
||||||
width={32}
|
|
||||||
height={32}
|
|
||||||
alt={alt}
|
|
||||||
className="svg-hover hover:cursor-pointer"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* end */}
|
|
||||||
<div className="items flex w-full max-w-[90%] flex-col md:max-w-[70%] lg:w-[45%] xl:w-[50%]">
|
|
||||||
<p className="text-[20px] font-[400] text-black 2xl:text-[24px]">
|
|
||||||
{dict.website.homePage.top.greeting}
|
|
||||||
</p>
|
|
||||||
<p className="text-[30px] font-[600] capitalize text-[#FD6F00] 2xl:text-[32px]">
|
|
||||||
{dict.website.homePage.top.name}
|
|
||||||
</p>
|
|
||||||
<p className="whitespace-nowrap text-start text-[30px] font-[700] text-black sm:text-[50px] lg:text-[45px] xl:text-[60px] 2xl:text-[80px]">
|
|
||||||
{dict.website.homePage.top.profession}
|
|
||||||
</p>
|
|
||||||
<p className="mt-[-20px] text-end text-[50px] font-[700] text-black lg:mt-[-30px] lg:text-[50px] xl:text-[70px] 2xl:text-[100px]">
|
|
||||||
{dict.website.homePage.top.roll}
|
|
||||||
</p>
|
|
||||||
<p className="my-[40px] text-justify text-[21px] font-[400] text-black lg:pe-0">
|
|
||||||
{dict.website.homePage.top.descriptionAboutMe}
|
|
||||||
</p>
|
|
||||||
{/* <button type="button" className="flex h-[52px] w-[188px] items-center whitespace-nowrap rounded-md bg-[#FD6F00] p-[18px] px-[52px] text-[21px] font-[400] text-[#fff]">
|
|
||||||
Hire Me
|
|
||||||
</button> */}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AboutMe />
|
|
||||||
|
|
||||||
<Services dict={dict} />
|
|
||||||
|
|
||||||
<MyProjects dict={dict} />
|
|
||||||
|
|
||||||
<Testimonials dict={dict} />
|
|
||||||
|
|
||||||
<Contact dict={dict} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
113
src/app/(routes)/(website)/components/aboutMe/AboutMeContent.tsx
Normal file
113
src/app/(routes)/(website)/components/aboutMe/AboutMeContent.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { AboutMeDataType } from "@/app/types/types";
|
||||||
|
|
||||||
|
interface AboutMeContentProps {
|
||||||
|
data: AboutMeDataType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CATEGORIES = {
|
||||||
|
"Core Tech": ["React", "Next.js", "TypeScript", "JavaScript"],
|
||||||
|
"Backend & Tools": ["Laravel", "PHP", "MySQL", "Node.js", "Docker", "Git"],
|
||||||
|
"Architecture & Others": ["REST API", "TDD", "UI/UX Design", "Clean Architecture"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AboutMeContent({ data }: AboutMeContentProps) {
|
||||||
|
const categorizedSkills = {
|
||||||
|
"Core Tech": [],
|
||||||
|
"Backend & Tools": [],
|
||||||
|
"Architecture & Others": [],
|
||||||
|
"Other": [],
|
||||||
|
};
|
||||||
|
|
||||||
|
data.skills.forEach((skill) => {
|
||||||
|
let matched = false;
|
||||||
|
for (const [cat, skills] of Object.entries(CATEGORIES)) {
|
||||||
|
if (skills.some((s) => skill.title.toLowerCase().includes(s.toLowerCase()))) {
|
||||||
|
categorizedSkills[cat].push(skill.title);
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!matched) categorizedSkills["Other"].push(skill.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isRTL = typeof document !== "undefined" && document.dir === "rtl";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="relative flex w-full max-w-[1200px] flex-col items-center justify-center gap-12 py-20 lg:flex-row"
|
||||||
|
id="about-me"
|
||||||
|
>
|
||||||
|
{/* Text Section */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: isRTL ? 50 : -50 }}
|
||||||
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.8 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="flex w-full flex-col items-start gap-6 lg:w-[55%]"
|
||||||
|
>
|
||||||
|
<h2 className="text-4xl md:text-5xl font-bold text-foreground tracking-tight">
|
||||||
|
{data.title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-muted-foreground leading-relaxed text-balance">
|
||||||
|
{data.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-8 grid grid-cols-1 md:grid-cols-2 gap-8 w-full">
|
||||||
|
{Object.entries(categorizedSkills).map(([category, skills]) => (
|
||||||
|
skills.length > 0 && (
|
||||||
|
<motion.div
|
||||||
|
key={category}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="flex flex-col gap-3"
|
||||||
|
>
|
||||||
|
<h3 className="text-sm font-mono font-bold text-primary uppercase tracking-widest">
|
||||||
|
{category}
|
||||||
|
</h3>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{skills.map((skill, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="tech-tag"
|
||||||
|
>
|
||||||
|
{skill}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Image Section */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
whileInView={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.8 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="relative w-full max-w-[450px] flex justify-center lg:w-[40%]"
|
||||||
|
>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute -inset-4 bg-gradient-to-tr from-primary to-purple-600 rounded-3xl blur opacity-20 group-hover:opacity-40 transition duration-500"></div>
|
||||||
|
<div className="relative overflow-hidden rounded-3xl border-4 border-background shadow-2xl">
|
||||||
|
<Image
|
||||||
|
src={process.env.NEXT_PUBLIC_BACK_URL + data.image}
|
||||||
|
alt="profile picture"
|
||||||
|
width={450}
|
||||||
|
height={450}
|
||||||
|
className="w-full h-auto transition-transform duration-500 group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -3,47 +3,26 @@ import { cookies } from "next/headers";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Skeleton from "react-loading-skeleton";
|
import Skeleton from "react-loading-skeleton";
|
||||||
import "react-loading-skeleton/dist/skeleton.css";
|
import "react-loading-skeleton/dist/skeleton.css";
|
||||||
|
import AboutMeContent from "./AboutMeContent";
|
||||||
|
|
||||||
export default async function AboutMe() {
|
export default async function AboutMe() {
|
||||||
const aboutMeData: AboutMeDataType = await fetch(
|
const aboutMeData: AboutMeDataType = await fetch(
|
||||||
`${process.env.NEXT_PUBLIC_API_URL}/website/about-me/1/translation/${(await cookies()).get("selectedLocale")?.value ? (await cookies()).get("selectedLocale")?.value : (await cookies()).get("defaultLocale")?.value}`,
|
`${process.env.NEXT_PUBLIC_API_URL}/website/about-me/1/translation/${(await cookies()).get("selectedLocale")?.value ? (await cookies()).get("selectedLocale")?.value : (await cookies()).get("defaultLocale")?.value}`,
|
||||||
).then((res) => res.json());
|
).then((res) => res.json());
|
||||||
|
|
||||||
return (
|
if (!aboutMeData) {
|
||||||
<div
|
return (
|
||||||
className="my-10 flex w-full max-w-[90%] flex-wrap items-center justify-center gap-20 pt-[120px]"
|
<div className="my-10 flex w-full max-w-[90%] flex-wrap items-center justify-center gap-20 pt-[120px]">
|
||||||
id="about-me"
|
<div className="flex w-full flex-col items-center md:w-[50%]">
|
||||||
>
|
<Skeleton width={300} height={100} />
|
||||||
<div className="flex w-full flex-col items-center md:w-[50%]">
|
<Skeleton width={350} height={30} />
|
||||||
<h2 className="w-full text-start text-[55px] font-[800] text-black">
|
</div>
|
||||||
{aboutMeData?.title || <Skeleton width={300} height={100} />}
|
<div className="flex w-full max-w-[90%] items-center justify-center md:w-[40%]">
|
||||||
</h2>
|
<Skeleton width={400} height={400} />
|
||||||
<p className="my-5 text-start text-[21px] font-[400] text-black">
|
</div>
|
||||||
{aboutMeData?.description || <Skeleton width={350} height={30} />}
|
|
||||||
</p>
|
|
||||||
<ul className="mt-20 flex w-full flex-col gap-10">
|
|
||||||
{aboutMeData.skills.map((skill, index) => (
|
|
||||||
<li key={index}>
|
|
||||||
<h3 className="mb-2 text-[18px] font-[700]">{skill.title}</h3>
|
|
||||||
<div className="relative h-[12px] w-full rounded-lg bg-[#EDECEC]">
|
|
||||||
<span
|
|
||||||
className="absolute h-[12px] rounded-lg bg-[#FD6F00]"
|
|
||||||
style={{ width: `${skill.percentage}%` }}
|
|
||||||
></span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full max-w-[90%] items-center justify-center md:w-[40%]">
|
);
|
||||||
<img
|
}
|
||||||
src={
|
|
||||||
process.env.NEXT_PUBLIC_BACK_URL + aboutMeData.image
|
return <AboutMeContent data={aboutMeData} />;
|
||||||
}
|
|
||||||
alt="profile picture"
|
|
||||||
className="rounded-2xl"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { Send } from "lucide-react";
|
||||||
|
|
||||||
interface ContactProps {
|
interface ContactProps {
|
||||||
dict: {
|
dict: {
|
||||||
@ -16,32 +20,85 @@ interface ContactProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Contact({ dict }: ContactProps) {
|
export default function Contact({ dict }: ContactProps) {
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [submitted, setSubmitted] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
// Simulate API call
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
setIsSubmitting(false);
|
||||||
|
setSubmitted(true);
|
||||||
|
setTimeout(() => setSubmitted(false), 5000);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<section
|
||||||
className="mt-10 flex w-full max-w-[90%] flex-col gap-[30px] pt-[120px]"
|
className="relative flex w-full max-w-[1200px] flex-col items-center py-20"
|
||||||
id="contact"
|
id="contact"
|
||||||
>
|
>
|
||||||
<h2 className="text-center text-[35px] font-[800] capitalize text-[#000]">
|
<div className="text-center mb-16 space-y-4">
|
||||||
{dict.website.homePage.contact.slogan}
|
<motion.h2
|
||||||
</h2>
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<p className="text-center text-[18px] font-[600] text-[#000]">
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
{dict.website.homePage.contact.description}
|
transition={{ duration: 0.6 }}
|
||||||
</p>
|
viewport={{ once: true }}
|
||||||
<div className="my-5 flex w-full flex-wrap items-center justify-center gap-[25px]">
|
className="text-4xl md:text-5xl font-bold text-foreground tracking-tight"
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="min-w-[340px] rounded-lg border border-[#AFAFAF] bg-[#F8F8F8] p-3 text-[16px] font-[400] text-[#797979]"
|
|
||||||
placeholder={dict.website.homePage.contact.placeholder}
|
|
||||||
name="email"
|
|
||||||
id="email"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="whitespace-nowrap rounded-lg bg-[#FD6F00] px-5 py-3 text-[16px] font-[600] capitalize text-[#fff]"
|
|
||||||
>
|
>
|
||||||
{dict.website.homePage.contact.button}
|
{dict.website.homePage.contact.slogan}
|
||||||
</button>
|
</motion.h2>
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.2 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="text-lg text-muted-foreground max-w-2xl mx-auto text-balance"
|
||||||
|
>
|
||||||
|
{dict.website.homePage.contact.description}
|
||||||
|
</motion.p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
whileInView={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.8 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="w-full max-w-3xl p-8 md:p-12 rounded-3xl glass-panel shadow-2xl"
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit} className="flex flex-col md:flex-row gap-4 items-start md:items-center">
|
||||||
|
<div className="relative flex-grow w-full">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
name="email"
|
||||||
|
id="email"
|
||||||
|
placeholder={dict.website.homePage.contact.placeholder}
|
||||||
|
className="w-full px-6 py-4 rounded-2xl bg-background border border-border text-foreground placeholder:text-muted-foreground outline-none focus:ring-2 focus:ring-primary/50 transition-all duration-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
type="submit"
|
||||||
|
className="relative w-full md:w-auto flex items-center justify-center gap-2 px-8 py-4 rounded-2xl bg-primary text-primary-foreground font-bold transition-all duration-300 disabled:opacity-70 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||||
|
) : submitted ? (
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
Sent!
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>{dict.website.homePage.contact.button}</span>
|
||||||
|
<Send className="w-4 h-4" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,104 +6,76 @@ import Link from "next/link";
|
|||||||
|
|
||||||
export default async function Footer() {
|
export default async function Footer() {
|
||||||
const dict = await getDictionary(await getDefaultLocale());
|
const dict = await getDictionary(await getDefaultLocale());
|
||||||
|
|
||||||
|
const footerLinks = [
|
||||||
|
{ href: "/#home", label: dict.website.footer.Home },
|
||||||
|
{ href: "/#about-me", label: dict.website.footer.AboutMe },
|
||||||
|
{ href: "/#services", label: dict.website.footer.Services },
|
||||||
|
{ href: "/#projects", label: dict.website.footer.Projects },
|
||||||
|
{ href: "/#testimonials", label: dict.website.footer.Testimonials },
|
||||||
|
{ href: "/#contact", label: dict.website.footer.Contact },
|
||||||
|
{ href: "/blog", label: dict.website.footer.Blog },
|
||||||
|
];
|
||||||
|
|
||||||
|
const socialLinks = [
|
||||||
|
{ src: "/images/icons/facebook.svg", alt: "facebook" },
|
||||||
|
{ src: "/images/icons/twitter.svg", alt: "twitter" },
|
||||||
|
{ src: "/images/icons/instagram.svg", alt: "instagram" },
|
||||||
|
{ src: "/images/icons/linkedin.svg", alt: "linkedin" },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col">
|
<footer className="w-full flex flex-col">
|
||||||
<div className="flex flex-col items-center gap-[50px] bg-[#F8F8F8] py-[50px]">
|
<div className="flex flex-col items-center gap-12 bg-muted/50 py-16 px-6 transition-colors duration-300">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center gap-3 group">
|
||||||
<Image src={"/images/logo.svg"} width={47} height={47} alt="logo" />
|
<Image src="/images/logo.svg" width={47} height={47} alt="logo" className="transition-transform group-hover:scale-110 duration-300" />
|
||||||
<p className="ms-2 text-[30px] font-[800] capitalize text-[#fd6f00]">
|
<p className="text-2xl font-bold uppercase tracking-tighter text-primary">
|
||||||
parsa aghayi
|
parsa aghayi
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ul className="flex w-full flex-wrap items-center justify-center">
|
|
||||||
<a
|
<nav>
|
||||||
className="whitespace-nowrap px-[14px] py-[10px] text-[18px] font-[500] hover:text-[#fd6f00]"
|
<ul className="flex flex-wrap items-center justify-center gap-x-6 gap-y-2">
|
||||||
href="/#home"
|
{footerLinks.map((link, index) => (
|
||||||
>
|
<li key={index}>
|
||||||
{dict.website.footer.Home}
|
<Link
|
||||||
</a>
|
href={link.href}
|
||||||
<a
|
className="text-sm font-medium text-muted-foreground transition-all duration-300 hover:text-primary"
|
||||||
className="whitespace-nowrap px-[14px] py-[10px] text-[18px] font-[500] hover:text-[#fd6f00]"
|
>
|
||||||
href="/#about-me"
|
{link.label}
|
||||||
>
|
</Link>
|
||||||
{dict.website.footer.AboutMe}
|
</li>
|
||||||
</a>
|
))}
|
||||||
<a
|
</ul>
|
||||||
className="whitespace-nowrap px-[14px] py-[10px] text-[18px] font-[500] hover:text-[#fd6f00]"
|
</nav>
|
||||||
href="/#services"
|
|
||||||
>
|
<div className="flex items-center gap-5">
|
||||||
{dict.website.footer.Services}
|
{socialLinks.map((social, index) => (
|
||||||
</a>
|
<a
|
||||||
<a
|
key={index}
|
||||||
className="whitespace-nowrap px-[14px] py-[10px] text-[18px] font-[500] hover:text-[#fd6f00]"
|
href="#"
|
||||||
href="/#projects"
|
className="p-2 rounded-full glass-panel transition-all duration-300 hover:scale-110 hover:text-primary"
|
||||||
>
|
>
|
||||||
{dict.website.footer.Projects}
|
<Image
|
||||||
</a>
|
src={social.src}
|
||||||
<a
|
width={24}
|
||||||
className="whitespace-nowrap px-[14px] py-[10px] text-[18px] font-[500] hover:text-[#fd6f00]"
|
height={24}
|
||||||
href="/#testimonials"
|
alt={social.alt}
|
||||||
>
|
className="w-6 h-6"
|
||||||
{dict.website.footer.Testimonials}
|
/>
|
||||||
</a>
|
</a>
|
||||||
<a
|
))}
|
||||||
className="whitespace-nowrap px-[14px] py-[10px] text-[18px] font-[500] hover:text-[#fd6f00]"
|
|
||||||
href="/#contact"
|
|
||||||
>
|
|
||||||
{dict.website.footer.Contact}
|
|
||||||
</a>
|
|
||||||
<Link
|
|
||||||
className="whitespace-nowrap px-[14px] py-[10px] text-[18px] font-[500] hover:text-[#fd6f00]"
|
|
||||||
href="/blog"
|
|
||||||
>
|
|
||||||
{dict.website.footer.Blog}
|
|
||||||
</Link>
|
|
||||||
</ul>
|
|
||||||
{/* socials */}
|
|
||||||
<div className="flex w-full justify-center">
|
|
||||||
<div className="flex items-center gap-[25px]">
|
|
||||||
<Image
|
|
||||||
src={"/images/icons/facebook.svg"}
|
|
||||||
width={32}
|
|
||||||
height={32}
|
|
||||||
alt="facebook"
|
|
||||||
className="svg-hover hover:cursor-pointer"
|
|
||||||
/>
|
|
||||||
<Image
|
|
||||||
src={"/images/icons/twitter.svg"}
|
|
||||||
width={32}
|
|
||||||
height={32}
|
|
||||||
alt="twitter"
|
|
||||||
className="svg-hover hover:cursor-pointer"
|
|
||||||
/>
|
|
||||||
<Image
|
|
||||||
src={"/images/icons/instagram.svg"}
|
|
||||||
width={32}
|
|
||||||
height={32}
|
|
||||||
alt="instagram"
|
|
||||||
className="svg-hover hover:cursor-pointer"
|
|
||||||
/>
|
|
||||||
<Image
|
|
||||||
src={"/images/icons/linkedin.svg"}
|
|
||||||
width={32}
|
|
||||||
height={32}
|
|
||||||
alt="linkedin"
|
|
||||||
className="svg-hover hover:cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center gap-1 whitespace-nowrap bg-[#545454]">
|
|
||||||
<p className="py-2 text-[15px] font-[400] text-white">
|
<div className="flex items-center justify-center gap-2 py-6 px-4 bg-background border-t border-border text-center">
|
||||||
{dict.website.footer.Year}
|
<p className="text-xs font-medium text-muted-foreground">
|
||||||
|
{dict.website.footer.Year} {dict.website.footer.CopyRight}
|
||||||
</p>
|
</p>
|
||||||
<p className="py-2 text-[15px] font-[400] text-white">
|
<span className="text-xs font-bold text-primary uppercase tracking-widest">
|
||||||
{dict.website.footer.CopyRight}
|
|
||||||
</p>
|
|
||||||
<span className="mx-[5px] whitespace-nowrap text-[18px] font-[700] capitalize text-[#FD6F00]">
|
|
||||||
{dict.website.footer.Name}
|
{dict.website.footer.Name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</footer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,85 +2,74 @@ import React from "react";
|
|||||||
import BurgerButton from "../burger-button/burgerButton";
|
import BurgerButton from "../burger-button/burgerButton";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import SelectLanguage from "../selectLanguage/selectLanguage";
|
import SelectLanguage from "../selectLanguage/selectLanguage";
|
||||||
|
import { ThemeToggle } from "../theme-toggle/theme-toggle";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { getDictionary } from "@/app/lib/functions/dictionaries";
|
import { getDictionary } from "@/app/lib/functions/dictionaries";
|
||||||
import { getDefaultLocale } from "@/app/lib/functions/utils";
|
import { getDefaultLocale } from "@/app/lib/functions/utils";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
export default async function Header() {
|
export default async function Header() {
|
||||||
const dict = await getDictionary(await getDefaultLocale());
|
const dict = await getDictionary(await getDefaultLocale());
|
||||||
|
|
||||||
|
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 },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<header
|
||||||
className="fixed z-40 flex h-[67px] w-full items-center justify-between bg-[#fff] px-2 py-[50px] xl:px-10"
|
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"
|
||||||
id="header"
|
id="header"
|
||||||
>
|
>
|
||||||
<div className="flex flex-wrap items-center justify-center xl:flex-nowrap xl:justify-normal">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex w-[40px] sm:w-[50px]">
|
<Link href="/" className="flex items-center gap-3 group">
|
||||||
<img src={"/images/logo.svg"} className="w-full" alt="logo" />
|
<div className="relative w-10 h-10 overflow-hidden rounded-xl transition-transform group-hover:scale-110 duration-300">
|
||||||
</div>
|
<img src="/images/logo.svg" className="w-full h-full object-cover" alt="logo" />
|
||||||
<p className="ms-2 font-[900] capitalize text-[#FD6F00] text-[px] sm:text-[23px] xl:ms-3 xl:text-[30px]">
|
</div>
|
||||||
{dict.website.header.Name}
|
<span className="font-mono font-bold text-xl md:text-2xl text-primary tracking-tighter uppercase italic">
|
||||||
</p>
|
{dict.website.header.Name}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<ul className="hidden items-center justify-center rounded p-2 lg:flex">
|
|
||||||
<Link
|
<nav className="hidden lg:flex items-center gap-1">
|
||||||
href="/#home"
|
<ul className="flex items-center gap-2 me-4">
|
||||||
className="mx-2 whitespace-nowrap rounded-lg px-1 py-[10px] text-[15px] font-[600] capitalize hover:text-[#FD6F00] xl:text-[20px]"
|
{navLinks.map((link) => (
|
||||||
>
|
<li key={link.href}>
|
||||||
{dict.website.header.Home}
|
<Link
|
||||||
</Link>
|
href={link.href}
|
||||||
<Link
|
className="px-4 py-2 text-sm font-medium text-muted-foreground transition-all duration-300 hover:text-primary rounded-full hover:bg-primary/10"
|
||||||
href="/#about-me"
|
>
|
||||||
className="mx-2 whitespace-nowrap rounded-lg px-1 py-[10px] text-[15px] font-[600] capitalize hover:text-[#FD6F00] xl:text-[20px]"
|
{link.label}
|
||||||
>
|
</Link>
|
||||||
{dict.website.header.AboutMe}
|
</li>
|
||||||
</Link>
|
))}
|
||||||
<Link
|
</ul>
|
||||||
href="/#services"
|
<div className="flex items-center gap-2 border-s border-border ps-4">
|
||||||
className="mx-2 whitespace-nowrap rounded-lg px-1 py-[10px] text-[15px] font-[600] capitalize hover:text-[#FD6F00] xl:text-[20px]"
|
<SelectLanguage
|
||||||
>
|
selectedLocale={(await cookies()).get("selectedLocale")}
|
||||||
{dict.website.header.Services}
|
defaultLocale={(await cookies()).get("defaultLocale")}
|
||||||
</Link>
|
/>
|
||||||
<Link
|
<ThemeToggle />
|
||||||
href="/#projects"
|
</div>
|
||||||
className="mx-2 whitespace-nowrap rounded-lg px-1 py-[10px] text-[15px] font-[600] capitalize hover:text-[#FD6F00] xl:text-[20px]"
|
</nav>
|
||||||
>
|
|
||||||
{dict.website.header.Projects}
|
<div className="flex items-center gap-3">
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="/#testimonials"
|
|
||||||
className="mx-2 whitespace-nowrap rounded-lg px-1 py-[10px] text-[15px] font-[600] capitalize hover:text-[#FD6F00] xl:text-[20px]"
|
|
||||||
>
|
|
||||||
{dict.website.header.Testimonials}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="/#contact"
|
|
||||||
className="mx-2 whitespace-nowrap rounded-lg px-1 py-[10px] text-[15px] font-[600] capitalize hover:text-[#FD6F00] xl:text-[20px]"
|
|
||||||
>
|
|
||||||
{dict.website.header.Contact}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
className="mx-2 whitespace-nowrap rounded-lg px-1 py-[10px] text-[15px] font-[600] capitalize hover:text-[#FD6F00] xl:text-[20px]"
|
|
||||||
href={"/blog"}
|
|
||||||
>
|
|
||||||
{dict.website.header.Blog}
|
|
||||||
</Link>
|
|
||||||
<SelectLanguage
|
|
||||||
selectedLocale={(await cookies()).get("selectedLocale")}
|
|
||||||
defaultLocale={(await cookies()).get("defaultLocale")}
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<a
|
<a
|
||||||
href="/Parsa Aghayi CV Resume.pdf"
|
href="/Parsa Aghayi CV Resume.pdf"
|
||||||
download={true}
|
download
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="flex items-center justify-center whitespace-nowrap rounded-lg bg-[#FD6F00] px-4 py-2 text-[15px] font-[600] text-[#fff] xl:text-[20px]"
|
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(255,84,128,0.3)] transition-all duration-300 hover:shadow-[0_0_30px_rgba(255,84,128,0.5)] hover:scale-105 active:scale-95"
|
||||||
>
|
>
|
||||||
{dict.website.header.Button}
|
{dict.website.header.Button}
|
||||||
</a>
|
</a>
|
||||||
<BurgerButton dict={dict} />
|
<BurgerButton dict={dict} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
143
src/app/(routes)/(website)/components/hero/hero.tsx
Normal file
143
src/app/(routes)/(website)/components/hero/hero.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
interface HeroProps {
|
||||||
|
dict: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Hero({ dict }: HeroProps) {
|
||||||
|
const socialLinks = [
|
||||||
|
{ href: "#", src: "/images/icons/facebook.svg", alt: "facebook" },
|
||||||
|
{ href: "#", src: "/images/icons/twitter.svg", alt: "twitter" },
|
||||||
|
{ href: "#", src: "/images/icons/instagram.svg", alt: "instagram" },
|
||||||
|
{ href: "#", src: "/images/icons/linkedin.svg", alt: "linkedin" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Detect RTL
|
||||||
|
const isRTL = typeof document !== "undefined" && document.dir === "rtl";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="home"
|
||||||
|
className="relative flex w-full flex-col items-center justify-center gap-12 pt-[120px] pb-20 lg:flex-row xl:gap-20"
|
||||||
|
>
|
||||||
|
{/* Image Section */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: isRTL ? 50 : -50 }}
|
||||||
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="relative z-10 flex w-full max-w-[500px] flex-col items-center justify-center lg:w-[45%] xl:w>[50%]"
|
||||||
|
>
|
||||||
|
<div className="relative group">
|
||||||
|
{/* Glowing background effect */}
|
||||||
|
<div className="absolute -inset-1 bg-gradient-to-r from-primary to-purple-600 rounded-full blur opacity-25 group-hover:opacity-50 transition duration-1000 group-hover:duration-200"></div>
|
||||||
|
|
||||||
|
<div className="relative w-64 h-64 md:w-80 md:h-80 lg:w-96 lg:h-96 overflow-hidden rounded-full border-4 border-background shadow-2xl">
|
||||||
|
<Image
|
||||||
|
src="/images/profile.png"
|
||||||
|
alt="Profile picture"
|
||||||
|
width={500}
|
||||||
|
height={500}
|
||||||
|
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Socials */}
|
||||||
|
<div className="mt-8 flex items-center gap-6">
|
||||||
|
{socialLinks.map(({ href, src, alt }, index) => (
|
||||||
|
<motion.a
|
||||||
|
key={alt}
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.5 + index * 0.1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
whileHover={{ scale: 1.2, rotate: 5 }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
className="p-2 rounded-full glass-panel hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
alt={alt}
|
||||||
|
className="w-6 h-6"
|
||||||
|
/>
|
||||||
|
</motion.a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Text Section */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: isRTL ? -50 : 50 }}
|
||||||
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="flex w-full max-w-[700px] flex-col items-start text-start"
|
||||||
|
>
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
whileInView={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.3 }}
|
||||||
|
className="text-lg md:text-xl font-medium text-muted-foreground mb-2"
|
||||||
|
>
|
||||||
|
{dict.website.homePage.top.greeting}
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.h1
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.4 }}
|
||||||
|
className="text-3xl md:text-4xl font-bold text-primary mb-1"
|
||||||
|
>
|
||||||
|
{dict.website.homePage.top.name}
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
className="flex flex-col gap-0"
|
||||||
|
>
|
||||||
|
<span className="text-4xl md:text-6xl lg:text-7xl xl:text-8xl font-extrabold tracking-tighter text-foreground leading-tight">
|
||||||
|
{dict.website.homePage.top.profession}
|
||||||
|
</span>
|
||||||
|
<span className="text-3xl md:text-5xl lg:text-6xl xl:text-7xl font-mono font-bold text-primary/80 tracking-tighter leading-tight italic">
|
||||||
|
{dict.website.homePage.top.roll}
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
whileInView={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.6 }}
|
||||||
|
className="mt-6 text-lg md:text-xl text-muted-foreground leading-relaxed max-w-2xl text-balance"
|
||||||
|
>
|
||||||
|
{dict.website.homePage.top.descriptionAboutMe}
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.8 }}
|
||||||
|
className="mt-10"
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<span className="relative z-10">Let's Work Together</span>
|
||||||
|
<div className="absolute inset-0 rounded-full bg-primary blur-lg opacity-50 group-hover:opacity-100 transition-opacity"></div>
|
||||||
|
</a>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -11,6 +11,8 @@ import Link from "next/link";
|
|||||||
import React, { useEffect, useState, useMemo } from "react";
|
import React, { useEffect, useState, useMemo } from "react";
|
||||||
import Skeleton from "react-loading-skeleton";
|
import Skeleton from "react-loading-skeleton";
|
||||||
import "react-loading-skeleton/dist/skeleton.css";
|
import "react-loading-skeleton/dist/skeleton.css";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
interface MyProjectsProps {
|
interface MyProjectsProps {
|
||||||
dict: {
|
dict: {
|
||||||
@ -31,20 +33,13 @@ const MAX_CATEGORIES = 5;
|
|||||||
export default function Projects({ dict }: MyProjectsProps) {
|
export default function Projects({ dict }: MyProjectsProps) {
|
||||||
const [categories, setCategories] = useState<ProjectCategory[]>([]);
|
const [categories, setCategories] = useState<ProjectCategory[]>([]);
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string>(
|
const [selectedCategory, setSelectedCategory] = useState<string>("");
|
||||||
getCookie("selectedLocale") === "fa" || getCookie("defaultLocale") === "fa"
|
|
||||||
? "جدیدترینها"
|
|
||||||
: "Newest",
|
|
||||||
);
|
|
||||||
const locale = getCookie("selectedLocale") || getCookie("defaultLocale");
|
const locale = getCookie("selectedLocale") || getCookie("defaultLocale");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedCategory(
|
const initialCategory = locale === "fa" ? "جدیدترینها" : "Newest";
|
||||||
getCookie("selectedLocale") === "fa" ||
|
setSelectedCategory(initialCategory);
|
||||||
getCookie("defaultLocale") === "fa"
|
|
||||||
? "جدیدترینها"
|
|
||||||
: "Newest",
|
|
||||||
);
|
|
||||||
const fetchProjects = async () => {
|
const fetchProjects = async () => {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`/api/website/projects/translation/${locale}?sort=desc&per_page=40`,
|
`/api/website/projects/translation/${locale}?sort=desc&per_page=40`,
|
||||||
@ -54,13 +49,11 @@ export default function Projects({ dict }: MyProjectsProps) {
|
|||||||
(data.projects as ProjectResponse).data || [];
|
(data.projects as ProjectResponse).data || [];
|
||||||
const resCategories: CategoryResponse[] = data.categories || [];
|
const resCategories: CategoryResponse[] = data.categories || [];
|
||||||
|
|
||||||
// Set projects for display
|
|
||||||
setProjects(resProjects);
|
setProjects(resProjects);
|
||||||
|
|
||||||
// Set categories including "Newest" as the first item
|
|
||||||
const uniqueCategories: ProjectCategory[] = [
|
const uniqueCategories: ProjectCategory[] = [
|
||||||
{
|
{
|
||||||
label: locale === "fa" ? "جدیدترینها" : "Newest",
|
label: initialCategory,
|
||||||
active: true,
|
active: true,
|
||||||
},
|
},
|
||||||
...resCategories
|
...resCategories
|
||||||
@ -77,130 +70,144 @@ export default function Projects({ dict }: MyProjectsProps) {
|
|||||||
fetchProjects();
|
fetchProjects();
|
||||||
}, [locale]);
|
}, [locale]);
|
||||||
|
|
||||||
// Group projects by category
|
const groupedProjects = useMemo(
|
||||||
// const groupProjectsByCategory = (projects: Project[]) => {
|
() => projects.reduce<Record<string, Project[]>>((acc, project) => {
|
||||||
// return projects.reduce<Record<string, Project[]>>((acc, project) => {
|
|
||||||
// const category =
|
|
||||||
// project.category ||
|
|
||||||
// (locale === "fa" ? "بدون دستهبندی" : "Uncategorized");
|
|
||||||
// if (!acc[category]) {
|
|
||||||
// acc[category] = [];
|
|
||||||
// }
|
|
||||||
// acc[category].push(project);
|
|
||||||
// return acc;
|
|
||||||
// }, {});
|
|
||||||
// };
|
|
||||||
|
|
||||||
const groupProjectsByCategory = (projects: Project[]) => {
|
|
||||||
return projects.reduce<Record<string, Project[]>>((acc, project) => {
|
|
||||||
const category =
|
const category =
|
||||||
project.category ||
|
project.category ||
|
||||||
(locale === "fa" ? "بدون دستهبندی" : "Uncategorized");
|
(locale === "fa" ? "بدون دستهبندی" : "Uncategorized");
|
||||||
if (!acc[category]) {
|
if (!acc[category]) acc[category] = [];
|
||||||
acc[category] = [];
|
|
||||||
}
|
|
||||||
acc[category].push(project);
|
acc[category].push(project);
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {}),
|
||||||
};
|
[projects, locale],
|
||||||
|
|
||||||
const groupedProjects = useMemo(
|
|
||||||
() => groupProjectsByCategory(projects),
|
|
||||||
[projects],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filter projects by selected category
|
|
||||||
// const filteredProjects: Project[] = useMemo(() => {
|
|
||||||
// if (selectedCategory === (locale === "fa" ? "جدیدترینها" : "Newest")) {
|
|
||||||
// return projects.slice(0, MAX_PROJECTS);
|
|
||||||
// }
|
|
||||||
// return groupedProjects[selectedCategory]?.slice(0, MAX_PROJECTS) || [];
|
|
||||||
// }, [selectedCategory, projects, groupedProjects, locale]);
|
|
||||||
|
|
||||||
const filteredProjects: Project[] = useMemo(() => {
|
const filteredProjects: Project[] = useMemo(() => {
|
||||||
if (selectedCategory === (locale === "fa" ? "جدیدترینها" : "Newest")) {
|
const newestLabel = locale === "fa" ? "جدیدترینها" : "Newest";
|
||||||
|
if (selectedCategory === newestLabel) {
|
||||||
return projects.slice(0, MAX_PROJECTS);
|
return projects.slice(0, MAX_PROJECTS);
|
||||||
}
|
}
|
||||||
return groupedProjects[selectedCategory]?.slice(0, MAX_PROJECTS) || [];
|
return groupedProjects[selectedCategory]?.slice(0, MAX_PROJECTS) || [];
|
||||||
}, [selectedCategory, projects, groupedProjects, locale]);
|
}, [selectedCategory, projects, groupedProjects, locale]);
|
||||||
|
|
||||||
const renderSkeletons = (count: number) => (
|
const renderSkeletons = (count: number) => (
|
||||||
<div className="flex w-full flex-wrap justify-center gap-5">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 w-full">
|
||||||
{Array.from({ length: count }, (_, index) => (
|
{Array.from({ length: count }, (_, index) => (
|
||||||
<div key={index} className="w-[45%] md:w-[30%]">
|
<div key={index} className="flex flex-col gap-4">
|
||||||
<Skeleton containerClassName={"w-full"} height={150} />
|
<Skeleton containerClassName="w-full rounded-2xl" height={250} />
|
||||||
<Skeleton containerClassName={"w-full"} height={40} />
|
<Skeleton containerClassName="w-full" height={32} />
|
||||||
<Skeleton containerClassName={"w-full"} height={60} />
|
<Skeleton containerClassName="w-full" height={60} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<section
|
||||||
className="flex w-full max-w-[90%] flex-wrap items-center justify-center pt-[50px]"
|
className="relative flex w-full max-w-[1200px] flex-col items-center py-20"
|
||||||
id="projects"
|
id="projects"
|
||||||
>
|
>
|
||||||
<h2 className="my-10 mt-20 text-[55px] font-[800] capitalize text-black">
|
<div className="text-center mb-16 space-y-4">
|
||||||
{dict.website.homePage.projects.title}
|
<motion.h2
|
||||||
</h2>
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<p className="text-center text-[21px] font-[400] text-black">
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
{dict.website.homePage.projects.description}
|
transition={{ duration: 0.6 }}
|
||||||
</p>
|
viewport={{ once: true }}
|
||||||
<ul className="mb-10 mt-20 flex w-full flex-wrap items-center justify-center gap-2">
|
className="text-4xl md:text-5xl font-bold text-foreground tracking-tight"
|
||||||
|
>
|
||||||
|
{dict.website.homePage.projects.title}
|
||||||
|
</motion.h2>
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.2 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="text-lg text-muted-foreground max-w-2xl mx-auto text-balance"
|
||||||
|
>
|
||||||
|
{dict.website.homePage.projects.description}
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center justify-center gap-3 mb-12">
|
||||||
{categories.length > 0
|
{categories.length > 0
|
||||||
? categories.map((category, index) => (
|
? categories.map((category, index) => (
|
||||||
<li
|
<motion.button
|
||||||
key={index}
|
key={index}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
onClick={() => setSelectedCategory(category.label)}
|
onClick={() => setSelectedCategory(category.label)}
|
||||||
className={`select-none whitespace-nowrap rounded-md bg-[#F8F8F8] p-2 text-[14px] font-[600] capitalize hover:cursor-pointer hover:border-[#FD6F00] md:text-[18px] ${
|
className={`px-5 py-2 rounded-full text-sm font-medium transition-all duration-300 ${
|
||||||
selectedCategory === category.label
|
selectedCategory === category.label
|
||||||
? "bg-[#FD6F00] text-[#FFFFFF]"
|
? "bg-primary text-primary-foreground shadow-lg shadow-primary/30"
|
||||||
: "text-[#000]"
|
: "bg-muted text-muted-foreground hover:bg-muted-foreground/20"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{category.label}
|
{category.label}
|
||||||
</li>
|
</motion.button>
|
||||||
))
|
))
|
||||||
: renderSkeletons(4)}
|
: renderSkeletons(4)}
|
||||||
</ul>
|
|
||||||
<div className="flex w-full flex-wrap items-start justify-center gap-8 lg:gap-20">
|
|
||||||
{filteredProjects.length > 0
|
|
||||||
? filteredProjects.map((project, index) => (
|
|
||||||
<div key={index} className="project flex w-[45%] flex-wrap">
|
|
||||||
<div className="relative my-5 h-[200px] w-full overflow-hidden bg-[#FFEBDB] md:h-[250px] lg:h-[300px] rounded-lg">
|
|
||||||
{project.images.map((image, idx) => (
|
|
||||||
<Link
|
|
||||||
href={
|
|
||||||
process.env.NEXT_PUBLIC_BACK_URL +
|
|
||||||
"/images/projects/" +
|
|
||||||
image.src
|
|
||||||
}
|
|
||||||
target="_blank"
|
|
||||||
key={idx}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={
|
|
||||||
process.env.NEXT_PUBLIC_BACK_URL +
|
|
||||||
"/images/projects/" +
|
|
||||||
image.src
|
|
||||||
}
|
|
||||||
alt={`Image for ${project.title}`}
|
|
||||||
className={`absolute ${idx === 0 ? "bottom-0 start-0 z-[10]" : "end-0 top-0 z-[2]"} project-image w-[85%] max-w-[350px]`}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<h5 className="w-full text-[15px] font-[600] text-[#FD6F00] lg:text-[20px]">
|
|
||||||
{project.category}
|
|
||||||
</h5>
|
|
||||||
<p className="text-[18px] font-[800] text-black hover:cursor-pointer lg:text-[22px]">
|
|
||||||
{project.title}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
: renderSkeletons(3)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<motion.div
|
||||||
|
layout
|
||||||
|
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 w-full"
|
||||||
|
>
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{filteredProjects.length > 0
|
||||||
|
? filteredProjects.map((project, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={project.id}
|
||||||
|
layout
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="group relative rounded-3xl overflow-hidden glass-panel"
|
||||||
|
>
|
||||||
|
{/* Project Image Gallery */}
|
||||||
|
<div className="relative h-64 w-full overflow-hidden">
|
||||||
|
{project.images.map((image, idx) => (
|
||||||
|
<Image
|
||||||
|
key={idx}
|
||||||
|
src={process.env.NEXT_PUBLIC_BACK_URL + "/images/projects/" + image.src}
|
||||||
|
alt={`Image for ${project.title}`}
|
||||||
|
fill
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Project Details */}
|
||||||
|
<div className="p-6">
|
||||||
|
<h5 className="text-sm font-mono text-primary mb-1">
|
||||||
|
{project.category}
|
||||||
|
</h5>
|
||||||
|
<p className="text-xl font-bold text-foreground group-hover:text-primary transition-colors">
|
||||||
|
{project.title}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))
|
||||||
|
: renderSkeletons(3)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import { getCookie } from "cookies-next";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Skeleton from "react-loading-skeleton";
|
import Skeleton from "react-loading-skeleton";
|
||||||
import "react-loading-skeleton/dist/skeleton.css";
|
import "react-loading-skeleton/dist/skeleton.css";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
interface ServicesProps {
|
interface ServicesProps {
|
||||||
dict: {
|
dict: {
|
||||||
@ -22,8 +24,6 @@ interface ServicesProps {
|
|||||||
export default function Services({ dict }: ServicesProps) {
|
export default function Services({ dict }: ServicesProps) {
|
||||||
const [services, setServices] = useState<Service[]>([]);
|
const [services, setServices] = useState<Service[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
// const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchServices = async () => {
|
const fetchServices = async () => {
|
||||||
@ -40,54 +40,76 @@ export default function Services({ dict }: ServicesProps) {
|
|||||||
}, [getCookie("selectedLocale")]);
|
}, [getCookie("selectedLocale")]);
|
||||||
|
|
||||||
const renderServiceCard = (service?: Service, index?: number) => (
|
const renderServiceCard = (service?: Service, index?: number) => (
|
||||||
<div
|
<motion.div
|
||||||
key={service?.id || index}
|
key={service?.id || index}
|
||||||
className="flex w-[45%] flex-wrap items-center rounded-lg bg-[#F8F8F8] p-[22px] lg:w-[22%]"
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5, delay: (index || 0) * 0.1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
whileHover={{ y: -10 }}
|
||||||
|
className="group flex w-full flex-col items-start rounded-3xl glass-panel p-8 transition-all duration-300 hover:border-primary/50"
|
||||||
>
|
>
|
||||||
<div className="my-5 flex w-full justify-start">
|
<div className="mb-6 relative overflow-hidden rounded-2xl w-20 h-20 bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Skeleton containerClassName="w-[100px]" height={120} />
|
<Skeleton containerClassName="w-12 h-12" />
|
||||||
) : (
|
) : (
|
||||||
<img src={service!.imageSrc} alt={service!.altText} />
|
<img
|
||||||
|
src={service!.imageSrc}
|
||||||
|
alt={service!.altText}
|
||||||
|
className="w-12 h-12 object-contain transition-transform duration-300 group-hover:scale-110"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="mb-5 w-full text-[18px] font-[700] text-black xl:text-[23px]">
|
<h3 className="mb-4 text-2xl font-bold text-foreground group-hover:text-primary transition-colors">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Skeleton containerClassName="w-full" height={32} />
|
<Skeleton containerClassName="w-full" height={32} />
|
||||||
) : (
|
) : (
|
||||||
service!.title
|
service!.title
|
||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="w-full text-start text-[14px] font-[400] text-black xl:text-[20px]">
|
<p className="text-muted-foreground leading-relaxed text-sm md:text-base">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Skeleton containerClassName="w-full" height={60} />
|
<Skeleton containerClassName="w-full" height={60} />
|
||||||
) : (
|
) : (
|
||||||
service!.description
|
service!.description
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// if (error) return <p>Error: {error}</p>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<section
|
||||||
className="mb-20 flex w-full max-w-[90%] flex-col flex-wrap items-center pt-[100px]"
|
className="relative flex w-full max-w-[1200px] flex-col items-center py-20"
|
||||||
id="services"
|
id="services"
|
||||||
>
|
>
|
||||||
<h2 className="mb-10 text-[55px] font-[800] capitalize text-black">
|
<div className="text-center mb-16 space-y-4">
|
||||||
{dict.website.homePage.services.title}
|
<motion.h2
|
||||||
</h2>
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<p className="text-center text-[21px] font-[400] text-black">
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
{dict.website.homePage.services.description}
|
transition={{ duration: 0.6 }}
|
||||||
</p>
|
viewport={{ once: true }}
|
||||||
<div className="service mt-20 flex w-full flex-wrap items-start justify-between gap-8">
|
className="text-4xl md:text-5xl font-bold text-foreground tracking-tight"
|
||||||
|
>
|
||||||
|
{dict.website.homePage.services.title}
|
||||||
|
</motion.h2>
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.2 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="text-lg text-muted-foreground max-w-2xl mx-auto text-balance"
|
||||||
|
>
|
||||||
|
{dict.website.homePage.services.description}
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 w-full">
|
||||||
{loading
|
{loading
|
||||||
? Array.from({ length: 4 }).map((_, index) =>
|
? Array.from({ length: 4 }).map((_, index) =>
|
||||||
renderServiceCard(undefined, index),
|
renderServiceCard(undefined, index),
|
||||||
)
|
)
|
||||||
: services.map((service) => renderServiceCard(service))}
|
: services.map((service, index) => renderServiceCard(service, index))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Testimonial } from "@/app/types/types";
|
import { Testimonial } from "@/app/types/types";
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { Quote } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
interface TestimonialsProps {
|
interface TestimonialsProps {
|
||||||
dict: {
|
dict: {
|
||||||
@ -55,106 +58,78 @@ const testimonials: Testimonial[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function Testimonials({ dict }: TestimonialsProps) {
|
export default function Testimonials({ dict }: TestimonialsProps) {
|
||||||
const [currentIndex, setCurrentIndex] = useState<number>(0); // شروع با آیتم وسطی
|
|
||||||
const sliderRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const [isHovered, setIsHovered] = useState<boolean>(false);
|
|
||||||
const [bulletClicked, setBulletClicked] = useState<boolean>(false);
|
|
||||||
|
|
||||||
// Auto-scroll every 5 seconds
|
|
||||||
useEffect(() => {
|
|
||||||
if (sliderRef.current) {
|
|
||||||
let maxHeight = 0;
|
|
||||||
|
|
||||||
// Iterate through each child of the slider
|
|
||||||
Array.from(sliderRef.current.children).forEach((child) => {
|
|
||||||
const childHeight = child.clientHeight; // Get the height of the child
|
|
||||||
if (childHeight > maxHeight) {
|
|
||||||
maxHeight = childHeight; // Update maxHeight if current child is taller
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// setMaxHeight(maxHeight + 20); // Set the maximum height
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isHovered && !bulletClicked) {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setCurrentIndex((prevIndex) =>
|
|
||||||
prevIndex === testimonials.length - 1 ? 0 : prevIndex + 1,
|
|
||||||
);
|
|
||||||
}, 2000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, [isHovered, bulletClicked]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<section
|
||||||
className="flex w-full max-w-[90%] flex-wrap justify-center pt-[100px]"
|
className="relative flex w-full max-w-[1200px] flex-col items-center py-20"
|
||||||
id="testimonials"
|
id="testimonials"
|
||||||
>
|
>
|
||||||
<h2 className="text-[55px] font-[800] capitalize text-black">
|
<div className="text-center mb-16 space-y-4">
|
||||||
{dict.website.homePage.testimonials.title}
|
<motion.h2
|
||||||
</h2>
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<p className="my-10 text-center text-[21px] font-[400] text-black">
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
{dict.website.homePage.testimonials.description}
|
transition={{ duration: 0.6 }}
|
||||||
</p>
|
viewport={{ once: true }}
|
||||||
<div
|
className="text-4xl md:text-5xl font-bold text-foreground tracking-tight"
|
||||||
className="no-scrollbar relative flex min-h-[800px] w-full select-none flex-nowrap items-start gap-8 overflow-x-scroll py-4 sm:min-h-[590px] md:min-h-[560px] lg:min-h-[450px] xl:min-h-[370px] 2xl:min-h-[370px]" // Hide scrollbar using CSS
|
>
|
||||||
ref={sliderRef}
|
{dict.website.homePage.testimonials.title}
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
</motion.h2>
|
||||||
onMouseLeave={() => {
|
<motion.p
|
||||||
setIsHovered(false);
|
initial={{ opacity: 0, y: 20 }}
|
||||||
setBulletClicked(false);
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
}}
|
transition={{ duration: 0.6, delay: 0.2 }}
|
||||||
>
|
viewport={{ once: true }}
|
||||||
{testimonials.map((testimonial, index) => (
|
className="text-lg text-muted-foreground max-w-2xl mx-auto text-balance"
|
||||||
<div
|
>
|
||||||
className={`testimonial absolute flex w-[80%] flex-wrap items-center justify-around gap-3 rounded-lg bg-[#F8F8F8] p-5 hover:cursor-grab md:w-[70%] ${currentIndex === index ? "start-[15%] z-20 scale-105 drop-shadow-lg sm:start-[15%] md:start-[25%]" : "start-0 z-10 scale-90 grayscale"} ${
|
{dict.website.homePage.testimonials.description}
|
||||||
currentIndex === index ? "" : "blur-[2px]"
|
</motion.p>
|
||||||
}`}
|
|
||||||
key={index}
|
|
||||||
>
|
|
||||||
<div className="flex justify-center lg:w-[30%]">
|
|
||||||
<img
|
|
||||||
src={testimonial.image}
|
|
||||||
className="pointer-events-none w-[50px] min-w-[100px] sm:w-[150px]"
|
|
||||||
alt="user profile picture"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap items-center lg:w-[60%]">
|
|
||||||
<p className="mb-3 w-full text-justify text-[15px] font-[400] text-black">
|
|
||||||
<span className="me-2 text-[12px] font-[700] text-[#FD6F00] sm:text-[21px]">
|
|
||||||
“
|
|
||||||
</span>
|
|
||||||
{testimonial.quote}
|
|
||||||
<span className="ms-2 text-[21px] font-[700] text-[#FD6F00]">
|
|
||||||
“
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<h5 className="w-full text-[20px] font-[700] text-[#000]">
|
|
||||||
{testimonial.name}
|
|
||||||
</h5>
|
|
||||||
<p className="w-full text-[15px] font-[400] text-[#000]">
|
|
||||||
{testimonial.position}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className="mt-10 flex w-full items-center justify-center gap-5">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">
|
||||||
{testimonials.map((_, index) => (
|
{testimonials.map((testimonial, index) => (
|
||||||
<li
|
<motion.div
|
||||||
key={index}
|
key={index}
|
||||||
className={`${
|
initial={{ opacity: 0, y: 30 }}
|
||||||
currentIndex === index ? "bg-[#FD6F00] shadow-lg" : "bg-[#D9D9D9]"
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
} h-[40px] w-[40px] rounded-lg hover:cursor-pointer sm:h-[30px] sm:w-[50px] md:h-[30px] md:w-[50px] lg:h-[20px] lg:w-[50px]`}
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||||
onClick={() => {
|
viewport={{ once: true }}
|
||||||
setCurrentIndex(index);
|
whileHover={{ y: -10 }}
|
||||||
setBulletClicked(true);
|
className="relative p-8 rounded-3xl glass-panel flex flex-col h-full"
|
||||||
}}
|
>
|
||||||
></li>
|
<div className="absolute -top-6 -left-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>
|
||||||
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Moon, Sun } from "lucide-react";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
const [mounted, setMounted] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return <div className="w-10 h-10" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||||
|
className="p-2 rounded-xl glass-panel text-foreground transition-colors hover:bg-primary/20"
|
||||||
|
aria-label="Toggle Theme"
|
||||||
|
>
|
||||||
|
{theme === "dark" ? (
|
||||||
|
<Sun className="w-5 h-5 text-yellow-400" />
|
||||||
|
) : (
|
||||||
|
<Moon className="w-5 h-5 text-slate-700" />
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,319 +2,90 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
@layer base {
|
||||||
--color-orange: #fd6f00;
|
:root {
|
||||||
--font-mikhak: "mikhak";
|
--background: 240 10% 98%;
|
||||||
}
|
--foreground: 240 10% 3.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
--primary: 340 80% 50%; /* Accent Pink/Red */
|
||||||
|
--primary-foreground: 0 0% 100%;
|
||||||
|
--secondary: 240 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
--muted: 240 4.8% 95.9%;
|
||||||
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
--accent: 240 4.8% 95.9%;
|
||||||
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
|
--input: 240 5.9% 90%;
|
||||||
|
--ring: 240 5.9% 10%;
|
||||||
|
--radius: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
.dark {
|
||||||
font-family: var(--font-mikhak);
|
--background: 240 10% 3.9%;
|
||||||
}
|
--foreground: 0 0% 98%;
|
||||||
|
--card: 240 10% 6%;
|
||||||
* {
|
--card-foreground: 0 0% 98%;
|
||||||
margin: 0;
|
--popover: 240 10% 3.9%;
|
||||||
padding: 0;
|
--popover-foreground: 0 0% 98%;
|
||||||
}
|
--primary: 340 80% 60%;
|
||||||
|
--primary-foreground: 0 0% 10%;
|
||||||
html {
|
--secondary: 240 3.7% 15.9%;
|
||||||
scroll-behavior: smooth;
|
--secondary-foreground: 0 0% 98%;
|
||||||
}
|
--muted: 240 3.7% 15.9%;
|
||||||
|
--muted-foreground: 240 5% 64.9%;
|
||||||
.scroll-watcher {
|
--accent: 240 3.7% 15.9%;
|
||||||
height: 10px;
|
--accent-foreground: 0 0% 98%;
|
||||||
position: fixed;
|
--destructive: 0 62.8% 30.6%;
|
||||||
top: 0;
|
--destructive-foreground: 0 0% 98%;
|
||||||
z-index: 1000;
|
--border: 240 3.7% 15.9%;
|
||||||
background-color: var(--color-orange);
|
--input: 240 3.7% 15.9%;
|
||||||
width: 100%;
|
--ring: 240 4.9% 83.9%;
|
||||||
scale: 0 1;
|
|
||||||
-webkit-animation: scroll-watcher linear;
|
|
||||||
animation-timeline: scroll();
|
|
||||||
transform-origin: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service,
|
|
||||||
.project {
|
|
||||||
scale: 0.5;
|
|
||||||
opacity: 0.1;
|
|
||||||
-webkit-animation: fade-in linear forwards;
|
|
||||||
animation-timeline: view();
|
|
||||||
animation-range: 100px 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes scroll-watcher {
|
|
||||||
to {
|
|
||||||
scale: 1 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@-webkit-keyframes fade-in {
|
@layer base {
|
||||||
to {
|
* {
|
||||||
scale: 1;
|
@apply border-border;
|
||||||
opacity: 1;
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground font-sans transition-colors duration-300;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress {
|
@layer components {
|
||||||
-webkit-animation: progress linear forwards;
|
.glass-panel {
|
||||||
animation-timeline: view();
|
@apply backdrop-blur-md bg-white/10 dark:bg-black/20 border border-white/20 dark:border-white/10;
|
||||||
animation-range: 25px 150px;
|
}
|
||||||
}
|
|
||||||
|
.tech-tag {
|
||||||
@-webkit-keyframes progress {
|
@apply px-3 py-1 rounded-full text-xs font-mono border border-primary/30 bg-primary/10 text-primary transition-all hover:bg-primary hover:text-white;
|
||||||
from {
|
|
||||||
width: 0px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#back-to-top {
|
@layer utilities {
|
||||||
opacity: 0;
|
.text-balance {
|
||||||
scale: 0;
|
text-wrap: balance;
|
||||||
bottom: 50px;
|
|
||||||
z-index: 1000;
|
|
||||||
-webkit-animation: back-to-top linear forwards;
|
|
||||||
animation-timeline: scroll();
|
|
||||||
animation-range: 400px 550px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes back-to-top {
|
|
||||||
to {
|
|
||||||
opacity: 0.9;
|
|
||||||
bottom: 100px;
|
|
||||||
scale: 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* #header {
|
|
||||||
-webkit-animation: stick-to-top linear forwards;
|
|
||||||
animation-timeline: scroll();
|
|
||||||
animation-range: 67px 100px;
|
|
||||||
}
|
|
||||||
@-webkit-keyframes stick-to-top {
|
|
||||||
to {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .img-container {
|
|
||||||
position: relative;
|
|
||||||
width: 538px;
|
|
||||||
height: 617px;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .top-half-circle {
|
|
||||||
position: absolute;
|
|
||||||
width: 538px;
|
|
||||||
height: 269px;
|
|
||||||
bottom: 269px;
|
|
||||||
border: 10px solid var(--color-orange);
|
|
||||||
border-bottom: 10px solid transparent;
|
|
||||||
border-top-left-radius: 269px;
|
|
||||||
border-top-right-radius: 269px;
|
|
||||||
z-index: 0;
|
|
||||||
-webkit-animation: top-half 1.5s ease-out;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .top-half-circle img {
|
|
||||||
position: absolute;
|
|
||||||
bottom: -269px;
|
|
||||||
z-index: +5;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .bottom-half-circle {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
width: 538px;
|
|
||||||
height: 269px;
|
|
||||||
border: 10px solid var(--color-orange);
|
|
||||||
border-top: 10px solid transparent;
|
|
||||||
border-bottom-left-radius: 269px;
|
|
||||||
border-bottom-right-radius: 269px;
|
|
||||||
overflow: hidden;
|
|
||||||
z-index: 6;
|
|
||||||
-webkit-animation: bottom-half 2s infinite linear;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .bottom-half-circle img {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0px;
|
|
||||||
z-index: 0;
|
|
||||||
} */
|
|
||||||
|
|
||||||
@-webkit-keyframes top-half {
|
|
||||||
0% {
|
|
||||||
bottom: -1000px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@-webkit-keyframes bottom-half {
|
|
||||||
0% {
|
|
||||||
transform: rotate(-6deg);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: rotate(6deg);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(-6deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.aboutme-image {
|
|
||||||
position: relative;
|
|
||||||
background: rgba(255, 255, 255, 0.6);
|
|
||||||
-webkit-animation: 5s water linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@property --angle {
|
|
||||||
syntax: "<angle>";
|
|
||||||
initial-value: 0deg;
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
.aboutme-image::after,
|
|
||||||
.aboutme-image::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
translate: -50% -50%;
|
|
||||||
background-image: conic-gradient(
|
|
||||||
from var(--angle),
|
|
||||||
#ff0095,
|
|
||||||
#fd6f00,
|
|
||||||
#00ff99,
|
|
||||||
#fd6f00,
|
|
||||||
#006aff,
|
|
||||||
#fd6f00,
|
|
||||||
#ff0095
|
|
||||||
);
|
|
||||||
z-index: -1;
|
|
||||||
padding: 3px;
|
|
||||||
border-top-left-radius: 25%;
|
|
||||||
border-top-right-radius: 25%;
|
|
||||||
border-bottom-right-radius: 10px;
|
|
||||||
border-bottom-left-radius: 10px;
|
|
||||||
-webkit-animation: 3s spin linear infinite;
|
|
||||||
}
|
|
||||||
.aboutme-image::before {
|
|
||||||
filter: blur(1.5rem);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aboutme-image {
|
|
||||||
width: 90%;
|
|
||||||
height: 80%;
|
|
||||||
border-radius: 50%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes spin {
|
|
||||||
0% {
|
|
||||||
--angle: 0deg;
|
|
||||||
scale: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
--angle: 180deg;
|
|
||||||
scale: 1.1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
--angle: 360deg;
|
|
||||||
scale: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes water {
|
|
||||||
0% {
|
|
||||||
border-radius: 25% 46% 25% 25% / 25% 50% 25% 25%;
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
border-radius: 59% 33% 56% 25% / 55% 38% 26% 25%;
|
|
||||||
}
|
|
||||||
40% {
|
|
||||||
border-radius: 53% 41% 63% 25% / 46% 53% 36% 25%;
|
|
||||||
}
|
|
||||||
60% {
|
|
||||||
border-radius: 41% 40% 66% 25% / 38% 47% 47% 34%;
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
border-radius: 35% 33% 61% 28% / 25% 59% 30% 68%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
border-radius: 25% 46% 25% 25% / 25% 50% 25% 25%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-image:hover {
|
|
||||||
scale: 1.05;
|
|
||||||
z-index: 10000;
|
|
||||||
cursor: zoom-in;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svg-hover {
|
|
||||||
transition: all ease-in-out 123ms;
|
|
||||||
-webkit-transition: -webkit-filter 123ms ease-in-out;
|
|
||||||
scale: 1;
|
|
||||||
}
|
|
||||||
.svg-hover:hover {
|
|
||||||
scale: 1.05;
|
|
||||||
filter: brightness(0) saturate(100%) invert(46%) sepia(59%) saturate(3359%)
|
|
||||||
hue-rotate(6deg) brightness(103%) contrast(104%);
|
|
||||||
transition: all ease-in-out 123ms;
|
|
||||||
-webkit-transition: -webkit-filter 123ms ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.burger-menu {
|
|
||||||
filter: brightness(0) saturate(100%) invert(100%) sepia(6%) saturate(0%)
|
|
||||||
hue-rotate(115deg) brightness(108%) contrast(108%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-menu {
|
|
||||||
opacity: 0.95;
|
|
||||||
transition: all ease-in-out 0.5s;
|
|
||||||
}
|
|
||||||
.no-scrollbar::-webkit-scrollbar {
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.no-scrollbar {
|
.no-scrollbar {
|
||||||
-ms-overflow-style: none; /* IE and Edge */
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none;
|
||||||
}
|
|
||||||
.testimonial {
|
|
||||||
transition: all ease-in-out 0.5s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.center-center {
|
.mesh-gradient {
|
||||||
@apply items-center justify-center;
|
background-image:
|
||||||
}
|
radial-gradient(at 0% 0%, hsla(340, 80%, 50%, 0.15) 0px, transparent 50%),
|
||||||
|
radial-gradient(at 100% 0%, hsla(220, 80%, 50%, 0.15) 0px, transparent 50%),
|
||||||
.teste {
|
radial-gradient(at 100% 100%, hsla(340, 80%, 50%, 0.15) 0px, transparent 50%),
|
||||||
-webkit-mask-image: linear-gradient(
|
radial-gradient(at 0% 100%, hsla(220, 80%, 50%, 0.15) 0px, transparent 50%);
|
||||||
to top,
|
|
||||||
transparent,
|
|
||||||
white 20%,
|
|
||||||
white 80%,
|
|
||||||
transparent
|
|
||||||
),
|
|
||||||
linear-gradient(to bottom, transparent, white 10%, white 90%, transparent),
|
|
||||||
linear-gradient(to left, transparent, white 10%, white 90%, transparent),
|
|
||||||
linear-gradient(to right, transparent, white 10%, white 90%, transparent);
|
|
||||||
mask-image: linear-gradient(
|
|
||||||
to top,
|
|
||||||
transparent,
|
|
||||||
white 20%,
|
|
||||||
white 90%,
|
|
||||||
transparent
|
|
||||||
),
|
|
||||||
linear-gradient(to bottom, transparent, white 10%, white 90%, transparent),
|
|
||||||
linear-gradient(to left, transparent, white 10%, white 90%, transparent),
|
|
||||||
linear-gradient(to right, transparent, white 10%, white 90%, transparent);
|
|
||||||
mask-composite: intersect;
|
|
||||||
-webkit-mask-composite: destination-in;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import localFont from "next/font/local";
|
import localFont from "next/font/local";
|
||||||
|
import { Inter, JetBrains_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { getDefaultLocale } from "./lib/functions/utils";
|
import { getDefaultLocale } from "./lib/functions/utils";
|
||||||
|
import { ThemeProvider } from "./lib/providers/theme-provider";
|
||||||
|
|
||||||
const mikhak = localFont({
|
const mikhak = localFont({
|
||||||
src: "lib/fonts/mikhak-v32/variable/Mikhak[DSTY,KSHD,wght].woff2",
|
src: "lib/fonts/mikhak-v32/variable/Mikhak[DSTY,KSHD,wght].woff2",
|
||||||
@ -8,6 +10,16 @@ const mikhak = localFont({
|
|||||||
weight: "100 900",
|
weight: "100 900",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const inter = Inter({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-inter",
|
||||||
|
});
|
||||||
|
|
||||||
|
const jetbrainsMono = JetBrains_Mono({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-jetbrains-mono",
|
||||||
|
});
|
||||||
|
|
||||||
export default async function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
@ -16,14 +28,21 @@ export default async function RootLayout({
|
|||||||
const defaultLocale = await getDefaultLocale();
|
const defaultLocale = await getDefaultLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={defaultLocale} dir={defaultLocale === "fa" ? "rtl" : "ltr"}>
|
<html lang={defaultLocale} dir={defaultLocale === "fa" ? "rtl" : "ltr"} suppressHydrationWarning>
|
||||||
<body
|
<body
|
||||||
suppressHydrationWarning={true}
|
suppressHydrationWarning={true}
|
||||||
className={`${mikhak.variable} relative flex h-full w-full flex-col items-center justify-between antialiased`}
|
className={`${mikhak.variable} ${inter.variable} ${jetbrainsMono.variable} relative flex h-full w-full flex-col items-center justify-between antialiased`}
|
||||||
>
|
>
|
||||||
<div className="main flex h-full w-full flex-wrap justify-center">
|
<ThemeProvider
|
||||||
{children}
|
attribute="class"
|
||||||
</div>
|
defaultTheme="dark"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
|
<div className="main flex h-full w-full flex-wrap justify-center">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
11
src/app/lib/providers/theme-provider.tsx
Normal file
11
src/app/lib/providers/theme-provider.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||||
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||||
|
}
|
||||||
@ -8,6 +8,11 @@ const config: Config = {
|
|||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ["var(--font-inter)", "var(--font-mikhak)", "ui-sans-serif", "system-ui"],
|
||||||
|
mono: ["var(--font-jetbrains-mono)", "ui-monospace", "SFMono-Regular", "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", "monospace"],
|
||||||
|
mikhak: ["var(--font-mikhak)", "ui-sans-serif", "system-ui"],
|
||||||
|
},
|
||||||
colors: {
|
colors: {
|
||||||
background: "var(--background)",
|
background: "var(--background)",
|
||||||
foreground: "var(--foreground)",
|
foreground: "var(--foreground)",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user