added interactive map and real count of projects

This commit is contained in:
2024-08-22 14:58:03 +05:00
parent c0bff6b715
commit 0a3e9c1dd3
75 changed files with 409 additions and 122 deletions
+9
View File
@@ -1,6 +1,15 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'graff.estate',
port: '',
},
],
},
};
export default nextConfig;

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 1021 B

After

Width:  |  Height:  |  Size: 1021 B

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 383 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

+1 -2
View File
@@ -1,4 +1,3 @@
import { Ellipse } from '@/components/Layout/Ellipse';
import { AvailablesSlider } from '@/components/pages/MainPage/Availables/AvailablesSlider';
import { Calculator } from '@/components/pages/MainPage/Calculator/Calculator';
import { Clients } from '@/components/pages/MainPage/Clients';
@@ -16,7 +15,7 @@ import { Winners } from '@/components/pages/MainPage/Winners';
export default function Home() {
return (
<>
<Ellipse />
{/* <Ellipse /> */}
<Motivation />
<Showreel />
<Statistics />
+2 -6
View File
@@ -14,14 +14,10 @@ export function Ellipse() {
}
useEffect(() => {
document
.querySelector('main')
?.addEventListener('mousemove', handleMouseMove);
document.body?.addEventListener('mousemove', handleMouseMove);
return () => {
document
.querySelector('main')
?.removeEventListener('mousemove', handleMouseMove);
document.body?.removeEventListener('mousemove', handleMouseMove);
};
}, []);
+18 -8
View File
@@ -4,7 +4,7 @@ import { api } from '@/api';
import { ClassNameWrapper } from '@/hocs/ClassNameWrapper';
import { useModalStore } from '@/stores/useModalStore';
import { Button } from '@/ui/Button';
import { FormEvent, useEffect, useState } from 'react';
import { FormEvent, useEffect, useRef, useState } from 'react';
import ReactInputMask from 'react-input-mask';
import { ArrowRightIcon } from '../icons/ArrowRightIcon';
import { ChevronDownIcon } from '../icons/ChevronDownIcon';
@@ -13,10 +13,6 @@ import { CloseIcon } from '../icons/CloseIcon';
import { LoaderIcon } from '../icons/LoaderIcon';
import { MailIcon } from '../icons/MailIcon';
type PhoneCode = '+7' | '+375' | '+380' | '+44';
export const phoneCodes: PhoneCode[] = ['+7', '+375', '+380', '+44'];
export function ModalWithForm() {
const { setModal } = useModalStore();
const [name, setName] = useState('');
@@ -27,6 +23,16 @@ export function ModalWithForm() {
const [isLoading, setIsLoading] = useState(false);
const [isSend, setIsSend] = useState(false);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (textAreaRef.current) {
textAreaRef.current.style.height = 'auto';
textAreaRef.current.style.height =
textAreaRef.current.scrollHeight + 'px';
}
}, [textAreaRef, description]);
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
@@ -70,7 +76,7 @@ export function ModalWithForm() {
return (
<div className="fixed flex flex-col gap-4 top-0 right-0 h-full sm:w-[408px] w-full bg-[#14161F] overflow-y-auto sm:p-8 p-6">
{!isSend ? (
{isSend ? (
<div className="space-y-8">
<div className="flex justify-between items-center">
<p className="font-medium accent">Оставьте заявку</p>
@@ -157,12 +163,13 @@ export function ModalWithForm() {
Задача
</label>
<textarea
ref={textAreaRef}
id="description"
placeholder="Опишите вашу задачу"
value={description}
rows={1}
onChange={e => setDescription(e.target.value)}
className="bg-transparent border-b py-4 focus:border-white rounded-none border-[#3D425C] resize-none outline-none transition-all w-full placeholder:h4 placeholder:font-medium placeholder:select-none"
className="bg-transparent border-b py-4 focus:border-white max-h-[300px] h-auto rounded-none border-[#3D425C] resize-none outline-none transition-all w-full placeholder:h4 placeholder:font-medium placeholder:select-none"
/>
</div>
</div>
@@ -224,6 +231,9 @@ export function ModalWithForm() {
);
}
type PhoneCode = '+7' | '+375' | '+380' | '+44';
export const phoneCodes: PhoneCode[] = ['+7', '+375', '+380', '+44'];
function SelectPhoneCode({
currentPhoneCode,
onClick,
@@ -246,7 +256,7 @@ function SelectPhoneCode({
{open ? <ChevronUpIcon /> : <ChevronDownIcon />}
</button>
{open && (
<div className="absolute bg-[#14161F] w-full top-[100%]">
<div className="absolute z-10 bg-[#14161F] top-[100%] w-[calc(100%+4px)] -left-1 border border-t-0 p-1 rounded-b-lg border-[#3D425C]">
{phoneCodes
.filter(phonecode => phonecode !== currentPhoneCode)
.map(phoneCode => (
+9 -9
View File
@@ -1,18 +1,18 @@
export function ArrowRightIcon() {
return (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g opacity="0.8">
<path
d="M13.8788 24.2931L21.1717 17.0002H5.58591L7.58591 15.0002H21.1717L13.8788 7.70727L15.293 6.29306L16.7072 7.70727L25.0001 16.0002L15.293 25.7073L13.8788 24.2931Z"
fill="white"
/>
</g>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.8788 10.7071L4.29297 10.7071L6.29297 12.7071L15.8788 12.7071L10.5859 18L12.0001 19.4142L19.7072 11.7071L12.0001 4L10.5859 5.41421L15.8788 10.7071Z"
fill="white"
/>
</svg>
);
}
+20
View File
@@ -0,0 +1,20 @@
export function FullScreenIcon() {
return (
<svg
width="28"
height="28"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.3165 23.3335L11.3249 18.3251L9.67496 16.6752L4.66659 21.6836V16.6752L2.33325 19.0085V25.6668H8.99154L11.3249 23.3335H6.3165Z"
fill="white"
/>
<path
d="M23.3333 6.31675V11.3251L25.6666 8.99179V2.3335L19.0083 2.3335L16.675 4.66683H21.6833L16.675 9.6752L18.3249 11.3251L23.3333 6.31675Z"
fill="white"
/>
</svg>
);
}
+10 -16
View File
@@ -1,25 +1,19 @@
export function MailIcon() {
return (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
color="currentColor"
>
<g id="Icon/Mail" opacity="0.8">
<g id="Vector">
<path
d="M4 11.3111C4 10.7929 4.5653 10.4728 5.00965 10.7394L15.314 16.9216C15.7363 17.1749 16.2637 17.1749 16.686 16.9216L26.9903 10.739C27.4347 10.4724 28 10.7924 28 11.3106V22.6665C28 23.4029 27.403 23.9998 26.6667 23.9998H5.33333C4.59695 23.9998 4 23.4029 4 22.6665V11.3111Z"
fill="white"
/>
<path
d="M4.73055 7.90483C4.15076 7.55696 4.3974 6.6665 5.07354 6.6665H26.9265C27.6026 6.6665 27.8492 7.55696 27.2695 7.90483L16.686 14.2549C16.2638 14.5083 15.7362 14.5083 15.314 14.2549L4.73055 7.90483Z"
fill="white"
/>
</g>
</g>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 20H20L22 18V4H4L2 6V20ZM6.30278 6L12 9.79815L17.6972 6H6.30278ZM4 6.86852V18H19H20V17V6.86852L12 12.2019L4 6.86852Z"
fill="white"
/>
<path d="M19 18H20V17L19 18Z" fill="white" />
</svg>
);
}
+9 -62
View File
@@ -6,6 +6,7 @@ import { ClassNameWrapper } from '@/hocs/ClassNameWrapper';
import { IProject } from '@/types/IProject';
import { Button } from '@/ui/Button';
import { Descriptor } from '@/ui/Descriptor';
import { getProjectsCount } from '@/utils/getProjectsCount';
import { getSortedProjects } from '@/utils/getSortedProjects';
import Link from 'next/link';
import { useEffect, useState } from 'react';
@@ -16,8 +17,6 @@ export function Projects() {
const [sortedProjects, setSortedProjects] =
useState<Map<string | number, IProject[]>>();
const [currentProjects, setCurrentProjects] = useState<IProject[]>([]);
async function getProjects() {
try {
const projects: IProject[] = await api.get('projects').json();
@@ -35,10 +34,6 @@ export function Projects() {
getProjects();
}, []);
function handleShowAll() {
setAll(true);
}
return (
<div>
<div className="border-y border-[#3D425C] py-6 grid grid-cols-4">
@@ -47,8 +42,13 @@ export function Projects() {
<p className="accent font-medium max-w-[42vw]">
За <span className="text-gradient">13 лет</span> работы мы
реализовали
<span className="text-gradient"> 40 проектов</span> в разных городах
России и мира
<span className="text-gradient">
{' '}
{getProjectsCount(
Array.from(sortedProjects?.values() ?? [])?.flat().length,
)}
</span>{' '}
в разных городах России и мира
</p>
</div>
<Button
@@ -69,12 +69,7 @@ export function Projects() {
showMore={!all}
onClick={!all ? () => setAll(true) : undefined}
year={'В работе'}
projects={
projectsInProccess?.slice(
0,
Math.round(projectsInProccess.length / 2),
) ?? []
}
projects={projectsInProccess ?? []}
/>
{all && (
<>
@@ -90,51 +85,3 @@ export function Projects() {
</div>
);
}
// function ProjectCard({
// city,
// company,
// devices,
// image,
// name,
// stage = 6,
// className,
// }: Omit<IProject, 'releaseDate' | 'id'> & { className?: string }) {
// const stagePercentage = Math.round((100 / 6) * stage);
// return (
// <div className={'relative ' + className}>
// <div
// className={
// 'bg-no-repeat bg-cover bg-center flex items-end absolute top-0 left-0 w-full h-full p-6'
// }
// style={{
// backgroundImage: `url(${process.env.NEXT_PUBLIC_API}/upload/${image})`,
// }}
// >
// <div className="absolute top-0 left-0 w-full h-full bg-gradient-card" />
// <div className="space-y-6 relative">
// <div className="space-y-2">
// <p className="accent font-medium">{name}</p>
// <p className="m-text">
// {company}, {city}
// </p>
// </div>
// <div className="flex gap-2">
// {stage < 6 && (
// <div className="bg-[#14161F] px-3 py-2 rounded-full w-fit flex items-center gap-1">
// <p className="leading-none btn-text font-semibold">
// {stagePercentage}%
// </p>
// <ProgressPie value={stagePercentage} />
// </div>
// )}
// {devices.map(device => (
// <DeviceBadge badgeType={device} key={device} />
// ))}
// </div>
// </div>
// </div>
// </div>
// );
// }
@@ -0,0 +1,90 @@
import { api } from '@/api';
import { cities } from '@/consts/cities';
import { IProject } from '@/types/IProject';
import { getProjectsGroupedByCities } from '@/utils/getProjectsGroupedByCities';
import Image from 'next/image';
import { useEffect, useRef, useState } from 'react';
import { useHover } from 'usehooks-ts';
export function ProjectsMap() {
const [groupedProjects, setGroupedProjects] = useState<
Map<string, IProject[]>
>(new Map<string, IProject[]>());
async function getGroupedProjects() {
const projects = await api.get('projects').json<IProject[]>();
setGroupedProjects(getProjectsGroupedByCities(projects!));
}
useEffect(() => {
getGroupedProjects();
}, []);
return (
<div className="relative">
<Image
src={'/img/pages/home/stats/map.jpg'}
alt={''}
fill
className="!relative z-10 aspect-[1160/490]"
/>
{cities.map(({ x, y, city, projects }) => (
<CityPoint key={city} x={x} y={y} city={city} projects={projects} />
))}
</div>
);
}
export function CityPoint({
city,
projects,
x,
y,
}: {
city: string;
projects: Pick<IProject, 'company' | 'name' | 'image'>[];
x: number;
y: number;
}) {
const ref = useRef<HTMLDivElement>(null);
const hovered = useHover(ref);
return (
<>
<div
ref={ref}
className="absolute z-10 p-[5px] group"
style={{ left: x + '%', top: y + '%' }}
>
<div className="p-[5px] group-hover:p-[3px] rounded-full bg-[#FFFFFF29] group-hover:bg-transparent hover:bg-[#798FFF]">
<div className="w-[10px] h-[10px] bg-white group-hover:bg-[#798FFF] rounded-full group-hover:w-[14px] group-hover:h-[14px]" />
</div>
</div>
<div
className={
'rounded p-4 z-20 min-w-[280px] space-y-3 [background:url(/img/pages/home/stats/map_highlight.png)_no-repeat,#14161F] bg-no-repeat bg-cover absolute hover:block translate-x-7 translate-y-[5px]' +
(hovered ? '' : ' hidden')
}
style={{ left: x + '%', top: y + '%' }}
>
<p className="font-medium text-sm w-full">{city}</p>
{projects?.map(project => (
<div key={project.name} className="flex gap-x-3 items-stretch">
<Image
src={project.image}
alt={project.name}
fill
className="object-cover max-w-12 max-h-12 aspect-square !relative self-stretch"
/>
<div className="space-y-1 pt-[13px] border-t border-[#3D425C] flex-1">
<p className="font-medium text-sm">{project.name}</p>
<p className="m-caption text-[#9299BD] font-medium">
{project.company}
</p>
</div>
</div>
))}
</div>
</>
);
}
+6 -15
View File
@@ -1,36 +1,27 @@
'use client';
import { PauseIcon } from '@/components/icons/PauseIcon';
import { PlayIcon } from '@/components/icons/PlayIcon';
import { ClassNameWrapper } from '@/hocs/ClassNameWrapper';
import { useRef, useState } from 'react';
import { FullScreenIcon } from '@/components/icons/FullScreenIcon';
import { useState } from 'react';
export function Showreel() {
const [playing, setPlaying] = useState(true);
const videoRef = useRef<HTMLVideoElement>(null);
const [fullScreen, setFullScreen] = useState(false);
return (
<div className=" lg:mb-[200px] w-full relative aspect-[1551/616] flex justify-center items-center group">
<div className="lg:mb-[200px] w-full relative aspect-[1551/616] flex justify-center items-center group">
<video
src="/videos/pages/home/showreel.mp4"
autoPlay
loop
muted
ref={videoRef}
className="w-full aspect-[1552/616] object-cover self-stretch"
/>
<button
className="absolute z-10 p-8 rounded-full border group-hover:block hidden bg-[#14161F33]"
onClick={() => {
playing ? videoRef.current?.pause() : videoRef.current?.play();
setPlaying(prev => !prev);
setFullScreen(prev => !prev);
}}
>
<ClassNameWrapper
className="w-10 h-10"
element={playing ? <PauseIcon /> : <PlayIcon />}
/>
<FullScreenIcon />
</button>
</div>
);
+29 -4
View File
@@ -1,11 +1,35 @@
'use client';
import { api } from '@/api';
import { IProject } from '@/types/IProject';
import { Descriptor } from '@/ui/Descriptor';
import { Title } from '@/ui/Title';
import { getProjectsCount } from '@/utils/getProjectsCount';
import { Manrope } from 'next/font/google';
import Image from 'next/image';
import { useEffect, useState } from 'react';
import { ProjectsMap } from './ProjectsMap';
const manrope = Manrope({ subsets: ['latin'] });
export function Statistics() {
const [projects, setProjects] = useState<IProject[]>([]);
async function getProjects() {
try {
const projects: IProject[] = await api.get('projects').json();
return projects;
} catch (error) {
if (error instanceof Error) {
alert(`Error: ${error.message}`);
}
}
}
useEffect(() => {
getProjects().then(projects => setProjects(projects!));
}, []);
return (
<section>
<Title className="mb-20 leading-[-2em]">
@@ -19,16 +43,17 @@ export function Statistics() {
</Title>
<div className="grid grid-cols-4 border-t border-[#3D425C]">
<div className="col-span-1 pt-10 border-r border-b border-[#3D425C] accent font-medium">
Мы собрали статистику за 13 лет работы с застройщиками, реализовав 40
проектов
Мы собрали статистику за 13 лет работы с застройщиками, реализовав 
{getProjectsCount(projects.length)}
</div>
<div className="col-start-2 col-span-full py-10 pl-4 flex items-center justify-center border-b border-[#3D425C] relative">
<Image
{/* <Image
src={'/img/pages/home/stats/map.jpg'}
alt={''}
fill
className="!relative object-cover z-10"
/>
/> */}
<ProjectsMap />
</div>
<div className="col-span-1 py-10 flex flex-col justify-between border-b border-r border-[#3D425C]">
<Descriptor title="экономическая эффективность" />
+183
View File
@@ -0,0 +1,183 @@
import { ICity } from '@/types/ICity';
export const cities: ICity[] = [
{
x: 23700 / 1160,
y: 19900 / 490,
city: 'Москва',
projects: [
{
name: 'Sezar City',
company: 'Sezar Group',
image: '/img/components/main_projects/sezar.png',
},
{
name: 'Upside Towers',
company: 'Upside Development',
image: '/img/components/main_projects/upside.png',
},
{
company: 'Северный порт',
name: 'ГК Легенда',
image: '/img/components/main_projects/north_port.png',
},
],
},
{
x: 24900 / 1160,
y: 12500 / 490,
city: 'Санкт-Петербург',
projects: [
{
name: 'Фотограф',
company: 'Мавис',
image: '/img/components/main_projects/photograph.png',
},
{
name: 'Графика',
company: 'Мавис',
image: '/img/components/main_projects/graphica.png',
},
],
},
{
x: 18800 / 1160,
y: 19900 / 490,
city: 'Брянск',
projects: [
{
name: 'Новая Атмосфера',
company: 'Застройщик Атмосфера',
image: '/img/components/main_projects/new_atmosphera.png',
},
],
},
{
x: 24900 / 1160,
y: 26100 / 490,
city: 'Пенза',
projects: [
{
name: 'Scala City',
company: 'Рисан',
image: '/img/components/main_projects/scala.png',
},
],
},
{
x: 28700 / 1160,
y: 24800 / 490,
city: 'Казань',
projects: [
{
name: 'Риваят',
company: 'КамаСтройИнвест',
image: '/img/components/main_projects/rivayat.png',
},
],
},
{
x: 34900 / 1160,
y: 26100 / 490,
city: 'Пермь',
projects: [
{
name: 'Кама',
company: 'ГК Альфа',
image: '/img/components/main_projects/kama.png',
},
],
},
{
x: 37300 / 1160,
y: 27300 / 490,
city: 'Нижний Тагил',
projects: [
{
name: 'Александровский',
company: 'АС-Строй',
image: '/img/components/main_projects/alexandrovsky.png',
},
],
},
{
x: 36100 / 1160,
y: 28600 / 490,
city: 'Екатеринбург',
projects: [
{
name: 'Re:volution Towers',
company: 'НКС-девелопмент',
image: '/img/components/main_projects/revolution.png',
},
{
name: 'Тёплые кварталы',
company: 'Паритет Девелопмент',
image: '/img/components/main_projects/warm_quartals.png',
},
{
name: 'Тактика',
company: 'Fortis Development',
image: '/img/components/main_projects/tactic.png',
},
],
},
{
x: 34900 / 1160,
y: 31000 / 490,
city: 'Челябинск',
projects: [
{
name: 'Голос в сердце города',
company: 'Голос Девелопмент',
image: '/img/components/main_projects/voice.png',
},
],
},
{
x: 39800 / 1160,
y: 31000 / 490,
city: 'Тюмень',
projects: [
{
name: 'Август',
company: 'Родина Девелопмент',
image: '/img/components/main_projects/august.png',
},
{
name: 'Новатор',
company: 'СБК',
image: '/img/components/main_projects/novator.png',
},
{
name: 'Айвазовский City',
company: 'ЭНКО',
image: '/img/components/main_projects/aivazovsky.png',
},
],
},
{
x: 90500 / 1160,
y: 39700 / 490,
city: 'Хабаровск',
projects: [
{
name: 'Ориент',
company: 'СК+',
image: '/img/components/main_projects/orient.png',
},
],
},
{
x: 90500 / 1160,
y: 47100 / 490,
city: 'Владивосток',
projects: [
{
name: 'DNS Ситиs',
company: 'DNS Девелопмент',
image: '/img/components/main_projects/dns.png',
},
],
},
];
+8
View File
@@ -0,0 +1,8 @@
import { IProject } from './IProject';
export interface ICity {
x: number;
y: number;
city: string;
projects: Pick<IProject, 'image' | 'name' | 'company'>[];
}
+3
View File
@@ -0,0 +1,3 @@
export function getProjectsCount(count: number) {
return `${count} проект${count > 10 && count < 15 ? 'ов' : count % 10 === 1 ? '' : count % 10 === 2 || count % 10 === 3 || count % 10 === 4 ? 'а' : 'ов'}`;
}
+12
View File
@@ -0,0 +1,12 @@
import { IProject } from '@/types/IProject';
export function getProjectsGroupedByCities(projects: IProject[]) {
const projectsGroupedByCities: Map<string, IProject[]> = new Map();
for (const project of projects) {
if (!projectsGroupedByCities.has(project.city)) {
projectsGroupedByCities.set(project.city, []);
}
projectsGroupedByCities.get(project.city)!.push(project);
}
return projectsGroupedByCities;
}