Refactor AvailableDemos component to utilize getStreamingProjects for data retrieval, enhancing localization support. Remove unused getProjects query and related constants, and adjust project rendering logic for improved responsiveness and swipe handling.

This commit is contained in:
2026-04-16 14:30:50 +05:00
parent a92ce6a6ab
commit dbefb051da
7 changed files with 71 additions and 66 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

+58
View File
@@ -0,0 +1,58 @@
import type { IProject } from "@/types";
const BASE: Omit<
IProject,
| "id"
| "title"
| "englishTitle"
| "city"
| "englishCity"
| "buildFilename"
| "image"
> = {
description: "",
stage: 0,
releaseDate: "2026-01-01T00:00:00.000Z",
tags: [],
};
const RU_PROJECTS: IProject[] = [
{
...BASE,
id: "revolution-towers",
title: "МФК «Revolution towers»",
englishTitle: "Revolution Towers",
city: "Россия, Екатеринбург",
englishCity: "Russia, Yekaterinburg",
buildFilename: "nksJukovaDev",
image: "/img/projects/nks.jpg",
},
{
...BASE,
id: "life-residence",
title: "ЖК «Life Резиденция»",
englishTitle: "Life Residence",
city: "Россия, Тюмень",
englishCity: "Russia, Tyumen",
buildFilename: "lifeResidence",
image: "/img/projects/liferes.jpg",
},
];
const EN_PROJECTS: IProject[] = [
{
...BASE,
id: "upside-towers",
title: "Upside Towers",
englishTitle: "Upside Towers",
city: "Russia, Moscow",
englishCity: "Russia, Moscow",
buildFilename: "upsideTowersDevEn",
image: "/img/projects/upside.jpg",
},
];
/** Demo projects for the streaming section, selected by UI language. */
export function getStreamingProjects(i18nLanguage: string): IProject[] {
return i18nLanguage.startsWith("ru") ? RU_PROJECTS : EN_PROJECTS;
}
+13 -17
View File
@@ -1,30 +1,26 @@
import { useRef, useState, type MouseEvent as ReactMouseEvent } from "react"; import { useMemo, useRef, useState, type MouseEvent as ReactMouseEvent } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import BR from "@/components/Layout/LineBreak"; import BR from "@/components/Layout/LineBreak";
import { import { getStreamingProjects } from "@/data/streamingProjects";
REMOTE_DEMO_TAG,
useGetProjectsQuery,
} from "@/queries/getProjects";
import { StreamingProject } from "./StreamingProject"; import { StreamingProject } from "./StreamingProject";
import { useSwipeable } from "react-swipeable"; import { useSwipeable } from "react-swipeable";
import { useMediaQueries } from "@/hooks/useMediaQueries"; import { useMediaQueries } from "@/hooks/useMediaQueries";
export default function AvailableDemos() { export default function AvailableDemos() {
const { t } = useTranslation(); const { t, i18n } = useTranslation();
const { isMd } = useMediaQueries(); const { isMd } = useMediaQueries();
const { data: streamingProjects } = useGetProjectsQuery(REMOTE_DEMO_TAG); const projects = useMemo(
() => getStreamingProjects(i18n.language),
[i18n.language]
);
const [current, setCurrent] = useState(0); const [current, setCurrent] = useState(0);
const projects = streamingProjects ?? [];
const slideCount = Math.min(projects.length + 1, 4); const slideCount = projects.length + 1;
const handlers = useSwipeable({ const handlers = useSwipeable({
onSwipedLeft: () => onSwipedLeft: () =>
setCurrent((prev) => (prev + 1) % Math.max(slideCount, 1)), setCurrent((prev) => (prev + 1) % Math.max(slideCount, 1)),
onSwipedRight: () => onSwipedRight: () =>
setCurrent( setCurrent((prev) => (prev + projects.length) % Math.max(slideCount, 1)),
(prev) =>
(prev + Math.min(projects.length, 4)) % Math.max(slideCount, 1)
),
trackMouse: true, trackMouse: true,
preventScrollOnSwipe: true, preventScrollOnSwipe: true,
touchEventOptions: { passive: false }, touchEventOptions: { passive: false },
@@ -65,7 +61,7 @@ export default function AvailableDemos() {
className="grid lg:hidden md:hidden grid-cols-4 gap-3 px-5 [scrollbar-width:none] relative max-md:aspect-[340/344] [transform-style:preserve-3d] items-stretch mb-[5.556vw]" className="grid lg:hidden md:hidden grid-cols-4 gap-3 px-5 [scrollbar-width:none] relative max-md:aspect-[340/344] [transform-style:preserve-3d] items-stretch mb-[5.556vw]"
{...handlers} {...handlers}
> >
{projects.slice(0, 3).map((project, index, { length }) => ( {projects.map((project, index, { length }) => (
<StreamingProject <StreamingProject
key={project.id} key={project.id}
{...project} {...project}
@@ -112,12 +108,12 @@ export default function AvailableDemos() {
<div <div
ref={sliderRef} ref={sliderRef}
onMouseDown={isMd ? onSliderMouseDown : undefined} onMouseDown={isMd ? onSliderMouseDown : undefined}
className="max-md:hidden flex md:-mx-[2.604vw] md:w-[calc(100%+5.208vw)] md:px-[2.604vw] lg:gap-[0.833vw] md:gap-[1.563vw] lg:h-[27.5vw] md:h-[51.563vw] md:overflow-x-scroll hide-scrollbars max-md:cursor-grab active:cursor-grabbing select-none touch-pan-x" className="max-md:hidden flex md:-mx-[2.604vw] md:w-[calc(100%+5.208vw)] md:px-[2.604vw] lg:gap-[0.833vw] md:gap-[1.563vw] lg:h-[27.5vw] md:h-[51.563vw] md:overflow-x-scroll hide-scrollbars lg:overflow-x-visible max-md:cursor-grab active:cursor-grabbing lg:cursor-default select-none touch-pan-x"
> >
{projects.slice(0, 3).map((project, index, { length }) => ( {projects.map((project, index, { length }) => (
<div <div
key={project.id} key={project.id}
className={`w-full ${index === 0 ? "flex-auto" : "flex-1"}`} className="w-full min-w-0 flex-1 basis-0 shrink"
> >
<StreamingProject <StreamingProject
{...project} {...project}
-47
View File
@@ -1,47 +0,0 @@
import { api, hasApiConfigured } from "@/lib/api";
import type { IProject } from "@/types";
import { queryKeys } from "@/queries/keys";
import { queryOptions, useQuery } from "@tanstack/react-query";
/** Тег фильтра на стороне API (как в CMS). */
export const REMOTE_DEMO_TAG = "Удаленная демонстрация";
function releaseTimestamp(p: IProject): number {
const t = Date.parse(p.releaseDate);
return Number.isNaN(t) ? 0 : t;
}
export function useGetProjectsQuery(tags?: string | string[]) {
const tagList =
tags && tags.length > 0
? Array.isArray(tags)
? tags
: [tags]
: [];
return useQuery(
queryOptions({
queryKey: queryKeys.projectsWithTags(tagList),
queryFn: () => {
if (tagList.length === 0) {
return api.get("projects").json<IProject[]>();
}
const qs = tagList
.map((tag) => `tags=${encodeURIComponent(tag)}`)
.join("&");
return api.get(`projects?${qs}`).json<IProject[]>();
},
enabled: hasApiConfigured,
select:
tags === REMOTE_DEMO_TAG ||
(Array.isArray(tags) &&
tags.length === 1 &&
tags[0] === REMOTE_DEMO_TAG)
? (data) =>
[...data]
.sort((a, b) => releaseTimestamp(b) - releaseTimestamp(a))
.slice(0, 3)
: undefined,
})
);
}
-2
View File
@@ -1,5 +1,3 @@
export const queryKeys = { export const queryKeys = {
projects: ["projects"] as const,
projectsWithTags: (tags: string[]) => ["projects", ...tags] as const,
countryCode: ["countryCode"] as const, countryCode: ["countryCode"] as const,
}; };