added new page
This commit is contained in:
parent
15f05cbdc3
commit
e6327ba6b8
|
|
@ -12,6 +12,9 @@ export { LangMenu } from "./lang-menu";
|
|||
export { Menu } from "./menu";
|
||||
export { Chevron } from "./chevron";
|
||||
export { Loader } from "./loader";
|
||||
export { Tabs } from "./tabs";
|
||||
export { ParticipantItem } from "./participant-item";
|
||||
export { MediaModal } from "./media-modal";
|
||||
|
||||
export * from "./about";
|
||||
export * from "./home";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
import { FC, useEffect, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChevronLeftIcon, ChevronRightIcon, X } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import useEmblaCarousel from "embla-carousel-react";
|
||||
import { useScrollLock } from "usehooks-ts";
|
||||
import { usePhotos } from "@/hooks/tanstack/use-photos";
|
||||
import { useVideos } from "@/hooks/tanstack/use-videos";
|
||||
|
||||
interface Props {
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
activeItem: { id: number; type: string };
|
||||
setActiveItem: ({ id, type }: { id: number; type: "string" }) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const MediaModal: FC<Props> = ({ className, setIsOpen, activeItem }) => {
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel({
|
||||
align: "center",
|
||||
loop: false,
|
||||
dragFree: false,
|
||||
slidesToScroll: 1,
|
||||
});
|
||||
|
||||
const { data } = usePhotos(1);
|
||||
const { data: videos } = useVideos(1);
|
||||
|
||||
useScrollLock();
|
||||
|
||||
const [canScrollPrev, setCanScrollPrev] = useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!emblaApi) return;
|
||||
|
||||
const onSelect = () => {
|
||||
setCanScrollPrev(emblaApi.canScrollPrev());
|
||||
setCanScrollNext(emblaApi.canScrollNext());
|
||||
};
|
||||
|
||||
emblaApi.on("select", onSelect);
|
||||
onSelect();
|
||||
|
||||
return () => {
|
||||
emblaApi.off("select", onSelect);
|
||||
};
|
||||
}, [emblaApi]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!emblaApi || !data) return;
|
||||
|
||||
emblaApi.scrollTo(activeItem.id, true);
|
||||
}, [emblaApi, data, activeItem.id]);
|
||||
|
||||
const slides =
|
||||
activeItem.type === "photo"
|
||||
? data?.photos?.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="embla__slide flex-[0_0_100%] h-[350px] md:h-[500px] lg:h-[700px] lg:px-[20%] flex items-center justify-center"
|
||||
>
|
||||
<img
|
||||
src={item?.photo?.path}
|
||||
alt={item.photo.file_name}
|
||||
className="max-h-full max-w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
: videos?.videos?.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="embla__slide flex-[0_0_100%] h-[350px] md:h-[500px] lg:h-[700px] lg:px-[20%] flex items-center justify-center"
|
||||
>
|
||||
<video
|
||||
src={item?.video?.path ?? ""}
|
||||
controls
|
||||
autoPlay
|
||||
muted
|
||||
className="max-h-full max-w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className={cn(
|
||||
"fixed z-[100] top-0 left-0 pb-40 pt-28 lg:px-28 px-6 overflow-hidden min-h-screen w-full bg-black/90",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<X
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="absolute top-10 right-6 p-3 md:size-20 size-16 cursor-pointer z-50 text-white hover:scale-110 transition-all duration-300"
|
||||
/>
|
||||
{
|
||||
<div className="hidden md:block">
|
||||
<ChevronLeftIcon
|
||||
onClick={() => canScrollPrev && emblaApi?.scrollPrev()}
|
||||
className={cn(
|
||||
"absolute left-10 top-1/2 -translate-y-1/2 text-white size-20 cursor-pointer hover:scale-110 transition-all duration-300 z-40",
|
||||
!canScrollPrev && "opacity-20 pointer-events-none"
|
||||
)}
|
||||
/>
|
||||
<ChevronRightIcon
|
||||
onClick={() => canScrollNext && emblaApi?.scrollNext()}
|
||||
className={cn(
|
||||
"absolute right-10 top-1/2 -translate-y-1/2 text-white size-20 cursor-pointer hover:scale-110 transition-all duration-300 z-40",
|
||||
!canScrollNext && "opacity-20 pointer-events-none"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div ref={emblaRef} className="embla overflow-hidden">
|
||||
<div className="embla__container flex">{slides}</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import { FC } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useLangStore } from "@/store/lang";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
index: number;
|
||||
name: string;
|
||||
image?: {
|
||||
path: string;
|
||||
};
|
||||
image_country: {
|
||||
path?: string;
|
||||
};
|
||||
country: string;
|
||||
about: string;
|
||||
arr: number;
|
||||
}
|
||||
|
||||
export const ParticipantItem: FC<Props> = ({
|
||||
className,
|
||||
index,
|
||||
name,
|
||||
image,
|
||||
image_country,
|
||||
country,
|
||||
about,
|
||||
arr,
|
||||
}) => {
|
||||
const lang = useLangStore((state) => state.lang);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* MOBILE */}
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
"flex border-b md:hidden py-2 border-outline_v",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-1 flex-col items-start gap-4">
|
||||
<div className="flex items-center w-full">
|
||||
<div className="flex items-center flex-[0_0_80%] gap-3">
|
||||
<h3 className="text-xs normal flex-[0_0_90px]">
|
||||
{lang === "ru" ? "Название" : "Company"}:
|
||||
</h3>
|
||||
<h4 className="text-xs flex-[0_0_70%]">{name}</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center w-full">
|
||||
<div className="flex items-center flex-[0_0_80%] gap-3">
|
||||
<h3 className="text-xs normal flex-[0_0_90px]">
|
||||
{lang === "ru" ? "Страна" : "Country"}:
|
||||
</h3>
|
||||
|
||||
<div className="flex items-center flex-[0_0_50%] gap-2">
|
||||
<img
|
||||
src={image_country?.path}
|
||||
alt="flag"
|
||||
className="size-4 flex-[0_0_16px] object-contain"
|
||||
/>
|
||||
<h4 className="text-xs flex-1">{country}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center ">
|
||||
<div className="flex items-center flex-[0_0_80%] gap-3">
|
||||
<h3 className="text-xs normal flex-[0_0_90px]">
|
||||
{lang === "ru" ? "Сфера" : "Industry"}:
|
||||
</h3>
|
||||
<h4 className="text-xs flex-1">{about}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-[0_0_88px] size-[88px] bg-white ">
|
||||
<img
|
||||
src={image?.path}
|
||||
alt="company"
|
||||
className="size-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* DESKTOP */}
|
||||
<div
|
||||
className={cn(
|
||||
"md:flex hidden p-4 ",
|
||||
arr !== index + 1 && "border-b border-outline_v"
|
||||
)}
|
||||
>
|
||||
<div className="flex-[0_0_45.54%] flex gap-8">
|
||||
<div className="flex-[0_0_88px] sm:flex-[0_0_128px] size-[88px] bg-white sm:size-[128px]">
|
||||
<img
|
||||
src={image?.path}
|
||||
alt="company"
|
||||
className="size-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<h3 className="text-lg xl:mr-20 mr-10">{name}</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex-[0_0_19.80%] flex gap-2.5">
|
||||
<img src={image_country?.path} alt="flag" className="size-4" />
|
||||
<h4 className="text-sm normal">{country}</h4>
|
||||
</div>
|
||||
|
||||
<h3 className="text-sm normal md:ml-1 flex-[0_0_34.65%]">{about}</h3>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
import { FC, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import useEmblaCarousel from "embla-carousel-react";
|
||||
import { useMediaQuery } from "usehooks-ts";
|
||||
import { useLangStore } from "@/store/lang";
|
||||
|
||||
const tabs = [
|
||||
{ id: 0, title: "Все компании", titleEn: "All companies" },
|
||||
{
|
||||
id: 3,
|
||||
title: "Государственные учреждения ",
|
||||
titleEn: "Government Institutions",
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: "Местные компании",
|
||||
titleEn: "Local companies",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Иностранные компании",
|
||||
titleEn: "Foreign companies",
|
||||
},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
state: number;
|
||||
setState: (val: number) => void;
|
||||
data?: typeof tabs;
|
||||
}
|
||||
|
||||
export const Tabs: FC<Props> = ({
|
||||
className,
|
||||
setState,
|
||||
state,
|
||||
data = tabs,
|
||||
}) => {
|
||||
const lang = useLangStore((state) => state.lang);
|
||||
const isDesktop = useMediaQuery("(min-width: 768px)");
|
||||
const [indicatorStyle, setIndicatorStyle] = useState({ left: 0, width: 0 });
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel(
|
||||
{
|
||||
axis: "x",
|
||||
align: "start",
|
||||
containScroll: "trimSnaps",
|
||||
},
|
||||
[]
|
||||
);
|
||||
const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const updateIndicator = useCallback(() => {
|
||||
const activeTabElement = tabRefs.current[state];
|
||||
if (!activeTabElement) return;
|
||||
|
||||
if (isDesktop) {
|
||||
setIndicatorStyle({
|
||||
left: activeTabElement.offsetLeft,
|
||||
width: activeTabElement.offsetWidth,
|
||||
});
|
||||
} else if (emblaApi) {
|
||||
const emblaNode = emblaApi.rootNode();
|
||||
const emblaContainer = emblaApi.containerNode();
|
||||
if (!emblaNode || !emblaContainer) return;
|
||||
|
||||
const scrollLeft = emblaContainer.scrollLeft;
|
||||
const tabLeft = activeTabElement.offsetLeft;
|
||||
|
||||
setIndicatorStyle({
|
||||
left: tabLeft - scrollLeft,
|
||||
width: activeTabElement.offsetWidth,
|
||||
});
|
||||
}
|
||||
}, [state, isDesktop, emblaApi]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDesktop && emblaApi) return;
|
||||
|
||||
emblaApi?.scrollTo(state);
|
||||
|
||||
updateIndicator();
|
||||
}, [emblaApi, state, updateIndicator, isDesktop]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!emblaApi) return;
|
||||
|
||||
updateIndicator();
|
||||
|
||||
emblaApi.on("scroll", updateIndicator);
|
||||
emblaApi.on("resize", updateIndicator);
|
||||
|
||||
return () => {
|
||||
emblaApi.off("scroll", updateIndicator);
|
||||
emblaApi.off("resize", updateIndicator);
|
||||
};
|
||||
}, [emblaApi, updateIndicator]);
|
||||
|
||||
const handleTabClick = useCallback(
|
||||
(index: number) => {
|
||||
setState(index);
|
||||
if (!isDesktop && emblaApi) {
|
||||
emblaApi.scrollTo(index);
|
||||
}
|
||||
},
|
||||
[emblaApi, setState, isDesktop]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn("relative mx-auto", className)}
|
||||
style={{ width: "fit-content", maxWidth: "100%" }}
|
||||
>
|
||||
{isDesktop ? (
|
||||
<div className="flex" role="tablist">
|
||||
{data?.map((tab, index) => (
|
||||
<button
|
||||
ref={(el) => (tabRefs.current[index] = el)}
|
||||
key={tab.id}
|
||||
role="tab"
|
||||
aria-selected={state === index}
|
||||
className={cn(
|
||||
"shrink-0 text-center relative h-12 mx-4 py-2 text-sm md:text-base whitespace-nowrap transition-all",
|
||||
state === index ? "text-primary" : "text-on_surface_v"
|
||||
)}
|
||||
onClick={() => handleTabClick(index)}
|
||||
>
|
||||
{lang === "ru" ? tab.title : tab.titleEn}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div ref={emblaRef} className="" role="tablist">
|
||||
<div className="flex">
|
||||
{data?.map((tab, index) => (
|
||||
<button
|
||||
ref={(el) => (tabRefs.current[index] = el)}
|
||||
key={tab.id}
|
||||
role="tab"
|
||||
aria-selected={state === index}
|
||||
className={cn(
|
||||
"shrink-0 text-center relative after:transition-all after:rounded after:w-full after:h-0.5 after:bg-primary after:opacity-0 after:absolute after:bottom-0 after:left-0 h-12 mx-4 py-2 text-sm md:text-base whitespace-nowrap transition-all",
|
||||
state === index
|
||||
? "text-primary after:opacity-100"
|
||||
: "text-on_surface_v"
|
||||
)}
|
||||
onClick={() => handleTabClick(index)}
|
||||
>
|
||||
{lang === "ru" ? tab.title : tab.titleEn}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="absolute md:block hidden bottom-0 h-[3px] rounded-t-[2px] bg-primary transition-all duration-200"
|
||||
style={indicatorStyle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { getParticipants } from "@/services/service";
|
||||
import { useLangStore } from "@/store/lang";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
export const useParticipants = () => {
|
||||
const lang = useLangStore((state) => state.lang);
|
||||
|
||||
const { data, isPending } = useQuery({
|
||||
queryKey: ["participants", lang],
|
||||
queryFn: () => getParticipants(lang),
|
||||
select: ({ data }) => data.data,
|
||||
});
|
||||
|
||||
return { data, isPending };
|
||||
};
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getPhotos } from "@/services/service";
|
||||
|
||||
export const usePhotos = (id: number) => {
|
||||
const { data, isPending, isError } = useQuery({
|
||||
queryKey: ["photos", id],
|
||||
queryFn: () => getPhotos(id),
|
||||
select: ({ data }) => data.data,
|
||||
});
|
||||
|
||||
return { data, isPending, isError };
|
||||
};
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getVideos } from "@/services/service";
|
||||
|
||||
export const useVideos = (id: number) => {
|
||||
const { data, isPending, isError } = useQuery({
|
||||
queryKey: ["videos", id],
|
||||
queryFn: () => getVideos(id),
|
||||
select: ({ data }) => data.data,
|
||||
});
|
||||
|
||||
return { data, isPending, isError };
|
||||
};
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
{ "text": "About exhibition", "link": "/about" },
|
||||
{
|
||||
"text": "Media",
|
||||
"link": ""
|
||||
"link": "/media"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
{ "text": "Why visit?", "link": "" },
|
||||
{
|
||||
"text": "List of Participants",
|
||||
"link": ""
|
||||
"link": "/participants"
|
||||
},
|
||||
{
|
||||
"text": "Programme",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
{ "text": "О выставке", "link": "/about" },
|
||||
{
|
||||
"text": "Медиа",
|
||||
"link": ""
|
||||
"link": "/media"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
{ "text": "Почему стоит посетить?", "link": "" },
|
||||
{
|
||||
"text": "Список участников",
|
||||
"link": ""
|
||||
"link": "/participants"
|
||||
},
|
||||
{
|
||||
"text": "Программа",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ const News = lazy(() => import(/* webpackPrefetch: true */ "./pages/news"));
|
|||
const NewsInner = lazy(
|
||||
() => import(/* webpackPrefetch: true */ "./pages/news-inner")
|
||||
);
|
||||
const Participants = lazy(
|
||||
() => import(/* webpackPrefetch: true */ "./pages/participants")
|
||||
);
|
||||
const Media = lazy(() => import(/* webpackPrefetch: true */ "./pages/media"));
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
|
|
@ -35,6 +39,8 @@ const router = createBrowserRouter([
|
|||
{ element: <Contacts />, path: "/contacts" },
|
||||
{ element: <News />, path: "/news" },
|
||||
{ element: <NewsInner />, path: "/news/:id" },
|
||||
{ element: <Participants />, path: "/participants" },
|
||||
{ element: <Media />, path: "/media" },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
import { FC, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useLangStore } from "@/store/lang";
|
||||
import { Loader, MediaModal, Tabs } from "@/components/shared";
|
||||
import { AnimatePresence } from "motion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Play } from "lucide-react";
|
||||
import { Container, Cover } from "@/components/layout";
|
||||
import { usePhotos } from "@/hooks/tanstack/use-photos";
|
||||
import { useVideos } from "@/hooks/tanstack/use-videos";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const momentsTabs = [
|
||||
{
|
||||
id: 0,
|
||||
title: "Фото",
|
||||
titleEn: "Photo",
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: "Видео",
|
||||
titleEn: "Video",
|
||||
},
|
||||
];
|
||||
|
||||
const Media: FC<Props> = ({ className }) => {
|
||||
const [state, setState] = useState(0);
|
||||
const { data, isPending } = usePhotos(1);
|
||||
const { data: videos } = useVideos(1);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [activeItem, setActiveItem] = useState({ id: 0, type: "photo" });
|
||||
|
||||
const lang = useLangStore((state) => state.lang);
|
||||
|
||||
const onItem = ({ id, type }: { id: number; type: string }) => {
|
||||
setIsModalOpen(true);
|
||||
setActiveItem({ id, type });
|
||||
};
|
||||
|
||||
console.log(videos);
|
||||
const { t } = useTranslation("main");
|
||||
|
||||
const [isCollapse, setIsCollapse] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{isModalOpen && (
|
||||
<MediaModal
|
||||
activeItem={activeItem}
|
||||
setActiveItem={setActiveItem}
|
||||
setIsOpen={setIsModalOpen}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<section className={cn("", className)}>
|
||||
<Cover title={lang === "ru" ? "Моменты" : "Moments"} />
|
||||
|
||||
<Container className="page-bottom md:pt-10 pt-6">
|
||||
{isPending ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<div className="flex justify-center flex-col">
|
||||
<Tabs
|
||||
state={state}
|
||||
setState={setState}
|
||||
data={momentsTabs}
|
||||
className="mb-6"
|
||||
/>
|
||||
<h3 className="md:text-3xl text-2xl mb-6">2025</h3>
|
||||
<div className="grid lg:grid-cols-4 lg:gap-y-4 lg:gap-x-6 md:gap-6 gap-4 grid-cols-2 place-items-center">
|
||||
{state === 0
|
||||
? data?.photos
|
||||
?.slice(0, isCollapse ? 1000 : 16)
|
||||
?.map((photo, i) => (
|
||||
<div
|
||||
onClick={() => onItem({ id: i, type: "photo" })}
|
||||
key={i}
|
||||
className="cursor-pointer embla__slide basis-1/1 overflow-hidden"
|
||||
>
|
||||
<img
|
||||
src={photo?.photo?.path ?? ""}
|
||||
alt={photo?.photo?.file_name ?? "photo"}
|
||||
className="size-full object-cover hover:scale-105 duration-300 transition-all"
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
: videos?.videos?.map((video, i) => (
|
||||
<div
|
||||
onClick={() => onItem({ id: i, type: "video" })}
|
||||
key={i}
|
||||
className="cursor-pointer group embla__slide basis-1/1 overflow-hidden relative"
|
||||
>
|
||||
<Play
|
||||
fill="white"
|
||||
size={20}
|
||||
color="white"
|
||||
className="absolute group-hover:scale-125 transition-all duration-300 top-1/2 z-10 left-1/2 -translate-x-1/2 -translate-y-1/2 "
|
||||
/>
|
||||
<div className="absolute top-0 left-0 size-full bg-[#2C57A752]/[32%]" />
|
||||
<img
|
||||
src={video?.video_photo?.path ?? ""}
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{data?.photos?.length &&
|
||||
data?.photos?.length > 16 &&
|
||||
!isCollapse && (
|
||||
<Button
|
||||
onClick={() => setIsCollapse(true)}
|
||||
className="mx-auto w-[288px] mt-10 text-on_surface"
|
||||
size={"lg"}
|
||||
variant={"outline"}
|
||||
>
|
||||
{t("media.button")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Media;
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import { FC, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useLangStore } from "@/store/lang";
|
||||
import { Container } from "@/components/layout";
|
||||
import { Loader, ParticipantItem, Tabs } from "@/components/shared";
|
||||
import { useParticipants } from "@/hooks/tanstack/use-participants";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Participants: FC<Props> = ({ className }) => {
|
||||
const lang = useLangStore((state) => state.lang);
|
||||
|
||||
const { data, isPending } = useParticipants();
|
||||
|
||||
const dataHeader = {
|
||||
company: lang === "ru" ? "Название компании" : "Company name",
|
||||
country: lang === "ru" ? "Страна" : "Country",
|
||||
industry: lang === "ru" ? "Сфера деятельности" : "Industry",
|
||||
};
|
||||
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
const splitedData = data?.[0]?.participants.concat(
|
||||
data?.[1]?.participants,
|
||||
data?.[2]?.participants
|
||||
);
|
||||
|
||||
const filteredData = data?.filter((item) =>
|
||||
activeTab === 1
|
||||
? item.id === 3
|
||||
: activeTab === 2
|
||||
? item.id === 1
|
||||
: item.id === 2
|
||||
);
|
||||
|
||||
console.log(activeTab);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<section className={cn("page-padding overflow-x-hidden", className)}>
|
||||
<h1 className="text-center md:text-5xl text-3xl mb-4">
|
||||
{lang === "ru" ? "Список участников" : "List of participants"}
|
||||
</h1>
|
||||
<Tabs
|
||||
state={activeTab}
|
||||
setState={setActiveTab}
|
||||
className="mb-6 md:w-fit"
|
||||
/>
|
||||
|
||||
<div className="p-2 sm:p-0 bg-surface_container overflow-hidden">
|
||||
<div className="sm:flex items-center hidden h-[52px] border-b border-outline_v">
|
||||
<h3 className="flex-[0_0_45.54%] pl-4">{dataHeader?.company}</h3>
|
||||
<h3 className="flex-[0_0_19.80%]">{dataHeader?.country}</h3>
|
||||
<h3 className="flex-[0_0_34.65%]">{dataHeader?.industry}</h3>
|
||||
</div>
|
||||
|
||||
{isPending ? (
|
||||
<Loader />
|
||||
) : activeTab === 0 ? (
|
||||
splitedData?.map((item, index, arr) => (
|
||||
<ParticipantItem
|
||||
{...item}
|
||||
arr={arr?.length}
|
||||
index={index}
|
||||
image={item.image ?? { path: "" }}
|
||||
image_country={item.image_country ?? { path: "" }}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
filteredData?.[0]?.participants?.map((item, index, arr) => (
|
||||
<ParticipantItem
|
||||
{...item}
|
||||
arr={arr?.length}
|
||||
index={index}
|
||||
image={item.image ?? { path: "" }}
|
||||
image_country={item.image_country ?? { path: "" }}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Participants;
|
||||
|
|
@ -11,6 +11,9 @@ import { IndustriesType } from "@/hooks/tanstack/use-industries";
|
|||
import { TimeType } from "@/hooks/tanstack/use-exhibition-time";
|
||||
import { PartnersType } from "@/hooks/tanstack/use-partners";
|
||||
import { NewsInnerType, NewsType } from "./types/news.type";
|
||||
import { ParticipantsType } from "./types/participants.type";
|
||||
import { PhotoTypes } from "./types/photo.type";
|
||||
import { VideoTypes } from "./types/videos.type";
|
||||
|
||||
const axios_url = axios.create({
|
||||
baseURL: "https://turkmentextile.turkmenexpo.com/app/api/v1/",
|
||||
|
|
@ -135,3 +138,25 @@ export const getNewsInner = async (id: number, lang: string) => {
|
|||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getParticipants = async (lang: LangState["lang"]) => {
|
||||
const data = axios_url<ParticipantsType>("participants", {
|
||||
headers: {
|
||||
"Accept-Language": lang,
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getPhotos = async (id: number) => {
|
||||
const data = axios_url<PhotoTypes>("photos/category/" + id);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getVideos = async (id: number) => {
|
||||
const data = axios_url<VideoTypes>("videos/category/" + id);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
export interface ParticipantsType {
|
||||
status: string;
|
||||
data: Datum[];
|
||||
}
|
||||
|
||||
export interface Datum {
|
||||
id: number;
|
||||
name: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
participants: Participant[];
|
||||
}
|
||||
|
||||
export interface Participant {
|
||||
id: number;
|
||||
name: string;
|
||||
about: string;
|
||||
country: string;
|
||||
participant_category_id: number;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
image: Image | null;
|
||||
image_country: Image | null;
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
id: number;
|
||||
disk_name: string;
|
||||
file_name: string;
|
||||
file_size: number;
|
||||
content_type: ContentType;
|
||||
title: null;
|
||||
description: null;
|
||||
field: Field;
|
||||
sort_order: number;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
path: string;
|
||||
extension: Extension;
|
||||
}
|
||||
|
||||
export enum ContentType {
|
||||
ImagePNG = "image/png",
|
||||
}
|
||||
|
||||
export enum Extension {
|
||||
PNG = "png",
|
||||
}
|
||||
|
||||
export enum Field {
|
||||
Image = "image",
|
||||
ImageCountry = "image_country",
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
export interface PhotoTypes {
|
||||
status: string;
|
||||
data: Data;
|
||||
}
|
||||
|
||||
export interface Data {
|
||||
id: number;
|
||||
name: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
photos: PhotoElement[];
|
||||
}
|
||||
|
||||
export interface PhotoElement {
|
||||
id: number;
|
||||
category_photo_media_id: number;
|
||||
name: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
photo: PhotoPhoto;
|
||||
}
|
||||
|
||||
export interface PhotoPhoto {
|
||||
id: number;
|
||||
disk_name: string;
|
||||
file_name: string;
|
||||
file_size: number;
|
||||
content_type: ContentType;
|
||||
title: null;
|
||||
description: null;
|
||||
field: Field;
|
||||
sort_order: number;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
path: string;
|
||||
extension: Extension;
|
||||
}
|
||||
|
||||
export enum ContentType {
|
||||
ImageWebp = "image/webp",
|
||||
}
|
||||
|
||||
export enum Extension {
|
||||
Webp = "webp",
|
||||
}
|
||||
|
||||
export enum Field {
|
||||
Photo = "photo",
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
export interface VideoTypes {
|
||||
status: string;
|
||||
data: Data;
|
||||
}
|
||||
|
||||
export interface Data {
|
||||
id: number;
|
||||
name: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
videos: VideoElement[];
|
||||
}
|
||||
|
||||
export interface VideoElement {
|
||||
id: number;
|
||||
name: string;
|
||||
category_video_media_id: number;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
video: VideoPhotoClass;
|
||||
video_photo: VideoPhotoClass;
|
||||
}
|
||||
|
||||
export interface VideoPhotoClass {
|
||||
id: number;
|
||||
disk_name: string;
|
||||
file_name: string;
|
||||
file_size: number;
|
||||
content_type: string;
|
||||
title: null;
|
||||
description: null;
|
||||
field: string;
|
||||
sort_order: number;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
path: string;
|
||||
extension: string;
|
||||
}
|
||||
Loading…
Reference in New Issue