feat: migrate i18n to next-intl with locale-based URL routing

Replace manual cookie-based language system with next-intl v4. All routes
now use /ru and /en URL prefixes, translation strings live in JSON files,
and components use useTranslations/getTranslations instead of useLang/Redux.
This commit is contained in:
Atash03 2026-02-12 01:45:37 +05:00
parent 7940c7b2f4
commit f53753487f
71 changed files with 1576 additions and 1101 deletions

3
.gitignore vendored
View File

@ -33,3 +33,6 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
.claude
.agents

View File

@ -1,16 +1,15 @@
import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar";
import { getAbout } from "@/services/about"; import { getAbout } from "@/services/about";
import { cookies } from "next/headers"; import { getLocale, getTranslations } from "next-intl/server";
export default async function AboutPage() { export default async function AboutPage() {
const lang = cookies().get("lang")?.value ?? "ru"; const lang = await getLocale();
const t = await getTranslations("about");
const data = await getAbout(lang); const data = await getAbout(lang);
const aboutText = lang === "en" ? "About us" : "Коротко о нас";
return ( return (
<LayoutWithSidebar second={aboutText} title={aboutText}> <LayoutWithSidebar second={t("title")} title={t("title")}>
<div <div
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: data, __html: data,

View File

@ -5,26 +5,28 @@ import Image from "next/image";
import { GreenBtn } from "@/components/ui/Buttons"; import { GreenBtn } from "@/components/ui/Buttons";
import { useAppDispatch, useAppSelector } from "@/redux/hooks"; import { useAppDispatch } from "@/redux/hooks";
import { selectHeader, setShowInput } from "@/redux/slices/headerSlice"; import { setShowInput } from "@/redux/slices/headerSlice";
import { baseAPI } from "@/lib/API"; import { baseAPI } from "@/lib/API";
import { NewsPageType } from "@/lib/types/NewsPage.type"; import { NewsPageType } from "@/lib/types/NewsPage.type";
import Link from "next/link"; import { Link } from "@/i18n/navigation";
import { BreadCrumbs } from "@/components/ui/bread-crumbs"; import { BreadCrumbs } from "@/components/ui/bread-crumbs";
import { Title } from "@/components/ui/title"; import { Title } from "@/components/ui/title";
import Loader from "@/components/ui/Loader"; import Loader from "@/components/ui/Loader";
import { useLocale, useTranslations } from "next-intl";
export default function SingleNewsPage({ params }: { params: { id: string } }) { export default function SingleNewsPage({ params }: { params: { id: string } }) {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const locale = useLocale();
const t = useTranslations("news");
const { activeLang } = useAppSelector(selectHeader);
const [newsItemData, setNewsItemData] = React.useState<NewsPageType>(); const [newsItemData, setNewsItemData] = React.useState<NewsPageType>();
const fetchData = async () => { const fetchData = async () => {
try { try {
const response = await fetch(`${baseAPI}news/${params.id}`, { const response = await fetch(`${baseAPI}news/${params.id}`, {
headers: { headers: {
"Accept-Language": activeLang.localization, "Accept-Language": locale,
}, },
}); });
@ -43,11 +45,11 @@ export default function SingleNewsPage({ params }: { params: { id: string } }) {
React.useEffect(() => { React.useEffect(() => {
fetchData(); fetchData();
dispatch(setShowInput(false)); dispatch(setShowInput(false));
}, [activeLang.localization]); }, [locale]);
return ( return (
<div className="section-mb w-full"> <div className="section-mb w-full">
<BreadCrumbs second="Новости" path="/news" third="Статья" /> <BreadCrumbs second={t("title")} path="/news" third={t("article")} />
{newsItemData ? ( {newsItemData ? (
<div className="mb-5"> <div className="mb-5">
<Title text={newsItemData.data.title} /> <Title text={newsItemData.data.title} />
@ -84,7 +86,7 @@ export default function SingleNewsPage({ params }: { params: { id: string } }) {
</div> </div>
<Link href="/news" className="flex justify-center"> <Link href="/news" className="flex justify-center">
<GreenBtn text="Все новости" px /> <GreenBtn text={t("allNews")} px />
</Link> </Link>
</div> </div>
); );

View File

@ -2,9 +2,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useLang } from "@/utils/useLang"; import { useLocale, useTranslations } from "next-intl";
import { useAppSelector } from "@/redux/hooks";
import { selectHeader } from "@/redux/slices/headerSlice";
import { NewsData } from "@/lib/types/NewsData.type"; import { NewsData } from "@/lib/types/NewsData.type";
import { baseAPI } from "@/lib/API"; import { baseAPI } from "@/lib/API";
import Loader from "@/components/ui/Loader"; import Loader from "@/components/ui/Loader";
@ -15,9 +13,10 @@ import { Pagination } from "@/components/ui/Pagination";
import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar";
const News = () => { const News = () => {
const { activeLang } = useAppSelector(selectHeader); const locale = useLocale();
const t = useTranslations("news");
const tCommon = useTranslations("common");
const menu = ["Новости", "СМИ о нас"];
const [current, setCurrent] = React.useState<number>(1); const [current, setCurrent] = React.useState<number>(1);
const [perPage, setPerPage] = React.useState<number>(6); const [perPage, setPerPage] = React.useState<number>(6);
const [totalNews, setTotalNews] = React.useState<number>(0); const [totalNews, setTotalNews] = React.useState<number>(0);
@ -36,7 +35,7 @@ const News = () => {
`${baseAPI}news?page=${current}&per_page=${perPage}`, `${baseAPI}news?page=${current}&per_page=${perPage}`,
{ {
headers: { headers: {
"Accept-Language": activeLang.localization, "Accept-Language": locale,
}, },
} }
); );
@ -57,7 +56,7 @@ const News = () => {
React.useEffect(() => { React.useEffect(() => {
fetchNews(); fetchNews();
}, [current, perPage, activeLang.localization]); }, [current, perPage, locale]);
const handleOnClickButton = () => { const handleOnClickButton = () => {
setPerPage((prev) => prev + 6); setPerPage((prev) => prev + 6);
@ -70,23 +69,13 @@ const News = () => {
return ( return (
<div> <div>
<LayoutWithSidebar <LayoutWithSidebar
title={useLang("News", "Новости", activeLang.localization)} title={t("title")}
second={useLang("News", "Новости", activeLang.localization)} second={t("title")}
cursor={false} cursor={false}
> >
<div> <div>
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex flex-col"> <div className="flex flex-col">
<div className="hidden sm:flex justify-between items-center pb-[5px] ">
<div className="pointer-events-none opacity-0 flex items-center gap-5">
{menu.map((item, i) => (
<p key={i} className="cursor-pointer leading-[130%]">
{item}
</p>
))}
</div>
</div>
<div <div
className={clsx( className={clsx(
"mb-[48px] lg:mb-[108px]", "mb-[48px] lg:mb-[108px]",
@ -112,7 +101,7 @@ const News = () => {
<div className="hidden sm:flex flex-col gap-6 w-full max-w-[180px] mx-auto justify-center items-center"> <div className="hidden sm:flex flex-col gap-6 w-full max-w-[180px] mx-auto justify-center items-center">
{newsData && totalNews > perPage && perPage >= totalNews && ( {newsData && totalNews > perPage && perPage >= totalNews && (
<div onClick={handleOnClickButton}> <div onClick={handleOnClickButton}>
<BorderBtn px text={"Показать ещё"} /> <BorderBtn px text={tCommon("showMore")} />
</div> </div>
)} )}

View File

@ -1,8 +1,10 @@
import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar";
import { getServices } from "@/services/services"; import { getServices } from "@/services/services";
import { getLocale } from "next-intl/server";
export default async function AdvertisingPage() { export default async function AdvertisingPage() {
const { data } = await getServices(); const lang = await getLocale();
const { data } = await getServices(lang);
return ( return (
<LayoutWithSidebar <LayoutWithSidebar

View File

@ -1,8 +1,10 @@
import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar";
import { getServices } from "@/services/services"; import { getServices } from "@/services/services";
import { getLocale } from "next-intl/server";
export default async function AdvertisingPage() { export default async function AdvertisingPage() {
const data = await getServices(); const lang = await getLocale();
const data = await getServices(lang);
return ( return (
<LayoutWithSidebar <LayoutWithSidebar
@ -12,7 +14,7 @@ export default async function AdvertisingPage() {
<div <div
className="select-inner" className="select-inner"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: data ? data.data[1].content : "", __html: data?.data ? data.data[1].content : "",
}} }}
/> />
</LayoutWithSidebar> </LayoutWithSidebar>

View File

@ -1,8 +1,10 @@
import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar";
import { getServices } from "@/services/services"; import { getServices } from "@/services/services";
import { getLocale } from "next-intl/server";
export default async function BusinessTours() { export default async function BusinessTours() {
const { data } = await getServices(); const lang = await getLocale();
const { data } = await getServices(lang);
return ( return (
<LayoutWithSidebar <LayoutWithSidebar

View File

@ -1,8 +1,10 @@
import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar";
import { getServices } from "@/services/services"; import { getServices } from "@/services/services";
import { getLocale } from "next-intl/server";
export default async function CertificationPage() { export default async function CertificationPage() {
const data = await getServices(); const lang = await getLocale();
const data = await getServices(lang);
return ( return (
<LayoutWithSidebar <LayoutWithSidebar
@ -12,7 +14,7 @@ export default async function CertificationPage() {
<div <div
className="select-inner" className="select-inner"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: data ? data.data[3].content : "", __html: data?.data ? data.data[3].content : "",
}} }}
/> />
</LayoutWithSidebar> </LayoutWithSidebar>

View File

@ -1,8 +1,10 @@
import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar";
import { getServices } from "@/services/services"; import { getServices } from "@/services/services";
import { getLocale } from "next-intl/server";
export default async function ForwardingPage() { export default async function ForwardingPage() {
const data = await getServices(); const lang = await getLocale();
const data = await getServices(lang);
return ( return (
<LayoutWithSidebar <LayoutWithSidebar
@ -12,7 +14,7 @@ export default async function ForwardingPage() {
<div <div
className="select-inner" className="select-inner"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: data ? data.data[4].content : "", __html: data?.data ? data.data[4].content : "",
}} }}
/> />
</LayoutWithSidebar> </LayoutWithSidebar>

View File

@ -1,8 +1,10 @@
import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar";
import { getServices } from "@/services/services"; import { getServices } from "@/services/services";
import { getLocale } from "next-intl/server";
export default async function HybridsPage() { export default async function HybridsPage() {
const data = await getServices(); const lang = await getLocale();
const data = await getServices(lang);
return ( return (
<LayoutWithSidebar <LayoutWithSidebar
@ -12,7 +14,7 @@ export default async function HybridsPage() {
<div <div
className="select-inner" className="select-inner"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: data ? data.data[2].content : "", __html: data?.data ? data.data[2].content : "",
}} }}
/> />
</LayoutWithSidebar> </LayoutWithSidebar>

View File

@ -1,8 +1,10 @@
import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar";
import { getServices } from "@/services/services"; import { getServices } from "@/services/services";
import { getLocale } from "next-intl/server";
export default async function MeetingsPage() { export default async function MeetingsPage() {
const data = await getServices(); const lang = await getLocale();
const data = await getServices(lang);
return ( return (
<LayoutWithSidebar <LayoutWithSidebar
@ -12,7 +14,7 @@ export default async function MeetingsPage() {
<div <div
className="select-inner" className="select-inner"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: data ? data.data[9].content : "", __html: data?.data ? data.data[9].content : "",
}} }}
/> />
</LayoutWithSidebar> </LayoutWithSidebar>

View File

@ -1,8 +1,10 @@
import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar";
import { getServices } from "@/services/services"; import { getServices } from "@/services/services";
import { getLocale } from "next-intl/server";
export default async function MissionsPage() { export default async function MissionsPage() {
const data = await getServices(); const lang = await getLocale();
const data = await getServices(lang);
return ( return (
<LayoutWithSidebar <LayoutWithSidebar
@ -12,7 +14,7 @@ export default async function MissionsPage() {
<div <div
className="select-inner" className="select-inner"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: data ? data.data[10].content : "", __html: data?.data ? data.data[10].content : "",
}} }}
/> />
</LayoutWithSidebar> </LayoutWithSidebar>

View File

@ -1,8 +1,10 @@
import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar";
import { getServices } from "@/services/services"; import { getServices } from "@/services/services";
import { getLocale } from "next-intl/server";
export default async function OrganizationPage() { export default async function OrganizationPage() {
const data = await getServices(); const lang = await getLocale();
const data = await getServices(lang);
return ( return (
<LayoutWithSidebar <LayoutWithSidebar

View File

@ -1,8 +1,10 @@
import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar";
import { getServices } from "@/services/services"; import { getServices } from "@/services/services";
import { getLocale } from "next-intl/server";
export default async function OutsourcingPage() { export default async function OutsourcingPage() {
const data = await getServices(); const lang = await getLocale();
const data = await getServices(lang);
return ( return (
<LayoutWithSidebar <LayoutWithSidebar
@ -12,7 +14,7 @@ export default async function OutsourcingPage() {
<div <div
className="select-inner" className="select-inner"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: data ? data.data[5].content : "", __html: data?.data ? data.data[5].content : "",
}} }}
/> />
</LayoutWithSidebar> </LayoutWithSidebar>

View File

@ -1,8 +1,10 @@
import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar";
import { getServices } from "@/services/services"; import { getServices } from "@/services/services";
import { getLocale } from "next-intl/server";
export default async function PersonalTrainingPage() { export default async function PersonalTrainingPage() {
const data = await getServices(); const lang = await getLocale();
const data = await getServices(lang);
return ( return (
<LayoutWithSidebar <LayoutWithSidebar
@ -12,7 +14,7 @@ export default async function PersonalTrainingPage() {
<div <div
className="select-inner" className="select-inner"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: data ? data.data[6].content : "", __html: data?.data ? data.data[6].content : "",
}} }}
/> />
</LayoutWithSidebar> </LayoutWithSidebar>

View File

@ -2,20 +2,19 @@
import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar";
import { baseAPI } from "@/lib/API"; import { baseAPI } from "@/lib/API";
import { useAppSelector } from "@/redux/hooks"; import { useLocale, useTranslations } from "next-intl";
import { selectHeader } from "@/redux/slices/headerSlice";
import { useLang } from "@/utils/useLang";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
const Visitors = () => { const Visitors = () => {
const [visitorsData, setVisitorsData] = useState<string>(); const [visitorsData, setVisitorsData] = useState<string>();
const { activeLang } = useAppSelector(selectHeader); const locale = useLocale();
const t = useTranslations("visitors");
const fetchVisitors = async () => { const fetchVisitors = async () => {
try { try {
const res = await fetch(`${baseAPI}settings/visitors_page`, { const res = await fetch(`${baseAPI}settings/visitors_page`, {
headers: { headers: {
"Accept-Language": activeLang.localization, "Accept-Language": locale,
}, },
}); });
@ -33,17 +32,13 @@ const Visitors = () => {
useEffect(() => { useEffect(() => {
fetchVisitors(); fetchVisitors();
}, []); }, [locale]);
return ( return (
<div> <div>
<LayoutWithSidebar <LayoutWithSidebar
title={useLang( title={t("information")}
"Information for visitors", second={t("title")}
"Информация для посетителей",
activeLang.localization
)}
second={useLang("For visitors", "Посетителям", activeLang.localization)}
> >
<div <div
dangerouslySetInnerHTML={{ __html: visitorsData ? visitorsData : "" }} dangerouslySetInnerHTML={{ __html: visitorsData ? visitorsData : "" }}

View File

@ -2,20 +2,19 @@
import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar"; import { LayoutWithSidebar } from "@/components/page/LayoutWithSidebar";
import { baseAPI } from "@/lib/API"; import { baseAPI } from "@/lib/API";
import { useAppSelector } from "@/redux/hooks"; import { useLocale, useTranslations } from "next-intl";
import { selectHeader } from "@/redux/slices/headerSlice";
import { useLang } from "@/utils/useLang";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
const RulesForVisitors = () => { const RulesForVisitors = () => {
const [visitorsData, setVisitorsData] = useState<string>(); const [visitorsData, setVisitorsData] = useState<string>();
const { activeLang } = useAppSelector(selectHeader); const locale = useLocale();
const t = useTranslations("visitors");
const fetchVisitors = async () => { const fetchVisitors = async () => {
try { try {
const res = await fetch(`${baseAPI}settings/visiting_rules`, { const res = await fetch(`${baseAPI}settings/visiting_rules`, {
headers: { headers: {
"Accept-Language": activeLang.localization, "Accept-Language": locale,
}, },
}); });
@ -33,23 +32,15 @@ const RulesForVisitors = () => {
useEffect(() => { useEffect(() => {
fetchVisitors(); fetchVisitors();
}, []); }, [locale]);
return ( return (
<div> <div>
<LayoutWithSidebar <LayoutWithSidebar
title={useLang( title={t("rules")}
"Entrance rules", second={t("title")}
"Порядок регистрации посетителей",
activeLang.localization
)}
second={useLang("Visitors", "Посетителям", activeLang.localization)}
path="/visitors" path="/visitors"
third={useLang( third={t("rules")}
"Entrance rules",
"Порядок регистрации посетителей",
activeLang.localization
)}
> >
<div <div
dangerouslySetInnerHTML={{ __html: visitorsData ? visitorsData : "" }} dangerouslySetInnerHTML={{ __html: visitorsData ? visitorsData : "" }}

View File

@ -1,6 +1,6 @@
import { EventPageButtons } from "@/components/shared/event-page-buttons"; import { EventPageButtons } from "@/components/shared/event-page-buttons";
import { getEventPage } from "@/services/calendar"; import { getEventPage } from "@/services/calendar";
import { cookies } from "next/headers"; import { getLocale, getTranslations } from "next-intl/server";
import Image from "next/image"; import Image from "next/image";
export default async function EventPage({ export default async function EventPage({
@ -10,7 +10,8 @@ export default async function EventPage({
}) { }) {
const { id } = params; const { id } = params;
const lang = cookies().get("lang")?.value ?? "ru"; const lang = await getLocale();
const t = await getTranslations("events");
const data = await getEventPage(id, lang); const data = await getEventPage(id, lang);
@ -24,7 +25,7 @@ export default async function EventPage({
<div className="flex flex-col gap-20 w-full"> <div className="flex flex-col gap-20 w-full">
<div className="flex flex-col gap-8 event-block "> <div className="flex flex-col gap-8 event-block ">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h4>{lang === "en" ? "Date" : "Дата"}</h4> <h4>{t("date")}</h4>
<h5>{data?.date}</h5> <h5>{data?.date}</h5>
</div> </div>
@ -32,7 +33,7 @@ export default async function EventPage({
{data?.location && ( {data?.location && (
<> <>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h4>{lang === "en" ? "Venue" : "Место"}</h4> <h4>{t("venue")}</h4>
<h5>{data?.location}</h5> <h5>{data?.location}</h5>
</div> </div>
<hr /> <hr />
@ -42,7 +43,7 @@ export default async function EventPage({
{data?.organizers.length > 0 && ( {data?.organizers.length > 0 && (
<> <>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h4>{lang === "en" ? "Organiser" : "Организатор"}</h4> <h4>{t("organiser")}</h4>
<h5>{data?.organizers[0]?.name}</h5> <h5>{data?.organizers[0]?.name}</h5>
</div> </div>
</> </>
@ -53,7 +54,7 @@ export default async function EventPage({
<hr /> <hr />
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h4>{lang === "en" ? "Co-organiser" : "Со-организатор"}</h4> <h4>{t("coOrganiser")}</h4>
<h5>{data?.coorganizers[0]?.name}</h5> <h5>{data?.coorganizers[0]?.name}</h5>
</div> </div>
@ -81,7 +82,7 @@ export default async function EventPage({
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<div className="flex flex-col lg:flex-row gap-6"> <div className="flex flex-col lg:flex-row gap-6">
<h4 className="text_24 lg:flex-[0_0_392px]"> <h4 className="text_24 lg:flex-[0_0_392px]">
{lang === "en" ? "Description" : "Описание"} {t("description")}
</h4> </h4>
<p className="flex-1 text_16">{data?.description}</p> <p className="flex-1 text_16">{data?.description}</p>
</div> </div>
@ -92,7 +93,7 @@ export default async function EventPage({
<div className="flex flex-col lg:flex-row gap-6"> <div className="flex flex-col lg:flex-row gap-6">
<h4 className="text_24 lg:flex-[0_0_392px]"> <h4 className="text_24 lg:flex-[0_0_392px]">
{lang === "en" ? "Theme of event" : "Тематика мероприятия"} {t("theme")}
</h4> </h4>
<div <div
className="flex-1 text_16" className="flex-1 text_16"

View File

@ -3,26 +3,26 @@ import { BreadCrumbs } from "@/components/ui/bread-crumbs";
import { Title } from "@/components/ui/title"; import { Title } from "@/components/ui/title";
import { getCalendar } from "@/services/calendar"; import { getCalendar } from "@/services/calendar";
import { Metadata } from "next"; import { Metadata } from "next";
import { cookies } from "next/headers"; import { getLocale, getTranslations } from "next-intl/server";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "TurkmenExpo | Calendar", title: "TurkmenExpo | Calendar",
}; };
export default async function CalendarPage() { export default async function CalendarPage() {
const lang = cookies().get("lang")?.value ?? "ru"; const lang = await getLocale();
const t = await getTranslations("calendar");
const data = await getCalendar(lang); const data = await getCalendar(lang);
const title = lang === "en" ? "Calendar of events" : "Календарь мероприятий";
return ( return (
<div className="section-mb pt-10"> <div className="section-mb pt-10">
<div className="container flex flex-col items-start pt-5 gap-10 md:gap-12"> <div className="container flex flex-col items-start pt-5 gap-10 md:gap-12">
<div> <div>
<div className="mb-[24px]"> <div className="mb-[24px]">
<BreadCrumbs second={title} /> <BreadCrumbs second={t("title")} />
</div> </div>
<Title text={title} /> <Title text={t("title")} />
</div> </div>
<div className="flex flex-col gap-6 w-full"> <div className="flex flex-col gap-6 w-full">
{data.data.map((item, i) => ( {data.data.map((item, i) => (

View File

@ -2,32 +2,34 @@ import React from "react";
import { BreadCrumbs } from "@/components/ui/bread-crumbs"; import { BreadCrumbs } from "@/components/ui/bread-crumbs";
import { ContactsForm } from "@/components/shared/contacts-form"; import { ContactsForm } from "@/components/shared/contacts-form";
import { cookies } from "next/headers"; import { getTranslations } from "next-intl/server";
export default async function ContactsPage() { export default async function ContactsPage() {
const lang = cookies().get("lang")?.value; const t = await getTranslations("contacts");
return ( return (
<main className="bg-blueBg h-full w-full pt-12"> <main className="bg-blueBg h-full w-full pt-12">
<div className="container flex flex-col items-start"> <div className="container flex flex-col items-start">
<div className="mt-5"> <div className="mt-5">
<BreadCrumbs second={lang === "ru" ? "Контакты" : "Contacts"} /> <BreadCrumbs second={t("title")} />
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 w-full"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 w-full">
<ContactsForm /> <ContactsForm />
<div className="p-6 bg-bg_surface_container rounded-[8px]"> <div className="p-6 bg-bg_surface_container rounded-[8px]">
<h2 className="h2 mb-10 xl:mb-8 text-3xl font-normal">Контакты</h2> <h2 className="h2 mb-10 xl:mb-8 text-3xl font-normal">
{t("title")}
</h2>
<div className="flex flex-col gap-20"> <div className="flex flex-col gap-20">
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<img src="/assets/icons/contacts/address.svg" alt="address" /> <img src="/assets/icons/contacts/address.svg" alt="address" />
<div> <div>
<h3 className="text-xl mb-2">Адрес:</h3> <h3 className="text-xl mb-2">{t("address")}</h3>
<address className="text-base normal not-italic"> <address className="text-base normal not-italic">
744000, г. Ашхабад, просп. Битарап Туркменистан, 183 {t("addressValue")}
</address> </address>
</div> </div>
</div> </div>
@ -36,9 +38,7 @@ export default async function ContactsPage() {
<div> <div>
<h3 className="text-xl mb-2"></h3> <h3 className="text-xl mb-2"></h3>
<h4 className="text-base normal"> <h4 className="text-base normal">{t("phoneNumbers")}</h4>
+99371871814; 99363719588
</h4>
</div> </div>
</div> </div>

View File

@ -4,11 +4,11 @@ import { Partners } from "@/components/shared/Partners";
import { Reviews } from "@/components/shared/Reviews"; import { Reviews } from "@/components/shared/Reviews";
import { Slider } from "@/components/shared/Slider"; import { Slider } from "@/components/shared/Slider";
import Loader from "@/components/ui/Loader"; import Loader from "@/components/ui/Loader";
import { cookies } from "next/headers"; import { getLocale } from "next-intl/server";
import { Suspense } from "react"; import { Suspense } from "react";
export default async function HomePage() { export default async function HomePage() {
const lang = cookies().get("lang")?.value ?? "ru"; const lang = await getLocale();
return ( return (
<div className="bg-blueBg flex flex-col gap-[60px] md:gap-20 pb-20 mt-[70px]"> <div className="bg-blueBg flex flex-col gap-[60px] md:gap-20 pb-20 mt-[70px]">
@ -19,7 +19,6 @@ export default async function HomePage() {
</Suspense> </Suspense>
<News /> <News />
<Reviews /> <Reviews />
{/* <Video /> */}
<Partners /> <Partners />
</div> </div>
); );

45
app/[locale]/layout.tsx Normal file
View File

@ -0,0 +1,45 @@
import { Roboto } from "next/font/google";
import clsx from "clsx";
import { NextIntlClientProvider } from "next-intl";
import { getMessages, setRequestLocale } from "next-intl/server";
import { notFound } from "next/navigation";
import { locales } from "@/i18n/config";
import StoreProvider from "../StoreProvider";
import "../globals.css";
const roboto = Roboto({
subsets: ["latin"],
weight: ["300", "400", "500", "700"],
variable: "--font-roboto",
});
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: { locale: string };
}) {
const { locale } = params;
if (!locales.includes(locale as any)) notFound();
setRequestLocale(locale);
const messages = await getMessages();
return (
<html lang={locale}>
<StoreProvider>
<body className={clsx("antialiased font-roboto", roboto.variable)}>
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</StoreProvider>
</html>
);
}

View File

@ -1,15 +1,4 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Roboto } from "next/font/google";
import StoreProvider from "./StoreProvider";
import "./globals.css";
import clsx from "clsx";
const roboto = Roboto({
subsets: ["latin"],
weight: ["300", "400", "500", "700"],
variable: "--font-roboto",
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "TurkmenExpo", title: "TurkmenExpo",
@ -29,13 +18,5 @@ export default function RootLayout({
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return children;
<html>
<StoreProvider>
<body className={clsx("antialiased font-roboto", roboto.variable)}>
{children}
</body>
</StoreProvider>
</html>
);
} }

View File

@ -1,31 +0,0 @@
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
// 1. Проверяем существующую куку
let lang = request.cookies.get("lang")?.value;
// 2. Если куки нет, определяем язык из браузера
if (!lang) {
lang =
request.headers
.get("Accept-Language")
?.split(",")[0] // Берём первый язык из списка
.split("-")[0] // Убираем регион (en-US → en)
.toLowerCase() || "en"; // Fallback на английский
}
// 3. Сохраняем язык в куки
const response = NextResponse.next();
if (!request.cookies.has("lang")) {
response.cookies.set("lang", lang, {
maxAge: 60 * 60 * 24 * 365, // 1 год
httpOnly: true,
});
}
// 4. Добавляем язык в заголовки ВСЕХ исходящих запросов
response.headers.set("Accept-Language", lang);
return response;
}

View File

@ -2,9 +2,9 @@
import React from "react"; import React from "react";
import { BreadCrumbs } from "../ui/bread-crumbs"; import { BreadCrumbs } from "../ui/bread-crumbs";
import { usePathname } from "next/navigation"; import { useTranslations } from "next-intl";
import { useAppSelector } from "@/redux/hooks";
import { Title } from "../ui/title"; import { Title } from "../ui/title";
import { usePathname } from "@/i18n/navigation";
interface Props { interface Props {
title: string; title: string;
@ -26,20 +26,14 @@ export const LayoutWithSidebar = ({
cursor = false, cursor = false,
}: Props) => { }: Props) => {
const pathname = usePathname(); const pathname = usePathname();
const lang = useAppSelector( const t = useTranslations("services");
(state) => state.headerSlice.activeLang.localization
);
return ( return (
<div className="flex bg-white px-6 py-4 rounded-lg flex-col md:gap-6 gap-10 section-mb w-full"> <div className="flex bg-white px-6 py-4 rounded-lg flex-col md:gap-6 gap-10 section-mb w-full">
<div> <div>
<BreadCrumbs <BreadCrumbs
second={ second={
pathname.includes("/services") pathname.includes("/services") ? t("breadcrumb") : second ?? ""
? lang === "en"
? "Services"
: "Сервисы"
: second ?? ""
} }
path={path} path={path}
path2={path2} path2={path2}

View File

@ -1,19 +0,0 @@
"use client";
import React from "react";
import Link from "next/link";
import { topMenu } from "@/lib/database/pathnames";
export const Pathnames = ({ sort }: { sort: string }) => {
return (
<div className="text-[12px] text-gray4 flex items-center mob:mb-6 mb-5">
<Link href={"/"}>{"Главная "}</Link>
{topMenu
.filter((item) => item.path === sort)
.map((obj, i) =>
obj.links.map((item) => <p key={i}>{item.default}</p>)
)}
</div>
);
};

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import { Link } from "@/i18n/navigation";
interface Props { interface Props {
img: string; img: string;

View File

@ -1,4 +1,4 @@
import { getEvents } from "@/services/home"; import { getTranslations } from "next-intl/server";
const events = [ const events = [
{ {
@ -22,44 +22,18 @@ const events = [
]; ];
export const Events = async ({ lang }: { lang: string }) => { export const Events = async ({ lang }: { lang: string }) => {
// const data = await getEvents(lang); const t = await getTranslations("home");
const btnText =
lang === "en" ? "Show more" : lang === "ru" ? "Показать еще" : "Show more";
const title = lang === "en" ? "Exhibitions" : "Выставки";
const linkText = lang === "en" ? "Go to website" : "Перейти на сайт";
return ( return (
<section> <section>
<div className="container md:block"> <div className="container md:block">
<div className="sm:mb-10"> <div className="sm:mb-10">
<h2 className="text-center font-semibold text-3xl mb-8"> <h2 className="text-center font-semibold text-3xl mb-8">
{title} <span className="text-PRIMARY">Turkmen</span> {t("exhibitions")} <span className="text-PRIMARY">Turkmen</span>
<span className="text-red">Expo 2025</span> <span className="text-red">Expo 2025</span>
</h2> </h2>
{/* <Title
text={
lang === "en"
? "Upcoming exhibitions and events"
: lang === "ru"
? "Ближайшие выставки и мероприятия"
: "Upcoming exhibitions and events"
}
/> */}
</div> </div>
{/* <div className="w-full flex flex-col items-center gap-8"> */}
<div className="w-full flex flex-col items-center gap-8 lg:flex-row"> <div className="w-full flex flex-col items-center gap-8 lg:flex-row">
{/* {data.data.slice(0, 2).map((item, i) => (
<EventCard
coorganizers={item.coorganizers}
dark
key={i}
{...item}
/>
))}
<LinkButton href="/calendar">{btnText}</LinkButton> */}
{events.map((item, i) => ( {events.map((item, i) => (
<article <article
key={i} key={i}
@ -87,15 +61,13 @@ export const Events = async ({ lang }: { lang: string }) => {
target="_blank" target="_blank"
className="text-PRIMARY underline" className="text-PRIMARY underline"
> >
{linkText} {t("goToWebsite")}
</a> </a>
</div> </div>
</article> </article>
))} ))}
</div> </div>
</div> </div>
{/* <EventsMobile data={data.data.slice(0, 5)} /> */}
</section> </section>
); );
}; };

View File

@ -1,11 +1,10 @@
"use client"; "use client";
import React from "react"; import React from "react";
import Link from "next/link"; import { useTranslations } from "next-intl";
import { footerInfo, headerMenu2 } from "@/lib/database/pathnames"; import { headerMenu2 } from "@/lib/database/pathnames";
import { useAppSelector } from "@/redux/hooks"; import { Link } from "@/i18n/navigation";
import clsx from "clsx";
export const socials = [ export const socials = [
{ href: "https://www.linkedin.com/company/turkmen-expo", icon: "linkedin" }, { href: "https://www.linkedin.com/company/turkmen-expo", icon: "linkedin" },
@ -24,9 +23,7 @@ export const socials = [
]; ];
export const Footer = () => { export const Footer = () => {
const localization = useAppSelector( const t = useTranslations();
(state) => state.headerSlice.activeLang.localization
);
return ( return (
<footer className="bg-PRIMARY text-white pt-6 pb-5 mob:py-[40px]"> <footer className="bg-PRIMARY text-white pt-6 pb-5 mob:py-[40px]">
@ -36,40 +33,30 @@ export const Footer = () => {
<img className="h-12" src="/assets/icons/white-logo.svg" /> <img className="h-12" src="/assets/icons/white-logo.svg" />
<div className="flex flex-col mt-8 lg:flex-row items-center font-medium gap-12"> <div className="flex flex-col mt-8 lg:flex-row items-center font-medium gap-12">
{headerMenu2 {headerMenu2.map((item, i) => (
.filter((item) => (localization === "en" ? item.en : !item.en)) <Link key={i} href={item.link} className="cursor-pointer">
.map((item, i) => ( {t(item.titleKey)}
<Link key={i} href={item.link} className="cursor-pointer"> </Link>
{item.title} ))}
</Link>
))}
</div> </div>
</div> </div>
<div className="flex flex-col lg:flex-row lg:justify-between justify-center text-center lg:text-left lg:items-end items-center gap-6"> <div className="flex flex-col lg:flex-row lg:justify-between justify-center text-center lg:text-left lg:items-end items-center gap-6">
<div className="flex flex-col justify-end w-full"> <div className="flex flex-col justify-end w-full">
{localization === "ru" ? ( <div className="flex flex-col md:gap-y-[10px] gap-2">
<div className="flex flex-col md:gap-y-[10px] gap-2"> <p className="text-[12px] leading-[130%]">
{footerInfo.slice(0, 4).map((item, i) => ( {t("footer.address")}
<p </p>
className={clsx("text-[12px] leading-[130%]", { <p className="text-[12px] leading-[130%]">
"ml-8": i === 2, {t("footer.phone")}
})} </p>
key={i} <p className="text-[12px] leading-[130%] ml-8">
> {t("footer.phone2")}
{item} </p>
</p> <p className="text-[12px] leading-[130%]">
))} {t("footer.email")}
</div> </p>
) : localization === "en" ? ( </div>
<div className="flex flex-col md:gap-y-[10px] gap-0">
{footerInfo.slice(3, 7).map((item, i) => (
<p className="text-[12px] leading-[130%]" key={i}>
{item}
</p>
))}
</div>
) : null}
</div> </div>
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">

View File

@ -1,10 +1,10 @@
"use client"; "use client";
import React, { Suspense, useEffect, useState } from "react"; import React, { Suspense, useState } from "react";
import clsx from "clsx"; import clsx from "clsx";
import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { AnimatePresence } from "framer-motion"; import { AnimatePresence } from "framer-motion";
import { useTranslations } from "next-intl";
import logo from "@/public/assets/icons/logo.svg"; import logo from "@/public/assets/icons/logo.svg";
import search from "@/public/assets/icons/header/search.svg"; import search from "@/public/assets/icons/header/search.svg";
@ -17,14 +17,14 @@ import { BurgerMenu } from "../ui/burger-menu";
import { useAppDispatch, useAppSelector } from "@/redux/hooks"; import { useAppDispatch, useAppSelector } from "@/redux/hooks";
import { selectHeader, setShowInput } from "@/redux/slices/headerSlice"; import { selectHeader, setShowInput } from "@/redux/slices/headerSlice";
import { selectBurger, setBurgerOpen } from "@/redux/slices/burgerSlice"; import { selectBurger, setBurgerOpen } from "@/redux/slices/burgerSlice";
import { useStorage } from "@/hooks/useStorage"; import { Link } from "@/i18n/navigation";
export const Header = () => { export const Header = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { showInput } = useAppSelector(selectHeader); const { showInput } = useAppSelector(selectHeader);
const { burgerOpen } = useAppSelector(selectBurger); const { burgerOpen } = useAppSelector(selectBurger);
const { activeLang } = useAppSelector(selectHeader);
const [activeLink, setActiveLink] = useState(""); const [activeLink, setActiveLink] = useState("");
const t = useTranslations();
const toggleMenu = () => { const toggleMenu = () => {
dispatch(setBurgerOpen(!burgerOpen)); dispatch(setBurgerOpen(!burgerOpen));
@ -36,12 +36,6 @@ export const Header = () => {
dispatch(setBurgerOpen(false)); dispatch(setBurgerOpen(false));
}; };
const { setItem } = useStorage("language");
useEffect(() => {
setItem(activeLang);
}, [activeLang]);
return ( return (
<> <>
<AnimatePresence> <AnimatePresence>
@ -52,23 +46,17 @@ export const Header = () => {
)} )}
</AnimatePresence> </AnimatePresence>
{/* Mobile */}
{/* <div className="h-[74px] tab:hidden"> */}
<div className="h-[70px]"> <div className="h-[70px]">
<header <header
className={clsx( className={clsx(
"bg-[#EEF1F0] border-b z-30 border-[#DFE2E1] tab:hidden fixed top-0 left-0 right-0 flex items-center justify-between px-4 py-6", "bg-[#EEF1F0] border-b z-30 border-[#DFE2E1] tab:hidden fixed top-0 left-0 right-0 flex items-center justify-between px-4 py-6"
{
// 'fixed w-full top-0': burgerOpen,
}
)} )}
> >
<Image <Image
src={searchMob} src={searchMob}
height={32} height={32}
width={32} width={32}
alt="поиск" alt="search"
className="cursor-pointer" className="cursor-pointer"
onClick={onSearch} onClick={onSearch}
/> />
@ -84,7 +72,7 @@ export const Header = () => {
src={logo} src={logo}
height={24} height={24}
width={160} width={160}
alt="лого" alt="logo"
className="cursor-pointer" className="cursor-pointer"
/> />
</Link> </Link>
@ -121,11 +109,7 @@ export const Header = () => {
<AnimatePresence>{burgerOpen && <BurgerMenu />}</AnimatePresence> <AnimatePresence>{burgerOpen && <BurgerMenu />}</AnimatePresence>
</header> </header>
{/* </div> */}
{/* Desktop */}
{/* <div className="hidden h-[74px] tab:block"> */}
<header className="hidden border-b border-[#DFE2E1] fixed top-0 left-0 right-0 z-30 tab:flex flex-col"> <header className="hidden border-b border-[#DFE2E1] fixed top-0 left-0 right-0 z-30 tab:flex flex-col">
<div className="bg-[#EEF1F0] text-black"> <div className="bg-[#EEF1F0] text-black">
<div className="container py-[17px] flex items-center justify-between"> <div className="container py-[17px] flex items-center justify-between">
@ -139,7 +123,7 @@ export const Header = () => {
</Suspense> </Suspense>
<Image <Image
src={search} src={search}
alt="поиск" alt="search"
onClick={() => dispatch(setShowInput(true))} onClick={() => dispatch(setShowInput(true))}
className="cursor-pointer" className="cursor-pointer"
/> />
@ -147,25 +131,20 @@ export const Header = () => {
</div> </div>
<nav className="flex items-center gap-x-5 font-medium"> <nav className="flex items-center gap-x-5 font-medium">
{headerMenu2 {headerMenu2.map((item, i) => (
.filter((item) => <Link
activeLang.localization === "en" ? item.en : !item.en key={i}
) href={item.link}
.map((item, i) => ( onClick={() => setActiveLink(item.link)}
<Link >
key={i} {t(item.titleKey)}
href={item.link} </Link>
onClick={() => setActiveLink(item.link)} ))}
>
{item.title}
</Link>
))}
</nav> </nav>
</div> </div>
</div> </div>
</header> </header>
</div> </div>
{/* </div> */}
</> </>
); );
}; };

View File

@ -7,8 +7,6 @@ import { Navigation } from "swiper/modules";
import { NewsCard } from "./news-card"; import { NewsCard } from "./news-card";
import { NavBtn } from "../home/ui/NavBtn"; import { NavBtn } from "../home/ui/NavBtn";
import { useAppSelector } from "@/redux/hooks";
import { selectHeader } from "@/redux/slices/headerSlice";
import { NewsData } from "@/lib/types/NewsData.type"; import { NewsData } from "@/lib/types/NewsData.type";
import "swiper/css/bundle"; import "swiper/css/bundle";
@ -17,17 +15,18 @@ import "swiper/css";
import "swiper/css/navigation"; import "swiper/css/navigation";
import "swiper/css/pagination"; import "swiper/css/pagination";
import "swiper/css/scrollbar"; import "swiper/css/scrollbar";
import Link from "next/link"; import { useTranslations, useLocale } from "next-intl";
import { useLang } from "@/utils/useLang";
import { GreenBtn } from "../ui/Buttons"; import { GreenBtn } from "../ui/Buttons";
import { Title } from "../ui/title"; import { Title } from "../ui/title";
import Loader from "@/components/ui/Loader"; import Loader from "@/components/ui/Loader";
import { Link } from "@/i18n/navigation";
export const News = () => { export const News = () => {
const [newsData, setNewsData] = useState<NewsData>(); const [newsData, setNewsData] = useState<NewsData>();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const { activeLang } = useAppSelector(selectHeader); const t = useTranslations("home");
const locale = useLocale();
const fetchNews = async () => { const fetchNews = async () => {
try { try {
@ -35,7 +34,7 @@ export const News = () => {
const response = await fetch(`https://turkmenexpo.com/app/api/v1/news`, { const response = await fetch(`https://turkmenexpo.com/app/api/v1/news`, {
next: { revalidate: 1800 }, next: { revalidate: 1800 },
headers: { headers: {
"Accept-Language": activeLang.localization, "Accept-Language": locale,
}, },
}); });
@ -54,7 +53,7 @@ export const News = () => {
useEffect(() => { useEffect(() => {
fetchNews(); fetchNews();
}, [activeLang.localization]); }, [locale]);
if (loading) { if (loading) {
<Loader className="h-[300px] w-full mx-auto" />; <Loader className="h-[300px] w-full mx-auto" />;
@ -64,7 +63,7 @@ export const News = () => {
<section> <section>
<div className="container w-full"> <div className="container w-full">
<header className="flex items-center mb-5 sm:mb-[43px] justify-between"> <header className="flex items-center mb-5 sm:mb-[43px] justify-between">
<Title text={useLang("News", "Новости", activeLang.localization)} /> <Title text={t("news")} />
<div className="hidden sm:flex items-center gap-5"> <div className="hidden sm:flex items-center gap-5">
<NavBtn left /> <NavBtn left />
<NavBtn /> <NavBtn />
@ -105,9 +104,7 @@ export const News = () => {
</div> </div>
<Link href={"/news"} className="hidden sm:flex justify-center"> <Link href={"/news"} className="hidden sm:flex justify-center">
<GreenBtn <GreenBtn text={t("allNews")} />
text={useLang("All news", "Все новости", activeLang.localization)}
/>
</Link> </Link>
</div> </div>
</section> </section>

View File

@ -5,22 +5,21 @@ import Image from "next/image";
import { Swiper, SwiperSlide } from "swiper/react"; import { Swiper, SwiperSlide } from "swiper/react";
import { Autoplay, Pagination } from "swiper/modules"; import { Autoplay, Pagination } from "swiper/modules";
import { useAppSelector } from "@/redux/hooks";
import { selectHeader } from "@/redux/slices/headerSlice";
import { PartnersType } from "@/lib/types/PartnersData.type"; import { PartnersType } from "@/lib/types/PartnersData.type";
import { baseAPI } from "@/lib/API"; import { baseAPI } from "@/lib/API";
import { useLang } from "@/utils/useLang"; import { useTranslations, useLocale } from "next-intl";
import { Title } from "../ui/title"; import { Title } from "../ui/title";
export const Partners = () => { export const Partners = () => {
const { activeLang } = useAppSelector(selectHeader); const locale = useLocale();
const t = useTranslations("home");
const [partnersData, setPartnersData] = useState<PartnersType>(); const [partnersData, setPartnersData] = useState<PartnersType>();
const fetchPartners = async () => { const fetchPartners = async () => {
try { try {
const res = await fetch(`${baseAPI}partners`, { const res = await fetch(`${baseAPI}partners`, {
headers: { headers: {
"Accept-Language": activeLang.localization, "Accept-Language": locale,
}, },
}); });
@ -34,14 +33,12 @@ export const Partners = () => {
useEffect(() => { useEffect(() => {
fetchPartners(); fetchPartners();
}, [activeLang.localization]); }, [locale]);
return ( return (
<section className="container"> <section className="container">
<div className="mb-[40px]"> <div className="mb-[40px]">
<Title <Title text={t("partners")} />
text={useLang("Partners", "Партнёры", activeLang.localization)}
/>
</div> </div>
<div className="flex items-center"> <div className="flex items-center">

View File

@ -7,31 +7,21 @@ import "swiper/css";
import "swiper/css/pagination"; import "swiper/css/pagination";
import { useFetch } from "@/hooks/useFetch"; import { useFetch } from "@/hooks/useFetch";
import { ReviewsType } from "@/lib/types/Reviews.type"; import { ReviewsType } from "@/lib/types/Reviews.type";
import { useAppSelector } from "@/redux/hooks"; import { useTranslations } from "next-intl";
import { selectHeader } from "@/redux/slices/headerSlice";
import { Title } from "../ui/title"; import { Title } from "../ui/title";
import Image from "next/image"; import Image from "next/image";
export const Reviews = () => { export const Reviews = () => {
const { data } = useFetch<ReviewsType>("reviews"); const { data } = useFetch<ReviewsType>("reviews");
const t = useTranslations("home");
const {
activeLang: { localization },
} = useAppSelector(selectHeader);
const title = localization === "en" ? "Feedback" : "Обратная связь";
return ( return (
<section className="container my-20 pb-20 overflow-hidden"> <section className="container my-20 pb-20 overflow-hidden">
<Title text={title} className="!text-center mb-10" /> <Title text={t("feedback")} className="!text-center mb-10" />
<div className="max-w-[1200px] mx-auto px-4 md:px-0"> <div className="max-w-[1200px] mx-auto px-4 md:px-0">
<Swiper <Swiper
modules={[Autoplay, Pagination]} modules={[Autoplay, Pagination]}
// autoplay={{
// delay: 6000,
// disableOnInteraction: false,
// }}
style={{ overflow: "visible" }} style={{ overflow: "visible" }}
freeMode freeMode
speed={1000} speed={1000}
@ -87,7 +77,6 @@ export const Reviews = () => {
))} ))}
</Swiper> </Swiper>
{/* Кастомная пагинация */}
<div className="custom-pagination flex justify-center items-center mt-60"></div> <div className="custom-pagination flex justify-center items-center mt-60"></div>
</div> </div>
</section> </section>

View File

@ -14,7 +14,7 @@ import { useMediaQuery } from "usehooks-ts";
import { Autoplay, Pagination } from "swiper/modules"; import { Autoplay, Pagination } from "swiper/modules";
import Loader from "../ui/Loader"; import Loader from "../ui/Loader";
import { baseAPI } from "@/lib/API"; import { baseAPI } from "@/lib/API";
import Link from "next/link"; import { Link } from "@/i18n/navigation";
import clsx from "clsx"; import clsx from "clsx";
export const Slider = ({ lang }: { lang: string }) => { export const Slider = ({ lang }: { lang: string }) => {

View File

@ -1,28 +1,36 @@
"use client"; "use client";
import { FC, useState } from "react"; import { FC, useState } from "react";
import { Form, useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import clsx from "clsx"; import clsx from "clsx";
import { postContacts } from "@/services/contacts"; import { postContacts } from "@/services/contacts";
import { useTranslations } from "next-intl";
interface Props { interface Props {
className?: string; className?: string;
} }
const formSchema = z.object({ export type FormType = {
name: z.string().min(2, "Имя необходимо"), name: string;
email: z.string().email("Email необходим"), email: string;
phone: z.string().min(8, "Номер телефона необходим"), phone: string;
company: z.string().optional(), company?: string;
msg: z.string().min(5, "Сообщение необходимо"), msg: string;
}); };
export type FormType = z.infer<typeof formSchema>;
export const ContactsForm: FC<Props> = ({ className }) => { export const ContactsForm: FC<Props> = ({ className }) => {
const [status, setStatus] = useState(false); const [status, setStatus] = useState(false);
const t = useTranslations();
const formSchema = z.object({
name: z.string().min(2, t("validation.nameRequired")),
email: z.string().email(t("validation.emailRequired")),
phone: z.string().min(8, t("validation.phoneRequired")),
company: z.string().optional(),
msg: z.string().min(5, t("validation.messageRequired")),
});
const { register, handleSubmit, formState, reset } = useForm({ const { register, handleSubmit, formState, reset } = useForm({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
@ -50,13 +58,13 @@ export const ContactsForm: FC<Props> = ({ className }) => {
<div className={clsx("bg-PRIMARY rounded-[8px] py-8 px-6", className)}> <div className={clsx("bg-PRIMARY rounded-[8px] py-8 px-6", className)}>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<h2 className="h2 !text-white text-3xl font-medium lg:mb-8 mb-6"> <h2 className="h2 !text-white text-3xl font-medium lg:mb-8 mb-6">
Связаться с нами {t("contacts.contactUs")}
</h2> </h2>
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<div className="flex flex-col relative"> <div className="flex flex-col relative">
<label htmlFor="name" className="label"> <label htmlFor="name" className="label">
Имя {t("contacts.name")}
</label> </label>
<input <input
type="text" type="text"
@ -70,7 +78,7 @@ export const ContactsForm: FC<Props> = ({ className }) => {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 items-center"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 items-center">
<div className="flex flex-col relative"> <div className="flex flex-col relative">
<label htmlFor="email" className="label"> <label htmlFor="email" className="label">
E-mail {t("contacts.email")}
</label> </label>
<input <input
type="text" type="text"
@ -83,7 +91,7 @@ export const ContactsForm: FC<Props> = ({ className }) => {
<div className="flex flex-col relative"> <div className="flex flex-col relative">
<label htmlFor="phone" className="label"> <label htmlFor="phone" className="label">
Номер {t("contacts.phone")}
</label> </label>
<input <input
type="text" type="text"
@ -97,7 +105,7 @@ export const ContactsForm: FC<Props> = ({ className }) => {
<div className="flex flex-col relative"> <div className="flex flex-col relative">
<label htmlFor="company" className="label"> <label htmlFor="company" className="label">
Название компании {t("contacts.companyName")}
</label> </label>
<input <input
type="text" type="text"
@ -110,7 +118,7 @@ export const ContactsForm: FC<Props> = ({ className }) => {
<div className="flex flex-col relative"> <div className="flex flex-col relative">
<label htmlFor="msg" className="label"> <label htmlFor="msg" className="label">
Сообщение {t("contacts.message")}
</label> </label>
<textarea <textarea
rows={3} rows={3}
@ -124,7 +132,7 @@ export const ContactsForm: FC<Props> = ({ className }) => {
disabled={formState.isSubmitting || status} disabled={formState.isSubmitting || status}
className="bg-[#A4FFF3] text-sm text-ON_PRIMARY_CONTAINER h-10 rounded-[2px]" className="bg-[#A4FFF3] text-sm text-ON_PRIMARY_CONTAINER h-10 rounded-[2px]"
> >
{!status ? "Отправить" : "Форма отправлена"} {!status ? t("common.send") : t("common.formSubmitted")}
</button> </button>
</div> </div>
</form> </form>

View File

@ -10,9 +10,8 @@ import {
Organizer, Organizer,
Timing, Timing,
} from "@/lib/types/Calendar.type"; } from "@/lib/types/Calendar.type";
import { useAppSelector } from "@/redux/hooks"; import { useTranslations } from "next-intl";
import { useLang } from "@/utils/useLang"; import { Link } from "@/i18n/navigation";
import Link from "next/link";
interface Props { interface Props {
id: number; id: number;
@ -54,9 +53,7 @@ export const EventCard = ({
return formattedDate; return formattedDate;
}; };
const lang = useAppSelector( const t = useTranslations("events");
(state) => state.headerSlice.activeLang.localization
);
return ( return (
<Link className="w-full cursor-default" href={`/calendar/${id}`}> <Link className="w-full cursor-default" href={`/calendar/${id}`}>
@ -97,7 +94,6 @@ export const EventCard = ({
className="lg:size-[228px] size-[100px] object-cover rounded-l-[4px]" className="lg:size-[228px] size-[100px] object-cover rounded-l-[4px]"
/> />
{/* CENTER */}
<div className="flex flex-col gap-[10px] md:gap-4 max-w-[683px] py-6 w-full"> <div className="flex flex-col gap-[10px] md:gap-4 max-w-[683px] py-6 w-full">
<p className="text-[12px] text-[#6B7674] font-normal leading-none"> <p className="text-[12px] text-[#6B7674] font-normal leading-none">
{category} {category}
@ -110,13 +106,10 @@ export const EventCard = ({
> >
{title} {title}
</h3> </h3>
{/* <p className={clsx('text-base line-clamp-2 leading-[130%] md:leading-[150%]', {})}>
{description}
</p> */}
</div> </div>
<div className="flex flex-col gap-[10px]"> <div className="flex flex-col gap-[10px]">
<p className="text-[#6B7674] uppercase font-normal leading-none text-[10px]"> <p className="text-[#6B7674] uppercase font-normal leading-none text-[10px]">
{useLang("Organiser:", "Организатор:", lang)} {t("organiserLabel")}
</p> </p>
<p className="text-[#6B7674] font-normal text-[13px] leading-[125%]"> <p className="text-[#6B7674] font-normal text-[13px] leading-[125%]">
{organizers ? organizers[0]?.name : null} {organizers ? organizers[0]?.name : null}
@ -125,7 +118,7 @@ export const EventCard = ({
{coorganizers && coorganizers?.length > 0 && ( {coorganizers && coorganizers?.length > 0 && (
<div className="flex flex-col gap-[10px]"> <div className="flex flex-col gap-[10px]">
<p className="text-[#6B7674] uppercase font-normal leading-none text-[10px]"> <p className="text-[#6B7674] uppercase font-normal leading-none text-[10px]">
{useLang("Co-organiser:", "Со-организатор:", lang)} {t("coOrganiserLabel")}
</p> </p>
<p className="text-[#6B7674] font-normal text-[13px] leading-[125%]"> <p className="text-[#6B7674] font-normal text-[13px] leading-[125%]">
{coorganizers ? coorganizers[0]?.name : null} {coorganizers ? coorganizers[0]?.name : null}
@ -133,7 +126,6 @@ export const EventCard = ({
</div> </div>
)} )}
</div> </div>
{/* CENTER */}
<div className="hidden md:flex text-2xl flex-col text-white leading- none h-[228px] min-w-[245px] items-center justify-center bg-[#31A898] px-12 text-center"> <div className="hidden md:flex text-2xl flex-col text-white leading- none h-[228px] min-w-[245px] items-center justify-center bg-[#31A898] px-12 text-center">
<p className="">{formatDate(starts_at)}</p>{" "} <p className="">{formatDate(starts_at)}</p>{" "}

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import { useAppSelector } from "@/redux/hooks"; import { useTranslations } from "next-intl";
import Link from "next/link"; import { Link } from "@/i18n/navigation";
import { FC } from "react"; import { FC } from "react";
interface Props { interface Props {
@ -18,9 +18,7 @@ export const EventPageButtons: FC<Props> = ({
visit, visit,
details, details,
}) => { }) => {
const lang = useAppSelector( const t = useTranslations("events");
(state) => state.headerSlice.activeLang.localization
);
return ( return (
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
@ -28,19 +26,19 @@ export const EventPageButtons: FC<Props> = ({
<> <>
<Link href={register || ""} className="w-full" target="_blank"> <Link href={register || ""} className="w-full" target="_blank">
<button className="full-btn bg-PRIMARY py-2.5 text-white"> <button className="full-btn bg-PRIMARY py-2.5 text-white">
{lang === "en" ? "Registration" : "Зарегистрироваться"} {t("registration")}
</button> </button>
</Link> </Link>
<Link href={visit || ""} className="w-full" target="_blank"> <Link href={visit || ""} className="w-full" target="_blank">
<button className="full-btn bg-SECONDARY_CONTAINER py-2.5 text-ON_SECONDARY_CONTAINER"> <button className="full-btn bg-SECONDARY_CONTAINER py-2.5 text-ON_SECONDARY_CONTAINER">
{lang === "en" ? "Visit site" : "Посетить сайт"} {t("visitSite")}
</button> </button>
</Link> </Link>
</> </>
) : ( ) : (
<Link href={details || ""} className="w-full" target="_blank"> <Link href={details || ""} className="w-full" target="_blank">
<button className="full-btn w-full bg-PRIMARY py-2.5 text-white"> <button className="full-btn w-full bg-PRIMARY py-2.5 text-white">
{lang === "en" ? "More" : "Подробнее"} {t("more")}
</button> </button>
</Link> </Link>
)} )}

View File

@ -2,14 +2,12 @@
import "swiper/css"; import "swiper/css";
import "swiper/css/pagination"; import "swiper/css/pagination";
// import "./styles/events.css";
import { useLang } from "@/utils/useLang"; import { useTranslations } from "next-intl";
import { FC } from "react"; import { FC } from "react";
import { Swiper, SwiperSlide } from "swiper/react"; import { Swiper, SwiperSlide } from "swiper/react";
import { EventCard } from "./event-card"; import { EventCard } from "./event-card";
import { CalendarType } from "@/lib/types/Calendar.type"; import { CalendarType } from "@/lib/types/Calendar.type";
import { useAppSelector } from "@/redux/hooks";
interface Props { interface Props {
data: CalendarType["data"]; data: CalendarType["data"];
@ -17,17 +15,12 @@ interface Props {
} }
export const EventsMobile: FC<Props> = ({ data }) => { export const EventsMobile: FC<Props> = ({ data }) => {
const lang = useAppSelector( const t = useTranslations("home");
(state) => state.headerSlice.activeLang.localization
);
return ( return (
<div className="md:hidden container"> <div className="md:hidden container">
<h2 className="text-[26px] mb-5 sm:mb-10 font-semibold leading-[115%]"> <h2 className="text-[26px] mb-5 sm:mb-10 font-semibold leading-[115%]">
{useLang( {t("upcomingExhibitions")}
"Upcoming exhibitions and events",
"Ближайшие выставки и мероприятия",
lang
)}
</h2> </h2>
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex items-center gap-y-[10px]"> <div className="flex items-center gap-y-[10px]">

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import { Link } from '@/i18n/navigation';
interface Props { interface Props {
img: string; img: string;

View File

@ -1,10 +1,8 @@
"use client"; "use client";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import Image from "next/image";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useAppDispatch, useAppSelector } from "@/redux/hooks"; import { useAppDispatch, useAppSelector } from "@/redux/hooks";
import close from "@/public/assets/icons/home/close-input.svg";
import { selectInput, setInputStatus } from "@/redux/slices/inputSlice"; import { selectInput, setInputStatus } from "@/redux/slices/inputSlice";
import { v4 } from "uuid"; import { v4 } from "uuid";
@ -12,20 +10,14 @@ import { selectHeader, setShowInput } from "@/redux/slices/headerSlice";
import clsx from "clsx"; import clsx from "clsx";
import { selectBurger } from "@/redux/slices/burgerSlice"; import { selectBurger } from "@/redux/slices/burgerSlice";
import { baseAPI } from "@/lib/API"; import { baseAPI } from "@/lib/API";
import Link from "next/link";
import { SearchTypes } from "@/lib/types/Search.type"; import { SearchTypes } from "@/lib/types/Search.type";
import { useMediaQuery } from "usehooks-ts"; import { useMediaQuery } from "usehooks-ts";
import { useTranslations, useLocale } from "next-intl";
export const inputRadio = [ import { Link } from "@/i18n/navigation";
{ name: "Везде", id: "all" },
{ name: "В событиях", id: "events" },
{ name: "В новостях", id: "news" },
];
export const SearchInput = ({ mob = false }: { mob?: boolean }) => { export const SearchInput = ({ mob = false }: { mob?: boolean }) => {
const localization = useAppSelector( const locale = useLocale();
(state) => state.headerSlice.activeLang.localization const t = useTranslations("search");
);
const wrapper = document.querySelector(".wrapper"); const wrapper = document.querySelector(".wrapper");
const tab = useMediaQuery("(min-width: 980px)"); const tab = useMediaQuery("(min-width: 980px)");
@ -38,12 +30,17 @@ export const SearchInput = ({ mob = false }: { mob?: boolean }) => {
const { showInput } = useAppSelector(selectHeader); const { showInput } = useAppSelector(selectHeader);
const inputRadio = [
{ name: t("all"), id: "all" },
{ name: t("inEvents"), id: "events" },
{ name: t("inNews"), id: "news" },
];
useEffect(() => { useEffect(() => {
wrapper?.classList.remove("overflow-hidden"); wrapper?.classList.remove("overflow-hidden");
wrapper?.classList.add("overflow-hidden"); wrapper?.classList.add("overflow-hidden");
setStatus(inputRadio[0].id); setStatus(inputRadio[0].id);
inputRadio;
return () => { return () => {
wrapper?.classList.remove("overflow-hidden"); wrapper?.classList.remove("overflow-hidden");
@ -56,7 +53,7 @@ export const SearchInput = ({ mob = false }: { mob?: boolean }) => {
try { try {
const res = await fetch(`${baseAPI}search?search=${value}`, { const res = await fetch(`${baseAPI}search?search=${value}`, {
headers: { headers: {
"Accept-Language": localization, "Accept-Language": locale,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
}); });
@ -135,7 +132,7 @@ export const SearchInput = ({ mob = false }: { mob?: boolean }) => {
} }
value={value} value={value}
type="text" type="text"
placeholder="Что найти?" placeholder={t("placeholder")}
className="p-3 w-full leading-[150%] placeholder:leading-[150%] placeholder:text-gray focus:outline-none rounded-sm bg-transparent border-[1px] border-[#BCC4CC]" className="p-3 w-full leading-[150%] placeholder:leading-[150%] placeholder:text-gray focus:outline-none rounded-sm bg-transparent border-[1px] border-[#BCC4CC]"
/> />
</div> </div>
@ -167,7 +164,7 @@ export const SearchInput = ({ mob = false }: { mob?: boolean }) => {
</div> </div>
) : ( ) : (
<div className="bg-green py-[17px] px-[70px] rounded-sm"> <div className="bg-green py-[17px] px-[70px] rounded-sm">
Найти {t("findButton")}
</div> </div>
)} )}
</button> </button>
@ -177,11 +174,12 @@ export const SearchInput = ({ mob = false }: { mob?: boolean }) => {
searchData.data.posts.length ? ( searchData.data.posts.length ? (
<> <>
<div className="mb-12 font-light"> <div className="mb-12 font-light">
По запросу « <span className="font-bold">{searchValue}</span>{" "} {t("byRequest")}
» нашлось <span className="font-bold">{searchValue}</span>
{t("found")}{" "}
{searchData.data.expo_events.length + {searchData.data.expo_events.length +
searchData.data.posts.length}{" "} searchData.data.posts.length}{" "}
результатов {t("results")}
</div> </div>
<div className="flex flex-col gap-9 w-full tab:mb-0 mb-20"> <div className="flex flex-col gap-9 w-full tab:mb-0 mb-20">
{inputStatus === "all" || inputStatus === "events" {inputStatus === "all" || inputStatus === "events"
@ -191,7 +189,7 @@ export const SearchInput = ({ mob = false }: { mob?: boolean }) => {
{item.title} {item.title}
</h3> </h3>
<Link href={`/calendar/${item.id}`}> <Link href={`/calendar/${item.id}`}>
Перейти на страницу {t("goToPage")}
</Link> </Link>
<hr className="mt-9 border-OUTLINE_VAR" /> <hr className="mt-9 border-OUTLINE_VAR" />
</div> </div>
@ -204,7 +202,7 @@ export const SearchInput = ({ mob = false }: { mob?: boolean }) => {
{item.title} {item.title}
</h3> </h3>
<Link href={`/news/${item.id}`}> <Link href={`/news/${item.id}`}>
Перейти на страницу {t("goToPage")}
</Link> </Link>
<hr className="mt-9 border-OUTLINE_VAR" /> <hr className="mt-9 border-OUTLINE_VAR" />
</div> </div>
@ -213,7 +211,7 @@ export const SearchInput = ({ mob = false }: { mob?: boolean }) => {
</div> </div>
</> </>
) : ( ) : (
<h3>По вашему запросу ничего не найдено</h3> <h3>{t("noResults")}</h3>
) )
) : null} ) : null}
</div> </div>

View File

@ -4,13 +4,10 @@ import React from "react";
import Image from "next/image"; import Image from "next/image";
import { contactCardData, roadCardData } from "@/lib/database/homeInfoData"; import { contactCardData, roadCardData } from "@/lib/database/homeInfoData";
import { useLang } from "@/utils/useLang"; import { useTranslations } from "next-intl";
import { useAppSelector } from "@/redux/hooks";
export const Services = () => { export const Services = () => {
const localization = useAppSelector( const t = useTranslations();
(state) => state.headerSlice.activeLang.localization
);
return ( return (
<div <div
@ -18,7 +15,7 @@ export const Services = () => {
> >
<div className="flex flex-col items-start gap-x-5 w-full"> <div className="flex flex-col items-start gap-x-5 w-full">
<h3 className="text-[21px] leading-[100%] font-bold text-bgWhite md:text-[#B0E6A1] uppercase md:mb-10 mb-5"> <h3 className="text-[21px] leading-[100%] font-bold text-bgWhite md:text-[#B0E6A1] uppercase md:mb-10 mb-5">
{useLang("Routes", "как добраться", localization)} {t("services.routes")}
</h3> </h3>
<div className="flex flex-col gap-y-5 w-full"> <div className="flex flex-col gap-y-5 w-full">
{roadCardData.map((item, i) => ( {roadCardData.map((item, i) => (
@ -29,7 +26,7 @@ export const Services = () => {
<div className="flex items-center gap-10 md:gap-5"> <div className="flex items-center gap-10 md:gap-5">
<Image className="img-auto" src={item.icon} alt="icon" /> <Image className="img-auto" src={item.icon} alt="icon" />
<p className="text-[16px] leading-[120%] md:leading-[100%]"> <p className="text-[16px] leading-[120%] md:leading-[100%]">
{localization === "ru" ? item.text : item.enText} {t(item.textKey)}
</p> </p>
</div> </div>
</div> </div>
@ -38,7 +35,7 @@ export const Services = () => {
</div> </div>
<div className="flex flex-col items-start md:mt-0 mt-10 gap-x-5 w-full"> <div className="flex flex-col items-start md:mt-0 mt-10 gap-x-5 w-full">
<h3 className="text-[21px] leading-[100%] text-bgWhite md:text-[#FFD288] uppercase font-bold md:mb-10 mb-5"> <h3 className="text-[21px] leading-[100%] text-bgWhite md:text-[#FFD288] uppercase font-bold md:mb-10 mb-5">
{useLang("Contacts", "контакты", localization)} {t("contacts.title")}
</h3> </h3>
<div className="flex flex-col gap-y-5 w-full"> <div className="flex flex-col gap-y-5 w-full">
{contactCardData.map((item, i) => ( {contactCardData.map((item, i) => (
@ -49,7 +46,7 @@ export const Services = () => {
<div className="flex items-center md:gap-5 gap-10"> <div className="flex items-center md:gap-5 gap-10">
<Image src={item.icon} alt="icon" /> <Image src={item.icon} alt="icon" />
<p className="text-[16px] leading-[120%] md:leading-[100%]"> <p className="text-[16px] leading-[120%] md:leading-[100%]">
{localization === "ru" ? item.text : item.enText} {t(item.textKey)}
</p> </p>
</div> </div>
</div> </div>

View File

@ -3,9 +3,9 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Image from "next/image"; import Image from "next/image";
import { useMediaQuery } from "usehooks-ts"; import { useMediaQuery } from "usehooks-ts";
import { useAppSelector } from "@/redux/hooks";
import { useSliderBanner } from "@/hooks/use-slider"; import { useSliderBanner } from "@/hooks/use-slider";
import Loader from "../ui/Loader"; import Loader from "../ui/Loader";
import { useLocale } from "next-intl";
export const SliderClient = () => { export const SliderClient = () => {
const isTab = useMediaQuery("(min-width: 1024px)"); const isTab = useMediaQuery("(min-width: 1024px)");
@ -14,16 +14,14 @@ export const SliderClient = () => {
const [data, setData] = useState<any>(null); const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const bannerType = useSliderBanner(isTab, isMd); const bannerType = useSliderBanner(isTab, isMd);
const lang = useAppSelector( const locale = useLocale();
(state) => state.headerSlice.activeLang.localization
);
const fetchData = async () => { const fetchData = async () => {
try { try {
setLoading(true); setLoading(true);
const res = await fetch(`/api/banners?bannerType=${bannerType}`, { const res = await fetch(`/api/banners?bannerType=${bannerType}`, {
headers: { headers: {
"Accept-Language": lang, "Accept-Language": locale,
}, },
}); });
const json = await res.json(); const json = await res.json();
@ -36,7 +34,7 @@ export const SliderClient = () => {
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
}, [bannerType, lang]); }, [bannerType, locale]);
if (loading) return <Loader className="h-[600px] min-h-[320px]" />; if (loading) return <Loader className="h-[600px] min-h-[320px]" />;

View File

@ -6,51 +6,27 @@ import clsx from "clsx";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import triangle from "@/public/assets/icons/drop-icon.svg"; import triangle from "@/public/assets/icons/drop-icon.svg";
import { useAppSelector, useAppDispatch } from "@/redux/hooks"; import { useLocale } from "next-intl";
import { activeLangType, selectHeader } from "@/redux/slices/headerSlice"; import { useRouter, usePathname } from "@/i18n/navigation";
import { setActiveLang } from "@/redux/slices/headerSlice"; import { locales } from "@/i18n/config";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
export const lang = [ const langNames: Record<string, string> = {
{ ru: "Русский",
title: "Русский", en: "English",
localization: "ru", };
},
// {
// title: 'Tm',
// localization: 'tm',
// },
{
title: "English",
localization: "en",
},
];
export const LangMenu = () => { export const LangMenu = () => {
const { activeLang } = useAppSelector(selectHeader); const locale = useLocale();
const { refresh } = useRouter(); const router = useRouter();
const pathname = usePathname();
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
const dispatch = useAppDispatch();
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
const setLang = (lang: activeLangType) => {
// Устанавливаем cookie через document.cookie const switchLocale = (newLocale: string) => {
document.cookie = `lang=${lang.localization}; path=/; max-age=31536000`; router.replace(pathname, { locale: newLocale });
dispatch(setActiveLang(lang)); setActive(false);
refresh();
}; };
useEffect(() => {
const cookieLang = document.cookie
.split("; ")
.find((row) => row.startsWith("lang="))
?.split("=")[1];
if (cookieLang) {
const foundLang = lang.find((l) => l.localization === cookieLang);
if (foundLang) dispatch(setActiveLang(foundLang));
}
}, [dispatch]);
useEffect(() => { useEffect(() => {
const handleClick = (e: MouseEvent) => { const handleClick = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) { if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
@ -72,7 +48,7 @@ export const LangMenu = () => {
}} }}
> >
<div className="flex items-center px-[12px]"> <div className="flex items-center px-[12px]">
<p>{activeLang.title}</p> <p>{langNames[locale]}</p>
<Image <Image
src={triangle} src={triangle}
alt="arrow" alt="arrow"
@ -98,21 +74,15 @@ export const LangMenu = () => {
}} }}
className="absolute rounded-[4px] w-[112px] overflow-hidden custom-shadow z-10 flex flex-col top-[27px] bg-[#EEF1F0] transition-all duration-300" className="absolute rounded-[4px] w-[112px] overflow-hidden custom-shadow z-10 flex flex-col top-[27px] bg-[#EEF1F0] transition-all duration-300"
> >
{lang {locales
.filter((item) => item.title !== activeLang.title) .filter((l) => l !== locale)
.map((item, i) => ( .map((l, i) => (
<div <div
key={i} key={i}
onClick={() => setLang(item)} onClick={() => switchLocale(l)}
className={clsx( className="p-3 pr-[22px] py-4 text-extraSm transition-all hover:bg-ON_SECONDARY_CONTAINER/[8%]"
"p-3 pr-[22px] py-4 text-extraSm transition-all",
{
"hover:bg-ON_SECONDARY_CONTAINER/[8%]":
item.title === item.title,
}
)}
> >
{item.title} {langNames[l]}
</div> </div>
))} ))}
</motion.div> </motion.div>

View File

@ -1,6 +1,7 @@
"use client"; "use client";
import React, { Dispatch, SetStateAction } from "react"; import React, { Dispatch, SetStateAction } from "react";
import { useTranslations } from "next-intl";
interface IProps { interface IProps {
lastPage?: number; lastPage?: number;
@ -11,6 +12,8 @@ interface IProps {
} }
export const Pagination = ({ current, setCurrent, lastPage = 3 }: IProps) => { export const Pagination = ({ current, setCurrent, lastPage = 3 }: IProps) => {
const t = useTranslations("common");
const onNext = () => { const onNext = () => {
setCurrent(current + 1); setCurrent(current + 1);
}; };
@ -39,7 +42,9 @@ export const Pagination = ({ current, setCurrent, lastPage = 3 }: IProps) => {
<div className="border-[1px] border-OUTLINE_VAR rounded-sm px-3 py-[9px]"> <div className="border-[1px] border-OUTLINE_VAR rounded-sm px-3 py-[9px]">
{current} {current}
</div> </div>
<p>из {lastPage}</p> <p>
{t("of")} {lastPage}
</p>
<button onClick={onNext} disabled={current >= lastPage} type="button"> <button onClick={onNext} disabled={current >= lastPage} type="button">
<svg <svg
width="32" width="32"

View File

@ -1,19 +1,15 @@
"use client"; "use client";
import React from "react"; import React from "react";
import Link from "next/link";
import clsx from "clsx"; import clsx from "clsx";
import { usePathname } from "next/navigation"; import { useTranslations } from "next-intl";
import { sidebarData } from "@/lib/database/pathnames"; import { sidebarData } from "@/lib/database/pathnames";
import { useAppSelector } from "@/redux/hooks"; import { Link, usePathname } from "@/i18n/navigation";
export const Sidebar = () => { export const Sidebar = () => {
const pathname = usePathname(); const pathname = usePathname();
const t = useTranslations();
const lang = useAppSelector(
(state) => state.headerSlice.activeLang.localization
);
return ( return (
<div className="flex flex-col bg-white rounded-lg px-6 py-4 items-start gap-y-[12px] pb-5 mt-12 sticky top-0 left-0 overflow-hidden"> <div className="flex flex-col bg-white rounded-lg px-6 py-4 items-start gap-y-[12px] pb-5 mt-12 sticky top-0 left-0 overflow-hidden">
@ -31,7 +27,7 @@ export const Sidebar = () => {
<p <p
className={clsx("mb-[16px] text-[16px] font-bold leading-[1.5]")} className={clsx("mb-[16px] text-[16px] font-bold leading-[1.5]")}
> >
{lang === "ru" ? item.pathname : item.pathnameEn} {t(item.pathnameKey)}
</p> </p>
<div className="flex flex-col items-start gap-y-[8px]"> <div className="flex flex-col items-start gap-y-[8px]">
<div className="flex flex-col gap-[10px] pl-4 pr-10"> <div className="flex flex-col gap-[10px] pl-4 pr-10">
@ -47,7 +43,7 @@ export const Sidebar = () => {
)} )}
key={i} key={i}
> >
{lang === "ru" ? obj.title : obj.titleEn} {t(obj.titleKey)}
</Link> </Link>
))} ))}
</div> </div>

View File

@ -1,9 +1,7 @@
"use client"; "use client";
import { useAppSelector } from "@/redux/hooks"; import { useTranslations } from "next-intl";
import { selectHeader } from "@/redux/slices/headerSlice"; import { Link } from "@/i18n/navigation";
import { useLang } from "@/utils/useLang";
import Link from "next/link";
export const BreadCrumbs = ({ export const BreadCrumbs = ({
second, second,
@ -18,12 +16,12 @@ export const BreadCrumbs = ({
path2?: string; path2?: string;
cursor?: boolean; cursor?: boolean;
}) => { }) => {
const { activeLang } = useAppSelector(selectHeader); const t = useTranslations("common");
return ( return (
<div className="text-[12px] text-[#6B7674] flex items-center mob:mb-6 mb-5"> <div className="text-[12px] text-[#6B7674] flex items-center mob:mb-6 mb-5">
<Link href={"/"}> <Link href={"/"}>
{useLang("Home", "Главная", activeLang.localization)} {t("home")}
</Link> </Link>
<p className="px-1">/</p> <p className="px-1">/</p>

View File

@ -1,11 +1,12 @@
"use client"; "use client";
import { burgerMenuData } from "@/lib/database/pathnames"; import { burgerMenuData } from "@/lib/database/pathnames";
import { useAppDispatch, useAppSelector } from "@/redux/hooks"; import { useAppDispatch } from "@/redux/hooks";
import Link from "next/link"; import { Link } from "@/i18n/navigation";
import React from "react"; import React from "react";
import Image from "next/image"; import Image from "next/image";
import { v4 } from "uuid"; import { v4 } from "uuid";
import { useTranslations } from "next-intl";
import arrow from "@/public/assets/icons/header/burger-arrow.svg"; import arrow from "@/public/assets/icons/header/burger-arrow.svg";
import { import {
@ -22,6 +23,7 @@ export const BurgerDrop = ({
setDrop: (name: boolean) => void; setDrop: (name: boolean) => void;
}) => { }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const t = useTranslations();
return burgerMenuData return burgerMenuData
.filter((obj) => !obj.only) .filter((obj) => !obj.only)
@ -38,7 +40,7 @@ export const BurgerDrop = ({
className="cursor-pointer flex items-center gap-[10px] mb-[10px]" className="cursor-pointer flex items-center gap-[10px] mb-[10px]"
> >
<Image src={arrow} alt="стрелка" className="rotate-180" /> <Image src={arrow} alt="стрелка" className="rotate-180" />
<h3 className="leading-[135%] text-[18px]">{item.title}</h3> <h3 className="leading-[135%] text-[18px]">{t(item.titleKey)}</h3>
</div> </div>
<hr className="border-bgWhite mb-5" /> <hr className="border-bgWhite mb-5" />
@ -53,7 +55,7 @@ export const BurgerDrop = ({
setDrop(false); setDrop(false);
}} }}
> >
{subj.title} {t(subj.titleKey)}
</Link> </Link>
))} ))}
</div> </div>

View File

@ -1,88 +1,43 @@
"use client"; "use client";
import Link from "next/link";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { v4 } from "uuid"; import { v4 } from "uuid";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useLocale, useTranslations } from "next-intl";
import { useAppDispatch, useAppSelector } from "@/redux/hooks"; import { useAppDispatch } from "@/redux/hooks";
import { setBurgerOpen } from "@/redux/slices/burgerSlice"; import { setBurgerOpen } from "@/redux/slices/burgerSlice";
import { activeLangType, setActiveLang } from "@/redux/slices/headerSlice";
import clsx from "clsx"; import clsx from "clsx";
import { burgerMenu, burgerMenu2 } from "@/lib/database/header"; import { burgerMenu, burgerMenu2 } from "@/lib/database/header";
import { useRouter } from "next/navigation"; import { useRouter, usePathname } from "@/i18n/navigation";
import { lang } from "./LangMenu"; import { Link } from "@/i18n/navigation";
import { locales } from "@/i18n/config";
interface flagTypes { const burgerLangs = [
// title: 'Ру' | 'En' | 'Tm'; { title: "Ру", localization: "ru" as const },
title: "Ру" | "En"; { title: "En", localization: "en" as const },
localization: "ru" | "en";
// localization: 'ru' | 'en' | 'tm';
}
const burgerLangs: flagTypes[] = [
// {
// title: 'Tm',
// localization: 'tm',
// },
{
title: "Ру",
localization: "ru",
},
{
title: "En",
localization: "en",
},
]; ];
export const BurgerMenu = () => { export const BurgerMenu = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const wrapper = document.querySelector(".wrapper"); const locale = useLocale();
const { refresh } = useRouter(); const t = useTranslations();
const router = useRouter();
const localization = useAppSelector( const pathname = usePathname();
(state) => state.headerSlice.activeLang.localization
);
const [activeMenu, setActiveMenu] = useState<string>(""); const [activeMenu, setActiveMenu] = useState<string>("");
const [activeMenu2, setActiveMenu2] = useState<string>(""); const [activeMenu2, setActiveMenu2] = useState<string>("");
const setActiveTitle = () => { const setActiveTitle = () => {
if (activeMenu.includes("/ser")) if (activeMenu.includes("/ser")) return t("nav.services");
return (
(localization === "ru" && "Услуги") ||
(localization === "en" && "Services")
);
}; };
const setActiveTitle2 = () => { const setActiveTitle2 = () => {
if (activeMenu2.includes("/company")) if (activeMenu2.includes("/company")) return t("nav.about");
return (
(localization === "ru" && "О компании") ||
(localization === "en" && "About company")
);
};
const setLang = (lang: activeLangType) => {
// Устанавливаем cookie через document.cookie
document.cookie = `lang=${lang.localization}; path=/; max-age=31536000`;
dispatch(setActiveLang(lang));
refresh();
}; };
useEffect(() => { useEffect(() => {
const cookieLang = document.cookie const wrapper = document.querySelector(".wrapper");
.split("; ")
.find((row) => row.startsWith("lang="))
?.split("=")[1];
if (cookieLang) {
const foundLang = lang.find((l) => l.localization === cookieLang);
if (foundLang) dispatch(setActiveLang(foundLang));
}
}, [dispatch]);
useEffect(() => {
wrapper?.classList.remove("overflow-hidden"); wrapper?.classList.remove("overflow-hidden");
wrapper?.classList.add("overflow-hidden"); wrapper?.classList.add("overflow-hidden");
@ -145,7 +100,7 @@ export const BurgerMenu = () => {
onClick={() => dispatch(setBurgerOpen(false))} onClick={() => dispatch(setBurgerOpen(false))}
href={obj.link} href={obj.link}
> >
{localization === "en" ? obj.titleEn : obj.title} {t(obj.titleKey)}
</Link> </Link>
)) ))
)} )}
@ -163,8 +118,7 @@ export const BurgerMenu = () => {
}} }}
href={item.link} href={item.link}
> >
{(localization === "en" && item.titleEn) || {t(item.titleKey)}
(localization === "ru" && item.title)}
</Link> </Link>
) : ( ) : (
<div <div
@ -174,8 +128,7 @@ export const BurgerMenu = () => {
setActiveMenu(item.link); setActiveMenu(item.link);
}} }}
> >
{(localization === "en" && item.titleEn) || {t(item.titleKey)}
(localization === "ru" && item.title)}
{item.drop && ( {item.drop && (
<svg <svg
width="24" width="24"
@ -234,8 +187,7 @@ export const BurgerMenu = () => {
dispatch(setBurgerOpen(false)); dispatch(setBurgerOpen(false));
}} }}
> >
{(localization === "en" && item.titleEn) || {t(item.titleKey)}
(localization === "ru" && item.title)}
</Link> </Link>
)) ))
)} )}
@ -246,9 +198,8 @@ export const BurgerMenu = () => {
<div <div
key={i} key={i}
onClick={() => { onClick={() => {
dispatch(setActiveLang(item)); router.replace(pathname, { locale: item.localization });
dispatch(setBurgerOpen(false)); dispatch(setBurgerOpen(false));
setLang(item);
}} }}
className="flex cursor-pointer items-center gap-[10px]" className="flex cursor-pointer items-center gap-[10px]"
> >

View File

@ -1,8 +1,7 @@
"use client"; "use client";
import { baseAPI } from "@/lib/API"; import { baseAPI } from "@/lib/API";
import { useAppSelector } from "@/redux/hooks"; import { useLocale } from "next-intl";
import { selectHeader } from "@/redux/slices/headerSlice";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
interface FetchState<T> { interface FetchState<T> {
@ -21,9 +20,7 @@ export const useFetch = <T>(
error: null, error: null,
}); });
const { const locale = useLocale();
activeLang: { localization },
} = useAppSelector(selectHeader);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
@ -32,7 +29,7 @@ export const useFetch = <T>(
const response = await fetch(baseAPI + url, { const response = await fetch(baseAPI + url, {
headers: { headers: {
"Accept-Language": localization, "Accept-Language": locale,
}, },
}); });
@ -57,13 +54,7 @@ export const useFetch = <T>(
}; };
fetchData(); fetchData();
}, [url, localization]); // Добавьте options в зависимости, если они динамические }, [url, locale]);
return state; return state;
}; };
// Пример использования
interface User {
id: number;
name: string;
}

3
i18n/config.ts Normal file
View File

@ -0,0 +1,3 @@
export const locales = ["ru", "en"] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = "ru";

7
i18n/navigation.ts Normal file
View File

@ -0,0 +1,7 @@
import { createNavigation } from "next-intl/navigation";
import { locales, defaultLocale } from "./config";
export const { Link, redirect, usePathname, useRouter } = createNavigation({
locales,
defaultLocale,
});

15
i18n/request.ts Normal file
View File

@ -0,0 +1,15 @@
import { getRequestConfig } from "next-intl/server";
import { locales, type Locale } from "./config";
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
if (!locale || !locales.includes(locale as Locale)) {
locale = "ru";
}
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
});

View File

@ -1,123 +1,82 @@
export const burgerMenu = [ interface BurgerMenuItem {
titleKey: string;
link: string;
services?: boolean;
drop?: boolean;
dropDown?: {
titleKey: string;
link: string;
}[];
}
export const burgerMenu: BurgerMenuItem[] = [
{ {
title: "Календарь мероприятий", titleKey: "nav.calendar",
titleEn: "Calendar of events",
link: "/calendar", link: "/calendar",
}, },
{ {
services: true, services: true,
title: "Услуги", titleKey: "nav.services",
drop: true, drop: true,
titleEn: "Services",
link: "/services/organization", link: "/services/organization",
dropDown: [ dropDown: [
{ { titleKey: "sidebar.organizeExhibitions", link: "/services/organization" },
title: "Организация выставок", { titleKey: "sidebar.b2bMeetings", link: "/services/meetings" },
titleEn: "Organization of exhibitions", { titleKey: "sidebar.businessMissions", link: "/services/missions" },
link: "/services/organization", { titleKey: "burger.advertisingActivity", link: "/services/advertising" },
}, { titleKey: "sidebar.hybridEvents", link: "/services/hybrid-organizations" },
{ { titleKey: "sidebar.certification", link: "/services/certification" },
title: "B2B Встречи и Матчмейкинг", { titleKey: "burger.freightForwarding", link: "/services/forwarding" },
titleEn: "B2B Meetings & Business Matchmaking", { titleKey: "burger.accountingOutsourcing", link: "/services/outsourcing" },
link: "/services/meetings", { titleKey: "burger.personnelTraining", link: "/services/personnel-training" },
}, { titleKey: "sidebar.businessTours", link: "/services/business-tours" },
{
title: "Бизнес-Миссии",
titleEn: "Business Missions",
link: "/services/missions",
},
{
title: "Рекламная деятельность",
titleEn: "Advertising activity",
link: "/services/advertising",
},
{
title: "Организация гибридных мероприятий",
titleEn: "Organization of hybrid events",
link: "/services/hybrid-organizations",
},
{
title: "Сертификация",
titleEn: "Certification",
link: "/services/certification",
},
{
title: "Экспедирование грузов и таможенное оформление",
titleEn: "Freight forwarding and customs clearance",
link: "/services/forwarding",
},
{
title: "Аутсорсинг бух.учета и аудита",
titleEn: "Outsourcing of accounting and auditing",
link: "/services/outsourcing",
},
{
title: "Обучения персонала",
titleEn: "Personnel-training",
link: "/services/personnel-training",
},
{
title: "Бизнес - туры",
titleEn: "Business - tours",
link: "/services/business-tours",
},
], ],
}, },
{ {
title: "О компании", titleKey: "nav.about",
titleEn: "Calendar of events",
link: "/company/aboutus", link: "/company/aboutus",
}, },
{ {
title: "Новости", titleKey: "nav.news",
titleEn: "News",
link: "/news", link: "/news",
}, },
{ {
title: "Контакты", titleKey: "nav.contacts",
titleEn: "Contacts",
link: "/contacts", link: "/contacts",
}, },
// { en: true, title: "Exhibition", link: "/exhibition-about" },
// { en: true, title: "Participants", link: "/participants-info" },
// { en: true, title: "For visitors", link: "/visitors-info" },
]; ];
export const burgerMenu2 = [ interface BurgerMenu2Item {
titleKey: string;
link: string;
company?: boolean;
drop?: boolean;
dropDown?: {
titleKey: string;
link: string;
}[];
}
export const burgerMenu2: BurgerMenu2Item[] = [
{ {
company: true, company: true,
title: "О компании", titleKey: "nav.about",
titleEn: "About company",
link: "/company/aboutus", link: "/company/aboutus",
drop: true, drop: true,
dropDown: [ dropDown: [
{ { titleKey: "burger.aboutUs", link: "/company/aboutus" },
title: "О нас",
titleEn: "About us",
link: "/company/aboutus",
},
], ],
}, },
{ {
title: "Новости", titleKey: "nav.news",
titleEn: "News",
link: "/news", link: "/news",
}, },
{ {
title: "FAQ", titleKey: "nav.faq",
titleEn: "FAQ",
link: "/faq", link: "/faq",
}, },
{ {
title: "Контакты", titleKey: "nav.contacts",
titleEn: "Контакты",
link: "/contacts", link: "/contacts",
}, },
]; ];

View File

@ -1,56 +1,36 @@
import car from '@/public/assets/icons/service/car.svg'; import car from "@/public/assets/icons/service/car.svg";
import bus from '@/public/assets/icons/service/bus.svg'; import bus from "@/public/assets/icons/service/bus.svg";
import track from '@/public/assets/icons/service/track.svg'; import track from "@/public/assets/icons/service/track.svg";
import { RoadCardProps } from '../types'; import { StaticImageData } from "next/image";
export interface RoadCardProps {
icon: StaticImageData;
textKey: string;
}
export const roadCardData: RoadCardProps[] = [ export const roadCardData: RoadCardProps[] = [
{ { icon: car, textKey: "services.byCar" },
icon: car, { icon: bus, textKey: "services.byBus" },
text: 'На машине', { icon: track, textKey: "services.freight" },
enText: 'Car',
},
{
icon: bus,
text: 'На автобусе',
enText: 'Public transport',
},
{
icon: track,
text: 'Freight transport',
enText: 'Car',
},
]; ];
import call from '@/public/assets/icons/service/call.svg'; import call from "@/public/assets/icons/service/call.svg";
import megaphone from '@/public/assets/icons/service/megaphone.svg'; import megaphone from "@/public/assets/icons/service/megaphone.svg";
import bag from '@/public/assets/icons/service/bag.svg'; import bag from "@/public/assets/icons/service/bag.svg";
export const contactCardData: RoadCardProps[] = [ export const contactCardData: RoadCardProps[] = [
{ { icon: call, textKey: "services.callCentre" },
icon: call, { icon: bag, textKey: "services.serviceBureau" },
text: 'Справочный центр', { icon: megaphone, textKey: "services.advertising" },
enText: 'Call centre',
},
{
icon: bag,
text: 'Услуги',
enText: 'Servise bureau',
},
{
icon: megaphone,
text: 'Реклама',
enText: 'Advertising',
},
]; ];
import partner from '@/public/assets/icons/home/partner.svg'; import partner from "@/public/assets/icons/home/partner.svg";
import { StaticImageData } from 'next/image';
export const partnersData: StaticImageData[] = [partner, partner, partner, partner, partner]; export const partnersData: StaticImageData[] = [
partner,
partner,
partner,
partner,
partner,
];

View File

@ -1,148 +1,110 @@
interface MenuItemType {
titleKey: string;
link: string;
}
interface MenuType { interface MenuType {
pathname: string; pathnameKey: string;
pathnameEn: string;
company?: boolean; company?: boolean;
members?: boolean; members?: boolean;
visitors?: boolean; visitors?: boolean;
news?: boolean; news?: boolean;
services?: boolean; services?: boolean;
info: { info: MenuItemType[];
titleEn: string;
title: string;
link: string;
}[];
} }
export const sidebarData: MenuType[] = [ export const sidebarData: MenuType[] = [
{ {
company: true, company: true,
pathname: 'О компании', pathnameKey: "sidebar.company",
pathnameEn: 'About company',
info: [ info: [
{ title: 'Коротко о нас', titleEn: 'About us', link: '/company/aboutus' }, { titleKey: "sidebar.aboutUs", link: "/company/aboutus" },
// { title: 'Выставочная деятельность', link: '' },
// { title: 'История и награды', link: '' },
// { title: 'Партнеры', link: '' },
// { title: 'Работы в компании', link: '' },
// { title: 'Наши издания', link: '' },
], ],
}, },
{ {
members: true, members: true,
pathname: 'Участникам', pathnameKey: "sidebar.participants",
pathnameEn: 'Participants',
info: [ info: [
{ { titleKey: "sidebar.participantsInfo", link: "/members" },
title: 'Информация для участников', { titleKey: "sidebar.participantsApplication", link: "/members/bid" },
titleEn: 'Information for participants', { titleKey: "sidebar.participantsRules", link: "/members/members-rules" },
link: '/members',
},
{
title: 'Онлайн заявка для участников',
titleEn: 'Online application',
link: '/members/bid',
},
{
title: 'Правила для участников',
titleEn: 'Rules for participants',
link: '/members/members-rules',
},
], ],
}, },
{ {
news: true, news: true,
pathname: 'Новости', pathnameKey: "sidebar.news",
pathnameEn: 'News', info: [{ titleKey: "sidebar.news", link: "/news" }],
info: [
{ title: 'Новости', titleEn: 'News', link: '/news' },
// { title: 'Пресс-релизы', link: '' },
],
}, },
{ {
visitors: true, visitors: true,
pathname: 'Посетителям', pathnameKey: "sidebar.visitors",
pathnameEn: 'Visitors',
info: [ info: [
{ { titleKey: "sidebar.visitorsInfo", link: "/visitors" },
title: 'Информация для посетителей', { titleKey: "sidebar.visitorsRules", link: "/visitors/rules-for-visitors" },
titleEn: 'Information for visitors',
link: '/visitors',
},
{
title: 'Порядок регистрации посетителей',
titleEn: 'Entrance rules',
link: '/visitors/rules-for-visitors',
},
], ],
}, },
{ {
services: true, services: true,
pathname: 'Услуги', pathnameKey: "sidebar.services",
pathnameEn: 'Services',
info: [ info: [
{ { titleKey: "sidebar.organizeExhibitions", link: "/services/organization" },
title: 'Организация выставок', { titleKey: "sidebar.b2bMeetings", link: "/services/meetings" },
titleEn: 'Organization of exhibitions', { titleKey: "sidebar.businessMissions", link: "/services/missions" },
link: '/services/organization', { titleKey: "sidebar.marketingServices", link: "/services/advertising" },
}, { titleKey: "sidebar.hybridEvents", link: "/services/hybrid-organizations" },
{ { titleKey: "sidebar.certification", link: "/services/certification" },
title: 'B2B Встречи и Матчмейкинг', { titleKey: "sidebar.forwarding", link: "/services/forwarding" },
titleEn: 'B2B Meetings & Business Matchmaking', { titleKey: "sidebar.outsourcing", link: "/services/outsourcing" },
link: '/services/meetings', { titleKey: "sidebar.personalTraining", link: "/services/personnel-training" },
}, { titleKey: "sidebar.businessTours", link: "/services/business-tours" },
{ { titleKey: "sidebar.additionalServices", link: "/services/additional" },
title: 'Бизнес-Миссии',
titleEn: 'Business Missions',
link: '/services/missions',
},
{
title: 'Маркетинговые услуги',
titleEn: 'Advertising activity',
link: '/services/advertising',
},
{
title: 'Организация гибридных мероприятий',
titleEn: 'Organization of hybrid events',
link: '/services/hybrid-organizations',
},
{
title: 'Сертификация',
titleEn: 'Certification',
link: '/services/certification',
},
{
title: 'Организация перевозки и таможенное оформление',
titleEn: 'Freight forwarding and customs clearance',
link: '/services/forwarding',
},
{
title: 'Аутсорсинг бухгалтерского учета и аудита',
titleEn: 'Outsourcing of accounting and auditing',
link: '/services/outsourcing',
},
{
title: 'Тренинг для персонала',
titleEn: 'Personnel-training',
link: '/services/personnel-training',
},
{
title: 'Бизнес - туры',
titleEn: 'Business - tours',
link: '/services/business-tours',
},
{
title: 'Дополнительные услуги',
titleEn: 'Additional servies',
link: '/services/additional',
},
], ],
}, },
]; ];
interface HeaderType {
titleKey: string;
link: string;
id: number;
}
export const headerMenu: HeaderType[] = [
{ titleKey: "nav.about", link: "/company/aboutus", id: 1 },
{ titleKey: "nav.news", link: "/news", id: 2 },
{ titleKey: "nav.faq", link: "/faq", id: 3 },
{ titleKey: "nav.contacts", link: "/contacts", id: 4 },
];
export const headerMenu2: HeaderType[] = [
{ titleKey: "nav.calendar", link: "/calendar", id: 1 },
{ titleKey: "nav.services", link: "/services/organization", id: 2 },
{ titleKey: "nav.about", link: "/company/aboutus", id: 3 },
{ titleKey: "nav.news", link: "/news", id: 4 },
{ titleKey: "nav.contacts", link: "/contacts", id: 5 },
];
interface FooterMenuType {
titleKey: string;
link: string;
one?: boolean;
}
export const footerMenu: FooterMenuType[] = [
{ titleKey: "nav.calendar", link: "/calendar", one: true },
{ titleKey: "nav.participants", link: "/members" },
{ titleKey: "nav.visitors", link: "" },
{ titleKey: "nav.services", link: "/services/organization" },
];
export const footerMenu2: FooterMenuType[] = [
{ titleKey: "footer.territory", link: "" },
{ titleKey: "nav.about", link: "/company/aboutus" },
{ titleKey: "footer.pressCenter", link: "" },
{ titleKey: "nav.faq", link: "/faq" },
{ titleKey: "nav.contacts", link: "/contacts" },
{ titleKey: "footer.callCentre", link: "" },
];
interface BurgerDataTypes { interface BurgerDataTypes {
pathname: string; pathname: string;
company?: boolean; company?: boolean;
@ -155,278 +117,71 @@ interface BurgerDataTypes {
visitors?: boolean; visitors?: boolean;
contacts?: boolean; contacts?: boolean;
first?: boolean; first?: boolean;
title: string; titleKey: string;
drop?: string; drop?: string;
info?: { info?: {
title: string; titleKey: string;
link: string; link: string;
}[]; }[];
} }
export const burgerMenuData: BurgerDataTypes[] = [ export const burgerMenuData: BurgerDataTypes[] = [
{ {
pathname: '/calendar', pathname: "/calendar",
calendar: true, calendar: true,
only: true, only: true,
title: 'Календарь мероприятий', titleKey: "nav.calendar",
first: true, first: true,
}, },
{ {
members: true, members: true,
title: 'Участникам', titleKey: "nav.participants",
pathname: '/members', pathname: "/members",
drop: 'members', drop: "members",
first: true, first: true,
info: [ info: [
{ title: 'Информация для участников', link: '/members' }, { titleKey: "sidebar.participantsInfo", link: "/members" },
{ title: 'Онлайн заявка для участников', link: '/members/bid' }, { titleKey: "sidebar.participantsApplication", link: "/members/bid" },
], ],
}, },
{ {
pathname: '/visitors', pathname: "/visitors",
visitors: true, visitors: true,
only: true, only: true,
title: 'Посетителям', titleKey: "nav.visitors",
first: true, first: true,
}, },
{ {
pathname: '/services', pathname: "/services",
services: true, services: true,
only: true, only: true,
title: 'Услуги', titleKey: "nav.services",
first: true, first: true,
}, },
{ {
pathname: '/faq', pathname: "/faq",
faq: true, faq: true,
only: true, only: true,
title: 'FAQ', titleKey: "nav.faq",
}, },
{ {
pathname: '/contacts', pathname: "/contacts",
contacts: true, contacts: true,
only: true, only: true,
title: 'Контакты', titleKey: "nav.contacts",
}, },
{ {
company: true, company: true,
title: 'О компании', titleKey: "nav.about",
pathname: '/company/aboutus', pathname: "/company/aboutus",
drop: 'company', drop: "company",
info: [ info: [{ titleKey: "sidebar.aboutUs", link: "/company/aboutus" }],
{ title: 'Коротко о нас', link: '/company/aboutus' },
// { title: "Выставочная деятельность", link: "" },
// { title: "История и награды", link: "" },
// { title: "Партнеры", link: "" },
// { title: "Работы в компании", link: "" },
// { title: "Наши издания", link: "" },
],
}, },
{ {
news: true, news: true,
title: 'Новости', titleKey: "nav.news",
pathname: '/news', pathname: "/news",
drop: 'news', drop: "news",
info: [ info: [{ titleKey: "sidebar.news", link: "/news" }],
{ title: 'Новости', link: '/news' },
// { title: "Пресс-релизы", link: "/news" },
],
},
];
interface HeaderType {
title: string;
link: string;
id: number;
one?: boolean;
en?: boolean;
}
export const headerMenu: HeaderType[] = [
{ title: 'О компании', link: '/company/aboutus', id: 1 },
{ title: 'Новости', link: '/news', id: 2 },
{ title: 'FAQ', link: '/faq', id: 3 },
{ title: 'Контакты', link: '/contacts', id: 4 },
{ en: true, title: 'About company', link: '/company/aboutus', id: 1 },
{ en: true, title: 'News', link: '/news', id: 2 },
{ en: true, title: 'FAQ', link: '/faq', id: 3 },
{ en: true, title: 'Contacts', link: '/contacts', id: 4 },
];
export const headerMenu2: HeaderType[] = [
{ title: 'Календарь мероприятий', link: '/calendar', id: 1 },
{ title: 'Услуги', link: '/services/organization', id: 2 },
{ title: 'О компании', link: '/company/aboutus', id: 3 },
{ title: 'Новости', link: '/news', id: 4 },
{ title: 'Контакты', link: '/contacts', id: 5 },
// { title: 'Участникам', link: '/members', id: 2 },
// { title: 'Посетителям', link: '/visitors', id: 3 },
{ en: true, title: 'Calendar of events', link: '/calendar', id: 1 },
{ en: true, title: 'Services', link: '/services/organization', id: 2 },
{ en: true, title: 'About company', link: '/company/aboutus', id: 3 },
{ en: true, title: 'News', link: '/news', id: 4 },
{ en: true, title: 'Contacts', link: '/contacts', id: 5 },
// { en: true, title: 'Participants', link: '/members', id: 2 },
// { en: true, title: 'Visitors', link: '/visitors', id: 3 },
];
interface FooterType {
title: string;
link: string;
}
export const footerMenu = [
{ title: 'Календарь мероприятий', link: '/calendar', one: true },
{ title: 'Участникам', link: '/members' },
{ title: 'Посетителям', link: '' },
{ title: 'Услуги', link: '/services/organization' },
];
export const footerMenu2: FooterType[] = [
{ title: 'Территория комплекса', link: '' },
{ title: 'О компании', link: '/company/aboutus' },
{ title: 'Пресс-центр', link: '' },
{ title: 'FAQ', link: '/faq' },
{ title: 'Контакты', link: '/contacts' },
{ title: 'Справочный центр', link: '' },
];
export const footerInfo: string[] = [
'Адрес: 744000, г. Ашхабад, просп. Битарап Туркменистан, 183',
'Тел.: +99362006200',
'+99312454111',
'E-mail: contact@turkmenexpo.com',
'Address: Ashgabat, 183 Bitarap Turkmenistan',
'ave.,744000',
'E-mail: contact@turkmenexpo.com',
];
export const topMenu = [
{
path: 'about',
links: [{ active: 'Главная', default: '/ О компании / Коротко нас' }],
},
{
path: 'members',
links: [
{
active: 'Главная',
active2: ' / Участникам',
default: ' / Информация для участников',
},
],
},
{
path: 'members/bid',
links: [
{
active: 'Главная',
default: ' / Участникам / Онлайн заявка для участников',
},
],
},
{
path: 'events',
links: [
{
active: 'Главная ',
default: '/ Календарь мероприятий',
},
],
},
{
path: 'faq',
links: [
{
active: 'Главная',
default: ' / FAQ',
},
],
},
{
path: 'contacts',
links: [
{
active: 'Главная',
default: ' / Контакты',
},
],
},
{
path: 'calendar',
links: [
{
active: 'Главная',
default: ' / Календарь мероприятий',
},
],
},
{
path: 'news',
links: [
{
active: 'Главная',
default: ' / Новости',
},
],
},
];
export const burgerLinks = [
{
title: 'О компании',
about: true,
links: [
{ name: 'Коротко о нас', link: '/about/company' },
{ name: 'Выставочная деятельность', link: '/about/company' },
{ name: 'История и награды', link: '/about/company' },
{ name: 'Партнеры', link: '/about/company' },
{ name: 'Работа в компании', link: '/about/company' },
{ name: 'Наши издания', link: '/about/company' },
],
},
{
title: 'Новости',
news: true,
links: [
{ name: 'Новости', link: '/news' },
{ name: 'Пресс центр', link: '/news' },
],
},
{
title: 'Участникам',
members: true,
links: [
{ name: 'Участникам', link: '/members' },
{ name: 'Онлайн заявка', link: '/members/bid' },
],
},
{
title: 'Контакты',
contacts: true,
links: [
{ name: 'Новости', link: '/news' },
{ name: 'Пресс центр', link: '/news' },
],
},
{
title: 'FAQ',
faq: true,
links: [
{ name: 'faq', link: '/news' },
{ name: 'Пресс центр', link: '/news' },
],
},
{
news: true,
links: [
{ name: 'Новости', link: '/news' },
{ name: 'Пресс центр', link: '/news' },
],
}, },
]; ];

View File

@ -1,12 +0,0 @@
import { cookies } from "next/headers";
export const getLang = () => {
const cookieStore = cookies();
const langCookie = cookieStore.get("lang")?.value || "ru";
// Возвращаем полный объект языка
return {
title: langCookie === "en" ? "English" : "Русский",
localization: langCookie,
};
};

View File

@ -22,6 +22,5 @@ export type NewsCardProps = {
export type RoadCardProps = { export type RoadCardProps = {
icon: any; icon: any;
text: string; textKey: string;
enText: string;
}; };

144
messages/en.json Normal file
View File

@ -0,0 +1,144 @@
{
"common": {
"home": "Home",
"showMore": "Show more",
"send": "Send",
"formSubmitted": "Form submitted",
"of": "of"
},
"nav": {
"calendar": "Calendar of events",
"services": "Services",
"about": "About company",
"news": "News",
"contacts": "Contacts",
"faq": "FAQ",
"participants": "Participants",
"visitors": "Visitors"
},
"sidebar": {
"company": "About company",
"aboutUs": "About us",
"participants": "Participants",
"participantsInfo": "Information for participants",
"participantsApplication": "Online application",
"participantsRules": "Rules for participants",
"news": "News",
"visitors": "Visitors",
"visitorsInfo": "Information for visitors",
"visitorsRules": "Entrance rules",
"services": "Services",
"organizeExhibitions": "Organization of exhibitions",
"b2bMeetings": "B2B Meetings & Business Matchmaking",
"businessMissions": "Business Missions",
"marketingServices": "Advertising activity",
"hybridEvents": "Organization of hybrid events",
"certification": "Certification",
"forwarding": "Freight forwarding and customs clearance",
"outsourcing": "Outsourcing of accounting and auditing",
"personalTraining": "Personnel-training",
"businessTours": "Business - tours",
"additionalServices": "Additional services"
},
"burger": {
"services": "Services",
"about": "About company",
"aboutUs": "About us",
"advertisingActivity": "Advertising activity",
"freightForwarding": "Freight forwarding and customs clearance",
"accountingOutsourcing": "Outsourcing of accounting and auditing",
"personnelTraining": "Personnel-training",
"calendar": "Calendar of events",
"participantsInfo": "Information for participants",
"participantsApplication": "Online application"
},
"home": {
"exhibitions": "Exhibitions",
"goToWebsite": "Go to website",
"news": "News",
"allNews": "All news",
"upcomingExhibitions": "Upcoming exhibitions and events",
"partners": "Partners",
"feedback": "Feedback"
},
"events": {
"organiserLabel": "Organiser:",
"coOrganiserLabel": "Co-organiser:",
"registration": "Registration",
"visitSite": "Visit site",
"more": "More",
"showMore": "Show more",
"date": "Date",
"venue": "Venue",
"organiser": "Organiser",
"coOrganiser": "Co-organiser",
"description": "Description",
"theme": "Theme of event"
},
"contacts": {
"title": "Contacts",
"contactUs": "Contact us",
"name": "Name",
"email": "E-mail",
"phone": "Phone",
"companyName": "Company name",
"message": "Message",
"address": "Address:",
"addressValue": "744000, Ashgabat, 183 Bitarap Turkmenistan ave.",
"phoneNumbers": "+99371871814; 99363719588"
},
"footer": {
"address": "Address: Ashgabat, 183 Bitarap Turkmenistan ave., 744000",
"phone": "Phone: +99362006200",
"phone2": "+99312454111",
"email": "E-mail: contact@turkmenexpo.com",
"territory": "Complex territory",
"pressCenter": "Press center",
"callCentre": "Call centre"
},
"search": {
"placeholder": "What to find?",
"all": "Everywhere",
"inEvents": "In events",
"inNews": "In news",
"findButton": "Find",
"byRequest": "By request \"",
"found": "\" found",
"results": "results",
"goToPage": "Go to page",
"noResults": "Nothing found for your request"
},
"services": {
"byCar": "Car",
"byBus": "Public transport",
"freight": "Freight transport",
"callCentre": "Call centre",
"serviceBureau": "Service bureau",
"advertising": "Advertising",
"breadcrumb": "Services",
"routes": "Routes"
},
"validation": {
"nameRequired": "Name is required",
"emailRequired": "Email is required",
"phoneRequired": "Phone number is required",
"messageRequired": "Message is required"
},
"visitors": {
"title": "For visitors",
"information": "Information for visitors",
"rules": "Entrance rules"
},
"about": {
"title": "About us"
},
"news": {
"title": "News",
"mediaAboutUs": "Media about us",
"allNews": "All news",
"article": "Article"
},
"calendar": {
"title": "Calendar of events"
}
}

144
messages/ru.json Normal file
View File

@ -0,0 +1,144 @@
{
"common": {
"home": "Главная",
"showMore": "Показать ещё",
"send": "Отправить",
"formSubmitted": "Форма отправлена",
"of": "из"
},
"nav": {
"calendar": "Календарь мероприятий",
"services": "Услуги",
"about": "О компании",
"news": "Новости",
"contacts": "Контакты",
"faq": "FAQ",
"participants": "Участникам",
"visitors": "Посетителям"
},
"sidebar": {
"company": "О компании",
"aboutUs": "Коротко о нас",
"participants": "Участникам",
"participantsInfo": "Информация для участников",
"participantsApplication": "Онлайн заявка для участников",
"participantsRules": "Правила для участников",
"news": "Новости",
"visitors": "Посетителям",
"visitorsInfo": "Информация для посетителей",
"visitorsRules": "Порядок регистрации посетителей",
"services": "Услуги",
"organizeExhibitions": "Организация выставок",
"b2bMeetings": "B2B Встречи и Матчмейкинг",
"businessMissions": "Бизнес-Миссии",
"marketingServices": "Маркетинговые услуги",
"hybridEvents": "Организация гибридных мероприятий",
"certification": "Сертификация",
"forwarding": "Организация перевозки и таможенное оформление",
"outsourcing": "Аутсорсинг бухгалтерского учета и аудита",
"personalTraining": "Тренинг для персонала",
"businessTours": "Бизнес - туры",
"additionalServices": "Дополнительные услуги"
},
"burger": {
"services": "Услуги",
"about": "О компании",
"aboutUs": "О нас",
"advertisingActivity": "Рекламная деятельность",
"freightForwarding": "Экспедирование грузов и таможенное оформление",
"accountingOutsourcing": "Аутсорсинг бух.учета и аудита",
"personnelTraining": "Обучения персонала",
"calendar": "Календарь мероприятий",
"participantsInfo": "Информация для участников",
"participantsApplication": "Онлайн заявка для участников"
},
"home": {
"exhibitions": "Выставки",
"goToWebsite": "Перейти на сайт",
"news": "Новости",
"allNews": "Все новости",
"upcomingExhibitions": "Ближайшие выставки и мероприятия",
"partners": "Партнёры",
"feedback": "Обратная связь"
},
"events": {
"organiserLabel": "Организатор:",
"coOrganiserLabel": "Со-организатор:",
"registration": "Зарегистрироваться",
"visitSite": "Посетить сайт",
"more": "Подробнее",
"showMore": "Показать еще",
"date": "Дата",
"venue": "Место",
"organiser": "Организатор",
"coOrganiser": "Со-организатор",
"description": "Описание",
"theme": "Тематика мероприятия"
},
"contacts": {
"title": "Контакты",
"contactUs": "Связаться с нами",
"name": "Имя",
"email": "E-mail",
"phone": "Номер",
"companyName": "Название компании",
"message": "Сообщение",
"address": "Адрес:",
"addressValue": "744000, г. Ашхабад, просп. Битарап Туркменистан, 183",
"phoneNumbers": "+99371871814; 99363719588"
},
"footer": {
"address": "Адрес: 744000, г. Ашхабад, просп. Битарап Туркменистан, 183",
"phone": "Тел.: +99362006200",
"phone2": "+99312454111",
"email": "E-mail: contact@turkmenexpo.com",
"territory": "Территория комплекса",
"pressCenter": "Пресс-центр",
"callCentre": "Справочный центр"
},
"search": {
"placeholder": "Что найти?",
"all": "Везде",
"inEvents": "В событиях",
"inNews": "В новостях",
"findButton": "Найти",
"byRequest": "По запросу «",
"found": "» нашлось",
"results": "результатов",
"goToPage": "Перейти на страницу",
"noResults": "По вашему запросу ничего не найдено"
},
"services": {
"byCar": "На машине",
"byBus": "На автобусе",
"freight": "Грузовой транспорт",
"callCentre": "Справочный центр",
"serviceBureau": "Услуги",
"advertising": "Реклама",
"breadcrumb": "Сервисы",
"routes": "Как добраться"
},
"validation": {
"nameRequired": "Имя необходимо",
"emailRequired": "Email необходим",
"phoneRequired": "Номер телефона необходим",
"messageRequired": "Сообщение необходимо"
},
"visitors": {
"title": "Посетителям",
"information": "Информация для посетителей",
"rules": "Порядок регистрации посетителей"
},
"about": {
"title": "Коротко о нас"
},
"news": {
"title": "Новости",
"mediaAboutUs": "СМИ о нас",
"allNews": "Все новости",
"article": "Статья"
},
"calendar": {
"title": "Календарь мероприятий"
}
}

13
middleware.ts Normal file
View File

@ -0,0 +1,13 @@
import createMiddleware from "next-intl/middleware";
import { locales, defaultLocale } from "./i18n/config";
export default createMiddleware({
locales,
defaultLocale,
localePrefix: "always",
localeDetection: true,
});
export const config = {
matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};

View File

@ -1,7 +1,11 @@
import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin("./i18n/request.ts");
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
images: { unoptimized: true }, images: { unoptimized: true },
distDir: "build", distDir: "build",
}; };
export default nextConfig; export default withNextIntl(nextConfig);

717
package-lock.json generated
View File

@ -15,6 +15,7 @@
"clsx": "^2.1.0", "clsx": "^2.1.0",
"framer-motion": "^11.0.25", "framer-motion": "^11.0.25",
"next": "14.1.0", "next": "14.1.0",
"next-intl": "^4.8.2",
"pm2": "^5.3.1", "pm2": "^5.3.1",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
@ -59,6 +60,67 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@formatjs/ecma402-abstract": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-3.1.1.tgz",
"integrity": "sha512-jhZbTwda+2tcNrs4kKvxrPLPjx8QsBCLCUgrrJ/S+G9YrGHWLhAyFMMBHJBnBoOwuLHd7L14FgYudviKaxkO2Q==",
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "3.1.0",
"@formatjs/intl-localematcher": "0.8.1",
"decimal.js": "^10.6.0",
"tslib": "^2.8.1"
}
},
"node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.1.tgz",
"integrity": "sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==",
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "3.1.0",
"tslib": "^2.8.1"
}
},
"node_modules/@formatjs/fast-memoize": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.0.tgz",
"integrity": "sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.1"
}
},
"node_modules/@formatjs/icu-messageformat-parser": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.1.tgz",
"integrity": "sha512-sSDmSvmmoVQ92XqWb499KrIhv/vLisJU8ITFrx7T7NZHUmMY7EL9xgRowAosaljhqnj/5iufG24QrdzB6X3ItA==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "3.1.1",
"@formatjs/icu-skeleton-parser": "2.1.1",
"tslib": "^2.8.1"
}
},
"node_modules/@formatjs/icu-skeleton-parser": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.1.tgz",
"integrity": "sha512-PSFABlcNefjI6yyk8f7nyX1DC7NHmq6WaCHZLySEXBrXuLOB2f935YsnzuPjlz+ibhb9yWTdPeVX1OVcj24w2Q==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "3.1.1",
"tslib": "^2.8.1"
}
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz",
"integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==",
"license": "MIT",
"dependencies": {
"tslib": "2"
}
},
"node_modules/@googlemaps/js-api-loader": { "node_modules/@googlemaps/js-api-loader": {
"version": "1.16.2", "version": "1.16.2",
"resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.16.2.tgz", "resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.16.2.tgz",
@ -831,6 +893,313 @@
"uuid": "bin/uuid" "uuid": "bin/uuid"
} }
}, },
"node_modules/@parcel/watcher": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.3",
"is-glob": "^4.0.3",
"node-addon-api": "^7.0.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.6",
"@parcel/watcher-darwin-arm64": "2.5.6",
"@parcel/watcher-darwin-x64": "2.5.6",
"@parcel/watcher-freebsd-x64": "2.5.6",
"@parcel/watcher-linux-arm-glibc": "2.5.6",
"@parcel/watcher-linux-arm-musl": "2.5.6",
"@parcel/watcher-linux-arm64-glibc": "2.5.6",
"@parcel/watcher-linux-arm64-musl": "2.5.6",
"@parcel/watcher-linux-x64-glibc": "2.5.6",
"@parcel/watcher-linux-x64-musl": "2.5.6",
"@parcel/watcher-win32-arm64": "2.5.6",
"@parcel/watcher-win32-ia32": "2.5.6",
"@parcel/watcher-win32-x64": "2.5.6"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
"integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
"integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
"integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
"integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
"integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
"integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
"integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
"integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
"integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
"integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
"integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
"integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
"integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -1155,6 +1524,178 @@
} }
} }
}, },
"node_modules/@schummar/icu-type-parser": {
"version": "1.21.5",
"resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz",
"integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==",
"license": "MIT"
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.11.tgz",
"integrity": "sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.11.tgz",
"integrity": "sha512-S52Gu1QtPSfBYDiejlcfp9GlN+NjTZBRRNsz8PNwBgSE626/FUf2PcllVUix7jqkoMC+t0rS8t+2/aSWlMuQtA==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.11.tgz",
"integrity": "sha512-lXJs8oXo6Z4yCpimpQ8vPeCjkgoHu5NoMvmJZ8qxDyU99KVdg6KwU9H79vzrmB+HfH+dCZ7JGMqMF//f8Cfvdg==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.11.tgz",
"integrity": "sha512-chRsz1K52/vj8Mfq/QOugVphlKPWlMh10V99qfH41hbGvwAU6xSPd681upO4bKiOr9+mRIZZW+EfJqY42ZzRyA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.11.tgz",
"integrity": "sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.11.tgz",
"integrity": "sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.11.tgz",
"integrity": "sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.11.tgz",
"integrity": "sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.11.tgz",
"integrity": "sha512-6XnzORkZCQzvTQ6cPrU7iaT9+i145oLwnin8JrfsLG41wl26+5cNQ2XV3zcbrnFEV6esjOceom9YO1w9mGJByw==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.11.tgz",
"integrity": "sha512-IQ2n6af7XKLL6P1gIeZACskSxK8jWtoKpJWLZmdXTDj1MGzktUy4i+FvpdtxFmJWNavRWH1VmTr6kAubRDHeKw==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"license": "Apache-2.0"
},
"node_modules/@swc/helpers": { "node_modules/@swc/helpers": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
@ -1163,6 +1704,15 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@swc/types": {
"version": "0.1.25",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
"integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==",
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3"
}
},
"node_modules/@tootallnate/quickjs-emscripten": { "node_modules/@tootallnate/quickjs-emscripten": {
"version": "0.23.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
@ -2005,6 +2555,12 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
},
"node_modules/deep-extend": { "node_modules/deep-extend": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
@ -2027,9 +2583,10 @@
} }
}, },
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.0.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@ -2570,6 +3127,21 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/icu-minify": {
"version": "4.8.2",
"resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.8.2.tgz",
"integrity": "sha512-LHBQV+skKkjZSPd590pZ7ZAHftUgda3eFjeuNwA8/15L8T8loCNBktKQyTlkodAU86KovFXeg/9WntlAo5wA5A==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/amannn"
}
],
"license": "MIT",
"dependencies": {
"@formatjs/icu-messageformat-parser": "^3.4.0"
}
},
"node_modules/immer": { "node_modules/immer": {
"version": "10.0.3", "version": "10.0.3",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz", "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz",
@ -2598,6 +3170,18 @@
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
}, },
"node_modules/intl-messageformat": {
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.1.2.tgz",
"integrity": "sha512-ucSrQmZGAxfiBHfBRXW/k7UC8MaGFlEj4Ry1tKiDcmgwQm1y3EDl40u+4VNHYomxJQMJi9NEI3riDRlth96jKg==",
"license": "BSD-3-Clause",
"dependencies": {
"@formatjs/ecma402-abstract": "3.1.1",
"@formatjs/fast-memoize": "3.1.0",
"@formatjs/icu-messageformat-parser": "3.5.1",
"tslib": "^2.8.1"
}
},
"node_modules/invariant": { "node_modules/invariant": {
"version": "2.2.4", "version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@ -3103,6 +3687,91 @@
} }
} }
}, },
"node_modules/next-intl": {
"version": "4.8.2",
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.8.2.tgz",
"integrity": "sha512-GuuwyvyEI49/oehQbBXEoY8KSIYCzmfMLhmIwhMXTb+yeBmly1PnJcpgph3KczQ+HTJMXwXCmkizgtT8jBMf3A==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/amannn"
}
],
"license": "MIT",
"dependencies": {
"@formatjs/intl-localematcher": "^0.5.4",
"@parcel/watcher": "^2.4.1",
"@swc/core": "^1.15.2",
"icu-minify": "^4.8.2",
"negotiator": "^1.0.0",
"next-intl-swc-plugin-extractor": "^4.8.2",
"po-parser": "^2.1.1",
"use-intl": "^4.8.2"
},
"peerDependencies": {
"next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0",
"typescript": "^5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/next-intl-swc-plugin-extractor": {
"version": "4.8.2",
"resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.8.2.tgz",
"integrity": "sha512-sHDs36L1VZmFHj3tPHsD+KZJtnsRudHlNvT0ieIe3iFVn5OpGLTxW3d/Zc/2LXSj5GpGuR6wQeikbhFjU9tMQQ==",
"license": "MIT"
},
"node_modules/next-intl/node_modules/@swc/core": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.11.tgz",
"integrity": "sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.25"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.15.11",
"@swc/core-darwin-x64": "1.15.11",
"@swc/core-linux-arm-gnueabihf": "1.15.11",
"@swc/core-linux-arm64-gnu": "1.15.11",
"@swc/core-linux-arm64-musl": "1.15.11",
"@swc/core-linux-x64-gnu": "1.15.11",
"@swc/core-linux-x64-musl": "1.15.11",
"@swc/core-win32-arm64-msvc": "1.15.11",
"@swc/core-win32-ia32-msvc": "1.15.11",
"@swc/core-win32-x64-msvc": "1.15.11"
},
"peerDependencies": {
"@swc/helpers": ">=0.5.17"
},
"peerDependenciesMeta": {
"@swc/helpers": {
"optional": true
}
}
},
"node_modules/next-intl/node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/next/node_modules/postcss": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@ -3130,6 +3799,12 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT"
},
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.14", "version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
@ -3651,6 +4326,12 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/po-parser": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz",
"integrity": "sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==",
"license": "MIT"
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.35", "version": "8.4.35",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
@ -4757,9 +5438,10 @@
"dev": true "dev": true
}, },
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.6.2", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
}, },
"node_modules/tv4": { "node_modules/tv4": {
"version": "1.3.0", "version": "1.3.0",
@ -4793,7 +5475,7 @@
"version": "5.3.3", "version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"dev": true, "devOptional": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -4882,6 +5564,27 @@
"react": ">=16.8.0" "react": ">=16.8.0"
} }
}, },
"node_modules/use-intl": {
"version": "4.8.2",
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.8.2.tgz",
"integrity": "sha512-3VNXZgDnPFqhIYosQ9W1Hc6K5q+ZelMfawNbexdwL/dY7BTHbceLUBX5Eeex9lgogxTp0pf1SjHuhYNAjr9H3g==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/amannn"
}
],
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "^3.1.0",
"@schummar/icu-type-parser": "1.21.5",
"icu-minify": "^4.8.2",
"intl-messageformat": "^11.1.0"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
}
},
"node_modules/use-sync-external-store": { "node_modules/use-sync-external-store": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",

View File

@ -17,6 +17,7 @@
"clsx": "^2.1.0", "clsx": "^2.1.0",
"framer-motion": "^11.0.25", "framer-motion": "^11.0.25",
"next": "14.1.0", "next": "14.1.0",
"next-intl": "^4.8.2",
"pm2": "^5.3.1", "pm2": "^5.3.1",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",

View File

@ -1,13 +1,12 @@
import { baseAPI } from "@/lib/API"; import { baseAPI } from "@/lib/API";
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { activeLangType } from "./headerSlice";
export const fetchAbout = createAsyncThunk( export const fetchAbout = createAsyncThunk(
"about/fetchAbout", "about/fetchAbout",
async ({ activeLang }: { activeLang: activeLangType }) => { async ({ locale }: { locale: string }) => {
const res = await fetch(`${baseAPI}settings/about_us`, { const res = await fetch(`${baseAPI}settings/about_us`, {
headers: { headers: {
"Accept-Language": activeLang.localization, "Accept-Language": locale,
}, },
}); });
@ -35,12 +34,9 @@ const aboutSlice = createSlice({
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(fetchAbout.pending, (state) => { builder.addCase(fetchAbout.pending, (state) => {});
// console.log('pending');
});
builder.addCase(fetchAbout.fulfilled, (state, action) => { builder.addCase(fetchAbout.fulfilled, (state, action) => {
// console.log('success', action.payload);
state.aboutData = action.payload; state.aboutData = action.payload;
}); });

View File

@ -1,21 +1,11 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState } from '../store'; import { RootState } from '../store';
export type activeLangType = {
title: 'Ру' | 'En' | 'Tm' | string;
localization: 'ru' | 'en' | 'tm' | string;
};
interface HeaderState { interface HeaderState {
activeLang: activeLangType;
showInput: boolean; showInput: boolean;
} }
const initialState: HeaderState = { const initialState: HeaderState = {
activeLang: {
title: 'Русский',
localization: 'ru',
},
showInput: false, showInput: false,
}; };
@ -23,9 +13,6 @@ const headerSlice = createSlice({
name: 'header', name: 'header',
initialState, initialState,
reducers: { reducers: {
setActiveLang(state, action: PayloadAction<activeLangType>) {
state.activeLang = action.payload;
},
setShowInput(state, action: PayloadAction<boolean>) { setShowInput(state, action: PayloadAction<boolean>) {
state.showInput = action.payload; state.showInput = action.payload;
}, },
@ -34,6 +21,6 @@ const headerSlice = createSlice({
export const selectHeader = (state: RootState) => state.headerSlice; export const selectHeader = (state: RootState) => state.headerSlice;
export const { setActiveLang, setShowInput } = headerSlice.actions; export const { setShowInput } = headerSlice.actions;
export default headerSlice.reducer; export default headerSlice.reducer;

View File

@ -1,21 +1,18 @@
import { baseAPI } from "@/lib/API"; import { baseAPI } from "@/lib/API";
import { ServicesType } from "@/lib/types/Services.data"; import { ServicesType } from "@/lib/types/Services.data";
import { cookies } from "next/headers";
export const getServices = async () => {
const lang = cookies().get("lang")?.value || "en";
export const getServices = async (lang: string) => {
const res = await fetch(`${baseAPI}services`, { const res = await fetch(`${baseAPI}services`, {
headers: { headers: {
"Accept-Language": lang, // Передаём язык в заголовке "Accept-Language": lang,
}, },
next: { next: {
revalidate: 1000, revalidate: 1000,
tags: [`services-${lang}`], // Кешируем отдельно для каждого языка tags: [`services-${lang}`],
}, },
}); });
if (!res.ok) throw new Error("Ошибка при загрузке данных"); if (!res.ok) throw new Error("Failed to load services");
return (await res.json()) as ServicesType; return (await res.json()) as ServicesType;
}; };

View File

@ -1,9 +0,0 @@
"use client";
export const useLang = (en: string, ru: string, localization: string) => {
if (localization === "en") {
return en;
} else {
return ru;
}
};