This commit is contained in:
2025-07-31 15:10:21 +05:00
parent 69ed9c0506
commit 3a811669e8
24 changed files with 232 additions and 197 deletions
BIN
View File
Binary file not shown.
+3 -4
View File
@@ -13,8 +13,6 @@
}, },
"dependencies": { "dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.5.0", "@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": "^5.62.7",
"@tanstack/react-query-devtools": "^5.64.2", "@tanstack/react-query-devtools": "^5.64.2",
"@tinymce/tinymce-react": "^5.1.1", "@tinymce/tinymce-react": "^5.1.1",
@@ -30,7 +28,6 @@
"libphonenumber-js": "^1.11.7", "libphonenumber-js": "^1.11.7",
"next": "14.2.5", "next": "14.2.5",
"next-intl": "^4.0.2", "next-intl": "^4.0.2",
"postcss": "^8.5.1",
"react": "^18", "react": "^18",
"react-circular-progressbar": "^2.1.0", "react-circular-progressbar": "^2.1.0",
"react-dom": "^18", "react-dom": "^18",
@@ -46,7 +43,6 @@
"react-transition-group": "^4.4.5", "react-transition-group": "^4.4.5",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"sharp": "^0.33.5", "sharp": "^0.33.5",
"tailwindcss": "^4.0.0",
"tinymce": "^7.4.1", "tinymce": "^7.4.1",
"usehooks-ts": "^3.1.0", "usehooks-ts": "^3.1.0",
"zustand": "^4.5.4" "zustand": "^4.5.4"
@@ -61,8 +57,11 @@
"@types/react-phone-number-input": "^3.1.37", "@types/react-phone-number-input": "^3.1.37",
"@types/react-rangeslider": "^2.2.7", "@types/react-rangeslider": "^2.2.7",
"@types/react-transition-group": "^4.4.11", "@types/react-transition-group": "^4.4.11",
"autoprefixer": "^10.4.21",
"eslint": "^8", "eslint": "^8",
"eslint-config-next": "14.2.5", "eslint-config-next": "14.2.5",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17",
"typescript": "^5" "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;
+17 -13
View File
@@ -2,7 +2,9 @@ import { api } from "@/api";
import { IArticle } from "@/types/IArticle"; import { IArticle } from "@/types/IArticle";
import { QueryClient } from "@tanstack/react-query"; import { QueryClient } from "@tanstack/react-query";
import { Metadata } from "next"; import { Metadata } from "next";
import NotFound from "@/app/not-found"; import Link from "next/link";
import CloseIcon from "@/components/icons/CloseIcon";
import { RelevantArticlesPreview } from "@/components/pages/ArticlePage/RelevantArticlesPreview";
export async function generateMetadata({ export async function generateMetadata({
params, params,
@@ -42,17 +44,19 @@ export default async function Layout({
const { slug } = await params; const { slug } = await params;
return ( return (
// <section className="fixed inset-0 bg-[#0F101199] [backdrop-filter:blur(16px)] z-[14]"> <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} /> <RelevantArticlesPreview slug={slug} />
// {children} {children}
// <Link <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" 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" /> <div className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white">
// </Link> <CloseIcon />
// </section> </div>
<NotFound /> </Link>
</section>
// <NotFound />
); );
} }
+10 -7
View File
@@ -1,12 +1,12 @@
import { api } from '@/api'; import { api } from "@/api";
import { ArticleSyncPage } from '@/components/pages/ArticlePage/ArticleSyncPage'; import { ArticleSyncPage } from "@/components/pages/ArticlePage/ArticleSyncPage";
import { IArticle } from '@/types/IArticle'; import { IArticle } from "@/types/IArticle";
import { import {
dehydrate, dehydrate,
HydrationBoundary, HydrationBoundary,
QueryClient, QueryClient,
queryOptions, queryOptions,
} from '@tanstack/react-query'; } from "@tanstack/react-query";
export default async function ArticlePage({ export default async function ArticlePage({
params, params,
@@ -19,7 +19,7 @@ export default async function ArticlePage({
await queryClient.prefetchQuery( await queryClient.prefetchQuery(
queryOptions({ queryOptions({
queryKey: ['articles', slug], queryKey: ["articles", slug],
queryFn: () => api.get(`articles/${slug}`).json<IArticle>(), queryFn: () => api.get(`articles/${slug}`).json<IArticle>(),
}) })
); );
@@ -35,8 +35,11 @@ export async function generateStaticParams() {
const queryClient = new QueryClient(); const queryClient = new QueryClient();
const articles = await queryClient.fetchQuery<IArticle[]>({ const articles = await queryClient.fetchQuery<IArticle[]>({
queryKey: ['articles'], queryKey: ["articles"],
queryFn: () => api.get('articles').json<IArticle[]>(), queryFn: () =>
api
.get("articles", { searchParams: { locale: "en" } })
.json<IArticle[]>(),
}); });
return articles return articles
+14 -14
View File
@@ -1,8 +1,6 @@
import { NotFoundPage } from "@/components/pages/NotFoundPage";
import { StoriesButton } from "@/ui/StoriesButton"; import { StoriesButton } from "@/ui/StoriesButton";
import { Title } from "@/ui/Title"; import { Title } from "@/ui/Title";
import { Metadata, ResolvingMetadata } from "next"; import { Metadata, ResolvingMetadata } from "next";
import { InProcess } from "@/components/pages/InProcess";
export async function generateMetadata( export async function generateMetadata(
{}, {},
@@ -19,17 +17,19 @@ export default async function BlogLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
// <section className="space-y-12"> <section className="space-y-12">
// <div className="flex flex-col gap-6 items-center"> <div className="flex flex-col gap-6 items-center">
// <Title className="text-center" headerLevel={2}> <Title className="text-center" headerLevel={2}>
// All about company's work: All about company&apos;s work:
// <br /> <br />
// <span className="text-[#7A7A7A]">articles, videos and publications</span> <span className="text-[#7A7A7A]">
// </Title> articles, videos and publications
// <StoriesButton /> </span>
// </div> </Title>
// <div className="grid grid-cols-6 lg:gap-x-[0.833vw]">{children}</div> <StoriesButton />
// </section> </div>
<InProcess /> <div className="grid grid-cols-6 lg:gap-x-[0.833vw]">{children}</div>
</section>
// <InProcess />
); );
} }
+4 -1
View File
@@ -14,7 +14,10 @@ export default async function BlogPage() {
await queryClient.prefetchQuery({ await queryClient.prefetchQuery({
queryKey: ["articles"], queryKey: ["articles"],
queryFn: () => api.get("articles").json<IArticle[]>(), queryFn: () =>
api
.get("articles", { searchParams: { locale: "en" } })
.json<IArticle[]>(),
}); });
return ( return (
+54 -49
View File
@@ -1,5 +1,8 @@
@import url('/fonts/TTHovesProAll/stylesheet.css'); @import url("/fonts/TTHovesProAll/stylesheet.css");
@import 'tailwindcss';
@tailwind base;
@tailwind components;
@tailwind utilities;
@theme { @theme {
--gradient: linear-gradient(87deg, #798fff 15%, #d375ff 100%); --gradient: linear-gradient(87deg, #798fff 15%, #d375ff 100%);
@@ -10,7 +13,7 @@ html {
} }
body { body {
font-family: 'TTHovesPro'; font-family: "TTHovesPro";
color: #fff; color: #fff;
background-color: #0f1011; background-color: #0f1011;
} }
@@ -21,7 +24,7 @@ html {
} }
.bg-gradient-card { .bg-gradient-card {
content: ''; content: "";
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
@@ -34,7 +37,7 @@ html {
} }
*::-webkit-scrollbar { *::-webkit-scrollbar {
width: 4px; width: 12px;
} }
*::-webkit-scrollbar-thumb { *::-webkit-scrollbar-thumb {
@@ -46,62 +49,64 @@ html {
border-width: 2px; border-width: 2px;
} }
@utility line1 { @layer utilities {
@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%]; .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%];
}
@utility line2 { .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%]; @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%];
} }
@utility heading1 { .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]; @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];
} }
@utility heading2 { .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]; @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];
} }
@utility accent { .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; @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;
} }
@utility text1 { .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]; @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];
} }
@utility text2 { .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]; @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];
} }
@utility btnl { .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; @apply lg:text-[clamp(18px,1.25vw,28px)] md:max-lg:text-[clamp(16px,16+(100vw-768px)/672*2,18px)] text-base leading-none;
} }
@utility btnm { .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; @apply lg:text-[clamp(16px,1.111vw,24px)] md:max-lg:text-[clamp(14px,14px+(100vw-768px)/672*2,16px)] text-sm leading-none;
} }
@utility btns { .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; @apply lg:text-[clamp(14px,0.972vw,20px)] md:max-lg:text-[clamp(12px,12px+(100vw-768px)/672*2,14px)] text-xs leading-none;
} }
@utility caption { .caption {
@apply text-[clamp(14px,14px+(100vw-360px)/1240*2,16px)] leading-none; @apply text-[clamp(14px,14px+(100vw-360px)/1240*2,16px)] leading-none;
} }
@utility text-gradient { .text-gradient {
/* -webkit-background-clip: text; /* -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
@apply bg-gradient-to-r from-[#798FFF] to-[#D375FF] bg-clip-text; */ @apply bg-gradient-to-r from-[#798FFF] to-[#D375FF] bg-clip-text; */
background: linear-gradient(87deg, #798fff 15%, #d375ff 100%); background: linear-gradient(87deg, #798fff 15%, #d375ff 100%);
background-clip: text; background-clip: text;
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
} }
@utility bg-gradient { .bg-gradient {
background: linear-gradient(87deg, #798fff 15%, #d375ff 100%); background: linear-gradient(87deg, #798fff 15%, #d375ff 100%);
}
} }
@theme { @theme {
@@ -84,6 +84,7 @@ export function ArticleContentFormModal({
drafted, drafted,
posterImage, posterImage,
id, id,
locale: "en",
}} }}
/> />
) )
+19 -22
View File
@@ -1,27 +1,24 @@
'use client'; "use client";
import { useArticleMutation } from '@/hooks/useArticleMutation'; import { useArticleMutation } from "@/hooks/useArticleMutation";
import { useModalStore } from '@/stores/useModalStore'; import { useModalStore } from "@/stores/useModalStore";
import { IArticle } from '@/types/IArticle'; import { IArticle } from "@/types/IArticle";
import { CheckboxesGroup } from '@/ui/CheckboxesGroup'; import { CheckboxesGroup } from "@/ui/CheckboxesGroup";
import { TextInput } from '@/ui/TextInput'; import { TextInput } from "@/ui/TextInput";
import { FormProvider, useForm, useWatch } from 'react-hook-form'; import { FormProvider, useForm, useWatch } from "react-hook-form";
import { ImageUploader } from '../ImageUploader'; import { ImageUploader } from "../ImageUploader";
import { ArticleContentFormModal } from './ArticleContentFormModal'; import { ArticleContentFormModal } from "./ArticleContentFormModal";
import { ArticleFormActions } from './ArticleFormActions'; import { ArticleFormActions } from "./ArticleFormActions";
import { FormModalHeader } from './FormModalHeader'; import { FormModalHeader } from "./FormModalHeader";
import ReactLenis, { LenisRef } from 'lenis/react';
import { useEffect, useRef } from 'react';
import { useLenis } from '@/hooks/useLenis';
interface IArticleFormModalProps<TAction extends 'create' | 'edit'> { interface IArticleFormModalProps<TAction extends "create" | "edit"> {
action: TAction; 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, action,
defaultValues, defaultValues,
}: IArticleFormModalProps<TAction>) { }: IArticleFormModalProps<TAction>) {
@@ -34,7 +31,7 @@ export function ArticleFormModal<TAction extends 'create' | 'edit'>({
tags: [], tags: [],
drafted: true, drafted: true,
}, },
mode: 'onChange', mode: "onChange",
}); });
async function onSubmit(data: IArticleInput) { async function onSubmit(data: IArticleInput) {
@@ -53,7 +50,7 @@ export function ArticleFormModal<TAction extends 'create' | 'edit'>({
await mutateAsync({ await mutateAsync({
...getValues(), ...getValues(),
blocks: JSON.stringify( blocks: JSON.stringify(
defaultValues ? defaultValues.blocks : getValues('blocks') ?? [] defaultValues ? defaultValues.blocks : getValues("blocks") ?? []
), ),
drafted, drafted,
}); });
@@ -61,7 +58,7 @@ export function ArticleFormModal<TAction extends 'create' | 'edit'>({
} }
const { mutateAsync } = useArticleMutation( const { mutateAsync } = useArticleMutation(
action === 'create' action === "create"
? { action, id: undefined } ? { action, id: undefined }
: { action, id: defaultValues!.id } : { action, id: defaultValues!.id }
); );
@@ -101,7 +98,7 @@ export function ArticleFormModal<TAction extends 'create' | 'edit'>({
</label> </label>
<CheckboxesGroup <CheckboxesGroup
name="tags" name="tags"
options={['Недвижимость', 'Награды', 'Выставки']} options={["Недвижимость", "Награды", "Выставки"]}
/> />
</div> </div>
<ImageUploader <ImageUploader
+10 -9
View File
@@ -7,7 +7,6 @@ import { createRef, RefObject, useEffect, useRef, useState } from "react";
import { useSwipeable } from "react-swipeable"; import { useSwipeable } from "react-swipeable";
import CloseIcon from "@/components/icons/CloseIcon"; import CloseIcon from "@/components/icons/CloseIcon";
import { ItemActions } from "../ItemActions"; import { ItemActions } from "../ItemActions";
import { useTranslations } from "next-intl";
export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) { export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
const { setModal } = useModalStore(); const { setModal } = useModalStore();
@@ -79,8 +78,6 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
onSwipedRight: () => setCurrentIndex((prev) => Math.max(0, prev - 1)), onSwipedRight: () => setCurrentIndex((prev) => Math.max(0, prev - 1)),
}); });
const t = useTranslations("stories");
if (!stories || !stories.length) return null; if (!stories || !stories.length) return null;
return ( return (
@@ -102,7 +99,7 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
className={`flex lg:gap-[1.667vw] md:max-lg:gap-6 items-center h-full relative`} className={`flex lg:gap-[1.667vw] md:max-lg:gap-6 items-center h-full relative`}
> >
{!!videoRefs.length && {!!videoRefs.length &&
stories?.map(({ id, video, preview, text, createdAt }, index) => ( stories.map(({ id, video, preview, text, createdAt }, index) => (
<div <div
style={{ style={{
transform: isLg transform: isLg
@@ -113,10 +110,10 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
left: isLg || isMd ? `calc(${1 - currentIndex}*1.667vw)` : 0, left: isLg || isMd ? `calc(${1 - currentIndex}*1.667vw)` : 0,
}} }}
key={id} 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 ${ className={`select-none relative flex items-end group overflow-hidden lg:rounded-[0.833vw] md:max-lg:rounded-xl cursor-pointer w-1/2 transition-transform lg:p-[1.111vw] p-4 ${
index === currentIndex 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: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: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)} onClick={() => setCurrentIndex(index)}
> >
@@ -136,7 +133,10 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
/> />
{currentIndex === index && ( {currentIndex === index && (
<div className="space-y-5 z-1 max-md:hidden"> <div className="space-y-5 z-1 max-md:hidden">
<p className="heading1 font-medium">{t(id)}</p> <p className="heading1 font-medium">
Interactive sales tool GRAFF.estate for the Upside Towers
residential complex
</p>
<div className="bg-white/30 w-full h-1 rounded-[34px]"> <div className="bg-white/30 w-full h-1 rounded-[34px]">
<div <div
className="h-1 bg-white transition-[width] rounded-[34px]" className="h-1 bg-white transition-[width] rounded-[34px]"
@@ -152,7 +152,8 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
{stories && stories.length > 0 && ( {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"> <p className="heading1 font-medium">
{stories[currentIndex]?.text} Interactive sales tool GRAFF.estate for the Upside Towers
residential complex
</p> </p>
<div className="flex gap-1"> <div className="flex gap-1">
{stories.map(({ id }, index) => ( {stories.map(({ id }, index) => (
@@ -15,6 +15,7 @@ import ArrowMoreIcon from "@/components/icons/ArrowMoreIcon";
import EditIcon from "@/components/icons/EditIcon"; import EditIcon from "@/components/icons/EditIcon";
import { ReactLenis } from "lenis/react"; import { ReactLenis } from "lenis/react";
import { useLenis } from "@/hooks/useLenis"; import { useLenis } from "@/hooks/useLenis";
import { useTranslations } from "next-intl";
export function ArticleSyncPage({ slug }: { slug: string }) { export function ArticleSyncPage({ slug }: { slug: string }) {
const { data: article } = useGetArticleById(slug); const { data: article } = useGetArticleById(slug);
@@ -25,6 +26,8 @@ export function ArticleSyncPage({ slug }: { slug: string }) {
const lenis = useLenis(); const lenis = useLenis();
const t = useTranslations("feedback");
if (!article) return null; if (!article) return null;
return ( return (
@@ -68,9 +71,9 @@ export function ArticleSyncPage({ slug }: { slug: string }) {
{article.tags.map((tag) => ( {article.tags.map((tag) => (
<div <div
key={tag} key={tag}
className="bg-[#37393B99] lg:rounded-[1.181vw] rounded-[17px] lg:px-[0.833vw] lg:py-[0.556vw] px-3 py-[7px] btns font-medium backdrop-blur-2xl" className="bg-[#37393B99] text-nowrap lg:rounded-[1.181vw] rounded-[17px] lg:px-[0.833vw] lg:py-[0.556vw] px-3 py-[7px] btns font-medium backdrop-blur-2xl"
> >
{tag} {t(tag)}
</div> </div>
))} ))}
</div> </div>
@@ -80,7 +83,7 @@ export function ArticleSyncPage({ slug }: { slug: string }) {
{article.blocks.map((block, index) => ( {article.blocks.map((block, index) => (
<Fragment key={index}> <Fragment key={index}>
{block.type === "Content" ? ( {block.type === "Content" ? (
<div className="lg:max-w-2/3 [&_p_*]:!text1 [&_h1_*]:!heading1 [&_h2_*]:!heading2"> <div className="[&_p_*]:!text1 [&_h1_*]:!heading1 [&_h2_*]:!heading2">
{parse(block.content)} {parse(block.content)}
</div> </div>
) : block.type === "ButtonLink" ? ( ) : block.type === "ButtonLink" ? (
+13 -9
View File
@@ -1,10 +1,11 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
'use client'; "use client";
import { ItemActions } from '@/components/ItemActions'; import { ItemActions } from "@/components/ItemActions";
import { IArticle } from '@/types/IArticle'; import { IArticle } from "@/types/IArticle";
import { PostTag } from '@/ui/PostTag'; import { PostTag } from "@/ui/PostTag";
import { useRouter, useSearchParams } from 'next/navigation'; import { useTranslations } from "next-intl";
import { useRouter, useSearchParams } from "next/navigation";
export function ArticleCard({ export function ArticleCard({
id, id,
@@ -22,11 +23,13 @@ export function ArticleCard({
const { push } = useRouter(); const { push } = useRouter();
const t = useTranslations("feedback");
return ( return (
<div <div
onClick={() => !drafted && push('/blog/' + slug)} onClick={() => !drafted && push("/blog/" + slug)}
className={`${ className={`${
className ? className + ' ' : '' className ? className + " " : ""
}hover:backdrop-blur-[500px] cursor-pointer h-full group flex flex-col justify-between gap-3 overflow-hidden transition-[background-size] lg:bg-[length:100%_0%] bg-bottom hover:bg-[length:100%_100%] bg-no-repeat lg:rounded-[1.111vw] rounded-2xl lg:p-[0.833vw] p-3 relative`} }hover:backdrop-blur-[500px] cursor-pointer h-full group flex flex-col justify-between gap-3 overflow-hidden transition-[background-size] lg:bg-[length:100%_0%] bg-bottom hover:bg-[length:100%_100%] bg-no-repeat lg:rounded-[1.111vw] rounded-2xl lg:p-[0.833vw] p-3 relative`}
> >
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom,#7A7A7A50,transparent)] max-lg:opacity-100 opacity-0 group-hover:opacity-100 transition-opacity" /> <div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom,#7A7A7A50,transparent)] max-lg:opacity-100 opacity-0 group-hover:opacity-100 transition-opacity" />
@@ -42,9 +45,9 @@ export function ArticleCard({
<div className="flex flex-wrap lg:gap-[0.278vw] gap-1"> <div className="flex flex-wrap lg:gap-[0.278vw] gap-1">
{tags.map((tag) => ( {tags.map((tag) => (
<PostTag <PostTag
text={tag} text={t(tag)}
key={tag} key={tag}
active={params.getAll('tags').includes(tag)} active={params.getAll("tags").includes(tag)}
/> />
))} ))}
</div> </div>
@@ -58,6 +61,7 @@ export function ArticleCard({
drafted, drafted,
posterImage, posterImage,
createdAt, createdAt,
locale: "en",
}} }}
/> />
</div> </div>
@@ -32,7 +32,7 @@ export function ArticlesList() {
className="lg:w-[21.435vw] lg:aspect-[308.67/352] md:max-lg:w-[47.135vw] max-md:w-full" className="lg:w-[21.435vw] lg:aspect-[308.67/352] md:max-lg:w-[47.135vw] max-md:w-full"
/> />
))} ))}
<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="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="text1 font-medium lg:max-w-[25.069vw]">
We haven&apos;t moved all publications here yet, but you can find We haven&apos;t moved all publications here yet, but you can find
the actual content in our Telegram channel{" "} the actual content in our Telegram channel{" "}
@@ -52,7 +52,7 @@ export function ArticlesList() {
<TelegramIcon /> <TelegramIcon />
</div> </div>
</Link> </Link>
</div> </div> */}
</div> </div>
</div> </div>
</div> </div>
@@ -50,6 +50,7 @@ export function Clients() {
"Центр-Инвест", "Центр-Инвест",
"DNS", "DNS",
"ПСК Дом девелопмент", "ПСК Дом девелопмент",
"ПИК",
]; ];
useEffect(() => { useEffect(() => {
+19 -19
View File
@@ -1,14 +1,14 @@
import { enMapProject } from '@/consts/mockEnMapProject'; import { enMapProjects } from "@/consts/mockEnMapProject";
import { useLongPress } from '@/hooks/useLongPress'; import { useLongPress } from "@/hooks/useLongPress";
import { useMediaQueries } from '@/hooks/useMediaQueries'; import { useMediaQueries } from "@/hooks/useMediaQueries";
import { useGetProjectsCountQuery } from '@/queries/getProjectsCount'; import { useGetProjectsCountQuery } from "@/queries/getProjectsCount";
import { useCityPointStore } from '@/stores/useCityPointStore'; import { useCityPointStore } from "@/stores/useCityPointStore";
import { ICityProjects } from '@/types/ICityProjects'; import { ICityProjects } from "@/types/ICityProjects";
import { IMapProject } from '@/types/IMapProject'; import { IMapProject } from "@/types/IMapProject";
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from "framer-motion";
import { useTranslations } from 'next-intl'; import { useTranslations } from "next-intl";
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
import { useHover } from 'usehooks-ts'; import { useHover } from "usehooks-ts";
export function CityPoint({ export function CityPoint({
x, x,
@@ -25,7 +25,7 @@ export function CityPoint({
setCurrentHovered: Dispatch<SetStateAction<number | undefined>>; setCurrentHovered: Dispatch<SetStateAction<number | undefined>>;
projects?: IMapProject[]; projects?: IMapProject[];
}) { }) {
const count = enMapProject[title as keyof typeof enMapProject]?.length || 0; const count = enMapProjects.length || 0;
const { setCityPoint } = useCityPointStore(); const { setCityPoint } = useCityPointStore();
@@ -55,7 +55,7 @@ export function CityPoint({
const [animationCompleted, setAnimationCompleted] = useState(false); const [animationCompleted, setAnimationCompleted] = useState(false);
const t = useTranslations('cities'); const t = useTranslations("cities");
useEffect(() => { useEffect(() => {
if (animationCompleted && circleRef.current) if (animationCompleted && circleRef.current)
@@ -87,18 +87,18 @@ export function CityPoint({
ref={pointRef} ref={pointRef}
style={{ left: `${x}%`, top: `${y}%` }} 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 ${ 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 <p
className={`heading2 font-medium${active ? ' z-2' : ''}`} className={`heading2 font-medium${active ? " z-2" : ""}`}
ref={refTitle} ref={refTitle}
> >
{localeKey ? t(localeKey) : title} {localeKey ? t(localeKey) : title}
</p> </p>
<p <p
className={`btns lg:h-[2.083vw] font-medium h-[5.114vw]${ className={`btns lg:h-[2.083vw] font-medium h-[5.114vw]${
active ? ' z-2' : '' active ? " z-2" : ""
}`} }`}
ref={refCount} ref={refCount}
> >
@@ -121,7 +121,7 @@ export function CityPoint({
}, },
}} }}
onAnimationComplete={() => setAnimationCompleted(true)} 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 && {!!projects?.length &&
circleRef.current && circleRef.current &&
@@ -189,8 +189,8 @@ export function LogoItem({
top: -circleRadius, top: -circleRadius,
}} }}
exit={{ opacity: 0, transition: { delay: index * 0.1 } }} 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]' className="max-w-[6.264vw] w-full aspect-square absolute rounded-[1.567vw] -left-[9.049vw]"
/> />
); );
} }
+22 -22
View File
@@ -1,14 +1,14 @@
'use client'; "use client";
import { cities, mobileCities } from '@/consts/cities'; import { cities, mobileCities } from "@/consts/cities";
import { useGetMapPointByCity } from '@/queries/getMapPointByCity'; import { useGetMapPointByCity } from "@/queries/getMapPointByCity";
import { useCityPointStore } from '@/stores/useCityPointStore'; import { useCityPointStore } from "@/stores/useCityPointStore";
import { useState } from 'react'; import { useState } from "react";
import { CityPoint } from './CityPoint'; import { CityPoint } from "./CityPoint";
import { Slider } from './Slider'; import { Slider } from "./Slider";
import { useMediaQueries } from '@/hooks/useMediaQueries'; import { useMediaQueries } from "@/hooks/useMediaQueries";
import { Icon } from '@/ui/Icon'; import { Icon } from "@/ui/Icon";
import { useTranslations } from 'next-intl'; import { useTranslations } from "next-intl";
export function Map() { export function Map() {
const { cityPoint } = useCityPointStore(); const { cityPoint } = useCityPointStore();
@@ -19,12 +19,12 @@ export function Map() {
const { isLg } = useMediaQueries(); const { isLg } = useMediaQueries();
const t = useTranslations('map'); const t = useTranslations("map");
return ( return (
<div className='relative'> <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="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="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">
{(isLg ? cities : mobileCities).map((point, index) => ( {(isLg ? cities : mobileCities).map((point, index) => (
<CityPoint <CityPoint
key={point.title} key={point.title}
@@ -37,21 +37,21 @@ export function Map() {
} }
index={index} index={index}
setCurrentHovered={setCurrentHovered} setCurrentHovered={setCurrentHovered}
projects={point.title === cityPoint?.title ? mapPoint : undefined} // projects={point.title === cityPoint?.title ? mapPoint : undefined}
/> />
))} ))}
<Slider mapPoint={mapPoint} /> <Slider />
</div> </div>
</div> </div>
<div className='lg:hidden absolute left-[50vw] bottom-[4.444vw] translate-y-1/2 -translate-x-1/2 w-[68.611vw] aspect-[247/56] rounded-[4.444vw] bg-[#37393B99] px-[4.167vw] py-[4.444vw] flex gap-[2.222vw] justify-between items-center'> <div className="lg:hidden absolute left-[50vw] bottom-[4.444vw] translate-y-1/2 -translate-x-1/2 w-[68.611vw] aspect-[247/56] rounded-[4.444vw] bg-[#37393B99] px-[4.167vw] py-[4.444vw] flex gap-[2.222vw] justify-between items-center">
<div className='w-[6.667vw] h-[6.667vw]'> <div className="w-[6.667vw] h-[6.667vw]">
<Icon <Icon
name='finger_print' name="finger_print"
svgProp={{ className: 'w-[6.667vw] h-[6.667vw]' }} svgProp={{ className: "w-[6.667vw] h-[6.667vw]" }}
/> />
</div> </div>
<p className='caption leading-[120%] font-medium select-none'> <p className="caption leading-[120%] font-medium select-none">
{t('instruction')} {t("instruction")}
</p> </p>
</div> </div>
</div> </div>
+12 -13
View File
@@ -4,7 +4,7 @@ import { queryOptions, useQuery } from '@tanstack/react-query';
export const queryArticlesOptions = { export const queryArticlesOptions = {
queryKey: ['articles'], queryKey: ['articles'],
queryFn: () => api.get('articles').json<IArticle[]>(), queryFn: () => api.get('articles', { searchParams: { locale: "en" } }).json<IArticle[]>(),
}; };
export function useGetArticlesQuery(tags?: string | string[]) { export function useGetArticlesQuery(tags?: string | string[]) {
@@ -12,18 +12,17 @@ export function useGetArticlesQuery(tags?: string | string[]) {
queryOptions( queryOptions(
tags && tags.length > 0 tags && tags.length > 0
? { ? {
queryKey: ['articles', tags], queryKey: ['articles', tags],
queryFn: () => queryFn: () =>
api api
.get( .get(
`articles?${ `articles?${Array.isArray(tags)
Array.isArray(tags) ? tags.map((tag) => `tags=${tag}`).join('&')
? tags.map((tag) => `tags=${tag}`).join('&') : 'tags=' + tags
: 'tags=' + tags }&locale=en`
}` )
) .json<IArticle[]>(),
.json<IArticle[]>(), }
}
: queryArticlesOptions : queryArticlesOptions
) )
); );
+2 -2
View File
@@ -1,5 +1,5 @@
import { api } from '@/api'; import { api } from '@/api';
import { enMapProject } from '@/consts/mockEnMapProject'; import { enMapProjects } from '@/consts/mockEnMapProject';
import { useCityPointStore } from '@/stores/useCityPointStore'; import { useCityPointStore } from '@/stores/useCityPointStore';
import { IMapProject } from '@/types/IMapProject'; import { IMapProject } from '@/types/IMapProject';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
@@ -15,6 +15,6 @@ export function useGetMapPointByCity() {
// }); // });
return { return {
data: enMapProject[cityPoint?.title as keyof typeof enMapProject], data: enMapProjects,
}; };
} }
+1
View File
@@ -6,5 +6,6 @@ export function useGetStories() {
return useQuery({ return useQuery({
queryKey: ['stories'], queryKey: ['stories'],
queryFn: () => api.get('stories').json<IStory[]>(), queryFn: () => api.get('stories').json<IStory[]>(),
select: (data) => data.slice(2, 3),
}); });
} }
+1
View File
@@ -44,4 +44,5 @@ export interface IArticle {
drafted: boolean; drafted: boolean;
slug?: string; slug?: string;
blocks: Block[]; blocks: Block[];
locale: 'ru' | 'en';
} }
+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: [],
};