From 08e94d7e91911115bd344ffeca58840339da7661 Mon Sep 17 00:00:00 2001 From: Autli Date: Sat, 26 Sep 2026 20:32:10 +0500 Subject: [PATCH] add translate i18n --- public/sitemap.xml | 26 +- src/app/[lang]/layout.tsx | 86 +++ src/app/[lang]/page.tsx | 10 + src/app/[lang]/projects/page.tsx | 39 ++ src/app/layout.tsx | 31 +- src/app/page.tsx | 27 +- src/app/projects/page.tsx | 40 +- src/components/about/about.tsx | 24 +- src/components/hero/hero.tsx | 27 +- src/components/process/process.tsx | 41 +- src/components/projects/ProjectCard.tsx | 11 +- src/components/projects/projects.tsx | 26 +- src/components/sponsors/sponsors.tsx | 11 +- src/components/testimonials/testimonials.tsx | 44 +- src/shared/config/i18n/context.tsx | 33 ++ src/shared/config/i18n/dictionaries.ts | 519 ++++++++++++++++++ src/shared/config/i18n/index.ts | 3 + src/shared/config/i18n/types.ts | 149 +++++ src/shared/config/projects/projects.ts | 97 +--- src/static/header/header.module.css | 46 ++ src/views/projects/ui/ProjectsPage.tsx | 23 +- src/widgets/context-menu/ui/ContextMenu.tsx | 110 +--- .../cookie-consent/ui/CookieConsent.tsx | 16 +- src/widgets/footer/ui/Footer.tsx | 34 +- src/widgets/header/ui/Header.tsx | 65 ++- src/widgets/preloader/ui/Preloader.tsx | 7 +- src/widgets/project-modal/ui/ProjectModal.tsx | 39 +- 27 files changed, 1173 insertions(+), 411 deletions(-) create mode 100644 src/app/[lang]/layout.tsx create mode 100644 src/app/[lang]/page.tsx create mode 100644 src/app/[lang]/projects/page.tsx create mode 100644 src/shared/config/i18n/context.tsx create mode 100644 src/shared/config/i18n/dictionaries.ts create mode 100644 src/shared/config/i18n/index.ts create mode 100644 src/shared/config/i18n/types.ts diff --git a/public/sitemap.xml b/public/sitemap.xml index 44a3a80..7b9c51d 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -1,13 +1,33 @@ - + - https://autli.ru/ + https://autli.ru/ru/ + + 2026-09-26 monthly 1.0 - https://autli.ru/projects/ + https://autli.ru/en/ + + + 2026-09-26 + monthly + 0.9 + + + https://autli.ru/ru/projects/ + + + 2026-09-26 + monthly + 0.8 + + + https://autli.ru/en/projects/ + + 2026-09-26 monthly 0.8 diff --git a/src/app/[lang]/layout.tsx b/src/app/[lang]/layout.tsx new file mode 100644 index 0000000..6dcb212 --- /dev/null +++ b/src/app/[lang]/layout.tsx @@ -0,0 +1,86 @@ +import type { ReactNode } from "react"; +import type { Metadata } from "next"; +import { I18nProvider, dictionaries, type Locale } from "@/shared/config/i18n"; +import { ContextMenu } from "@/widgets/context-menu"; +import { CookieConsent } from "@/widgets/cookie-consent"; +import { Cursor } from "@/widgets/cursor"; +import { Preloader } from "@/widgets/preloader"; +import { ScrollCounter } from "@/widgets/scroll-counter"; +import { YandexMetrika } from "@/widgets/yandex-metrika"; + +export function generateStaticParams() { + return [{ lang: "ru" }, { lang: "en" }]; +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ lang: string }>; +}): Promise { + const { lang } = await params; + const locale: Locale = lang === "en" ? "en" : "ru"; + const dict = dictionaries[locale]; + const siteUrl = "https://autli.ru"; + + return { + metadataBase: new URL(siteUrl), + title: { + default: dict.seo.title, + template: `%s | ${locale === "ru" ? "Никита Егоров" : "Nikita Egorov"}`, + }, + description: dict.seo.description, + keywords: dict.seo.keywords, + alternates: { + canonical: `/${locale}/`, + languages: { + ru: "/ru/", + en: "/en/", + }, + }, + openGraph: { + title: dict.seo.title, + description: dict.seo.ogDescription, + url: `${siteUrl}/${locale}/`, + siteName: locale === "ru" ? "Никита Егоров — Портфолио" : "Nikita Egorov — Portfolio", + locale: locale === "ru" ? "ru_RU" : "en_US", + type: "website", + images: [ + { + url: "/og-image.png", + width: 1200, + height: 630, + alt: locale === "ru" ? "Никита Егоров — Frontend & Web Developer" : "Nikita Egorov — Frontend & Web Developer", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: dict.seo.title, + description: dict.seo.ogDescription, + images: ["/og-image.png"], + }, + }; +} + +export default async function LocaleLayout({ + children, + params, +}: { + children: ReactNode; + params: Promise<{ lang: string }>; +}) { + const { lang } = await params; + const locale: Locale = lang === "en" ? "en" : "ru"; + + return ( + + + + + + {children} + + + + ); +} diff --git a/src/app/[lang]/page.tsx b/src/app/[lang]/page.tsx new file mode 100644 index 0000000..15bb717 --- /dev/null +++ b/src/app/[lang]/page.tsx @@ -0,0 +1,10 @@ +import { HomePage } from "@/views/home"; +import "@/static/globals.css"; + +export function generateStaticParams() { + return [{ lang: "ru" }, { lang: "en" }]; +} + +export default async function Page() { + return ; +} diff --git a/src/app/[lang]/projects/page.tsx b/src/app/[lang]/projects/page.tsx new file mode 100644 index 0000000..2ef0dbf --- /dev/null +++ b/src/app/[lang]/projects/page.tsx @@ -0,0 +1,39 @@ +import type { Metadata } from "next"; +import { ProjectsPage } from "@/views/projects"; +import { dictionaries, type Locale } from "@/shared/config/i18n"; + +export function generateStaticParams() { + return [{ lang: "ru" }, { lang: "en" }]; +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ lang: string }>; +}): Promise { + const { lang } = await params; + const locale: Locale = lang === "en" ? "en" : "ru"; + const dict = dictionaries[locale]; + const siteUrl = "https://autli.ru"; + + return { + title: dict.seo.projectsPageTitle, + description: dict.seo.projectsPageDesc, + alternates: { + canonical: `/${locale}/projects/`, + languages: { + ru: "/ru/projects/", + en: "/en/projects/", + }, + }, + openGraph: { + title: `${dict.seo.projectsPageTitle} | ${locale === "ru" ? "Никита Егоров" : "Nikita Egorov"}`, + description: dict.seo.projectsPageDesc, + url: `${siteUrl}/${locale}/projects/`, + }, + }; +} + +export default async function Projects() { + return ; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6094f3d..426c531 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,12 +1,6 @@ -import type { Metadata } from "next"; import type { ReactNode } from "react"; +import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; -import { ContextMenu } from "@/widgets/context-menu"; -import { CookieConsent } from "@/widgets/cookie-consent"; -import { Cursor } from "@/widgets/cursor"; -import { Preloader } from "@/widgets/preloader"; -import { ScrollCounter } from "@/widgets/scroll-counter"; -import { YandexMetrika } from "@/widgets/yandex-metrika"; import "@/static/globals.css"; const geistSans = Geist({ @@ -55,7 +49,11 @@ export const metadata: Metadata = { telephone: false, }, alternates: { - canonical: "/", + canonical: "/ru/", + languages: { + ru: "/ru/", + en: "/en/", + }, }, openGraph: { title: "Никита Егоров — Frontend & Web Developer | Портфолио", @@ -104,10 +102,7 @@ const jsonLd = { alternateName: "Nikita Egorov", jobTitle: "Frontend & Web Developer", url: siteUrl, - sameAs: [ - "https://t.me/autli", - "https://github.com/autli5", - ], + sameAs: ["https://t.me/autli", "https://github.com/autli5"], knowsAbout: [ "React", "Next.js", @@ -137,7 +132,7 @@ const jsonLd = { ], }; -const RootLayout = ({ children }: { children: ReactNode }) => { +export default function RootLayout({ children }: { children: ReactNode }) { return ( { /> - - - - {children} - - ); -}; - -export default RootLayout; \ No newline at end of file +} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index 2fb9a48..e4c4107 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,8 +1,23 @@ -import { HomePage } from "@/views/home"; -import "@/static/globals.css"; +"use client"; -const Page = () => { - return ; -}; +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; -export default Page; \ No newline at end of file +export default function RootPage() { + const router = useRouter(); + + useEffect(() => { + router.replace("/ru/"); + }, [router]); + + return ( + <> + + + +
+ Redirecting to /ru/... +
+ + ); +} \ No newline at end of file diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx index be6db3f..5f1e082 100644 --- a/src/app/projects/page.tsx +++ b/src/app/projects/page.tsx @@ -1,23 +1,23 @@ -import type { Metadata } from "next"; -import { ProjectsPage } from "@/views/projects"; +"use client"; -export const metadata: Metadata = { - title: "Все проекты", - description: - "Коммерческие и пет-проекты Egorov Nikita: веб-приложения, интернет-магазины, SaaS-дашборды и сайты под ключ на React, Next.js, TypeScript и Django.", - alternates: { - canonical: "/projects/", - }, - openGraph: { - title: "Все проекты | Никита Егоров", - description: - "Коммерческие и пет-проекты Egorov Nikita: веб-приложения, интернет-магазины, SaaS-дашборды и сайты под ключ.", - url: "https://autli.ru/projects/", - }, -}; +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; -const Projects = () => { - return ; -}; +export default function ProjectsRedirect() { + const router = useRouter(); -export default Projects; \ No newline at end of file + useEffect(() => { + router.replace("/ru/projects/"); + }, [router]); + + return ( + <> + + + +
+ Redirecting to /ru/projects/... +
+ + ); +} \ No newline at end of file diff --git a/src/components/about/about.tsx b/src/components/about/about.tsx index 5eb08aa..1d1e6f7 100644 --- a/src/components/about/about.tsx +++ b/src/components/about/about.tsx @@ -1,6 +1,7 @@ "use client"; import { useInView } from "@/shared/hooks/useInView"; +import { useI18n } from "@/shared/config/i18n"; import styles from "@/static/about/about.module.css"; const techStack = [ @@ -13,9 +14,10 @@ const techStack = [ { name: "Node.js", level: 33 }, ]; -const About = () => { +export const About = () => { const [introRef, introInView] = useInView(0.2); const [skillsRef, skillsInView] = useInView(0.3); + const { dict } = useI18n(); const reveal = (visible: boolean) => `transition-all duration-700 ease-out will-change-transform ${ @@ -33,42 +35,40 @@ const About = () => {

- Разработчик, для которого важны{" "} - продуманность и детали + {dict.about.titleStart}{" "} + {dict.about.titleAccent}{" "} + {dict.about.titleEnd}

- Привет! Я Никита — веб-разработчик. Строю современные, быстрые и - аккуратные интерфейсы на React и Next.js. Уделяю внимание - производительности, семантике и мелочам, из которых складывается - хорошее впечатление от сайта. + {dict.about.desc}

-
опыт
+
{dict.about.stats.expLabel}
3+
-
проекты
+
{dict.about.stats.projectsLabel}
10+
-
стек
+
{dict.about.stats.stackLabel}
{techStack.length}
@@ -78,7 +78,7 @@ const About = () => { className={`${styles.skills} ${reveal(skillsInView)}`} style={{ transitionDelay: "150ms" }} > -

Стек и навыки

+

{dict.about.skillsTitle}

    {techStack.map((item, index) => ( diff --git a/src/components/hero/hero.tsx b/src/components/hero/hero.tsx index ff3aad0..a053a4b 100644 --- a/src/components/hero/hero.tsx +++ b/src/components/hero/hero.tsx @@ -1,6 +1,8 @@ -import { sociallinks } from "@/shared/config/social-links/links"; -import styles from "@/static/hero/hero.module.css"; +"use client"; +import { sociallinks } from "@/shared/config/social-links/links"; +import { useI18n } from "@/shared/config/i18n"; +import styles from "@/static/hero/hero.module.css"; import FlashlightText from "./FlashlightText"; const socialIcon = (label: string) => { @@ -34,7 +36,9 @@ const socialIcon = (label: string) => { } }; -const Hero = () => { +export const Hero = () => { + const { dict, locale } = useI18n(); + return (
    @@ -42,21 +46,18 @@ const Hero = () => {
    - веб-разработчик + {dict.hero.role}

    - +

    -

    - Создаю современные, быстрые и продуманные веб-сайты и - веб-приложения под ключ. -

    +

    {dict.hero.desc}

    - - Смотреть проекты + + {dict.hero.ctaButton}
      @@ -81,14 +82,14 @@ const Hero = () => {
    diff --git a/src/components/process/process.tsx b/src/components/process/process.tsx index 4c41647..6640c0a 100644 --- a/src/components/process/process.tsx +++ b/src/components/process/process.tsx @@ -1,34 +1,13 @@ "use client"; import { useInView } from "@/shared/hooks/useInView"; +import { useI18n } from "@/shared/config/i18n"; import styles from "@/static/process/process.module.css"; -const steps = [ - { - label: "01", - title: "Бриф", - text: "Разбираю задачу, аудиторию, цели сайта и фиксирую, какой результат должен получиться.", - }, - { - label: "02", - title: "Структура", - text: "Собираю логику страниц, сценарии пользователя и расставляю смысловые акценты.", - }, - { - label: "03", - title: "Разработка", - text: "Верстаю адаптивный интерфейс, добавляю интерактив, формы, аналитику и базовое SEO.", - }, - { - label: "04", - title: "Запуск", - text: "Проверяю скорость, мобильную версию, мелкие состояния и помогаю спокойно выкатить сайт.", - }, -]; - -const Process = () => { +export const Process = () => { const [headRef, headInView] = useInView(0.24); const [stepsRef, stepsInView] = useInView(0.18); + const { dict } = useI18n(); const reveal = (visible: boolean) => `transition-all duration-700 ease-out will-change-transform ${ @@ -47,7 +26,7 @@ const Process = () => {

    @@ -55,16 +34,14 @@ const Process = () => { className={`${styles.title} ${reveal(headInView)}`} style={{ transitionDelay: "100ms" }} > - От идеи до запуска без хаоса и лишней суеты + {dict.process.title}

    - Сайт должен не просто красиво открываться, а вести человека к - действию. Поэтому я двигаюсь по понятным этапам: сначала смысл, - потом интерфейс, затем аккуратная разработка и проверка деталей. + {dict.process.desc}

    @@ -73,13 +50,13 @@ const Process = () => { ref={stepsRef} className={`${styles.timeline} ${stepsInView ? styles.active : ""}`} > - {steps.map((step, index) => ( + {dict.process.steps.map((step, index) => (
    - {step.label} + {step.number}

    {step.title}

    {step.text}

    diff --git a/src/components/projects/ProjectCard.tsx b/src/components/projects/ProjectCard.tsx index 5b279f0..ab4baac 100644 --- a/src/components/projects/ProjectCard.tsx +++ b/src/components/projects/ProjectCard.tsx @@ -1,8 +1,8 @@ "use client"; import type { KeyboardEvent, MouseEvent } from "react"; - import type { Project } from "@/shared/config/projects/types"; +import { useI18n } from "@/shared/config/i18n"; import styles from "@/static/projects/project-card.module.css"; type ProjectCardProps = { @@ -10,10 +10,11 @@ type ProjectCardProps = { onOpen: (project: Project) => void; }; -const ProjectCard = ({ project, onOpen }: ProjectCardProps) => { +export const ProjectCard = ({ project, onOpen }: ProjectCardProps) => { + const { dict } = useI18n(); + const handleSpotlight = (e: MouseEvent) => { const element = e.currentTarget; - const rect = element.getBoundingClientRect(); element.style.setProperty( @@ -40,7 +41,7 @@ const ProjectCard = ({ project, onOpen }: ProjectCardProps) => { tabIndex={0} data-cursor-hover aria-haspopup="dialog" - aria-label={`${project.title}: подробнее`} + aria-label={`${project.title}: ${dict.projectsSection.cardDetails}`} onClick={() => onOpen(project)} onKeyDown={handleKeyDown} onMouseMove={handleSpotlight} @@ -63,7 +64,7 @@ const ProjectCard = ({ project, onOpen }: ProjectCardProps) => {
- подробнее + {dict.projectsSection.cardDetails} diff --git a/src/components/projects/projects.tsx b/src/components/projects/projects.tsx index 84f85e0..f67ee06 100644 --- a/src/components/projects/projects.tsx +++ b/src/components/projects/projects.tsx @@ -1,22 +1,21 @@ "use client"; import { useState } from "react"; - import Link from "next/link"; - import { useInView } from "@/shared/hooks/useInView"; -import { projects } from "@/shared/config/projects/projects"; +import { getProjects } from "@/shared/config/projects/projects"; import type { Project } from "@/shared/config/projects/types"; import { ProjectModal } from "@/widgets/project-modal"; +import { useI18n } from "@/shared/config/i18n"; +import ProjectCard from "./ProjectCard"; import styles from "@/static/projects/projects.module.css"; -import ProjectCard from "./ProjectCard"; - -const preview = projects.slice(0, 6); - -const ProjectsSection = () => { +export const ProjectsSection = () => { const [gridRef, gridInView] = useInView(0.12); const [selected, setSelected] = useState(null); + const { locale, dict } = useI18n(); + + const preview = getProjects(locale).slice(0, 6); const reveal = (visible: boolean) => `transition-all duration-700 ease-out will-change-transform ${ @@ -36,16 +35,19 @@ const ProjectsSection = () => {

- Избранные проекты + {dict.projectsSection.titleStart}{" "} + + {dict.projectsSection.titleAccent} +

- - все проекты + + {dict.projectsSection.viewAll} diff --git a/src/components/sponsors/sponsors.tsx b/src/components/sponsors/sponsors.tsx index 9a28395..1ce3909 100644 --- a/src/components/sponsors/sponsors.tsx +++ b/src/components/sponsors/sponsors.tsx @@ -1,3 +1,6 @@ +"use client"; + +import { useI18n } from "@/shared/config/i18n"; import styles from "@/static/sponsors/sponsors.module.css"; const sponsors = [ @@ -40,11 +43,13 @@ const List = ({ hidden = false }: { hidden?: boolean }) => ( ); -const Sponsors = () => { +export const Sponsors = () => { + const { dict } = useI18n(); + return ( -
+
-

С ними я работал

+

{dict.sponsors.label}

diff --git a/src/components/testimonials/testimonials.tsx b/src/components/testimonials/testimonials.tsx index 9bfaeb5..920d015 100644 --- a/src/components/testimonials/testimonials.tsx +++ b/src/components/testimonials/testimonials.tsx @@ -1,37 +1,12 @@ "use client"; import { useInView } from "@/shared/hooks/useInView"; +import { useI18n } from "@/shared/config/i18n"; import styles from "@/static/testimonials/testimonials.module.css"; -const testimonials = [ - { - quote: - "Никита сделал лендинг, который реально приносит лиды. Быстро, аккуратно, без лишней воды в коммуникации.", - name: "Алексей Орлов", - role: "Основатель финтех-стартапа", - }, - { - quote: - "Код понятный, страницы летают, а правки после сдачи вносить легко. Работать с ним — одно удовольствие.", - name: "Мария Соколова", - role: "Продакт-менеджер", - }, - { - quote: - "За 2 недели собрал нам интернет-магазин под ключ: каталог, оплата, админка. Всё чётко и в срок.", - name: "Дмитрий Волков", - role: "Владелец бренда одежды", - }, -]; - -const initials = (name: string) => - name - .split(" ") - .map((part) => part[0]) - .join(""); - -const Testimonials = () => { +export const Testimonials = () => { const [gridRef, gridInView] = useInView(0.15); + const { dict } = useI18n(); const reveal = (visible: boolean) => `transition-all duration-700 ease-out will-change-transform ${ @@ -49,17 +24,18 @@ const Testimonials = () => {

- Что обо мне говорят + {dict.testimonials.titleStart}{" "} + {dict.testimonials.titleAccent}

- {testimonials.map((item, index) => ( + {dict.testimonials.items.map((item, index) => (
@@ -75,10 +51,10 @@ const Testimonials = () => {
- {item.name} + {item.author} {item.role}
diff --git a/src/shared/config/i18n/context.tsx b/src/shared/config/i18n/context.tsx new file mode 100644 index 0000000..e153244 --- /dev/null +++ b/src/shared/config/i18n/context.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { createContext, useContext, ReactNode } from "react"; +import type { Dictionary, Locale } from "./types"; +import { dictionaries } from "./dictionaries"; + +interface I18nContextType { + locale: Locale; + dict: Dictionary; +} + +const I18nContext = createContext({ + locale: "ru", + dict: dictionaries.ru, +}); + +export const I18nProvider = ({ + locale, + children, +}: { + locale: Locale; + children: ReactNode; +}) => { + const dict = dictionaries[locale] || dictionaries.ru; + + return ( + + {children} + + ); +}; + +export const useI18n = () => useContext(I18nContext); diff --git a/src/shared/config/i18n/dictionaries.ts b/src/shared/config/i18n/dictionaries.ts new file mode 100644 index 0000000..1b4fab7 --- /dev/null +++ b/src/shared/config/i18n/dictionaries.ts @@ -0,0 +1,519 @@ +import type { Dictionary, Locale, LocalizedProject } from "./types"; + +export const dictionaries: Record = { + ru: { + locale: "ru", + nav: { + about: "Обо мне", + projects: "Проекты", + contact: "Контакты", + }, + hero: { + role: "веб-разработчик", + name: "Egorov Nikita", + desc: "Создаю современные, быстрые и продуманные веб-сайты и веб-приложения под ключ.", + ctaButton: "Смотреть проекты", + portfolioWord: "портфолио", + scrollHint: "листай", + }, + about: { + eyebrow: "обо мне", + titleStart: "Разработчик, для которого важны", + titleAccent: "продуманность", + titleEnd: "и детали", + desc: "Привет! Я Никита — веб-разработчик. Строю современные, быстрые и аккуратные интерфейсы на React и Next.js. Уделяю внимание производительности, семантике и мелочам, из которых складывается хорошее впечатление от сайта.", + stats: { + expLabel: "опыт", + expValue: "3+", + projectsLabel: "проекты", + projectsValue: "10+", + stackLabel: "стек", + }, + skillsTitle: "Стек и навыки", + }, + process: { + eyebrow: "процесс", + title: "От идеи до запуска без хаоса и лишней суеты", + desc: "Сайт должен не просто красиво открываться, а вести человека к действию. Поэтому я двигаюсь по понятным этапам: сначала смысл, потом интерфейс, затем аккуратная разработка и проверка деталей.", + steps: [ + { + number: "01", + title: "Бриф", + text: "Разбираю задачу, аудиторию, цели сайта и фиксирую, какой результат должен получиться.", + }, + { + number: "02", + title: "Структура", + text: "Собираю логику страниц, сценарии пользователя и расставляю смысловые акценты.", + }, + { + number: "03", + title: "Разработка", + text: "Верстаю адаптивный интерфейс, добавляю интерактив, формы, аналитику и базовое SEO.", + }, + { + number: "04", + title: "Запуск", + text: "Проверяю скорость, мобильную версию, мелкие состояния и помогаю спокойно выкатить сайт.", + }, + ], + }, + sponsors: { + label: "С ними я работал", + }, + testimonials: { + eyebrow: "отзывы", + titleStart: "Что обо мне", + titleAccent: "говорят", + items: [ + { + quote: "Никита сделал лендинг, который реально приносит лиды. Быстро, аккуратно, без лишней воды в коммуникации.", + rating: 5, + author: "Алексей Орлов", + role: "Основатель финтех-стартапа", + avatar: "АО", + }, + { + quote: "Код понятный, страницы летают, а правки после сдачи вносить легко. Работать с ним — одно удовольствие.", + rating: 5, + author: "Мария Соколова", + role: "Продакт-менеджер", + avatar: "МС", + }, + { + quote: "За 2 недели собрал нам интернет-магазин под ключ: каталог, оплата, админка. Всё чётко и в срок.", + rating: 5, + author: "Дмитрий Волков", + role: "Владелец бренда одежды", + avatar: "ДВ", + }, + ], + }, + projectsSection: { + eyebrow: "работы", + titleStart: "Избранные", + titleAccent: "проекты", + viewAll: "все проекты", + cardDetails: "подробнее", + }, + projectsPage: { + eyebrow: "проекты", + titleStart: "Все", + titleAccent: "работы", + countSuffix: "проектов под ключ", + }, + projectModal: { + aboutTitle: "О проекте", + featuresTitle: "Особенности и результат", + stackTitle: "Стек технологий", + viewSite: "Смотреть сайт", + viewCode: "Исходный код", + closeHint: "Esc для закрытия", + }, + footer: { + eyebrow: "контакты", + titleStart: "Есть проект?", + titleAccent: "Напиши мне", + desc: "Отвечаю обычно в течение дня. Расскажи, что хочешь сделать, — я помогу с идеей, оценкой и реализацией.", + telegramButton: "Написать в Telegram", + githubButton: "GitHub", + copyright: "© 2025-2026 Egorov Nikita", + }, + contextMenu: { + name: "Никита Егоров", + role: "Frontend & Web Developer", + statusTitle: "Открыт к предложениям", + primaryPdf: "/Nikita_Egorov_Resume_RU.pdf", + primaryFilename: "Nikita_Egorov_Resume_RU.pdf", + primaryLabel: "Скачать резюме (RU)", + primaryBadge: "RU", + secondaryPdf: "/Nikita_Egorov_Resume_EN.pdf", + secondaryFilename: "Nikita_Egorov_Resume_EN.pdf", + secondaryLabel: "Скачать резюме (EN)", + secondaryBadge: "EN", + previewLabel: "Открыть в новой вкладке", + copyLabel: "Скопировать ссылку", + copiedLabel: "Ссылка скопирована!", + telegramLabel: "Написать в Telegram", + footerMenu: "Меню сайта", + esc: "Esc", + }, + cookieConsent: { + title: "Мы используем файлы cookie", + text: "Этот сайт использует файлы cookie и сервис веб-аналитики для сбора обезличенных данных и улучшения работы.", + acceptButton: "Понятно", + }, + preloader: { + loadingText: "загрузка", + }, + seo: { + title: "Никита Егоров — Frontend & Web Developer | Портфолио", + description: "Портфолио веб-разработчика Никиты Егорова. Создание современных, быстрых и адаптивных веб-сайтов, интернет-магазинов и веб-сервисов на React, Next.js, TypeScript и Django.", + keywords: [ + "Никита Егоров", + "Nikita Egorov", + "autli", + "веб-разработчик", + "фронтенд разработчик", + "Frontend Developer", + "React", + "Next.js", + "TypeScript", + "Tailwind CSS", + "Django", + ], + ogDescription: "Разработка современных, быстрых и продуманных веб-сервисов, интернет-магазинов и дашбордов на React, Next.js и TypeScript.", + projectsPageTitle: "Все проекты", + projectsPageDesc: "Коммерческие и пет-проекты Egorov Nikita: веб-приложения, интернет-магазины, SaaS-дашборды и сайты под ключ на React, Next.js, TypeScript и Django.", + }, + }, + en: { + locale: "en", + nav: { + about: "About", + projects: "Projects", + contact: "Contact", + }, + hero: { + role: "web developer", + name: "Egorov Nikita", + desc: "Crafting modern, high-performance, and thoughtful websites and web applications end-to-end.", + ctaButton: "View projects", + portfolioWord: "portfolio", + scrollHint: "scroll", + }, + about: { + eyebrow: "about me", + titleStart: "A developer who cares about", + titleAccent: "precision", + titleEnd: "and detail", + desc: "Hi! I'm Nikita — a web developer. I build modern, fast, and clean interfaces with React and Next.js. I prioritize performance, semantic code, and the small details that make a great digital experience.", + stats: { + expLabel: "experience", + expValue: "3+", + projectsLabel: "projects", + projectsValue: "10+", + stackLabel: "stack", + }, + skillsTitle: "Stack & Skills", + }, + process: { + eyebrow: "process", + title: "From idea to production without chaos or friction", + desc: "A website shouldn't just look great — it should drive action. That's why I follow a structured approach: strategy first, then design, clean engineering, and rigorous testing.", + steps: [ + { + number: "01", + title: "Brief", + text: "Analyzing goals, target audience, technical requirements, and defining clear milestones.", + }, + { + number: "02", + title: "Structure", + text: "Mapping user journeys, page hierarchies, data flows, and design semantics.", + }, + { + number: "03", + title: "Development", + text: "Writing clean, responsive code, animations, form validations, analytics, and technical SEO.", + }, + { + number: "04", + title: "Launch", + text: "Verifying Core Web Vitals, cross-browser compatibility, and ensuring a seamless deployment.", + }, + ], + }, + sponsors: { + label: "Companies I've worked with", + }, + testimonials: { + eyebrow: "testimonials", + titleStart: "What people", + titleAccent: "say about me", + items: [ + { + quote: "Nikita delivered a high-converting landing page that really brings leads. Fast, meticulous, with zero fluff in communication.", + rating: 5, + author: "Alexey Orlov", + role: "Fintech Startup Founder", + avatar: "AO", + }, + { + quote: "Clean code, blazing fast pages, and extremely easy to maintain. Working with him was an absolute pleasure.", + rating: 5, + author: "Maria Sokolova", + role: "Product Manager", + avatar: "MS", + }, + { + quote: "Delivered a turnkey e-commerce store in just 2 weeks: catalog, checkout, custom admin panel. On time and top quality.", + rating: 5, + author: "Dmitry Volkov", + role: "Apparel Brand Owner", + avatar: "DV", + }, + ], + }, + projectsSection: { + eyebrow: "works", + titleStart: "Featured", + titleAccent: "projects", + viewAll: "all projects", + cardDetails: "details", + }, + projectsPage: { + eyebrow: "projects", + titleStart: "All", + titleAccent: "works", + countSuffix: "completed turnkey projects", + }, + projectModal: { + aboutTitle: "About project", + featuresTitle: "Features & Impact", + stackTitle: "Tech Stack", + viewSite: "Live Website", + viewCode: "Source Code", + closeHint: "Esc to close", + }, + footer: { + eyebrow: "contact", + titleStart: "Have a project?", + titleAccent: "Let's talk", + desc: "I usually reply within a day. Tell me what you'd like to build — I'll help with planning, estimation, and execution.", + telegramButton: "Message on Telegram", + githubButton: "GitHub", + copyright: "© 2025-2026 Egorov Nikita", + }, + contextMenu: { + name: "Nikita Egorov", + role: "Frontend & Web Developer", + statusTitle: "Available for hire", + primaryPdf: "/Nikita_Egorov_Resume_EN.pdf", + primaryFilename: "Nikita_Egorov_Resume_EN.pdf", + primaryLabel: "Download Resume (EN)", + primaryBadge: "EN", + secondaryPdf: "/Nikita_Egorov_Resume_RU.pdf", + secondaryFilename: "Nikita_Egorov_Resume_RU.pdf", + secondaryLabel: "Download Resume (RU)", + secondaryBadge: "RU", + previewLabel: "Open in new tab", + copyLabel: "Copy website link", + copiedLabel: "Link copied!", + telegramLabel: "Message on Telegram", + footerMenu: "Site menu", + esc: "Esc", + }, + cookieConsent: { + title: "We use cookies", + text: "This site uses cookies and analytics services to collect anonymized data and optimize your user experience.", + acceptButton: "Got it", + }, + preloader: { + loadingText: "loading", + }, + seo: { + title: "Nikita Egorov — Frontend & Web Developer | Portfolio", + description: "Portfolio of web developer Nikita Egorov. Building modern, fast, and responsive websites, e-commerce stores, and web apps with React, Next.js, TypeScript, and Django.", + keywords: [ + "Nikita Egorov", + "autli", + "Frontend Developer", + "Web Developer", + "React", + "Next.js", + "TypeScript", + "Tailwind CSS", + "Django", + "Web Development", + "Portfolio", + ], + ogDescription: "Developing modern, fast, and thoughtful web applications, e-commerce platforms, and dashboards with React, Next.js, and TypeScript.", + projectsPageTitle: "All Projects", + projectsPageDesc: "Commercial and pet projects by Nikita Egorov: web apps, online stores, SaaS dashboards, and turnkey websites built with React, Next.js, TypeScript, and Django.", + }, + }, +}; + +export const localizedProjects: Record = { + ru: [ + { + id: "01", + title: "Центр инструментов Мастер", + description: + "Корпоративный лендинг для магазина инструментов: каталог, формы обратной связи и высокая производительность.", + year: "2025", + tags: ["HTML", "CSS", "JavaScript", "Django"], + details: + "Одностраничный сайт для компании по продаже инструментов: продуманная структура, анимации появления, формы заявок и интеграция с аналитикой. Упор на скорость загрузки (Lighthouse 95+) и адаптивность.", + features: [ + "Скорость загрузки < 1 секунды", + "Анимированные блоки и валидация форм", + "Интеграция с Яндекс.Метрикой", + "Полная адаптация под мобильные устройства", + ], + code: "https://github.com/autli5", + link: "https://orentools.ru", + }, + { + id: "02", + title: "Запретный плод", + description: + "Маркетплейс с ролевой моделью: каталог, корзина, онлайн-оплата и личные кабинеты.", + year: "2025", + tags: ["Python", "Django", "HTML", "CSS", "PostgreSQL"], + details: + "Платформа для продажи и покупки товаров с разделением ролей покупателя и продавца. Включает каталог с фильтрами, систему корзины, оформление заказов и панель управления для продавцов.", + features: [ + "Каталог с динамическими фильтрами и поиском", + "Система корзины и имитация онлайн-оплаты", + "Личный кабинет с историей заказов", + "Админ-панель для управления товарами", + ], + code: "https://github.com/autli5", + link: "https://z-plod.ru", + }, + { + id: "03", + title: "Лазертаг Беркут", + description: + "SaaS-дашборд с аналитикой в реальном времени: графики, отчёты и экспорт данных.", + year: "2026", + tags: ["Next.js", "TypeScript", "Tailwind CSS"], + details: + "Интерактивная панель управления для бизнеса: витрина метрик в реальном времени, построение пользовательских отчётов и экспорт данных. Поддержка тёмной темы и кастомизация виджетов.", + features: [ + "Визуализация данных с помощью интерактивных графиков", + "Конструктор отчётов с экспортом в CSV", + "Настраиваемые виджеты и тёмная тема", + "Ролевая модель доступа к данным", + ], + code: "https://github.com/autli5", + link: "https://lasergame56.ru", + }, + { + id: "04", + title: "Сайт портфолио", + description: + "Персональный сайт-портфолио на современном стеке с фокусом на производительность и SEO.", + year: "2026", + tags: ["Next.js", "TypeScript", "Tailwind CSS"], + details: + "Оптимизированный сайт для демонстрации проектов: серверный рендеринг (SSR) для отличного SEO, плавные анимации переходов и чистая, минималистичная архитектура компонентов.", + features: [ + "Отличная SEO-семантика и Open Graph разметка", + "Высокая производительность (Core Web Vitals)", + "Адаптивный и доступный (a11y) интерфейс", + "Типизированный код на TypeScript", + ], + code: "https://github.com/autli5", + }, + { + id: "05", + title: "freebie (E-commerce Platform)", + description: + "Современный интернет-магазин с динамической корзиной, каталогом товаров и REST API.", + year: "2026", + tags: ["React", "Vite", "TypeScript", "Django", "Django REST Framework"], + details: + "Полноценное SPA-приложение для электронной коммерции. Фронтенд на React (Vite) взаимодействует с бэкендом на Django. Реализована глобальная корзина в реальном времени, система отзывов, промокодов и адаптивная верстка.", + features: [ + "Глобальное состояние корзины с мгновенным обновлением (React Context)", + "REST API на Django для управления товарами и сессиями", + "Адаптивный UI/UX дизайн, вдохновленный топовыми маркетплейсами", + "Динамический расчет стоимости с учетом промокодов и доставки", + ], + code: "https://github.com/autli5/freebie", + }, + ], + en: [ + { + id: "01", + title: "Tool Center Master", + description: + "Corporate landing page for a power tools supplier: product catalog, lead forms, and top performance.", + year: "2025", + tags: ["HTML", "CSS", "JavaScript", "Django"], + details: + "High-performance website for an industrial tools retailer: intuitive layout, smooth entrance animations, lead generation forms, and web analytics integration. Focused on instant load times (Lighthouse 95+) and responsive design.", + features: [ + "Page load time under 1 second", + "Interactive animated blocks & form validation", + "Yandex Metrika analytics integration", + "Fully responsive layout across all mobile devices", + ], + code: "https://github.com/autli5", + link: "https://orentools.ru", + }, + { + id: "02", + title: "Zapretny Plod (Marketplace)", + description: + "E-commerce marketplace with role-based access: product catalog, shopping cart, online checkout, and seller dashboards.", + year: "2025", + tags: ["Python", "Django", "HTML", "CSS", "PostgreSQL"], + details: + "Full-fledged buying and selling platform with separated buyer and seller roles. Features dynamic filterable catalog, persistent cart system, checkout flow, and custom Django admin panel for merchants.", + features: [ + "Dynamic search and multi-parameter filtering", + "Cart system with simulated online payments", + "User dashboard with full order history", + "Custom management panel for inventory control", + ], + code: "https://github.com/autli5", + link: "https://z-plod.ru", + }, + { + id: "03", + title: "Lasertag & Airsoft Club Berkut", + description: + "SaaS dashboard with real-time analytics: data visualization charts, custom reports, and CSV exports.", + year: "2026", + tags: ["Next.js", "TypeScript", "Tailwind CSS"], + details: + "Interactive business management platform: live business metrics showcase, custom report builder, and data export tools. Features dark mode UI and modular widget customization.", + features: [ + "Real-time interactive chart visualizations", + "Custom report generator with CSV exports", + "Customizable dashboard widgets & dark theme", + "Role-based access control for team members", + ], + code: "https://github.com/autli5", + link: "https://lasergame56.ru", + }, + { + id: "04", + title: "Developer Portfolio", + description: + "Personal developer portfolio built on a modern stack with strong focus on performance and SEO.", + year: "2026", + tags: ["Next.js", "TypeScript", "Tailwind CSS"], + details: + "High-performance website designed to showcase projects: server-side rendering (SSR) for optimal SEO discovery, smooth entrance animations, and clean modular component architecture.", + features: [ + "Semantic SEO architecture and Open Graph metadata", + "Peak Core Web Vitals and Lighthouse scores", + "Fully accessible (a11y) and mobile-first interface", + "Strictly typed codebase in TypeScript", + ], + code: "https://github.com/autli5", + }, + { + id: "05", + title: "freebie (E-commerce Platform)", + description: + "Modern SPA e-commerce platform with dynamic shopping cart, product catalog, and REST API integration.", + year: "2026", + tags: ["React", "Vite", "TypeScript", "Django", "Django REST Framework"], + details: + "Comprehensive single-page application for e-commerce. React + Vite frontend communicating seamlessly with a Django REST API. Features real-time cart state management, product review system, promo codes, and responsive design.", + features: [ + "Global real-time cart state powered by React Context", + "Django REST API for products and session management", + "Modern UI/UX inspired by leading marketplace platforms", + "Dynamic price calculation with promo codes and delivery", + ], + code: "https://github.com/autli5/freebie", + }, + ], +}; diff --git a/src/shared/config/i18n/index.ts b/src/shared/config/i18n/index.ts new file mode 100644 index 0000000..fccc7d9 --- /dev/null +++ b/src/shared/config/i18n/index.ts @@ -0,0 +1,3 @@ +export * from "./types"; +export * from "./dictionaries"; +export * from "./context"; diff --git a/src/shared/config/i18n/types.ts b/src/shared/config/i18n/types.ts new file mode 100644 index 0000000..e7e682c --- /dev/null +++ b/src/shared/config/i18n/types.ts @@ -0,0 +1,149 @@ +export type Locale = "ru" | "en"; + +export interface NavItem { + label: string; + href: string; +} + +export interface SkillItem { + name: string; + level: number; +} + +export interface ProcessStep { + number: string; + title: string; + text: string; +} + +export interface TestimonialItem { + quote: string; + rating: number; + author: string; + role: string; + avatar: string; +} + +export interface LocalizedProject { + id: string; + title: string; + description: string; + year: string; + tags: string[]; + details: string; + features: string[]; + code?: string; + link?: string; +} + +export interface Dictionary { + locale: Locale; + nav: { + about: string; + projects: string; + contact: string; + }; + hero: { + role: string; + name: string; + desc: string; + ctaButton: string; + portfolioWord: string; + scrollHint: string; + }; + about: { + eyebrow: string; + titleStart: string; + titleAccent: string; + titleEnd: string; + desc: string; + stats: { + expLabel: string; + expValue: string; + projectsLabel: string; + projectsValue: string; + stackLabel: string; + }; + skillsTitle: string; + }; + process: { + eyebrow: string; + title: string; + desc: string; + steps: ProcessStep[]; + }; + sponsors: { + label: string; + }; + testimonials: { + eyebrow: string; + titleStart: string; + titleAccent: string; + items: TestimonialItem[]; + }; + projectsSection: { + eyebrow: string; + titleStart: string; + titleAccent: string; + viewAll: string; + cardDetails: string; + }; + projectsPage: { + eyebrow: string; + titleStart: string; + titleAccent: string; + countSuffix: string; + }; + projectModal: { + aboutTitle: string; + featuresTitle: string; + stackTitle: string; + viewSite: string; + viewCode: string; + closeHint: string; + }; + footer: { + eyebrow: string; + titleStart: string; + titleAccent: string; + desc: string; + telegramButton: string; + githubButton: string; + copyright: string; + }; + contextMenu: { + name: string; + role: string; + statusTitle: string; + primaryPdf: string; + primaryFilename: string; + primaryLabel: string; + primaryBadge: string; + secondaryPdf: string; + secondaryFilename: string; + secondaryLabel: string; + secondaryBadge: string; + previewLabel: string; + copyLabel: string; + copiedLabel: string; + telegramLabel: string; + footerMenu: string; + esc: string; + }; + cookieConsent: { + title: string; + text: string; + acceptButton: string; + }; + preloader: { + loadingText: string; + }; + seo: { + title: string; + description: string; + keywords: string[]; + ogDescription: string; + projectsPageTitle: string; + projectsPageDesc: string; + }; +} diff --git a/src/shared/config/projects/projects.ts b/src/shared/config/projects/projects.ts index d2ce2fa..d495f01 100644 --- a/src/shared/config/projects/projects.ts +++ b/src/shared/config/projects/projects.ts @@ -1,92 +1,9 @@ import type { Project } from "./types"; +import { localizedProjects } from "@/shared/config/i18n/dictionaries"; +import type { Locale } from "@/shared/config/i18n/types"; -export const projects: Project[] = [ - { - id: "01", - title: "Центр инструментов Мастер", - description: - "Корпоративный лендинг для магазина инструментов: каталог, формы обратной связи и высокая производительность.", // <-- ИСПРАВЛЕНО под название - year: "2025", - tags: ["HTML", "CSS", "JavaScript", "Django"], - details: - "Одностраничный сайт для компании по продаже инструментов: продуманная структура, анимации появления, формы заявок и интеграция с аналитикой. Упор на скорость загрузки (Lighthouse 95+) и адаптивность.", - features: [ - "Скорость загрузки < 1 секунды", - "Анимированные блоки и валидация форм", - "Интеграция с Яндекс.Метрикой", - "Полная адаптация под мобильные устройства", - ], - code: "https://github.com/autli5", - link: "https://orentools.ru", - }, - { - id: "02", - title: "Запретный плод", - description: - "Маркетплейс с ролевой моделью: каталог, корзина, онлайн-оплата и личные кабинеты.", - year: "2025", - tags: ["Python", "Django", "HTML", "CSS", "PostgreSQL"], // <-- Добавил Python и БД для солидности - details: - "Платформа для продажи и покупки товаров с разделением ролей покупателя и продавца. Включает каталог с фильтрами, систему корзины, оформление заказов и панель управления для продавцов.", - features: [ - "Каталог с динамическими фильтрами и поиском", - "Система корзины и имитация онлайн-оплаты", - "Личный кабинет с историей заказов", - "Админ-панель для управления товарами", - ], - code: "https://github.com/autli5", - link: "https://z-plod.ru" - }, - { - id: "03", - title: "Лазертаг Беркут", - description: - "SaaS-дашборд с аналитикой в реальном времени: графики, отчёты и экспорт данных.", - year: "2026", - tags: ["Next.js", "TypeScript", "Tailwind CSS"], // <-- Исправлены кавычки и добавлен TS - details: - "Интерактивная панель управления для бизнеса: витрина метрик в реальном времени, построение пользовательских отчётов и экспорт данных. Поддержка тёмной темы и кастомизация виджетов.", - features: [ - "Визуализация данных с помощью интерактивных графиков", - "Конструктор отчётов с экспортом в CSV", - "Настраиваемые виджеты и тёмная тема", - "Ролевая модель доступа к данным", - ], - code: "https://github.com/autli5", - link: "https://lasergame56.ru" - }, - { - id: "04", - title: "Сайт портфолио", - description: - "Персональный сайт-портфолио на современном стеке с фокусом на производительность и SEO.", - year: "2026", - tags: ["Next.js", "TypeScript", "Tailwind CSS"], // <-- Исправлены кавычки - details: - "Оптимизированный сайт для демонстрации проектов: серверный рендеринг (SSR) для отличного SEO, плавные анимации переходов и чистая, минималистичная архитектура компонентов.", - features: [ - "Отличная SEO-семантика и Open Graph разметка", - "Высокая производительность (Core Web Vitals)", - "Адаптивный и доступный (a11y) интерфейс", - "Типизированный код на TypeScript", - ], - code: "https://github.com/autli5", - }, - { - id: "05", - title: "freebie (E-commerce Platform)", - description: - "Современный интернет-магазин с динамической корзиной, каталогом товаров и REST API.", - year: "2026", - tags: ["React", "Vite", "TypeScript", "Django", "Django REST Framework"], - details: - "Полноценное SPA-приложение для электронной коммерции. Фронтенд на React (Vite) взаимодействует с бэкендом на Django. Реализована глобальная корзина в реальном времени, система отзывов, промокодов и адаптивная верстка.", - features: [ - "Глобальное состояние корзины с мгновенным обновлением (React Context)", - "REST API на Django для управления товарами и сессиями", - "Адаптивный UI/UX дизайн, вдохновленный топовыми маркетплейсами", - "Динамический расчет стоимости с учетом промокодов и доставки", - ], - code: "https://github.com/autli5/freebie", - }, -]; \ No newline at end of file +export const getProjects = (locale: Locale = "ru"): Project[] => { + return localizedProjects[locale] || localizedProjects.ru; +}; + +export const projects: Project[] = localizedProjects.ru; \ No newline at end of file diff --git a/src/static/header/header.module.css b/src/static/header/header.module.css index 4aaed57..150df10 100644 --- a/src/static/header/header.module.css +++ b/src/static/header/header.module.css @@ -60,6 +60,52 @@ gap: var(--space-md); } +/* ========================= + LANG TOGGLE +========================= */ + +.langToggle { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + height: 32px; + padding: 0 12px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 9999px; + color: var(--color-text); + font-size: 11.5px; + font-weight: 600; + letter-spacing: 0.04em; + text-decoration: none; + cursor: pointer; + transition: all 0.2s ease; + user-select: none; +} + +.langToggle:hover { + background: rgba(37, 99, 235, 0.15); + border-color: rgba(59, 130, 246, 0.4); + color: #93c5fd; + transform: translateY(-1px); +} + +.langToggleDot { + width: 5px; + height: 5px; + border-radius: 50%; + background: #3b82f6; +} + +.mobileLangRow { + display: flex; + align-items: center; + margin-top: var(--space-lg); + padding-top: var(--space-md); + border-top: 1px solid var(--color-line); +} + /* ========================= DESKTOP NAV ========================= */ diff --git a/src/views/projects/ui/ProjectsPage.tsx b/src/views/projects/ui/ProjectsPage.tsx index 554858e..f528f86 100644 --- a/src/views/projects/ui/ProjectsPage.tsx +++ b/src/views/projects/ui/ProjectsPage.tsx @@ -1,20 +1,23 @@ "use client"; import { useState } from "react"; - import { Header } from "@/widgets/header"; import { Footer } from "@/widgets/footer"; import { useInView } from "@/shared/hooks/useInView"; -import { projects } from "@/shared/config/projects/projects"; +import { getProjects } from "@/shared/config/projects/projects"; import type { Project } from "@/shared/config/projects/types"; import { ProjectModal } from "@/widgets/project-modal"; +import { useI18n } from "@/shared/config/i18n"; import ProjectCard from "@/components/projects/ProjectCard"; import styles from "@/static/projects/projects-page.module.css"; -const ProjectsPage = () => { +export const ProjectsPage = () => { const [selected, setSelected] = useState(null); const [headRef, headInView] = useInView(0.4); const [gridRef, gridInView] = useInView(0.08); + const { locale, dict } = useI18n(); + + const currentProjects = getProjects(locale); const reveal = (visible: boolean) => `transition-all duration-700 ease-out will-change-transform ${ @@ -29,27 +32,31 @@ const ProjectsPage = () => {