add translate i18n

This commit is contained in:
Nikita 2026-09-26 20:32:10 +05:00
parent f5c8acca6e
commit 08e94d7e91
27 changed files with 1173 additions and 411 deletions

View file

@ -1,13 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://autli.ru/</loc>
<loc>https://autli.ru/ru/</loc>
<xhtml:link rel="alternate" hreflang="ru" href="https://autli.ru/ru/" />
<xhtml:link rel="alternate" hreflang="en" href="https://autli.ru/en/" />
<lastmod>2026-09-26</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://autli.ru/projects/</loc>
<loc>https://autli.ru/en/</loc>
<xhtml:link rel="alternate" hreflang="ru" href="https://autli.ru/ru/" />
<xhtml:link rel="alternate" hreflang="en" href="https://autli.ru/en/" />
<lastmod>2026-09-26</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://autli.ru/ru/projects/</loc>
<xhtml:link rel="alternate" hreflang="ru" href="https://autli.ru/ru/projects/" />
<xhtml:link rel="alternate" hreflang="en" href="https://autli.ru/en/projects/" />
<lastmod>2026-09-26</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://autli.ru/en/projects/</loc>
<xhtml:link rel="alternate" hreflang="ru" href="https://autli.ru/ru/projects/" />
<xhtml:link rel="alternate" hreflang="en" href="https://autli.ru/en/projects/" />
<lastmod>2026-09-26</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>

86
src/app/[lang]/layout.tsx Normal file
View file

@ -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<Metadata> {
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 (
<I18nProvider locale={locale}>
<Preloader />
<Cursor />
<ScrollCounter />
<ContextMenu />
{children}
<CookieConsent />
<YandexMetrika />
</I18nProvider>
);
}

10
src/app/[lang]/page.tsx Normal file
View file

@ -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 <HomePage />;
}

View file

@ -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<Metadata> {
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 <ProjectsPage />;
}

View file

@ -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 (
<html
lang="ru"
@ -151,16 +146,8 @@ const RootLayout = ({ children }: { children: ReactNode }) => {
/>
</head>
<body className="min-h-full flex flex-col">
<Preloader />
<Cursor />
<ScrollCounter />
<ContextMenu />
{children}
<CookieConsent />
<YandexMetrika />
</body>
</html>
);
};
export default RootLayout;
}

View file

@ -1,8 +1,23 @@
import { HomePage } from "@/views/home";
import "@/static/globals.css";
"use client";
const Page = () => {
return <HomePage />;
};
import { useEffect } from "react";
import { useRouter } from "next/navigation";
export default Page;
export default function RootPage() {
const router = useRouter();
useEffect(() => {
router.replace("/ru/");
}, [router]);
return (
<>
<head>
<meta httpEquiv="refresh" content="0;url=/ru/" />
</head>
<div style={{ padding: "20px", textAlign: "center", color: "#666" }}>
Redirecting to /ru/...
</div>
</>
);
}

View file

@ -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 <ProjectsPage />;
};
export default function ProjectsRedirect() {
const router = useRouter();
export default Projects;
useEffect(() => {
router.replace("/ru/projects/");
}, [router]);
return (
<>
<head>
<meta httpEquiv="refresh" content="0;url=/ru/projects/" />
</head>
<div style={{ padding: "20px", textAlign: "center", color: "#666" }}>
Redirecting to /ru/projects/...
</div>
</>
);
}

View file

