added interactive map and real count of projects
@@ -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 |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 383 KiB After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 308 KiB |
@@ -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 />
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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 => (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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="экономическая эффективность" />
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,8 @@
|
||||
import { IProject } from './IProject';
|
||||
|
||||
export interface ICity {
|
||||
x: number;
|
||||
y: number;
|
||||
city: string;
|
||||
projects: Pick<IProject, 'image' | 'name' | 'company'>[];
|
||||
}
|
||||
@@ -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 ? 'а' : 'ов'}`;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||