upd
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
"@tinymce/tinymce-react": "^5.1.1",
|
||||
"countries-phone-masks": "^1.1.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"decline-word": "^1.4.0",
|
||||
"framer-motion": "^11.17.0",
|
||||
"html-react-parser": "^5.1.18",
|
||||
"jose": "^5.9.3",
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
HydrationBoundary,
|
||||
QueryClient,
|
||||
} from '@tanstack/react-query';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
export default async function HomePage() {
|
||||
const queryClient = new QueryClient();
|
||||
@@ -42,9 +41,9 @@ export default async function HomePage() {
|
||||
});
|
||||
|
||||
await queryClient.prefetchQuery({
|
||||
queryKey: ['projects', 'Удаленная дмонстрация'],
|
||||
queryKey: ['projects', 'Удаленная демонстрация'],
|
||||
queryFn: async ({ queryKey: [, tag] }) =>
|
||||
await api.get('projects/tags=' + tag).json<IProject[]>(),
|
||||
await api.get('projects?tags=' + tag).json<IProject[]>(),
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -52,15 +51,12 @@ export default async function HomePage() {
|
||||
<Motivation />
|
||||
<Presentation />
|
||||
<Statistics />
|
||||
{/* <ProjectsMap /> */}
|
||||
<NewMap />
|
||||
<Streaming />
|
||||
<Calculator />
|
||||
<Reviews />
|
||||
<Awards />
|
||||
<Suspense>
|
||||
<Projects />
|
||||
</Suspense>
|
||||
<Projects />
|
||||
<Clients />
|
||||
</HydrationBoundary>
|
||||
);
|
||||
|
||||
@@ -57,7 +57,7 @@ export function AwardItem({
|
||||
: 'lg:self-start'
|
||||
}`}
|
||||
>
|
||||
<div className="relative lg:min-w-[calc(276/458*100%)] sm:min-w-[calc(276/362*100%)] min-w-[calc(276/340*100%)] lg:self-end">
|
||||
<div className="relative lg:max-w-[calc(276/458*100%)] sm:max-w-[calc(276/362*100%)] max-w-[calc(276/340*100%)] lg:self-end self-center">
|
||||
<Image
|
||||
src={image}
|
||||
fill
|
||||
|
||||
@@ -14,7 +14,7 @@ import { StatsColumn } from './StatColumn';
|
||||
|
||||
export function Calculator() {
|
||||
const [selectedRegion, setSelectedRegion] = useState<Region>();
|
||||
const [consultations, setConsultations] = useState<number>(100);
|
||||
const [consultations, setConsultations] = useState<number>(10);
|
||||
const [implementationPeriod, setImplementationPeriod] = useState<number>(
|
||||
null!
|
||||
);
|
||||
@@ -145,7 +145,7 @@ export function Calculator() {
|
||||
/>
|
||||
)}
|
||||
<ConsultationRange
|
||||
consultations={consultations!}
|
||||
consultations={consultations}
|
||||
setConsultations={setConsultations}
|
||||
/>
|
||||
<GradientButton
|
||||
@@ -270,7 +270,7 @@ export function Calculator() {
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-[linear-gradient(to_top,#7A7A7A40,#7A7A7A30)] p-7 w-1/2 relative overflow-hidden">
|
||||
<div className="flex flex-col justify-between">
|
||||
<div className="flex flex-col justify-between h-full">
|
||||
<p className="text1 mb-11 font-medium">Ежемесячный доход</p>
|
||||
<p className="font-medium">
|
||||
<span className="line2">
|
||||
|
||||
@@ -26,11 +26,9 @@ export function ConsultationRange({
|
||||
setStart(
|
||||
Math.max(
|
||||
Math.min(
|
||||
isMouseEvent(e)
|
||||
? e.clientX
|
||||
: e.touches[0].clientX -
|
||||
root.current!.getBoundingClientRect().x -
|
||||
offset,
|
||||
(isMouseEvent(e) ? e.clientX : e.touches[0].clientX) -
|
||||
root.current!.getBoundingClientRect().x -
|
||||
offset,
|
||||
root.current!.clientWidth
|
||||
),
|
||||
root.current!.clientWidth / 35
|
||||
@@ -43,11 +41,9 @@ export function ConsultationRange({
|
||||
if (!el || !isMouseDown) return;
|
||||
const dx = Math.max(
|
||||
Math.min(
|
||||
isMouseEvent(e)
|
||||
? (e as MouseEvent).clientX
|
||||
: e.touches[0].clientX -
|
||||
root.current!.getBoundingClientRect().x -
|
||||
start,
|
||||
(isMouseEvent(e) ? e.clientX : e.touches[0].clientX) -
|
||||
root.current!.getBoundingClientRect().x -
|
||||
start,
|
||||
root.current!.clientWidth - 48
|
||||
),
|
||||
(root.current!.clientWidth - 48) / 35
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { ItemActions } from '@/components/ItemActions';
|
||||
import { CompanyFormModal } from '@/components/modals/CompanyFormModal';
|
||||
import { OpenFormModalWrapper } from '@/hocs/OpenFormModalWrapper';
|
||||
import { useMediaQueries } from '@/hooks/useMediaQueries';
|
||||
import { useGetCompaniesQuery } from '@/queries/getCompanies';
|
||||
import { GradientButton } from '@/ui/GradientButton';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
@@ -14,38 +15,61 @@ import Link from 'next/link';
|
||||
export function Clients() {
|
||||
const { data: companies } = useGetCompaniesQuery();
|
||||
|
||||
const { isLg, isSm } = useMediaQueries();
|
||||
|
||||
return (
|
||||
<div className="lg:space-y-16 space-y-10 lg:mt-40 mt-[100px]">
|
||||
<Title className="mx-auto text-center max-w-2/3">
|
||||
<Title className="mx-auto text-center max-w-3/4">
|
||||
<span className="text-gradient">
|
||||
{companies !== undefined && getCompaniesCount(companies.length)}
|
||||
</span>{' '}
|
||||
уже внедрили наш продукт в свою цепочку продаж
|
||||
</Title>
|
||||
{companies && (
|
||||
<div className="lg:grid-cols-9 sm:grid-cols-6 grid grid-cols-3 gap-2">
|
||||
{companies?.map(
|
||||
(company) =>
|
||||
company.logo && (
|
||||
<div className="relative" key={company.id}>
|
||||
<Link
|
||||
href={'/projects?companyId=' + company.id}
|
||||
className="bg-[#232425] rounded-xl aspect-square flex justify-center items-center opacity-60 hover:opacity-100 transition-opacity p-5"
|
||||
>
|
||||
<div className="relative max-h-[calc(100%)]">
|
||||
<Image
|
||||
fill
|
||||
src={process.env.NEXT_PUBLIC_S3_BUCKET + company.logo}
|
||||
className="!relative object-cover select-none pointer-events-none"
|
||||
alt={company.title}
|
||||
sizes="100%"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
<ItemActions item={company} />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
<div className="lg:grid-cols-9 sm:grid-cols-5 grid grid-cols-3 gap-2">
|
||||
{companies
|
||||
.filter((company) => company.logo)
|
||||
.map((company, index, { length }) => (
|
||||
<div
|
||||
className="relative"
|
||||
key={company.id}
|
||||
style={
|
||||
index > length - 1 - (length % (isLg ? 9 : isSm ? 5 : 3))
|
||||
? {
|
||||
transform: `translateX(${
|
||||
(Math.trunc((isLg ? 9 : isSm ? 5 : 3) / 2) -
|
||||
Math.trunc(
|
||||
(length % (isLg ? 9 : isSm ? 5 : 3)) / 2
|
||||
)) *
|
||||
100
|
||||
}%)`,
|
||||
left:
|
||||
(Math.trunc((isLg ? 9 : isSm ? 5 : 3) / 2) -
|
||||
Math.trunc(
|
||||
(length % (isLg ? 9 : isSm ? 5 : 3)) / 2
|
||||
)) *
|
||||
8,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Link
|
||||
href={'/projects?companyId=' + company.id}
|
||||
className="bg-[#232425] rounded-2xl aspect-square flex justify-center items-center opacity-60 hover:opacity-100 transition-opacity p-5"
|
||||
>
|
||||
<div className="relative">
|
||||
<Image
|
||||
fill
|
||||
src={process.env.NEXT_PUBLIC_S3_BUCKET + company.logo}
|
||||
className="!relative object-cover select-none pointer-events-none"
|
||||
alt={company.title}
|
||||
sizes="100%"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
<ItemActions item={company} />
|
||||
</div>
|
||||
))}
|
||||
<OpenFormModalWrapper
|
||||
modal={<CompanyFormModal action="create" />}
|
||||
modalName="addCompany"
|
||||
|
||||
@@ -1,90 +1,90 @@
|
||||
import { ICityProjects } from '@/types/ICityProjects';
|
||||
import { distinctCompanies } from '@/utils/distinctCompanies';
|
||||
import Image from 'next/image';
|
||||
import { Dispatch, SetStateAction, useEffect, useRef } from 'react';
|
||||
import { useHover } from 'usehooks-ts';
|
||||
// import { ICityProjects } from '@/types/ICityProjects';
|
||||
// import { distinctCompanies } from '@/utils/distinctCompanies';
|
||||
// import Image from 'next/image';
|
||||
// import { Dispatch, SetStateAction, useEffect, useRef } from 'react';
|
||||
// import { useHover } from 'usehooks-ts';
|
||||
|
||||
export function CityPoint({
|
||||
title,
|
||||
companies,
|
||||
x,
|
||||
y,
|
||||
chooseCity,
|
||||
active,
|
||||
setCurrentHovered,
|
||||
index,
|
||||
}: ICityProjects & {
|
||||
chooseCity: (_: ICityProjects) => void;
|
||||
active: boolean;
|
||||
setCurrentHovered: Dispatch<SetStateAction<number | undefined>>;
|
||||
index: number;
|
||||
}) {
|
||||
const companiesWithMapIcon = distinctCompanies(
|
||||
companies.filter((company) => company.mapIcon)
|
||||
);
|
||||
// export function CityPoint({
|
||||
// title,
|
||||
// companies,
|
||||
// x,
|
||||
// y,
|
||||
// chooseCity,
|
||||
// active,
|
||||
// setCurrentHovered,
|
||||
// index,
|
||||
// }: ICityProjects & {
|
||||
// chooseCity: (_: ICityProjects) => void;
|
||||
// active: boolean;
|
||||
// setCurrentHovered: Dispatch<SetStateAction<number | undefined>>;
|
||||
// index: number;
|
||||
// }) {
|
||||
// const companiesWithMapIcon = distinctCompanies(
|
||||
// companies.filter((company) => company.mapIcon)
|
||||
// );
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
// const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const hovered = useHover(ref);
|
||||
// const hovered = useHover(ref);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentHovered(hovered ? index : undefined);
|
||||
}, [hovered, index, setCurrentHovered]);
|
||||
// useEffect(() => {
|
||||
// setCurrentHovered(hovered ? index : undefined);
|
||||
// }, [hovered, index, setCurrentHovered]);
|
||||
|
||||
return (
|
||||
<div className="absolute" style={{ top: y + '%', left: x + '%' }}>
|
||||
<div
|
||||
className={`p-[11px] rounded-full 2xl:bg-[#37393B99] transition-colors relative z-10 ${
|
||||
active ? '2xl:bg-transparent' : ''
|
||||
}`}
|
||||
onClick={() => chooseCity({ title, x, y, companies })}
|
||||
ref={ref}
|
||||
>
|
||||
<div className="w-[10px] h-[10px] bg-white rounded-full" />
|
||||
</div>
|
||||
<div
|
||||
className={`transition-all delay-200 duration-400 opacity-0 backdrop-blur-xl rounded-r-lg rounded-t-lg p-2 min-w-[132px] ${
|
||||
active ? 'opacity-100 z-[11]' : 'opacity-0 -z-10'
|
||||
} bg-[#37393B99] absolute translate-x-5 -translate-y-[calc(100%+24px)] space-y-0.5`}
|
||||
>
|
||||
<p className="w-full font-medium leading-4 text2">{title}</p>
|
||||
<div className="relative flex py-px">
|
||||
{companiesWithMapIcon
|
||||
.slice(0, 3)
|
||||
.map(({ mapIcon, title, id }, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="relative p-px rounded-lg odd:rotate-[-3.5deg] w-[34px] h-[34px]"
|
||||
style={{
|
||||
right: 6 * index,
|
||||
zIndex: index,
|
||||
}}
|
||||
>
|
||||
{mapIcon && (
|
||||
<Image
|
||||
key={id}
|
||||
src={process.env.NEXT_PUBLIC_S3_BUCKET + '' + mapIcon}
|
||||
alt={title}
|
||||
className="!relative object-cover"
|
||||
sizes=""
|
||||
fill
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{Math.min(companiesWithMapIcon.length, 3) !== companies.length && (
|
||||
<div
|
||||
className="rounded-lg bg-[#7A7A7A] flex justify-center items-center w-[34px] h-[34px] aspect-square text-xs relative"
|
||||
style={{
|
||||
right: Math.min(companiesWithMapIcon.length, 3) * 6,
|
||||
zIndex: companies.length,
|
||||
}}
|
||||
>
|
||||
+{companies.length - Math.min(companiesWithMapIcon.length, 3)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// return (
|
||||
// <div className="absolute" style={{ top: y + '%', left: x + '%' }}>
|
||||
// <div
|
||||
// className={`p-[11px] rounded-full 2xl:bg-[#37393B99] transition-colors relative z-10 ${
|
||||
// active ? '2xl:bg-transparent' : ''
|
||||
// }`}
|
||||
// onClick={() => chooseCity({ title, x, y, companies })}
|
||||
// ref={ref}
|
||||
// >
|
||||
// <div className="w-[10px] h-[10px] bg-white rounded-full" />
|
||||
// </div>
|
||||
// <div
|
||||
// className={`transition-all delay-200 duration-400 opacity-0 backdrop-blur-xl rounded-r-lg rounded-t-lg p-2 min-w-[132px] ${
|
||||
// active ? 'opacity-100 z-[11]' : 'opacity-0 -z-10'
|
||||
// } bg-[#37393B99] absolute translate-x-5 -translate-y-[calc(100%+24px)] space-y-0.5`}
|
||||
// >
|
||||
// <p className="w-full font-medium leading-4 text2">{title}</p>
|
||||
// <div className="relative flex py-px">
|
||||
// {companiesWithMapIcon
|
||||
// .slice(0, 3)
|
||||
// .map(({ mapIcon, title, id }, index) => (
|
||||
// <div
|
||||
// key={index}
|
||||
// className="relative p-px rounded-lg odd:rotate-[-3.5deg] w-[34px] h-[34px]"
|
||||
// style={{
|
||||
// right: 6 * index,
|
||||
// zIndex: index,
|
||||
// }}
|
||||
// >
|
||||
// {mapIcon && (
|
||||
// <Image
|
||||
// key={id}
|
||||
// src={process.env.NEXT_PUBLIC_S3_BUCKET + '' + mapIcon}
|
||||
// alt={title}
|
||||
// className="!relative object-cover"
|
||||
// sizes=""
|
||||
// fill
|
||||
// />
|
||||
// )}
|
||||
// </div>
|
||||
// ))}
|
||||
// {Math.min(companiesWithMapIcon.length, 3) !== companies.length && (
|
||||
// <div
|
||||
// className="rounded-lg bg-[#7A7A7A] flex justify-center items-center w-[34px] h-[34px] aspect-square text-xs relative"
|
||||
// style={{
|
||||
// right: Math.min(companiesWithMapIcon.length, 3) * 6,
|
||||
// zIndex: companies.length,
|
||||
// }}
|
||||
// >
|
||||
// +{companies.length - Math.min(companiesWithMapIcon.length, 3)}
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
@@ -1,40 +1,133 @@
|
||||
'use client';
|
||||
|
||||
import { useGetProjectsQuery } from '@/queries/getProjects';
|
||||
import { cities } from '@/consts/cities';
|
||||
import { mapVideos } from '@/consts/mapVideos';
|
||||
import { useGetCompniesByCityQuery } from '@/queries/getCompaniesByCity';
|
||||
import { useGetProjectsCountQuery } from '@/queries/getProjectsCount';
|
||||
import { useCityPointStore } from '@/stores/useCityPointStore';
|
||||
import { ICityProjects } from '@/types/ICityProjects';
|
||||
import { ICompany } from '@/types/ICompany';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { prepositionCity } from '@/utils/prepositionCity';
|
||||
import Image from 'next/image';
|
||||
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';
|
||||
import { useHover } from 'usehooks-ts';
|
||||
|
||||
export function NewMap() {
|
||||
const { cityPoint, setCityPoint } = useCityPointStore();
|
||||
const { cityPoint } = useCityPointStore();
|
||||
|
||||
const [companies, setCompanies] = useState<ICompany[]>([]);
|
||||
const [currentHovered, setCurrentHovered] = useState<number | undefined>();
|
||||
|
||||
const { data: projects } = useGetProjectsQuery(cityPoint.title);
|
||||
|
||||
useEffect(() => {
|
||||
setCompanies(
|
||||
projects!.map(({ company }) => company)?.filter((company) => !!company)
|
||||
);
|
||||
}, []);
|
||||
|
||||
const [currentCompany, setCurrentCompany] = useState('Бруниска');
|
||||
const { data: companies } = useGetCompniesByCityQuery(cityPoint.title);
|
||||
|
||||
return (
|
||||
<div className="bg-[url(/img/pages/home/stats/new_map.png)] bg-no-repeat bg-center bg-contain aspect-[1333.4/731.98] relative">
|
||||
<div className="absolute top-[calc(94/731.98*100%)] left-[calc(679.1/1333.4*100%)] rounded-3xl overflow-hidden w-[calc(341/1333.4*100%)] h-[calc(534/731.98*100%)] bg-[#37393B99] backdrop-blur-xs">
|
||||
<video src="" className="aspect-[341/396] bg-white w-full" />
|
||||
<div className="p-[17px] flex flex-col justify-between">
|
||||
<p className="btnl font-medium opacity-60">Последнее в Москве</p>
|
||||
<div className="flex gap-1">
|
||||
{companies.map(({ id, mapIcon, title }) => (
|
||||
<div key={mapIcon}></div>
|
||||
))}
|
||||
</div>
|
||||
<div className="bg-[url(/img/pages/home/stats/new_map.png)] bg-no-repeat bg-center bg-contain aspect-[1333.4/731.98] relative max-lg:hidden">
|
||||
{cities.map((point, index) => (
|
||||
<CityPoint
|
||||
key={point.title}
|
||||
{...point}
|
||||
active={
|
||||
currentHovered === index ||
|
||||
(point.title === cityPoint.title && currentHovered === undefined)
|
||||
}
|
||||
index={index}
|
||||
setCurrentHovered={setCurrentHovered}
|
||||
/>
|
||||
))}
|
||||
{companies && <Slider companies={companies} city={cityPoint.title} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CityPoint({
|
||||
x,
|
||||
y,
|
||||
title,
|
||||
active,
|
||||
index,
|
||||
setCurrentHovered,
|
||||
}: ICityProjects & {
|
||||
active: boolean;
|
||||
index: number;
|
||||
setCurrentHovered: Dispatch<SetStateAction<number | undefined>>;
|
||||
}) {
|
||||
const { data: count } = useGetProjectsCountQuery(title);
|
||||
|
||||
const { setCityPoint } = useCityPointStore();
|
||||
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
const hovered = useHover(ref);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentHovered(hovered ? index : undefined);
|
||||
}, [hovered, index, setCurrentHovered]);
|
||||
|
||||
return (
|
||||
<button
|
||||
style={{ left: `${x}%`, top: `${y}%` }}
|
||||
ref={ref}
|
||||
onClick={() => setCityPoint({ title, x, y })}
|
||||
className={`absolute outline-none px-4 py-[9px] flex gap-1 items-center transition-colors font-medium ${
|
||||
active ? 'text-white' : 'text-[#7A7A7A]'
|
||||
}`}
|
||||
>
|
||||
<p className="heading2">{title}</p>
|
||||
<p className="btnm h-[30px]">({count})</p>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function Slider({
|
||||
companies,
|
||||
city,
|
||||
}: {
|
||||
companies: ICompany[];
|
||||
city: string;
|
||||
}) {
|
||||
const [current, setCurrent] = useState(0);
|
||||
|
||||
const src =
|
||||
companies.length &&
|
||||
mapVideos.find(({ title }) => title === companies[current].title)?.src!;
|
||||
|
||||
return (
|
||||
<div className="absolute top-[calc(94/731.98*100%)] left-[calc(679.1/1333.4*100%)] rounded-3xl overflow-hidden w-[calc(341/1333.4*100%)] flex flex-col h-[calc(534/731.98*100%)] bg-[#37393B99] backdrop-blur-xs">
|
||||
{!src ? (
|
||||
<div className="aspect-[341/396] w-full" />
|
||||
) : src.startsWith('/') ? (
|
||||
<video
|
||||
src={src}
|
||||
muted
|
||||
autoPlay
|
||||
playsInline
|
||||
loop
|
||||
className="aspect-[341/396] object-cover w-full"
|
||||
/>
|
||||
) : (
|
||||
<iframe src={src} className="aspect-[341/396]" />
|
||||
)}
|
||||
<div className="p-[17px] flex flex-col gap-4 flex-1">
|
||||
<p className="btnl font-medium opacity-60">
|
||||
Последнее в{city.startsWith('В') ? 'о' : ''} {prepositionCity(city)}
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
{companies.map(({ id, mapIcon, title }, index) => (
|
||||
<button
|
||||
onClick={() => setCurrent(index)}
|
||||
key={id}
|
||||
className={`p-1 bg-gradient-to-r from-[#FFFFFF14] rounded-2xl${
|
||||
index === current ? ' border' : ''
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
src={process.env.NEXT_PUBLIC_S3_BUCKET + mapIcon!}
|
||||
width={48}
|
||||
height={48}
|
||||
alt={title}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CityPoint() {}
|
||||
|
||||
@@ -1,102 +1,102 @@
|
||||
'use client';
|
||||
// 'use client';
|
||||
|
||||
import { cities } from '@/consts/cities';
|
||||
import { useGetProjectsQuery } from '@/queries/getProjects';
|
||||
import { useCityPointStore } from '@/stores/useCityPointStore';
|
||||
import { ICityProjects } from '@/types/ICityProjects';
|
||||
import { ICompany } from '@/types/ICompany';
|
||||
import { IProject } from '@/types/IProject';
|
||||
import { getProjectsGroupedByCities } from '@/utils/getProjectsGroupedByCities';
|
||||
import Image from 'next/image';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { CityPoint } from './CityPoint';
|
||||
import { ProjectsSlider } from './ProjectsSlider';
|
||||
// import { cities } from '@/consts/cities';
|
||||
// import { useGetProjectsQuery } from '@/queries/getProjects';
|
||||
// import { useCityPointStore } from '@/stores/useCityPointStore';
|
||||
// import { ICityProjects } from '@/types/ICityProjects';
|
||||
// import { ICompany } from '@/types/ICompany';
|
||||
// import { IProject } from '@/types/IProject';
|
||||
// import { getProjectsGroupedByCities } from '@/utils/getProjectsGroupedByCities';
|
||||
// import Image from 'next/image';
|
||||
// import { useEffect, useMemo, useState } from 'react';
|
||||
// import { CityPoint } from './CityPoint';
|
||||
// import { ProjectsSlider } from './ProjectsSlider';
|
||||
|
||||
export function ProjectsMap() {
|
||||
const { data: projects } = useGetProjectsQuery();
|
||||
// export function ProjectsMap() {
|
||||
// const { data: projects } = useGetProjectsQuery();
|
||||
|
||||
const { cityPoint, setCityPoint } = useCityPointStore();
|
||||
// const { cityPoint, setCityPoint } = useCityPointStore();
|
||||
|
||||
const [cityProjects, setCityProjects] = useState<IProject[]>([]);
|
||||
// const [cityProjects, setCityProjects] = useState<IProject[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (projects)
|
||||
setCityProjects(
|
||||
projects.filter((project) => project.city === cityPoint.title)
|
||||
);
|
||||
}, [projects, cityPoint]);
|
||||
// useEffect(() => {
|
||||
// if (projects)
|
||||
// setCityProjects(
|
||||
// projects.filter((project) => project.city === cityPoint.title)
|
||||
// );
|
||||
// }, [projects, cityPoint]);
|
||||
|
||||
return (
|
||||
projects && (
|
||||
<div className="grid xl:grid-cols-[3fr_1fr] gap-3 xl:pl-[23px] mt-16 max-xl:hidden">
|
||||
<Map projects={projects} chooseCity={setCityPoint} />
|
||||
<ProjectsSlider
|
||||
projects={cityProjects}
|
||||
city={cityPoint.title}
|
||||
count={cityProjects.length}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
// return (
|
||||
// projects && (
|
||||
// <div className="grid xl:grid-cols-[3fr_1fr] gap-3 xl:pl-[23px] mt-16 max-xl:hidden">
|
||||
// <Map projects={projects} chooseCity={setCityPoint} />
|
||||
// <ProjectsSlider
|
||||
// projects={cityProjects}
|
||||
// city={cityPoint.title}
|
||||
// count={cityProjects.length}
|
||||
// />
|
||||
// </div>
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
|
||||
export function Map({
|
||||
projects,
|
||||
chooseCity,
|
||||
}: {
|
||||
projects: IProject[];
|
||||
chooseCity: (_: ICityProjects) => void;
|
||||
}) {
|
||||
const { cityPoint } = useCityPointStore();
|
||||
// export function Map({
|
||||
// projects,
|
||||
// chooseCity,
|
||||
// }: {
|
||||
// projects: IProject[];
|
||||
// chooseCity: (_: ICityProjects) => void;
|
||||
// }) {
|
||||
// const { cityPoint } = useCityPointStore();
|
||||
|
||||
const [currentHovered, setCurrentHovered] = useState<number>();
|
||||
// const [currentHovered, setCurrentHovered] = useState<number>();
|
||||
|
||||
const groupedProjects = useMemo(
|
||||
() =>
|
||||
Array.from(
|
||||
getProjectsGroupedByCities(
|
||||
projects.filter(({ city }) =>
|
||||
cities.some(({ title }) => title === city)
|
||||
)
|
||||
).entries()
|
||||
),
|
||||
[projects]
|
||||
);
|
||||
// const groupedProjects = useMemo(
|
||||
// () =>
|
||||
// Array.from(
|
||||
// getProjectsGroupedByCities(
|
||||
// projects.filter(({ city }) =>
|
||||
// cities.some(({ title }) => title === city)
|
||||
// )
|
||||
// ).entries()
|
||||
// ),
|
||||
// [projects]
|
||||
// );
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Image
|
||||
src="/img/pages/home/stats/newmap.png"
|
||||
alt="map"
|
||||
className="!relative aspect-[1027/560] object-cover"
|
||||
fill
|
||||
sizes=""
|
||||
/>
|
||||
{Array.from(groupedProjects).map(([city, projects], index) => {
|
||||
const point = cities.find(({ title }) => title === city)!;
|
||||
// return (
|
||||
// <div className="relative">
|
||||
// <Image
|
||||
// src="/img/pages/home/stats/newmap.png"
|
||||
// alt="map"
|
||||
// className="!relative aspect-[1027/560] object-cover"
|
||||
// fill
|
||||
// sizes=""
|
||||
// />
|
||||
// {Array.from(groupedProjects).map(([city, projects], index) => {
|
||||
// const point = cities.find(({ title }) => title === city)!;
|
||||
|
||||
return (
|
||||
point && (
|
||||
<CityPoint
|
||||
active={
|
||||
currentHovered === index ||
|
||||
(cityPoint.title === city && currentHovered === undefined)
|
||||
}
|
||||
key={city}
|
||||
chooseCity={chooseCity}
|
||||
title={city}
|
||||
companies={
|
||||
projects
|
||||
.filter((project) => project.company)
|
||||
.map(({ company }) => company) as ICompany[]
|
||||
}
|
||||
x={point.x ?? 0}
|
||||
y={point.y ?? 0}
|
||||
{...{ setCurrentHovered, index }}
|
||||
/>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// return (
|
||||
// point && (
|
||||
// <CityPoint
|
||||
// active={
|
||||
// currentHovered === index ||
|
||||
// (cityPoint.title === city && currentHovered === undefined)
|
||||
// }
|
||||
// key={city}
|
||||
// chooseCity={chooseCity}
|
||||
// title={city}
|
||||
// companies={
|
||||
// projects
|
||||
// .filter((project) => project.company)
|
||||
// .map(({ company }) => company) as ICompany[]
|
||||
// }
|
||||
// x={point.x ?? 0}
|
||||
// y={point.y ?? 0}
|
||||
// {...{ setCurrentHovered, index }}
|
||||
// />
|
||||
// )
|
||||
// );
|
||||
// })}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
@@ -22,10 +22,7 @@ export function Motivation() {
|
||||
|
||||
return (
|
||||
<div className="lg:space-y-16 space-y-10">
|
||||
<Title
|
||||
headerLevel={1}
|
||||
className="sm:max-lg:text-5xl text-center 2xl:atext-[160px]"
|
||||
>
|
||||
<Title headerLevel={1} className="sm:max-lg:text-5xl text-center">
|
||||
Помогаем девелоперам
|
||||
<br />
|
||||
продавать недвижимость проще и
|
||||
|
||||
@@ -16,7 +16,7 @@ export function Infrastructure({ slide }: { slide?: number }) {
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
x: '100%',
|
||||
// x: '100%',
|
||||
}}
|
||||
className="absolute col-span-2 col-start-5 rounded-2xl p-6 xl:p-12 h-full bg-radial-[at_0%_100%] from-[#7A7A7A99] flex flex-col gap-2 z-10 backdrop-blur-[500px] select-none"
|
||||
>
|
||||
|
||||
@@ -7,7 +7,9 @@ export function Insolation({ slide }: { slide?: number }) {
|
||||
<AnimatePresence>
|
||||
{slide === 3 && (
|
||||
<motion.div
|
||||
exit={{ opacity: 0, x: '100%' }}
|
||||
initial={{ y: '100%', opacity: 0 }}
|
||||
animate={{ y: '0%', opacity: 1, transition: { duration: 0.5 } }}
|
||||
exit={{ opacity: 0, y: '-100%', transition: { duration: 0.5 } }}
|
||||
className="absolute col-span-2 col-start-5 rounded-2xl p-6 xl:p-12 h-full bg-radial-[at_0%_100%] from-[#7A7A7A99] flex flex-col gap-2 z-10 backdrop-blur-[500px] select-none"
|
||||
>
|
||||
<p className="heading2 font-medium">Интерактивная инсоляция</p>
|
||||
|
||||
@@ -43,13 +43,14 @@ export function PresentationMini() {
|
||||
|
||||
return (
|
||||
<div className="mt-25 lg:hidden">
|
||||
<div className="top-20 md:space-y-12 sticky space-y-10">
|
||||
<Title>
|
||||
<div className="top-15 md:space-y-12 sticky space-y-5">
|
||||
<Title className="text-nowrap">
|
||||
Интерактивная презентация{' '}
|
||||
<span className="text-gradient">
|
||||
улучшает опыт выбора недвижимости
|
||||
</span>{' '}
|
||||
и увеличивают темпы продаж квартир в жилом комплексе
|
||||
и увеличивают темпы продаж
|
||||
<span className="max-sm:hidden">квартир в жилом комплексе</span>
|
||||
</Title>
|
||||
<div className="md:h-95 h-62 relative">
|
||||
{videos.map((video, index) => (
|
||||
@@ -63,7 +64,7 @@ export function PresentationMini() {
|
||||
style={{
|
||||
zIndex: videos.length - index,
|
||||
}}
|
||||
className={`absolute w-full h-full transition-opacity${
|
||||
className={`absolute w-full h-full object-cover transition-opacity${
|
||||
slide > index && index !== videos.length - 1 ? ' opacity-0' : ''
|
||||
}`}
|
||||
/>
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { IReview } from '@/types/IReview';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export function ReviewContent({
|
||||
author,
|
||||
text,
|
||||
src,
|
||||
active = false,
|
||||
index,
|
||||
}: IReview & { active: boolean; index: number }) {
|
||||
const ref = useRef<HTMLVideoElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && active) ref.current!.currentTime = !index ? 6 : 5;
|
||||
}, [active, index]);
|
||||
|
||||
return (
|
||||
active && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 1 }}
|
||||
className="relative flex items-center justify-center h-full max-h-[calc(100vh-40px)]"
|
||||
>
|
||||
<div className="lg:space-y-6 space-y-4 lg:max-w-[60%] absolute lg:left-6 lg:top-6 sm:top-4 left-4 bottom-4 transition-opacity z-[5]">
|
||||
<p className="max-w-[507px] text2 opacity-80">{author}</p>
|
||||
<p className="accent font-medium">{text}</p>
|
||||
</div>
|
||||
{/* <VideoPlayer src={src} showMutingBtn={false} /> */}
|
||||
<video
|
||||
ref={ref}
|
||||
src={src}
|
||||
loop
|
||||
muted
|
||||
autoPlay
|
||||
playsInline
|
||||
className="rounded-2xl absolute object-cover object-center w-full h-full"
|
||||
/>
|
||||
<div className="transition-colors delay-500 max-sm:bg-gradient-to-t from-[rgba(20,22,31,0.6)] to-[rgba(20,22,31,0)] absolute h-full w-full rounded-2xl z-[4]" />
|
||||
</motion.div>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -31,7 +31,7 @@ export function ReviewTab({
|
||||
return (
|
||||
<motion.button
|
||||
onClick={onClick}
|
||||
className="flex items-stretch rounded-xl p-1 backdrop-blur-[34.2px] bg-[#37393B99] max-h-[104px] outline-none max-lg:min-w-[280px]"
|
||||
className="flex items-stretch rounded-2xl p-1 backdrop-blur-[34.2px] bg-[#37393B99] max-h-[104px] outline-none max-lg:min-w-[280px]"
|
||||
animate={{
|
||||
width: active ? 283 : 104,
|
||||
}}
|
||||
|
||||
@@ -3,14 +3,19 @@
|
||||
import reviewsData from '@/consts/reviews.json';
|
||||
import { useMediaQueries } from '@/hooks/useMediaQueries';
|
||||
import { useScroll } from '@/hooks/useScroll';
|
||||
import { useRef, useState } from 'react';
|
||||
import { ReviewContent } from './ReviewContent';
|
||||
import { VideoPlayer } from '@/ui/VideoPlayer';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { ReviewTab } from './ReviewTab';
|
||||
|
||||
export function Reviews() {
|
||||
const [tab, setTab] = useState(0);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (videoRef.current) videoRef.current!.currentTime = tab === 0 ? 6 : 5;
|
||||
}, [tab]);
|
||||
|
||||
const scroll = useScroll(ref);
|
||||
|
||||
@@ -35,32 +40,32 @@ export function Reviews() {
|
||||
: '100%',
|
||||
}}
|
||||
>
|
||||
<div className="absolute flex flex-nowrap bottom-6 left-6 gap-2 z-[6] max-lg:hidden">
|
||||
{reviewsData.map(({ author, title, miniImage }, index) => (
|
||||
<ReviewTab
|
||||
key={author}
|
||||
image={miniImage}
|
||||
onClick={() => setTab(index)}
|
||||
active={
|
||||
currentHovered === index ||
|
||||
(tab === index && currentHovered === undefined)
|
||||
}
|
||||
title={title}
|
||||
index={index}
|
||||
setCurrentHovered={setCurrentHovered}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{reviewsData.map((review, index) => (
|
||||
<ReviewContent
|
||||
{...review}
|
||||
key={review.author}
|
||||
active={tab === index}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
<VideoPlayer src={reviewsData[tab].src} showMutingBtn ref={videoRef}>
|
||||
<div className="lg:space-y-6 space-y-4 lg:max-w-[60%] absolute lg:left-6 lg:top-6 sm:top-4 left-4 bottom-4 transition-opacity z-[5]">
|
||||
<p className="max-w-[507px] text2 opacity-80">
|
||||
{reviewsData[tab].author}
|
||||
</p>
|
||||
<p className="accent font-medium">{reviewsData[tab].text}</p>
|
||||
</div>
|
||||
<div className="absolute flex flex-nowrap bottom-6 left-6 gap-2 z-[6] max-lg:hidden">
|
||||
{reviewsData.map(({ author, title, miniImage }, index) => (
|
||||
<ReviewTab
|
||||
key={author}
|
||||
image={miniImage}
|
||||
onClick={() => setTab(index)}
|
||||
active={
|
||||
currentHovered === index ||
|
||||
(tab === index && currentHovered === undefined)
|
||||
}
|
||||
title={title}
|
||||
index={index}
|
||||
setCurrentHovered={setCurrentHovered}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</VideoPlayer>
|
||||
</div>
|
||||
<div className="flex gap-2 z-[3] lg:hidden max-lg:-mx-5 max-lg:px-5 overflow-auto [scrollbar-width:none]">
|
||||
<div className="flex gap-2 z-[3] lg:hidden mt-3 max-lg:-mx-5 max-lg:px-5 overflow-auto [scrollbar-width:none]">
|
||||
{reviewsData.map(({ author, title, miniImage }, index) => (
|
||||
<ReviewTab
|
||||
key={author}
|
||||
|
||||
@@ -27,7 +27,7 @@ export function Statistics() {
|
||||
text="Конверсия из консультации в бронирование увеличивается на"
|
||||
className="xl:col-start-1 col-start-2 xl:aspect-[344/225] sm:aspect-[362/255]"
|
||||
/>
|
||||
<div className="p-6 flex flex-col justify-between xl:col-start-2 xl:row-span-2 xl:row-start-1 sm:col-start-1 sm:row-start-2 col-span-2 sm:max-xl:aspect-[736/360] bg-[url(/img/pages/home/stats/building2.png),linear-gradient(to_bottom_right,#7A7A7A50,transparent)] bg-no-repeat bg-right-bottom bg-[length:65%,100%] rounded-lg">
|
||||
<div className="p-6 flex flex-col justify-between xl:col-start-2 xl:row-span-2 xl:row-start-1 sm:col-start-1 sm:row-start-2 col-span-2 sm:max-xl:aspect-[736/360] bg-[url(/img/pages/home/stats/building2.png),linear-gradient(to_bottom_right,#7A7A7A50,transparent)] bg-no-repeat bg-right-bottom bg-[length:65%,100%] rounded-2xl">
|
||||
<p className="text1 lg:max-w-[35%] max-w-[70%]">
|
||||
Время реализации проекта сокращается до
|
||||
</p>
|
||||
@@ -36,7 +36,7 @@ export function Statistics() {
|
||||
<span className="heading2">раз</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="xl:col-start-4 xl:row-start-1 max-sm:col-span-2 sm:col-start-1 sm:row-start-3 aspect-[338/225] bg-[linear-gradient(to_top_left,#7a7a7a50,transparent)] p-6 rounded-lg flex flex-col justify-between relative">
|
||||
<div className="xl:col-start-4 xl:row-start-1 max-sm:col-span-2 sm:col-start-1 sm:row-start-3 aspect-[338/225] bg-[linear-gradient(to_top_left,#7a7a7a50,transparent)] p-6 rounded-2xl flex flex-col justify-between relative">
|
||||
<p className="text1">
|
||||
Время на подготовку рекламных материалов сокращается до
|
||||
</p>
|
||||
|
||||
+33
-33
@@ -2,63 +2,63 @@ import { ICityProjects } from '@/types/ICityProjects';
|
||||
|
||||
export const cities: Omit<ICityProjects, 'logos' | 'companies'>[] = [
|
||||
{
|
||||
x: 15200 / 1027,
|
||||
y: 24600 / 560,
|
||||
title: 'Пенза',
|
||||
},
|
||||
{
|
||||
x: 16800 / 1027,
|
||||
y: 16500 / 560,
|
||||
x: 9210 / 1333.4,
|
||||
y: 29282 / 731.98,
|
||||
title: 'Санкт-Петербург',
|
||||
},
|
||||
{
|
||||
x: 9900 / 1027,
|
||||
y: 24600 / 560,
|
||||
x: 22910 / 1333.4,
|
||||
y: 41482 / 731.98,
|
||||
title: 'Пенза',
|
||||
},
|
||||
{
|
||||
x: 9110 / 1333.4,
|
||||
y: 40782 / 731.98,
|
||||
title: 'Брянск',
|
||||
},
|
||||
{
|
||||
x: 16800 / 1027,
|
||||
y: 31400 / 560,
|
||||
x: 15010 / 1333.4,
|
||||
y: 35582 / 731.98,
|
||||
title: 'Москва',
|
||||
},
|
||||
{
|
||||
x: 20800 / 1027,
|
||||
y: 29800 / 560,
|
||||
x: 30110 / 1333.4,
|
||||
y: 34682 / 731.98,
|
||||
title: 'Казань',
|
||||
},
|
||||
// {
|
||||
// x: 27600 / 1027,
|
||||
// y: 31400 / 560,
|
||||
// title: 'Пермь',
|
||||
// },
|
||||
// {
|
||||
// x: 30200 / 1027,
|
||||
// y: 32800 / 560,
|
||||
// title: 'Нижний Тагил',
|
||||
// },
|
||||
{
|
||||
x: 27600 / 1027,
|
||||
y: 31400 / 560,
|
||||
title: 'Пермь',
|
||||
},
|
||||
{
|
||||
x: 30200 / 1027,
|
||||
y: 32800 / 560,
|
||||
title: 'Нижний Тагил',
|
||||
},
|
||||
{
|
||||
x: 28800 / 1027,
|
||||
y: 34300 / 560,
|
||||
x: 36510 / 1333.4,
|
||||
y: 41982 / 731.98,
|
||||
title: 'Екатеринбург',
|
||||
},
|
||||
{
|
||||
x: 27600 / 1027,
|
||||
y: 36900 / 560,
|
||||
x: 46710 / 1333.4,
|
||||
y: 46282 / 731.98,
|
||||
title: 'Челябинск',
|
||||
},
|
||||
{
|
||||
x: 33000 / 1027,
|
||||
y: 36900 / 560,
|
||||
x: 48710 / 1333.4,
|
||||
y: 37882 / 731.98,
|
||||
title: 'Тюмень',
|
||||
},
|
||||
{
|
||||
x: 87300 / 1027,
|
||||
y: 46400 / 560,
|
||||
x: 105510 / 1333.4,
|
||||
y: 53482 / 731.98,
|
||||
title: 'Хабаровск',
|
||||
},
|
||||
{
|
||||
x: 87300 / 1027,
|
||||
y: 53100 / 560,
|
||||
x: 114910 / 1333.4,
|
||||
y: 64682 / 731.98,
|
||||
title: 'Владивосток',
|
||||
},
|
||||
];
|
||||
|
||||
+27
-8
@@ -1,77 +1,96 @@
|
||||
export const mapVideos = [
|
||||
{
|
||||
city: 'Москва',
|
||||
title: 'Upside Development',
|
||||
src: 'https://rutube.ru/video/8db3147bba72177d055d7f4bc198a8f3/',
|
||||
src: 'https://rutube.ru/play/embed/8db3147bba72177d055d7f4bc198a8f3/',
|
||||
},
|
||||
{
|
||||
city: 'Москва',
|
||||
title: 'Sezar Group',
|
||||
src: '/videos/pages/home/map/Sezar.mp4',
|
||||
},
|
||||
{
|
||||
city: 'Москва',
|
||||
title: 'Брусника',
|
||||
src: 'https://rutube.ru/video/01c1354c4b4572d91a0d6768b3d9370a/',
|
||||
src: 'https://rutube.ru/play/embed/01c1354c4b4572d91a0d6768b3d9370a/',
|
||||
},
|
||||
{
|
||||
city: 'Москва',
|
||||
title: 'Легенда',
|
||||
src: '/videos/pages/home/map/SevernyPort.mp4',
|
||||
},
|
||||
{
|
||||
city: 'Екатеринбург',
|
||||
title: 'Форум',
|
||||
src: '/videos/pages/home/map/Zoolog.mp4',
|
||||
},
|
||||
{
|
||||
city: 'Екатеринбург',
|
||||
title: 'Паритет',
|
||||
src: 'https://rutube.ru/video/8dec2951fb80b6de87e6b08fad0b8871/',
|
||||
src: 'https://rutube.ru/play/embed/8dec2951fb80b6de87e6b08fad0b8871/',
|
||||
},
|
||||
{
|
||||
city: 'Екатеринбург',
|
||||
title: 'Лето',
|
||||
src: '/videos/pages/home/map/Leto.mp4',
|
||||
},
|
||||
{
|
||||
city: 'Екатеринбург',
|
||||
title: 'НКС',
|
||||
src: 'https://rutube.ru/video/fb90512a39b945788f9fd21196c0d45b/',
|
||||
src: 'https://rutube.ru/play/embed/fb90512a39b945788f9fd21196c0d45b/',
|
||||
},
|
||||
{
|
||||
city: 'Тюмень',
|
||||
title: 'Делом',
|
||||
src: 'https://rutube.ru/video/2ada2ae6fc2c5079ae1b6b06ce5979a8/',
|
||||
src: 'https://rutube.ru/play/embed/2ada2ae6fc2c5079ae1b6b06ce5979a8/',
|
||||
},
|
||||
{
|
||||
city: 'Тюмень',
|
||||
title: 'Rodina',
|
||||
src: 'https://rutube.ru/video/2ada2ae6fc2c5079ae1b6b06ce5979a8/',
|
||||
src: '/videos/pages/home/map/August.mp4',
|
||||
},
|
||||
{
|
||||
city: 'Тюмень',
|
||||
title: 'ЭНКО',
|
||||
src: 'https://rutube.ru/video/2979e821ed713651864666b1cb1e91f7/',
|
||||
src: 'https://rutube.ru/play/embed/2979e821ed713651864666b1cb1e91f7/',
|
||||
},
|
||||
{
|
||||
city: 'Казань',
|
||||
title: 'Брусника',
|
||||
src: 'https://rutube.ru/video/829c2ddc24351d08df1c4aa423f10f59/',
|
||||
src: 'https://rutube.ru/play/embed/829c2ddc24351d08df1c4aa423f10f59/',
|
||||
},
|
||||
{
|
||||
city: 'Пенза',
|
||||
title: 'Рисан',
|
||||
src: '/videos/pages/home/map/Risan.MOV',
|
||||
},
|
||||
{
|
||||
city: 'Пермь',
|
||||
title: 'ГК Альфа',
|
||||
src: '/videos/pages/home/map/Risan.MOV',
|
||||
},
|
||||
{
|
||||
city: 'Челябинск',
|
||||
title: 'Голос',
|
||||
src: '/videos/pages/home/map/Risan.MOV',
|
||||
},
|
||||
{
|
||||
city: 'Хабаровск',
|
||||
title: 'СК+',
|
||||
src: '/videos/pages/home/map/Risan.MOV',
|
||||
},
|
||||
{
|
||||
city: 'Хабаровск',
|
||||
title: 'DNS',
|
||||
src: '/videos/pages/home/map/DNS.mp4',
|
||||
},
|
||||
{
|
||||
city: 'Хабаровск',
|
||||
title: 'Edelweiss',
|
||||
src: '/videos/pages/home/map/DNS.mp4',
|
||||
},
|
||||
{
|
||||
city: 'Санкт-Петербург',
|
||||
title: 'Мавис',
|
||||
src: '/videos/pages/home/map/Mavis.mp4',
|
||||
},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
[
|
||||
{
|
||||
"image": "/img/pages/home/reviews/1.png",
|
||||
"miniImage": "/img/pages/home/reviews/1_mini.png",
|
||||
"text": "«Эффективность инструмента была подтверждена буквально в первый день после его внедрения»",
|
||||
"author": "Егор Бобров, Коммерческий директор авторского квартала «Машаров»",
|
||||
@@ -8,7 +7,6 @@
|
||||
"title": "Авторский Квартал «Машаров»"
|
||||
},
|
||||
{
|
||||
"image": "/img/pages/home/reviews/3.jpg",
|
||||
"miniImage": "/img/pages/home/reviews/2_mini.png",
|
||||
"text": "«Одним из преимуществ инструмента является возможность посмотреть 3D-модель квартиры с готовым дизайнерским ремонтом и оценить видовые характеристиками, изменяя время суток»",
|
||||
"author": "Алина Веселова, Ведущий специалист отдела продаж",
|
||||
@@ -16,7 +14,6 @@
|
||||
"title": "ЖК «Айвазовский»"
|
||||
},
|
||||
{
|
||||
"image": "/img/pages/home/reviews/2.jpg",
|
||||
"miniImage": "/img/pages/home/reviews/3_mini.png",
|
||||
"text": "«Клиенты особенно ценят возможность легко выбрать квартиру с помощью 3D-модель жилого комплекса»",
|
||||
"author": "Олег Бондорев, Ведущий менеджер компании «ЭНКО»",
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { api } from '@/api';
|
||||
import { ICompany } from '@/types/ICompany';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
export function useGetCompniesByCityQuery(city: string) {
|
||||
return useQuery({
|
||||
queryKey: ['companies', city],
|
||||
queryFn: async () =>
|
||||
await api
|
||||
.get(`companies?${city ? 'city=' + city : ''}`)
|
||||
.json<ICompany[]>(),
|
||||
select: (companies) => companies.filter((company) => company.mapIcon),
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,21 @@
|
||||
import { api } from '@/api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
export function useGetProjectsCountQuery() {
|
||||
export function useGetProjectsCountQuery(
|
||||
city?: string,
|
||||
tags: string | string[] = []
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: ['project', 'count'],
|
||||
queryFn: async () => await api.get('projects/count').json<number>(),
|
||||
queryKey: ['project', 'count', city, tags],
|
||||
queryFn: async () =>
|
||||
await api
|
||||
.get(
|
||||
`projects/count?${city ? 'city=' + city : ''}&${
|
||||
Array.isArray(tags)
|
||||
? tags.map((tag) => `tags=${tag}`).join('&')
|
||||
: 'tags=' + tags
|
||||
}`
|
||||
)
|
||||
.json<number>(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { ICompany } from './ICompany';
|
||||
|
||||
export interface ICityProjects {
|
||||
x: number;
|
||||
y: number;
|
||||
title: string;
|
||||
companies: ICompany[];
|
||||
// companies: ICompany[];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export interface IReview {
|
||||
image: string;
|
||||
miniImage: string;
|
||||
text: string;
|
||||
author: string;
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ export function Figure({
|
||||
<motion.div
|
||||
ref={root}
|
||||
className={
|
||||
'bg-[linear-gradient(to_top_left,#7a7a7a50,transparent)] p-6 rounded-lg flex flex-col justify-between relative ' +
|
||||
'bg-[linear-gradient(to_top_left,#7a7a7a50,transparent)] p-6 rounded-2xl flex flex-col justify-between relative ' +
|
||||
className
|
||||
}
|
||||
>
|
||||
|
||||
@@ -13,7 +13,7 @@ export function GradientButton({
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`bg-gradient-to-bl outline-none p-px rounded-full animate-spin [animation-duration:2s] from-[#BE69F5] to-[#798FFF00]${
|
||||
className={`bg-gradient-to-bl outline-none p-px rounded-full animate-spin [animation-duration:2s] from-[#BE69F5] cursor-pointer to-[#798FFF00]${
|
||||
className ? ' ' + className : ''
|
||||
}`}
|
||||
>
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ export function Option<T extends FieldValues>({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`cursor-pointer transition-colors rounded-xl font-medium select-none ${
|
||||
className={`cursor-pointer transition-colors rounded-xl font-medium text-nowrap select-none ${
|
||||
chosen ? 'bg-white text-black' : 'bg-[#37393B99]'
|
||||
} ${fieldName === 'products' ? 'px-6 py-[17px] btnm' : 'px-3 py-2 btns'}`}
|
||||
onClick={() => setChosen(!chosen)}
|
||||
|
||||
@@ -9,14 +9,14 @@ export function VanyaBoom({ className }: { className?: string }) {
|
||||
>
|
||||
<video
|
||||
src="/videos/pages/inProcess/impla.mp4"
|
||||
className="aspect-[128/62] absolute rounded-3xl"
|
||||
className="aspect-[128/62] absolute rounded-3xl w-full object-cover"
|
||||
muted
|
||||
autoPlay
|
||||
loop
|
||||
playsInline
|
||||
/>
|
||||
<Image
|
||||
src={'/img/pages/about/vanya_workaet.png'}
|
||||
src={'/img/pages/inProcess/vanya_workaet.png'}
|
||||
alt="vanya workaet"
|
||||
className="relative mix-blend-lighten"
|
||||
width={128}
|
||||
|
||||
@@ -43,7 +43,7 @@ export function VideoMutingBtn({
|
||||
}, [animate, handleMouseMove]);
|
||||
|
||||
return (
|
||||
<div className="absolute left-0 top-0 h-5/6 w-full z-[1]">
|
||||
<div className="absolute left-0 top-0 h-5/6 w-full z-7">
|
||||
<div ref={ref} className="relative w-full h-full group">
|
||||
<button
|
||||
className="bg-[#37393B99] p-[50px] backdrop-blur-[30.72px] rounded-full group-hover:opacity-100 transition-opacity group-hover:cursor-none opacity-0 sticky outline-none"
|
||||
|
||||
+97
-73
@@ -1,82 +1,106 @@
|
||||
'use client';
|
||||
|
||||
import { ComponentProps, useEffect, useRef, useState } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import {
|
||||
ComponentProps,
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { VideoMutingBtn } from './VideoMutingBtn';
|
||||
import { VideoProgressBar } from './VideoProgressBar';
|
||||
|
||||
export function VideoPlayer({
|
||||
src,
|
||||
showMutingBtn,
|
||||
children,
|
||||
loop = true,
|
||||
autoPlay = true,
|
||||
}: {
|
||||
src: string;
|
||||
showMutingBtn: boolean;
|
||||
children?: React.ReactNode;
|
||||
} & ComponentProps<'video'>) {
|
||||
const progressbarRef = useRef<HTMLDivElement>(null);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
export const VideoPlayer = forwardRef<
|
||||
HTMLVideoElement,
|
||||
{
|
||||
src: string;
|
||||
showMutingBtn: boolean;
|
||||
children?: React.ReactNode;
|
||||
} & ComponentProps<'video'>
|
||||
>(
|
||||
(
|
||||
{ src, showMutingBtn, children, loop = true, autoPlay = true, className },
|
||||
ref
|
||||
) => {
|
||||
const progressbarRef = useRef<HTMLDivElement>(null);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
const [muted, setMuted] = useState(autoPlay);
|
||||
const [playing, setPlaying] = useState(autoPlay);
|
||||
const [progress, setProgress] = useState(0);
|
||||
useImperativeHandle(ref, () => videoRef.current!);
|
||||
|
||||
function handleProgressbarClick(e: React.MouseEvent) {
|
||||
videoRef.current!.currentTime =
|
||||
(videoRef.current!.duration *
|
||||
(e.clientX - progressbarRef.current!.getBoundingClientRect().x)) /
|
||||
progressbarRef.current!.clientWidth;
|
||||
setProgress(
|
||||
((videoRef.current?.currentTime ?? 0) /
|
||||
(videoRef.current?.duration ?? 1)) *
|
||||
100
|
||||
const [muted, setMuted] = useState(autoPlay);
|
||||
const [playing, setPlaying] = useState(autoPlay);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
function handleProgressbarClick(e: React.MouseEvent) {
|
||||
videoRef.current!.currentTime =
|
||||
(videoRef.current!.duration *
|
||||
(e.clientX - progressbarRef.current!.getBoundingClientRect().x)) /
|
||||
progressbarRef.current!.clientWidth;
|
||||
setProgress(
|
||||
((videoRef.current?.currentTime ?? 0) /
|
||||
(videoRef.current?.duration ?? 1)) *
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
function handlePlaybackClick() {
|
||||
if (!videoRef.current) return;
|
||||
setPlaying(videoRef.current.paused);
|
||||
videoRef.current[videoRef.current.paused ? 'play' : 'pause']();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
const timeUpdateHandler = () =>
|
||||
setProgress(((video.currentTime ?? 0) / (video.duration ?? 1)) * 100);
|
||||
|
||||
videoRef.current.addEventListener('timeupdate', timeUpdateHandler);
|
||||
return () => video.removeEventListener('timeupdate', timeUpdateHandler);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={src}
|
||||
autoPlay={autoPlay}
|
||||
muted={muted}
|
||||
loop={loop}
|
||||
playsInline
|
||||
className={`lg:aspect-[1400/640] sm:aspect-[736/480] aspect-[340/600] rounded-2xl w-full h-full object-cover${
|
||||
className ? ' ' + className : ''
|
||||
}`}
|
||||
/>
|
||||
{showMutingBtn && (
|
||||
<VideoMutingBtn
|
||||
handleClick={() => setMuted(!videoRef.current!.muted)}
|
||||
muted={muted}
|
||||
/>
|
||||
)}
|
||||
<div className="absolute w-full h-full top-0 bg-gradient-to-t from-[rgba(20,22,31,0.6)] to-[rgba(20,22,31,0)] rounded-2xl" />
|
||||
<AnimatePresence>
|
||||
{muted && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<VideoProgressBar
|
||||
muted={muted}
|
||||
progress={progress}
|
||||
progressbarRef={progressbarRef}
|
||||
playing={playing}
|
||||
handlePlaybackClick={handlePlaybackClick}
|
||||
handleProgressbarClick={handleProgressbarClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function handlePlaybackClick() {
|
||||
if (!videoRef.current) return;
|
||||
setPlaying(videoRef.current.paused);
|
||||
videoRef.current[videoRef.current.paused ? 'play' : 'pause']();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
const timeUpdateHandler = () =>
|
||||
setProgress(((video.currentTime ?? 0) / (video.duration ?? 1)) * 100);
|
||||
|
||||
videoRef.current.addEventListener('timeupdate', timeUpdateHandler);
|
||||
return () => video.removeEventListener('timeupdate', timeUpdateHandler);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={src}
|
||||
autoPlay={autoPlay}
|
||||
muted={muted}
|
||||
loop={loop}
|
||||
playsInline
|
||||
className="lg:aspect-[1400/640] sm:aspect-[736/480] aspect-[340/600] rounded-2xl w-full h-full object-cover"
|
||||
/>
|
||||
{showMutingBtn && (
|
||||
<VideoMutingBtn
|
||||
handleClick={() => setMuted(!videoRef.current!.muted)}
|
||||
muted={muted}
|
||||
/>
|
||||
)}
|
||||
<div className="absolute w-full h-full top-0 bg-gradient-to-t from-[rgba(20,22,31,0.6)] to-[rgba(20,22,31,0)] rounded-2xl" />
|
||||
{muted && children}
|
||||
<VideoProgressBar
|
||||
muted={muted}
|
||||
progress={progress}
|
||||
progressbarRef={progressbarRef}
|
||||
playing={playing}
|
||||
handlePlaybackClick={handlePlaybackClick}
|
||||
handleProgressbarClick={handleProgressbarClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export function prepositionCity(city: string) {
|
||||
if (city.at(-1) === 'ь') return city.slice(0, -1) + 'и';
|
||||
return ('аеёиоуыэю'.includes(city.at(-1)!) ? city.slice(0, -1) : city) + 'e';
|
||||
}
|
||||
Reference in New Issue
Block a user