@ -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<HTMLDivElement>(0.2);
const [skillsRef, skillsInView] = useInView<HTMLDivElement>(0.3);
const { dict } = useI18n();
const reveal = (visible: boolean) =>
`transition-all duration-700 ease-out will-change-transform ${
@ -33,42 +35,40 @@ const About = () => {
<div ref={introRef} className="page-container relative z-10">
<p className={`${styles.eyebrow} ${reveal(introInView)}`}>
<span className={styles.rule} aria-hidden="true" />
обо мне
{dict.about.eyebrow}
</p>
<h2
className={`${styles.title} ${reveal(introInView)}`}
style={{ transitionDelay: "100ms" }}
>
Разработчик, для которого важны{" "}
<span className={styles.titleAccent}>продуманность</span> и детали
{dict.about.titleStart}{" "}
<span className={styles.titleAccent}>{dict.about.titleAccent}</span>{" "}
{dict.about.titleEnd}
</h2>
<p
className={`${styles.text} ${reveal(introInView)}`}
style={{ transitionDelay: "200ms" }}
>
Привет! Я Никита — веб-разработчик. Строю современные, быстрые и
аккуратные интерфейсы на React и Next.js. Уделяю внимание
производительности, семантике и мелочам, из которых складывается
хорошее впечатление от сайта.
{dict.about.desc}
</p>
<dl className={`${styles.stats} ${reveal(introInView)}`} style={{ transitionDelay: "300ms" }}>
<div>
<dt className={styles.statLabel}>опыт</dt>
<dt className={styles.statLabel}>{dict.about.stats.expLabel}</dt>
<dd className={styles.statValue}>
3<span className={styles.statValueAccent}>+</span>
</dd>
</div>
<div>
<dt className={styles.statLabel}>проекты</dt>
<dt className={styles.statLabel}>{dict.about.stats.projectsLabel}</dt>
<dd className={styles.statValue}>
10<span className={styles.statValueAccent}>+</span>
</dd>
</div>
<div>
<dt className={styles.statLabel}>стек</dt>
<dt className={styles.statLabel}>{dict.about.stats.stackLabel}</dt>
<dd className={styles.statValue}>{techStack.length}</dd>
</div>
</dl>
@ -78,7 +78,7 @@ const About = () => {
className={`${styles.skills} ${reveal(skillsInView)}`}
style={{ transitionDelay: "150ms" }}
>
<h3 className={styles.skillsTitle}>Стек и навыки</h3>
<h3 className={styles.skillsTitle}>{dict.about.skillsTitle}</h3>
<ul className={styles.skillList}>
{techStack.map((item, index) => (

View file

@ -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 (
<section className={styles.hero} data-scroll-section>
<div className={`cont-header ${styles.inner}`}>
@ -42,21 +46,18 @@ const Hero = () => {
<div className={styles.info}>
<span className={styles.role}>
<span className={styles.dot} />
веб-разработчик
{dict.hero.role}
</span>
<h1 className={styles.name} data-cursor-hover>
<FlashlightText text="Egorov Nikita" />
<FlashlightText text={dict.hero.name} />
</h1>
<p className={styles.desc}>
Создаю современные, быстрые и продуманные веб-сайты и
веб-приложения под ключ.
</p>
<p className={styles.desc}>{dict.hero.desc}</p>
<div className={styles.actions}>
<a href="#projects" className={styles.button}>
Смотреть проекты
<a href={`/${locale}/#projects`} className={styles.button}>
{dict.hero.ctaButton}
</a>
<ul className={styles.socials}>
@ -81,14 +82,14 @@ const Hero = () => {
<div className={styles.right}>
<div className={styles.visual}>
<span className={styles.index} aria-hidden="true">
<i>портфолио</i>
<i>{dict.hero.portfolioWord}</i>
</span>
</div>
</div>
</div>
<div className={styles.hint} aria-hidden="true">
листай
{dict.hero.scrollHint}
<span className={styles.hintLine} />
</div>
</section>

View file

@ -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<HTMLDivElement>(0.24);
const [stepsRef, stepsInView] = useInView<HTMLDivElement>(0.18);
const { dict } = useI18n();
const reveal = (visible: boolean) =>
`transition-all duration-700 ease-out will-change-transform ${
@ -47,7 +26,7 @@ const Process = () => {
<div ref={headRef} className={styles.head}>
<p className={`${styles.eyebrow} ${reveal(headInView)}`}>
<span className={styles.rule} aria-hidden="true" />
процесс
{dict.process.eyebrow}
</p>
<div className={styles.headGrid}>
@ -55,16 +34,14 @@ const Process = () => {
className={`${styles.title} ${reveal(headInView)}`}
style={{ transitionDelay: "100ms" }}
>
От идеи до запуска без хаоса и лишней суеты
{dict.process.title}
</h2>
<p
className={`${styles.text} ${reveal(headInView)}`}
style={{ transitionDelay: "200ms" }}
>
Сайт должен не просто красиво открываться, а вести человека к
действию. Поэтому я двигаюсь по понятным этапам: сначала смысл,
потом интерфейс, затем аккуратная разработка и проверка деталей.
{dict.process.desc}
</p>
</div>
</div>
@ -73,13 +50,13 @@ const Process = () => {
ref={stepsRef}
className={`${styles.timeline} ${stepsInView ? styles.active : ""}`}
>
{steps.map((step, index) => (
{dict.process.steps.map((step, index) => (
<article
key={step.label}
key={step.number}
className={`${styles.step} ${reveal(stepsInView)}`}
style={{ transitionDelay: `${index * 90}ms` }}
>
<span className={styles.stepNumber}>{step.label}</span>
<span className={styles.stepNumber}>{step.number}</span>
<h3 className={styles.stepTitle}>{step.title}</h3>
<p className={styles.stepText}>{step.text}</p>
</article>

View file

@ -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<HTMLElement>) => {
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) => {
</ul>
<span className={styles.cardLink}>
подробнее
{dict.projectsSection.cardDetails}
<span className={styles.cardLinkArrow} aria-hidden="true">
↗
</span>

View file

@ -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<HTMLDivElement>(0.12);
const [selected, setSelected] = useState<Project | null>(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 = () => {
<div>
<p className={styles.eyebrow}>
<span className={styles.rule} aria-hidden="true" />
работы
{dict.projectsSection.eyebrow}
</p>
<h2 className={styles.title}>
Избранные <span className={styles.titleAccent}>проекты</span>
{dict.projectsSection.titleStart}{" "}
<span className={styles.titleAccent}>
{dict.projectsSection.titleAccent}
</span>
</h2>
</div>
<Link href="/projects" className={styles.viewAll}>
все проекты
<Link href={`/${locale}/projects/`} className={styles.viewAll}>
{dict.projectsSection.viewAll}
<span className={styles.viewAllArrow} aria-hidden="true">
↗
</span>

View file

@ -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 }) => (
</ul>
);
const Sponsors = () => {
export const Sponsors = () => {
const { dict } = useI18n();
return (
<section className={styles.section} aria-label="Партнёры и бренды">
<section className={styles.section} aria-label={dict.sponsors.label}>
<div className="page-container">
<p className={styles.label}>С ними я работал</p>
<p className={styles.label}>{dict.sponsors.label}</p>
</div>
<div className={styles.ticker}>

View file

@ -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<HTMLDivElement>(0.15);
const { dict } = useI18n();
const reveal = (visible: boolean) =>
`transition-all duration-700 ease-out will-change-transform ${
@ -49,17 +24,18 @@ const Testimonials = () => {
<div className="page-container relative z-10">
<p className={styles.eyebrow}>
<span className={styles.rule} aria-hidden="true" />
отзывы
{dict.testimonials.eyebrow}
</p>
<h2 className={styles.title}>
Что обо мне <span className={styles.titleAccent}>говорят</span>
{dict.testimonials.titleStart}{" "}
<span className={styles.titleAccent}>{dict.testimonials.titleAccent}</span>
</h2>
<div ref={gridRef} className={styles.grid}>
{testimonials.map((item, index) => (
{dict.testimonials.items.map((item, index) => (
<article
key={item.name}
key={item.author}
className={`${styles.card} ${reveal(gridInView)}`}
style={{ transitionDelay: `${index * 90}ms` }}
>
@ -75,10 +51,10 @@ const Testimonials = () => {
<footer className={styles.author}>
<span className={styles.avatar} aria-hidden="true">
{initials(item.name)}
{item.avatar}
</span>
<span>
<span className={styles.authorName}>{item.name}</span>
<span className={styles.authorName}>{item.author}</span>
<span className={styles.authorRole}>{item.role}</span>
</span>
</footer>

View file

@ -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<I18nContextType>({
locale: "ru",
dict: dictionaries.ru,
});
export const I18nProvider = ({
locale,
children,
}: {
locale: Locale;
children: ReactNode;
}) => {
const dict = dictionaries[locale] || dictionaries.ru;
return (
<I18nContext.Provider value={{ locale, dict }}>
{children}
</I18nContext.Provider>
);
};
export const useI18n = () => useContext(I18nContext);

View file

@ -0,0 +1,519 @@
import type { Dictionary, Locale, LocalizedProject } from "./types";
export const dictionaries: Record<Locale, Dictionary> = {
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<Locale, LocalizedProject[]> = {
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",
},
],
};

View file

@ -0,0 +1,3 @@
export * from "./types";
export * from "./dictionaries";
export * from "./context";

View file

@ -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;
};
}

View file

@ -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",
},
];
export const getProjects = (locale: Locale = "ru"): Project[] => {
return localizedProjects[locale] || localizedProjects.ru;
};
export const projects: Project[] = localizedProjects.ru;

View file

@ -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
========================= */

View file

@ -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<Project | null>(null);
const [headRef, headInView] = useInView<HTMLDivElement>(0.4);
const [gridRef, gridInView] = useInView<HTMLDivElement>(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 = () => {
<section className={styles.projectsPage} data-scroll-section>
<span className={styles.glow} aria-hidden="true" />
<span className={styles.ghost} aria-hidden="true">
{String(projects.length).padStart(2, "0")}
{String(currentProjects.length).padStart(2, "0")}
</span>
<div className="page-container relative z-10">
<div ref={headRef} className={styles.head}>
<p className={`${styles.eyebrow} ${reveal(headInView)}`}>
<span className={styles.rule} aria-hidden="true" />
проекты
{dict.projectsPage.eyebrow}
</p>
<h1 className={`${styles.title} ${reveal(headInView)}`}>
Все <span className={styles.titleAccent}>работы</span>
{dict.projectsPage.titleStart}{" "}
<span className={styles.titleAccent}>
{dict.projectsPage.titleAccent}
</span>
</h1>
<p className={`${styles.count} ${reveal(headInView)}`}>
{String(projects.length).padStart(2, "0")} проектов под ключ
{String(currentProjects.length).padStart(2, "0")}{" "}
{dict.projectsPage.countSuffix}
</p>
</div>
<div ref={gridRef} className={styles.grid}>
{projects.map((project, index) => (
{currentProjects.map((project, index) => (
<div
key={project.id}
className={reveal(gridInView)}

View file

@ -1,72 +1,32 @@
"use client";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { usePathname } from "next/navigation";
import { useI18n, dictionaries, type Locale } from "@/shared/config/i18n";
import styles from "@/static/context-menu/context-menu.module.css";
const PADDING = 12;
const ESTIMATED_WIDTH = 275;
const ESTIMATED_HEIGHT = 345;
type Lang = "ru" | "en";
const content = {
ru: {
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",
},
en: {
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",
},
};
const ESTIMATED_HEIGHT = 320;
export const ContextMenu = () => {
const [isOpen, setIsOpen] = useState(false);
const [lang, setLang] = useState<Lang>("ru");
const [coords, setCoords] = useState({ x: 0, y: 0 });
const [renderCoords, setRenderCoords] = useState({ x: 0, y: 0 });
const [copied, setCopied] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const { locale: contextLocale } = useI18n();
const pathname = usePathname() || "";
const handleSetLang = (newLang: Lang) => {
setLang(newLang);
try {
localStorage.setItem("portfolio_lang", newLang);
} catch {
// ignore
}
};
// Auto-detect current active language from URL pathname or i18n context
const currentLocale: Locale = pathname.startsWith("/en")
? "en"
: pathname.startsWith("/ru")
? "ru"
: contextLocale || "ru";
const current = dictionaries[currentLocale].contextMenu;
// Synchronous layout adjustment with actual DOM dimensions
useLayoutEffect(() => {
@ -87,7 +47,7 @@ export const ContextMenu = () => {
}
setRenderCoords({ x: Math.round(nextX), y: Math.round(nextY) });
}, [isOpen, coords, lang]);
}, [isOpen, coords, currentLocale]);
// Contextmenu listener
useEffect(() => {
@ -139,15 +99,6 @@ export const ContextMenu = () => {
targetY = Math.max(PADDING, window.innerHeight - ESTIMATED_HEIGHT - PADDING);
}
try {
const saved = localStorage.getItem("portfolio_lang") as Lang | null;
if (saved === "ru" || saved === "en") {
setLang(saved);
}
} catch {
// ignore
}
setCoords({ x: e.clientX, y: e.clientY });
setRenderCoords({ x: Math.round(targetX), y: Math.round(targetY) });
setCopied(false);
@ -227,8 +178,6 @@ export const ContextMenu = () => {
if (!isOpen) return null;
const current = content[lang];
return (
<div
ref={menuRef}
@ -242,7 +191,7 @@ export const ContextMenu = () => {
onClick={(e) => e.stopPropagation()}
onContextMenu={(e) => e.preventDefault()}
>
{/* Header with name and language switcher */}
{/* Header with profile info */}
<div className={styles.header}>
<div className={styles.headerInfo}>
<div className={styles.headerTitleRow}>
@ -255,33 +204,6 @@ export const ContextMenu = () => {
</div>
<span className={styles.headerSubtitle}>{current.role}</span>
</div>
<div className={styles.langToggle} role="group" aria-label="Язык / Language">
<button
type="button"
className={`${styles.langBtn} ${lang === "ru" ? styles.langBtnActive : ""}`}
onClick={(e) => {
e.stopPropagation();
handleSetLang("ru");
}}
data-cursor-hover
aria-pressed={lang === "ru"}
>
RU
</button>
<button
type="button"
className={`${styles.langBtn} ${lang === "en" ? styles.langBtnActive : ""}`}
onClick={(e) => {
e.stopPropagation();
handleSetLang("en");
}}
data-cursor-hover
aria-pressed={lang === "en"}
>
EN
</button>
</div>
</div>
<div className={styles.divider} />
@ -479,3 +401,5 @@ export const ContextMenu = () => {
</div>
);
};
export default ContextMenu;

View file

@ -1,14 +1,15 @@
"use client";
import { useEffect, useState } from "react";
import { useI18n } from "@/shared/config/i18n";
import styles from "@/static/cookie-consent/cookie-consent.module.css";
export const COOKIE_CONSENT_KEY = "autli-cookie-consent";
export const COOKIE_CONSENT_EVENT = "autli-cookie-consent-accepted";
const CookieConsent = () => {
export const CookieConsent = () => {
const [visible, setVisible] = useState(false);
const { dict } = useI18n();
useEffect(() => {
const timer = window.setTimeout(() => {
@ -27,18 +28,15 @@ const CookieConsent = () => {
if (!visible) return null;
return (
<section className={styles.banner} aria-label="Согласие на использование cookie">
<section className={styles.banner} aria-label={dict.cookieConsent.title}>
<div className={styles.content}>
<div className={styles.text}>
<p className={styles.title}>Cookie-файлы</p>
<p className={styles.description}>
Я использую cookie и Яндекс Метрику, чтобы понимать, как работает
сайт, и улучшать его для посетителей.
</p>
<p className={styles.title}>{dict.cookieConsent.title}</p>
<p className={styles.description}>{dict.cookieConsent.text}</p>
</div>
<button type="button" className={styles.button} onClick={acceptCookies}>
Принять
{dict.cookieConsent.acceptButton}
</button>
</div>
</section>

View file

@ -1,10 +1,13 @@
import Link from "next/link";
"use client";
import { navigation } from "@/shared/config/navigation/navigation";
import Link from "next/link";
import { sociallinks } from "@/shared/config/social-links/links";
import { useI18n } from "@/shared/config/i18n";
import styles from "@/static/footer/footer.module.css";
const Footer = () => {
export const Footer = () => {
const { locale, dict } = useI18n();
const telegram = sociallinks.find(
(link) => link.label.toLowerCase() === "telegram"
);
@ -12,6 +15,12 @@ const Footer = () => {
(link) => link.label.toLowerCase() === "github"
);
const navItems = [
{ label: dict.nav.about, href: `/${locale}/#about` },
{ label: dict.nav.projects, href: `/${locale}/projects/` },
{ label: dict.nav.contact, href: `/${locale}/#contact` },
];
return (
<footer id="contact" className={styles.footer} data-scroll-section>
<span className={styles.watermark} aria-hidden="true">
@ -20,18 +29,15 @@ const Footer = () => {
<div className="page-container">
<div className={styles.cta}>
<p className={styles.eyebrow}>контакты</p>
<p className={styles.eyebrow}>{dict.footer.eyebrow}</p>
<h2 className={styles.title}>
Есть проект?
{dict.footer.titleStart}
<br />
<span className={styles.titleAccent}>Напиши мне</span>
<span className={styles.titleAccent}>{dict.footer.titleAccent}</span>
</h2>
<p className={styles.text}>
Отвечаю обычно в течение дня. Расскажи, что хочешь сделать, — я
помогу с идеей, оценкой и реализацией.
</p>
<p className={styles.text}>{dict.footer.desc}</p>
<div className={styles.actions}>
<a
@ -40,7 +46,7 @@ const Footer = () => {
rel="noreferrer"
className={styles.button}
>
Написать в Telegram
{dict.footer.telegramButton}
<span aria-hidden="true">↗</span>
</a>
@ -50,7 +56,7 @@ const Footer = () => {
rel="noreferrer"
className={styles.buttonGhost}
>
GitHub
{dict.footer.githubButton}
<span aria-hidden="true">↗</span>
</a>
</div>
@ -58,14 +64,14 @@ const Footer = () => {
<div className={styles.bottom}>
<span className={styles.wordmark}>
<Link href="/">
<Link href={`/${locale}/`}>
Autli
</Link>
</span>
<nav className={styles.nav} aria-label="Навигация в подвале">
<ul>
{navigation.map((item) => (
{navItems.map((item) => (
<li key={item.href}>
<Link href={item.href}>{item.label}</Link>
</li>

View file

@ -1,14 +1,40 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { navigation } from "@/shared/config/navigation/navigation";
import { usePathname } from "next/navigation";
import { useI18n } from "@/shared/config/i18n";
import styles from "@/static/header/header.module.css";
const Header = () => {
export const Header = () => {
const [open, setOpen] = useState(false);
const { locale, dict } = useI18n();
const pathname = usePathname() || `/${locale}/`;
// Compute opposite target language and switch link
const targetLocale = locale === "ru" ? "en" : "ru";
const targetLabel = targetLocale.toUpperCase();
// Replace current locale prefix in pathname
let switchHref = pathname;
if (pathname.startsWith("/ru")) {
switchHref = pathname.replace(/^\/ru(\/|$)/, `/${targetLocale}$1`);
} else if (pathname.startsWith("/en")) {
switchHref = pathname.replace(/^\/en(\/|$)/, `/${targetLocale}$1`);
} else {
switchHref = `/${targetLocale}/`;
}
// Ensure trailing slash if needed
if (!switchHref.endsWith("/") && !switchHref.includes("#")) {
switchHref = `${switchHref}/`;
}
const navItems = [
{ label: dict.nav.about, href: `/${locale}/#about` },
{ label: dict.nav.projects, href: `/${locale}/projects/` },
{ label: dict.nav.contact, href: `/${locale}/#contact` },
];
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
@ -37,7 +63,7 @@ const Header = () => {
<header className={styles.header}>
<div className="cont-header">
<div className={styles.left}>
<Link href="/" className={styles.logo} onClick={close}>
<Link href={`/${locale}/`} className={styles.logo} onClick={close}>
Autli
</Link>
</div>
@ -45,7 +71,7 @@ const Header = () => {
<div className={styles.right}>
<nav className={styles.desktopNav} aria-label="Основная навигация">
<ul>
{navigation.map((item) => (
{navItems.map((item) => (
<li key={item.href}>
<Link href={item.href}>{item.label}</Link>
</li>
@ -53,7 +79,18 @@ const Header = () => {
</ul>
</nav>
{/* Гамбургер */}
{/* Modern Single-Button Language Switcher */}
<Link
href={switchHref}
className={styles.langToggle}
aria-label={`Switch to ${targetLocale === "en" ? "English" : "Russian"}`}
data-cursor-hover
>
<span className={styles.langToggleDot} aria-hidden="true" />
<span>{targetLabel}</span>
</Link>
{/* Hamburger */}
<button
className={`${styles.hamburger} ${open ? styles.active : ""}`}
type="button"
@ -73,7 +110,7 @@ const Header = () => {
aria-label="Мобильная навигация"
>
<ul>
{navigation.map((item) => (
{navItems.map((item) => (
<li key={item.href}>
<Link href={item.href} onClick={close}>
{item.label}
@ -81,6 +118,18 @@ const Header = () => {
</li>
))}
</ul>
<div className={styles.mobileLangRow}>
<Link
href={switchHref}
className={styles.langToggle}
onClick={close}
aria-label={`Switch to ${targetLocale === "en" ? "English" : "Russian"}`}
>
<span className={styles.langToggleDot} aria-hidden="true" />
<span>{targetLabel}</span>
</Link>
</div>
</nav>
</header>
);

View file

@ -1,13 +1,14 @@
"use client";
import { useEffect, useState } from "react";
import { useI18n } from "@/shared/config/i18n";
import styles from "@/static/preloader/preloader.module.css";
const Preloader = () => {
export const Preloader = () => {
const [progress, setProgress] = useState(0);
const [done, setDone] = useState(false);
const [hidden, setHidden] = useState(false);
const { dict } = useI18n();
useEffect(() => {
document.documentElement.classList.add("no-scroll");
@ -76,7 +77,7 @@ const Preloader = () => {
</div>
<div className={styles.meta}>
<span className={styles.status}>загрузка</span>
<span className={styles.status}>{dict.preloader.loadingText}</span>
<span className={styles.percent}>{progress}%</span>
</div>
</div>

View file

@ -1,8 +1,8 @@
"use client";
import { useEffect, useRef } from "react";
import type { Project } from "@/shared/config/projects/types";
import { useI18n } from "@/shared/config/i18n";
import styles from "@/static/project-modal/project-modal.module.css";
type ProjectModalProps = {
@ -10,8 +10,9 @@ type ProjectModalProps = {
onClose: () => void;
};
const ProjectModal = ({ project, onClose }: ProjectModalProps) => {
export const ProjectModal = ({ project, onClose }: ProjectModalProps) => {
const panelRef = useRef<HTMLDivElement>(null);
const { dict, locale } = useI18n();
useEffect(() => {
if (!project) return;
@ -38,7 +39,7 @@ const ProjectModal = ({ project, onClose }: ProjectModalProps) => {
className={styles.overlay}
role="dialog"
aria-modal="true"
aria-label={`Проект ${project.title}`}
aria-label={`${dict.projectModal.aboutTitle}: ${project.title}`}
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
@ -46,11 +47,13 @@ const ProjectModal = ({ project, onClose }: ProjectModalProps) => {
<div ref={panelRef} className={styles.panel} tabIndex={-1}>
<div className={styles.wrap}>
<header className={styles.head}>
<span className={styles.meta}>Проект {project.id}</span>
<span className={styles.meta}>
{locale === "ru" ? `Проект ${project.id}` : `Project ${project.id}`}
</span>
<button
type="button"
className={styles.close}
aria-label="Закрыть"
aria-label={dict.projectModal.closeHint}
onClick={onClose}
>
×
@ -63,16 +66,16 @@ const ProjectModal = ({ project, onClose }: ProjectModalProps) => {
<ul className={styles.metaList}>
<li>
<span>Год</span>
<span>{locale === "ru" ? "Год" : "Year"}</span>
<strong>{project.year}</strong>
</li>
<li>
<span>Стек</span>
<span>{dict.projectModal.stackTitle}</span>
<strong>{project.tags.join(" · ")}</strong>
</li>
</ul>
<h4 className={styles.subheading}>Что внутри</h4>
<h4 className={styles.subheading}>{dict.projectModal.featuresTitle}</h4>
<ul className={styles.features}>
{project.features.map((feature) => (
@ -81,14 +84,14 @@ const ProjectModal = ({ project, onClose }: ProjectModalProps) => {
</ul>
<footer className={styles.actions}>
{project.demo ? (
{project.link ? (
<a
href={project.demo}
href={project.link}
target="_blank"
rel="noreferrer"
className={styles.actionButton}
>
Живая версия
{dict.projectModal.viewSite}
<span aria-hidden="true">↗</span>
</a>
) : null}
@ -100,19 +103,7 @@ const ProjectModal = ({ project, onClose }: ProjectModalProps) => {
rel="noreferrer"
className={styles.actionGhost}
>
Код проекта
<span aria-hidden="true">↗</span>
</a>
) : null}
{project.link ? (
<a
href={project.link}
target="_blank"
rel="noreferrer"
className={styles.actionGhost}
>
Перейти
{dict.projectModal.viewCode}
<span aria-hidden="true">↗</span>
</a>
) : null}