tailwind 4 -> 3

This commit is contained in:
2025-05-30 12:30:23 +05:00
parent ba23078579
commit ca8499ab18
82 changed files with 1486 additions and 1262 deletions
+51
View File
@@ -0,0 +1,51 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
animation: {
"infinite-scroll": "infinite-scroll 45s linear infinite",
"highlight-product": "highlight-product 0.1s ease-in 0s",
},
keyframes: {
"infinite-scroll": {
from: { transform: "translateX(0%)" },
to: { transform: "translateX(-100%)" },
},
"highlight-product": {
"100%": {
backgroundImage: "url(/img/components/products/highlight.svg)",
},
},
scaling: {
"0%": {
transform: "min-width 31.6vw min-height 31.8vw",
transition: "transform 500ms",
},
"100%": {
transform: "min-width 48vw min-height 48vw",
},
},
},
},
},
plugins: [
function ({ addBase }) {
const preflightStyles = postcss.parse(
fs.readFileSync(
require.resolve("tailwindcss/lib/css/preflight.css"),
"utf8"
)
);
preflightStyles.walkRules((rule) => {
rule.selector = ".no-tailwind-base " + rule.selector;
});
addBase(preflightStyles.nodes);
},
],
};
BIN
View File
Binary file not shown.
+47
View File
@@ -0,0 +1,47 @@
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name graff.estate;
root /var/www/graff.estate/client/dist;
# SSL
ssl_certificate /etc/letsencrypt/live/graff.estate/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/graff.estate/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/graff.estate/chain.pem;
# security
include nginxconfig.io/security.conf;
# logging
access_log /var/log/nginx/access.log combined buffer=512k flush=1m;
error_log /var/log/nginx/error.log warn;
# index.html fallback
location / {
# try_files $uri $uri/ /index.html;
try_files $uri $uri.html $uri/ /index.html;
}
location /api {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:3003;
proxy_set_header Host $host;
include nginxconfig.io/proxy.conf;
}
# additional config
include nginxconfig.io/general.conf;
}
# HTTP redirect
server {
listen 80;
listen [::]:80;
server_name graff.estate;
include nginxconfig.io/letsencrypt.conf;
location / {
return 301 https://graff.estate$request_uri;
}
}
+10 -10
View File
@@ -1,7 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
distDir: 'dist',
output: "export",
distDir: "dist",
reactStrictMode: false,
future: { webpack: true },
@@ -15,7 +15,7 @@ const nextConfig = {
}
const fileLoaderRule = config.module.rules.find((rule) =>
rule.test?.test?.('.svg')
rule.test?.test?.(".svg")
);
config.module.rules.push(
@@ -30,7 +30,7 @@ const nextConfig = {
test: /\.svg$/i,
issuer: fileLoaderRule.issuer,
resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, // exclude if *.svg?url
use: ['@svgr/webpack'],
use: ["@svgr/webpack"],
}
);
@@ -39,14 +39,14 @@ const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'graff.estate',
port: '',
protocol: "https",
hostname: "graff.estate",
port: "",
},
{
protocol: 'https',
hostname: 'storage.yandexcloud.net',
port: '',
protocol: "https",
hostname: "storage.yandexcloud.net",
port: "",
},
],
},
+3 -4
View File
@@ -13,8 +13,6 @@
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.5.0",
"@tailwindcss/postcss": "^4.0.0",
"@tailwindcss/vite": "^4.0.0",
"@tanstack/react-query": "^5.62.7",
"@tanstack/react-query-devtools": "^5.64.2",
"@tinymce/tinymce-react": "^5.1.1",
@@ -29,7 +27,6 @@
"lenis": "^1.2.1",
"libphonenumber-js": "^1.11.7",
"next": "14.2.5",
"postcss": "^8.5.1",
"react": "^18",
"react-circular-progressbar": "^2.1.0",
"react-dom": "^18",
@@ -45,7 +42,6 @@
"react-transition-group": "^4.4.5",
"resize-observer-polyfill": "^1.5.1",
"sharp": "^0.33.5",
"tailwindcss": "^4.0.0",
"tinymce": "^7.4.1",
"usehooks-ts": "^3.1.0",
"zustand": "^4.5.4"
@@ -60,8 +56,11 @@
"@types/react-phone-number-input": "^3.1.37",
"@types/react-rangeslider": "^2.2.7",
"@types/react-transition-group": "^4.4.11",
"autoprefixer": "^10.4.21",
"eslint": "^8",
"eslint-config-next": "14.2.5",
"postcss": "^8.5.4",
"tailwindcss": "3",
"typescript": "^5"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
-8
View File
@@ -1,8 +0,0 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
};
export default config;
+1 -1
View File
@@ -1,4 +1,4 @@
import { InProcess } from '@/components/pages/InProcess';
import { InProcess } from "@/components/pages/InProcess";
export default function AboutPage() {
return <InProcess />;
+11 -11
View File
@@ -1,10 +1,10 @@
import { api } from '@/api';
import { RelevantArticlesPreview } from '@/components/pages/ArticlePage/RelevantArticlesPreview';
import { IArticle } from '@/types/IArticle';
import { QueryClient } from '@tanstack/react-query';
import { Metadata } from 'next';
import Link from 'next/link';
import CloseIcon from '../../../../../public/icons/close.svg';
import { api } from "@/api";
import { RelevantArticlesPreview } from "@/components/pages/ArticlePage/RelevantArticlesPreview";
import { IArticle } from "@/types/IArticle";
import { QueryClient } from "@tanstack/react-query";
import { Metadata } from "next";
import Link from "next/link";
import CloseIcon from "../../../../../public/icons/close.svg";
export async function generateMetadata({
params,
@@ -16,7 +16,7 @@ export async function generateMetadata({
const queryClient = new QueryClient();
const { cardImage, tags, title } = await queryClient.fetchQuery<IArticle>({
queryKey: ['articles', slug],
queryKey: ["articles", slug],
queryFn: async () => await api.get(`articles/${slug}`).json<IArticle>(),
});
@@ -29,7 +29,7 @@ export async function generateMetadata({
images: {
url: process.env.NEXT_PUBLIC_S3_BUCKET + cardImage,
},
siteName: 'graff.estate',
siteName: "graff.estate",
},
};
}
@@ -45,11 +45,11 @@ export default async function Layout({
return (
<section className="fixed inset-0 bg-[#0F101199] [backdrop-filter:blur(16px)] z-[14]">
<Link href={'/blog'} className="inset-0 fixed"></Link>
<Link href={"/blog"} className="inset-0 fixed"></Link>
<RelevantArticlesPreview slug={slug} />
{children}
<Link
href={'/blog'}
href={"/blog"}
className="fixed lg:right-[1.389vw] lg:top-[1.389vw] right-5 top-5 lg:rounded-[1.111vw] rounded-2xl bg-[#37393B99] lg:p-[1.111vw] p-4 cursor-pointer"
>
<CloseIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white" />
+10 -8
View File
@@ -1,12 +1,12 @@
import { api } from '@/api';
import { ArticleSyncPage } from '@/components/pages/ArticlePage/ArticleSyncPage';
import { IArticle } from '@/types/IArticle';
import { api } from "@/api";
import { ArticleSyncPage } from "@/components/pages/ArticlePage/ArticleSyncPage";
import { IArticle } from "@/types/IArticle";
import {
dehydrate,
HydrationBoundary,
QueryClient,
queryOptions,
} from '@tanstack/react-query';
} from "@tanstack/react-query";
export default async function ArticlePage({
params,
@@ -19,7 +19,7 @@ export default async function ArticlePage({
await queryClient.prefetchQuery(
queryOptions({
queryKey: ['articles', slug],
queryKey: ["articles", slug],
queryFn: () => api.get(`articles/${slug}`).json<IArticle>(),
})
);
@@ -32,11 +32,13 @@ export default async function ArticlePage({
}
export async function generateStaticParams() {
const queryClient = new QueryClient();
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 0 } },
});
const articles = await queryClient.fetchQuery<IArticle[]>({
queryKey: ['articles'],
queryFn: () => api.get('articles').json<IArticle[]>(),
queryKey: ["articles"],
queryFn: () => api.get("articles").json<IArticle[]>(),
});
return articles
+8 -8
View File
@@ -1,20 +1,20 @@
import { api } from '@/api';
import { ArticlesList } from '@/components/pages/BlogPage/ArticlesList';
import { ArticlesPageActions } from '@/components/pages/BlogPage/ArticlesPageActions';
import { IArticle } from '@/types/IArticle';
import { api } from "@/api";
import { ArticlesList } from "@/components/pages/BlogPage/ArticlesList";
import { ArticlesPageActions } from "@/components/pages/BlogPage/ArticlesPageActions";
import { IArticle } from "@/types/IArticle";
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query';
import { Suspense } from 'react';
} from "@tanstack/react-query";
import { Suspense } from "react";
export default async function BlogPage() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ['articles'],
queryFn: () => api.get('articles').json<IArticle[]>(),
queryKey: ["articles"],
queryFn: () => api.get("articles").json<IArticle[]>(),
});
return (
+6 -6
View File
@@ -1,18 +1,18 @@
import { Feedback } from '@/components/Layout/Feedback';
import { Footer } from '@/components/Layout/Footer';
import dynamic from 'next/dynamic';
import { PropsWithChildren } from 'react';
import { Feedback } from "@/components/Layout/Feedback";
import { Footer } from "@/components/Layout/Footer";
import dynamic from "next/dynamic";
import { PropsWithChildren } from "react";
export default function MainLayout({ children }: PropsWithChildren) {
const Header = dynamic(
() => import('@/components/Layout/Header').then((mod) => mod.Header),
() => import("@/components/Layout/Header").then((mod) => mod.Header),
{ ssr: false }
);
return (
<div className="flex flex-col">
<Header />
<main className="flex-1 md:max-lg:pt-[120px] pt-25 md:max-lg: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] overflow-clip relative">
{children}
<Feedback />
</main>
+22 -27
View File
@@ -1,28 +1,23 @@
import { api } from '@/api';
import { Awards } from '@/components/pages/MainPage/Awards';
import { Calculator } from '@/components/pages/MainPage/Calculator/Calculator';
import { Clients } from '@/components/pages/MainPage/Clients/Clients';
import { Map } from '@/components/pages/MainPage/Map/Map';
import { Motivation } from '@/components/pages/MainPage/Motivation';
import { Presentation } from '@/components/pages/MainPage/Presentation/Presentation';
import { Projects } from '@/components/pages/MainPage/Projects';
import { Reviews } from '@/components/pages/MainPage/Reviews/Reviews';
import { Statistics } from '@/components/pages/MainPage/Statistics';
import { Streaming } from '@/components/pages/MainPage/Streaming/Streaming';
import { ICompany } from '@/types/ICompany';
import { IProject } from '@/types/IProject';
import { api } from "@/api";
import { Awards } from "@/components/pages/MainPage/Awards";
import { Calculator } from "@/components/pages/MainPage/Calculator/Calculator";
import { Motivation } from "@/components/pages/MainPage/Motivation";
import { Reviews } from "@/components/pages/MainPage/Reviews/Reviews";
import { Statistics } from "@/components/pages/MainPage/Statistics";
import { Streaming } from "@/components/pages/MainPage/Streaming/Streaming";
import { IProject } from "@/types/IProject";
import {
dehydrate,
HydrationBoundary,
QueryClient,
queryOptions,
} from '@tanstack/react-query';
import { Integrations } from '@/components/pages/MainPage/Integrations/Integrations';
} from "@tanstack/react-query";
import { Integrations } from "@/components/pages/MainPage/Integrations/Integrations";
// import { Suspense } from 'react';
import dynamic from 'next/dynamic';
import { queryProjectsOptions } from '@/queries/getProjects';
import { queryCompaniesOptions } from '@/queries/getCompanies';
import { queryCompaniesCountOptions } from '@/queries/getCompaniesCount';
import dynamic from "next/dynamic";
import { queryProjectsOptions } from "@/queries/getProjects";
import { queryCompaniesOptions } from "@/queries/getCompanies";
import { queryCompaniesCountOptions } from "@/queries/getCompaniesCount";
import { Projects } from "@/components/pages/MainPage/Projects";
export default async function HomePage() {
const queryClient = new QueryClient();
@@ -30,8 +25,8 @@ export default async function HomePage() {
await queryClient.prefetchQuery(queryProjectsOptions);
await queryClient.prefetchQuery({
queryKey: ['projects', 'count'],
queryFn: () => api.get('projects/count').json<number>(),
queryKey: ["projects", "count"],
queryFn: () => api.get("projects/count").json<number>(),
});
await queryClient.prefetchQuery(queryCompaniesOptions);
@@ -39,27 +34,27 @@ export default async function HomePage() {
await queryClient.prefetchQuery(queryCompaniesCountOptions);
await queryClient.prefetchQuery({
queryKey: ['projects', 'Удаленная демонстрация'],
queryKey: ["projects", "Удаленная демонстрация"],
queryFn: () =>
api.get('projects?tags=Удаленная демонстрация').json<IProject[]>(),
api.get("projects?tags=Удаленная демонстрация").json<IProject[]>(),
});
const Presentation = dynamic(
() =>
import('@/components/pages/MainPage/Presentation/Presentation').then(
import("@/components/pages/MainPage/Presentation/Presentation").then(
(mod) => mod.Presentation
),
{ ssr: false }
);
const Map = dynamic(
() => import('@/components/pages/MainPage/Map/Map').then((mod) => mod.Map),
() => import("@/components/pages/MainPage/Map/Map").then((mod) => mod.Map),
{ ssr: false }
);
const Clients = dynamic(
() =>
import('@/components/pages/MainPage/Clients/Clients').then(
import("@/components/pages/MainPage/Clients/Clients").then(
(mod) => mod.Clients
),
{ ssr: false }
+9 -10
View File
@@ -1,13 +1,12 @@
import { InProcess } from '@/components/pages/InProcess';
// import { PrimeDesktopPage } from '@/components/pages/PrimePage/PrimePage';
// import PrimePageMobile from '@/components/pages/PrimePageMobile/PrimePageMobile';
import { PrimeDesktopPage } from "@/components/pages/PrimePage/PrimePage";
import PrimePageMobile from "@/components/pages/PrimePageMobile/PrimePageMobile";
export default function PrimePage() {
// return (
// <div className="relative">
// <PrimePageMobile />
// <PrimeDesktopPage />
// </div>
// );
return <InProcess />;
return (
<div className="relative">
<PrimePageMobile />
<PrimeDesktopPage />
</div>
);
// return <InProcess />;
}
+52 -60
View File
@@ -1,5 +1,7 @@
@import url('/fonts/TTHovesProAll/stylesheet.css');
@import 'tailwindcss';
@import url("/fonts/TTHovesProAll/stylesheet.css");
@tailwind base;
@tailwind components;
@tailwind utilities;
@theme {
--gradient: linear-gradient(87deg, #798fff 15%, #d375ff 100%);
@@ -10,7 +12,7 @@ html {
}
body {
font-family: 'TTHovesPro';
font-family: "TTHovesPro";
color: #fff;
background-color: #0f1011;
}
@@ -21,7 +23,7 @@ html {
}
.bg-gradient-card {
content: '';
content: "";
top: 0;
left: 0;
width: 100%;
@@ -46,67 +48,61 @@ html {
border-width: 2px;
}
@utility line1 {
@apply 2xl:text-[128px] lg:max-2xl:text-[clamp(96px,96px+(100vw-1440px)/96*32,128px)] md:max-lg:text-[clamp(56px,56px+(100vw-768px)/672*40,96px)] xs:max-md:text-[clamp(40px,40px+(100vw-360px)/408*16,56px)] text-[40px] leading-[85%];
}
@layer utilities {
.line1 {
@apply 2xl:text-[128px] lg:text-[clamp(96px,96px+(100vw-1440px)/96*32,128px)] md:text-[clamp(56px,56px+(100vw-768px)/672*40,96px)] xs:text-[clamp(40px,40px+(100vw-360px)/408*16,56px)] text-[40px] leading-[85%];
}
@utility line2 {
@apply lg:text-[clamp(64px,4.444vw,88px)] md:max-lg:text-[clamp(40px,40px+(100vw-768px)/672*24,64px)] xs:max-md:text-[clamp(32px,32px+(100vw-360px)/408*8,40px)] max-xs:text-[32px] leading-[95%];
}
.line2 {
@apply lg:text-[clamp(64px,4.444vw,88px)] md:text-[clamp(40px,40px+(100vw-768px)/672*24,64px)] xs:text-[clamp(32px,32px+(100vw-360px)/408*8,40px)] max-xs:text-[32px] leading-[95%];
}
@utility heading1 {
@apply lg:text-[clamp(28px,1.944vw,42px)] md:max-lg:text-[clamp(24px,24px+(100vw-768px)/672*4,28px)] text-2xl leading-[1.167];
}
.heading1 {
@apply lg:text-[clamp(28px,1.944vw,42px)] md:text-[clamp(24px,24px+(100vw-768px)/672*4,28px)] text-2xl leading-[1.167];
}
@utility heading2 {
@apply lg:text-[clamp(24px,1.667vw,36px)] md:max-lg:text-[clamp(20px,20px+(100vw-768px)/672*4,24px)] xs:max-md:text-[clamp(16px,16px+(100vw-360px)/408*4,20px)] text-base lg:leading-[1.2] leading-[1.125];
}
.heading2 {
@apply lg:text-[clamp(24px,1.667vw,36px)] md:text-[clamp(20px,20px+(100vw-768px)/672*4,24px)] xs:text-[clamp(16px,16px+(100vw-360px)/408*4,20px)] text-base lg:leading-[1.2] leading-[1.125];
}
@utility accent {
@apply lg:text-[clamp(32px,2.222vw,56px)] md:max-lg:text-[clamp(24px,24px+(100vw-768px)/672*8,32px)] text-2xl lg:leading-[1.1] leading-none;
}
.accent {
@apply lg:text-[clamp(32px,2.222vw,56px)] md:text-[clamp(24px,24px+(100vw-768px)/672*8,32px)] text-2xl lg:leading-[1.1] leading-none;
}
@utility text1 {
@apply lg:text-[clamp(18px,1.25vw,24px)] md:max-lg:text-[clamp(14px,14px+(100vw-768px)/672*4,18px)] text-sm leading-[1.35];
}
.text1 {
@apply lg:text-[clamp(18px,1.25vw,24px)] md:text-[clamp(14px,14px+(100vw-768px)/672*4,18px)] text-sm leading-[1.35];
}
@utility text2 {
@apply lg:text-[clamp(12px,0.972vw,20px)] md:max-lg:text-[clamp(12px,12px+(100vw-768px)/672*2,14px)] text-xs leading-[1.35];
}
.text2 {
@apply lg:text-[clamp(12px,0.972vw,20px)] md:text-[clamp(12px,12px+(100vw-768px)/672*2,14px)] text-xs leading-[1.35];
}
@utility btnl {
@apply lg:text-[clamp(18px,1.25vw,28px)] md:max-lg:text-[clamp(16px,16+(100vw-768px)/672*2,18px)] text-base leading-none;
}
.btnl {
@apply lg:text-[clamp(18px,1.25vw,28px)] md:text-[clamp(16px,16+(100vw-768px)/256*2,18px)] text-base leading-none;
}
@utility btnm {
@apply lg:text-[clamp(16px,1.111vw,24px)] md:max-lg:text-[clamp(14px,14px+(100vw-768px)/672*2,16px)] text-sm leading-none;
}
.btnm {
@apply lg:text-[clamp(16px,1.111vw,24px)] md:text-[clamp(14px,14px+(100vw-768px)/256*2,16px)] text-sm leading-none;
}
@utility btns {
@apply lg:text-[clamp(14px,0.972vw,20px)] md:max-lg:text-[clamp(12px,12px+(100vw-768px)/672*2,14px)] text-xs leading-none;
}
.btns {
@apply lg:text-[clamp(14px,0.972vw,20px)] md:text-[clamp(12px,12px+(100vw-768px)/256*2,14px)] text-xs leading-none;
}
@utility caption {
@apply text-[clamp(14px,14px+(100vw-360px)/1240*2,16px)] leading-none;
}
.caption {
@apply text-[clamp(14px,14px+(100vw-360px)/1240*2,16px)] leading-none;
}
@utility text-gradient {
/* -webkit-background-clip: text;
-webkit-text-fill-color: transparent;
@apply bg-gradient-to-r from-[#798FFF] to-[#D375FF] bg-clip-text; */
background: linear-gradient(87deg, #798fff 15%, #d375ff 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.text-gradient {
background: linear-gradient(87deg, #798fff 15%, #d375ff 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
@utility bg-gradient {
background: linear-gradient(87deg, #798fff 15%, #d375ff 100%);
}
@theme {
--breakpoint-lg: 1440px;
--breakpoint-xs: 360px;
.bg-gradient {
background: linear-gradient(87deg, #798fff 15%, #d375ff 100%);
}
}
body {
@@ -130,7 +126,7 @@ body {
}
}
@layer components {
/* @layer components {
.h1 {
@apply -tracking-[.02em] 2xl:text-[clamp(96px,96px+(100vw-1560px)/360*16,112px)] text-[clamp(40px,40px+(100vw-360px)/1240*56,96px)] leading-[clamp(36px,36px+(100vw-360px)/1240*50.4,86.4px)] font-medium;
}
@@ -147,10 +143,6 @@ body {
@apply 2xl:text-[clamp(20px,20px+(100vw-1560px)/360*8,28px)] text-[clamp(16px,16px+(100vw-360px)/1240*4,20px)] leading-[clamp(17.6px,17.6px+(100vw-360px)/1240*6.4,24px)];
}
/* .accent {
@apply -tracking-[.02em] md:text-[clamp(28px,28px+(100vw-768px)/832*4,32px)] text-[clamp(20px,20px+(100vw-360px)/408*8,28px)] md:leading-[clamp(28px,28px+(100vw-768px)/832*7.2,35.2px)] leading-[clamp(20px,20px+(100vw-360px)/408*8,28px)];
} */
.l-text {
@apply 2xl:text-[clamp(20px,20px+(100vw-1560px)/360*4,24px)] text-[clamp(16px,16px+(100vw-360px)/1240*4,20px)] leading-[clamp(21.6px,21.6px+(100vw-360px)/1240*5.4,27px)];
}
@@ -225,7 +217,7 @@ body {
.card-gradient-5 {
@apply bg-[radial-gradient(circle_closest-side_at_center,#5545AC,transparent)] bg-[length:0px_0px] hover:bg-[length:100%_100%] bg-center bg-no-repeat transition-all duration-300 delay-100;
}
/* */
.line1 {
@apply min-[1440px]:text-[clamp(96px,96px+(100vw-1440px)/480*32,128px)] md:text-[clamp(92px,92px+(100vw-768px)/672*8,100px)] sm:text-[clamp(40px,40px+(100vw-360px)/408*16,56px)] text-[40px] leading-[85%];
}
@@ -274,7 +266,7 @@ body {
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
}
} */
@keyframes custom-ping {
0% {
+85
View File
@@ -0,0 +1,85 @@
import { api } from "@/api";
import { IArticle } from "@/types/IArticle";
import { MetadataRoute } from "next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = "https://graff.estate";
// Статические страницы
const staticPages: MetadataRoute.Sitemap = [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 1,
},
{
url: `${baseUrl}/projects`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${baseUrl}/prime`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${baseUrl}/blog`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.8,
},
{
url: `${baseUrl}/about`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.7,
},
{
url: `${baseUrl}/web`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.7,
},
{
url: `${baseUrl}/stream`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.7,
},
{
url: `${baseUrl}/picture`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.7,
},
{
url: `${baseUrl}/walk`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.7,
},
];
try {
// Получаем все статьи блога
const articles = await api.get("articles").json<IArticle[]>();
const blogPages: MetadataRoute.Sitemap = articles
.filter(({ slug, drafted }) => slug && !drafted)
.map(({ slug, createdAt }) => ({
url: `${baseUrl}/blog/${slug}`,
lastModified: createdAt ? new Date(createdAt) : new Date(),
changeFrequency: "monthly",
priority: 0.8,
}));
return [...staticPages.slice(0, 4), ...blogPages, ...staticPages.slice(4)];
} catch (error) {
console.error("Error generating sitemap:", error);
// Возвращаем хотя бы статические страницы, если API недоступен
return staticPages;
}
}
+22 -21
View File
@@ -1,23 +1,24 @@
import { api } from '@/api';
import { Button } from '@/ui/Button';
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
import Dropzone from 'react-dropzone';
/* eslint-disable @next/next/no-img-element */
import { api } from "@/api";
import { Button } from "@/ui/Button";
import { ChangeEvent, useCallback, useEffect, useState } from "react";
import Dropzone from "react-dropzone";
import {
FieldValues,
Path,
PathValue,
useFormContext,
useWatch,
} from 'react-hook-form';
import AddIcon from '../../public/icons/add.svg';
import RestartIcon from '../../public/icons/restart.svg';
import TrashIcon from '../../public/icons/trash.svg';
} from "react-hook-form";
import AddIcon from "../../public/icons/add.svg";
import RestartIcon from "../../public/icons/restart.svg";
import TrashIcon from "../../public/icons/trash.svg";
export function ImageUploader<T extends FieldValues>({
dest,
name,
label = 'Выберите или перетащите изображение',
className = '',
label = "Выберите или перетащите изображение",
className = "",
required = false,
}: {
dest: string;
@@ -27,7 +28,7 @@ export function ImageUploader<T extends FieldValues>({
className?: string;
}) {
const [file, setFile] = useState<File>();
const [previewFile, setPreviewFile] = useState('');
const [previewFile, setPreviewFile] = useState("");
const { register, setValue, control } = useFormContext();
@@ -44,12 +45,12 @@ export function ImageUploader<T extends FieldValues>({
if (!file) return;
const formData = new FormData();
formData.append('dest', dest);
formData.append('files', file);
formData.append("dest", dest);
formData.append("files", file);
try {
const filePaths = await api
.post('upload', { body: formData })
.post("upload", { body: formData })
.json<PathValue<T, Path<T>>[]>();
setValue(name, filePaths[0]!);
} catch (error) {
@@ -71,19 +72,19 @@ export function ImageUploader<T extends FieldValues>({
setFile(file);
setPreviewFile(URL.createObjectURL(file));
}}
accept={{ 'image/*': ['*'] }}
accept={{ "image/*": ["*"] }}
noClick
>
{({ getRootProps, getInputProps, inputRef }) => (
<div
{...getRootProps()}
className={`relative border border-[#37393B] px-3 py-2 rounded-lg flex flex-col justify-center items-center gap-2${
className ? ' ' + className : ''
className ? " " + className : ""
}`}
>
<input
type="file"
accept={'image/*'}
accept={"image/*"}
required={required}
{...{ ...register(name, { required }), ref: inputRef }}
onChange={handleChangeFile}
@@ -93,7 +94,7 @@ export function ImageUploader<T extends FieldValues>({
<>
<div
className={`relative${
name === 'image' ? ' max-w-[calc(292/824*100%)]' : ''
name === "image" ? " max-w-[calc(292/824*100%)]" : ""
}`}
>
<img
@@ -101,9 +102,9 @@ export function ImageUploader<T extends FieldValues>({
previewFile ||
process.env.NEXT_PUBLIC_S3_BUCKET + currentImg
}
alt={''}
alt={""}
className={`pointer-events-none aspect-square object-${
name === 'image' ? 'cover rounded-2xl' : 'contain'
name === "image" ? "cover rounded-2xl" : "contain"
} !relative`}
sizes="100%"
/>
@@ -125,7 +126,7 @@ export function ImageUploader<T extends FieldValues>({
e.preventDefault();
setFile(undefined!);
setValue(name, undefined!);
setPreviewFile('');
setPreviewFile("");
}}
className="bg-[#37393B99] px-3 py-2 rounded-xl cursor-pointer"
>
+44 -35
View File
@@ -1,25 +1,34 @@
'use client';
"use client";
import { useCheckAuthQuery } from '@/queries/checkAuth';
import { useModalStore } from '@/stores/useModalStore';
import { IArticle } from '@/types/IArticle';
import { ICompany } from '@/types/ICompany';
import { IProject } from '@/types/IProject';
import { IStory } from '@/types/IStory';
import { isArticle } from '@/utils/isArticle';
import { isCompany } from '@/utils/isCompany';
import { isProject } from '@/utils/isProject';
import { isStory } from '@/utils/isStory';
import { SyntheticEvent } from 'react';
import EditIcon from '../../public/icons/edit.svg';
import TrashIcon from '../../public/icons/trash.svg';
import { DeleteItemModal } from './DeleteItemModal';
import { ArticleContentFormModal } from './modals/ArticleContentFormModal';
import { CompanyFormModal } from './modals/CompanyFormModal';
import { MapPointProjectFormModal } from './modals/MapPointFormModal';
import { ProjectFormModal } from './modals/ProjectFormModal';
import { StoryFormModal } from './modals/StoryFormModal';
import { IMapProject } from '@/types/IMapProject';
import { useCheckAuthQuery } from "@/queries/checkAuth";
import { useModalStore } from "@/stores/useModalStore";
import { IArticle } from "@/types/IArticle";
import { ICompany } from "@/types/ICompany";
import { IProject } from "@/types/IProject";
import { IStory } from "@/types/IStory";
import { isArticle } from "@/utils/isArticle";
import { isCompany } from "@/utils/isCompany";
import { isProject } from "@/utils/isProject";
import { isStory } from "@/utils/isStory";
import { SyntheticEvent } from "react";
import EditIcon from "../../public/icons/edit.svg";
import TrashIcon from "../../public/icons/trash.svg";
import { DeleteItemModal } from "./DeleteItemModal";
// import { ArticleContentFormModal } from './modals/ArticleContentFormModal';
import { CompanyFormModal } from "./modals/CompanyFormModal";
import { MapPointProjectFormModal } from "./modals/MapPointFormModal";
import { ProjectFormModal } from "./modals/ProjectFormModal";
import { StoryFormModal } from "./modals/StoryFormModal";
import { IMapProject } from "@/types/IMapProject";
import dynamic from "next/dynamic";
const ArticleContentFormModal = dynamic(
() =>
import("./modals/ArticleContentFormModal").then(
(mod) => mod.ArticleContentFormModal
),
{ ssr: false }
);
export function ItemActions({
item,
@@ -42,10 +51,10 @@ export function ItemActions({
setModal(<CompanyFormModal action="edit" defaultValues={company} />);
} else if (isArticle(item)) setModal(<ArticleContentFormModal {...item} />);
else if (isStory(item))
setModal(<StoryFormModal action={'edit'} defaultValues={item} />);
setModal(<StoryFormModal action={"edit"} defaultValues={item} />);
else
setModal(
<MapPointProjectFormModal action={'edit'} defaultValues={item} />
<MapPointProjectFormModal action={"edit"} defaultValues={item} />
);
}
@@ -55,34 +64,34 @@ export function ItemActions({
<DeleteItemModal
id={item.id}
title={
'Удаление ' +
"Удаление " +
(isProject(item)
? 'проекта'
? "проекта"
: isCompany(item)
? 'компании'
? "компании"
: isArticle(item)
? 'статьи'
? "статьи"
: isStory(item)
? 'истории'
: 'проекта на карте')
? "истории"
: "проекта на карте")
}
entity={
isProject(item)
? 'projects'
? "projects"
: isCompany(item)
? 'companies'
? "companies"
: isArticle(item)
? 'articles'
? "articles"
: isStory(item)
? 'stories'
: 'map'
? "stories"
: "map"
}
/>
);
}
return (
<div className="group-hover:opacity-100 absolute top-0 left-0 z-11 flex gap-1 p-4 transition-opacity opacity-0">
<div className="group-hover:opacity-100 absolute top-0 left-0 z-[11] flex gap-1 p-4 transition-opacity opacity-0">
<button
onClick={handleEdit}
className="relative lg:px-[0.833vw] lg:py-[0.556vw] px-3 py-2 bg-[#37393B99] backdrop-blur-sm rounded-full outline-none cursor-pointer"
+38 -32
View File
@@ -1,19 +1,19 @@
'use client';
"use client";
import { api } from '@/api';
import { projectsTags } from '@/consts/projectsTags';
import { Product } from '@/types/Product';
import { Button } from '@/ui/Button';
import { CheckboxesGroup } from '@/ui/CheckboxesGroup';
import { getExampleNumber } from 'libphonenumber-js';
import examples from 'libphonenumber-js/mobile/examples';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import ReactInputMask from 'react-input-mask';
import { Country } from 'react-phone-number-input';
import CheckIcon from '../../../public/icons/check.svg';
import { api } from "@/api";
import { projectsTags } from "@/consts/projectsTags";
import { Product } from "@/types/Product";
import { Button } from "@/ui/Button";
import { CheckboxesGroup } from "@/ui/CheckboxesGroup";
import { getExampleNumber } from "libphonenumber-js";
import examples from "libphonenumber-js/mobile/examples";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ChangeEvent, useMemo, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import ReactInputMask from "react-input-mask";
import { Country } from "react-phone-number-input";
import CheckIcon from "../../../public/icons/check.svg";
export function Feedback() {
const pathname = usePathname();
@@ -22,8 +22,8 @@ export function Feedback() {
<div
id="contacts"
className={`lg:mb-20 md:mb-12 lg:flex lg:gap-[0.833vw] max-lg:space-y-12 justify-between lg:mt-[14.07vh]${
!pathname.startsWith('/form') ? ' mt-25' : ''
} mb-10${pathname.startsWith('/prime') ? ' max-lg:hidden' : ''}`}
!pathname.startsWith("/form") ? " mt-[100px]" : ""
} mb-10`}
>
<h2 className="line2 font-medium max-lg:mb-6 lg:max-w-[45%]">
<span className="text-[#7A7A7A]">Хотите увеличить конверсию?</span>
@@ -45,15 +45,15 @@ interface IInput {
export function FeedbackForm() {
const [[phoneCode, country], setPhoneCodeAndCountry] = useState<
[string, Country]
>(['+7', 'RU']);
>(["+7", "RU"]);
const placeholder = useMemo(
() =>
getExampleNumber(country, examples)
?.formatInternational()
.split(' ')
.split(" ")
.slice(1)
.join(' '),
.join(" "),
[country]
);
@@ -64,7 +64,7 @@ export function FeedbackForm() {
const { register, handleSubmit, formState } = form;
async function onSubmit(data: IInput) {
await api.post('mail', { json: data }).json();
await api.post("mail", { json: data }).json();
}
return (
@@ -86,7 +86,7 @@ export function FeedbackForm() {
type="text"
required
placeholder="Имя*"
{...register('fullname')}
{...register("fullname")}
className="bg-transparent border-b border-[#37393B] focus:border-white py-4 rounded-none outline-none transition-all w-full placeholder:btnl btnl placeholder:font-medium placeholder:select-none"
/>
<input
@@ -95,7 +95,7 @@ export function FeedbackForm() {
id="email"
type="email"
placeholder="Email*"
{...register('email')}
{...register("email")}
className="bg-transparent border-b border-[#37393B] focus:border-white py-4 rounded-none btnl outline-none transition-all w-full placeholder:btnl placeholder:font-medium placeholder:select-none"
/>
<div className="flex gap-x-3 py-2 border-[#3D425C] relative">
@@ -103,15 +103,21 @@ export function FeedbackForm() {
type="tel"
autoComplete="none"
onChange={(e) => {
form.setValue(
'phone',
phoneCode + e.target.value.replaceAll(/ /g, '')
);
console.log(e.nativeEvent.type);
if (e.nativeEvent.type.startsWith("input")) {
form.setValue(
"phone",
((e.nativeEvent as InputEvent)?.inputType !==
"insertFromPaste"
? phoneCode
: "") + e.target.value.replaceAll(/ /g, "")
);
}
}}
id={'tel'}
id={"tel"}
maskChar={null}
mask={'+7 ' + (placeholder?.replace(/\d/g, '9') ?? '')}
placeholder={'+7 ' + placeholder}
mask={"+7 " + (placeholder?.replace(/\d/g, "9") ?? "")}
placeholder={"+7 " + placeholder}
className="placeholder:btnl placeholder:font-medium placeholder:select-none peer btnl w-full h-full transition-all bg-transparent rounded-none outline-none"
/>
<div className="bottom-0 absolute w-full border-b border-[#37393B] peer-focus:border-white -mb-2" />
@@ -124,13 +130,13 @@ export function FeedbackForm() {
Оставить заявку
</Button>
<Link
href={'/policy'}
href={"/policy"}
className="text2 xl:max-w-[60%] md:max-lg:max-w-[40%] md:max-lg:py-1"
>
<p>
<span className="text-[#7A7A7A]">
*Нажимая кнопку отправить, вы принимаете
</span>{' '}
</span>{" "}
условия использования и&nbsp;политику конфиденциальности
</p>
</Link>
+1 -1
View File
@@ -37,7 +37,7 @@ export function Footer() {
<ContactLink href="https://rutube.ru/channel/25505040">
<RutubeIcon className="lg:w-[1.389vw] lg:h-[1.389vw] md:max-lg:w-[2.083vw] md:max-lg:h-[2.083vw] w-[5.556vw] h-[5.556vw] text-white group-hover:text-black" />
</ContactLink>
<ContactLink href="https://vk.com/graff.interactive">
<ContactLink href="https://vk.com/graffinteractive?from=groups">
<VkIcon className="lg:w-[1.389vw] lg:h-[1.389vw] md:max-lg:w-[2.083vw] md:max-lg:h-[2.083vw] w-[5.556vw] h-[5.556vw] text-white group-hover:text-black" />
</ContactLink>
<ContactLink href="https://www.youtube.com/@GRAFFtech">
+47 -51
View File
@@ -1,29 +1,25 @@
/* eslint-disable @next/next/no-img-element */
'use client';
"use client";
import { api } from '@/api';
import { useMediaQueries } from '@/hooks/useMediaQueries';
import { useScroll } from '@/hooks/useScroll';
import { useCheckAuthQuery } from '@/queries/checkAuth';
import { HeaderLink } from '@/ui/HeaderLink';
import {
QueryClient,
useMutation,
useQueryClient,
} from '@tanstack/react-query';
import { AnimatePresence, motion } from 'framer-motion';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useRef, useState } from 'react';
import { useOnClickOutside } from 'usehooks-ts';
import BurgerIcon from '../../../public/icons/burger.svg';
import CloseIcon from '../../../public/icons/close.svg';
import GraffIcon from '../../../public/icons/graff.svg';
import LogoIcon from '../../../public/icons/logo_hor.svg';
import SkolkovoIcon from '../../../public/icons/skolkovo.svg';
import { Products } from './Products';
import { useAuthStore } from '@/stores/useAuthStore';
import { getQueryClient } from '@/lib/queryClient';
import { api } from "@/api";
import { useMediaQueries } from "@/hooks/useMediaQueries";
import { useScroll } from "@/hooks/useScroll";
import { useCheckAuthQuery } from "@/queries/checkAuth";
import { HeaderLink } from "@/ui/HeaderLink";
import { useMutation } from "@tanstack/react-query";
import { AnimatePresence, motion } from "framer-motion";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useRef, useState } from "react";
import { useOnClickOutside } from "usehooks-ts";
import BurgerIcon from "../../../public/icons/burger.svg";
import CloseIcon from "../../../public/icons/close.svg";
import GraffIcon from "../../../public/icons/graff.svg";
import LogoIcon from "../../../public/icons/logo_hor.svg";
import SkolkovoIcon from "../../../public/icons/skolkovo.svg";
import { Products } from "./Products";
import { useAuthStore } from "@/stores/useAuthStore";
import { getQueryClient } from "@/lib/queryClient";
export function Header() {
const { setToken } = useAuthStore();
@@ -33,9 +29,9 @@ export function Header() {
const { data: auth } = useCheckAuthQuery();
const { mutate: logout } = useMutation({
mutationFn: () => api.get('auth/logout').json<{ success: boolean }>(),
mutationFn: () => api.get("auth/logout").json<{ success: boolean }>(),
onSuccess() {
queryClient.invalidateQueries({ queryKey: ['checkAuth'] });
queryClient.invalidateQueries({ queryKey: ["checkAuth"] });
setToken(null);
},
});
@@ -67,7 +63,7 @@ export function Header() {
return (
<header className="lg:mt-[1.389vw] relative flex lg:px-[1.389vw]">
<Link
href={'/'}
href={"/"}
ref={logoRef}
className="max-lg:hidden cursor-pointer outline-none"
>
@@ -78,13 +74,13 @@ export function Header() {
<motion.nav
ref={navRef}
animate={{
width: burgerOpened && (isXs || isSm) ? 340 : 'auto',
width: burgerOpened && (isXs || isSm) ? 340 : "auto",
}}
className="fixed self-center max-lg:top-4 top-[1.389vw] lg:p-[0.278vw] p-1 lg:rounded-[1.389vw] rounded-[20px] bg-[#37393B99] [backdrop-filter:blur(40px)] lg:gap-[0.278vw] flex gap-1 z-12 mx-auto left-1/2 -translate-x-1/2"
className="fixed self-center max-lg:top-4 top-[1.389vw] lg:p-[0.278vw] p-1 lg:rounded-[1.389vw] rounded-[20px] bg-[#37393B99] [backdrop-filter:blur(40px)] lg:gap-[0.278vw] flex gap-1 z-[12] mx-auto left-1/2 -translate-x-1/2"
>
{((isLg && scroll < -logoRef.current?.clientHeight!) || !isLg) && (
<Link
href={'/'}
href={"/"}
className="aspect-square lg:p-[1.111vw] p-3 hover:bg-[#232425] group active:bg-white lg:rounded-[1.111vw] rounded-2xl content-center m-auto"
>
<GraffIcon className="text-white group-active:text-black lg:w-[1.111vw] md:max-lg:w-[2.083vw] w-4 lg:h-[1.111vw] md:max-lg:h-[2.083vw] h-4" />
@@ -96,10 +92,10 @@ export function Header() {
>
<button
className={
'lg:px-[1.667vw] lg:py-[1.111vw] px-6 py-4 font-medium btnm lg:rounded-[1.111vw] rounded-2xl active:bg-white cursor-pointer outline-none' +
"lg:px-[1.667vw] lg:py-[1.111vw] px-6 py-4 font-medium btnm lg:rounded-[1.111vw] rounded-2xl active:bg-white cursor-pointer outline-none" +
(productsOpened
? ' bg-white text-black'
: ' active:text-black hover:bg-[#232425]')
? " bg-white text-black"
: " active:text-black hover:bg-[#232425]")
}
onClick={() => setProductsOpened((prev) => !prev)}
>
@@ -108,22 +104,22 @@ export function Header() {
</div>
<HeaderLink
className="max-md:hidden btnm"
href={'/about'}
text={'О нас'}
href={"/about"}
text={"О нас"}
/>
<HeaderLink
className="max-md:hidden btnm"
href={'/blog'}
text={'Блог'}
href={"/blog"}
text={"Блог"}
/>
<HeaderLink
className="max-md:hidden btnm"
href={'/projects'}
text={'Проекты'}
href={"/projects"}
text={"Проекты"}
/>
<div className="md:justify-end flex justify-center flex-1">
<Link
href={'/form'}
href={"/form"}
className="btnm bg-gradient font-medium lg:px-[1.667vw] lg:py-[1.181vw] py-[17px] px-6 text-nowrap lg:rounded-[1.111vw] rounded-2xl flex items-center"
>
Оставить заявку
@@ -162,18 +158,18 @@ export function Header() {
>
<div className="px-2 -mx-4 space-y-1">
<HeaderLink
href={'/about'}
text={'О нас'}
href={"/about"}
text={"О нас"}
className="accent"
/>
<HeaderLink
href={'/blog'}
text={'Блог'}
href={"/blog"}
text={"Блог"}
className="accent"
/>
<HeaderLink
href={'/projects'}
text={'Проекты'}
href={"/projects"}
text={"Проекты"}
className="accent"
/>
</div>
@@ -185,7 +181,7 @@ export function Header() {
<div>
<p className="btnm opacity-60 mb-1">Контакты:</p>
<Link
href={'tel:88007700067'}
href={"tel:88007700067"}
className="accent font-medium outline-none"
>
8 800 770 00 67
@@ -206,7 +202,7 @@ export function Header() {
ref={productsRef}
initial={{ opacity: 0 }}
animate={{
width: isLg ? 'max(68.889vw,360px)' : 'calc(100vw - 32px)',
width: isLg ? "max(68.889vw,360px)" : "calc(100vw - 32px)",
opacity: 100,
}}
exit={{ opacity: 0 }}
@@ -219,13 +215,13 @@ export function Header() {
)}
</AnimatePresence>
</div>
{pathname.startsWith('/projects') ? (
{pathname.startsWith("/projects") ? (
<Link
href={'https://dprofile.ru/graff.estate'}
href={"https://dprofile.ru/graff.estate"}
className="max-xl:hidden rounded-[20px] bg-[#37393B99] backdrop-blur-[20px] hover:bg-[#232425] py-5 pl-[63px] pr-[33px] right-5 fixed z-[2] top-5 overflow-clip bg-[url(/img/components/header/dp.png)] bg-no-repeat bg-right-bottom"
>
<img
src={'/img/components/header/show_case.png'}
src={"/img/components/header/show_case.png"}
width={53.77}
height={104.48}
alt="кейс dprofile"
+6 -6
View File
@@ -1,18 +1,18 @@
'use client';
"use client";
import { useModalStore } from '@/stores/useModalStore';
import { useEffect } from 'react';
import { useModalStore } from "@/stores/useModalStore";
import { useEffect } from "react";
export function ModalContainer() {
const { modal, setModal } = useModalStore();
useEffect(() => {
const listener = (e: KeyboardEvent) => {
if (e.key === 'Escape') setModal(null);
if (e.key === "Escape") setModal(null);
};
document.addEventListener('keydown', listener);
document.addEventListener("keydown", listener);
return () => document.removeEventListener('keydown', listener);
return () => document.removeEventListener("keydown", listener);
}, [setModal]);
return (
+7 -7
View File
@@ -1,20 +1,20 @@
import products from '@/consts/products.json';
import { ProductItem } from '@/ui/ProductItem';
import products from "@/consts/products.json";
import { ProductItem } from "@/ui/ProductItem";
export function Products() {
return (
<div className="grid md:grid-cols-2 grid-cols-3 md:grid-rows-6 grid-rows-2 gap-1 lg:gap-2 rounded-2xl max-h-[calc(100dvh-100px)] lg:w-[68.889vw]a lg:h-[38.889vw] amd:max-lg:w-[95.703vw] md:max-lg:h-[72.917vw] h-full w-full">
<div className="grid md:grid-cols-2 grid-cols-3 md:grid-rows-6 grid-rows-2 gap-1 lg:gap-2 rounded-2xl max-h-[calc(100dvh-100px)] lg:h-[38.889vw] md:h-[72.917vw] h-full w-full">
{products.map((product, index) => (
<ProductItem
href={'/' + product.title.toLowerCase()}
href={"/" + product.title.toLowerCase()}
key={product.id}
{...product}
className={
index < 2
? 'max-md:aspect-[100/114] md:col-start-1 md:row-span-3'
? "max-md:aspect-[100/114] md:col-start-1 md:row-span-3"
: index === 4
? 'col-span-2 md:col-span-1 md:col-start-2 md:row-start-5 md:row-end-7'
: 'max-md:aspect-[100/114] md:col-span-1 md:col-start-2 md:nth-3:row-start-1 md:nth-3:row-end-3 md:nth-4:row-start-3 md:nth-4:row-end-5'
? "col-span-2 md:col-span-1 md:col-start-2 md:row-start-5 md:row-end-7"
: "max-md:aspect-[100/114] md:col-span-1 md:col-start-2 md:[&:nth-child(3)]:row-start-1 md:[&:nth-child(3)]:row-end-3 md:[&:nth-child(4)]:row-start-3 md:[&:nth-child(4)]:row-end-5"
}
/>
))}
@@ -1,13 +1,14 @@
'use client';
"use client";
import { api } from '@/api';
import { useFieldArrayFormContext } from '@/lib/FieldArrayFormProvider';
import { Button } from '@/ui/Button';
import { Dispatch, SetStateAction, useRef } from 'react';
import { FieldArrayWithId, useController } from 'react-hook-form';
import { Editor } from 'tinymce';
import { IArticleInput } from '../modals/ArticleFormModal';
import dynamic from 'next/dynamic';
import { api } from "@/api";
import { useFieldArrayFormContext } from "@/lib/FieldArrayFormProvider";
import { Button } from "@/ui/Button";
import { Dispatch, SetStateAction, useEffect, useRef } from "react";
import { FieldArrayWithId, useController } from "react-hook-form";
import { Editor } from "tinymce";
import { IArticleInput } from "../modals/ArticleFormModal";
import dynamic from "next/dynamic";
import { BundledEditor } from "@/lib/BundledEditor";
export function ArticleContentEditor({
index,
@@ -16,7 +17,7 @@ export function ArticleContentEditor({
}: {
index: number;
setEditing: Dispatch<SetStateAction<boolean>>;
item: FieldArrayWithId<IArticleInput, 'blocks', 'id'> & { content: string };
item: FieldArrayWithId<IArticleInput, "blocks", "id"> & { content: string };
}) {
const ref = useRef<Editor | null>(null);
const videoUploadRef = useRef<HTMLInputElement>(null);
@@ -31,10 +32,10 @@ export function ArticleContentEditor({
defaultValue: item.content,
});
const BundledEditor = dynamic(
() => import('@/lib/BundledEditor').then((mod) => mod.BundledEditor),
{ ssr: false }
);
// const BundledEditor = dynamic(
// () => import("@/lib/BundledEditor").then((mod) => mod.BundledEditor),
// { ssr: false }
// );
return (
<div className="w-full space-y-4">
@@ -47,7 +48,7 @@ export function ArticleContentEditor({
}}
init={{
content_style:
'body {color: #fff; background: #14161f; font-size:16px; height: 100vh}',
"body {color: #fff; background: #14161f; font-size:16px; height: 100vh}",
video_template_callback: (data: { source: string }) =>
'<video src="' +
data.source +
@@ -55,34 +56,34 @@ export function ArticleContentEditor({
images_upload_credentials: true,
images_upload_handler: async (blobInfo) => {
const formData = new FormData();
formData.append('files', blobInfo.blob(), blobInfo.filename());
formData.append('dest', 'blog');
formData.append("files", blobInfo.blob(), blobInfo.filename());
formData.append("dest", "blog");
const res = await api
.post('upload', { body: formData })
.post("upload", { body: formData })
.json<string[]>();
return process.env.NEXT_PUBLIC_S3_BUCKET + res[0];
},
font_size_formats: '10px 12px 14px 16px 18px 20px 24px 28px 30px',
file_picker_types: 'image media',
font_size_formats: "10px 12px 14px 16px 18px 20px 24px 28px 30px",
file_picker_types: "image media",
file_picker_callback: async (cb) => {
videoUploadRef.current!.onchange = async function () {
const reader = new FileReader();
const file = videoUploadRef.current?.files?.[0];
reader.onload = async function () {
const id = 'blobId' + new Date().getTime();
const id = "blobId" + new Date().getTime();
const blobCache = ref.current?.editorUpload.blobCache;
const base64 = reader.result?.toString().split(',')[1];
const base64 = reader.result?.toString().split(",")[1];
const blobInfo = blobCache?.create(id, file!, base64!);
blobCache?.add(blobInfo!);
const formData = new FormData();
formData.append(
'files',
"files",
blobInfo?.blob()!,
blobInfo?.filename()
);
formData.append('dest', 'blog');
formData.append("dest", "blog");
const res = await api
.post('upload', { body: formData })
.post("upload", { body: formData })
.json<{ files: string[] }>();
cb(process.env.NEXT_PUBLIC_S3_BUCKET + res.files[0], {
@@ -95,22 +96,22 @@ export function ArticleContentEditor({
},
automatic_uploads: true,
plugins: [
'anchor',
'autolink',
'charmap',
'codesample',
'emoticons',
'image',
'link',
'lists',
'media',
'nonbreaking',
'preview',
'save',
'searchreplace',
'table',
'visualblocks',
'wordcount',
"anchor",
"autolink",
"charmap",
"codesample",
"emoticons",
"image",
"link",
"lists",
"media",
"nonbreaking",
"preview",
"save",
"searchreplace",
"table",
"visualblocks",
"wordcount",
],
}}
/>
@@ -1,16 +1,22 @@
'use client';
"use client";
import { Reorder } from 'framer-motion';
import parse from 'html-react-parser';
import { useEffect, useRef, useState } from 'react';
import { FieldArrayWithId, useFormContext } from 'react-hook-form';
import { BlockActions } from '../BlockActions';
import { IArticleInput } from '../modals/ArticleFormModal';
import { ArticleContentEditor } from './ArticleContentEditor';
import { Reorder } from "framer-motion";
import parse from "html-react-parser";
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import { FieldArrayWithId, useFormContext } from "react-hook-form";
import { BlockActions } from "../BlockActions";
import { IArticleInput } from "../modals/ArticleFormModal";
import { ArticleContentEditor } from "./ArticleContentEditor";
export interface IArticleContentInputProps {
index: number;
item: FieldArrayWithId<IArticleInput, 'blocks', 'id'> & { content: string };
item: FieldArrayWithId<IArticleInput, "blocks", "id"> & { content: string };
}
export function ArticleContentInput({
@@ -19,7 +25,7 @@ export function ArticleContentInput({
}: IArticleContentInputProps) {
const ref = useRef<HTMLButtonElement>(null);
const [editing, setEditing] = useState(false);
const [editing, setEditing] = useState(true);
const [content, setContent] = useState(item.content);
@@ -27,9 +33,9 @@ export function ArticleContentInput({
useEffect(() => {
const { unsubscribe } = watch(({ blocks }) => {
if (!blocks || !blocks.length || blocks[index]?.type !== 'Content')
if (!blocks || !blocks.length || blocks[index]?.type !== "Content")
return;
setContent(blocks[index].content ?? '');
setContent(blocks[index].content ?? "");
});
return unsubscribe;
}, [index, watch]);
@@ -38,6 +44,7 @@ export function ArticleContentInput({
<Reorder.Item
as="div"
value={item}
key={item.id}
className="lg:col-start-2 lg:col-span-2 sm:col-span-3 col-span-full relative flex items-start gap-4"
onDoubleClick={() => ref.current && ref.current.click()}
>
@@ -49,7 +56,7 @@ export function ArticleContentInput({
/>
) : (
<div className="border-[#3D425C] border p-4 rounded-3xl [&_p_*]:!text1 [&_h1_*]:!heading1 [&_h2_*]:!heading2">
{item.type === 'Content' && parse(content)}
{item.type === "Content" && parse(content)}
</div>
)}
<BlockActions
@@ -1,23 +1,23 @@
'use client';
"use client";
import { useArticleMutation } from '@/hooks/useArticleMutation';
import { FieldArrayFormProvider } from '@/lib/FieldArrayFormProvider';
import { useModalStore } from '@/stores/useModalStore';
import { IArticle } from '@/types/IArticle';
import { Button } from '@/ui/Button';
import { reorderFields } from '@/utils/reorderFields';
import { Reorder } from 'framer-motion';
import { FormProvider, useFieldArray, useForm } from 'react-hook-form';
import AddIcon from '../../../public/icons/add.svg';
import EditIcon from '../../../public/icons/edit.svg';
import { ArticleButtonLinkInput } from '../articleInputs/ArticleButtonLinkInput';
import { ArticleContentInput } from '../articleInputs/ArticleContentInput';
import { ArticleImageInput } from '../articleInputs/ArticleImageInput';
import { ArticleQuoteInput } from '../articleInputs/ArticleQuoteInput';
import { ArticleSliderInput } from '../articleInputs/ArticleSliderInput';
import { ArticleVideoUploader } from '../articleInputs/ArticleVideoUploader';
import { ArticleFormActions } from './ArticleFormActions';
import { ArticleFormModal, IArticleInput } from './ArticleFormModal';
import { useArticleMutation } from "@/hooks/useArticleMutation";
import { FieldArrayFormProvider } from "@/lib/FieldArrayFormProvider";
import { useModalStore } from "@/stores/useModalStore";
import { IArticle } from "@/types/IArticle";
import { Button } from "@/ui/Button";
import { reorderFields } from "@/utils/reorderFields";
import { Reorder } from "framer-motion";
import { FormProvider, useFieldArray, useForm } from "react-hook-form";
import AddIcon from "../../../public/icons/add.svg";
import EditIcon from "../../../public/icons/edit.svg";
import { ArticleButtonLinkInput } from "../articleInputs/ArticleButtonLinkInput";
import { ArticleContentInput } from "../articleInputs/ArticleContentInput";
import { ArticleImageInput } from "../articleInputs/ArticleImageInput";
import { ArticleQuoteInput } from "../articleInputs/ArticleQuoteInput";
import { ArticleSliderInput } from "../articleInputs/ArticleSliderInput";
import { ArticleVideoUploader } from "../articleInputs/ArticleVideoUploader";
import { ArticleFormActions } from "./ArticleFormActions";
import { ArticleFormModal, IArticleInput } from "./ArticleFormModal";
export function ArticleContentFormModal({
posterImage,
@@ -47,17 +47,17 @@ export function ArticleContentFormModal({
const fieldArrayMethods = useFieldArray({
control,
name: 'blocks',
name: "blocks",
});
const { append, fields, swap } = fieldArrayMethods;
const { mutateAsync: save } = useArticleMutation({ action: 'edit', id });
const { mutateAsync: save } = useArticleMutation({ action: "edit", id });
async function handleSave(drafted: boolean) {
await save({
...getValues(),
blocks: JSON.stringify(getValues('blocks')),
blocks: JSON.stringify(getValues("blocks")),
drafted,
});
setModal(null);
@@ -66,17 +66,17 @@ export function ArticleContentFormModal({
return (
<>
<ArticleFormActions disabled={false} handleSave={handleSave} />
<div className="relative space-y-4 bg-[#232425] rounded-[28px] top-5 w-[calc(954/1440*100vw)] h-[calc(100vh-40px)] z-1 overflow-y-auto">
<div className="relative space-y-4 bg-[#232425] rounded-[28px] top-5 w-[calc(954/1440*100vw)] h-[calc(100vh-40px)] z-10 overflow-y-auto">
<div className="top-3 right-4 absolute">
<Button
className="bg-[#37393B99] z-3 backdrop-blur-sm p-4 btnm lg:rounded-[1.111vw] rounded-2xl"
className="bg-[#37393B99] z-[3] backdrop-blur-sm p-4 btnm lg:rounded-[1.111vw] rounded-2xl"
color="secondary"
onClick={() =>
setModal(
<ArticleFormModal
action="edit"
defaultValues={{
blocks: getValues('blocks') ?? [],
blocks: getValues("blocks") ?? [],
tags,
title,
cardImage,
@@ -96,7 +96,7 @@ export function ArticleContentFormModal({
</Button>
</div>
<div
className="bg-no-repeat bg-cover bg-top px-[75px] pb-6 z-2 w-full aspect-[954/261] relative flex items-end justify-between gap-4 before:absolute before:left-0 before:top-0 before:w-full before:h-full before:-z-1 before:aspect-[954/261] before:bg-gradient-to-t before:from-[#00000099]"
className="bg-no-repeat bg-cover bg-top px-[75px] pb-6 z-[2] w-full aspect-[954/261] relative flex items-end justify-between gap-4 before:absolute before:left-0 before:top-0 before:w-full before:h-full before:-z-1 before:aspect-[954/261] before:bg-gradient-to-t before:from-[#00000099]"
style={{
backgroundImage: `url(${
process.env.NEXT_PUBLIC_S3_BUCKET + posterImage
@@ -125,31 +125,31 @@ export function ArticleContentFormModal({
className="py-10 px-[75px] space-y-5"
>
{fields.map((item, index) =>
item.type === 'Content' ? (
item.type === "Content" ? (
<ArticleContentInput
key={item.id}
index={index}
item={item}
/>
) : item.type === 'Quote' ? (
) : item.type === "Quote" ? (
<ArticleQuoteInput
key={item.id}
index={index}
item={item}
/>
) : item.type === 'Slider' ? (
) : item.type === "Slider" ? (
<ArticleSliderInput
key={item.id}
index={index}
item={item}
/>
) : item.type === 'Video' ? (
) : item.type === "Video" ? (
<ArticleVideoUploader
key={item.id}
item={item}
index={index}
/>
) : item.type === 'ButtonLink' ? (
) : item.type === "ButtonLink" ? (
<ArticleButtonLinkInput
key={item.id}
index={index}
@@ -171,7 +171,7 @@ export function ArticleContentFormModal({
<div className="fixed bottom-5 left-[84.028vw] z-10 space-y-2">
<button
className="flex items-center gap-2 lg:px-[0.833vw] lg:py-[0.556vw] px-3 py-2 bg-[#232425] lg:rounded-[0.833vw] rounded-xl cursor-pointer hover:bg-[#24252699] btns active:bg-white active:text-black group"
onClick={() => append({ type: 'Content', content: '' })}
onClick={() => append({ type: "Content", content: "" })}
>
<AddIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white group-active:text-black" />
Абзац
@@ -180,11 +180,11 @@ export function ArticleContentFormModal({
className="flex items-center gap-2 lg:px-[0.833vw] lg:py-[0.556vw] px-3 py-2 bg-[#232425] lg:rounded-[0.833vw] rounded-xl cursor-pointer hover:bg-[#24252699] btns active:bg-white active:text-black group"
onClick={() =>
append({
type: 'Quote',
avatar: '',
name: '',
position: '',
text: '',
type: "Quote",
avatar: "",
name: "",
position: "",
text: "",
})
}
>
@@ -193,28 +193,28 @@ export function ArticleContentFormModal({
</button>
<button
className="flex items-center gap-2 lg:px-[0.833vw] lg:py-[0.556vw] px-3 py-2 bg-[#232425] lg:rounded-[0.833vw] rounded-xl cursor-pointer hover:bg-[#24252699] btns active:bg-white active:text-black group"
onClick={() => append({ type: 'Video', src: '' })}
onClick={() => append({ type: "Video", src: "" })}
>
<AddIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white group-active:text-black" />
Видео
</button>
<button
className="flex items-center gap-2 lg:px-[0.833vw] lg:py-[0.556vw] px-3 py-2 bg-[#232425] lg:rounded-[0.833vw] rounded-xl cursor-pointer hover:bg-[#24252699] btns active:bg-white active:text-black group"
onClick={() => append({ type: 'Image', img: '' })}
onClick={() => append({ type: "Image", img: "" })}
>
<AddIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white group-active:text-black" />
Картинка
</button>
<button
className="flex items-center gap-2 lg:px-[0.833vw] lg:py-[0.556vw] px-3 py-2 bg-[#232425] lg:rounded-[0.833vw] rounded-xl cursor-pointer hover:bg-[#24252699] btns active:bg-white active:text-black group"
onClick={() => append({ type: 'Slider', images: [] })}
onClick={() => append({ type: "Slider", images: [] })}
>
<AddIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white group-active:text-black" />
Слайдер
</button>
<button
className="flex items-center gap-2 lg:px-[0.833vw] lg:py-[0.556vw] px-3 py-2 bg-[#232425] lg:rounded-[0.833vw] rounded-xl cursor-pointer hover:bg-[#24252699] btns active:bg-white active:text-black group"
onClick={() => append({ type: 'ButtonLink', title: '', link: '' })}
onClick={() => append({ type: "ButtonLink", title: "", link: "" })}
>
<AddIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white group-active:text-black" />
Кнопка
+28 -22
View File
@@ -1,27 +1,33 @@
'use client';
"use client";
import { useArticleMutation } from '@/hooks/useArticleMutation';
import { useModalStore } from '@/stores/useModalStore';
import { IArticle } from '@/types/IArticle';
import { CheckboxesGroup } from '@/ui/CheckboxesGroup';
import { TextInput } from '@/ui/TextInput';
import { FormProvider, useForm, useWatch } from 'react-hook-form';
import { ImageUploader } from '../ImageUploader';
import { ArticleContentFormModal } from './ArticleContentFormModal';
import { ArticleFormActions } from './ArticleFormActions';
import { FormModalHeader } from './FormModalHeader';
import ReactLenis, { LenisRef } from 'lenis/react';
import { useEffect, useRef } from 'react';
import { useLenis } from '@/hooks/useLenis';
import { useArticleMutation } from "@/hooks/useArticleMutation";
import { useModalStore } from "@/stores/useModalStore";
import { IArticle } from "@/types/IArticle";
import { CheckboxesGroup } from "@/ui/CheckboxesGroup";
import { TextInput } from "@/ui/TextInput";
import { FormProvider, useForm, useWatch } from "react-hook-form";
import { ImageUploader } from "../ImageUploader";
// import { ArticleContentFormModal } from "./ArticleContentFormModal";
import { ArticleFormActions } from "./ArticleFormActions";
import { FormModalHeader } from "./FormModalHeader";
import dynamic from "next/dynamic";
interface IArticleFormModalProps<TAction extends 'create' | 'edit'> {
const ArticleContentFormModal = dynamic(
() =>
import("./ArticleContentFormModal").then(
(mod) => mod.ArticleContentFormModal
),
{ ssr: false }
);
interface IArticleFormModalProps<TAction extends "create" | "edit"> {
action: TAction;
defaultValues?: TAction extends 'edit' ? IArticle : undefined;
defaultValues?: TAction extends "edit" ? IArticle : undefined;
}
export interface IArticleInput extends Omit<IArticle, 'id'> {}
export interface IArticleInput extends Omit<IArticle, "id"> {}
export function ArticleFormModal<TAction extends 'create' | 'edit'>({
export function ArticleFormModal<TAction extends "create" | "edit">({
action,
defaultValues,
}: IArticleFormModalProps<TAction>) {
@@ -34,7 +40,7 @@ export function ArticleFormModal<TAction extends 'create' | 'edit'>({
tags: [],
drafted: true,
},
mode: 'onChange',
mode: "onChange",
});
async function onSubmit(data: IArticleInput) {
@@ -53,7 +59,7 @@ export function ArticleFormModal<TAction extends 'create' | 'edit'>({
await mutateAsync({
...getValues(),
blocks: JSON.stringify(
defaultValues ? defaultValues.blocks : getValues('blocks') ?? []
defaultValues ? defaultValues.blocks : getValues("blocks") ?? []
),
drafted,
});
@@ -61,7 +67,7 @@ export function ArticleFormModal<TAction extends 'create' | 'edit'>({
}
const { mutateAsync } = useArticleMutation(
action === 'create'
action === "create"
? { action, id: undefined }
: { action, id: defaultValues!.id }
);
@@ -101,7 +107,7 @@ export function ArticleFormModal<TAction extends 'create' | 'edit'>({
</label>
<CheckboxesGroup
name="tags"
options={['Недвижимость', 'Награды', 'Выставки']}
options={["Недвижимость", "Награды", "Выставки"]}
/>
</div>
<ImageUploader
+17 -24
View File
@@ -1,16 +1,14 @@
'use client';
"use client";
import { api } from '@/api';
import { useModalStore } from '@/stores/useModalStore';
import { ICompany } from '@/types/ICompany';
import { TextInput } from '@/ui/TextInput';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
import CloseIcon from '../../../public/icons/close.svg';
import { ImageUploader } from '../ImageUploader';
import { FormModalHeader } from './FormModalHeader';
import ReactLenis from 'lenis/react';
import { useLenis } from '@/hooks/useLenis';
import { api } from "@/api";
import { useModalStore } from "@/stores/useModalStore";
import { ICompany } from "@/types/ICompany";
import { TextInput } from "@/ui/TextInput";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import CloseIcon from "../../../public/icons/close.svg";
import { ImageUploader } from "../ImageUploader";
import { FormModalHeader } from "./FormModalHeader";
interface ICompanyFormInput {
title: string;
@@ -19,14 +17,14 @@ interface ICompanyFormInput {
logo?: string;
}
interface ICompanyFormModalProps<TAction extends 'create' | 'edit'> {
interface ICompanyFormModalProps<TAction extends "create" | "edit"> {
action: TAction;
defaultValues?: TAction extends 'edit'
defaultValues?: TAction extends "edit"
? ICompanyFormInput & { id: string }
: undefined;
}
export function CompanyFormModal<TAction extends 'create' | 'edit'>({
export function CompanyFormModal<TAction extends "create" | "edit">({
action,
defaultValues,
}: ICompanyFormModalProps<TAction>) {
@@ -40,17 +38,15 @@ export function CompanyFormModal<TAction extends 'create' | 'edit'>({
const { mutateAsync } = useMutation<ICompany, Error, ICompanyFormInput>({
mutationFn: async (json) =>
action === 'create'
? await api.post('companies', { json }).json()
action === "create"
? await api.post("companies", { json }).json()
: await api.put(`companies/${defaultValues?.id}`, { json }).json(),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['companies'] });
await queryClient.invalidateQueries({ queryKey: ["companies"] });
setModal(null);
},
});
// const lenis = useLenis();
return (
<>
<button
@@ -59,10 +55,7 @@ export function CompanyFormModal<TAction extends 'create' | 'edit'>({
>
<CloseIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white" />
</button>
<div
// ref={lenis}
className="relative space-y-10 py-10 bg-[#232425] rounded-[28px] top-5 w-[calc(954/1440*100vw)] max-h-[calc(100vh-40px)] pl-[75px] pr-[55px] overflow-y-auto z-[1]"
>
<div className="relative space-y-10 py-10 bg-[#232425] rounded-[28px] top-5 w-[calc(954/1440*100vw)] max-h-[calc(100vh-40px)] pl-[75px] pr-[55px] overflow-y-auto z-[1]">
<FormModalHeader
disabled={false}
submitHandler={form.handleSubmit(
+14 -14
View File
@@ -1,12 +1,12 @@
'use client';
"use client";
/* eslint-disable react-hooks/exhaustive-deps */
import CloseIcon from '../../../public/icons/close.svg';
import TelegramIcon from '../../../public/icons/tg.svg';
import { useEffect } from 'react';
import { motion } from 'framer-motion';
import Link from 'next/link';
import { usePopupStore } from '@/stores/usePopupStore';
import CloseIcon from "../../../public/icons/close.svg";
import TelegramIcon from "../../../public/icons/tg.svg";
import { useEffect } from "react";
import { motion } from "framer-motion";
import Link from "next/link";
import { usePopupStore } from "@/stores/usePopupStore";
function Popup() {
const { isShowPopup, setIsShowPopup } = usePopupStore();
@@ -29,25 +29,25 @@ function Popup() {
{isShowPopup && (
<motion.div
animate={{ y: -40 }}
transition={{ type: 'spring' }}
className="fixed bottom-0 sm:left-10 left-1/2 max-md:-translate-x-1/2 z-30 w-[320px] lg:w-[22.222vw] p-px lg:p-[0.069vw] rounded-2xl bg-gradient"
transition={{ type: "spring" }}
className="fixed bottom-0 md:left-10 left-[calc((100vw-320px)/2)] z-[30] w-[320px] lg:min-w-80 lg:w-[16vw] p-px lg:p-[0.069vw] rounded-2xl bg-gradient"
>
<div className="bg-[#14161F] rounded-2xl p-4 flex flex-col gap-y-5 lg:gap-y-10">
<p className="btns font-medium leading-[15.4px]">
Новости разработки интерактивных решений для девелоперов в нашем
<p className="btns font-medium">
Новости разработки интерактивных решений для девелоперов в нашем
телеграм канале
</p>
<Link
href={'https://t.me/graffestate'}
href={"https://t.me/graffestate"}
target="_blank"
onClick={handlePopupClick}
className="flex items-center gap-x-1 w-full justify-center font-semibold btns bg-gradient rounded-full py-2 hover:opacity-80 transition-opacity"
className="gap-x-1 btns bg-gradient hover:opacity-80 flex items-center justify-center w-full py-2 font-semibold transition-opacity rounded-full"
>
<TelegramIcon className="lg:w-[1.667vw] lg:h-[1.667vw] w-4 h-4" />
Перейти
</Link>
<button
className="absolute top-2 right-2 hover:bg-white rounded-full flex p-px hover:bg-opacity-10 group transition-colors"
className="top-2 right-2 hover:bg-white hover:bg-opacity-10 group absolute flex p-px transition-colors rounded-full cursor-pointer"
onClick={handlePopupClick}
>
<CloseIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 m-auto group-hover:text-black transition-colors" />
+20 -20
View File
@@ -1,12 +1,12 @@
'use client';
"use client";
import { useMediaQueries } from '@/hooks/useMediaQueries';
import { useGetStories } from '@/queries/getStories';
import { useModalStore } from '@/stores/useModalStore';
import { createRef, RefObject, useEffect, useRef, useState } from 'react';
import { useSwipeable } from 'react-swipeable';
import CloseIcon from '../../../public/icons/close.svg';
import { ItemActions } from '../ItemActions';
import { useMediaQueries } from "@/hooks/useMediaQueries";
import { useGetStories } from "@/queries/getStories";
import { useModalStore } from "@/stores/useModalStore";
import { createRef, RefObject, useEffect, useRef, useState } from "react";
import { useSwipeable } from "react-swipeable";
import CloseIcon from "../../../public/icons/close.svg";
import { ItemActions } from "../ItemActions";
export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
const { setModal } = useModalStore();
@@ -39,9 +39,9 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
const handleTimeUpdate = () =>
setCurrentProgress(video.currentTime / video.duration);
video.addEventListener('timeupdate', handleTimeUpdate);
video.addEventListener("timeupdate", handleTimeUpdate);
return () => video.removeEventListener('timeupdate', handleTimeUpdate);
return () => video.removeEventListener("timeupdate", handleTimeUpdate);
}, [currentIndex, videoRefs]);
useEffect(() => {
@@ -90,7 +90,7 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
</button>
<div
{...handlers}
className="overflow-hidden z-1 md:m-auto lg:w-[83.958vw] lg:h-[76.433vh] md:max-lg:w-[157.422vw] md:max-lg:h-[67.669vh] max-md:w-screen h-dvh max-md:top-0"
className="overflow-hidden z-[1] md:m-auto lg:w-[83.958vw] lg:h-[76.433vh] md:max-lg:w-[157.422vw] md:max-lg:h-[67.669vh] max-md:w-screen h-dvh max-md:top-0"
>
<div
ref={ref}
@@ -110,8 +110,8 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
key={id}
className={`select-none relative flex items-end group overflow-hidden lg:rounded-[0.833vw] md:max-lg:rounded-xl cursor-pointer transition-transform lg:p-[1.111vw] p-4 ${
index === currentIndex
? 'lg:min-w-[28.125vw] lg:h-[76.433vh] md:max-lg:min-w-[52.734vw] md:max-lg:h-[67.669vh] max-md:min-w-screen max-md:h-dvh'
: 'lg:min-w-[26.25vw] lg:h-[71.338vh] md:max-lg:min-w-[49.219vw] md:max-lg:h-[63.158vh] max-md:min-w-screen max-md:h-dvh'
? "lg:min-w-[28.125vw] lg:h-[76.433vh] md:max-lg:min-w-[52.734vw] md:max-lg:h-[67.669vh] max-md:min-w-screen max-md:h-dvh"
: "lg:min-w-[26.25vw] lg:h-[71.338vh] md:max-lg:min-w-[49.219vw] md:max-lg:h-[63.158vh] max-md:min-w-screen max-md:h-dvh"
}`}
onClick={() => setCurrentIndex(index)}
>
@@ -125,17 +125,17 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
<div
className={`absolute bottom-0 left-1/2 -translate-x-1/2 w-full h-full lg:rounded-[0.833vw] md:max-lg:rounded-xl transition-colors ${
index === currentIndex
? 'lg:bg-[radial-gradient(37.292vw_23.036vh_at_bottom,#6078F2,#C868F5,transparent)] md:max-lg:bg-[radial-gradient(69.922vw_20.395vh_at_bottom,#6078F2,#C868F5,transparent)] bg-[radial-gradient(149.167vw_33.906vh_at_bottom,#6078F2,#C868F5,transparent)]'
: 'md:bg-[#0F101199]'
? "lg:bg-[radial-gradient(37.292vw_23.036vh_at_bottom,#6078F2,#C868F5,transparent)] md:max-lg:bg-[radial-gradient(69.922vw_20.395vh_at_bottom,#6078F2,#C868F5,transparent)] bg-[radial-gradient(149.167vw_33.906vh_at_bottom,#6078F2,#C868F5,transparent)]"
: "md:bg-[#0F101199]"
}`}
/>
{currentIndex === index && (
<div className="space-y-5 z-1 max-md:hidden">
<div className="z-1 max-md:hidden space-y-5">
<p className="heading1 font-medium">{text}</p>
<div className="bg-white/30 w-full h-1 rounded-[34px]">
<div
className="h-1 bg-white transition-[width] rounded-[34px]"
style={{ width: currentProgress * 100 + '%' }}
style={{ width: currentProgress * 100 + "%" }}
/>
</div>
</div>
@@ -145,7 +145,7 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
))}
</div>
{stories && stories.length > 0 && (
<div className="md:hidden absolute space-y-6 left-2.5 right-2.5 bottom-4 z-1 w-full">
<div className="md:hidden absolute space-y-6 left-2.5 right-2.5 bottom-4 z-[1] w-full">
<p className="heading1 font-medium">
{stories[currentIndex]?.text}
</p>
@@ -156,8 +156,8 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
className="bg-white rounded-[34px] h-1 transition-all"
style={
currentIndex === index
? { width: currentProgress * 100 + '%' }
: { width: currentIndex > index ? '100%' : '0%' }
? { width: currentProgress * 100 + "%" }
: { width: currentIndex > index ? "100%" : "0%" }
}
/>
</div>
@@ -1,20 +1,29 @@
/* eslint-disable @next/next/no-img-element */
'use client';
"use client";
import { ArticleContentFormModal } from '@/components/modals/ArticleContentFormModal';
import { useCheckAuthQuery } from '@/queries/checkAuth';
import { useGetArticleById } from '@/queries/getArticleById';
import { useModalStore } from '@/stores/useModalStore';
import { ArticleNewSlider } from '@/ui/ArticleNewSlider';
import { ArticleVideoPlayer } from '@/ui/ArticleVideoPlayer';
import { Button } from '@/ui/Button';
import parse from 'html-react-parser';
import Link from 'next/link';
import { Fragment } from 'react';
import ArrowMoreIcon from '../../../../public/icons/arrow_more.svg';
import EditIcon from '../../../../public/icons/edit.svg';
import { ReactLenis } from 'lenis/react';
import { useLenis } from '@/hooks/useLenis';
// import { ArticleContentFormModal } from "@/components/modals/ArticleContentFormModal";
import { useCheckAuthQuery } from "@/queries/checkAuth";
import { useGetArticleById } from "@/queries/getArticleById";
import { useModalStore } from "@/stores/useModalStore";
import { ArticleNewSlider } from "@/ui/ArticleNewSlider";
import { ArticleVideoPlayer } from "@/ui/ArticleVideoPlayer";
import { Button } from "@/ui/Button";
import parse from "html-react-parser";
import Link from "next/link";
import { Fragment, useEffect } from "react";
import ArrowMoreIcon from "../../../../public/icons/arrow_more.svg";
import EditIcon from "../../../../public/icons/edit.svg";
import { ReactLenis } from "lenis/react";
import { useLenis } from "@/hooks/useLenis";
import dynamic from "next/dynamic";
const ArticleContentFormModal = dynamic(
() =>
import("@/components/modals/ArticleContentFormModal").then(
(mod) => mod.ArticleContentFormModal
),
{ ssr: false }
);
export function ArticleSyncPage({ slug }: { slug: string }) {
const { data: article } = useGetArticleById(slug);
@@ -28,16 +37,16 @@ export function ArticleSyncPage({ slug }: { slug: string }) {
if (!article) return null;
return (
<div className="absolute -translate-x-1/2 left-1/2 lg:h-[calc(100vh-40px)] lg:w-[66.25vw] w-screen lg:top-5 lg:rounded-[1.944vw] rounded-[28px] bg-[#232425] lg:m-auto overflow-y-hidden max-h-dvh h-full outline-none">
<ReactLenis
ref={lenis}
className="relative h-full py-1 overflow-y-scroll overflow-x-hidden"
>
<ReactLenis
ref={lenis}
className="absolute -translate-x-1/2 left-1/2 lg:h-[calc(100vh-40px)] lg:w-[66.25vw] w-screen lg:top-5 lg:rounded-[1.944vw] rounded-[28px] bg-[#232425] lg:m-auto overflow-y-hidden max-h-dvh h-full outline-none"
>
<div className="relative h-full py-1 overflow-x-hidden overflow-y-auto">
<div className="relative w-full lg:h-[18.125vw] md:max-lg:h-[261px] h-[209px]">
{auth && (
<div className="top-3 left-4 absolute">
<Button
className="bg-[#37393B99] z-3 backdrop-blur-sm p-4 btnm"
className="bg-[#37393B99] z-[3] backdrop-blur-sm p-4 btnm"
color="secondary"
rounded="2xl"
onClick={() =>
@@ -77,11 +86,11 @@ export function ArticleSyncPage({ slug }: { slug: string }) {
<div className="lg:py-[2.778vw] lg:px-[5.208vw] py-10 md:max-lg:px-4 px-2.5 w-full md:space-y-10 space-y-8">
{article.blocks.map((block, index) => (
<Fragment key={index}>
{block.type === 'Content' ? (
{block.type === "Content" ? (
<div className="lg:max-w-2/3 [&_p_*]:!text1 [&_h1_*]:!heading1 [&_h2_*]:!heading2">
{parse(block.content)}
</div>
) : block.type === 'ButtonLink' ? (
) : block.type === "ButtonLink" ? (
<Link
href={block.link}
className="rounded-2xl bg-gradient btnm w-fit flex items-center gap-3 px-6 py-4"
@@ -89,7 +98,7 @@ export function ArticleSyncPage({ slug }: { slug: string }) {
{block.title}
<ArrowMoreIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white" />
</Link>
) : block.type === 'Quote' ? (
) : block.type === "Quote" ? (
<div className="lg:p-[3.333vw] md:max-lg:p-12 p-4 space-y-6 rounded-2xl bg-[radial-gradient(ellipse_at_bottom,#7A7A7A50,transparent)]">
<div className="flex gap-4">
<div className="aspect-square relative">
@@ -107,9 +116,9 @@ export function ArticleSyncPage({ slug }: { slug: string }) {
</div>
<p className="accent font-medium">{block.text}</p>
</div>
) : block.type === 'Slider' ? (
) : block.type === "Slider" ? (
<ArticleNewSlider images={block.images.map(({ img }) => img)} />
) : block.type === 'Video' ? (
) : block.type === "Video" ? (
<ArticleVideoPlayer src={block.src} />
) : (
<div className="relative">
@@ -117,14 +126,14 @@ export function ArticleSyncPage({ slug }: { slug: string }) {
className="!relative lg:rounded-[1.111vw] rounded-2xl w-full"
src={process.env.NEXT_PUBLIC_S3_BUCKET + block.img}
sizes="100%"
alt={''}
alt={""}
/>
</div>
)}
</Fragment>
))}
</div>
</ReactLenis>
</div>
</div>
</ReactLenis>
);
}
+17 -17
View File
@@ -1,18 +1,18 @@
'use client';
"use client";
import { useCheckAuthQuery } from '@/queries/checkAuth';
import { useGetArticlesQuery } from '@/queries/getArticles';
import { useShowDrafted } from '@/stores/useShowDrafted';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import TelegramIcon from '../../../../public/icons/tg.svg';
import { ArticleCard } from './ArticleCard';
import { DraftsList } from './DraftsList';
import { useCheckAuthQuery } from "@/queries/checkAuth";
import { useGetArticlesQuery } from "@/queries/getArticles";
import { useShowDrafted } from "@/stores/useShowDrafted";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import TelegramIcon from "../../../../public/icons/tg.svg";
import { ArticleCard } from "./ArticleCard";
import { DraftsList } from "./DraftsList";
export function ArticlesList() {
const searchParams = useSearchParams();
const { data: articles } = useGetArticlesQuery(searchParams.getAll('tags'));
const { data: articles } = useGetArticlesQuery(searchParams.getAll("tags"));
const { data: auth } = useCheckAuthQuery();
@@ -20,8 +20,8 @@ export function ArticlesList() {
return (
<div className="space-y-6 lg:col-span-4 col-span-full lg:w-[65.972vw]">
{auth && show && <DraftsList tags={searchParams.getAll('tags')} />}
<div className="space-y-2 pt-2">
{auth && show && <DraftsList tags={searchParams.getAll("tags")} />}
<div className="pt-2 space-y-2">
{auth && <p className="text-[#7A7A7A] text1 pl-3">Опубликованное</p>}
<div className="lg:gap-x-[0.833vw] md:gap-x-3 gap-y-6 md:flex flex-wrap col-start-2">
{articles &&
@@ -35,17 +35,17 @@ export function ArticlesList() {
<div className="bg-[#37393B99] nth-[3n+1]:m-auto nth-[3n]:w-[21.25vw] max-lg:hidden lg:rounded-[1.111vw] rounded-2xl p-[1.111vw] w-[43.611vw] h-[24.444vw] bg-[url(/img/pages/blog/tg/into_lapenko.jpg)] bg-no-repeat bg-right-bottom bg-[length:calc(582.85/628*100%)] flex flex-col justify-between">
<div className="text1 font-medium lg:max-w-[25.069vw]">
Мы еще не перенесли сюда все публикации, но все самое горячее
можно прочитать в телеграмме{' '}
можно прочитать в телеграмме{" "}
<div className="relative inline-block rounded-full lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 bg-[#2AABEE] align-middle">
<TelegramIcon className="lg:w-[0.833vw] lg:h-[0.833vw] w-2 h-2 absolute left-1/2 -translate-1/2 top-1/2" />
<TelegramIcon className="lg:w-[0.833vw] lg:h-[0.833vw] w-2 h-2 absolute left-1/2 -translate-x-1/2 -translate-y-1/2 top-1/2" />
</div>
</div>
<Link
href={'https://t.me/graffestate'}
href={"https://t.me/graffestate"}
className="bg-gradient lg:rounded-[1.111vw] rounded-2xl p-[1.111vw] pl-[1.667vw] flex items-center w-fit lg:gap-[0.556vw] gap-2 group relative cursor-pointer"
>
<div className="absolute w-full h-full top-0 left-0 group-hover:bg-black/10 rounded-2xl transition-colors" />
<p className="btnm font-medium z-1">В телеграм</p>
<div className="group-hover:bg-black/10 rounded-2xl absolute top-0 left-0 w-full h-full transition-colors" />
<p className="btnm font-medium z-[1]">В телеграм</p>
<TelegramIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-2 h-2" />
</Link>
</div>
@@ -1,7 +1,7 @@
'use client';
"use client";
import React, { useEffect, useRef, useState } from 'react';
import { Icon } from '@/ui/Icon';
import React, { useEffect, useRef, useState } from "react";
import { Icon } from "@/ui/Icon";
export function ConsultationRange({
consultations,
@@ -17,7 +17,7 @@ export function ConsultationRange({
const isMouseEvent = (
e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent
): e is React.MouseEvent | MouseEvent => 'clientX' in e;
): e is React.MouseEvent | MouseEvent => "clientX" in e;
function handleMouseDown(e: React.MouseEvent | React.TouchEvent) {
if (!root.current) return;
@@ -57,23 +57,23 @@ export function ConsultationRange({
}
useEffect(() => {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener("mousemove", handleMouseMove);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener("mousemove", handleMouseMove);
};
}, [handleMouseMove, isMouseDown]);
useEffect(() => {
document.addEventListener('mouseleave', handleMouseLeave);
document.addEventListener("mouseleave", handleMouseLeave);
return () => {
document.removeEventListener('mouseleave', handleMouseLeave);
document.removeEventListener("mouseleave", handleMouseLeave);
};
}, [handleMouseLeave, isMouseDown]);
useEffect(() => {
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [handleMouseUp, isMouseDown]);
@@ -92,7 +92,7 @@ export function ConsultationRange({
>
<div
className="absolute left-0 top-0 lg:rounded-[1.111vw] rounded-2xl h-full bg-[#37393B99] backdrop-blur-2xl flex items-center z-[2]"
style={{ width: (consultations / 350) * 100 + '%' }}
style={{ width: (consultations / 350) * 100 + "%" }}
>
<p className="btnl lg:pl-6 pl-3 font-medium select-none">
{consultations}
@@ -105,7 +105,7 @@ export function ConsultationRange({
name="dots"
svgProp={{
className:
'lg:w-[1.667vw] lg:h-[1.667vw] w-6 h-6 text-white select-none',
"lg:w-[1.667vw] lg:h-[1.667vw] w-6 h-6 text-white select-none",
}}
/>
</div>
@@ -1,9 +1,9 @@
/* eslint-disable @next/next/no-img-element */
import { ItemActions } from '@/components/ItemActions';
import { ICompany } from '@/types/ICompany';
import { motion } from 'framer-motion';
import { forwardRef, useState } from 'react';
import { GridItem } from 'react-grid-dnd';
import { ItemActions } from "@/components/ItemActions";
import { ICompany } from "@/types/ICompany";
import { motion } from "framer-motion";
import { forwardRef, useState } from "react";
import { GridItem } from "react-grid-dnd";
export const ClientItem = forwardRef<HTMLDivElement, ICompany>(
(company, ref) => {
@@ -17,7 +17,7 @@ export const ClientItem = forwardRef<HTMLDivElement, ICompany>(
return (
<GridItem>
<motion.div
viewport={{ margin: '-10% 0% 0% 0%', once: true }}
viewport={{ margin: "-10% 0% 0% 0%", once: true }}
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
onViewportEnter={handleOnViewportFeatureEnter}
@@ -30,12 +30,12 @@ export const ClientItem = forwardRef<HTMLDivElement, ICompany>(
</p>
)}
<div className="aspect-square relative flex items-center justify-center p-5">
<div className="max-w-4/5 absolute w-full">
<div className="max-w-[80%] absolute w-full">
<img
src={
isViewportEntered
? process.env.NEXT_PUBLIC_S3_BUCKET + company.logo!
: ''
: ""
}
className="object-cover object-center select-none pointer-events-none !relative min-w-full"
alt={company.title}
@@ -50,4 +50,4 @@ export const ClientItem = forwardRef<HTMLDivElement, ICompany>(
}
);
ClientItem.displayName = 'ClientItem';
ClientItem.displayName = "ClientItem";
@@ -1,20 +1,20 @@
'use client';
"use client";
import { CompanyFormModal } from '@/components/modals/CompanyFormModal';
import { OpenFormModalWrapper } from '@/hocs/OpenFormModalWrapper';
import { useMediaQueries } from '@/hooks/useMediaQueries';
import { useGetCompaniesQuery } from '@/queries/getCompanies';
import { ICompany } from '@/types/ICompany';
import { GradientButton } from '@/ui/GradientButton';
import { Title } from '@/ui/Title';
import { getCompaniesCount } from '@/utils/getCompaniesCount';
import { shuffle } from '@/utils/shuffle';
import { useEffect, useRef, useState } from 'react';
import { GridContextProvider, GridDropZone, swap } from 'react-grid-dnd';
import AddIcon from '../../../../../public/icons/add.svg';
import RestartIcon from '../../../../../public/icons/restart.svg';
import { ClientItem } from './ClientItem';
import { useGetCompaniesCountQuery } from '@/queries/getCompaniesCount';
import { CompanyFormModal } from "@/components/modals/CompanyFormModal";
import { OpenFormModalWrapper } from "@/hocs/OpenFormModalWrapper";
import { useMediaQueries } from "@/hooks/useMediaQueries";
import { useGetCompaniesQuery } from "@/queries/getCompanies";
import { ICompany } from "@/types/ICompany";
import { GradientButton } from "@/ui/GradientButton";
import { Title } from "@/ui/Title";
import { getCompaniesCount } from "@/utils/getCompaniesCount";
import { shuffle } from "@/utils/shuffle";
import { useEffect, useRef, useState } from "react";
import { GridContextProvider, GridDropZone, swap } from "react-grid-dnd";
import AddIcon from "../../../../../public/icons/add.svg";
import RestartIcon from "../../../../../public/icons/restart.svg";
import { ClientItem } from "./ClientItem";
import { useGetCompaniesCountQuery } from "@/queries/getCompaniesCount";
export function Clients() {
const { data: companies } = useGetCompaniesQuery();
@@ -50,9 +50,9 @@ export function Clients() {
setSize(clientRef.current!.clientWidth);
}
window.addEventListener('resize', handleResize);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener('resize', handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [isLg, isMd, isXs]);
return (
@@ -64,7 +64,7 @@ export function Clients() {
<Title className="mx-auto">
<span className="text-gradient">
{count !== undefined && getCompaniesCount(count)}
</span>{' '}
</span>{" "}
уже внедрили наш продукт в свою цепочку продаж
</Title>
<OpenFormModalWrapper
@@ -105,7 +105,7 @@ export function Clients() {
/>
))}
<div
className="absolute flex z-1 flex-col items-center justify-center gap-3 lg:w-[calc((100vw-120px)/11)] md:max-lg:w-[calc((100vw-64px)/5)] w-[calc((100vw-36px)/3)] aspect-square"
className="absolute flex z-[1] flex-col items-center justify-center gap-3 lg:w-[calc((100vw-120px)/11)] md:max-lg:w-[calc((100vw-64px)/5)] w-[calc((100vw-36px)/3)] aspect-square"
style={{
left:
(shuffled.length % (isLg ? 11 : isMd ? 5 : 3)) * (size + 6),
@@ -1,14 +1,17 @@
'use client';
"use client";
import ArrowMoreIcon from '../../../../../public/icons/arrow_more.svg';
import YoutubeIcon from '../../../../../public/icons/youtube.svg';
import { StoriesModal } from '@/components/modals/StoriesModal';
import { useModalStore } from '@/stores/useModalStore';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { motion, useScroll, useTransform } from 'framer-motion';
import Link from 'next/link';
import { useRef } from 'react';
import { api } from "@/api";
import ArrowMoreIcon from "../../../../../public/icons/arrow_more.svg";
import YoutubeIcon from "../../../../../public/icons/youtube.svg";
import { StoriesModal } from "@/components/modals/StoriesModal";
import { useModalStore } from "@/stores/useModalStore";
import { Button } from "@/ui/Button";
import { Icon } from "@/ui/Icon";
import { useQuery } from "@tanstack/react-query";
import { motion, useScroll, useTransform } from "framer-motion";
import Link from "next/link";
import { useRef } from "react";
import { IArticle } from "@/types/IArticle";
/* eslint-disable @next/next/no-img-element */
export function IntegrationsDesktop() {
@@ -16,14 +19,20 @@ export function IntegrationsDesktop() {
const { scrollYProgress } = useScroll({ target });
const x1 = useTransform(scrollYProgress, [0, 1], ['0%', '-77.5%']);
const x1 = useTransform(scrollYProgress, [0, 1], ["0%", "-77.5%"]);
const x2 = useTransform(scrollYProgress, [0, 1], ['0%', '77.5%']);
const x2 = useTransform(scrollYProgress, [0, 1], ["0%", "77.5%"]);
const opacity = useTransform(scrollYProgress, [0, 0.15], [1, 0]);
const { setModal } = useModalStore();
const { data: articles } = useQuery({
queryKey: ["articles"],
queryFn: () => api.get("articles").json<IArticle[]>(),
select: (data) => data.filter(({ slug, drafted }) => slug && !drafted),
});
return (
<div className="max-lg:hidden relative h-[600vh] mt-[140px]" ref={target}>
<div className="sticky top-[12vh] w-full space-y-[0.833vw]">
@@ -73,6 +82,7 @@ export function IntegrationsDesktop() {
title="Офис продаж Авторского квартала Машаров"
className="w-[21.111vw] aspect-[448/354]"
/>
<VideoLink
title="Интерактивный инструмент продаж GRAFF.estate для ЖК «Will Towers»"
src="/videos/pages/home/integrations/Will_compressed.mp4"
@@ -91,6 +101,10 @@ export function IntegrationsDesktop() {
linkTitle="Лучший офис продаж по версии WOW AWARDS 2023"
className="w-[46.389vw] aspect-[812/354]"
/>
{/* hidden links for SEO */}
{articles?.map(({ slug }) => (
<Link href={`/blog/${slug}`} key={slug} className="hidden" />
))}
<IntegrationItem
title="Офисы продаж Паритет девелопмент"
mainSrc="/img/pages/home/integrations/paritet.jpg"
@@ -127,19 +141,19 @@ export function IntegrationItem({
}) {
return (
<div
className={`lg:rounded-[1.111vw] rounded-2xl relative overflow-hidden p-4 lg:p-[1.667vw] before:absolute before:inset-0 before:[background:linear-gradient(to_bottom_right,#00000099,transparent)] before:-z-1 flex-shrink-0 group${
className ? ' ' + className : ''
className={`lg:rounded-[1.111vw] rounded-2xl relative overflow-hidden p-4 lg:p-[1.667vw] before:absolute before:inset-0 before:[background:linear-gradient(to_bottom_right,#00000099,transparent)] before:-z-[1] flex-shrink-0 group${
className ? " " + className : ""
}`}
>
<img
src={mainSrc}
className="w-full h-full left-0 top-0 absolute object-cover object-center -z-2"
className="-z-[2] absolute top-0 left-0 object-cover object-center w-full h-full"
alt={title}
/>
<p className="heading2 font-medium z-2">{title}</p>
<p className="heading2 font-medium z-[2]">{title}</p>
{linkSrc && linkTitle && href && (
<div className="absolute w-[14.722vw] rounded-t-[0.833vw] left-[1.667vw] bottom-0 group-hover:translate-y-0 group-hover:opacity-100 opacity-0 translate-y-full transition-all duration-500 bg-[#37393B99] backdrop-blur p-[0.833vw] space-y-[0.556vw]">
<div className="flex gap-[0.278vw] items-center">
<div className="absolute w-[14.722vw] rounded-t-[0.833vw] left-[1.667vw] bottom-0 group-hover:translate-y-0 group-hover:opacity-100 opacity-0 translate-y-full transition-all duration-500 bg-[#37393B99] backdrop-blur p-[0.833vw]">
<div className="flex gap-[0.278vw] items-center mb-[0.556vw]">
<img src={linkSrc} className="w-[1.944vw]" alt={linkTitle} />
<p className="text-[0.694vw]">{linkTitle}</p>
</div>
@@ -147,7 +161,7 @@ export function IntegrationItem({
<Button
type="button"
width="full"
className="rounded-[0.833vw] justify-center py-[0.556vw]"
className="rounded-[0.833vw] !justify-center py-[0.556vw]"
color="primary"
icon={
<ArrowMoreIcon className="w-[1.111vw] h-[1.111vw] text-white" />
@@ -176,7 +190,7 @@ export function VideoLink({
return (
<div
className={`bg-[#37393B99] lg:rounded-[1.111vw] rounded-2xl lg:p-[1.667vw] lg:pb-[2.639vw] p-4 flex-shrink-0 flex flex-col justify-between overflow-hidden relative group${
className ? ' ' + className : ''
className ? " " + className : ""
}`}
>
<p className="heading2 font-medium">{title}</p>
@@ -201,7 +215,7 @@ export function VideoLink({
<YoutubeIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4" />
}
>
<p className="font-medium btnl">Смотреть все</p>
<p className="btnl font-medium">Смотреть все</p>
</Button>
</Link>
<Link href={link} className="lg:hidden self-end">
@@ -212,7 +226,7 @@ export function VideoLink({
<YoutubeIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4" />
}
>
<p className="font-medium btnl">Смотреть все</p>
<p className="btnl font-medium">Смотреть все</p>
</Button>
</Link>
</div>
@@ -1,9 +1,21 @@
import { Title } from '@/ui/Title';
import { IntegrationItem, VideoLink } from './IntegrationsDesktop';
"use client";
import { Title } from "@/ui/Title";
import { IntegrationItem, VideoLink } from "./IntegrationsDesktop";
import { IArticle } from "@/types/IArticle";
import { api } from "@/api";
import { useQuery } from "@tanstack/react-query";
import Link from "next/link";
export function IntegrationsMini() {
const { data: articles } = useQuery({
queryKey: ["articles"],
queryFn: () => api.get("articles").json<IArticle[]>(),
select: (data) => data.filter(({ slug, drafted }) => slug && !drafted),
});
return (
<div className="lg:hidden space-y-10 mt-25">
<div className="lg:hidden space-y-10 mt-[100px]">
<Title>Интеграция в офисы продаж</Title>
<div className="space-y-2">
<IntegrationItem
@@ -24,6 +36,10 @@ export function IntegrationsMini() {
title="Интерактивный инструмент продаж GRAFF.estate для ЖК «Will Towers»"
className="aspect-[340/316.52]"
/>
{/* hidden links for SEO */}
{articles?.map(({ slug }) => (
<Link href={`/blog/${slug}`} key={slug} className="hidden" />
))}
<IntegrationItem
title="Офисы продаж Паритет девелопмент"
mainSrc="/img/pages/home/integrations/paritet.jpg"
+14 -14
View File
@@ -1,12 +1,12 @@
import { useLongPress } from '@/hooks/useLongPress';
import { useMediaQueries } from '@/hooks/useMediaQueries';
import { useGetProjectsCountQuery } from '@/queries/getProjectsCount';
import { useCityPointStore } from '@/stores/useCityPointStore';
import { ICityProjects } from '@/types/ICityProjects';
import { IMapProject } from '@/types/IMapProject';
import { AnimatePresence, motion } from 'framer-motion';
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';
import { useHover } from 'usehooks-ts';
import { useLongPress } from "@/hooks/useLongPress";
import { useMediaQueries } from "@/hooks/useMediaQueries";
import { useGetProjectsCountQuery } from "@/queries/getProjectsCount";
import { useCityPointStore } from "@/stores/useCityPointStore";
import { ICityProjects } from "@/types/ICityProjects";
import { IMapProject } from "@/types/IMapProject";
import { AnimatePresence, motion } from "framer-motion";
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
import { useHover } from "usehooks-ts";
export function CityPoint({
x,
@@ -82,18 +82,18 @@ export function CityPoint({
ref={pointRef}
style={{ left: `${x}%`, top: `${y}%` }}
className={`absolute outline-none lg:px-[1.111vw] lg:py-[0.625vw] flex lg:gap-[0.278vw] items-center transition-colors cursor-pointer select-none font-medium ${
active ? 'text-white' : 'text-[#7A7A7A]'
active ? "text-white" : "text-[#7A7A7A]"
}`}
>
<p
className={`heading2 font-medium${active ? ' z-2' : ''}`}
className={`heading2 font-medium${active ? " z-[2]" : ""}`}
ref={refTitle}
>
{title}
</p>
<p
className={`btns lg:h-[2.083vw] font-medium h-[5.114vw]${
active ? ' z-2' : ''
active ? " z-[2]" : ""
}`}
ref={refCount}
>
@@ -116,7 +116,7 @@ export function CityPoint({
},
}}
onAnimationComplete={() => setAnimationCompleted(true)}
className="absolute rounded-full aspect-square z-1 [backdrop-filter:blur(3.21px)] bg-[radial-gradient(#37393B00,#37393B99)] -translate-x-1/2 left-1/2"
className="absolute rounded-full aspect-square z-[1] [backdrop-filter:blur(3.21px)] bg-[radial-gradient(#37393B00,#37393B99)] -translate-x-1/2 left-1/2"
/>
{!!projects?.length &&
circleRef.current &&
@@ -184,7 +184,7 @@ export function LogoItem({
top: -circleRadius,
}}
exit={{ opacity: 0, transition: { delay: index * 0.1 } }}
transition={{ delay: index * 0.1 + 0.3, bounce: 'none' }}
transition={{ delay: index * 0.1 + 0.3, bounce: "none" }}
className="max-w-[6.264vw] w-full aspect-square absolute rounded-[1.567vw] -left-[9.049vw]"
/>
);
+12 -12
View File
@@ -1,13 +1,13 @@
'use client';
"use client";
import { cities, mobileCities } from '@/consts/cities';
import { useGetMapPointByCity } from '@/queries/getMapPointByCity';
import { useCityPointStore } from '@/stores/useCityPointStore';
import { useState } from 'react';
import { CityPoint } from './CityPoint';
import { Slider } from './Slider';
import { useMediaQueries } from '@/hooks/useMediaQueries';
import { Icon } from '@/ui/Icon';
import { cities, mobileCities } from "@/consts/cities";
import { useGetMapPointByCity } from "@/queries/getMapPointByCity";
import { useCityPointStore } from "@/stores/useCityPointStore";
import { useState } from "react";
import { CityPoint } from "./CityPoint";
import { Slider } from "./Slider";
import { useMediaQueries } from "@/hooks/useMediaQueries";
import { Icon } from "@/ui/Icon";
export function Map() {
const { cityPoint } = useCityPointStore();
@@ -20,8 +20,8 @@ export function Map() {
return (
<div className="relative">
<div className="max-lg:overflow-x-auto overflow-y-visible h-full scrollbar-hide max-lg:aspect-[340/620] md:max-lg:-mx-4 max-md:-mx-2.5 mt-16 relative">
<div className="lg:bg-[url(/img/pages/home/stats/map2.0.png)] bg-[url(/img/pages/home/stats/map_mobile.png)] -mt-19 bg-no-repeat bg-contain lg:aspect-[1432.4/731.98] h-full aspect-[1068.86/586.76] relative">
<div className="max-lg:overflow-x-auto overflow-y-visible h-full [scrollbar-width:none] max-lg:aspect-[340/620] md:max-lg:-mx-4 max-md:-mx-2.5 mt-16 relative">
<div className="lg:bg-[url(/img/pages/home/stats/map2.0.png)] bg-[url(/img/pages/home/stats/map_mobile.png)] -mt-[76px] bg-no-repeat bg-contain lg:aspect-[1432.4/731.98] h-full aspect-[1068.86/586.76] relative">
{(isLg ? cities : mobileCities).map((point, index) => (
<CityPoint
key={point.title}
@@ -44,7 +44,7 @@ export function Map() {
<div className="w-[6.667vw] h-[6.667vw]">
<Icon
name="finger_print"
svgProp={{ className: 'w-[6.667vw] h-[6.667vw]' }}
svgProp={{ className: "w-[6.667vw] h-[6.667vw]" }}
/>
</div>
<p className="caption leading-[120%] font-medium select-none">
@@ -1,17 +1,17 @@
'use client';
"use client";
import { videos } from '@/consts/presentation/videos';
import { Title } from '@/ui/Title';
import { useMotionValueEvent, useScroll } from 'framer-motion';
import { useEffect, useRef, useState } from 'react';
import { Engine } from '../../../slides/Engine';
import { Infrastructure } from '../../../slides/Infrastructure';
import { Insolation } from '../../../slides/Insolation';
import { IntegrationCRM } from '../../../slides/IntegrationCRM';
import { SearchAndSelect } from '../../../slides/SearchAndSelect';
import { ThreeDTour } from '../../../slides/ThreeDTour';
import { VideoLayerMain } from '../../../slides/VideoLayerMain';
import { PrimeProgressItem } from '@/ui/PrimeProgressItem';
import { videos } from "@/consts/presentation/videos";
import { Title } from "@/ui/Title";
import { useMotionValueEvent, useScroll } from "framer-motion";
import { useRef, useState } from "react";
import { Engine } from "../../../slides/Engine";
import { Infrastructure } from "../../../slides/Infrastructure";
import { Insolation } from "../../../slides/Insolation";
import { IntegrationCRM } from "../../../slides/IntegrationCRM";
import { SearchAndSelect } from "../../../slides/SearchAndSelect";
import { ThreeDTour } from "../../../slides/ThreeDTour";
import { VideoLayerMain } from "../../../slides/VideoLayerMain";
import { PrimeProgressItem } from "@/ui/PrimeProgressItem";
export function PresentationDesktop() {
const target = useRef<HTMLDivElement>(null);
@@ -23,19 +23,19 @@ export function PresentationDesktop() {
const [currentHovered, setCurrentHovered] = useState<number | undefined>();
useMotionValueEvent(scrollYProgress, 'change', (value) =>
useMotionValueEvent(scrollYProgress, "change", (value) =>
setSlide(Math.min(Math.trunc(value * videos.length), videos.length - 1))
);
return (
<div className="mt-25 mb-60 max-lg:hidden relative">
<div className="mt-[100px] mb-[240px] max-lg:hidden relative">
<Title className="mb-16">
Интерактивная презентация{' '}
<span className="text-gradient">улучшает опыт выбора недвижимости</span>{' '}
Интерактивная презентация{" "}
<span className="text-gradient">улучшает опыт выбора недвижимости</span>{" "}
и&nbsp;увеличивает темпы продаж квартир в&nbsp;жилом комплексе
</Title>
<div className="relative h-[233.334vw]" ref={container}>
<div className="top-30 w-full h-[38.889vw] sticky">
<div className="top-[120px] w-full h-[38.889vw] sticky">
<VideoLayerMain scroll={scrollYProgress} />
<SearchAndSelect scrollProgress={scrollYProgress} page="main" />
<ThreeDTour scrollProgress={scrollYProgress} page="main" />
@@ -43,7 +43,7 @@ export function PresentationDesktop() {
<Insolation scrollProgress={scrollYProgress} page="main" />
<Engine scroll={scrollYProgress} />
<IntegrationCRM scrollProgress={scrollYProgress} page="main" />
<div className="flex absolute bottom-0 p-[0.556vw] rounded-[1.875vw] bg-[#37393B99] backdrop-blur-[20px] left-1/2 -translate-x-1/2 translate-y-1/2 z-50">
<div className="flex absolute bottom-0 p-[0.556vw] rounded-[1.875vw] bg-[#37393B99] backdrop-blur-[20px] left-1/2 -translate-x-1/2 translate-y-1/2 z-[50]">
{videos.map(({ src, anchorImg, title }, index) => (
<PrimeProgressItem
onClick={() => {
@@ -69,7 +69,7 @@ export function PresentationDesktop() {
))}
</div>
</div>
<div ref={target} className="h-full absolute top-0" />
<div ref={target} className="absolute top-0 h-full" />
</div>
</div>
);
@@ -1,16 +1,16 @@
'use client';
"use client";
import { videos } from '@/consts/presentation/videos';
import { useMediaQueries } from '@/hooks/useMediaQueries';
import { Title } from '@/ui/Title';
import { motion, useMotionValueEvent, useScroll } from 'framer-motion';
import { createRef, RefObject, useEffect, useRef, useState } from 'react';
import { Engine } from '../../../slides/Engine';
import { Infrastructure } from '../../../slides/Infrastructure';
import { Insolation } from '../../../slides/Insolation';
import { IntegrationCRM } from '../../../slides/IntegrationCRM';
import { SearchAndSelect } from '../../../slides/SearchAndSelect';
import { ThreeDTour } from '../../../slides/ThreeDTour';
import { videos } from "@/consts/presentation/videos";
import { useMediaQueries } from "@/hooks/useMediaQueries";
import { Title } from "@/ui/Title";
import { motion, useMotionValueEvent, useScroll } from "framer-motion";
import { createRef, RefObject, useEffect, useRef, useState } from "react";
import { Engine } from "../../../slides/Engine";
import { Infrastructure } from "../../../slides/Infrastructure";
import { Insolation } from "../../../slides/Insolation";
import { IntegrationCRM } from "../../../slides/IntegrationCRM";
import { SearchAndSelect } from "../../../slides/SearchAndSelect";
import { ThreeDTour } from "../../../slides/ThreeDTour";
export function PresentationMini() {
const [slide, setSlide] = useState(0);
@@ -30,7 +30,7 @@ export function PresentationMini() {
videoRefs[slide].current.play();
}, [slide, videoRefs]);
useMotionValueEvent(scrollYProgress, 'change', (value) =>
useMotionValueEvent(scrollYProgress, "change", (value) =>
setSlide(Math.ceil(value * 6))
);
@@ -58,29 +58,29 @@ export function PresentationMini() {
};
return (
<div className="mt-25 lg:hidden space-y-10">
<div className="mt-[100px] lg:hidden space-y-10">
<div
className="md:space-y-12 max-xs:top-0 xs:max-md:top-20 top-10 sticky space-y-5"
ref={root}
>
<div ref={titleContainer}>
<Title>
Интерактивная презентация{' '}
Интерактивная презентация{" "}
<span className="text-gradient">
улучшает&nbsp;опыт выбора&nbsp;недвижимости
</span>{' '}
</span>{" "}
и&nbsp;увеличивает темпы продаж
<span className="max-sm:hidden">
{' '}
{" "}
квартир в&nbsp;жилом комплексе
</span>
</Title>
</div>
<motion.div
viewport={{ margin: '-10% 0% 0% 0%', once: true }}
viewport={{ margin: "-10% 0% 0% 0%", once: true }}
onViewportEnter={handleOnViewportFeatureEnter}
ref={videoContainer}
className="md:aspect-[721/400] aspect-[340/193] perspective-[28.117vw] w-[94.444vw] md:w-[93.88vw] mx-auto relative bg-[url(/img/pages/home/presentation/touch_screen.png)] bg-no-repeat bg-top bg-[length:70.368vw]"
className="md:aspect-[721/400] aspect-[340/193] [perspective:28.117vw] w-[94.444vw] md:w-[93.88vw] mx-auto relative bg-[url(/img/pages/home/presentation/touch_screen.png)] bg-no-repeat bg-top bg-[length:70.368vw]"
>
{videos.map(({ src }, index) => (
<video
@@ -88,7 +88,7 @@ export function PresentationMini() {
src={
isViewportEntered
? `/videos/pages/home/presentation/${src}.mp4`
: ''
: ""
}
loop
muted
@@ -97,8 +97,8 @@ export function PresentationMini() {
style={{
zIndex: videos.length - index,
}}
className={`absolute w-[57.977vw] object-bottom object-cover h-[28vw] origin-top top-[5vw] !rotate-x-4 left-1/2 -translate-x-1/2 transition-opacity${
slide > index && index !== videos.length - 1 ? ' opacity-0' : ''
className={`absolute w-[57.977vw] object-bottom object-cover h-[28vw] origin-top top-[5vw] ![rotate:x_4deg] left-1/2 -translate-x-1/2 transition-opacity${
slide > index && index !== videos.length - 1 ? " opacity-0" : ""
}`}
/>
))}
+9 -9
View File
@@ -1,13 +1,13 @@
'use client';
"use client";
import {
queryProjectsOptions,
useGetProjectsQuery,
} from '@/queries/getProjects';
import { Title } from '@/ui/Title';
import { getProjectsCount } from '@/utils/getProjectsCount';
import { ProjectsSection } from '../ProjectsPage/ProjectsSection';
import { queryOptions, useQuery } from '@tanstack/react-query';
} from "@/queries/getProjects";
import { Title } from "@/ui/Title";
import { getProjectsCount } from "@/utils/getProjectsCount";
import { ProjectsSection } from "../ProjectsPage/ProjectsSection";
import { queryOptions, useQuery } from "@tanstack/react-query";
export function Projects() {
// const { data: projects } = useGetProjectsQuery();
@@ -17,11 +17,11 @@ export function Projects() {
<div className="lg:space-y-16 sm:space-y-12 space-y-10 lg:mt-40 mt-[100px]">
<div className="lg:flex sm:space-y-12 space-y-10">
<Title>
За 15 лет работы мы реализовали{' '}
За 15 лет работы мы реализовали{" "}
<span className="text-gradient">
{projects && getProjectsCount(projects?.length)}{' '}
{projects && getProjectsCount(projects?.length)}{" "}
для&nbsp;застройщиков
</span>{' '}
</span>{" "}
в&nbsp;сфере интерактивных технологий
</Title>
</div>
+11 -11
View File
@@ -1,22 +1,22 @@
/* eslint-disable @next/next/no-img-element */
'use client';
"use client";
import { useGetCompaniesCountQuery } from '@/queries/getCompaniesCount';
import { Figure } from '@/ui/Figure';
import { Title } from '@/ui/Title';
import { getCompaniesCount } from '@/utils/getCompaniesCount';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
import { useGetCompaniesCountQuery } from "@/queries/getCompaniesCount";
import { Figure } from "@/ui/Figure";
import { Title } from "@/ui/Title";
import { getCompaniesCount } from "@/utils/getCompaniesCount";
import { motion, useInView } from "framer-motion";
import { useRef } from "react";
export function Statistics() {
const { data: count } = useGetCompaniesCountQuery();
const root = useRef<HTMLDivElement>(null);
const inView = useInView(root, { margin: '-200px 0px 0px 0px', once: true });
const inView = useInView(root, { margin: "-200px 0px 0px 0px", once: true });
return (
<div className="lg:space-y-[4.444vw] md:max-lg:space-y-[6.25vw] space-y-[11.111vw] lg:mt-40 mt-25">
<div className="lg:space-y-[4.444vw] md:max-lg:space-y-[6.25vw] space-y-[11.111vw] lg:mt-40 mt-[100px]">
<Title>
Мы разрабатываем
<span className="text-gradient"> эффективный продукт,</span> которым
@@ -43,7 +43,7 @@ export function Statistics() {
opacity: +inView,
y: inView ? 0 : 100,
}}
transition={{ delay: 0.3, bounce: 'none' }}
transition={{ delay: 0.3, bounce: "none" }}
className="lg:p-[1.667vw] p-6 flex flex-col justify-between lg:col-start-2 lg:row-span-2 lg:row-start-1 md:max-lg:col-start-1 md:max-lg:row-start-2 col-span-2 md:max-lg:aspect-[736/360] bg-[url(/img/pages/home/stats/building2.png),linear-gradient(to_bottom_right,#7A7A7A50,transparent)] bg-no-repeat bg-right-bottom bg-[length:65%,100%] lg:rounded-[1.111vw] rounded-2xl relative"
>
<p className="text1 lg:max-w-[35%] max-w-[70%]">
@@ -59,7 +59,7 @@ export function Statistics() {
opacity: +inView,
y: inView ? 0 : 100,
}}
transition={{ delay: 0.6, bounce: 'none' }}
transition={{ delay: 0.6, bounce: "none" }}
className="lg:col-start-4 lg:row-start-1 max-md:col-span-2 lg:p-[1.667vw] md:max-lg:col-start-1 md:max-lg:row-start-3 aspect-[338/225] bg-[linear-gradient(to_top_left,#7a7a7a50,transparent)] p-6 lg:rounded-[1.111vw] rounded-2xl flex flex-col justify-between relative"
>
<p className="text1">
@@ -1,17 +1,17 @@
'use client';
"use client";
import { useGetProjectsQuery } from '@/queries/getProjects';
import { Title } from '@/ui/Title';
import { VideoPlayer } from '@/ui/VideoPlayer';
import { motion } from 'framer-motion';
import Link from 'next/link';
import { useState } from 'react';
import { useSwipeable } from 'react-swipeable';
import { StreamingProject } from './StreamingProject';
import { useGetProjectsQuery } from "@/queries/getProjects";
import { Title } from "@/ui/Title";
import { VideoPlayer } from "@/ui/VideoPlayer";
import { motion } from "framer-motion";
import Link from "next/link";
import { useState } from "react";
import { useSwipeable } from "react-swipeable";
import { StreamingProject } from "./StreamingProject";
export function Streaming() {
const { data: streamingProjects } = useGetProjectsQuery(
'Удаленная демонстрация'
"Удаленная демонстрация"
);
const [isViewportEntered, setIsViewportEntered] = useState(false);
@@ -43,12 +43,12 @@ export function Streaming() {
return (
<motion.div
onViewportEnter={handleOnViewportFeatureEnter}
viewport={{ margin: '-10% 0% 0% 0%', once: true }}
viewport={{ margin: "-10% 0% 0% 0%", once: true }}
className="lg:mt-[140px] mt-[100px] lg:space-y-[4.444vw] md:max-lg:space-y-[6.25vw] space-y-[11.111vw]"
>
<Title className="max-md:hidden select-none">
Уникальная технология
<span className="text-gradient"> удаленной демонстрации</span>{' '}
<span className="text-gradient"> удаленной демонстрации</span>{" "}
дает&nbsp;возможность презентовать объект покупателю из&nbsp;любой точки
мира
</Title>
@@ -57,7 +57,7 @@ export function Streaming() {
презентуйте объект покупателю из&nbsp;любой точки мира
</Title>
<div
className="lg:grid md:max-lg:flex grid-cols-4 gap-3 px-5 md:-mx-5 md:max-lg:overflow-auto [scrollbar-width:none] relative max-md:aspect-[340/344] transform-3d items-stretch"
className="lg:grid md:flex grid-cols-4 gap-3 px-5 md:-mx-5 md:overflow-auto [scrollbar-width:none] relative max-md:aspect-[340/344] [transform-style:preserve-3d] items-stretch"
{...handlers}
>
{streamingProjects?.slice(0, 3).map((project, index, { length }) => (
@@ -71,21 +71,21 @@ export function Streaming() {
/>
))}
<div
className={`bg-gradient-to-r from-[#FFFFFF14] to-[#FFFFFF00] [background:linear-gradient(to_right,#FFFFFF14,#FFFFFF00)] p-0.5 lg:rounded-[1.111vw] rounded-2xl flex flex-1 justify-center duration-500 items-center md:max-lg:min-w-[300px] group max-md:absolute self-stretch max-md:h-full transition-transform select-none max-md:w-[calc(100%-40px)] max-md:bg-[#0F101199] max-md:[backdrop-filter:blur(40px)] ${
className={`bg-gradient-to-r from-[#FFFFFF14] to-[#FFFFFF00] [background:linear-gradient(to_right,#FFFFFF14,#FFFFFF00)] p-0.5 lg:rounded-[1.111vw] rounded-2xl flex flex-1 justify-center !duration-500 items-center md:min-w-[300px] group max-md:absolute self-stretch max-md:h-full transition-[scale,transform] will-change-[transform,scale] select-none max-md:w-[calc(100%-40px)] max-md:bg-[#0F101199] max-md:[backdrop-filter:blur(40px)] ${
streamingProjects &&
(Math.min(streamingProjects!.length + 1, 4) - 1 === current
? 'max-md:translate-z-10'
: 'max-md:scale-85')
? "max-md:[transform:translateZ(40px)]"
: "max-md:[scale:85%]")
} ${
streamingProjects &&
(Math.min(streamingProjects!.length + 1, 4) - 1 ===
(current + 1) % Math.min(streamingProjects!.length + 1, 4)
? 'max-md:translate-x-[calc(7.5%+20px)]'
? "max-md:translate-x-[calc(7.5%+20px)]"
: Math.min(streamingProjects!.length + 1, 4) - 1 ===
(current - 1 + Math.min(streamingProjects!.length + 1, 4)) %
Math.min(streamingProjects!.length + 1, 4)
? 'max-md:translate-x-[calc(-7.5%-20px)]'
: '')
? "max-md:translate-x-[calc(-7.5%-20px)]"
: "")
}`}
>
<div className="md:bg-[#0F1011] h-full w-full lg:rounded-[1.111vw] rounded-2xl flex items-center p-6">
@@ -94,7 +94,7 @@ export function Streaming() {
Расскажем и покажем как это работает на&nbsp;созвоне
</p>
<Link
href={'/form'}
href={"/form"}
className="btnm font-medium group-hover:scale-105 duration-500 lg:px-[1.667vw] lg:py-[1.181vw] px-6 py-[17px] transition-transform lg:rounded-[0.833vw] rounded-xl bg-gradient"
>
Оставить заявку
@@ -1,7 +1,7 @@
import { streaming } from '@/consts/streaming';
import { IProject } from '@/types/IProject';
import Link from 'next/link';
import ArrowMoreIcon from '../../../../../public/icons/arrow_more.svg';
import { streaming } from "@/consts/streaming";
import { IProject } from "@/types/IProject";
import Link from "next/link";
import ArrowMoreIcon from "../../../../../public/icons/arrow_more.svg";
export function StreamingProject({
city,
@@ -12,7 +12,7 @@ export function StreamingProject({
index,
current,
count,
}: Pick<IProject, 'city' | 'title' | 'image' | 'company'> & {
}: Pick<IProject, "city" | "title" | "image" | "company"> & {
href: string;
index: number;
current: number;
@@ -20,14 +20,16 @@ export function StreamingProject({
}) {
return (
<div
className={`lg:aspect-[344/396] aspect-[300/344] flex-1 md:max-lg:min-w-[300px] lg:rounded-[1.111vw] rounded-2xl lg:p-[1.111vw] p-4 flex duration-500 relative overflow-hidden transition-transform group max-md:absolute max-md:w-[calc(100%-40px)] select-none h-full max-md:${
index === current ? 'translate-z-10' : 'scale-85'
} max-md:${
className={`lg:aspect-[344/396] aspect-[300/344] flex-1 md:max-lg:min-w-[300px] transition-[scale,transform] will-change-[transform,scale] lg:rounded-[1.111vw] rounded-2xl lg:p-[1.111vw] p-4 flex duration-500 relative overflow-hidden group max-md:absolute max-md:w-[calc(100%-40px)] select-none h-full ${
index === current
? "max-md:[transform:translateZ(40px)]"
: "max-md:[scale:85%]"
} ${
index === (current + 1) % count
? 'translate-x-[calc(7.5%+20px)]'
? "max-md:[transform:translateX(calc(7.5%+20px))]"
: index === (current - 1 + count) % count
? 'translate-x-[calc(-7.5%-20px)]'
: ''
? "max-md:[transform:translateX(calc(-7.5%-20px))]"
: ""
}`}
>
<div
@@ -56,18 +58,18 @@ export function StreamingProject({
<div className="bottom-4 right-4 lg:hidden absolute">
<Link
className="bg-gradient rounded-xl btns flex items-center gap-2 px-3 py-2 font-medium"
href={streaming.find((s) => s.title === title)?.url ?? '/'}
href={streaming.find((s) => s.title === title)?.url ?? "/"}
>
Смотреть
<ArrowMoreIcon className="w-4 h-4 text-white" />
</Link>
</div>
<Link
href={streaming.find((s) => s.title === title)?.url ?? '/'}
href={streaming.find((s) => s.title === title)?.url ?? "/"}
className="max-lg:hidden lg:group-hover:opacity-100 opacity-0 transition-opacity duration-500 absolute w-full h-full left-0 bottom-0 md:max-lg:rounded-2xl rounded-xl font-medium [backdrop-filter:blur(3px)] content-center text-center z-0"
>
<p className="btnl flex justify-center gap-2">
Начать демонстрацию{' '}
Начать демонстрацию{" "}
<ArrowMoreIcon className="text-white w-[1.389vw] h-[1.389vw]" />
</p>
</Link>
@@ -1,7 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import { useModalStore } from '@/stores/useModalStore';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
import { useModalStore } from "@/stores/useModalStore";
import { motion } from "framer-motion";
import { PrimeModal } from "../modals/PrimeModal";
export function ThreeDReelsCard({ slide }: { slide: number }) {
const { setModal } = useModalStore();
@@ -10,14 +10,14 @@ export function ThreeDReelsCard({ slide }: { slide: number }) {
<motion.div
animate={{
opacity: +(slide > 10),
bottom: slide > 10 ? '0vw' : undefined,
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]"
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] cursor-pointer"
onClick={() =>
setModal(
<PrimeModal
categoryTitle="Рекламные материалы"
packages={['Стандарт']}
packages={["Стандарт"]}
src="/videos/pages/prime/3d_reels.mp4"
title="3D-рилс"
text="3D-рилс — это быстрый путь в Instagram, TikTok и остальные соцсети. Наглядная мини-презентация ЖК в динамике, которую можно оперативно выпустить в ленту и зацепить потенциальных клиентов."
@@ -27,12 +27,12 @@ export function ThreeDReelsCard({ slide }: { slide: number }) {
>
<p className="btns font-medium px-[0.833vw] py-[0.486vw] rounded-[1.181vw] bg-[#37393B99] backdrop-blur-xs self-end">
{slide === 11
? '1 ролик'
? "1 ролик"
: slide === 12
? '3 ролика'
? "3 ролика"
: slide === 13
? '6 роликов'
: '12 роликов'}
? "6 роликов"
: "12 роликов"}
</p>
<img
src="/img/pages/prime/phone.png"
@@ -1,7 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import { useModalStore } from '@/stores/useModalStore';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
import { useModalStore } from "@/stores/useModalStore";
import { motion } from "framer-motion";
import { PrimeModal } from "../modals/PrimeModal";
export function AnaliticsCard({ slide }: { slide: number }) {
const { setModal } = useModalStore();
@@ -10,7 +10,7 @@ export function AnaliticsCard({ slide }: { slide: number }) {
<motion.div
animate={{
opacity: +(slide > 11),
bottom: slide > 11 ? '21.842vh' : '93.684vh',
bottom: slide > 11 ? "21.842vh" : "93.684vh",
}}
onClick={() =>
setModal(
@@ -18,12 +18,12 @@ export function AnaliticsCard({ slide }: { slide: number }) {
categoryTitle="Опции"
title="Аналитика поведения пользователя во время презентации"
text="Речевая аналитика позволяет совершенствовать отдел продаж, выявляя «болевые» точки общения, улучшать скрипты и грамотно обучать персонал, что приводит к росту конверсии."
packages={['Премиум', 'Бизнес']}
packages={["Премиум", "Бизнес"]}
src="/img/pages/prime/analyse.png"
/>
)
}
className="p-[1.389vw] -translate-y-[1.389vw] rounded-[1.389vw] absolute bg-[#37393B99] right-[1.667vw] w-[15.486vw] h-[28.816vh] flex flex-col justify-between"
className="p-[1.389vw] -translate-y-[1.389vw] rounded-[1.389vw] absolute bg-[#37393B99] right-[1.667vw] w-[15.486vw] h-[28.816vh] flex flex-col justify-between cursor-pointer"
>
<img
src="/img/pages/prime/analyse.png"
@@ -1,7 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import { useModalStore } from '@/stores/useModalStore';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
import { useModalStore } from "@/stores/useModalStore";
import { motion } from "framer-motion";
import { PrimeModal } from "../modals/PrimeModal";
export function ArchVisCard({ slide }: { slide: number }) {
const { setModal } = useModalStore();
@@ -10,7 +10,7 @@ export function ArchVisCard({ slide }: { slide: number }) {
<motion.div
animate={{
opacity: +(slide > 11),
bottom: slide > 11 ? '26.316vh' : '93.684vh',
bottom: slide > 11 ? "26.316vh" : "93.684vh",
}}
onClick={() =>
setModal(
@@ -18,19 +18,19 @@ export function ArchVisCard({ slide }: { slide: number }) {
categoryTitle="Рекламные материалы"
title="Архитектурная визуализация"
text="Архитектурная визуализация — это «классика» рекламы в недвижимости. Красивые рендеры перекликаются с интерактивной презентацией, формируя единый облик проекта и повышая его узнаваемость."
packages={['Премиум', 'Бизнес']}
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"
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 cursor-pointer"
>
<p className="px-[0.833vw] py-[0.486vw] rounded-[1.181vw] btns font-medium bg-[#37393B99] self-end">
{slide === 12
? '3 рендера'
? "3 рендера"
: slide === 13
? '6 рендеров'
: '18 рендеров'}
? "6 рендеров"
: "18 рендеров"}
</p>
<p className="btns font-medium">Архитектурная визуализация</p>
</motion.div>
@@ -1,7 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import { useModalStore } from '@/stores/useModalStore';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
import { useModalStore } from "@/stores/useModalStore";
import { motion } from "framer-motion";
import { PrimeModal } from "../modals/PrimeModal";
export function AvatarCard({ slide }: { slide: number }) {
const { setModal } = useModalStore();
@@ -10,7 +10,7 @@ export function AvatarCard({ slide }: { slide: number }) {
<motion.div
animate={{
opacity: +(slide > 13),
bottom: slide > 13 ? '56.711vh' : '93.684vh',
bottom: slide > 13 ? "56.711vh" : "93.684vh",
}}
onClick={() =>
setModal(
@@ -18,12 +18,12 @@ export function AvatarCard({ slide }: { slide: number }) {
categoryTitle="Опции"
title="Аватар клиента"
text="Аватар клиента — ещё один шаг к полной цифровой симуляции. «Войдя» в роль внутри 3D-презентации, покупатель ощущает реальное присутствие и меньше сомневается, подходит ли ему такой формат пространства."
packages={['Комфорт+']}
packages={["Комфорт+"]}
src="/img/pages/prime/avatar.png"
/>
)
}
className="p-[1.389vw] rounded-[1.389vw] -translate-y-[1.389vw] flex flex-col justify-between items-center absolute bg-[#37393B99] w-[8.75vw] h-[20.132vh] right-[17.778vw]"
className="p-[1.389vw] rounded-[1.389vw] -translate-y-[1.389vw] flex flex-col justify-between items-center absolute bg-[#37393B99] w-[8.75vw] h-[20.132vh] right-[17.778vw] cursor-pointer"
>
<img src="/img/pages/prime/avatar.png" alt="" />
<p className="btns font-medium">Аватар клиента</p>
@@ -1,7 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import { useModalStore } from '@/stores/useModalStore';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
import { useModalStore } from "@/stores/useModalStore";
import { motion } from "framer-motion";
import { PrimeModal } from "../modals/PrimeModal";
export function EngineCard({ slide }: { slide: number }) {
const { setModal } = useModalStore();
@@ -10,7 +10,7 @@ export function EngineCard({ slide }: { slide: number }) {
<motion.div
animate={{
opacity: +(slide > 11),
bottom: slide > 11 ? '21.842vh' : '93.684vh',
bottom: slide > 11 ? "21.842vh" : "93.684vh",
}}
onClick={() =>
setModal(
@@ -18,12 +18,12 @@ export function EngineCard({ slide }: { slide: number }) {
categoryTitle="Опции"
title="Модуль инженерных систем"
text="Модуль инженерных систем говорит о серьёзности подхода застройщика: если он готов показать инженерную «начинку», значит, скрывать нечего. Это особенно важно для клиентов, которые ценят технологичность."
packages={['Комфорт+']}
packages={["Комфорт+"]}
src="/videos/pages/home/presentation/5_engine.mp4"
/>
)
}
className="w-[9.236vw] h-[33.289vh] -translate-y-[1.389vw] p-[1.389vw] rounded-[1.389vw] absolute bg-[#37393B99] right-[17.778vw] flex flex-col justify-between"
className="w-[9.236vw] h-[33.289vh] -translate-y-[1.389vw] p-[1.389vw] rounded-[1.389vw] absolute bg-[#37393B99] right-[17.778vw] flex flex-col justify-between cursor-pointer"
>
<img
src="/img/pages/prime/ruble.png"
@@ -1,9 +1,9 @@
'use client';
"use client";
import { useModalStore } from '@/stores/useModalStore';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
import { categoryDescription } from '@/consts/categories';
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();
@@ -12,22 +12,22 @@ export function EnvironmentCard({ slide }: { slide: number }) {
<motion.div
animate={{
opacity: +(slide > 10),
bottom: slide > 10 ? '0vw' : undefined,
bottom: slide > 10 ? "0vw" : undefined,
}}
onClick={() =>
setModal(
<PrimeModal
src={'/videos/pages/home/presentation/3_infrastructure.mp4'}
packages={['Премиум', 'Бизнес', 'Комфорт+', 'Стандарт']}
src={"/videos/pages/home/presentation/3_infrastructure.mp4"}
packages={["Премиум", "Бизнес", "Комфорт+", "Стандарт"]}
categoryTitle="Детальная проработка окружения"
title={'Детальная проработка ЖК и ближайшего благоустойства'}
title={"Детальная проработка ЖК и ближайшего благоустойства"}
text={
categoryDescription['Детальная проработка окружения'][0].text1
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]"
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] cursor-pointer"
>
<p className="btns max-w-2/3 font-medium">
Детальная проработка ЖК и ближайшего благоустойства
@@ -1,7 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import { useModalStore } from '@/stores/useModalStore';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
import { useModalStore } from "@/stores/useModalStore";
import { motion } from "framer-motion";
import { PrimeModal } from "../modals/PrimeModal";
export function EquipmentCard({ slide }: { slide: number }) {
const { setModal } = useModalStore();
@@ -10,7 +10,7 @@ export function EquipmentCard({ slide }: { slide: number }) {
<motion.div
animate={{
opacity: +(slide > 10),
bottom: slide > 10 ? '0vw' : undefined,
bottom: slide > 10 ? "0vw" : undefined,
}}
onClick={() =>
setModal(
@@ -18,53 +18,53 @@ export function EquipmentCard({ slide }: { slide: number }) {
categoryTitle="Оборудование"
title={
slide === 11
? 'Настенная панель'
? "Настенная панель"
: slide === 12
? 'Брендированный стол 800 нит'
: 'Брендированный стол 2500 нит'
? "Брендированный стол 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.'
"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'
? "/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"
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 cursor-pointer"
>
<img
src="/img/pages/prime/wallPanel.png"
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'
slide === 11 ? "opacity-100" : "opacity-0"
}`}
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'
slide > 11 ? "opacity-100" : "opacity-0"
}`}
alt="brand tablet"
/>
<p className="btns font-medium z-1">
<p className="btns font-medium z-[1]">
{slide === 11
? 'Настенная панель'
? "Настенная панель"
: slide === 12
? 'Брендированный интерактивный стол 800 нит'
: 'Брендированный интерактивный стол 2500 нит'}
? "Брендированный интерактивный стол 800 нит"
: "Брендированный интерактивный стол 2500 нит"}
</p>
</motion.div>
);
@@ -1,9 +1,9 @@
'use client';
"use client";
/* eslint-disable @next/next/no-img-element */
import { useModalStore } from '@/stores/useModalStore';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
import { useModalStore } from "@/stores/useModalStore";
import { motion } from "framer-motion";
import { PrimeModal } from "../modals/PrimeModal";
export function ExcursionVRCard({ slide }: { slide: number }) {
const { setModal } = useModalStore();
@@ -13,8 +13,8 @@ export function ExcursionVRCard({ slide }: { slide: number }) {
onClick={() =>
setModal(
<PrimeModal
src={'/img/pages/prime/vrvr.png'}
packages={['Премиум', 'Бизнес']}
src={"/img/pages/prime/vrvr.png"}
packages={["Премиум", "Бизнес"]}
title="Экскурсия в VR"
text="Создается по референсам от заказчика в 1-3 вариантах. Квартира «оживает» мебелью и декором, это эмоционально «цепляет» потенциального покупателя."
categoryTitle="Опции"
@@ -23,9 +23,9 @@ export function ExcursionVRCard({ slide }: { slide: number }) {
}
animate={{
opacity: +(slide > 12),
bottom: slide > 12 ? '42.632vh' : '93.684vh',
bottom: slide > 12 ? "42.632vh" : "93.684vh",
}}
className="p-[1.389vw] rounded-[1.389vw] -translate-y-[1.389vw] flex flex-col justify-between absolute bg-[#37393B99] w-[10.347vw] h-[16.842vh] left-[17.708vw]"
className="p-[1.389vw] rounded-[1.389vw] -translate-y-[1.389vw] flex flex-col justify-between absolute bg-[#37393B99] w-[10.347vw] h-[16.842vh] left-[17.708vw] cursor-pointer"
>
<img
src="/img/pages/prime/ruble.png"
@@ -1,7 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import { useModalStore } from '@/stores/useModalStore';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
import { useModalStore } from "@/stores/useModalStore";
import { motion } from "framer-motion";
import { PrimeModal } from "../modals/PrimeModal";
export function ExtraInterestPointsCard({ slide }: { slide: number }) {
const { setModal } = useModalStore();
@@ -10,7 +10,7 @@ export function ExtraInterestPointsCard({ slide }: { slide: number }) {
<motion.div
animate={{
opacity: +(slide > 12),
bottom: slide > 12 ? '16.597vw' : '27.362vw',
bottom: slide > 12 ? "16.597vw" : "27.362vw",
}}
onClick={() =>
setModal(
@@ -18,19 +18,19 @@ export function ExtraInterestPointsCard({ slide }: { slide: number }) {
categoryTitle="Опции"
title="Дополнительные точки интереса"
text="Покупатель имеет возможность оценить всю инфраструктуру ЖК. Это помогает закрывать сделку, особенно если комплекс богат опциями для досуга."
packages={['Премиум']}
packages={["Премиум"]}
src="/videos/pages/prime/interes.mp4"
/>
)
}
className="p-[1.389vw] -translate-y-[1.389vw] rounded-[1.389vw] flex flex-col justify-end absolute w-[15.556vw] h-[25vh] left-[42.222vw] bg-[#37393B99] bg-[url(/img/pages/prime/wheel.png)] bg-contain bg-no-repeat bg-right-bottom before:absolute before:inset-0 before:bg-gradient-to-t before:from-[#37393B99] overflow-hidden"
className="p-[1.389vw] -translate-y-[1.389vw] rounded-[1.389vw] flex flex-col justify-end absolute w-[15.556vw] h-[25vh] left-[42.222vw] bg-[#37393B99] bg-[url(/img/pages/prime/wheel.png)] bg-contain bg-no-repeat bg-right-bottom before:absolute before:inset-0 before:bg-gradient-to-t before:from-[#37393B99] overflow-hidden cursor-pointer"
>
<img
src="/img/pages/prime/ruble.png"
className="absolute w-[0.833vw] top-[1.389vw]"
alt=""
/>
<p className="btns font-medium max-w-1/2 z-1">
<p className="btns font-medium max-w-1/2 z-[1]">
Дополнительные точки интереса
</p>
</motion.div>
@@ -1,7 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import { useModalStore } from '@/stores/useModalStore';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
import { useModalStore } from "@/stores/useModalStore";
import { motion } from "framer-motion";
import { PrimeModal } from "../modals/PrimeModal";
export function FinanceToolCard({ slide }: { slide: number }) {
const { setModal } = useModalStore();
@@ -10,7 +10,7 @@ export function FinanceToolCard({ slide }: { slide: number }) {
<motion.div
animate={{
opacity: +(slide > 12),
bottom: slide > 12 ? '42.632vh' : '93.684vh',
bottom: slide > 12 ? "42.632vh" : "93.684vh",
}}
onClick={() =>
setModal(
@@ -18,15 +18,15 @@ export function FinanceToolCard({ slide }: { slide: number }) {
categoryTitle="Опции"
title="Финансовые инсументы (ипотека, рассрочка)"
text="Финансовые инструменты превращают продажу в единый процесс: все расчёты на месте, клиент не уходит «подумать» и не ищет сторонние калькуляторы, а значит, высок шанс, что сделка состоится быстрее."
packages={['Премиум', 'Бизнес']}
packages={["Премиум", "Бизнес"]}
src="/videos/pages/prime/financial.mp4"
/>
)
}
className="p-[1.389vw] rounded-[1.389vw] -translate-y-[1.389vw] flex flex-col justify-end absolute bg-[#37393B99] w-[12.5vw] h-[23.684vh] left-[28.889vw] bg-[url(/img/pages/prime/finance.png)] bg-[length:12.083vw] overflow-hidden bg-no-repeat bg-[position:1.944vw_1.944vw] before:absolute before:w-[12.083vw] before:h-[12.083vw] before:-bottom-[22px] before:-right-[22px] before:bg-[linear-gradient(#27282A00,#27282A)]"
className="p-[1.389vw] rounded-[1.389vw] -translate-y-[1.389vw] flex flex-col justify-end absolute bg-[#37393B99] w-[12.5vw] h-[23.684vh] left-[28.889vw] bg-[url(/img/pages/prime/finance.png)] bg-[length:12.083vw] overflow-hidden bg-no-repeat bg-[position:1.944vw_1.944vw] before:absolute before:w-[12.083vw] before:h-[12.083vw] before:-bottom-[22px] before:-right-[22px] before:bg-[linear-gradient(#27282A00,#27282A)] cursor-pointer"
>
<p className="btns font-medium z-1">
Финансовые инсументы{' '}
<p className="btns font-medium z-[1]">
Финансовые инсументы{" "}
<span className="text-[#7A7A7A]">(ипотека, рассрочка)</span>
</p>
</motion.div>
@@ -1,16 +1,16 @@
import { primeVideos } from '@/consts/presentation/videos';
import { primeVideos } from "@/consts/presentation/videos";
import {
MotionValue,
motion,
useMotionValueEvent,
useTransform,
} from 'framer-motion';
import { useState } from 'react';
} from "framer-motion";
import { useState } from "react";
export function FolderAnimation({ scroll }: { scroll: MotionValue<number> }) {
const [slide, setSlide] = useState(0);
useMotionValueEvent(scroll, 'change', (value) =>
useMotionValueEvent(scroll, "change", (value) =>
setSlide(
Math.min(
Math.trunc(value * (primeVideos.length + 9)),
@@ -22,7 +22,7 @@ export function FolderAnimation({ scroll }: { scroll: MotionValue<number> }) {
const y = useTransform(
scroll,
[6 / (primeVideos.length + 7), 7 / (primeVideos.length + 7)],
['49.444vw', '0vw']
["49.444vw", "0vw"]
);
const scale = useTransform(
@@ -34,13 +34,13 @@ export function FolderAnimation({ scroll }: { scroll: MotionValue<number> }) {
const bottomBorder = useTransform(
scroll,
[9 / (primeVideos.length + 7), 10 / (primeVideos.length + 7)],
['17.014vw', '1.389vw']
["17.014vw", "1.389vw"]
);
const bottomFolder = useTransform(
scroll,
[9 / (primeVideos.length + 7), 10 / (primeVideos.length + 7)],
['19.931vw', '4.375vw']
["19.931vw", "4.375vw"]
);
const opacity = useTransform(
@@ -55,42 +55,42 @@ export function FolderAnimation({ scroll }: { scroll: MotionValue<number> }) {
animate={
slide > 7
? {
bottom: slide > 9 ? '2.986vw' : '19.931vw',
width: '13.958vw',
height: '12.014vw',
bottom: slide > 9 ? "2.986vw" : "19.931vw",
width: "13.958vw",
height: "12.014vw",
}
: {
bottom: '12.639vw',
width: '15.417vw',
height: '13.194vw',
bottom: "12.639vw",
width: "15.417vw",
height: "13.194vw",
}
}
style={{ y, scale, bottom: bottomFolder }}
transition={{ bounce: 'none' }}
style={{ y, scale, bottom: bottomFolder, x: "-50%" }}
transition={{ bounce: "none" }}
src="/icons/folderBack.svg"
className="left-1/2 absolute -translate-x-1/2"
className="left-1/2 absolute"
/>
<motion.img
animate={
slide > 7
? {
bottom: slide > 9 ? '2.986vw' : '19.931vw',
width: '15.833vw',
height: '9.792vw',
bottom: slide > 9 ? "2.986vw" : "19.931vw",
width: "15.833vw",
height: "9.792vw",
}
: { width: '17.361vw', height: '10.764vw', bottom: '12.639vw' }
: { width: "17.361vw", height: "10.764vw", bottom: "12.639vw" }
}
style={{ y, scale, bottom: bottomFolder }}
transition={{ bounce: 'none' }}
style={{ y, scale, bottom: bottomFolder, x: "-50%" }}
transition={{ bounce: "none" }}
src="/icons/folderFront.svg"
className="left-1/2 absolute z-12 -translate-x-1/2"
className="left-1/2 absolute z-[12]"
/>
<motion.div
style={{
bottom: bottomBorder,
opacity,
}}
transition={{ bounce: 'none' }}
transition={{ bounce: "none" }}
className="rounded-[1.389vw] border border-[#37393B] aspect-square w-[15.972vw] absolute left-1/2 -translate-x-1/2 p-[1.111vw] flex items-end"
>
<p className="btns font-medium">Базовый функционал</p>
@@ -1,7 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import { useModalStore } from '@/stores/useModalStore';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
import { useModalStore } from "@/stores/useModalStore";
import { motion } from "framer-motion";
import { PrimeModal } from "../modals/PrimeModal";
export function InteractiveWindowCard({ slide }: { slide: number }) {
const { setModal } = useModalStore();
@@ -10,7 +10,7 @@ export function InteractiveWindowCard({ slide }: { slide: number }) {
<motion.div
animate={{
opacity: +(slide > 13),
bottom: slide > 13 ? '57.105vh' : '93.684vh',
bottom: slide > 13 ? "57.105vh" : "93.684vh",
}}
onClick={() =>
setModal(
@@ -18,19 +18,19 @@ export function InteractiveWindowCard({ slide }: { slide: number }) {
categoryTitle="Опции"
title="Интерактивное окно"
text="Интерактивное окно выделяется среди стандартных экспозиций: клиенты подходят, «заглядывают» в виртуальный вид, и это запоминается надолго, усиливая эмоциональную связь с проектом."
packages={['Премиум']}
packages={["Премиум"]}
src="/img/pages/prime/interactiveWindow.png"
/>
)
}
className="p-[1.389vw] rounded-[1.389vw] -translate-y-[1.389vw] flex flex-col justify-end absolute bg-[#37393B99] w-[15.486vw] h-[36.316vh]"
className="p-[1.389vw] rounded-[1.389vw] -translate-y-[1.389vw] flex flex-col justify-end absolute bg-[#37393B99] w-[15.486vw] h-[36.316vh] cursor-pointer"
>
<img
src="/img/pages/prime/interactiveWindow.png"
className="absolute left-[2.014vw] bottom-0 h-full"
alt=""
/>
<p className="btns font-medium z-1">Интерактивное окно</p>
<p className="btns font-medium z-[1]">Интерактивное окно</p>
</motion.div>
);
}
@@ -1,8 +1,8 @@
/* eslint-disable @next/next/no-img-element */
import SwitchIcon from '../../../../../public/icons/switch.svg';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
import { useModalStore } from '@/stores/useModalStore';
import SwitchIcon from "../../../../../public/icons/switch.svg";
import { motion } from "framer-motion";
import { PrimeModal } from "../modals/PrimeModal";
import { useModalStore } from "@/stores/useModalStore";
export function InterierConfiguratorCard({ slide }: { slide: number }) {
const { setModal } = useModalStore();
@@ -11,20 +11,20 @@ export function InterierConfiguratorCard({ slide }: { slide: number }) {
<motion.div
animate={{
opacity: +(slide > 11),
bottom: slide > 11 ? '38.026vh' : '93.684vh',
bottom: slide > 11 ? "38.026vh" : "93.684vh",
}}
onClick={() =>
setModal(
<PrimeModal
categoryTitle="Опции"
packages={['Премиум', 'Бизнес']}
packages={["Премиум", "Бизнес"]}
text="Конфигуратор интерьера убирает лишние сомнения: клиент видит сразу несколько вариантов отделки и выбирает тот, что ближе к его вкусам — что часто ускоряет заключение договора."
title="Конфигуратор интерьера"
src="/img/pages/prime/configuratorInterior.png"
/>
)
}
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]"
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] cursor-pointer"
>
<img
src="/img/pages/prime/ruble.png"
@@ -37,7 +37,7 @@ export function InterierConfiguratorCard({ slide }: { slide: number }) {
alt=""
className="rounded-full w-[4.444vw] aspect-square"
/>
<div className="bg-gradient rounded-full p-[0.538vw] absolute z-1 left-1/2 top-1/2 -translate-1/2">
<div className="bg-gradient rounded-full p-[0.538vw] absolute z-[1] left-1/2 top-1/2 -translate-1/2">
<SwitchIcon className="text-white w-[1.076vw] h-[1.076vw]" />
</div>
<img
@@ -1,19 +1,19 @@
import { Title } from '@/ui/Title';
import { AnimatePresence, motion } from 'framer-motion';
import { Title } from "@/ui/Title";
import { AnimatePresence, motion } from "framer-motion";
export function PackageTitle({ slide }: { slide: number }) {
return (
<motion.div
animate={{ opacity: +(slide > 10) }}
className="absolute space-y-[2.639vw] left-1/2 -translate-x-1/2 overflow-hidden top-[calc(100px-1.389vw)]"
className="absolute space-y-[2.639vw] left-1/2 !-translate-x-1/2 overflow-hidden top-[calc(100px-1.389vw)]"
>
<Title headerLevel={1} className="text-center">
<AnimatePresence mode="popLayout">
{slide === 11 && (
<motion.span
initial={{ y: '100%', opacity: 0 }}
initial={{ y: "100%", opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: '-100%', opacity: 0 }}
exit={{ y: "-100%", opacity: 0 }}
transition={{ duration: 0.4 }}
>
стандарт
@@ -24,9 +24,9 @@ export function PackageTitle({ slide }: { slide: number }) {
{slide === 12 && (
<motion.span
transition={{ duration: 0.4 }}
initial={{ y: '100%', opacity: 0 }}
initial={{ y: "100%", opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: '-100%', opacity: 0 }}
exit={{ y: "-100%", opacity: 0 }}
>
комфорт+
</motion.span>
@@ -36,9 +36,9 @@ export function PackageTitle({ slide }: { slide: number }) {
{slide === 13 && (
<motion.span
transition={{ duration: 0.4 }}
initial={{ y: '100%', opacity: 0 }}
initial={{ y: "100%", opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: '-100%', opacity: 0 }}
exit={{ y: "-100%", opacity: 0 }}
>
бизнес
</motion.span>
@@ -48,9 +48,9 @@ export function PackageTitle({ slide }: { slide: number }) {
{slide === 14 && (
<motion.span
transition={{ duration: 0.4 }}
initial={{ y: '100%', opacity: 0 }}
initial={{ y: "100%", opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: '-100%', opacity: 0 }}
exit={{ y: "-100%", opacity: 0 }}
>
премиум
</motion.span>
@@ -60,13 +60,13 @@ export function PackageTitle({ slide }: { slide: number }) {
<motion.div className="flex gap-[0.556vw] justify-center">
<p
className={`px-[0.833vw] py-[0.764vw] rounded-[1.181vw] btnm font-medium relative w-[8.056vw] h-[2.5vw] ${
slide > 12 ? 'bg-gradient' : 'bg-[#B5F54E] text-black'
slide > 12 ? "bg-gradient" : "bg-[#B5F54E] text-black"
}`}
>
<AnimatePresence mode="popLayout">
{slide === 11 && (
<motion.span
className="absolute left-1/2 top-1/2 -translate-1/2"
className="absolute left-1/2 top-1/2 !-translate-x-1/2 !-translate-y-1/2"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
@@ -79,7 +79,7 @@ export function PackageTitle({ slide }: { slide: number }) {
<AnimatePresence mode="popLayout">
{slide === 12 && (
<motion.span
className="absolute left-1/2 top-1/2 -translate-1/2"
className="absolute left-1/2 top-1/2 !-translate-x-1/2 !-translate-y-1/2"
transition={{ duration: 0.4 }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -92,7 +92,7 @@ export function PackageTitle({ slide }: { slide: number }) {
<AnimatePresence mode="popLayout">
{slide === 13 && (
<motion.span
className="absolute left-1/2 top-1/2 -translate-1/2"
className="absolute left-1/2 top-1/2 !-translate-x-1/2 !-translate-y-1/2"
transition={{ duration: 0.4 }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -105,7 +105,7 @@ export function PackageTitle({ slide }: { slide: number }) {
<AnimatePresence mode="popLayout">
{slide === 14 && (
<motion.span
className="absolute left-1/2 top-1/2 -translate-1/2"
className="absolute left-1/2 top-1/2 !-translate-x-1/2 !-translate-y-1/2"
transition={{ duration: 0.4 }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -120,7 +120,7 @@ export function PackageTitle({ slide }: { slide: number }) {
<AnimatePresence mode="popLayout">
{slide === 11 && (
<motion.span
className="absolute left-1/2 top-1/2 -translate-1/2"
className="absolute left-1/2 top-1/2 !-translate-x-1/2 !-translate-y-1/2"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
@@ -133,7 +133,7 @@ export function PackageTitle({ slide }: { slide: number }) {
<AnimatePresence mode="popLayout">
{slide === 12 && (
<motion.span
className="absolute left-1/2 top-1/2 -translate-1/2"
className="absolute left-1/2 top-1/2 !-translate-x-1/2 !-translate-y-1/2"
transition={{ duration: 0.4 }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -146,7 +146,7 @@ export function PackageTitle({ slide }: { slide: number }) {
<AnimatePresence mode="popLayout">
{slide === 13 && (
<motion.span
className="absolute left-1/2 top-1/2 -translate-1/2"
className="absolute left-1/2 top-1/2 !-translate-x-1/2 !-translate-y-1/2"
transition={{ duration: 0.4 }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -159,7 +159,7 @@ export function PackageTitle({ slide }: { slide: number }) {
<AnimatePresence mode="popLayout">
{slide === 14 && (
<motion.span
className="absolute left-1/2 top-1/2 -translate-1/2"
className="absolute left-1/2 top-1/2 !-translate-x-1/2 !-translate-y-1/2"
transition={{ duration: 0.4 }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -1,13 +1,13 @@
import { primeVideos } from '@/consts/presentation/videos';
import { primeProgressItemsTranslates } from '@/consts/primeProgressItemsTranslates';
import { PrimeProgressItem } from '@/ui/PrimeProgressItem';
import { primeVideos } from "@/consts/presentation/videos";
import { primeProgressItemsTranslates } from "@/consts/primeProgressItemsTranslates";
import { PrimeProgressItem } from "@/ui/PrimeProgressItem";
import {
motion,
MotionValue,
useMotionValueEvent,
useTransform,
} from 'framer-motion';
import { useState } from 'react';
} from "framer-motion";
import { useState } from "react";
export function PrimeProgress({
scroll,
@@ -18,7 +18,7 @@ export function PrimeProgress({
}) {
const [slide, setSlide] = useState(0);
useMotionValueEvent(scroll, 'change', (value) =>
useMotionValueEvent(scroll, "change", (value) =>
setSlide(
Math.min(
Math.trunc(value * (primeVideos.length + 9)),
@@ -36,7 +36,7 @@ export function PrimeProgress({
9 / (primeVideos.length + 7),
10 / (primeVideos.length + 7),
],
['0vw', '31.389vw', '31.389vw', '28.794vw', '13.583vw']
["0vw", "31.389vw", "31.389vw", "28.794vw", "13.583vw"]
);
const scale = useTransform(
@@ -49,11 +49,13 @@ export function PrimeProgress({
<motion.div
style={{
bottom,
background: slide > 7 ? 'transparent' : '#37393B99',
background: slide > 7 ? "transparent" : "#37393B99",
scale,
x: "-50%",
y: "-50%",
}}
transition={{ bounce: false }}
className="flex absolute p-[0.556vw] z-10 rounded-[1.875vw] bg-[#37393B99] left-1/2 -translate-1/2 transition-colors"
className="flex absolute p-[0.556vw] z-10 rounded-[1.875vw] bg-[#37393B99] left-1/2 transition-colors"
>
{primeVideos.map(({ src, anchorImg, title }, index) => (
<PrimeProgressItem
@@ -1,7 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import { useModalStore } from '@/stores/useModalStore';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
import { useModalStore } from "@/stores/useModalStore";
import { motion } from "framer-motion";
import { PrimeModal } from "../modals/PrimeModal";
export function ScenarioCard({ slide }: { slide: number }) {
const { setModal } = useModalStore();
@@ -10,7 +10,7 @@ export function ScenarioCard({ slide }: { slide: number }) {
<motion.div
animate={{
opacity: +(slide > 13),
bottom: slide > 13 ? '78.421vh' : '93.684vh',
bottom: slide > 13 ? "78.421vh" : "93.684vh",
}}
onClick={() =>
setModal(
@@ -18,12 +18,12 @@ export function ScenarioCard({ slide }: { slide: number }) {
categoryTitle="Опции"
title="Сценарии проживания"
text="Сценарии проживания — это уже не просто «стены и планировка», а история жизни в новом месте. Такой приём отлично работает на эмоциональном уровне и помогает клиенту представить себя внутри проекта."
packages={['Премиум']}
packages={["Премиум"]}
src="/img/pages/prime/scenarioModal.jpg"
/>
)
}
className="p-[1.389vw] rounded-[1.389vw] -translate-y-[1.389vw] flex items-end gap-4 absolute bg-[#37393B99] w-[15.764vw] h-[15.395vh] right-[1.667vw]"
className="p-[1.389vw] rounded-[1.389vw] -translate-y-[1.389vw] flex items-end gap-4 absolute bg-[#37393B99] w-[15.764vw] h-[15.395vh] right-[1.667vw] cursor-pointer"
>
<p className="btns font-medium">Сценарии проживания</p>
<img
@@ -1,6 +1,6 @@
import { useModalStore } from '@/stores/useModalStore';
import { motion } from 'framer-motion';
import { PrimeModal } from '../modals/PrimeModal';
import { useModalStore } from "@/stores/useModalStore";
import { motion } from "framer-motion";
import { PrimeModal } from "../modals/PrimeModal";
export function SeasonsCard({ slide }: { slide: number }) {
const { setModal } = useModalStore();
@@ -9,7 +9,7 @@ export function SeasonsCard({ slide }: { slide: number }) {
<motion.div
animate={{
opacity: +(slide > 12),
bottom: slide > 12 ? '52.237vh' : '93.684vh',
bottom: slide > 12 ? "52.237vh" : "93.684vh",
}}
onClick={() =>
setModal(
@@ -17,12 +17,12 @@ export function SeasonsCard({ slide }: { slide: number }) {
categoryTitle="Сезонность"
title="Сезонность"
text="Сезонность даёт дополнительную глубину презентации: покупатель видит, как жилой комплекс выглядит осенью под золотыми листьями или зимой, украшенной гирляндами."
packages={['Комфорт+']}
packages={["Комфорт+"]}
src="/videos/pages/prime/seasons.mp4"
/>
)
}
className="p-[1.389vw] rounded-[1.389vw] -translate-y-[1.389vw] flex flex-col justify-end absolute w-[15.486vw] h-[24.605vh] bg-[url(/img/pages/prime/autumn.jpg)] bg-cover bg-no-repeat bg-center right-[1.667vw]"
className="p-[1.389vw] rounded-[1.389vw] -translate-y-[1.389vw] flex flex-col justify-end absolute w-[15.486vw] h-[24.605vh] bg-[url(/img/pages/prime/autumn.jpg)] bg-cover bg-no-repeat bg-center right-[1.667vw] cursor-pointer"
>
<p className="btns font-medium">Сезонность</p>
</motion.div>
@@ -1,8 +1,8 @@
/* 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';
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();
@@ -15,21 +15,21 @@ export function StreamingCard({ slide }: { slide: number }) {
categoryTitle="Удаленная демонстрация"
title="Graff.estate stream"
text="GRAFF.estate разворачивает «облако» и стримит 3D-сцену на устройство клиента (PC, мобильный), менеджер ведёт экскурсию в режиме реального времени."
packages={['Премиум']}
packages={["Премиум"]}
src="/videos/pages/home/streaming.mp4"
/>
)
}
animate={{
opacity: +(slide > 10),
bottom: slide > 10 ? '0vw' : undefined,
bottom: slide > 10 ? "0vw" : undefined,
}}
className="p-[1.389vw] rounded-[1.389vw] -translate-y-[1.389vw] bg-[#37393B99] backdrop-blur-xs absolute right-[1.667vw] w-[8.958vw] h-[20.263vh] flex flex-col justify-between"
>
<img src="/img/pages/prime/ruble.png" className="w-[0.833vw]" alt="" />
<div className="w-[3.333vw] aspect-square rounded-[1.111vw] bg-[#37393B99] relative self-center">
<div className="bg-[#FF4517] w-[0.486vw] aspect-square rounded-full top-[0.556vw] left-[0.556vw] absolute" />
<GraffIcon className="text-white w-[1.944vw] h-[1.944vw] absolute top-1/2 left-1/2 -translate-1/2" />
<GraffIcon className="text-white w-[1.944vw] h-[1.944vw] absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" />
</div>
<p className="btns font-medium">Graff.estate stream</p>
</motion.div>
@@ -1,8 +1,8 @@
/* 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';
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,
@@ -23,7 +23,7 @@ export function PrimeModal({
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">
<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"
@@ -51,8 +51,8 @@ export function PrimeModal({
<p
key={pack}
className={
'px-[0.833vw] py-[0.486vw] rounded-[1.181vw] btns font-medium ' +
(pack === 'Премиум' ? 'bg-gradient' : 'bg-[#37393B99]')
"px-[0.833vw] py-[0.486vw] rounded-[1.181vw] btns font-medium " +
(pack === "Премиум" ? "bg-gradient" : "bg-[#37393B99]")
}
>
{pack}
@@ -60,11 +60,11 @@ export function PrimeModal({
))}
</div>
<div className="gap-y-[0.278vw] flex flex-col flex-1">
{src.endsWith('mp4') ? (
{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]">
)?.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}
@@ -72,7 +72,7 @@ export function PrimeModal({
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]"
className="![rotate:x_4deg] left-1/2 -translate-x-1/2 absolute origin-top object-cover object-bottom w-[24.7vw] h-[11.5vw] top-[2.3vw]"
/>
</div>
) : (
@@ -1,11 +1,11 @@
'use client';
import { AnimatePresence, motion, PanInfo } from 'framer-motion';
import Close from '../../../../public/icons/close.svg';
import Coin from '../../../../public/icons/coin.svg';
import { categories, categoryDescription } from '@/consts/categories';
import { useEffect, useState } from 'react';
import { useModalStore } from '@/stores/useModalStore';
import VideoPrimeModal from './VideoPrimeModal';
"use client";
import { AnimatePresence, motion, PanInfo } from "framer-motion";
import Close from "../../../../public/icons/close.svg";
import Coin from "../../../../public/icons/coin.svg";
import { categories, categoryDescription } from "@/consts/categories";
import { useEffect, useState } from "react";
import { useModalStore } from "@/stores/useModalStore";
import VideoPrimeModal from "./VideoPrimeModal";
const variantsAnimations = {
enter: (direction: number) => {
@@ -60,9 +60,9 @@ function CategoryModal({
}
useEffect(() => {
document.body.style.overflow = 'hidden';
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = 'auto';
document.body.style.overflow = "auto";
};
}, []);
@@ -70,26 +70,26 @@ function CategoryModal({
<div
className={`fixed inset-0 bg-[#0F1011] z-[1] px-2.5 py-4 flex flex-col gap-5 h-full w-full md:p-10 md:gap-10`}
>
<div className='h-12 flex justify-between items-center'>
<div className="h-12 flex justify-between items-center">
<h1
className={`text-[24px] leading-[0.85] tracking-[-0.04em] w-fit md:text-3xl`}
>
{titleCategory}
</h1>
<motion.button
className='w-12 h-12 z-[2] bg-[#37393B99] rounded-2xl flex items-center justify-center md:cursor-pointer'
className="w-12 h-12 z-[2] bg-[#37393B99] rounded-2xl flex items-center justify-center md:cursor-pointer"
onTap={() => setModal(null)}
>
<Close className='w-5 h-5' />
<Close className="w-5 h-5" />
</motion.button>
</div>
<motion.div
key={page}
initial='enter'
animate='center'
exit='exit'
initial="enter"
animate="center"
exit="exit"
custom={direction}
drag='x'
drag="x"
dragConstraints={{ left: 0, right: 0 }}
dragElastic={0.1}
onDragEnd={onDragEnd}
@@ -97,17 +97,17 @@ function CategoryModal({
e.preventDefault();
}}
variants={variantsAnimations}
className='absolute top-20 left-1/2 -translate-x-1/2 w-full flex flex-col items-center md:top-25'
className="absolute top-20 left-1/2 -translate-x-1/2 w-full flex flex-col items-center md:top-25"
>
{description[page].type === 'video' ? (
{description[page].type === "video" ? (
<video
src={description[page].content}
autoPlay
loop
muted
className='md:w-[60%] md:h-[40vh] max-md:w-[94%] max-md:rounded-2xl'
className="md:w-[60%] md:h-[40vh] max-md:w-[94%] max-md:rounded-2xl"
></video>
) : description[page].type === 'videoScreen' ? (
) : description[page].type === "videoScreen" ? (
<VideoPrimeModal src={description[page].content} />
) : (
<div
@@ -115,36 +115,36 @@ function CategoryModal({
></div>
)}
<div className='bg-[#37393B99] rounded-2xl px-5 py-6 flex flex-col gap-3 w-[95%] '>
<h2 className='font-medium text-[20px] leading-[24px] tracking-[-0.02em]'>
<div className="bg-[#37393B99] rounded-2xl px-5 py-6 flex flex-col gap-3 w-[95%] ">
<h2 className="font-medium text-[20px] leading-[24px] tracking-[-0.02em]">
{description[page].title}
</h2>
<div className='md:flex md:gap-2.5 max-md:flex max-md:flex-col max-md:gap-3'>
<p className='font-normal text-[14px] leading-[135%] tracking-[0em] md:flex-1'>
<div className="md:flex md:gap-2.5 max-md:flex max-md:flex-col max-md:gap-3">
<p className="font-normal text-[14px] leading-[135%] tracking-[0em] md:flex-1">
{description[page].text1}
</p>
<p className='font-normal text-[14px] leading-[135%] tracking-[0em] md:flex-1'>
<p className="font-normal text-[14px] leading-[135%] tracking-[0em] md:flex-1">
{description[page].text2}
</p>
</div>
<div className='mt-[32px] flex gap-2'>
<div className='btns w-fit px-3 py-[7px] rounded-[17px] bg-gradient-to-r from-[#6078F2] via-[#7583f3] to-[#C868F5]'>
<div className="mt-[32px] flex gap-2">
<div className="btns w-fit px-3 py-[7px] rounded-[17px] bg-gradient-to-r from-[#6078F2] via-[#7583f3] to-[#C868F5]">
{description[page].package}
</div>
{description[page].isOption && (
<div className='btns w-fit px-3 py-[7px] rounded-[17px] bg-[#37393B99] flex gap-1 items-center'>
<Coin className='w-4 h-4' /> Опция
<div className="btns w-fit px-3 py-[7px] rounded-[17px] bg-[#37393B99] flex gap-1 items-center">
<Coin className="w-4 h-4" /> Опция
</div>
)}
</div>
</div>
</motion.div>
{description.length > 1 && (
<div className='absolute w-full flex justify-center gap-0.5 bottom-4'>
<div className="absolute w-full flex justify-center gap-0.5 bottom-4">
{description.map((item, index) => (
<div
className={`w-2 h-2 rounded-full ${
index === page ? 'bg-white' : 'bg-[#37393B99]'
index === page ? "bg-white" : "bg-[#37393B99]"
}`}
key={index}
></div>
@@ -1,125 +1,125 @@
'use client';
import { $package } from '@/stores/configurator-store/configurationStore';
import { Button } from '@/ui/Button';
import { useUnit } from 'effector-react';
import { motion } from 'framer-motion';
import { getExampleNumber } from 'libphonenumber-js';
import examples from 'libphonenumber-js/mobile/examples';
import Link from 'next/link';
import { useMemo, useState } from 'react';
import ReactInputMask from 'react-input-mask';
import { Country } from 'react-phone-number-input';
"use client";
import { $package } from "@/stores/configurator-store/configurationStore";
import { Button } from "@/ui/Button";
import { useUnit } from "effector-react";
import { motion } from "framer-motion";
import { getExampleNumber } from "libphonenumber-js";
import examples from "libphonenumber-js/mobile/examples";
import Link from "next/link";
import { useMemo, useState } from "react";
import ReactInputMask from "react-input-mask";
import { Country } from "react-phone-number-input";
function PrimeForm() {
const [visible, setVisible] = useState(false);
const { title, cost, time } = useUnit($package);
const [[phoneCode, country], setPhoneCodeAndCountry] = useState<
[string, Country]
>(['+7', 'RU']);
>(["+7", "RU"]);
const placeholder = useMemo(
() =>
getExampleNumber(country, examples)
?.formatInternational()
.split(' ')
.split(" ")
.slice(1)
.join(' '),
.join(" "),
[country]
);
return (
<div
className={`fixed bottom-0 left-1/2 -translate-x-1/2 rounded-2xl p-px z-2 ${
className={`fixed bottom-0 left-1/2 -translate-x-1/2 rounded-2xl p-px z-[2] ${
visible
? 'bg-[#0F101199] backdrop-blur-[16px] w-full h-full rounded-none z-13'
: 'bg-gradient-to-r from-white/10 to-white/0'
? "bg-[#0F101199] backdrop-blur-[16px] w-full h-full rounded-none z-[13]"
: "bg-gradient-to-r from-white/10 to-white/0"
}`}
onClick={() => setVisible(false)}
>
<motion.div
animate={visible ? 'open' : 'closed'}
animate={visible ? "open" : "closed"}
variants={{
open: { width: '95vw', height: '512px', bottom: '10px' },
closed: { width: '240px', height: '50px', bottom: '10px' },
open: { width: "95vw", height: "512px", bottom: "10px" },
closed: { width: "240px", height: "50px", bottom: "10px" },
}}
transition={{
bounce: 'none',
bounce: "none",
}}
onClick={(e) => {
e.stopPropagation();
setVisible(true);
}}
className='absolute left-1/2 -translate-x-1/2 bg-[#37393B99] backdrop-blur-2xl rounded-[15px] flex flex-col gap-4'
className="absolute left-1/2 -translate-x-1/2 bg-[#37393B99] backdrop-blur-2xl rounded-[15px] flex flex-col gap-4"
>
<div
className={`flex justify-center items-center gap-2 ${
visible
? 'px-5 mt-6 md:mt-10 md:justify-start md:ml-10 md:px-0'
: 'px-5 mt-4'
? "px-5 mt-6 md:mt-10 md:justify-start md:ml-10 md:px-0"
: "px-5 mt-4"
}`}
>
<p className='font-[20px] leading-[18px] text-nowrap tracking-[-0.02em]'>
<p className="font-[20px] leading-[18px] text-nowrap tracking-[-0.02em]">
{title}
</p>
<div className='flex gap-1'>
<div className='h-4.5 rounded-[30px] px-2 py-0.5 font-normal text-[10px] leading-[120%] tracking-[-0.01em] bg-[#37393B99] text-nowrap'>
<div className="flex gap-1">
<div className="h-4.5 rounded-[30px] px-2 py-0.5 font-normal text-[10px] leading-[120%] tracking-[-0.01em] bg-[#37393B99] text-nowrap">
~{cost} млн.
</div>
<div className='h-4.5 rounded-[30px] px-2 py-0.5 font-normal text-[10px] text-[#0F1011] leading-[120%] tracking-[-0.01em] bg-[#B5F54E] text-nowrap'>
<div className="h-4.5 rounded-[30px] px-2 py-0.5 font-normal text-[10px] text-[#0F1011] leading-[120%] tracking-[-0.01em] bg-[#B5F54E] text-nowrap">
{time} месяцев
</div>
</div>
</div>
{visible && (
<>
<h1 className='mx-5 h-[90px] font-medium text-[32px] leading-[95%] tracking-[-0.02em] text-center md:mx-10 md:text-left md:w-90 md:h-15'>
<h1 className="mx-5 h-[90px] font-medium text-[32px] leading-[95%] tracking-[-0.02em] text-center md:mx-10 md:text-left md:w-90 md:h-15">
Оставьте контакты, а&nbsp;мы сразу сформируем КП
</h1>
<form action='submit' className='flex flex-col gap-3 md:mx-5'>
<form action="submit" className="md:mx-5 flex flex-col gap-3">
<input
id='name'
autoComplete='none'
type='text'
id="name"
autoComplete="none"
type="text"
required
placeholder='Имя*'
placeholder="Имя*"
// {...register('fullname')}
className='h-[56px] mx-5 bg-transparent border-b border-[#37393B] focus:border-white py-4 rounded-none outline-none transition-all placeholder:btnl btnl placeholder:font-medium placeholder:select-none'
className="h-[56px] mx-5 bg-transparent border-b border-[#37393B] focus:border-white py-4 rounded-none outline-none transition-all placeholder:btnl btnl placeholder:font-medium placeholder:select-none"
/>
<div className='h-[56px] mx-5 flex gap-x-3 py-2 border-[#3D425C] relative'>
<div className="h-[56px] mx-5 flex gap-x-3 py-2 border-[#3D425C] relative">
<ReactInputMask
type='tel'
autoComplete='none'
type="tel"
autoComplete="none"
onChange={() => {}}
id={'tel'}
id={"tel"}
maskChar={null}
mask={'+7 ' + (placeholder?.replace(/\d/g, '9') ?? '')}
placeholder='Телефон*'
className='placeholder:btnl placeholder:font-medium placeholder:select-none peer btnl w-full h-full transition-all bg-transparent rounded-none outline-none'
mask={"+7 " + (placeholder?.replace(/\d/g, "9") ?? "")}
placeholder="Телефон*"
className="placeholder:btnl placeholder:font-medium placeholder:select-none peer btnl w-full h-full transition-all bg-transparent rounded-none outline-none"
/>
<div className=' bottom-0 absolute w-full border-b border-[#37393B] peer-focus:border-white -mb-2' />
<div className=" bottom-0 absolute w-full border-b border-[#37393B] peer-focus:border-white -mb-2" />
</div>
<input
autoComplete='none'
autoComplete="none"
required
id='email'
type='email'
placeholder='E-mail*'
className='h-[56px] mx-5 bg-transparent border-b border-[#37393B] focus:border-white py-4 rounded-none btnl outline-none transition-all placeholder:btnl placeholder:font-medium placeholder:select-none'
id="email"
type="email"
placeholder="E-mail*"
className="h-[56px] mx-5 bg-transparent border-b border-[#37393B] focus:border-white py-4 rounded-none btnl outline-none transition-all placeholder:btnl placeholder:font-medium placeholder:select-none"
/>
<div className='md:flex mx-5 items-stretch lg:gap-[0.833vw] gap-3'>
<div className="md:flex mx-5 items-stretch lg:gap-[0.833vw] gap-3">
<Button
type='submit'
className=' mt-6 btnl max-md:mb-3 max-md:w-full lg:px-[2.222vw] lg:py-[1.389vw] px-8 py-5 cursor-pointer lg:rounded-[1.111vw] rounded-2xl'
type="submit"
className=" mt-6 btnl max-md:mb-3 max-md:w-full lg:px-[2.222vw] lg:py-[1.389vw] px-8 py-5 cursor-pointer lg:rounded-[1.111vw] rounded-2xl"
>
Оставить заявку
</Button>
<Link
href={'/policy'}
className='text2 mt-3 xl:max-w-[60%] md:max-lg:max-w-[40%] md:max-lg:py-1'
href={"/policy"}
className="text2 mt-3 xl:max-w-[60%] md:max-lg:max-w-[40%] md:max-lg:py-1"
>
<p>
<span className='text-[#7A7A7A]'>
<span className="text-[#7A7A7A]">
*Нажимая кнопку отправить, вы принимаете
</span>{' '}
</span>{" "}
условия использования и&nbsp;политику конфиденциальности
</p>
</Link>
+17 -17
View File
@@ -1,9 +1,9 @@
import { search } from '@/consts/presentation/search';
import { useMediaQueries } from '@/hooks/useMediaQueries';
import { motion, MotionValue, useTransform } from 'framer-motion';
import HeartIcon from '../../../public/icons/hearth.svg';
import LocationIcon from '../../../public/icons/location.svg';
import { primeVideos, videos } from '@/consts/presentation/videos';
import { search } from "@/consts/presentation/search";
import { useMediaQueries } from "@/hooks/useMediaQueries";
import { motion, MotionValue, useTransform } from "framer-motion";
import HeartIcon from "../../../public/icons/hearth.svg";
import LocationIcon from "../../../public/icons/location.svg";
import { primeVideos, videos } from "@/consts/presentation/videos";
export function SearchAndSelect({
scrollProgress,
@@ -12,7 +12,7 @@ export function SearchAndSelect({
}: {
scrollProgress: MotionValue<number>;
top?: number;
page?: 'main' | 'prime';
page?: "main" | "prime";
}) {
const opacityMain = useTransform(
scrollProgress,
@@ -29,13 +29,13 @@ export function SearchAndSelect({
const x = useTransform(
scrollProgress,
[1 / (primeVideos.length + 7), 2 / (primeVideos.length + 7)],
['0%', '-100%']
["0%", "-100%"]
);
const y = useTransform(
scrollProgress,
[0 / (primeVideos.length + 7), 1 / (primeVideos.length + 7)],
['100%', '0%']
["100%", "0%"]
);
const opacityMini = useTransform(
@@ -51,16 +51,16 @@ export function SearchAndSelect({
style={
isLg
? {
opacity: page === 'main' ? opacityMain : opacityPrime,
y: page === 'prime' ? y : undefined,
x: page === 'prime' ? x : undefined,
opacity: page === "main" ? opacityMain : opacityPrime,
y: page === "prime" ? y : undefined,
x: page === "prime" ? x : undefined,
}
: { top, opacity: opacityMini }
}
className={`max-md:p-5 flex max-md:flex-col lg:flex-col md:gap-3 max-lg:sticky max-md:gap-y-7 gap-y-3 max-md:rounded-2xl max-md:h-full select-none max-md:[background:radial-gradient(ellipse_at_100%_100%,#7A7A7A33,transparent)] max-md:[backdrop-filter:blur(500px)] lg:absolute ${
page === 'main'
? 'lg:w-[31.875vw] h-full'
: 'lg:w-[23.611vw] h-[40.556vw]'
page === "main"
? "lg:w-[31.875vw] h-full"
: "lg:w-[23.611vw] h-[40.556vw]"
}`}
>
<div className="lg:p-[1.667vw] md:max-lg:p-6 lg:rounded-[1.111vw] md:max-lg:rounded-2xl md:[background:radial-gradient(ellipse_at_100%,#7A7A7A33,transparent)] md:[backdrop-filter:blur(500px)] max-md:space-y-4 md:flex flex-col justify-between md:flex-1">
@@ -70,7 +70,7 @@ export function SearchAndSelect({
<div className="bg-[#FF4517] lg:rounded-[0.625vw] rounded-[9px] lg:p-[0.556vw] p-1.5">
<HeartIcon className="text-[#232425] lg:w-[1.389vw] lg:h-[1.389vw] w-5 h-5" />
</div>
<p className={`text1${page === 'main' ? ' md:max-w-[70%]' : ''}`}>
<p className={`text1${page === "main" ? " md:max-w-[70%]" : ""}`}>
Эмоциональное вовлечение пользователя в выбор квартиры
</p>
</div>
@@ -78,7 +78,7 @@ export function SearchAndSelect({
<div className="bg-[#B5F54E] lg:rounded-[0.625vw] rounded-[9px] lg:p-[0.556vw] p-1.5">
<LocationIcon className="text-[#232425] lg:w-[1.389vw] lg:h-[1.389vw] w-5 h-5" />
</div>
<p className={`text1${page === 'main' ? ' md:max-w-[70%]' : ''}`}>
<p className={`text1${page === "main" ? " md:max-w-[70%]" : ""}`}>
Удобство выбора расположения и видовых характеристик
</p>
</div>
+35 -21
View File
@@ -1,12 +1,12 @@
import LoaderIcon from '../../../public/icons/loader.svg';
import { videos } from '@/consts/presentation/videos';
import LoaderIcon from "../../../public/icons/loader.svg";
import { videos } from "@/consts/presentation/videos";
import {
motion,
MotionValue,
useMotionValueEvent,
useTransform,
} from 'framer-motion';
import { createRef, Fragment, RefObject, useEffect, useState } from 'react';
} from "framer-motion";
import { createRef, Fragment, RefObject, useEffect, useState } from "react";
export function VideoLayerMain({ scroll }: { scroll: MotionValue<number> }) {
const [slide, setSlide] = useState(0);
@@ -30,31 +30,31 @@ export function VideoLayerMain({ scroll }: { scroll: MotionValue<number> }) {
const x = useTransform(
scroll,
[0, 1 / 5, 2 / 5, 3 / 5, 4 / 5, 1],
['32.708vw', '32.708vw', '0vw', '0vw', '30vw', '30vw']
["32.708vw", "32.708vw", "0vw", "0vw", "30vw", "30vw"]
);
const width = useTransform(
scroll,
[0, 1 / 5, 2 / 5, 3 / 5, 4 / 5, 1],
['64.444vw', '61.667vw', '64.444vw', '64.444vw', '61.667vw', '65vw']
["64.444vw", "61.667vw", "64.444vw", "64.444vw", "61.667vw", "65vw"]
);
const backgroundSize = useTransform(
scroll,
[0, 1 / 5, 2 / 5, 3 / 5, 4 / 5, 1],
['48.333vw', '53.962vw', '53.962vw', '62.946vw', '53.962vw', '47.813vw']
["48.333vw", "53.962vw", "53.962vw", "62.946vw", "53.962vw", "47.813vw"]
);
const videoWidth = useTransform(
scroll,
[0, 1 / 5, 2 / 5, 3 / 5, 4 / 5, 1],
['39.822vw', '44.46vw', '44.46vw', '51.861vw', '44.46vw', '39.392vw']
["39.822vw", "44.46vw", "44.46vw", "51.861vw", "44.46vw", "39.392vw"]
);
const videoHeight = useTransform(
scroll,
[0, 1 / 5, 2 / 5, 3 / 5, 4 / 5, 1],
['19vw', '21vw', '21vw', '24.5vw', '21vw', '18.5vw']
["19vw", "21vw", "21vw", "24.5vw", "21vw", "18.5vw"]
);
const opacity = useTransform(scroll, [4.5 / 5, 1], [0, 1]);
@@ -62,22 +62,38 @@ export function VideoLayerMain({ scroll }: { scroll: MotionValue<number> }) {
const backgroundPositionY = useTransform(
scroll,
[0, 1 / 5, 2 / 5, 3 / 5, 4 / 5, 1],
['0vw', '0vw', '0vw', '-2.569vw', '0vw', '7.292vw']
["0vw", "0vw", "0vw", "-2.569vw", "0vw", "7.292vw"]
);
const top = useTransform(
scroll,
[0, 1 / 5, 2 / 5, 3 / 5, 4 / 5, 1],
['3.602vw', '4.022vw', '4.022vw', '2.122vw', '4.022vw', '10.855vw']
["3.602vw", "4.022vw", "4.022vw", "2.122vw", "4.022vw", "10.855vw"]
);
useMotionValueEvent(scroll, 'change', (value) =>
useMotionValueEvent(scroll, "change", (value) =>
setSlide(Math.min(Math.trunc(value * videos.length), videos.length - 1))
);
const [currentBuffering, setCurrentBuffering] = useState(false);
useEffect(() => setCurrentBuffering(true), [slide]);
useEffect(() => {
videoRefs[slide]?.current?.addEventListener("waiting", () =>
setCurrentBuffering(true)
);
videoRefs[slide]?.current?.addEventListener("playing", () =>
setCurrentBuffering(false)
);
return () => {
videoRefs[slide]?.current?.removeEventListener("waiting", () =>
setCurrentBuffering(false)
);
videoRefs[slide]?.current?.removeEventListener("playing", () =>
setCurrentBuffering(false)
);
};
}, [slide, videoRefs]);
return (
<motion.div
@@ -90,7 +106,7 @@ export function VideoLayerMain({ scroll }: { scroll: MotionValue<number> }) {
}}
className="absolute h-full overflow-hidden bg-[url(/img/pages/home/presentation/touch_screen.png)] bg-cover bg-top bg-no-repeat"
onViewportEnter={handleOnViewportFeatureEnter}
viewport={{ margin: '-10% 0% 0% 0%', once: true }}
viewport={{ margin: "-10% 0% 0% 0%", once: true }}
>
{!!videoRefs.length &&
videos.map(({ src }, index) => (
@@ -106,12 +122,10 @@ 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`
: ''
: ""
}
style={{
zIndex: videos.length - index,
@@ -123,8 +137,8 @@ export function VideoLayerMain({ scroll }: { scroll: MotionValue<number> }) {
loop
muted
playsInline
className={`object-bottom object-cover origin-top absolute left-1/2 -translate-x-1/2 !rotate-x-[4deg] transition-[opacity,transform]${
slide > index ? ' opacity-0' : ''
className={`object-bottom object-cover origin-top absolute left-1/2 -translate-x-1/2 [rotate:x_4deg] transition-[opacity,transform]${
slide > index ? " opacity-0" : ""
}`}
/>
<motion.div
@@ -134,7 +148,7 @@ export function VideoLayerMain({ scroll }: { scroll: MotionValue<number> }) {
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"
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,
}}
@@ -145,7 +159,7 @@ export function VideoLayerMain({ scroll }: { scroll: MotionValue<number> }) {
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="w-full h-[5.556vw] bg-gradient-to-t from-[#0F1011] absolute -bottom-1"
className="w-full h-[5.556vw] bg-gradient-to-t from-[#0F1011] absolute -bottom-0"
/>
</Fragment>
))}
+35 -26
View File
@@ -1,12 +1,12 @@
import LoaderIcon from '../../../public/icons/loader.svg';
import { primeVideos } from '@/consts/presentation/videos';
import LoaderIcon from "../../../public/icons/loader.svg";
import { primeVideos } from "@/consts/presentation/videos";
import {
motion,
MotionValue,
useMotionValueEvent,
useTransform,
} from 'framer-motion';
import { createRef, RefObject, useEffect, useState } from 'react';
} from "framer-motion";
import { createRef, Fragment, RefObject, useEffect, useState } from "react";
export function VideoLayerPrime({ scroll }: { scroll: MotionValue<number> }) {
const [slide, setSlide] = useState(0);
@@ -22,7 +22,7 @@ export function VideoLayerPrime({ scroll }: { scroll: MotionValue<number> }) {
videoRefs[slide].current.play();
}, [slide, videoRefs]);
useMotionValueEvent(scroll, 'change', (value) =>
useMotionValueEvent(scroll, "change", (value) =>
setSlide(
Math.min(
Math.trunc(value * (primeVideos.length + 9)),
@@ -41,7 +41,7 @@ export function VideoLayerPrime({ scroll }: { scroll: MotionValue<number> }) {
const y = useTransform(
scroll,
[6 / (primeVideos.length + 7), 7 / (primeVideos.length + 7)],
['0%', '-100%']
["0%", "-100%"]
);
const x = useTransform(
@@ -55,7 +55,7 @@ export function VideoLayerPrime({ scroll }: { scroll: MotionValue<number> }) {
5 / (primeVideos.length + 7),
6 / (primeVideos.length + 7),
],
['32.708vw', '32.708vw', '0vw', '0vw', '30vw', '30vw', '0vw']
["32.708vw", "32.708vw", "0vw", "0vw", "30vw", "30vw", "0vw"]
);
const width = useTransform(
@@ -68,7 +68,7 @@ export function VideoLayerPrime({ scroll }: { scroll: MotionValue<number> }) {
4 / (primeVideos.length + 7),
5 / (primeVideos.length + 7),
],
['64.444vw', '61.667vw', '64.444vw', '64.444vw', '61.667vw', '65vw']
["64.444vw", "61.667vw", "64.444vw", "64.444vw", "61.667vw", "65vw"]
);
const backgroundSize = useTransform(
@@ -81,7 +81,7 @@ export function VideoLayerPrime({ scroll }: { scroll: MotionValue<number> }) {
4 / (primeVideos.length + 7),
5 / (primeVideos.length + 7),
],
['48.333vw', '53.962vw', '53.962vw', '62.946vw', '47.813vw', '53.962vw']
["48.333vw", "53.962vw", "53.962vw", "62.946vw", "47.813vw", "53.962vw"]
);
const videoWidth = useTransform(
@@ -94,7 +94,7 @@ export function VideoLayerPrime({ scroll }: { scroll: MotionValue<number> }) {
4 / (primeVideos.length + 7),
5 / (primeVideos.length + 7),
],
['39.822vw', '44.46vw', '44.46vw', '51.861vw', '39.392vw', '44.46vw']
["39.822vw", "44.46vw", "44.46vw", "51.861vw", "39.392vw", "44.46vw"]
);
const opacity = useTransform(
@@ -117,7 +117,7 @@ export function VideoLayerPrime({ scroll }: { scroll: MotionValue<number> }) {
4 / (primeVideos.length + 7),
5 / (primeVideos.length + 7),
],
['19vw', '21vw', '21vw', '24.5vw', '18.5vw', '21vw']
["19vw", "21vw", "21vw", "24.5vw", "18.5vw", "21vw"]
);
const backgroundPositionY = useTransform(
@@ -130,7 +130,7 @@ export function VideoLayerPrime({ scroll }: { scroll: MotionValue<number> }) {
4 / (primeVideos.length + 7),
5 / (primeVideos.length + 7),
],
['0vw', '0vw', '0vw', '-2.569vw', '7.292vw', '0vw']
["0vw", "0vw", "0vw", "-2.569vw", "7.292vw", "0vw"]
);
const top = useTransform(
@@ -143,14 +143,25 @@ export function VideoLayerPrime({ scroll }: { scroll: MotionValue<number> }) {
4 / (primeVideos.length + 7),
5 / (primeVideos.length + 7),
],
['3.602vw', '4.022vw', '4.022vw', '2.122vw', '10.855vw', '4.022vw']
["3.602vw", "4.022vw", "4.022vw", "2.122vw", "10.855vw", "4.022vw"]
);
const [currentBuffering, setCurrentBuffering] = useState(false);
useEffect(() => {
setCurrentBuffering(true);
}, [slide]);
if (!videoRefs[slide] || !videoRefs[slide].current) return;
const video = videoRefs[slide].current;
video.addEventListener("waiting", () => setCurrentBuffering(true));
videoRefs[slide].current.addEventListener("playing", () =>
setCurrentBuffering(false)
);
return () => {
video.removeEventListener("waiting", () => setCurrentBuffering(false));
video.removeEventListener("playing", () => setCurrentBuffering(false));
};
}, [slide, videoRefs]);
return (
<motion.div
@@ -162,14 +173,14 @@ export function VideoLayerPrime({ scroll }: { scroll: MotionValue<number> }) {
backgroundSize,
perspective: videoHeight,
}}
viewport={{ margin: '-10% 0% 0% 0%', once: true }}
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"
>
{!!videoRefs.length &&
primeVideos.map(({ src }, index) =>
src ? (
<>
<Fragment key={index}>
{index === slide && slide > 2 && (
<motion.p
style={{ opacity }}
@@ -184,7 +195,7 @@ export function VideoLayerPrime({ scroll }: { scroll: MotionValue<number> }) {
src={
isViewportEntered
? `/videos/pages/home/presentation/${src}.mp4`
: ''
: ""
}
ref={videoRefs[index]}
loop
@@ -196,12 +207,10 @@ 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${
className={`absolute w-[39.822vw] h-[19vw] origin-top object-bottom object-cover top-[3.602vw] ![rotate:x_4deg] left-1/2 -translate-x-1/2 transition-opacity${
slide > index && slide < primeVideos.length
? ' opacity-0'
: ''
? " opacity-0"
: ""
}`}
/>
<motion.div
@@ -211,7 +220,7 @@ export function VideoLayerPrime({ scroll }: { scroll: MotionValue<number> }) {
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"
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,
}}
@@ -220,13 +229,13 @@ export function VideoLayerPrime({ scroll }: { scroll: MotionValue<number> }) {
<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" />
</>
</Fragment>
) : (
<div
key={index}
style={{ zIndex: primeVideos.length + 7 - index }}
className={`inset-0 transition-opacity${
slide > index ? ' opacity-0' : ''
slide > index ? " opacity-0" : ""
}`}
/>
)
+70 -70
View File
@@ -5,175 +5,175 @@ export type SourceImageCategory = {
};
export interface ImagesCategories {
Оборудование: SourceImageCategory[];
'Детальная проработка окружения': SourceImageCategory[];
'Дизайн интерьеров': SourceImageCategory[];
'Рекламные материалы': SourceImageCategory[];
"Детальная проработка окружения": SourceImageCategory[];
"Дизайн интерьеров": SourceImageCategory[];
"Рекламные материалы": SourceImageCategory[];
Опции: SourceImageCategory[];
Сезонность: SourceImageCategory[];
'Удаленная демонстрация': SourceImageCategory[];
"Удаленная демонстрация": SourceImageCategory[];
}
export const imagesCategories: ImagesCategories = {
Оборудование: [
{
className:
'bg-no-repeat bg-cover bg-[left_4px_top_2px] bg-[url(/img/pages/prime/wallPanel.png)] md:bg-[length:90%] md:bg-contain md:bg-top',
"bg-no-repeat bg-cover bg-[left_4px_top_2px] bg-[url(/img/pages/prime/wallPanel.png)] md:bg-[length:90%] md:bg-contain md:bg-top",
source: [],
childrenClassName: '',
childrenClassName: "",
},
{
className:
'bg-no-repeat bg-cover bg-[left_6px_top_6px] bg-[url(/img/pages/prime/brandTablet800.png)] md:bg-[length:90%] md:bg-contain md:bg-center',
"bg-no-repeat bg-cover bg-[left_6px_top_6px] bg-[url(/img/pages/prime/brandTablet800.png)] md:bg-[length:90%] md:bg-contain md:bg-center",
source: [],
childrenClassName: '',
childrenClassName: "",
},
{
className:
'bg-no-repeat bg-cover bg-[left_6px_top_6px] bg-[url(/img/pages/prime/brandTablet800.png)] md:bg-[length:90%] md:bg-contain md:bg-center',
"bg-no-repeat bg-cover bg-[left_6px_top_6px] bg-[url(/img/pages/prime/brandTablet800.png)] md:bg-[length:90%] md:bg-contain md:bg-center",
source: [],
childrenClassName: '',
childrenClassName: "",
},
],
'Детальная проработка окружения': [
"Детальная проработка окружения": [
{
className: '',
className: "",
source: [],
childrenClassName: 'w-14 h-14 rounded-full top-2.5',
childrenClassName: "w-14 h-14 rounded-full top-2.5",
},
],
'Дизайн интерьеров': [
"Дизайн интерьеров": [
{
className: '',
source: ['designInterior1.jpg'],
className: "",
source: ["designInterior1.jpg"],
childrenClassName:
'bg-no-repeat bg-cover bg-center bg-[url(/img/pages/prime/designInterior1.jpg)]',
"bg-no-repeat bg-cover bg-center bg-[url(/img/pages/prime/designInterior1.jpg)]",
},
{
className: '',
source: ['designInterior2.jpg'],
className: "",
source: ["designInterior2.jpg"],
childrenClassName:
'bg-no-repeat bg-cover bg-center bg-[url(/img/pages/prime/designInterior2.jpg)]',
"bg-no-repeat bg-cover bg-center bg-[url(/img/pages/prime/designInterior2.jpg)]",
},
{
className: '',
source: ['designInterior3.jpg'],
className: "",
source: ["designInterior3.jpg"],
childrenClassName:
'bg-no-repeat bg-cover bg-center bg-[url(/img/pages/prime/designInterior3.jpg)]',
"bg-no-repeat bg-cover bg-center bg-[url(/img/pages/prime/designInterior3.jpg)]",
},
{
className: '',
className: "",
source: [
'designInterior4.jpg',
'designInterior4.jpg',
'designInterior4.jpg',
"designInterior4.jpg",
"designInterior4.jpg",
"designInterior4.jpg",
],
childrenClassName:
'bg-no-repeat bg-cover bg-center bg-[url(/img/pages/prime/designInterior4.jpg)]',
"bg-no-repeat bg-cover bg-center bg-[url(/img/pages/prime/designInterior4.jpg)]",
},
],
'Рекламные материалы': [
"Рекламные материалы": [
{
className:
'bg-no-repeat bg-contain bg-[top_8px_center] bg-[url(/img/pages/prime/architecture.png)] md:bg-center_top',
source: [''],
childrenClassName: '',
"bg-no-repeat bg-contain bg-[top_8px_center] bg-[url(/img/pages/prime/architecture.png)] md:bg-center_top",
source: [""],
childrenClassName: "",
},
{
className:
'bg-no-repeat bg-[length:57px_112px] bg-center bg-[url(/img/pages/prime/phone.png)] md:bg-[length:70px_140px]',
source: [''],
childrenClassName: '',
"bg-no-repeat bg-[length:57px_112px] bg-center bg-[url(/img/pages/prime/phone.png)] md:bg-[length:70px_140px]",
source: [""],
childrenClassName: "",
},
],
Опции: [
{
className: '',
source: ['scenario.png'],
childrenClassName: 'w-34 h-19.25 top-1/2 -translate-y-1/2',
className: "",
source: ["scenario.png"],
childrenClassName: "w-34 h-19.25 top-1/2 -translate-y-1/2",
},
{
className: '',
source: ['avatar.png'],
childrenClassName: 'w-20 h-20 top-2.5 rounded-full',
className: "",
source: ["avatar.png"],
childrenClassName: "w-20 h-20 top-2.5 rounded-full",
},
{
className: '',
source: ['configuratorInterier1.jpg', 'configuratorInterier2.jpg'],
className: "",
source: ["configuratorInterier1.jpg", "configuratorInterier2.jpg"],
childrenClassName:
'w-16 h-16 mt-5 rounded-full [&:nth-child(1)]:translate-x-[-100%] [&:nth-child(2)]:translate-x-[0%]',
"w-16 h-16 mt-5 rounded-full [&:nth-child(1)]:translate-x-[-100%] [&:nth-child(2)]:translate-x-[0%]",
},
{
className:
'bg-no-repeat bg-[length:60%] bg-center bg-[top_29px_left_21px] bg-[url(/img/pages/prime/vr.png)]',
"bg-no-repeat bg-[length:60%] bg-center bg-[top_29px_left_21px] bg-[url(/img/pages/prime/vr.png)]",
source: [],
childrenClassName: '',
childrenClassName: "",
},
{
className:
'bg-no-repeat z-1 bg-[24px_35px] bg-[url(/img/pages/prime/finance.png)] bg-no-repeat bg-contain before:absolute before:top-[35px] before:left-[24px] before:right-0 before:bottom-0 before:-z-10 before:bg-gradient-to-b before:from-[rgba(39,40,42,0)] before:via-[rgba(39,40,42,0.807)] before:to-[#27282A] before:content-[""] before:rounded-[13px] before:rounded-l-none before:rounded-t-none md:bg-[right_0px_top_35px]',
'bg-no-repeat z-[1] bg-[24px_35px] bg-[url(/img/pages/prime/finance.png)] bg-no-repeat bg-contain before:absolute before:top-[35px] before:left-[24px] before:right-0 before:bottom-0 before:-z-10 before:bg-gradient-to-b before:from-[rgba(39,40,42,0)] before:via-[rgba(39,40,42,0.807)] before:to-[#27282A] before:content-[""] before:rounded-[13px] before:rounded-l-none before:rounded-t-none md:bg-[right_0px_top_35px]',
source: [],
childrenClassName: '',
childrenClassName: "",
},
{
className:
'bg-[left_20%_top_1px] bg-no-repeat bg-contain bg-center bg-[url(/img/pages/prime/interactiveWindow.png)]',
"bg-[left_20%_top_1px] bg-no-repeat bg-contain bg-center bg-[url(/img/pages/prime/interactiveWindow.png)]",
source: [],
childrenClassName: '',
childrenClassName: "",
},
{
className: '',
className: "",
source: [
'moduleEngineer1.jpg',
'moduleEngineer2.jpg',
'moduleEngineer3.jpg',
"moduleEngineer1.jpg",
"moduleEngineer2.jpg",
"moduleEngineer3.jpg",
],
childrenClassName: 'w-14 h-14 rounded-full top-8',
childrenClassName: "w-14 h-14 rounded-full top-8",
},
{
className:
'bg-no-repeat z-1 bg-[length:140%] bg-[url(/img/pages/prime/wheel.png)] before:absolute before:top-[35px] before:left-[24px] before:right-0 before:bottom-0 before:-z-10 before:bg-gradient-to-b before:from-[rgba(39,40,42,0)] before:via-[rgba(39,40,42,0.807)] before:to-[#27282A] before:content-[""] before:rounded-2xl',
'bg-no-repeat z-[1] bg-[length:140%] bg-[url(/img/pages/prime/wheel.png)] before:absolute before:top-[35px] before:left-[24px] before:right-0 before:bottom-0 before:-z-10 before:bg-gradient-to-b before:from-[rgba(39,40,42,0)] before:via-[rgba(39,40,42,0.807)] before:to-[#27282A] before:content-[""] before:rounded-2xl',
source: [],
childrenClassName: '',
childrenClassName: "",
},
{
className:
'bg-[left_11%_top_1px]m bg-top bg-no-repeat bg-[length:77%] bg-[url(/img/pages/prime/analyse.png)]',
"bg-[left_11%_top_1px]m bg-top bg-no-repeat bg-[length:77%] bg-[url(/img/pages/prime/analyse.png)]",
source: [],
childrenClassName: '',
childrenClassName: "",
},
],
Сезонность: [
{
className:
'bg-no-repeat bg-cover bg-center bg-[url(/img/pages/prime/summer.jpg)]',
"bg-no-repeat bg-cover bg-center bg-[url(/img/pages/prime/summer.jpg)]",
source: [],
childrenClassName: '',
childrenClassName: "",
},
{
className:
'bg-no-repeat bg-cover bg-center bg-[url(/img/pages/prime/winter.jpg)]',
"bg-no-repeat bg-cover bg-center bg-[url(/img/pages/prime/winter.jpg)]",
source: [],
childrenClassName: '',
childrenClassName: "",
},
{
className:
'bg-no-repeat bg-cover bg-center bg-[url(/img/pages/prime/spring.jpg)]',
"bg-no-repeat bg-cover bg-center bg-[url(/img/pages/prime/spring.jpg)]",
source: [],
childrenClassName: '',
childrenClassName: "",
},
{
className:
'bg-no-repeat bg-cover bg-center bg-[url(/img/pages/prime/autumn.jpg)]',
"bg-no-repeat bg-cover bg-center bg-[url(/img/pages/prime/autumn.jpg)]",
source: [],
childrenClassName: '',
childrenClassName: "",
},
],
'Удаленная демонстрация': [
"Удаленная демонстрация": [
{
className:
'bg-no-repeat bg-[length:298px_169px] h-[219px] bg-center bg-[url(/img/pages/home/motivation/remote_demo.png)]',
"bg-no-repeat bg-[length:298px_169px] h-[219px] bg-center bg-[url(/img/pages/home/motivation/remote_demo.png)]",
source: [],
childrenClassName: '',
childrenClassName: "",
},
],
};
+3 -3
View File
@@ -1,4 +1,4 @@
import { RefObject, useCallback, useEffect, useState } from 'react';
import { RefObject, useCallback, useEffect, useState } from "react";
export function useScroll(ref: RefObject<HTMLElement>) {
const [scroll, setScroll] = useState(
@@ -11,9 +11,9 @@ export function useScroll(ref: RefObject<HTMLElement>) {
}, [ref]);
useEffect(() => {
document.addEventListener('scroll', handleScroll);
document.addEventListener("scroll", handleScroll);
return () => document.removeEventListener('scroll', handleScroll);
return () => document.removeEventListener("scroll", handleScroll);
}, [handleScroll, ref]);
return scroll;
+3 -3
View File
@@ -1,13 +1,13 @@
import { useLayoutEffect, useState } from 'react';
import { useLayoutEffect, useState } from "react";
export function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useLayoutEffect(() => {
const updateWidth = () => setWidth(window.innerWidth);
window.addEventListener('resize', updateWidth);
window.addEventListener("resize", updateWidth);
updateWidth();
return () => window.removeEventListener('resize', updateWidth);
return () => window.removeEventListener("resize", updateWidth);
}, []);
return width;
+39 -39
View File
@@ -1,55 +1,55 @@
'use client';
"use client";
import { Editor, IAllProps } from '@tinymce/tinymce-react';
import { Editor, IAllProps } from "@tinymce/tinymce-react";
// TinyMCE so the global var exists
import 'tinymce/tinymce';
import "tinymce/tinymce";
// DOM model
import 'tinymce/models/dom/model';
import "tinymce/models/dom/model";
// Theme
import 'tinymce/themes/silver';
import "tinymce/themes/silver";
// Toolbar icons
import 'tinymce/icons/default';
import "tinymce/icons/default";
// Editor styles
import 'tinymce/skins/ui/oxide/skin';
import "tinymce/skins/ui/oxide/skin";
// importing the plugin js.
// if you use a plugin that is not listed here the editor will fail to load
import 'tinymce/plugins/advlist';
import 'tinymce/plugins/anchor';
import 'tinymce/plugins/autolink';
import 'tinymce/plugins/autoresize';
import 'tinymce/plugins/autosave';
import 'tinymce/plugins/charmap';
import 'tinymce/plugins/code';
import 'tinymce/plugins/codesample';
import 'tinymce/plugins/directionality';
import 'tinymce/plugins/emoticons';
import 'tinymce/plugins/fullscreen';
import 'tinymce/plugins/help';
import 'tinymce/plugins/help/js/i18n/keynav/en';
import 'tinymce/plugins/image';
import 'tinymce/plugins/importcss';
import 'tinymce/plugins/insertdatetime';
import 'tinymce/plugins/link';
import 'tinymce/plugins/lists';
import 'tinymce/plugins/media';
import 'tinymce/plugins/nonbreaking';
import 'tinymce/plugins/pagebreak';
import 'tinymce/plugins/preview';
import 'tinymce/plugins/quickbars';
import 'tinymce/plugins/save';
import 'tinymce/plugins/searchreplace';
import 'tinymce/plugins/table';
import 'tinymce/plugins/visualblocks';
import 'tinymce/plugins/visualchars';
import 'tinymce/plugins/wordcount';
import "tinymce/plugins/advlist";
import "tinymce/plugins/anchor";
import "tinymce/plugins/autolink";
import "tinymce/plugins/autoresize";
import "tinymce/plugins/autosave";
import "tinymce/plugins/charmap";
import "tinymce/plugins/code";
import "tinymce/plugins/codesample";
import "tinymce/plugins/directionality";
import "tinymce/plugins/emoticons";
import "tinymce/plugins/fullscreen";
import "tinymce/plugins/help";
import "tinymce/plugins/help/js/i18n/keynav/en";
import "tinymce/plugins/image";
import "tinymce/plugins/importcss";
import "tinymce/plugins/insertdatetime";
import "tinymce/plugins/link";
import "tinymce/plugins/lists";
import "tinymce/plugins/media";
import "tinymce/plugins/nonbreaking";
import "tinymce/plugins/pagebreak";
import "tinymce/plugins/preview";
import "tinymce/plugins/quickbars";
import "tinymce/plugins/save";
import "tinymce/plugins/searchreplace";
import "tinymce/plugins/table";
import "tinymce/plugins/visualblocks";
import "tinymce/plugins/visualchars";
import "tinymce/plugins/wordcount";
// importing plugin resources
import 'tinymce/plugins/emoticons/js/emojis';
import "tinymce/plugins/emoticons/js/emojis";
// Content styles, including inline UI like fake cursors
import 'tinymce/skins/content/default/content';
import 'tinymce/skins/ui/oxide/content';
import "tinymce/skins/content/default/content";
import "tinymce/skins/ui/oxide/content";
export function BundledEditor(props: IAllProps) {
return <Editor licenseKey="gpl" {...props} />;
+23 -5
View File
@@ -1,11 +1,20 @@
'use client';
"use client";
import ReactLenis, { LenisRef } from 'lenis/react';
import { usePathname } from 'next/navigation';
import { PropsWithChildren, useEffect, useRef } from 'react';
import ReactLenis, { Lenis, LenisRef } from "lenis/react";
import { usePathname } from "next/navigation";
import {
createRef,
PropsWithChildren,
useEffect,
useRef,
useState,
} from "react";
import { useModalStore } from "@/stores/useModalStore";
export function LenisProvider({ children }: PropsWithChildren) {
const lenis = useRef<LenisRef>(null);
const { modal } = useModalStore();
const [lenisKey, setLenisKey] = useState(0);
const pathname = usePathname();
@@ -25,9 +34,18 @@ export function LenisProvider({ children }: PropsWithChildren) {
[pathname]
);
useEffect(() => {
if (modal) {
lenis.current?.lenis?.destroy();
} else {
setLenisKey((prev) => prev + 1);
}
}, [modal]);
return (
<ReactLenis
root={!pathname.startsWith('/blog/')}
key={lenisKey}
root={!pathname.startsWith("/blog/")}
className="relative"
options={{ autoRaf: false, overscroll: false }}
ref={lenis}
+1
View File
@@ -0,0 +1 @@
View File
View File
View File
+16 -16
View File
@@ -1,7 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import { AnimatePresence, motion } from 'framer-motion';
import { Dispatch, SetStateAction, useEffect, useRef } from 'react';
import { useHover } from 'usehooks-ts';
import { AnimatePresence, motion } from "framer-motion";
import { Dispatch, SetStateAction, useEffect, useRef } from "react";
import { useHover } from "usehooks-ts";
export function PrimeProgressItem({
active,
@@ -40,26 +40,26 @@ export function PrimeProgressItem({
return (
<motion.div
animate={canAnimate ? { x, y, rotateZ, zIndex } : {}}
transition={{ bounce: 'none' }}
transition={{ bounce: "none" }}
className="group max-lg:hidden flex items-center z-10 relative"
>
<motion.div
animate={{ opacity: +!canAnimate }}
className={
'aspect-[12/1] w-[0.833vw] group-first:hidden ' +
"aspect-[12/1] w-[0.833vw] group-first:hidden " +
(!active
? 'bg-[#37393B]'
: 'bg-gradient-to-r from-[#37393B] to-white')
? "bg-[#37393B]"
: "bg-gradient-to-r from-[#37393B] to-white")
}
/>
<motion.div
onClick={onClick}
animate={{
borderColor: canAnimate
? '#37393B00'
? "#37393B00"
: !active
? '#37393B'
: '#FFFFFF',
? "#37393B"
: "#FFFFFF",
}}
ref={ref}
className="p-[0.278vw] border relative cursor-pointer rounded-[1.111vw]"
@@ -73,10 +73,10 @@ export function PrimeProgressItem({
<motion.div
animate={{ opacity: +!canAnimate }}
className={
'aspect-[12/1] w-[0.833vw] group-last:hidden transition-colors ' +
"aspect-[12/1] w-[0.833vw] group-last:hidden transition-colors " +
(!active
? 'bg-[#37393B]'
: 'bg-gradient-to-r from-white to-[#37393B]')
? "bg-[#37393B]"
: "bg-gradient-to-r from-white to-[#37393B]")
}
/>
<AnimatePresence>
@@ -88,10 +88,10 @@ export function PrimeProgressItem({
y: active ? 0 : 100,
}}
exit={{ opacity: 0, y: 100 }}
transition={{ bounce: 'none' }}
className="flex flex-col items-center gap-[0.278vw] absolute -top-1/2 left-1/2 min-w-full -z-5 -translate-x-1/2 group-last:translate-x-[calc(-50%+0.4165vw)] group-first:translate-x-[calc(-50%-0.4165vw)]"
transition={{ bounce: "none" }}
className="flex flex-col items-center gap-[0.278vw] absolute -top-1/2 left-1/2 min-w-full -z-[5] !-translate-x-1/2 group-last:![transform:translateX(calc(-50%_+_0.4165vw))] group-first:![transform:translateX(calc(-50%_-_0.4165vw))]"
>
<p className="btnm font-medium text-centera text-nowrap">{title}</p>
<p className="btnm font-medium text-nowrap">{title}</p>
<motion.div className="w-[0.069vw] h-[0.556vw] rounded-[0.139vw] bg-[#7A7A7A]" />
</motion.div>
)}
+7 -7
View File
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import MuteIcon from '../../public/icons/mute.svg';
import UnmuteIcon from '../../public/icons/unmute.svg';
import { useEffect, useRef, useState } from "react";
import MuteIcon from "../../public/icons/mute.svg";
import UnmuteIcon from "../../public/icons/unmute.svg";
export function VideoMutingBtn({
handleClick,
@@ -15,18 +15,18 @@ export function VideoMutingBtn({
useEffect(() => {
const el = ref.current;
el?.addEventListener('mousemove', (e) => setPoint([e.clientX, e.clientY]));
el?.addEventListener("mousemove", (e) => setPoint([e.clientX, e.clientY]));
return () => {
el?.removeEventListener('mousemove', (e) =>
el?.removeEventListener("mousemove", (e) =>
setPoint([e.clientX, e.clientY])
);
};
}, []);
return (
<div className="absolute left-0 top-0 h-5/6 w-full z-7">
<div ref={ref} className="relative w-full h-full group">
<div className="absolute left-0 top-0 h-5/6 w-full z-[7]">
<div ref={ref} className="group relative w-full h-full">
<button
className="bg-[#37393B99] p-[1.736vw] [backdrop-filter:blur(30.72px)] rounded-full group-hover:opacity-100 transition-opacity group-hover:cursor-none opacity-0 sticky outline-none"
style={{ left: point[0] - 32, top: point[1] - 32 }}
+15
View File
@@ -0,0 +1,15 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
theme: {
screens: {
xs: "360px",
sm: "640px",
md: "768px",
lg: "1440px",
"2xl": "1536px",
},
extend: {},
},
plugins: [],
};
-63
View File
@@ -1,63 +0,0 @@
import fs from 'fs';
import postcss from 'postcss';
import { type Config } from 'tailwindcss';
const config: Config = {
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
theme: {
extend: {
// fontSize: {
// line1: 'line1',
// line2: 'line2',
// },
screens: {
'desktop-figma': '1600px',
},
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic':
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
},
animation: {
'infinite-scroll': 'infinite-scroll 45s linear infinite',
'highlight-product': 'highlight-product 0.1s ease-in 0s',
},
keyframes: {
'infinite-scroll': {
from: { transform: 'translateX(0%)' },
to: { transform: 'translateX(-100%)' },
},
'highlight-product': {
'100%': {
backgroundImage: 'url(/img/components/products/highlight.svg)',
},
},
scaling: {
'0%': {
transform: 'min-width 31.6vw min-height 31.8vw',
transition: 'transform 500ms',
},
'100%': {
transform: 'min-width 48vw min-height 48vw',
},
},
},
},
},
plugins: [
function ({ addBase }: { addBase: any }) {
const preflightStyles = postcss.parse(
fs.readFileSync(
require.resolve('tailwindcss/lib/css/preflight.css'),
'utf8'
)
);
preflightStyles.walkRules((rule) => {
rule.selector = '.no-tailwind-base ' + rule.selector;
});
addBase(preflightStyles.nodes);
},
],
};
export default config;