This commit is contained in:
2025-12-23 17:18:13 +05:00
parent 923a7e3806
commit daf2541192
28 changed files with 370 additions and 948 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 312 KiB

After

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 KiB

+1 -1
View File
@@ -4,7 +4,7 @@ export default function ResultsLayout({
children: React.ReactNode;
}) {
return (
<main className="flex-1 md:max-lg:pt-[120px] pt-[100px] md:px-4 lg:px-[1.389vw] px-[10px] overflow-clip relative">
<main className="flex-1 md:max-lg:pt-[120px] pt-[100px] md:px-4 lg:px-[1.389vw] px-[10px] relative">
{children}
</main>
);
+18
View File
@@ -0,0 +1,18 @@
import * as React from "react";
const ReadIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="m1.177 10.862 3.283 3.064 8.618-8.044m5.746.192-8.619 8.044-1.026-.958"
stroke="#fff"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default ReadIcon;
+2 -2
View File
@@ -40,14 +40,14 @@ export function Map() {
<Slider mapPoint={mapPoint} />
</div>
</div>
<div className="lg:hidden absolute left-[50vw] bottom-[4.444vw] translate-y-1/2 -translate-x-1/2 md:w-[42.161vw] w-[68.611vw] aspect-[247/56] rounded-[4.444vw] bg-[#37393B99] px-[4.167vw] py-[4.444vw] flex gap-[2.222vw] justify-between items-center">
{/* <div className="lg:hidden absolute left-[50vw] bottom-[4.444vw] translate-y-1/2 -translate-x-1/2 md:w-[42.161vw] w-[68.611vw] aspect-[247/56] rounded-[4.444vw] bg-[#37393B99] px-[4.167vw] py-[4.444vw] flex gap-[2.222vw] justify-between items-center">
<div className="text-white lg:size-[6.667vw] size-6 flex-shrink-0">
<FingerPrintIcon />
</div>
<p className="caption leading-[120%] font-medium select-none">
Нажмите и удерживайте, чтобы посмотреть всех партнеров в регионе
</p>
</div>
</div> */}
</div>
);
}
@@ -174,7 +174,7 @@ export default function ResultsAchievements() {
}}
className="flex justify-between h-[100vh] relative items-end -mx-[1.389vw]"
>
<h2 className="line2 !font-medium absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center z-[2] bg-[radial-gradient(rgba(0,0,0,0.2)_20%,rgba(0,0,0,0)_70%)]">
<h2 className="line2 !font-medium absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center z-[2] bg-[radial-gradient(rgba(0,0,0,0.65)_0%,rgba(0,0,0,0)_70%)]">
Кто хочет стать <br /> следующим?
</h2>
<div className="h-full flex gap-[1.111vw] min-h-0">
@@ -219,8 +219,8 @@ export default function ResultsAchievements() {
</h2>
<div className="p-[2.222vw] bg-[#FFFFFF1A] backdrop-blur-[24px] border-[2px] border-[#FFFFFF1A] rounded-[1.667vw] w-[42.431vw]">
<p className="headline2 mb-[1.667vw] !font-medium">
Научили AI-модель выполнять некоторые <br /> функции мнеджера даже
лучше человека
Научили AI-модель выполнять полезные
<br /> функции в помощь человеку
</p>
<div className="flex flex-wrap gap-x-[0.556vw] gap-y-[0.556vw] w-[100%] !font-medium">
<span className="btnm bg-[#FFFFFF29] py-[0.694vw] px-[1.389vw] rounded-full">
@@ -5,7 +5,7 @@ import ResultsAchievements from "./ResultsAchievements";
import QuestionFormModal from "@/components/modals/QuestionFormModal";
import ResultsTeamWheel from "../ResultsTeamWheel";
import { useModalStore } from "@/stores/useModalStore";
import ResutsCongratulations from "./ResutsCongratulations";
import ResultsCongratulations from "./ResutsCongratulations";
export default function ResultsEventsReelsWithAchievements() {
const containerRef = useRef<HTMLDivElement>(null);
@@ -32,11 +32,17 @@ export default function ResultsEventsReelsWithAchievements() {
</div>
{/* Team */}
<div className="flex flex-col justify-between h-[100vh]">
<div className="flex flex-col justify-between h-[100vh] relative">
<h2 className="line2 !font-medium mb-[1.944vw] mt-[10vw] z-[10] text-center">
Наш штат расширился на 30% <br /> Теперь в команде более 70 человек
</h2>
<div className="w-[66.806vw] h-[33.3vw] overflow-hidden mx-auto relative">
<div
style={{
maskImage:
"linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%)",
}}
className="w-[66.806vw] h-[33.3vw] overflow-hidden mx-auto relative"
>
<ResultsTeamWheel />
<div className="absolute left-[2.5vw] right-[2.5vw] top-[2.5vw] bottom-[-2.5vw] w-[calc(100%-5vw)] h-full rounded-t-full backdrop-blur-[24px]"></div>
</div>
@@ -51,6 +57,8 @@ export default function ResultsEventsReelsWithAchievements() {
Оставить заявку
</button>
</div>
<ResultsCongratulations />
</div>
</>
);
@@ -25,15 +25,15 @@ export default function ResultsGeneral() {
transform: useTransform(
discreteScroll,
[0.6, 0.7],
["translate(0%, 0%)", "translate(0%, -6.944vw)"]
["translate(0%, 0%)", "translate(0%, -8.944vw)"]
),
}}
className="text-center space-y-[1.528vw] absolute top-[16.056vw] z-10"
className="text-center space-y-[1.528vw] absolute top-[18.056vw] z-10"
>
<span className="accent">В 2025</span>
<ShiftText
className="absolute left-1/2 font-medium line2 text-center text-[4.444vw] h-[8.403vw] z-10"
className="absolute left-1/2 font-medium line2 text-center text-[4.444vw] h-[8.403vw] !z-[10]"
style={{ x: "-50%" }}
paragraphs={[
<>
@@ -270,7 +270,7 @@ function Appartaments({
<div className="size-[0.903vw] rotate-180">
<PlayIcon />
</div>
<div className="w-[9.722vw] h-[3.611vw] rounded-[1.111vw] bg-white flex items-center gap-[0.556vw] p-[0.139vw] pr-[1.111vw]">
<div className="w-[9.722vw] h-[3.611vw] rounded-[1.111vw] bg-white flex items-center gap-[0.556vw] p-[0.139vw] pl-[0.2vw] pr-[1.111vw]">
<div className="size-[3.264vw] rounded-[0.903vw] bg-[#7A55FF] flex items-center justify-center btnm !text-[1.181vw]">
{floor}
</div>
@@ -302,9 +302,9 @@ function Appartaments({
]
),
}}
src="/img/pages/results/general/building.png"
src="/img/pages/results/general/building_lg.png"
alt=""
className="absolute top-[6.167vw] left-1/2 w-[34.306vw] h-full object-cover"
className="absolute top-[6.167vw] left-1/2 h-full object-cover"
/>
<motion.div
style={{
@@ -324,12 +324,12 @@ function Appartaments({
]
),
}}
className="absolute bottom-[70px] left-[67.917vw] flex flex-col gap-[0.556vw]"
className="absolute bottom-[20px] left-[68.917vw] flex flex-col gap-[0.556vw]"
>
<FloorIndicator
floor={32}
appartamentsCount={8}
className="mb-[17.083vw]"
className="mb-[16.583vw]"
/>
<FloorIndicator floor={25} appartamentsCount={12} />
<FloorIndicator floor={24} appartamentsCount={11} />
@@ -437,7 +437,7 @@ function Reservations({
}}
src="/img/pages/results/general/booking.png"
alt=""
className="absolute bottom-0 left-1/2 w-[27.778vw] h-[23.1vw] object-cover z-[10]"
className="absolute bottom-0 left-1/2 w-[27.778vw] h-[23.1vw] object-cover z-[3]"
/>
<motion.img
style={{
@@ -459,7 +459,7 @@ function Reservations({
}}
src="/img/pages/results/general/booking.png"
alt=""
className="absolute bottom-0 left-1/2 w-[27.778vw] h-[23.1vw] object-cover z-[9]"
className="absolute bottom-0 left-1/2 w-[27.778vw] h-[23.1vw] object-cover z-[2]"
/>
<motion.img
style={{
@@ -481,7 +481,7 @@ function Reservations({
}}
src="/img/pages/results/general/booking.png"
alt=""
className="absolute bottom-0 left-1/2 w-[27.778vw] h-[23.1vw] object-cover z-[8]"
className="absolute bottom-0 left-1/2 w-[27.778vw] h-[23.1vw] object-cover z-[1]"
/>
</>
);
@@ -529,9 +529,9 @@ function ReservationsTotal({
className="bottom-[4.861vw] left-[20.833vw] "
/>
<BookingCard
src="/img/pages/results/general/booking_3.png"
className="bottom-[4.861vw] right-[20.833vw] "
imageClassName="translate-x-[2vw] h-[18.222vw] translate-y-[1vw]"
src="/img/pages/results/general/booking_3_alt.png"
className="bottom-[4.861vw] right-[20.833vw] overflow-hidden"
imageClassName="translate-x-[2vw] h-[24.444vw] translate-y-[1vw] ml-[5vw]"
/>
<BookingCard
src="/img/pages/results/general/booking_2.png"
@@ -1,78 +1,149 @@
/* eslint-disable @next/next/no-img-element */
import ReadIcon from "@/components/icons/ReadIcon";
import React from "react";
import congratulationsData from "@/consts/congratulations.json";
import { VideoPlayer } from "@/ui/VideoPlayer";
import { motion, useInView } from "framer-motion";
import { useEffect, useRef, useState } from "react";
import { ReviewTab } from "../../MainPage/Reviews/ReviewTab";
import { useMediaQueries } from "@/hooks/useMediaQueries";
export default function ResutsCongratulations() {
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 inView = useInView(ref, { margin: "100% 0px -75% 0px" });
return (
<div className="max-lg:space-y-2 h-[100vh] w-full">
<div
ref={ref}
className={`relative mx-auto lg:mt-[9.722vw] md:max-lg:mt-[13.021vw] mt-[100px] transition-all duration-500 md:max-lg:w-[95.833vw] md:max-lg:h-[62.5vw] w-[94.444vw] h-[154.722vw] ${
inView
? "lg:w-[97.222vw] lg:h-[42.778vw]"
: "lg:w-[72.222vw] lg:h-[37.5vw]"
}`}
>
<VideoPlayer
src={congratulationsData[tab].src}
showMutingBtn
ref={videoRef}
>
<div className="lg:space-y-6 space-y-4 absolute lg:left-6 lg:top-6 md:max-lg:top-4 left-4 bottom-4 transition-opacity z-[5] lg:max-w-[40vw]">
<p className="text2 opacity-80">
{congratulationsData[tab].author}
</p>
<p className="accent font-medium">
{congratulationsData[tab].text}
</p>
</div>
{inView && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="absolute flex flex-nowrap bottom-6 left-6 gap-2 z-[6] max-lg:hidden"
>
{congratulationsData.map(
({ author, title, miniImage }, index) => (
<ReviewTab
key={author}
image={miniImage}
onClick={() => setTab(index)}
currentPlaying={tab === index}
title={title}
<div className="w-full lg:pt-[15.556vw] lg:pb-[7.5vw] md:pb-[9.505vw] pb-[20.278vw]">
<div className="flex flex-col lg:gap-[5.556vw] md:gap-[5.556vw] gap-[0px] relative z-[10] items-center ">
<div className="flex lg:flex-row flex-col lg:gap-[2.778vw] md:gap-[6.25vw] gap-[6.111vw] items-end max-lg:w-full max-lg:items-center max-lg:justify-center max-lg:md:px-[13.151vw] max-md:px-[6.667vw] max-lg:min-h-screen">
<MessageSender
name="Георгий Уморин"
avatar="/img/pages/results/components/wheel-avatars/5.png"
subtitle="генеральный директор и основатель"
/>
)
)}
</motion.div>
)}
</VideoPlayer>
<MessageBubble time="13:07">
<p>
Для нас каждый проект это не просто ещё одна реализация, а
совместная работа с девелопером над конкретной бизнес-задачей. Мы
всегда смотрим на решения в долгую: как они будут работать через
год, два, пять, и как впишутся в реальные процессы отдела продаж.
</p>
<p>
GRAFF.estate строится как компания-партнёр, а не как подрядчик «на
один проект». Именно поэтому мы внимательно относимся к деталям,
срокам и обязательствам и мы рады работать с проектами, где можем
принести реальную пользу.
</p>
</MessageBubble>
</div>
<div className="flex gap-2 z-[3] lg:hidden mt-3 max-lg:-mx-5 max-lg:px-5 overflow-auto [scrollbar-width:none] [-webkit-scrollbar-width:none] snap-x snap-mandatory scroll-pl-6">
{congratulationsData.map(({ author, title, miniImage }, index) => (
<ReviewTab
key={author}
currentPlaying={tab === index}
image={miniImage}
onClick={() => setTab(index)}
title={title}
<div className="flex lg:flex-row-reverse flex-col lg:gap-[2.778vw] md:gap-[6.25vw] gap-[6.111vw] items-end max-lg:w-full max-lg:items-center max-lg:justify-center max-lg:md:px-[13.151vw] max-md:px-[6.667vw]">
<MessageSender
name="Вячеслав Зернов"
avatar="/img/pages/results/components/wheel-avatars/4.png"
subtitle="директор по продукту"
/>
))}
<MessageBubble position="right" time="13:08">
<p>
За этот год над проектами вместе с нами работала команда из более
чем 70 сильных специалистов. Мы относимся к продуктам наших
клиентов бережно и внимательно, потому что понимаем, какую роль
они играют в бизнесе и продажах.
</p>
<p>
Опираясь на этот опыт, мы развиваем и собственный продукт
цифровой инструмент продаж GRAFF.estate. Он растёт вместе с
проектами, которые мы делаем, и с задачами, которые вы перед нами
ставите.
</p>
<p>
Спасибо тем, кто уже доверил нам свои проекты. Мы ценим это
доверие и ответственность, которую оно за собой несёт. И, конечно,
будем рады новым задачам и новым партнёрствам в следующем году.
</p>
<strong>До встречи в новом году.</strong>
</MessageBubble>
</div>
</div>
</div>
);
}
function MessageBubble({
children,
position = "left",
time = "12:00",
}: {
children: React.ReactNode;
position?: "left" | "right";
time?: string;
}) {
const { isMd, isLg } = useMediaQueries();
return (
<div
className={`bg-[#FFFFFF1A] ${
position === "left" && isLg
? "lg:rounded-bl-none md:rounded-bl-none rounded-bl-none"
: "lg:rounded-br-none md:rounded-br-none rounded-br-none"
} backdrop-blur-[24px] text-white lg:rounded-[1.111vw] md:rounded-[2.083vw] rounded-[4.444vw] lg:w-[50.556vw] w-full lg:p-[1.944vw] md:p-[3.646vw] p-[5.556vw] lg:pb-[3.056vw] md:pb-[5.729vw] pb-[10.556vw] flex flex-col lg:gap-[1.111vw] md:gap-[2.083vw] gap-[3.333vw] lg:text-[1.389vw] text-[3.333vw] leading-[135%] tracking-[0] relative`}
>
{children}
{/* treugolnik */}
<div
className={`absolute bottom-0 ${
position === "left" && isLg
? "lg:left-[-1.111vw] md:left-[-2.083vw] left-[-1.563vw]"
: "lg:right-[-1.111vw] md:right-[-2.083vw] right-[-1.563vw]"
}
lg:size-[1.111vw] md:size-[2.083vw] size-[1.563vw] bg-[#FFFFFF1A] backdrop-blur-[24px]`}
style={{
maskImage: `radial-gradient(circle at ${
position === "left" && isLg ? "top left" : "top right"
}, transparent ${
isLg ? "1.111vw" : isMd ? "2.083vw" : "1.111vw"
}, black ${isLg ? "1.111vw" : isMd ? "2.083vw" : "1.111vw"} )`,
WebkitMaskImage: `radial-gradient(circle at ${
position === "left" && isLg ? "top left" : "top right"
}, transparent ${
isLg ? "1.111vw" : isMd ? "2.083vw" : "1.111vw"
}, black ${isLg ? "1.111vw" : isMd ? "2.083vw" : "1.111vw"} )`,
}}
/>
<div
className={`flex items-center lg:gap-[0.556vw] md:gap-[1.042vw] gap-[1.563vw] absolute lg:bottom-[0.556vw] md:bottom-[1.042vw] bottom-[2.222vw] ${
position === "left" && isLg
? "lg:left-[1.944vw] md:left-[3.646vw] left-[5.556vw]"
: "lg:right-[1.944vw] md:right-[3.646vw] right-[5.556vw]"
}`}
>
<p className="lg:text-[0.972vw] md:text-[1.823vw] text-[3.333vw] leading-[135%] tracking-[0]">
{time}
</p>
<div className="text-white lg:size-[1.389vw] md:size-[2.604vw] size-[4.444vw]">
<ReadIcon />
</div>
</div>
</div>
);
}
function MessageSender({
name,
avatar,
subtitle,
}: {
name: string;
avatar: string;
subtitle: string;
}) {
return (
<div className="flex flex-col items-center lg:gap-[0.833vw] md:gap-[1.563vw] gap-[3.333vw]">
<div className="text-white lg:size-[16.667vw] md:size-[31.25vw] size-[35.556vw]">
<img className="size-full" src={avatar} alt={name} />
</div>
<div className="flex flex-col items-center">
<p className="!font-medium lg:text-[1.667vw] md:text-[3.125vw] text-[4.444vw] leading-[135%] tracking-[0] headline-1">
{name}
</p>
<p className="lg:text-[0.972vw] md:text-[1.823vw] text-[2.778vw] leading-[135%] tracking-[0] text-1 opacity-60">
{subtitle}
</p>
</div>
</div>
);
@@ -42,7 +42,7 @@ export default function CardsSwiper({
return (
<>
<div
className={`relative md:aspect-[736/650] aspect-[320/360] [transform-style:preserve-3d] md:px-[20.24vw] px-[15vw] my-auto ${className}`}
className={`relative md:aspect-[736/650] aspect-[320/320] [transform-style:preserve-3d] md:px-[20.24vw] px-[15vw] my-auto ${className}`}
{...handlers}
>
{cards.map((card, index) => (
@@ -57,7 +57,7 @@ export default function CardsSwiper({
))}
</div>
{controls && (
<div className="flex items-center justify-center gap-[1.111vw] w-[94.444vw] mx-auto md:mb-[2vw] mb-[4.444vw] z-[3]">
<div className="flex items-center justify-center gap-[1.111vw] w-[94.444vw] mx-auto md:mb-[2vw] mb-[10.444vw] z-[3]">
<button
onClick={handleSwipeLeft}
className="size-[15.556vw] flex items-center justify-center"
@@ -14,7 +14,8 @@ export default function SlideWrapper({
return (
<div
className={clsx(
`snap-center md:rounded-[5.208vw] rounded-[11.111vw] overflow-hidden relative ${className}`
`snap-center md:rounded-[5.208vw] rounded-[11.111vw] overflow-hidden relative [-webkit-mask-image:-webkit-radial-gradient(white,black)] will-change-transform ${className}`
// [-webkit-mask-image:-webkit-radial-gradient(white,black)] will-change-transform - overflow fix for safari
)}
style={{
minHeight: "calc(var(--vh, 1vh) * 100)",
@@ -1,4 +1,4 @@
import React, { useRef } from "react";
import React, { useRef, useEffect, useCallback } from "react";
import { useViewportHeight } from "@/hooks/useViewportHeight";
interface SnapWrapperProps {
@@ -10,6 +10,74 @@ export default function SnapWrapper({ children }: SnapWrapperProps) {
const childRefs = useRef<HTMLDivElement[]>([]);
const flatChildren = React.Children.toArray(children);
const containerRef = useRef<HTMLDivElement>(null);
const lastScrollTop = useRef(0);
const rafId = useRef<number | null>(null);
// Handle scroll to hide/show iOS Safari address bar
const handleScroll = useCallback(() => {
const container = containerRef.current;
if (!container) return;
const currentScrollTop = container.scrollTop;
// Cancel previous animation frame if exists
if (rafId.current !== null) {
cancelAnimationFrame(rafId.current);
}
// If scroll is at the top ( < 40), show the address bar
if (currentScrollTop < 40) {
rafId.current = requestAnimationFrame(() => {
if (window.scrollY > 0) {
try {
window.scrollTo({ top: 0, behavior: "instant" as ScrollBehavior });
} catch {
// Fallback for older browsers
window.scrollTo(0, 0);
}
}
rafId.current = null;
});
}
// Check if user is scrolling down
else if (currentScrollTop > lastScrollTop.current && currentScrollTop > 0) {
// Trigger a minimal window scroll to hide the address bar on iOS
rafId.current = requestAnimationFrame(() => {
if (window.scrollY === 0) {
try {
window.scrollTo({ top: 1, behavior: "instant" as ScrollBehavior });
} catch {
// Fallback for older browsers
window.scrollTo(0, 1);
}
}
rafId.current = null;
});
}
lastScrollTop.current = currentScrollTop;
}, []);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
// Ensure body has enough height for minimal scroll on iOS
const originalBodyMinHeight = document.body.style.minHeight;
document.body.style.minHeight = "101vh";
container.addEventListener("scroll", handleScroll, { passive: true });
return () => {
container.removeEventListener("scroll", handleScroll);
document.body.style.minHeight = originalBodyMinHeight;
// Cancel any pending animation frame
if (rafId.current !== null) {
cancelAnimationFrame(rafId.current);
}
};
}, []);
return (
<div
@@ -17,6 +85,8 @@ export default function SnapWrapper({ children }: SnapWrapperProps) {
className="overflow-y-scroll overflow-x-hidden hide-scrollbars snap-y snap-mandatory"
style={{
height: "calc(var(--vh, 1vh) * 100)",
WebkitOverflowScrolling: "touch",
scrollBehavior: "smooth",
}}
>
{flatChildren.map((child, index) => (
@@ -42,8 +42,8 @@ export default function AI() {
<div className="md:mb-[6.25vw] mb-[11.111vw] z-[3]">
<p className="text-white text-center w-[91.111vw] mx-auto headline2 !font-medium ">
Научили AI-модель выполнять некоторые функции{" "}
<br className="md:block hidden " /> мнеджера даже лучше человека{" "}
Научили AI-модель выполнять полезные
<br className="md:block hidden " /> функции в&nbsp;помощь человеку
</p>
<div className="flex flex-wrap gap-[0.521vw] md:w-[60.469vw] w-[96.889vw] mx-auto justify-center mt-[3.333vw] ">
<GraffLabel title="Саммаризация встречи" />
@@ -55,7 +55,7 @@ export default function Details() {
/>
</div>
<div className="md:col-span-2 md:overflow-hidden flex flex-col md:justify-start justify-center gap-[0.556vw] backdrop-blur-[24px] [-webkit-backdrop-filter:blur(24px)] bg-[#FFFFFF1A] md:py-[4.167vw] md:pl-[4.167vw] flex-1 pl-[5.556vw] border-[2px] border-[#FFFFFF1A] md:rounded-[3.125vw] rounded-[8.889vw]">
<div className="md:col-span-2 md:overflow-hidden flex flex-col md:justify-start relative justify-center gap-[0.556vw] backdrop-blur-[24px] [-webkit-backdrop-filter:blur(24px)] bg-[#FFFFFF1A] md:py-[4.167vw] md:pl-[4.167vw] flex-1 pl-[5.556vw] border-[2px] border-[#FFFFFF1A] md:rounded-[3.125vw] rounded-[8.889vw]">
<p className="md:headline1 headline2 !font-medium mb-[2.222vw]">
Новая модель <br /> интерактивного стола
</p>
@@ -63,7 +63,7 @@ export default function Details() {
<img
src="/img/pages/results/reels/details/Mobile/table.png"
alt=""
className="absolute md:bottom-[-21.229vw] bottom-[5.556vw] md:right-[2.5vw] right-[5.556vw] md:h-[64.844vw] h-[29.444vw] object-cover"
className="absolute md:bottom-[-20.229vw] max-md:top-[50%] max-md:-translate-y-1/2 md:right-[2.5vw] right-[5.556vw] md:h-[64.844vw] h-[29.444vw] object-cover"
/>
</div>
</div>
@@ -1,5 +1,5 @@
/* eslint-disable @next/next/no-img-element */
import CardsSwiper from "../../../Tablet&Mobile/CardsSwiper";
import CardsSwiper from "../../components/CardsSwiper";
import SlideWrapper from "../../components/SlideWrapper";
export default function Modules() {
@@ -145,7 +145,7 @@ export default function Next() {
<img
src={src}
alt=""
className="h-full w-[50vw] flex-shrink-0 object-cover rounded-[1.111vw]"
className="h-full w-[50vw] flex-shrink-0 object-cover rounded-[2.222vw]"
/>
);
}
@@ -10,7 +10,7 @@ export default function Team() {
return (
<SlideWrapper
id="team-area"
className="snap-end md:mt-[1.042vw] mt-[2.222vw] flex flex-col bg-[url('/img/pages/results/components/gradients/mobile-team.png')] bg-center bg-cover bg-no-repeat bg-[#37393B99]"
className="md:mt-[1.042vw] mt-[2.222vw] flex flex-col bg-[url('/img/pages/results/components/gradients/mobile-team.png')] bg-center bg-cover bg-no-repeat bg-[#37393B99]"
>
<p className="text-white text-center z-[3] w-[91.111vw] mx-auto headline2 md:text-[5.208vw] !font-medium md:mt-[5.208vw] mt-[11.111vw]">
Наш штат расширился на 30%
@@ -18,9 +18,9 @@ export default function Team() {
<h2 className="text-white text-center text-[5.208vw] font-medium leading-[95%] tracking-[-0.02em] z-[10] md:mt-[1vw] mb-[12.222vw] line2 mt-[3.333vw]">
Теперь в команде <br className="md:hidden block" /> более 70 человек
</h2>
<div className="w-[267.222vw] ml-[-82.805vw] mx-auto overflow-hidden">
<ResultsTeamWheel />
<div className="w-[267.222vw] ml-[-82.805vw] mx-auto">
<ResultsTeamWheel />
<button
onClick={() =>
setModal(
@@ -3,105 +3,78 @@ import MuteIcon from "@/components/icons/MuteIcon";
import React, { useEffect, useRef, useState } from "react";
import { CircularProgressbar } from "react-circular-progressbar";
import SlideWrapper from "../../components/SlideWrapper";
import ResutsCongratulations from "../../../Desktop/ResutsCongratulations";
export default function MobileCongratulations() {
return (
<SlideWrapper className='md:mt-[1.042vw] mt-[2.222vw] flex flex-col bg-[url("/img/pages/results/components/gradients/mobile-congratulations.png")] bg-center bg-cover bg-no-repeat !h-max'>
<img
src="/img/pages/results/components/garland/garland_mobile.svg"
alt=""
className="absolute w-full object-cover"
/>
<div>
<p className="text-white text-center z-[3] w-[91.111vw] mx-auto md:mt-[49.479vw] md:text-[2.604vw] mt-[47.778vw] headline2 !font-medium">
Пару слов об итогах года <br /> от генерального директора и основателя
<br />
GRAFF.estate Георгия Уморина
</p>
<div className="md:w-[59.896vw] md:h-[59.896vw] w-[83.333vw] h-[83.333vw] mx-auto mt-[6.667vw] mb-[11.111vw]">
<RoundVideo src="/videos/pages/home/story.mp4" />
</div>
</div>
<div>
<p className="text-white text-center z-[3] w-[91.111vw] mx-auto md:text-[2.604vw] headline2 !font-medium">
Поздравление от директора по продукту
<br />
Вячеслава Зернова
</p>
<div className="md:w-[59.896vw] md:h-[59.896vw] w-[83.333vw] h-[83.333vw] mx-auto mt-[6.667vw] mb-[16.667vw]">
<RoundVideo src="/videos/pages/home/story.mp4" />
</div>
</div>
<SlideWrapper className='md:mt-[1.042vw] mt-[2.222vw] flex flex-col bg-[url("/img/pages/results/components/gradients/mobile-purple.png")] bg-center bg-cover bg-no-repeat !h-max !snap-start'>
<ResutsCongratulations />
</SlideWrapper>
);
}
function RoundVideo({ src }: { src: string }) {
const videoRef = useRef<HTMLVideoElement>(null);
const [progress, setProgress] = useState<number>(0);
const [isMuted, setIsMuted] = useState<boolean>(true);
// function RoundVideo({ src }: { src: string }) {
// const videoRef = useRef<HTMLVideoElement>(null);
// const [progress, setProgress] = useState<number>(0);
// const [isMuted, setIsMuted] = useState<boolean>(true);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
const timeUpdateHandler = () =>
setProgress((video.currentTime / video.duration) * 100);
// useEffect(() => {
// const video = videoRef.current;
// if (!video) return;
// const timeUpdateHandler = () =>
// setProgress((video.currentTime / video.duration) * 100);
videoRef.current.addEventListener("timeupdate", timeUpdateHandler);
return () => video.removeEventListener("timeupdate", timeUpdateHandler);
}, []);
// videoRef.current.addEventListener("timeupdate", timeUpdateHandler);
// return () => video.removeEventListener("timeupdate", timeUpdateHandler);
// }, []);
function onVideoClick() {
setIsMuted(!isMuted);
}
// function onVideoClick() {
// setIsMuted(!isMuted);
// }
return (
<div className="aspect-square relative">
<button
onClick={onVideoClick}
className="bg-transaparent w-full h-full absolute top-0 left-0 rounded-full z-[10]"
></button>
<video
ref={videoRef}
src={src}
poster="/img/pages/home/motivation/stories_poster.jpg"
className="aspect-square object-cover w-full h-full rounded-full cursor-pointer z-[9]"
loop
playsInline
autoPlay
muted={isMuted}
/>
// return (
// <div className="aspect-square relative">
// <button
// onClick={onVideoClick}
// className="bg-transaparent w-full h-full absolute top-0 left-0 rounded-full z-[10]"
// ></button>
// <video
// ref={videoRef}
// src={src}
// poster="/img/pages/home/motivation/stories_poster.jpg"
// className="aspect-square object-cover w-full h-full rounded-full cursor-pointer z-[9]"
// loop
// playsInline
// autoPlay
// muted={isMuted}
// />
{/* Индикатор заглушенного звука */}
{isMuted && (
<div className="md:size-[4.167vw] size-[8.889vw] absolute bottom-[3.333vw] left-[50%] translate-x-[-50%] z-[12] rounded-full bg-[#FFFFFF33] backdrop-blur-[24px] flex items-center justify-center text-white pointer-events-none">
<div className="md:size-[2.604vw] size-[4.444vw]">
<MuteIcon />
</div>
</div>
)}
// {/* Индикатор заглушенного звука */}
// {isMuted && (
// <div className="md:size-[4.167vw] size-[8.889vw] absolute bottom-[3.333vw] left-[50%] translate-x-[-50%] z-[12] rounded-full bg-[#FFFFFF33] backdrop-blur-[24px] flex items-center justify-center text-white pointer-events-none">
// <div className="md:size-[2.604vw] size-[4.444vw]">
// <MuteIcon />
// </div>
// </div>
// )}
<CircularProgressbar
value={progress}
strokeWidth={2}
styles={
Math.trunc(progress) === 0
? {}
: {
path: {
stroke: "white",
strokeLinecap: "round",
transition: "linear",
transitionDuration: "0.3s",
},
}
}
className="absolute top-0 text-white rounded-full"
/>
</div>
);
}
// <CircularProgressbar
// value={progress}
// strokeWidth={2}
// styles={
// Math.trunc(progress) === 0
// ? {}
// : {
// path: {
// stroke: "white",
// strokeLinecap: "round",
// transition: "linear",
// transitionDuration: "0.3s",
// },
// }
// }
// className="absolute top-0 text-white rounded-full"
// />
// </div>
// );
// }
@@ -48,28 +48,12 @@ export default function Appartaments() {
<br className="md:hidden block" /> нового дома
</p>
<div className="absolute md:h-[62.76vw] h-[78.278vw] md:w-[49.349vw] w-[63.056vw] md:top-[-62.76vw] top-[-78.8vw] left-1/2 -translate-x-1/2">
<div className="absolute md:h-[62.76vw] h-[78.278vw] md:w-[55.349vw] w-[63.056vw] md:top-[-62.86vw] top-[-78.8vw] left-1/2 -translate-x-1/2 ">
<img
src="/img/pages/results/general/building.png"
alt=""
className="w-full h-full "
/>
<FloorIndicator
floor={32}
appartamentsCount={8}
className="absolute md:top-[20.063vw] md:right-[-7.161vw] top-[21.389vw] right-[-3.333vw]"
/>
<FloorIndicator
floor={25}
appartamentsCount={8}
className="absolute md:top-[45.063vw] md:right-[-7.161vw] top-[33.333vw] right-[-3.333vw]"
/>
<FloorIndicator
floor={24}
appartamentsCount={6}
className="absolute md:top-[55.063vw] md:right-[-7.161vw] top-[65.556vw] right-[-3.333vw]"
/>
</div>
</div>
</SlideWrapper>
@@ -1,10 +1,18 @@
/* eslint-disable @next/next/no-img-element */
import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState, useMemo } from "react";
import ShiftText from "../../../ShiftText";
import { motion, useMotionValue } from "framer-motion";
import SlideWrapper from "../../components/SlideWrapper";
import { useViewportHeight } from "@/hooks/useViewportHeight";
import { useInView } from "framer-motion";
const calculateFlexBasis = (isMd: boolean) => {
const viewportHeight = window.innerHeight;
const vhMultiplier = isMd ? 85 : 77;
const calculatedValue = (viewportHeight * vhMultiplier) / 100 / 2.8;
return `${calculatedValue}px`;
};
export default function Projects({ isMd }: { isMd: boolean }) {
const [expanded, setExpanded] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
@@ -13,15 +21,17 @@ export default function Projects({ isMd }: { isMd: boolean }) {
ease: "easeInOut",
};
const progress = useMotionValue(0);
const isInView = useInView(containerRef);
useViewportHeight();
const initialFlexBasis = useMemo(() => calculateFlexBasis(isMd), [isMd]);
useEffect(() => {
if (isInView) {
const timeout = setTimeout(() => {
setExpanded(true);
progress.set(1);
}, 2000);
}, 500);
return () => clearTimeout(timeout);
}
}, [isInView]);
@@ -40,18 +50,14 @@ export default function Projects({ isMd }: { isMd: boolean }) {
gap: "1.111vw",
flexGrow: 0,
flexShrink: 0,
flexBasis: isMd ? "calc(85vh/2.8)" : "calc(77vh/2.8)",
flexBasis: initialFlexBasis,
}}
animate={{
translateX: expanded ? 100 * offsetDirection : 0,
gap: expanded ? "0.556vw" : "1.111vw",
flexGrow: expanded ? 1 : 0,
flexShrink: expanded ? 1 : 0,
flexBasis: expanded
? "0%"
: isMd
? "calc(85vh/2.8)"
: "calc(77vh/2.8)",
flexBasis: expanded ? "0%" : initialFlexBasis,
}}
transition={{
...transition,
@@ -76,7 +82,7 @@ export default function Projects({ isMd }: { isMd: boolean }) {
src={src}
alt=""
className={
"md:w-[70.703vw] md:aspect-[544/307] aspect-[294/166] object-cover flex-shrink h-full"
"md:w-[70.703vw] md:aspect-[544/307] aspect-[294/166] object-cover flex-shrink max-h-full"
}
/>
);
@@ -3,7 +3,7 @@
import { motion, MotionValue, useScroll, useTransform } from "framer-motion";
import ResultsGarland from "./Desktop/ResultsGarland";
import { useRef } from "react";
import { useRef, useEffect } from "react";
import ResultsProjects from "./Desktop/ResultsProjects";
import ResultsGeneral from "./Desktop/ResultsGeneral";
import ResultsEventsReelsWithAchievements from "./Desktop/ResultsEventsReelsWithAchievements";
@@ -40,6 +40,21 @@ export function Results2025() {
const { isLg, isMd } = useMediaQueries();
useEffect(() => {
// Сохраняем текущее значение overflow-y
const originalOverflowY = document.body.style.overflowY;
// Отключаем overflow-y на всех разрешениях кроме Lg
if (!isLg) {
document.body.style.overflowY = "hidden";
}
// Восстанавливаем при размонтировании
return () => {
document.body.style.overflowY = originalOverflowY;
};
}, [isLg]);
return (
<div
ref={containerRef}
@@ -53,7 +68,6 @@ export function Results2025() {
<ResultsMap />
<ResultsEventsReelsWithAchievements />
<ResutsCongratulations />
<Button scrollYProgress={scrollYProgress} />
</>
)}
@@ -79,6 +93,7 @@ export function Results2025() {
<Details />
<Team />
<MobileCongratulations />
<div className="px-[10px] pt-[10px] snap-start">
<Feedback />
</div>
@@ -118,40 +133,3 @@ function Button({ scrollYProgress }: { scrollYProgress: MotionValue<number> }) {
</motion.button>
);
}
function BgGradientTablet({
scrollYProgress,
}: {
scrollYProgress: MotionValue<number>;
}) {
return (
<div className="fixed top-0 bottom-0 h-[100vh] inset-0 overflow-hidden z-[-1] md:block hidden">
<motion.img
style={{
opacity: useTransform(
scrollYProgress,
[0.5, 0.53, 0.57, 0.6, 0.8, 0.9],
[1, 0, 0, 1, 1, 0]
),
}}
src="/img/pages/results/tablet_bg.png"
alt=""
draggable={false}
className="w-full h-full max-md:hidden"
/>
<motion.img
style={{
opacity: useTransform(
scrollYProgress,
[0.4, 0.45, 0.48, 0.53, 0.85, 0.9],
[1, 0, 0, 1, 1, 0]
),
}}
src="/img/pages/results/mobile_bg.png"
alt=""
draggable={false}
className="w-full h-full max-md:block hidden"
/>
</div>
);
}
@@ -211,7 +211,7 @@ export default function ResultsTeamWheel() {
return (
<div
ref={wheelRef}
className={`lg:size-[66.806vw] md:size-[160vw] size-[220.222vw] size- [267.222vw] relative ${
className={`lg:size-[66.806vw] md:size-[160vw] size-[220.222vw] relative ${
isDragging ? "cursor-grabbing" : isSpinning ? "" : "cursor-grab"
} bg-[url('/img/pages/results/components/wheel.svg')] bg-cover bg-center mx-auto rounded-full z-[10]`}
onMouseDown={handleMouseDown}
@@ -1,367 +0,0 @@
/* eslint-disable @next/next/no-img-element */
import React, { useState } from "react";
import CardsSwiper from "./CardsSwiper";
import { AnimatePresence, motion } from "framer-motion";
import ResultsTeamWheel from "../ResultsTeamWheel";
import QuestionFormModal from "@/components/modals/QuestionFormModal";
import { useModalStore } from "@/stores/useModalStore";
const cards = [
{
id: 0,
video: "/videos/pages/results/reels/Движение.mp4",
},
{
id: 1,
video: "/videos/pages/results/reels/Технобилд.mp4",
},
{
id: 2,
video: "/videos/pages/results/reels/WOW.mp4",
},
];
const idToTitle = {
0: "Форум недвижимости «Движение»",
1: "Международный строительный форум 100+ TechnoBuild",
2: "Фестиваль маркетинга и креатива WOW FEST",
};
export default function ResultsEventsReelsWithAchievementsMobile() {
const [currentCardId, setCurrentCardId] = useState(0);
function handleCardChange(newCardId: number) {
setCurrentCardId(newCardId);
}
const { setModal } = useModalStore();
return (
<div className="relative flex flex-col mt-[15.625vw]">
<h2 className="text-[5.208vw] text-white text-center leading-[95%] tracking-[-0.02em] max-md:line2">
Самые яркие <br /> события за 2025 год
</h2>
<div className="flex flex-col">
<CardsSwiper
cards={cards}
onChange={handleCardChange}
className="md:mt-[6.25vw] mt-[11.111vw]"
/>
<AnimatePresence mode="wait">
<motion.span
key={currentCardId}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
className="accent text-center md:mt-[3.125vw] mt-[5.556vw] md:w-[45.104vw] max-md:h-[20.833vw] w-[90vw] mx-auto"
>
{idToTitle[currentCardId as keyof typeof idToTitle]}
</motion.span>
</AnimatePresence>
<Legenda />
<GraffView />
<Modules />
<Details />
<Team />
</div>
</div>
);
}
function ModuleCard({
title,
description,
children,
bgImage,
className,
}: {
title: string;
description?: string;
children?: React.ReactNode;
bgImage?: string;
className?: string;
}) {
return (
<div
className={`relative border-[2px] border-[#FFFFFF1A] rounded-[3.125vw] md:p-[4.167vw] p-[5.556vw] bg-[#FFFFFF1A] ba ckdrop-blur-[24px] overflow-hidden ${className} ${
bgImage ? "bg-cover bg-center bg-no-repeat" : ""
}`}
style={{
willChange: "transform",
}}
>
{bgImage && (
<div
className="absolute top-0 left-0 w-full h-full bg-cover bg-center bg-no-repeat"
style={{ backgroundImage: `url(${bgImage})` }}
/>
)}
<h3 className="headline1 md:mb-[1.042vw] mb-[2.222vw] relative z-[2] whitespace-pre-line">
{title}
</h3>
{bgImage && (
<div className="absolute left-[-2px] top-[-2px] rounded-[3.125vw] w-[calc(100%+4px)] h-[calc(100%+4px)] bg-[linear-gradient(to_bottom,#00000050,#FFFFFF00)] z-[1]"></div>
)}
{description && <div className="text1 opacity-60">{description}</div>}
{children && <div className="mt-[1.042vw]">{children}</div>}
</div>
);
}
function Legenda() {
return (
<>
<h2 className="text-[5.208vw] md:mt-[13.021vw] mt-[27.778vw] md:mb-[6.25vw] mb-[11.111vw] text-white text-center leading-[95%] tracking-[-0.02em] max-md:line2">
Но победы <br /> были не только наши!
</h2>
<div className="relative w-full max-md:px-[2.778vw] max-md:grid max-md:grid-cols-2 max-md:grid-rows-[47.778vw_78.889vw] max-md:gap-[1.111vw]">
<video
src="/videos/pages/results/legenda.MOV"
autoPlay
loop
muted
playsInline
className="md:absolute top-[3.255vw] md:max-lg:left-[1.302vw] md:w-[31.25vw] w-full max-md:h-full md:rounded-[3.125vw] rounded-[4.444vw] object-cover md:aspect-[240/192] aspect-square"
/>
<img
src="/img/pages/results/achievments/Tablet/office-r.png"
className="md:absolute top-[25.479vw] right-[5.953vw] md:size-[31.25vw] w-full md:rounded-[3.125vw] rounded-[4.444vw]"
alt=""
/>
<img
src="/img/pages/results/achievments/Tablet/office.png"
className="md:size-[58.333vw] size-full object-cover mx-auto max-md:col-span-2 max-md:object-top md:rounded-[3.125vw] rounded-[4.444vw]"
alt=""
/>
<div className="md:w-[64.583vw] max-md:col-span-2 w-full mx-auto md:mt-[3.125vw] mt-[5.556vw] flex flex-col md:gap-[1.563vw] gap-[3.333vw] justify-center items-center max-md:accent">
<h3 className="accent md:text-center text-left">
Звание лучшего офиса продаж второй год подряд забрал наш партнер{" "}
</h3>
<span className="text-[1.823vw] md:w-[80%] w-full font leading-[135%] opacity-60 md:text-center text-left max-md:text1">
ГК Легенда для которой мы укомплектовали офис продаж ЖК Северный
Порт интерактивными инструментами продаж
</span>
</div>
</div>
</>
);
}
function GraffLabel({
title,
className,
}: {
title: string;
className?: string;
}) {
return (
<div
className={`bg-[#FFFFFF29] py-[1.563vw] backdrop-blur-[24px] px-[3.125vw] rounded-full md:btnm btns w-max ${className}`}
>
{title}
</div>
);
}
function GraffView() {
return (
<>
<span className="headline2 text-[#FFFFFF60] text-center md:mt-[13.021vw] mt-[27.778vw]">
Год был удачным не только снаружи,
<br /> но и внутри компании
</span>
<div className="w-full md:mt-[3.125vw] mt-[3.333vw]">
<h2 className="text-[5.208vw] text-white text-center leading-[95%] tracking-[-0.02em] max-md:line2 max-md:w-[94.444vw] mx-auto !font-medium">
Запустили новый продукт <br className="max-md:hidden" />
GRAFF.estate View
</h2>
<div className="w-full h-full relative">
<img
src="/img/pages/results/general/booking_3.png"
className="object-cover md:h-[70.125vw] h-[127.778vw] md:ml-[29.948vw] ml-[18.611vw] md:mt-[6.25vw] mt-[11.111vw] md:mb-[2.083vw] mb-[4.444vw] "
alt=""
/>
<h3 className="accent text-white md:text-center text-left md:w-[64.583vw] w-[94.444vw] mx-auto max-md:mb-[3.333vw]">
Показываем виды из любой квартиры еще
<br className="max-md:hidden" /> не построенного жилого комплекса
</h3>
<div className="md:absolute top-[22.786vw] left-[2.604vw] flex flex-col md:gap-[0.521vw] gap-[1.111vw] max-md:mb-[1.111vw] max-md:px-[2.778vw] text-white">
<GraffLabel title="Динамическое изменение перспективы" />
<GraffLabel title="Высокое качество визуализации" />
</div>
<div className="md:absolute top-[22.786vw] right-[2.604vw] md:max-w-[58.135vw] max-w-[90vw] flex-wrap flex md:justify-end md:gap-[0.521vw] max-md:px-[2.778vw] text-white">
<GraffLabel
title="Выбор квартиры"
className=" max-md:mr-[1.111vw]"
/>
<GraffLabel title="Выбор вида" />
{/* Переносит flex элементы на строку ниже */}
<div className="[flex-basis:100%]" />
<GraffLabel
title="Интеграция с CRM"
className=" max-md:mt-[1.111vw] max-md:mr-[1.111vw]"
/>
<GraffLabel
title="Кастомизация продукта"
className=" max-md:mt-[1.111vw]"
/>
</div>
</div>
</div>
</>
);
}
function Modules() {
return (
<div className="w-full md:mt-[13.021vw] mt-[27.778vw] md:px-[2.083vw] px-[2.778vw]">
<h2 className="text-[5.208vw] text-white text-center leading-[95%] tracking-[-0.02em] md:mb-[6.25vw] mb-[11.111vw] max-md:line2 !font-medium">
Добавили 6 новых модулей
<br className="max-md:hidden" /> в приложение
</h2>
<div className="grid md:grid-cols-2 grid-cols-1 md:gap-[1.563vw] gap-[2.222vw] md:grid-rows-[repeat(3,39.714vw)] grid-rows-[83.333vw_74.444vw_72.222vw_67.222vw_72.222vw_58.333vw]">
<ModuleCard
title="Цифровой город"
description="Маршруты до значимых точек города в режиме онлайн"
>
<img
src="/img/pages/results/reels/modules/1.png"
alt=""
className="absolute bottom-[0] left-1/2 -translate-x-1/2 md:w-[29.167vw] w-[62.222vw] object-cover rounded-t-[1.111vw]"
/>
</ModuleCard>
<ModuleCard
title="Сценарии жизни"
description="Визуализация подстраивается под конкретного пользователя. Приложение показывает интересные ему локации в ЖК, подходящие типы планировок"
>
<img
src="/img/pages/results/reels/modules/2.png"
alt=""
className="absolute md:bottom-[4.167vw] bottom-[5.556vw] md:right-[4.167vw] right-[5.556vw] md:w-[14.583vw] w-[29.167vw] object-cover"
/>
</ModuleCard>
<ModuleCard
title="Конфигуратор отделки"
bgImage="/img/pages/results/reels/modules/3.png"
/>
<ModuleCard
title="Инженерные системы"
description="Покажем клиенту все внутренние составляющие и технологичность проекта"
>
<img
src="/img/pages/results/reels/modules/4.png"
alt=""
className="absolute md:bottom-[4.167vw] bottom-[5.556vw] md:right-[4.167vw] right-[5.556vw] md:w-[52.552vw] w-[110vw] object-cover"
/>
</ModuleCard>
<ModuleCard
title="Смена сезонов"
bgImage="/img/pages/results/reels/modules/5.png"
></ModuleCard>
<ModuleCard title="Покупка парковочных мест и локеров">
<img
src="/img/pages/results/reels/modules/6.png"
alt=""
className="absolute md:bottom-[4.167vw] bottom-[5.556vw] md:right-[4.167vw] right-[5.556vw] md:w-[38.802vw] w-[83.333vw] object-cover"
/>
</ModuleCard>
</div>
</div>
);
}
function Details() {
return (
<div className="w-full md:mt-[13.021vw] mt-[27.778vw] md:px-[2.083vw] px-[2.778vw]">
<div className="text-[5.208vw] text-white text-center leading-[95%] tracking-[-0.02em] md:mb-[6.25vw] mb-[11.111vw] max-md:line2 ">
Кредо этого года: внимание к деталям, <br /> стремление к идеалу
</div>
<div className="grid md:grid-cols-2 grid-cols-1 gap-[1.563vw] md:grid-rows-[repeat(2,60.417vw)] grid-rows-[83.333vw_95vw_100vw]">
<ModuleCard
title="Облака по расписанию"
description="Интегрировали API, передающее направление и скорость ветра в реальном времени именно в той локации, где находится проект"
className="overflow-clip"
>
<img
src="/img/pages/results/reels/details/cloud.png"
alt=""
className="absolute md:bottom-[9.635vw] bottom-[5vw] md:right-[-10.75vw] right-[-25vw] md:w-[60.026vw] w-[128.056vw] object-cover"
/>
<img
src="/img/pages/results/reels/details/cloud.png"
alt=""
className="absolute md:bottom-[1.042vw] bottom-[-15.222vw] md:left-[-28.188vw] left-[-57.778vw] md:w-[60.026vw] w-[128.056vw] object-cover"
/>
</ModuleCard>
<ModuleCard
title="Добавили в приложения птиц"
description="Они не только летают в небе, но и садятся на землю или деревья вокруг ЖК"
>
<video
src="/img/pages/results/reels/details/birds.mp4"
autoPlay
loop
muted
playsInline
className="absolute md:bottom-[11.849vw] bottom-[5.556vw] md:right-[11.849vw] right-[5.556vw] md:size-[23.438vw] size-[50vw] object-cover md:rounded-[2.604vw] rounded-[5.556vw]"
/>
</ModuleCard>
<ModuleCard
title={"Новая модель\n интерактивного стола"}
description="Более тонкая и изящная"
className="md:col-span-2"
>
<img
src="/img/pages/results/reels/details/table.png"
alt=""
className="absolute bottom-[0] md:right-[3.472vw] md:w-[59.115vw] w-[80.333vw] h-auto object-cover"
/>
</ModuleCard>
</div>
</div>
);
}
function Team() {
const { setModal } = useModalStore();
return (
<div className="w-full mt-[13.021vw]">
<h2 className="text-[5.208vw] mx-auto text-white text-center leading-[95%] tracking-[-0.02em] mb-[6.25vw] max-md:line2 max-md:w-[94.444vw]">
Наш штат расширился на 30%, теперь <br />
<span className="text-gradient">в команде более 70 человек</span>
</h2>
<div className="md:w-[140vw] md:ml-[-20vw] md:h-[93vw] w-[267.222vw] ml-[-82.805vw] h-[130vw] mx-auto overflow-hidden">
<ResultsTeamWheel />
<div className="absolute bottom-0 left-0 w-full h-[50vw] bg-[linear-gradient(to_bottom,#00000000,#0F1011_90%)] z-[10]" />
<button
onClick={() =>
setModal(
<QuestionFormModal products={["Интерактивная презентация"]} />
)
}
className="btnl z-[11] w-max bg-[radial-gradient(circle_at_right_top,#FF79D2_0%,#C932E8_20%,#7A55FF_60%)] md:px-[4.688vw] px-[10.111vw] md:py-[2.604vw] py-[5.556vw] md:rounded-[2.083vw] rounded-[4.444vw] absolute md:bottom-[20.063vw] bottom-[5vw] left-1/2 -translate-x-1/2"
>
Оставить заявку
</button>
</div>
</div>
);
}
@@ -1,119 +0,0 @@
/* eslint-disable @next/next/no-img-element */
"use client";
import QuestionFormModal from "@/components/modals/QuestionFormModal";
import useDiscreteScroll from "@/hooks/useDiscreteScroll";
import { useMediaQueries } from "@/hooks/useMediaQueries";
import { useModalStore } from "@/stores/useModalStore";
import { motion, useScroll, useTransform } from "framer-motion";
import React, { useRef } from "react";
export default function ResultsGarlandMobile() {
const containerRef = useRef<HTMLDivElement>(null);
const { setModal } = useModalStore();
const { isMd } = useMediaQueries();
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start start", "end end"],
});
const discreteScroll = useDiscreteScroll(scrollYProgress, [0, 0.5, 0.7]);
const ANIMATION_BREAKPOINTS = {
opacity: [0, 1],
top: [!isMd ? "120vw" : "85vw", "0vw"],
height: [!isMd ? "25vh" : "25vh", "100vh"],
paddingInline: [!isMd ? "2.778vw" : "2.214vw", "0vw"],
radius: [!isMd ? "4.444vw" : "2.083vw", "0vw"],
width: [!isMd ? "100%" : "65vw", "100%"],
};
const overlayOpacity = useTransform(
discreteScroll,
[0.5, 0.7],
ANIMATION_BREAKPOINTS.opacity
);
const videoTop = useTransform(
discreteScroll,
[0.5, 0.7],
ANIMATION_BREAKPOINTS.top
);
const videoHeight = useTransform(
discreteScroll,
[0.5, 0.7],
ANIMATION_BREAKPOINTS.height
);
const videoPadding = useTransform(
discreteScroll,
[0.5, 0.7],
ANIMATION_BREAKPOINTS.paddingInline
);
const videoRadius = useTransform(
discreteScroll,
[0.5, 0.7],
ANIMATION_BREAKPOINTS.radius
);
const videoWidth = useTransform(
discreteScroll,
[0.5, 0.7],
ANIMATION_BREAKPOINTS.width
);
return (
<div ref={containerRef} className="relative h-[250vh]">
<div className="sticky top-0 min-h-[100vh] flex flex-col items-center justify-start">
<img
src="/img/pages/results/components/garland/garland_tablet.svg"
alt="garland"
draggable={false}
className="absolute inset-0 w-full z-10 max-md:scale-[2]"
/>
<h1 className="md:text-[6.25vw] text-[11.111vw] md:mb-[3.125vw] mb-[6.667vw] text-white text-center md:mt-[57.292vw] mt-[60.056vw] leading-[95%] tracking-[-0.02em] font-medium">
Подводим <br className="md:hidden block" /> итоги 2025 года <br /> в
GRAFF.estate
</h1>
<button
onClick={() => {
setModal(
<QuestionFormModal products={["Интерактивная презентация"]} />
);
}}
className="btnl
bg-[radial-gradient(circle_at_right_top,#FF79D2_0%,#C932E8_20%,#7A55FF_60%)]
md:px-[4.167vw] px-[4.444vw] md:py-[2.604vw] py-[4.722vw] md:rounded-[2.083vw] mb-[6.667vw] rounded-[4.444vw] mx-auto font-medium"
>
Стать первыми в 2026
</button>
<motion.div
style={{ opacity: overlayOpacity }}
className="absolute inset-0 bg-gradient-to-b from-[#0f101190] to-[#1a1b1c00]"
/>
<motion.div
style={{
height: videoHeight,
top: videoTop,
paddingInline: videoPadding,
}}
className="absolute left-0 w-full z-[20]"
>
<motion.video
style={{
borderRadius: videoRadius,
width: videoWidth,
}}
src="/videos/pages/about/hero_video.mp4"
autoPlay
muted
loop
playsInline
className=" h-full object-cover md:mx-auto"
/>
</motion.div>
</div>
</div>
);
}
@@ -1,51 +0,0 @@
/* eslint-disable @next/next/no-img-element */
"use client";
import React from "react";
import CardsSwiper from "./CardsSwiper";
import { useMediaQueries } from "@/hooks/useMediaQueries";
export default function ResultsGeneralMobile() {
const { isMd } = useMediaQueries();
const cards = [
{
id: 1,
image: isMd
? "/img/pages/results/general/tablet/slide_1.png"
: "/img/pages/results/general/mobile/slide_1.png",
},
{
id: 2,
image: isMd
? "/img/pages/results/general/tablet/slide_2.png"
: "/img/pages/results/general/mobile/slide_2.png",
},
{
id: 3,
image: isMd
? "/img/pages/results/general/tablet/slide_3.png"
: "/img/pages/results/general/mobile/slide_3.png",
},
{
id: 4,
image: isMd
? "/img/pages/results/general/tablet/slide_4.png"
: "/img/pages/results/general/mobile/slide_4.png",
},
{
id: 5,
image: isMd
? "/img/pages/results/general/tablet/slide_5.png"
: "/img/pages/results/general/mobile/slide_5.png",
},
];
return (
<div className="relative flex flex-col">
<div className="text-center md:mb-[6.25vw] mb-[6.667vw] md:mt-[13.021vw] mt-[27.778vw]">
<span className="accent">В 2025 году</span>
</div>
<CardsSwiper cards={cards} />
</div>
);
}
@@ -1,150 +0,0 @@
/* eslint-disable @next/next/no-img-element */
"use client";
import React, { useRef } from "react";
import { ResultsProjectsItem } from "../Desktop/ResultsProjects";
import { motion, MotionValue, useScroll, useTransform } from "framer-motion";
import ShiftText from "../ShiftText";
export default function ResultsProjectsMobile() {
const containerRef = useRef<HTMLDivElement>(null);
const latestContainerRef = useRef<HTMLDivElement>(null);
const { scrollYProgress: projectsScroll } = useScroll({
target: containerRef,
offset: ["start start", "end start"],
});
const { scrollYProgress: latestScroll } = useScroll({
target: latestContainerRef,
offset: ["start start", "end start"],
});
return (
<>
<div ref={containerRef} className="relative h-[250vh]">
<Projects projectsScroll={projectsScroll} />
</div>
<div ref={latestContainerRef} className="relative h-[180vh]">
<LatestProjects latestScroll={latestScroll} />
</div>
</>
);
}
function Projects({ projectsScroll }: { projectsScroll: MotionValue<number> }) {
return (
<div className="sticky top-0 h-[100vh] flex flex-col items-center md:pt-[12.021vw] pt-[23.778vw] pb-[13.889vw]">
<ShiftText
className="left-1/2 overflow-clip text-[5.208vw] pb-[5vw] text-white text-center leading-[95%] tracking-[-0.02em] max-md:line2 !font-medium"
// style={{ x: "-50%" }}
viewportPadding={0}
paragraphs={[
<>
Закончили разработку
<br />и сдали 24 проекта
</>,
<>
И теперь у нас более
<br /> 70 проектов
</>,
]}
shiftBreakpoints={[0, 0.3]}
scrollProgress={projectsScroll}
/>
<motion.div className="w-full md:h-[calc(100%-10.778vw)] h-[calc(100%-20.778vw)] grid grid-rows-3 md:gap-[1.563vw] gap-[2.222vw] origin-center mt-[5vw]">
<motion.div
style={{
translateX: useTransform(projectsScroll, [0.1, 1], [0, -300]),
}}
className="flex md:gap-[1.563vw] gap-[2.222vw] justify-center"
>
<ResultsProjectsItem src="/img/pages/results/projects/10.png" />
<ResultsProjectsItem src="/img/pages/results/projects/2.png" />
<ResultsProjectsItem src="/img/pages/results/projects/3.png" />
<ResultsProjectsItem src="/img/pages/results/projects/7.png" />
</motion.div>
<motion.div
style={{
translateX: useTransform(projectsScroll, [0.1, 1], [0, 300]),
}}
className="flex md:gap-[1.563vw] gap-[2.222vw] justify-center"
>
<ResultsProjectsItem src="/img/pages/results/projects/2.png" />
<ResultsProjectsItem src="/img/pages/results/projects/6.png" />
<ResultsProjectsItem src="/img/pages/results/projects/main.png" />
<ResultsProjectsItem src="/img/pages/results/projects/7.png" />
<ResultsProjectsItem src="/img/pages/results/projects/2.png" />
</motion.div>
<motion.div
style={{
translateX: useTransform(projectsScroll, [0.1, 1], [0, -300]),
}}
className="flex md:gap-[1.563vw] gap-[2.222vw] justify-center"
>
<ResultsProjectsItem src="/img/pages/results/projects/3.png" />
<ResultsProjectsItem src="/img/pages/results/projects/10.png" />
<ResultsProjectsItem src="/img/pages/results/projects/11.png" />
<ResultsProjectsItem src="/img/pages/results/projects/6.png" />
</motion.div>
</motion.div>
</div>
);
}
function LatestProjects({
latestScroll,
}: {
latestScroll: MotionValue<number>;
}) {
return (
<div className="sticky h-[100vh] top-0 flex flex-col items-center ">
<h2 className="text-[5.208vw] md:mt-[13.021vw] mt-[27.778vw] md:mb-[6.25vw] mb-[6.667vw] text-white text-center leading-[95%] tracking-[-0.02em] max-md:line2 !font-medium">
Последними <br /> из которых стали
</h2>
<div className="flex flex-col md:gap-[1.042vw] gap-[2.222vw] md:px-[4.818vw] px-[2.778vw] h-full pb-[2.778vw]">
<div className="w-[94.444vw] p-[4.444vw] flex-1 flex flex-col items-center justify-between bg-center bg-cover no-repeat rounded-[4.444vw] bg-[url('/img/pages/results/projects/badayevskiy.jpg')]">
<motion.img
style={{
opacity: useTransform(latestScroll, [0, 0.1], [0, 1]),
x: useTransform(latestScroll, [0, 0.1], [-100, 0]),
}}
src="/img/pages/results/projects/badayevskiy_logo.svg"
alt=""
className="w-[56.111vw]"
/>
<motion.img
style={{
opacity: useTransform(latestScroll, [0, 0.1], [0, 1]),
x: useTransform(latestScroll, [0, 0.1], [100, 0]),
}}
src="/img/pages/results/projects/badayevskiy_builder.svg"
alt=""
className="w-[19.444vw]"
/>
</div>
<div className="w-[94.444vw] p-[4.444vw] flex-1 flex flex-col items-center justify-between md:bg-[top_0%_left_150%] bg-[top_0%_left_57%] bg-cover no-repeat rounded-[4.444vw] bg-[url('/img/pages/results/projects/rozhdestvenka.png')]">
<motion.img
style={{
opacity: useTransform(latestScroll, [0, 0.1], [0, 1]),
x: useTransform(latestScroll, [0, 0.1], [-100, 0]),
}}
src="/img/pages/results/projects/rozhdestvenka_logo.svg"
alt=""
className="w-[68.611vw]"
/>
<motion.img
style={{
opacity: useTransform(latestScroll, [0, 0.1], [0, 1]),
x: useTransform(latestScroll, [0, 0.1], [100, 0]),
}}
src="/img/pages/results/projects/rozhdestvenka_builder.svg"
alt=""
className="w-[26.111vw]"
/>
</div>
</div>
</div>
);
}