This commit is contained in:
Batyr 2025-05-23 17:47:18 +05:00
parent 89aa735096
commit 9c8b6a132a
27 changed files with 479 additions and 47 deletions

BIN
README.md

Binary file not shown.

Binary file not shown.

BIN
public/impressions-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

View File

@ -3,13 +3,15 @@ import { FC } from "react";
interface Props {
className?: string;
color?: string;
w?: string;
h?: string;
}
export const Chevron: FC<Props> = ({ color = "white" }) => {
export const Chevron: FC<Props> = ({ color = "white", w = "20", h = "20" }) => {
return (
<svg
width="20"
height="20"
width={w}
height={h}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"

View File

@ -0,0 +1,38 @@
import { FC } from "react";
import { cn } from "@/lib/utils";
interface Props {
imgUrl: string;
name: string;
info: string;
className?: string;
}
export const DesignerCard: FC<Props> = ({ className, ...props }) => {
return (
<article
className={cn(
"relative bg-[url('/impressions/card-bg.png')] bg-no-repeat h-[326px] overflow-hidden",
className
)}
>
<img
src="/impressions/card-bg-bg.png"
className="absolute top-2.5 left-2.5 -z-10 size-full"
/>
<div className="flex items-center gap-4">
<img
src={props.imgUrl}
alt=""
className="flex-[0_0_44%] object-contain overflow-hidden"
/>
<div className="flex-auto z-20">
<h3>{props.name}</h3>
<p>{props.info}</p>
</div>
</div>
</article>
);
};

View File

@ -0,0 +1,77 @@
import { FC } from "react";
import { cn } from "@/lib/utils";
import { Link } from "react-router-dom";
import { Container } from "@/components/layout";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Menu } from "../menu";
import { Chevron } from "../chevron";
interface Props {
className?: string;
}
export const HomeActions: FC<Props> = ({ className }) => {
const { t } = useTranslation("home");
const { title, items } = t("buttons", { returnObjects: true }) as {
title: string;
items: {
text: string;
link?: string;
blank?: boolean;
dropdown?: boolean;
items: { text: string; link: string }[];
}[];
};
console.log(items);
return (
<section className={cn("bg-teritary_container py-10", className)}>
<Container>
<h2 className="h2 text-center mb-10 !text-on_teritary_container">
{title}
</h2>
<div className="grid grid-cols-3 gap-6">
{items.map((item) =>
!item.dropdown ? (
<Link to={item.link ?? ""}>
<Button
variant={"teritary"}
size={"lg"}
className="w-full drop-shadow-sm shadow-md text-xl bg-teritary text-on_teritary hover:bg-teritary/90"
>
{item.text}
</Button>
</Link>
) : (
<Link
target={item.blank ? "_blank" : ""}
key={title}
to={item.link ?? ""}
className="w-full"
>
<Menu
dropDownContent={item.items}
triggerClassName="w-full"
className="!w-full"
>
<Button
size={"lg"}
variant={"teritary"}
className="w-full drop-shadow-sm shadow-md text-xl bg-teritary text-on_teritary hover:bg-teritary/90"
>
{title}
<Chevron w={"40"} h={"40"} color="white" />
</Button>
</Menu>
</Link>
)
)}
</div>
</Container>
</section>
);
};

View File

@ -1,17 +1,11 @@
import useEmblaCarousel from "embla-carousel-react";
import { FC } from "react";
import { Link } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { Container } from "@/components/layout";
import { useMediaQuery } from "usehooks-ts";
import { btns } from "@/data/home/home-hero.data";
import { useTranslate } from "@/hooks/use-translate";
import { useLangStore } from "@/store/lang";
import { useTranslation } from "react-i18next";
import { HomeTimer } from "./";
export const HomeHero: FC = () => {
const [embalRef] = useEmblaCarousel();
const lang = useLangStore((state) => state.lang);
const { t } = useTranslation("home");
@ -23,38 +17,24 @@ export const HomeHero: FC = () => {
else if (md) return t("banners.md");
else return t("banners.sm");
}
return (
<section className="flex flex-col gap-5">
<div ref={embalRef} className="embla">
<div className="embla__container">
<div className="embla__slide">
<img
src={getBanner()}
alt=""
className="size-full object-cover lg:max-h-[600px] lg:min-h-[320px]"
/>
<div>
<section className="flex flex-col gap-5">
<div ref={embalRef} className="embla">
<div className="embla__container">
<div className="embla__slide">
<img
src={getBanner()}
alt=""
className="size-full object-cover lg:max-h-[600px] lg:min-h-[320px]"
/>
</div>
</div>
</div>
</div>
</section>
<Container className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 items-center gap-6 text-2xl">
{btns[useTranslate(lang)].data.map(({ title, link, blank }) => (
<Link
target={blank ? "_blank" : ""}
key={title}
to={link}
className="w-full"
>
<Button
size={"lg"}
variant={"teritary"}
className="w-full drop-shadow-sm shadow-md text-xl bg-teritary text-on_teritary hover:bg-teritary/90"
>
{title}
</Button>
</Link>
))}
</Container>
</section>
<HomeTimer />
</div>
);
};

View File

@ -20,7 +20,7 @@ export const HomeNews: FC = () => {
{lang === "en" ? "News" : "Новости"}
</h2>
<div className="grid md:grid-cols-3 grid-cols-1 gap-6">
{data?.map((item) => (
{data?.slice(0, 3).map((item) => (
<NewsCard {...item} key={item.title} />
))}
</div>

View File

@ -20,12 +20,14 @@ export const HomeTime: FC<Props> = ({ className }) => {
const { data, isPending } = useExhibitionTime();
const { data: contacts } = useHomeContacts();
const translate = useTranslate(lang);
if (isPending) return <Loader />;
return (
<section className={cn("bg-surface_high pt-10 pb-20", className)}>
<Container>
<h2 className="h2 mb-6">{times[useTranslate(lang)].title}</h2>
<h2 className="h2 mb-6">{times[translate].title}</h2>
<div className="flex flex-col gap-6">
<div className="flex flex-col md:flex-row items-center gap-6">

View File

@ -0,0 +1,117 @@
import { FC, useEffect, useState } from "react";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
import { TimerItem } from "../";
interface Props {
className?: string;
}
export const HomeTimer: FC<Props> = ({ className }) => {
const { t } = useTranslation("home");
const [timeLeft, setTimeLeft] = useState({
days: "0",
hours: "00",
minutes: "00",
seconds: "00",
});
const [prevValues, setPrevValues] = useState<Record<string, string>>({});
const targetDate = new Date("2025-06-11T08:00:00").getTime();
// Функция для вычисления оставшегося времени
const calculateTimeLeft = () => {
const now = new Date().getTime();
const diff = targetDate - now;
if (diff <= 0) return null;
return {
days: Math.floor(diff / (1000 * 60 * 60 * 24)),
hours: Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
minutes: Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)),
seconds: Math.floor((diff % (1000 * 60)) / 1000),
};
};
useEffect(() => {
// Первоначальный расчет
const initialTime = calculateTimeLeft();
if (initialTime) {
setTimeLeft({
days: initialTime.days.toString(),
hours: initialTime.hours.toString().padStart(2, "0"),
minutes: initialTime.minutes.toString().padStart(2, "0"),
seconds: initialTime.seconds.toString().padStart(2, "0"),
});
}
const timerInterval = setInterval(() => {
const newTime = calculateTimeLeft();
if (!newTime) {
clearInterval(timerInterval);
return;
}
// Обновляем предыдущие значения перед установкой новых
setPrevValues({
days: timeLeft.days,
hours: timeLeft.hours,
minutes: timeLeft.minutes,
seconds: timeLeft.seconds,
});
setTimeLeft({
days: newTime.days.toString(),
hours: newTime.hours.toString().padStart(2, "0"),
minutes: newTime.minutes.toString().padStart(2, "0"),
seconds: newTime.seconds.toString().padStart(2, "0"),
});
}, 1000);
return () => clearInterval(timerInterval);
}, [targetDate]); // Убрали зависимость от timeLeft
const {
title,
days: daysLabel,
hours: hoursLabel,
minutes: minutesLabel,
seconds: secondsLabel,
} = t("timer", {
returnObjects: true,
}) as {
title: string;
days: string;
hours: string;
minutes: string;
seconds: string;
};
return (
<section className={cn("container py-10", className)}>
<h2 className="h2 text-center mb-10">{title}</h2>
<div className="grid grid-cols-4 gap-6 place-items-center">
<TimerItem
value={timeLeft.days}
prevValue={prevValues.days || timeLeft.days}
label={daysLabel}
/>
<TimerItem
value={timeLeft.hours}
prevValue={prevValues.hours || timeLeft.hours}
label={hoursLabel}
/>
<TimerItem
value={timeLeft.minutes}
prevValue={prevValues.minutes || timeLeft.minutes}
label={minutesLabel}
/>
<TimerItem
value={timeLeft.seconds}
prevValue={prevValues.seconds || timeLeft.seconds}
label={secondsLabel}
/>
</div>
</section>
);
};

View File

@ -4,3 +4,6 @@ export { HomeAbout } from "./home-about";
export { HomeTheme } from "./home-theme";
export { HomeOffers } from "./home-offers";
export { HomeSponsors } from "./home-sponsors";
export { HomeNews } from "./home-news";
export { HomeTimer } from "./home-timer";
export { HomeActions } from "./home-actions";

View File

@ -15,6 +15,8 @@ export { Loader } from "./loader";
export { Tabs } from "./tabs";
export { ParticipantItem } from "./participant-item";
export { MediaModal } from "./media-modal";
export { DesignerCard } from "./designer-card";
export { TimerItem } from "./timer-item";
export * from "./about";
export * from "./home";

View File

@ -1,4 +1,4 @@
import { FC, useState } from "react";
import { FC, PropsWithChildren, useState } from "react";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import { Chevron } from "./";
import { Link } from "react-router-dom";
@ -9,18 +9,19 @@ import { useUiStore } from "@/store/ui";
interface Props {
className?: string;
title: string;
title?: string;
color?: string;
triggerClassName?: string;
dropDownContent?: DropDownContent[];
}
export const Menu: FC<Props> = ({
export const Menu: FC<PropsWithChildren<Props>> = ({
title,
dropDownContent,
color,
triggerClassName,
children,
}) => {
const [isOpen, setIsOpen] = useState(false);
const setSheet = useUiStore((state) => state.setSheet);
@ -31,7 +32,8 @@ export const Menu: FC<Props> = ({
className={cn("flex items-center gap-2", triggerClassName)}
>
{title}
<Chevron color={color} />
{children}
{!children && <Chevron color={color} />}
</PopoverTrigger>
<PopoverContent className="w-fit px-0 py-2 cursor-pointer bg-surface_container">

View File

@ -0,0 +1,29 @@
export const TimerItem = ({
value,
label,
prevValue,
}: {
value: string;
label: string;
prevValue: string;
}) => (
<div className="timer-item flex flex-col items-center">
<div className="relative h-14 w-16 overflow-hidden font-bold">
<span
className={`absolute left-1/2 -translate-x-1/2 black bg-gradient-to-t from-[#F38835] to-[#F1700D] text-transparent bg-clip-text text-5xl leading-none transition-all duration-300 ${
prevValue !== value ? " opacity-0" : " opacity-100"
}`}
>
{prevValue}
</span>
<span
className={`absolute left-1/2 -translate-x-1/2 black bg-gradient-to-t from-[#F38835] to-[#F1700D] text-transparent bg-clip-text text-5xl leading-none transition-all duration-300 ${
prevValue !== value ? "opacity-100" : "opacity-0"
}`}
>
{value}
</span>
</div>
<div className="label normal uppercase text-xl">{label}</div>
</div>
);

View File

@ -17,6 +17,11 @@
url(../fonts/Gilroy-Semibold.woff2) format("woff2");
font-weight: 600;
}
@font-face {
font-family: "Gilroy-Black";
src: local("Gilroy-Black"), url(../fonts/Gilroy-Black.woff2) format("woff2");
font-weight: 900;
}
@tailwind base;
@tailwind components;
@ -33,6 +38,14 @@
}
@layer utilities {
.container {
@apply w-full mx-auto max-w-[1240px] px-4;
}
.section-y {
@apply py-20;
}
.normal {
@apply font-["Gilroy-Regular"] font-normal;
}
@ -41,12 +54,16 @@
@apply font-["Gilroy-Semibold"] font-semibold;
}
.black {
@apply font-['Gilroy-Black'];
}
.h1 {
@apply text-[28px] leading-[130%] font-medium;
}
.h2 {
@apply md:text-[28px] text-2xl text-[#171717];
@apply md:text-[28px] text-[32px] text-[#171717];
}
.p {

View File

@ -5,6 +5,66 @@
"sm": "https://turkmentextile.turkmenexpo.com/app/storage/app/media/surat/ru/s.jpg"
},
"timer": {
"title": "Событие начнется через",
"days": "дней",
"hours": "часов",
"minutes": "минут",
"seconds": "секунд"
},
"buttons": {
"title": "Быстрые действия",
"items": [
{
"text": "План выставки",
"link": "https://turkmentextile.turkmenexpo.com/app/storage/app/media/Floor%20plan/floor%20plan.pdf",
"blank": true
},
{
"text": "Забронировать стенд",
"link": "/stend-form"
},
{
"text": "Список участников",
"link": "/participants"
},
{
"text": "B2B | B2G встречи",
"link": "/B2B-B2G"
},
{
"text": "Модные показы",
"dropdown": true,
"items": [
{
"text": "Зарубежных дизайнеров",
"link": ""
},
{
"text": "Туркменских дизайнеров",
"link": ""
}
]
},
{
"title": "Мастер-классы",
"dropdown": true,
"items": [
{
"text": "Для дизайнеров",
"link": ""
},
{
"text": "Для посетителей ",
"link": ""
}
]
}
]
},
"partners": {
"title": "Партнёры",
"button": ""

View File

@ -10,6 +10,7 @@ import {
BecomeSponsor,
Contacts,
Home,
Impressions,
Media,
News,
NewsInner,
@ -32,6 +33,7 @@ const router = createBrowserRouter([
{ element: <NewsInner />, path: "news/:id" },
{ element: <Participants />, path: "participants" },
{ element: <Media />, path: "media" },
{ element: <Impressions />, path: "impressions" },
],
},
]);

View File

@ -1,17 +1,19 @@
import {
HomeAbout,
HomeActions,
HomeHero,
HomeNews,
HomeOffers,
HomeSponsors,
HomeTheme,
HomeTime,
} from "@/components/shared";
import { HomeNews } from "@/components/shared/home/home-news";
export default function Home() {
return (
<div className="flex flex-col gap-20">
<HomeHero />
<HomeActions />
<HomeAbout />
<HomeSponsors />
<HomeOffers />

98
src/pages/impressions.tsx Normal file
View File

@ -0,0 +1,98 @@
import { Container } from "@/components/layout";
import { DesignerCard } from "@/components/shared";
import { Button } from "@/components/ui/button";
import { useScrollTop } from "@/hooks/use-scroll-top";
import useEmblaCarousel from "embla-carousel-react";
export default function Impressions() {
useScrollTop();
const [emblaRef] = useEmblaCarousel({ align: "center" });
return (
<div className="overflow-x-hidden">
<section className="">
<img
src="/impressions-cover.png"
alt=""
className="w-full object-contain max-h-[610px]"
/>
<h1 className="absolute opacity-0 top-0 left-0">Показы</h1>
<div className="bg-[url('/impressions-bg.png')] py-[200px] ">
<Container className="grid grid-cols-2 gap-6 h-[570px]">
<div className="flex flex-col gap-6 justify-center">
<h2 className="text-5xl leading-[120%]">
Дефиле туркменской моды на выставке "ТуркменТекстиль"
</h2>
<p className="text-on_surface_v normal text-base">
В рамках международной выставки "Туркмен текстиль" состоятся
эксклюзивные показы коллекций ведущих туркменских дизайнеров и
Домов моды. Гости мероприятия смогут оценить высокое мастерство
исполнения, богатство национальных традиций в современном
прочтении и актуальные тенденции текстильной индустрии
Туркменистана, воплощенные в уникальных моделях одежды и
аксессуаров.
</p>
</div>
<div className="relative">
<img
src="/impressions/gallery-1.png"
alt="impression image"
className="absolute top-0 right-0 z-10 shadow-xl drop-shadow-sm"
/>
<img
src="/impressions/gallery-2.png"
className="absolute left-0 top-1/4 shadow-xl drop-shadow-sm"
/>
<img
src="/impressions/gallery-3.png"
alt=""
className="absolute bottom-0 right-0 z-10 shadow-xl drop-shadow-sm"
/>
</div>
</Container>
</div>
</section>
<section className="py-[200px]">
<Container className="flex flex-col gap-10">
<h2 className="text-5xl">Дизайнеры одежды</h2>
<div ref={emblaRef} className="embla">
<div className="flex embla__container">
{[...Array(9)].map((_, i) => (
<DesignerCard
key={i}
name="Batyr"
info="test tesat tesxt test"
imgUrl="/impressions/designer.png"
className="embla__slide flex-[0_0_714px] mr-4"
/>
))}
</div>
</div>
</Container>
</section>
<section className="bg-[url('/CTA.png')] py-20 ">
<div className="flex flex-col gap-6 text-center max-w-[808px] mx-auto">
<h3 className="text-white text-3xl">
Тренды туркменского текстиля 2025/2026
</h3>
<p className="text-lg text-primary_03 normal">
Скачайте наш эксклюзивный гид и первыми узнайте о новейших
тенденциях, инновационных материалах и вдохновляющих коллекциях,
которые определят будущее туркменской текстильной индустрии
</p>
<Button
className="bg-reverse_primary w-fit mx-auto hover:bg-reverse_primary/90 text-on_secondary_container"
size={"sm"}
>
Скачать гид
</Button>
</div>
</section>
</div>
);
}

View File

@ -8,3 +8,4 @@ export { default as NewsInner } from "./news-inner";
export { default as News } from "./news";
export { default as Participants } from "./participants";
export { default as StendForm } from "./stend-form";
export { default as Impressions } from "./impressions";