started desktop prime modals

This commit is contained in:
2025-03-31 19:34:29 +05:00
parent d28e65b37d
commit 21a58d604e
16 changed files with 336 additions and 17 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 540 KiB

@@ -80,13 +80,6 @@ export function Slider({ mapPoint }: { mapPoint?: IMapProject[] }) {
}`}
>
<div className="flex gap-4 items-center">
{/* <Icon
name="loader"
svgProp={{
className:
'animate-spin lg:w-[1.389vw] lg:h-[1.389vw] h-5 w-5',
}}
/> */}
<LoaderIcon className="animate-spin lg:w-[1.389vw] lg:h-[1.389vw] h-5 w-5" />
<span className="text2 select-none">
Загружаем видео...
@@ -1,7 +1,11 @@
/* eslint-disable @next/next/no-img-element */
import { useModalStore } from '@/stores/useModalStore';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
export function ThreeDReelsCard({ slide }: { slide: number }) {
const { setModal } = useModalStore();
return (
<motion.div
animate={{
@@ -9,6 +13,17 @@ export function ThreeDReelsCard({ slide }: { slide: number }) {
bottom: slide > 10 ? '0vw' : undefined,
}}
className="p-[1.389vw] rounded-[1.389vw] -translate-y-[1.389vw] w-[13.611vw] h-[36.447vh] flex flex-col justify-between absolute bg-[#37393B99] right-[27.847vw]"
onClick={() =>
setModal(
<PrimeModal
categoryTitle="Рекламные материалы"
packages={['Стандарт']}
src="/videos/pages/prime/3d_reels.mp4"
title="3D-рилс"
text="3D-рилс — это быстрый путь в Instagram, TikTok и остальные соцсети. Наглядная мини-презентация ЖК в динамике, которую можно оперативно выпустить в ленту и зацепить потенциальных клиентов."
/>
)
}
>
<p className="btns font-medium px-[0.833vw] py-[0.486vw] rounded-[1.181vw] bg-[#37393B99] backdrop-blur-xs self-end">
{slide === 11
@@ -1,13 +1,28 @@
/* eslint-disable @next/next/no-img-element */
import { useModalStore } from '@/stores/useModalStore';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
export function ArchVisCard({ slide }: { slide: number }) {
const { setModal } = useModalStore();
return (
<motion.div
animate={{
opacity: +(slide > 11),
bottom: slide > 11 ? '26.316vh' : '93.684vh',
}}
onClick={() =>
setModal(
<PrimeModal
categoryTitle="Рекламные материалы"
title="Архитектурная визуализация"
text="Архитектурная визуализация — это «классика» рекламы в недвижимости. Красивые рендеры перекликаются с интерактивной презентацией, формируя единый облик проекта и повышая его узнаваемость."
packages={['Премиум', 'Бизнес']}
src="/img/pages/prime/seasons.png"
/>
)
}
className="w-[15.486vw] h-[29.211vh] -translate-y-[1.389vw] p-[1.389vw] rounded-[1.389vw] bg-[#37393B99] flex flex-col justify-between absolute bg-[length:9.979vw] bg-[url(/img/pages/prime/architecture.png)] bg-no-repeat bg-center"
>
<p className="px-[0.833vw] py-[0.486vw] rounded-[1.181vw] btns font-medium bg-[#37393B99] self-end">
@@ -1,15 +1,63 @@
'use client';
import { useModalStore } from '@/stores/useModalStore';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
import { categoryDescription } from '@/consts/categories';
/* eslint-disable @next/next/no-img-element */
export function DesignCard({ slide }: { slide: number }) {
const { setModal } = useModalStore();
return (
<motion.div
onClick={() =>
setModal(
<PrimeModal
src={`/img/pages/prime/designInterior${
slide === 11 ? '1' : slide === 12 ? '2' : slide === 13 ? '3' : '4'
}.jpg`}
packages={
slide === 11
? ['Стандарт']
: slide === 12
? ['Комфорт+', 'Стандарт']
: slide === 13
? ['Бизнес', 'Комфорт+', 'Стандарт']
: ['Премиум', 'Бизнес', 'Комфорт+', 'Стандарт']
}
title={
slide === 11
? `Чистовая отделка ( WhiteBox )`
: slide === 12
? 'В квартирах типовая мебель ( WhiteBox+ )'
: `Дизайн по референсам заказчика (${
slide === 13 ? '1 стиль' : '3 стиля'
})`
}
text={
categoryDescription['Дизайн интерьеров'].find(
(item) =>
item.title ===
(slide === 11
? `WhiteBox`
: slide === 12
? 'WhiteBox +'
: `Уникальный дизайн интерьеров${
slide === 13 ? '' : ' (3 стиля)'
}`)
)!.text1
}
categoryTitle="Дизайн интерьеров"
/>
)
}
animate={{
opacity: +(slide > 10),
bottom: slide > 10 ? '0vw' : undefined,
}}
transition={{ bounce: 'none' }}
className="p-[1.389vw] rounded-[1.389vw] -translate-y-[1.389vw] absolute bg-[#37393B99] backdrop-blur-[47.6px] w-[15.278vw] h-[24.737vh] flex flex-col justify-between"
className="p-[1.389vw] rounded-[1.389vw] -translate-y-[1.389vw] absolute bg-[#37393B99] backdrop-blur-[47.6px] w-[15.278vw] h-[24.737vh] flex flex-col justify-between cursor-pointer"
>
<div className="relative">
<img
@@ -1,12 +1,32 @@
'use client';
import { useModalStore } from '@/stores/useModalStore';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
import { categoryDescription } from '@/consts/categories';
export function EnvironmentCard({ slide }: { slide: number }) {
const { setModal } = useModalStore();
return (
<motion.div
animate={{
opacity: +(slide > 10),
bottom: slide > 10 ? '0vw' : undefined,
}}
onClick={() =>
setModal(
<PrimeModal
src={'/videos/pages/home/presentation/3_infrastructure.mp4'}
packages={['Премиум', 'Бизнес', 'Комфорт+', 'Стандарт']}
categoryTitle="Детальная проработка окружения"
title={'Детальная проработка ЖК и ближайшего благоустойства'}
text={
categoryDescription['Детальная проработка окружения'][0].text1
}
/>
)
}
className="p-[1.389vw] rounded-[1.389vw] -translate-y-[1.389vw] absolute bg-[url(/img/pages/prime/summer.jpg)] bg-cover bg-center flex items-end w-[23.681vw] h-[41.053vh] left-[17.708vw]"
>
<p className="btns max-w-2/3 font-medium">
@@ -1,12 +1,48 @@
/* eslint-disable @next/next/no-img-element */
import { useModalStore } from '@/stores/useModalStore';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
export function EquipmentCard({ slide }: { slide: number }) {
const { setModal } = useModalStore();
return (
<motion.div
animate={{
opacity: +(slide > 10),
bottom: slide > 10 ? '0vw' : undefined,
}}
onClick={() =>
setModal(
<PrimeModal
categoryTitle="Оборудование"
title={
slide === 11
? 'Настенная панель'
: slide === 12
? 'Брендированный стол 800 нит'
: 'Брендированный стол 2500 нит'
}
text={
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
}
packages={
slide === 11
? ['Стандарт']
: slide === 12
? ['Комфорт+', 'Стандарт']
: slide === 13
? ['Бизнес', 'Комфорт+', 'Стандарт']
: ['Премиум', 'Бизнес', 'Комфорт+', 'Стандарт']
}
src={
slide === 11
? '/img/pages/prime/wallPanel.png'
: '/img/pages/prime/brandTablet800.png'
}
/>
)
}
className="right-[11.458vw] p-[1.389vw] -translate-y-[1.389vw] rounded-[1.389vw] w-[15.556vw] h-[20.263vh] bg-[#37393B99] before:z-1 before:absolute before:inset-0 before:bg-gradient-to-t before:from-[#37393B99] overflow-hidden backdrop-blur-xs bg-cover bg-[position:calc(8/225*100%)_calc(15/225*100%)] flex items-end absolute"
>
<img
@@ -14,14 +50,14 @@ export function EquipmentCard({ slide }: { slide: number }) {
className={`absolute w-[15.625vw] transition-opacity bottom-0 left-1/2 -translate-x-1/2 object-cover object-center duration-300 ${
slide === 11 ? 'opacity-100' : 'opacity-0'
}`}
alt=""
alt="wall panel"
/>
<img
src="/img/pages/prime/brandTablet800.png"
className={`absolute w-[15.625vw] transition-opacity -bottom-1/3 left-1/2 -translate-x-1/2 object-cover object-center duration-300 ${
slide > 11 ? 'opacity-100' : 'opacity-0'
}`}
alt=""
alt="brand tablet"
/>
<p className="btns font-medium z-1">
{slide === 11
@@ -1,9 +1,25 @@
'use client';
/* eslint-disable @next/next/no-img-element */
import { useModalStore } from '@/stores/useModalStore';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
export function ExcursionVRCard({ slide }: { slide: number }) {
const { setModal } = useModalStore();
return (
<motion.div
onClick={() =>
setModal(
<PrimeModal
src={'/img/pages/prime/vrvr.png'}
packages={['Премиум', 'Бизнес']}
title="Экскурсия в VR"
text="Создается по референсам от заказчика в 1-3 вариантах. Квартира «оживает» мебелью и декором, это эмоционально «цепляет» потенциального покупателя."
/>
)
}
animate={{
opacity: +(slide > 12),
bottom: slide > 12 ? '42.632vh' : '93.684vh',
@@ -9,6 +9,7 @@ export function InterierConfiguratorCard({ slide }: { slide: number }) {
opacity: +(slide > 11),
bottom: slide > 11 ? '38.026vh' : '93.684vh',
}}
// onClick={() => setModal()}
className="p-[1.389vw] rounded-[1.389vw] -translate-y-[1.389vw] absolute bg-[#37393B99] w-[13.542vw] h-[17.105vh] flex flex-col justify-between backdrop-blur-[47.6px] right-[27.847vw]"
>
<img
@@ -1,10 +1,25 @@
/* eslint-disable @next/next/no-img-element */
import { useModalStore } from '@/stores/useModalStore';
import GraffIcon from '../../../../../public/icons/graff.svg';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
export function StreamingCard({ slide }: { slide: number }) {
const { setModal } = useModalStore();
return (
<motion.div
onClick={() =>
setModal(
<PrimeModal
categoryTitle="Удаленная демонстрация"
title="Graff.estate stream"
text="GRAFF.estate разворачивает «облако» и стримит 3D-сцену на устройство клиента (PC, мобильный), менеджер ведёт экскурсию в режиме реального времени."
packages={['Премиум']}
src="/videos/pages/home/streaming.mp4"
/>
)
}
animate={{
opacity: +(slide > 10),
bottom: slide > 10 ? '0vw' : undefined,
@@ -74,7 +74,9 @@ export function PrimeDesktopPage() {
<IntegrationCRM scrollProgress={scrollYProgress} page="prime" />
<Insolation scrollProgress={scrollYProgress} page="prime" />
<CommercialOffer scroll={scrollYProgress} />
<PackageTitle slide={slide} />
<DesignCard slide={slide} />
<EnvironmentCard slide={slide} />
<ThreeDReelsCard slide={slide} />
@@ -91,7 +93,9 @@ export function PrimeDesktopPage() {
<InteractiveWindowCard slide={slide} />
<AvatarCard slide={slide} />
<ScenarioCard slide={slide} />
<FolderAnimation scroll={scrollYProgress} />
<PrimeProgress target={target.current} scroll={scrollYProgress} />
</div>
<div ref={target} className="h-full" />
@@ -0,0 +1,110 @@
/* eslint-disable @next/next/no-img-element */
import { useModalStore } from '@/stores/useModalStore';
import CloseIcon from '../../../../../public/icons/close.svg';
import { categoryDescription } from '@/consts/categories';
import { useRef } from 'react';
export function PrimeModal({
src,
title,
text,
packages,
categoryTitle,
}: {
packages: string[];
src: string;
title: string;
text: string;
categoryTitle: string;
}) {
const { setModal } = useModalStore();
const ref = useRef<HTMLVideoElement>(null);
return (
<div className="flex items-center gap-[11.458vw] absolute inset-0 z-10 pl-[1.667vw] py-[1.389vw] before:inset-0 before:absolute before:bg-[#0F101199]">
<div className="flex flex-col gap-y-[0.417vw] z-1">
<button
onClick={() => setModal(null)}
className="w-[4.444vw] aspect-square rounded-[0.556vw] bg-white cursor-pointer"
/>
<button
onClick={() => setModal(null)}
className="w-[4.444vw] aspect-square rounded-[0.556vw] bg-white cursor-pointer"
/>
<button
onClick={() => setModal(null)}
className="w-[4.444vw] aspect-square rounded-[0.556vw] bg-white cursor-pointer"
/>
<button
onClick={() => setModal(null)}
className="w-[4.444vw] aspect-square rounded-[0.556vw] bg-white cursor-pointer"
/>
<button
onClick={() => setModal(null)}
className="w-[4.444vw] aspect-square rounded-[0.556vw] bg-white cursor-pointer"
/>
</div>
<div className="p-[3.333vw] h-full rounded-[1.944vw] bg-[#37393B99] [backdrop-filter:blur(120px)] flex flex-col justify-between gap-y-[1.667vw] w-[66.25vw]">
<div className="flex gap-[0.556vw]">
{packages.map((pack) => (
<p
key={pack}
className={
'px-[0.833vw] py-[0.486vw] rounded-[1.181vw] btns font-medium ' +
(pack === 'Премиум' ? 'bg-gradient' : 'bg-[#37393B99]')
}
>
{pack}
</p>
))}
</div>
<div className="gap-y-[0.278vw] flex flex-col flex-1">
{src.endsWith('mp4') ? (
categoryDescription[categoryTitle].find(
(item) => item.title === title
)?.type === 'videoScreen' ? (
<div className="bg-[url(/img/pages/home/presentation/touch_screen.png)] bg-no-repeat bg-[length:50%] bg-top relative h-full perspective-[11.5vw]">
<video
src={src}
ref={ref}
muted
autoPlay
playsInline
loop
className="rotate-x-4 left-1/2 -translate-x-1/2 absolute origin-top object-cover object-bottom w-[24.7vw] h-[11.5vw] top-[2.3vw]"
/>
</div>
) : (
<div className="flex-1 relative overflow-hidden rounded-[0.833vw]">
<video
className="rounded-[0.833vw] object-bottom absolute bottom-0"
src={src}
muted
autoPlay
playsInline
loop
/>
</div>
)
) : (
<div
className="h-full aspect-[858/401]a bg-[length:50%] bg-no-repeat bg-center rounded-[0.833vw]"
style={{ backgroundImage: `url(${src})` }}
/>
)}
<div className="p-[3.333vw] rounded-[1.111vw] bg-[#37393B99] flex flex-col gap-y-[1.389vw]">
<p className="heading1 font-medium">{title}</p>
<p className="text1">{text}</p>
</div>
</div>
</div>
<button
onClick={() => setModal(null)}
className="absolute top-[1.667vw] right-[1.667vw] bg-[#37393B99] p-[1.111vw] rounded-[1.111vw] cursor-pointer"
>
<CloseIcon className="w-[1.111vw] h-[1.111vw]" />
</button>
</div>
);
}
+22
View File
@@ -1,3 +1,4 @@
import LoaderIcon from '../../../public/icons/loader.svg';
import { videos } from '@/consts/presentation/videos';
import {
motion,
@@ -74,6 +75,10 @@ export function VideoLayerMain({ scroll }: { scroll: MotionValue<number> }) {
setSlide(Math.min(Math.trunc(value * videos.length), videos.length - 1))
);
const [currentBuffering, setCurrentBuffering] = useState(false);
useEffect(() => setCurrentBuffering(true), [slide]);
return (
<motion.div
style={{
@@ -101,6 +106,8 @@ export function VideoLayerMain({ scroll }: { scroll: MotionValue<number> }) {
)}
<motion.video
key={src}
onWaiting={() => setCurrentBuffering(true)}
onPlaying={() => setCurrentBuffering(false)}
src={
isViewportEntered
? `/videos/pages/home/presentation/${src}.mp4`
@@ -120,6 +127,21 @@ export function VideoLayerMain({ scroll }: { scroll: MotionValue<number> }) {
slide > index ? ' opacity-0' : ''
}`}
/>
<motion.div
style={{
zIndex: videos.length - index,
width: videoWidth,
height: videoHeight,
top,
}}
className="object-bottom object-cover origin-top absolute left-1/2 bg-black/50 -translate-x-1/2 !rotate-x-[4deg] flex justify-center items-center"
animate={{
opacity: currentBuffering ? 1 : 0,
}}
>
<LoaderIcon className="animate-spin lg:w-[1.389vw] lg:h-[1.389vw] h-5 w-5" />
<span className="text2 select-none">Загружаем видео...</span>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
+27 -3
View File
@@ -1,3 +1,4 @@
import LoaderIcon from '../../../public/icons/loader.svg';
import { primeVideos } from '@/consts/presentation/videos';
import {
motion,
@@ -145,6 +146,12 @@ export function VideoLayerPrime({ scroll }: { scroll: MotionValue<number> }) {
['3.602vw', '4.022vw', '4.022vw', '2.122vw', '10.855vw', '4.022vw']
);
const [currentBuffering, setCurrentBuffering] = useState(false);
useEffect(() => {
setCurrentBuffering(true);
}, [slide]);
return (
<motion.div
style={{
@@ -157,7 +164,7 @@ export function VideoLayerPrime({ scroll }: { scroll: MotionValue<number> }) {
}}
viewport={{ margin: '-10% 0% 0% 0%', once: true }}
onViewportEnter={handleOnViewportFeatureEnter}
className='absolute overflow-hidden h-[40.556vw] bg-[url(/img/pages/home/presentation/touch_screen.png)] bg-no-repeat bg-[length:48.333vw] bg-top'
className="absolute overflow-hidden h-[40.556vw] bg-[url(/img/pages/home/presentation/touch_screen.png)] bg-no-repeat bg-[length:48.333vw] bg-top"
>
{!!videoRefs.length &&
primeVideos.map(({ src }, index) =>
@@ -166,7 +173,7 @@ export function VideoLayerPrime({ scroll }: { scroll: MotionValue<number> }) {
{index === slide && slide > 2 && (
<motion.p
style={{ opacity }}
className='absolute left-[1.667vw] top-[1.667vw] z-10 max-w-[34.097vw] heading2 font-medium'
className="absolute left-[1.667vw] top-[1.667vw] z-10 max-w-[34.097vw] heading2 font-medium"
>
Graff.estate легко встраивается в существующую цепочку продаж,
собирает данные о пользователе и его поведении
@@ -189,13 +196,30 @@ export function VideoLayerPrime({ scroll }: { scroll: MotionValue<number> }) {
height: videoHeight,
top,
}}
onWaiting={() => setCurrentBuffering(true)}
onPlaying={() => setCurrentBuffering(false)}
className={`absolute w-[39.822vw] h-[19vw] origin-top object-bottom object-cover top-[3.602vw] !rotate-x-4 left-1/2 -translate-x-1/2 transition-opacity${
slide > index && slide < primeVideos.length
? ' opacity-0'
: ''
}`}
/>
<div className='h-[5.5vw] w-full bg-gradient-to-t from-[#0f1011] via-75% left-1/2 -translate-x-1/2 bottom-0 absolute' />
<motion.div
style={{
zIndex: primeVideos.length - index,
width: videoWidth,
height: videoHeight,
top,
}}
className="object-bottom object-cover origin-top absolute left-1/2 bg-black/50 -translate-x-1/2 !rotate-x-[4deg] flex justify-center items-center"
animate={{
opacity: currentBuffering ? 1 : 0,
}}
>
<LoaderIcon className="animate-spin lg:w-[1.389vw] lg:h-[1.389vw] h-5 w-5" />
<span className="text2 select-none">Загружаем видео...</span>
</motion.div>
<div className="h-[5.5vw] w-full bg-gradient-to-t from-[#0f1011] via-75% left-1/2 -translate-x-1/2 bottom-0 absolute" />
</>
) : (
<div
+2 -2
View File
@@ -103,7 +103,7 @@ export const categoryDescription: CategoryDescription = {
],
'Детальная проработка окружения': [
{
title: 'Детальная проработка ЖК и бижайшего благоустойства',
title: 'Детальная проработка ЖК и ближайшего благоустойства',
content: '/videos/pages/home/presentation/3_infrastructure.mp4',
text1:
'Когда клиент видит подробно отображённый район, он не тратит время на отдельный поиск инфографики и фотографий, а воспринимает всё в единой картине. Такое комплексное представление локации повышает доверие и ускоряет осознание удобств будущего проживания.',
@@ -116,7 +116,7 @@ export const categoryDescription: CategoryDescription = {
],
'Дизайн интерьеров': [
{
title: 'White Box',
title: 'WhiteBox',
content:
'bg-[url(/img/pages/prime/designInterior1.jpg)] rounded-2xl bg-cover md:w-[60vw] md:h-[40vh]',
text1: 'White Box — это квартиры без интерьеров',
+2 -2
View File
@@ -48,12 +48,12 @@ export const primeVideos = [
title: 'Инфраструктура',
},
{
src: '2_3dtour',
src: '7_favorites',
anchorImg: '/img/pages/prime/favorites.jpg',
title: 'Избранное',
},
{
src: '6_reservation',
src: '8_integrationCRM',
anchorImg: '/img/pages/prime/crm.jpg',
title: 'Интеграция с CRM',
},