diff --git a/_tailwind.config.js b/_tailwind.config.js new file mode 100644 index 00000000..30b4fb36 --- /dev/null +++ b/_tailwind.config.js @@ -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); + }, + ], +}; diff --git a/bun.lockb b/bun.lockb index a131a215..4da90ff2 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/graff.estate.conf b/graff.estate.conf new file mode 100644 index 00000000..2562ce02 --- /dev/null +++ b/graff.estate.conf @@ -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; + } +} \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index 3e01bd75..43c580da 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -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: "", }, ], }, diff --git a/package.json b/package.json index a1a9ac3d..591382f6 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 00000000..33ad091d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/postcss.config.mjs b/postcss.config.mjs deleted file mode 100644 index 5d6d8457..00000000 --- a/postcss.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('postcss-load-config').Config} */ -const config = { - plugins: { - '@tailwindcss/postcss': {}, - }, -}; - -export default config; diff --git a/src/app/(main)/about/page.tsx b/src/app/(main)/about/page.tsx index 03aada0a..ef116ae0 100644 --- a/src/app/(main)/about/page.tsx +++ b/src/app/(main)/about/page.tsx @@ -1,4 +1,4 @@ -import { InProcess } from '@/components/pages/InProcess'; +import { InProcess } from "@/components/pages/InProcess"; export default function AboutPage() { return ; diff --git a/src/app/(main)/blog/[slug]/layout.tsx b/src/app/(main)/blog/[slug]/layout.tsx index 3460a237..91c84135 100644 --- a/src/app/(main)/blog/[slug]/layout.tsx +++ b/src/app/(main)/blog/[slug]/layout.tsx @@ -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({ - queryKey: ['articles', slug], + queryKey: ["articles", slug], queryFn: async () => await api.get(`articles/${slug}`).json(), }); @@ -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 (
- + {children} diff --git a/src/app/(main)/blog/[slug]/page.tsx b/src/app/(main)/blog/[slug]/page.tsx index 54a06adc..e6d76c7c 100644 --- a/src/app/(main)/blog/[slug]/page.tsx +++ b/src/app/(main)/blog/[slug]/page.tsx @@ -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(), }) ); @@ -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({ - queryKey: ['articles'], - queryFn: () => api.get('articles').json(), + queryKey: ["articles"], + queryFn: () => api.get("articles").json(), }); return articles diff --git a/src/app/(main)/blog/page.tsx b/src/app/(main)/blog/page.tsx index b15e0013..97fa346e 100644 --- a/src/app/(main)/blog/page.tsx +++ b/src/app/(main)/blog/page.tsx @@ -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(), + queryKey: ["articles"], + queryFn: () => api.get("articles").json(), }); return ( diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index 2e58c825..0e7a2f1b 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -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 (
-
+
{children}
diff --git a/src/app/(main)/page.tsx b/src/app/(main)/page.tsx index 2e6d39f5..967a388c 100644 --- a/src/app/(main)/page.tsx +++ b/src/app/(main)/page.tsx @@ -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(), + queryKey: ["projects", "count"], + queryFn: () => api.get("projects/count").json(), }); 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(), + api.get("projects?tags=Удаленная демонстрация").json(), }); 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 } diff --git a/src/app/(main)/prime/page.tsx b/src/app/(main)/prime/page.tsx index 28d23b70..83e99738 100644 --- a/src/app/(main)/prime/page.tsx +++ b/src/app/(main)/prime/page.tsx @@ -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 ( - //
- // - // - //
- // ); - return ; + return ( +
+ + +
+ ); + // return ; } diff --git a/src/app/globals.css b/src/app/globals.css index dfb8496d..5d4ceb42 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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% { diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts new file mode 100644 index 00000000..868c0b84 --- /dev/null +++ b/src/app/sitemap.ts @@ -0,0 +1,85 @@ +import { api } from "@/api"; +import { IArticle } from "@/types/IArticle"; +import { MetadataRoute } from "next"; + +export default async function sitemap(): Promise { + 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(); + + 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; + } +} diff --git a/src/components/ImageUploader.tsx b/src/components/ImageUploader.tsx index c440514b..aa4ae8ac 100644 --- a/src/components/ImageUploader.tsx +++ b/src/components/ImageUploader.tsx @@ -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({ dest, name, - label = 'Выберите или перетащите изображение', - className = '', + label = "Выберите или перетащите изображение", + className = "", required = false, }: { dest: string; @@ -27,7 +28,7 @@ export function ImageUploader({ className?: string; }) { const [file, setFile] = useState(); - const [previewFile, setPreviewFile] = useState(''); + const [previewFile, setPreviewFile] = useState(""); const { register, setValue, control } = useFormContext(); @@ -44,12 +45,12 @@ export function ImageUploader({ 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>[]>(); setValue(name, filePaths[0]!); } catch (error) { @@ -71,19 +72,19 @@ export function ImageUploader({ setFile(file); setPreviewFile(URL.createObjectURL(file)); }} - accept={{ 'image/*': ['*'] }} + accept={{ "image/*": ["*"] }} noClick > {({ getRootProps, getInputProps, inputRef }) => (
({ <>
({ 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({ e.preventDefault(); setFile(undefined!); setValue(name, undefined!); - setPreviewFile(''); + setPreviewFile(""); }} className="bg-[#37393B99] px-3 py-2 rounded-xl cursor-pointer" > diff --git a/src/components/ItemActions.tsx b/src/components/ItemActions.tsx index 5b43c85c..5c090049 100644 --- a/src/components/ItemActions.tsx +++ b/src/components/ItemActions.tsx @@ -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(); } else if (isArticle(item)) setModal(); else if (isStory(item)) - setModal(); + setModal(); else setModal( - + ); } @@ -55,34 +64,34 @@ export function ItemActions({ ); } return ( -
+

*Нажимая кнопку отправить, вы принимаете - {' '} + {" "} условия использования и политику конфиденциальности

diff --git a/src/components/Layout/Footer.tsx b/src/components/Layout/Footer.tsx index 2dfb4f3a..f22b6044 100644 --- a/src/components/Layout/Footer.tsx +++ b/src/components/Layout/Footer.tsx @@ -37,7 +37,7 @@ export function Footer() { - + diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx index ffc3654c..1a5e16b1 100644 --- a/src/components/Layout/Header.tsx +++ b/src/components/Layout/Header.tsx @@ -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 (
@@ -78,13 +74,13 @@ export function Header() { {((isLg && scroll < -logoRef.current?.clientHeight!) || !isLg) && ( @@ -96,10 +92,10 @@ export function Header() { >
Оставить заявку @@ -162,18 +158,18 @@ export function Header() { >
@@ -185,7 +181,7 @@ export function Header() {

Контакты:

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() { )}
- {pathname.startsWith('/projects') ? ( + {pathname.startsWith("/projects") ? ( кейс dprofile { 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 ( diff --git a/src/components/Layout/Products.tsx b/src/components/Layout/Products.tsx index 3d86d327..342c5443 100644 --- a/src/components/Layout/Products.tsx +++ b/src/components/Layout/Products.tsx @@ -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 ( -
+
{products.map((product, index) => ( ))} diff --git a/src/components/articleInputs/ArticleContentEditor.tsx b/src/components/articleInputs/ArticleContentEditor.tsx index 12806e31..d06b2a42 100644 --- a/src/components/articleInputs/ArticleContentEditor.tsx +++ b/src/components/articleInputs/ArticleContentEditor.tsx @@ -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>; - item: FieldArrayWithId & { content: string }; + item: FieldArrayWithId & { content: string }; }) { const ref = useRef(null); const videoUploadRef = useRef(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 (
@@ -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 }) => '