This commit is contained in:
2024-12-25 18:57:43 +05:00
parent d2237313c0
commit 7589e2d14c
31 changed files with 366 additions and 146 deletions
+3 -1
View File
@@ -10,7 +10,9 @@ export default function Layout({ children }: { children: React.ReactNode }) {
const { data } = useQuery({
queryKey: ['checkAuth'],
queryFn: async () => await api.get('auth/check').json<{ auth: boolean }>(),
queryFn: async () => {
return await api.get('auth/check').json<{ auth: boolean }>();
},
});
useEffect(() => {
-1
View File
@@ -16,7 +16,6 @@ export default function LoginPage() {
const queryClient = useQueryClient();
const { mutate: login } = useMutation({
mutationKey: ['login'],
mutationFn: async ({
username,
password,
+1 -1
View File
@@ -7,7 +7,7 @@ export default function MainLayout({ children }: PropsWithChildren) {
return (
<div className="flex flex-col">
<NewHeader />
<main className="flex-1 pt-14 sm:px-5 px-4 overflow-hidden relative">
<main className="flex-1 pt-14 sm:px-5 px-4 overflow-clip relative">
{children}
<Feedback />
</main>
+1 -1
View File
@@ -34,7 +34,7 @@ export default async function ProjectsPage({
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<div className="lg:space-y-14 sm:space-y-8 space-y-4">
<div className="lg:space-y-14 sm:space-y-8 space-y-4 relative">
<ProjectsPageHeader />
<ProjectsList tags={tags} city={city} companyId={companyId} />
</div>
+42 -2
View File
@@ -121,9 +121,9 @@ 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 {
/* .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)];
@@ -199,4 +199,44 @@ 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%];
}
.line2 {
@apply min-[1440px]:text-[clamp(64px,64px+(100vw-1440px)/480*24,88px)] md:text-[clamp(40px,40px+(100vw-768px)/672*24,64px)] sm:text-[clamp(32px,32px+(100vw-360px)/408*8,40px)] text-[32px] leading-[95%];
}
.heading1 {
@apply min-[1440px]:text-[clamp(24px,24px+(100vw-1440px)/480*4,28px)] text-2xl leading-[1.167];
}
.heading2 {
@apply min-[1440px]:text-[clamp(20px,20px+(100vw-1440px)/480*4,24px)] sm:text-[clamp(16px,16px+(100vw-360px)/1080*4,20px)] text-base min-[1440px]:leading-[1.2] leading-[1.125];
}
.accent {
@apply min-[1440px]:text-[clamp(32px,32px+(100vw-1440px)/480*8,40px)] text-2xl min-[1440px]:leading-[1.1] leading-none;
}
.text1 {
@apply min-[1440px]:text-[clamp(14px,14px+(100vw-1440px)/480*4,18px)] text-sm leading-[1.35];
}
.text2 {
@apply min-[1440px]:text-[clamp(12px,12px+(100vw-1440px)/480*2,14px)] text-xs leading-[1.35];
}
.btnl {
@apply sm:text-[clamp(16px,16px+(100vw-360px)/1560*2,18px)] text-base leading-none;
}
.btnm {
@apply sm:text-[clamp(14px,14px+(100vw-360px)/1560*2,16px)] text-sm leading-none;
}
.btns {
@apply sm:text-[clamp(12px,12px+(100vw-360px)/1560*2,14px)] text-xs leading-none;
}
}
+3
View File
@@ -0,0 +1,3 @@
export function CompanyActions() {
return <div className="absolute top-0 right-0 p-4 flex gap-2 z-[5]"></div>;
}
+9 -9
View File
@@ -14,13 +14,13 @@ import { SelectPhoneCode } from './SelectPhoneCode';
export function Feedback() {
return (
<div id="contacts" className="pb-20 pt-[200px] flex gap-3">
<h2 className="lg:col-span-7 sm:col-span-full h2 max-lg:mb-6 max-w-[50%]">
<h2 className="lg:col-span-7 sm:col-span-full line2 font-medium max-lg:mb-6 max-w-[50%]">
<span className="text-[#7A7A7A]">Хотите увеличить конверсию?</span>
<br />
Давайте обсудим детали.
</h2>
<div className="space-y-4 flex-1">
<p className="font-medium text-xl">Нам нужна</p>
<p className="font-medium heading2">Нам нужна</p>
<FeedbackForm />
</div>
</div>
@@ -84,7 +84,7 @@ export function FeedbackForm() {
type="text"
placeholder="Имя*"
{...register('name')}
className="bg-transparent border-b border-[#3D425C] focus:border-white py-4 rounded-none outline-none transition-all w-full placeholder:m-text placeholder:font-medium placeholder:select-none"
className="bg-transparent border-b border-[#3D425C] focus:border-white py-4 rounded-none outline-none transition-all w-full placeholder:btnl placeholder:font-medium placeholder:select-none"
/>
</div>
<div>
@@ -100,7 +100,7 @@ export function FeedbackForm() {
mask={placeholder?.replace(/\d/g, '9') ?? ''}
maskChar={null}
placeholder={placeholder}
className="w-full transition-all bg-transparent rounded-none outline-none m-text placeholder:m-text placeholder:font-medium placeholder:select-none peer"
className="w-full transition-all bg-transparent rounded-none outline-none placeholder:btnl placeholder:font-medium placeholder:select-none peer"
/>
<div className="bottom-0 absolute w-full border-b border-[#3D425C] peer-focus:border-white -mb-2" />
</div>
@@ -112,14 +112,14 @@ export function FeedbackForm() {
type="text"
placeholder="E-mail*"
{...register('email')}
className="bg-transparent border-b border-[#3D425C] focus:border-white py-4 rounded-none outline-none transition-all w-full placeholder:m-text placeholder:font-medium placeholder:select-none"
className="bg-transparent border-b border-[#3D425C] focus:border-white py-4 rounded-none outline-none transition-all w-full placeholder:btnl placeholder:font-medium placeholder:select-none"
/>
</div>
<div className="flex gap-3 items-stretch">
<Button type="submit" rounded="2xl" className="py-5">
<Button type="submit" rounded="2xl" className="py-5 btnl">
Оставить заявку
</Button>
<Link href={'/policy'} className="text-xs">
<Link href={'/policy'} className="text2 max-w-[30%]">
<span className="text-[#7A7A7A]">
*Нажимая кнопку отправить, вы принимаете
</span>{' '}
@@ -150,11 +150,11 @@ export function ProductOption({
getValues('products').filter((product: string) => product !== text)
);
}
}, [chosen]);
}, [chosen, getValues, setValue, text]);
return (
<div
className={`cursor-pointer px-6 py-[17px] transition-colors rounded-2xl ${
className={`cursor-pointer px-6 py-[17px] transition-colors rounded-2xl btnm ${
chosen ? 'bg-white text-black' : 'bg-[#37393B99]'
}`}
onClick={() => setChosen(!chosen)}
+1 -1
View File
@@ -19,7 +19,7 @@ export function ModalContainer() {
modal && (
<div
className={
'fixed left-0 z-[9] w-full h-full flex justify-center items-start transition-opacity' +
'fixed left-0 z-[11] w-full h-full flex justify-center items-start transition-opacity' +
(name === 'video' || name === 'form'
? ' bg-black bg-opacity-90 [backdrop-filter:blur(10px);]'
: '') +
+7 -7
View File
@@ -14,8 +14,8 @@ export function NewFooter() {
href={'tel:' + '8 800 770 00 67'.replaceAll(' ', '')}
className="p-6 flex flex-col justify-between bg-[linear-gradient(to_top_left,#7A7A7A50,transparent)] rounded-2xl w-1/2"
>
<p className="text-[#7A7A7A] text-sm font-medium">Позвонить</p>
<div className="flex font-medium text-[64px] items-center">
<p className="text-[#7A7A7A] text1 font-medium">Позвонить</p>
<div className="flex font-medium line2 items-center">
8 800 770 00 67
<ClassNameWrapper
className="w-20 h-20"
@@ -27,8 +27,8 @@ export function NewFooter() {
href={'mailto:' + 'info@graff.tech'}
className="p-6 flex flex-col justify-between bg-[linear-gradient(to_top_left,#7A7A7A50,transparent)] rounded-2xl flex-1"
>
<p className="text-[#7A7A7A] text-sm font-medium">Написать</p>
<div className="flex font-medium text-[64px] items-center">
<p className="text-[#7A7A7A] text1 font-medium">Написать</p>
<div className="flex font-medium line2 items-center">
info@graff.tech
<ClassNameWrapper
className="w-20 h-20"
@@ -66,16 +66,16 @@ export function NewFooter() {
<div className="flex gap-x-6">
<Link
href={'/policy'}
className="text-[#37393B] tex-sm font-medium leading-[18.9px]"
className="text-[#37393B] text1 font-medium leading-[18.9px]"
>
Политика конфиденциальности и обработки персональных данных
</Link>
<p className="text-[#37393B] tex-sm font-medium leading-[18.9px]">
<p className="text-[#37393B] text1 font-medium leading-[18.9px]">
© 2024 GRAFF interactive. Все права защищены
</p>
<Link
href={'https://graff.tech'}
className="text-[#37393B] tex-sm font-medium leading-[18.9px]"
className="text-[#37393B] text1 font-medium leading-[18.9px]"
>
graff.tech
</Link>
+6 -2
View File
@@ -41,13 +41,17 @@ export function NewHeader() {
<HeaderLink href={'/projects'} text={'Проекты'} />
<HeaderLink href={'/blog'} text={'Блог'} />
<Button
className="btnm font-medium"
rounded="2xl"
onClick={() => setModal(<ModalWithForm />, 'form')}
>
Оставить заявку
</Button>
{data?.auth && (
<button className="p-3 rounded-full" onClick={() => logout()}>
<button
className="p-3 rounded-full btnm font-medium"
onClick={() => logout()}
>
Выйти
</button>
)}
@@ -64,7 +68,7 @@ export function HeaderLink({ href, text }: { href: string; text: string }) {
return (
<Link
href={href}
className={`px-6 py-4 font-medium text-sm hover:bg-[#fff] hover:text-black rounded-2xl transition-colors ${
className={`px-6 py-4 font-medium btnm hover:bg-[#fff] hover:text-black rounded-2xl transition-colors ${
pathname === href && 'opacity-50'
}`}
>
+6 -6
View File
@@ -26,7 +26,7 @@ export function SelectPhoneCode({
function handleExpand(e: SyntheticEvent) {
e.preventDefault();
setOpen(prev => !prev);
setOpen((prev) => !prev);
}
function pickPhoneCode(phoneCode: string, country: CountryCode) {
@@ -43,11 +43,11 @@ export function SelectPhoneCode({
<Image
width={16}
height={8}
src={countries.find(c => c.iso === currentCountry)?.flag || ''}
src={countries.find((c) => c.iso === currentCountry)?.flag || ''}
className="!relative w-4 sm:w-6"
alt={currentCountry}
/>
<p className="m-text">{currentPhoneCode}</p>
<p className="btnl font-medium">{currentPhoneCode}</p>
<ClassNameWrapper
className="flex-1 max-sm:w-4 sm:max-lg:w-5"
element={open ? <ChevronUpIcon /> : <ChevronDownIcon />}
@@ -56,10 +56,10 @@ export function SelectPhoneCode({
{open && (
<div className="space-y-1 absolute z-10 bg-[#14161F] top-[100%] -left-1 border border-t-0 rounded-b-lg border-[#3D425C] max-h-[300px] overflow-y-auto overflow-x-hidden">
{getCountries()
.map(country => [`+${getCountryCallingCode(country)}`, country])
.map((country) => [`+${getCountryCallingCode(country)}`, country])
.filter(
([phonecode, country]) =>
phonecode !== currentPhoneCode || country !== currentCountry,
phonecode !== currentPhoneCode || country !== currentCountry
)
.map(([phoneCode, country]) => (
<div
@@ -68,7 +68,7 @@ export function SelectPhoneCode({
onClick={() => pickPhoneCode(phoneCode, country as CountryCode)}
>
<Image
src={countries.find(c => c.iso === country)?.flag || ''}
src={countries.find((c) => c.iso === country)?.flag || ''}
alt={country}
className="!relative w-4 sm:w-6"
width={16}
+12 -1
View File
@@ -9,12 +9,18 @@ export function MediaUploader<T extends FieldValues>({
fieldName,
item,
label,
className,
width = 300,
height = 300,
}: {
dest: string;
setValue: UseFormSetValue<T>;
fieldName: Path<T>;
item: Record<'img', string> & Record<'id', string>;
label: string;
className?: string;
width?: number;
height?: number;
}) {
const [file, setFile] = useState<File>();
const [previewFile, setPreviewFile] = useState('');
@@ -52,7 +58,12 @@ export function MediaUploader<T extends FieldValues>({
}, [file, uploadFile]);
return (
<label className="relative border border-dashed border-neutral-500 px-3 py-2 hover:bg-opacity-10 hover:bg-black cursor-pointer rounded-lg flex flex-col gap-2">
<label
className={
'relative border border-dashed border-neutral-500 px-3 py-2 cursor-pointer rounded-lg flex flex-col gap-2 ' +
className
}
>
<input
type="file"
accept={'image/*'}
@@ -0,0 +1,86 @@
'use client';
import { api } from '@/api';
import { useModalStore } from '@/stores/useModalStore';
import { ICompany } from '@/types/ICompany';
import { Button } from '@/ui/Button';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
import { CloseIcon } from '../icons/CloseIcon';
import { MediaUploader } from '../MediaUploader';
interface IAddCompanyInput {
title: string;
color: string;
mapIcon?: string;
logo?: string;
}
export function CompanyFormModal({}) {
const { setModal } = useModalStore();
const { register, handleSubmit, setValue } = useForm<IAddCompanyInput>();
const queryClient = useQueryClient();
const { mutateAsync } = useMutation<ICompany, Error, IAddCompanyInput>({
mutationFn: async (json) => await api.post('companies', { json }).json(),
onSuccess: async () =>
await queryClient.invalidateQueries({ queryKey: ['companies'] }),
});
const onSubmit = async (data: IAddCompanyInput) => {
await mutateAsync(data);
setModal(null, '');
};
return (
<div className="text-black bg-white p-4 rounded-lg relative top-[100px] space-y-4">
<div className="flex justify-between items-center border-b border-[#ccc] pb-4 gap-4">
<p className="text-xl">Добавление компании</p>
<button
onClick={() => setModal(null, '')}
className="p-2 hover:bg-white hover:bg-opacity-10 transition-colors rounded-full"
>
<CloseIcon />
</button>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5 w-full">
<div className="flex flex-col">
<label htmlFor="title">Компания</label>
<input {...register('title')} name="title" placeholder="Название" />
</div>
<div className="flex justify-between items-center gap-x-3 w-full">
<MediaUploader
className="bg-[#232425] aspect-square text-white flex-1"
dest="projects"
fieldName="logo"
item={{ id: '', img: '' }}
label="Логотип"
setValue={setValue}
width={50}
height={50}
/>
<MediaUploader
className="aspect-square"
dest="projects"
fieldName="mapIcon"
item={{ id: '', img: '' }}
label="Иконка для карты"
setValue={setValue}
width={50}
height={50}
/>
</div>
<div className="flex justify-between">
<Button color="secondary" onClick={() => setModal(null, '')}>
Отмена
</Button>
<Button type="submit" className="text-white">
Добавить
</Button>
</div>
</form>
</div>
);
}
+2 -1
View File
@@ -2,6 +2,7 @@
import { api } from '@/api';
import { PlusIcon } from '@/components/icons/PlusIcon';
import { CompanyFormModal } from '@/components/modals/CompanyFormModal';
import { useModalStore } from '@/stores/useModalStore';
import { ICompany } from '@/types/ICompany';
import { Title } from '@/ui/Title';
@@ -55,7 +56,7 @@ export function Clients() {
{checkAuth?.auth && (
<button
onClick={() => {
// setModal(<div className="bg-white"></div>, 'addCompany');
setModal(<CompanyFormModal />, 'addCompany');
}}
className="aspect-square flex justify-center items-center bg-[#232425] rounded-xl opacity-60 hover:opacity-100 transition-opacity"
>
@@ -80,7 +80,7 @@ export function InteractivePresentation() {
className="rotate-[-4deg] right-2 relative"
/>
</div>
<p className="font-medium text-sm">
<p className="font-medium text1">
Клиент всегда видит актуальные данные об интересующем его лоте,
включая статус и стоимость.
</p>
@@ -104,7 +104,7 @@ export function InteractivePresentation() {
<PhoneIcon />
</div>
</div>
<p className="font-medium text-sm">
<p className="font-medium text1">
Клиент всегда видит актуальные данные об интересующем его лоте,
включая статус и стоимость.
</p>
@@ -150,10 +150,10 @@ export function DoubleCard() {
'Стоимость',
'Инсоляция',
'Особенности планировки',
].map(tag => (
].map((tag) => (
<p
key={tag}
className="px-5 py-[11px] rounded-2xl leading-none border border-[#37393B] font-medium text-sm"
className="px-5 py-[11px] rounded-2xl border border-[#37393B] font-medium btnm"
>
{tag}
</p>
@@ -176,13 +176,15 @@ export function CardContainer({
}>) {
return (
<div
className={`bg-[linear-gradient(to_top_right,#7A7A7A50,transparent),linear-gradient(to_bottom_left,#7A7A7A50,transparent)] p-6 rounded-lg flex flex-col ${className}${text ? '' : ' justify-between'}`}
className={`bg-[linear-gradient(to_top_right,#7A7A7A50,transparent),linear-gradient(to_bottom_left,#7A7A7A50,transparent)] p-6 rounded-lg flex flex-col ${className}${
text ? '' : ' justify-between'
}`}
>
<p className="text-xl font-medium leading-5">{title}</p>
<p className="heading2 font-medium">{title}</p>
<div className={!!text ? 'flex-1 content-center m-auto' : ''}>
{children}
</div>
{text && <p className="text-sm font-medium">{text}</p>}
{text && <p className="text1 font-medium">{text}</p>}
</div>
);
}
+2 -2
View File
@@ -63,8 +63,8 @@ export function AwardItem({
alt={title}
/>
<div className="space-y-2">
<p className="font-medium text-2xl">{title}</p>
<p className="font-medium text-sm text-[#7A7A7A]">{description}</p>
<p className="font-medium heading1">{title}</p>
<p className="font-medium text1 text-[#7A7A7A]">{description}</p>
</div>
</div>
);
+21 -19
View File
@@ -186,14 +186,16 @@ export function NewCalculator() {
<div className="flex gap-x-3">
<div className="rounded-2xl bg-[linear-gradient(to_top,#7A7A7A40,#7A7A7A30)] p-7 w-1/2 relative overflow-hidden">
<div className="space-y-11">
<p className="font-medium text-sm">Срок реализации объекта</p>
<p className="font-medium text-2xl">
<span className="text-[64px]">
<p className="font-medium text1">Срок реализации объекта</p>
<p className="font-medium">
<span className="line2">
{calculated ? implementationPeriod : oldImplementationPeriod}
</span>{' '}
{calculated
? implementationPeriodEnding
: oldImplementationPeriodEnding}
<span className="heading2">
{calculated
? implementationPeriodEnding
: oldImplementationPeriodEnding}
</span>
</p>
<AnimatePresence>
{calculated && (
@@ -205,7 +207,7 @@ export function NewCalculator() {
y: 0,
}}
exit={{ opacity: 0, y: -10 }}
className="rounded-2xl px-3 py-[7px] font-medium text-xs absolute bottom-7 right-6 bg-[#FF4517] z-[2]"
className="rounded-2xl px-3 py-[7px] font-medium btns absolute bottom-7 right-6 bg-[#FF4517] z-[2]"
>
Продано
</motion.p>
@@ -236,12 +238,12 @@ export function NewCalculator() {
</div>
<div className="rounded-2xl bg-[linear-gradient(to_top,#7A7A7A40,#7A7A7A30)] p-7 w-1/2 relative overflow-hidden">
<div className="space-y-11">
<p className="font-medium text-sm">Ежемесячный доход</p>
<p className="font-medium text-2xl">
<span className="text-[64px]">
<p className="font-medium text1">Ежемесячный доход</p>
<p className="font-medium">
<span className="line2">
{calculated ? monthlyIncome : oldMonthlyIncome}
</span>{' '}
млн руб.
<span className="heading2">млн руб.</span>
</p>
</div>
<AnimatePresence>
@@ -327,13 +329,13 @@ export function NewRegionSelector({
return (
<div className="space-y-3 relative select-none">
<p className="font-medium text-[#7A7A7A]">Регион</p>
<p className="font-medium text-[#7A7A7A] btnl">Регион</p>
<div
ref={root}
className="px-8 py-6 bg-[#37393B99] rounded-2xl flex items-center justify-between w-[360px]"
onClick={() => setOpened(!opened)}
>
<p className="font-medium">{chosen.name}</p>
<p className="font-medium btnl">{chosen.name}</p>
{opened ? <ChevronUpIcon /> : <ChevronDownIcon />}
</div>
{opened && (
@@ -381,7 +383,7 @@ export function ConsultationRange({
return (
<div className="space-y-3 self-stretch">
<p className="font-medium text-[#7A7A7A]">Консультаций в месяц</p>
<p className="font-medium text-[#7A7A7A] btnl">Консультаций в месяц</p>
<div
className="px-7 py-9 bg-[#37393B99] rounded-2xl relative w-[360px] flex"
ref={root}
@@ -390,7 +392,7 @@ export function ConsultationRange({
className="absolute left-0 top-0 rounded-2xl h-full bg-[#37393B99] backdrop-blur-2xl flex justify-between gap-x-3 items-center z-[2]"
ref={panRef}
>
<p className="font-medium select-none pl-4">{consultations}</p>
<p className="font-medium select-none pl-4 btnl">{consultations}</p>
<div
className="self-center select-none absolute [user-drag:none]"
style={{
@@ -406,7 +408,7 @@ export function ConsultationRange({
/>
</div>
</div>
<p className="absolute self-center right-8 font-medium text-[#7A7A7A] z-[1]">
<p className="absolute self-center right-8 font-medium text-[#7A7A7A] z-[1] btnl">
из 350
</p>
</div>
@@ -428,8 +430,8 @@ export function StatsColumn({
return (
<div className="space-y-1 w-1/3">
<div className="flex gap-1 items-center justify-center">
<p className="font-medium text-[32px]">{value}</p>
<p className="rounded-2xl px-2 p-[7px] bg-[#37393B99] text-xs font-medium">
<p className="font-medium accent">{value}</p>
<p className="rounded-2xl px-2 p-[7px] bg-[#37393B99] btns font-medium">
{percents}%
</p>
</div>
@@ -443,7 +445,7 @@ export function StatsColumn({
}}
/>
</div>
<p className="text-center font-medium text-sm">{title}</p>
<p className="text-center font-medium text1">{title}</p>
</div>
);
}
@@ -13,7 +13,7 @@ export function NewMotivation() {
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
scrollY.on('change', value => setScrolled(value > 250));
scrollY.on('change', (value) => setScrolled(value > 250));
}, [scrollY]);
return (
@@ -27,10 +27,16 @@ export function NewMotivation() {
дороже
</Title>
<div
className={`grid ${scrolled ? 'grid-cols-[3fr_1fr] w-[calc((100vw-48px)*1.33)]' : 'grid-cols-[1fr_1fr_1fr] w-[calc(100vw-48px)]'} grid-rows-2 gap-3 relative transition-all`}
className={`grid ${
scrolled
? 'grid-cols-[3fr_1fr] w-[calc((100vw-48px)*1.33)]'
: 'grid-cols-[1fr_1fr_1fr] w-[calc(100vw-48px)]'
} grid-rows-2 gap-3 relative transition-all`}
>
<div
className={`${scrolled ? 'col-span-1' : 'col-span-2'} row-span-2 relative`}
className={`${
scrolled ? 'col-span-1' : 'col-span-2'
} row-span-2 relative`}
>
<video
ref={ref}
@@ -39,13 +45,17 @@ export function NewMotivation() {
loop
muted
playsInline
className={`rounded-xl object-cover h-full w-full transition-all ${scrolled ? 'aspect-[1400/734]' : ''}`}
className={`rounded-xl object-cover h-full w-full transition-all ${
scrolled ? 'aspect-[1400/734]' : ''
}`}
/>
</div>
<div
className={`p-6 bg-[linear-gradient(to_top_left,#7A7A7A50,transparent)] rounded-2xl relative col-span-1 row-span-1 transition-all ${scrolled ? 'translate-x-full' : ''}`}
className={`p-6 bg-[linear-gradient(to_top_left,#7A7A7A50,transparent)] rounded-2xl relative col-span-1 row-span-1 transition-all ${
scrolled ? 'translate-x-full' : ''
}`}
>
<p className="font-medium text-xl max-w-[60%]">
<p className="font-medium heading2 max-w-[60%]">
Интеграция в&nbsp;офисы продаж
</p>
<div className="absolute bottom-6 right-6">
@@ -58,9 +68,11 @@ export function NewMotivation() {
</div>
</div>
<div
className={`p-6 bg-[linear-gradient(to_top_left,#7A7A7A50,transparent)] rounded-2xl col-span-1 relative row-span-1 flex flex-col items-center justify-between space-y-7 transition-all ${scrolled ? 'translate-x-full' : ''}`}
className={`p-6 bg-[linear-gradient(to_top_left,#7A7A7A50,transparent)] rounded-2xl col-span-1 relative row-span-1 flex flex-col items-center justify-between space-y-7 transition-all ${
scrolled ? 'translate-x-full' : ''
}`}
>
<p className="font-medium text-xl self-stretch">
<p className="font-medium heading2 self-stretch">
Удаленная демонстрация
</p>
<div className="">
@@ -49,7 +49,7 @@ export function NewProjects() {
}
return (
<div className="space-y-6 mt-40">
<div className="space-y-16 mt-40">
<div className="flex">
<Title>
За 15 лет работы мы реализовали{' '}
+66 -28
View File
@@ -15,7 +15,15 @@ import { getCompaniesCount } from '@/utils/getCompaniesCount';
import { getProjectsGroupedByCities } from '@/utils/getProjectsGroupedByCities';
import { useQuery } from '@tanstack/react-query';
import Image from 'next/image';
import { useEffect, useRef, useState } from 'react';
import {
Dispatch,
SetStateAction,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useHover } from 'usehooks-ts';
export function NewStats() {
const { data: projects } = useQuery({
@@ -49,7 +57,7 @@ export function NewStats() {
</Title>
<div className="grid grid-cols-4 grid-rows-2 gap-3 aspect-[1400/462]">
<div className="p-6 flex flex-col justify-between row-span-2 col-span-2 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%] rounded-lg">
<p className="text-sm font-medium max-w-[35%]">
<p className="text1 font-medium max-w-[35%]">
Время реализации проекта сокращается до
</p>
<div className="font-medium">
@@ -59,18 +67,16 @@ export function NewStats() {
</div>
<NewFigure
percent={12}
range={[30, 42]}
text="Конверсия из бронирования в продажу увеличивается на"
className=""
/>
<NewFigure
percent={18}
range={[30, 48]}
text="Конверсия из консультации в бронирование увеличивается на"
className="col-start-3"
/>
<div className="col-start-4 row-start-1 bg-[linear-gradient(to_top_left,#7a7a7a50,transparent)] p-6 rounded-lg flex flex-col justify-between relative">
<p className="font-medium text-sm w-3/4">
<p className="font-medium text1 w-3/4">
Время на подготовку рекламных материалов сокращается до
</p>
<p className="font-medium text-[32px]">
@@ -115,6 +121,22 @@ export function Map({
projects: IProject[];
chooseCity: (_: ICityProjects) => void;
}) {
const { cityPoint } = useCityPointStore();
const [currentHovered, setCurrentHovered] = useState<number | null>(null);
const groupedProjects = useMemo(
() =>
Array.from(
getProjectsGroupedByCities(
projects.filter(({ city }) =>
cities.some(({ title }) => title === city)
)
).entries()
),
[projects]
);
return (
<div className="relative">
<Image
@@ -123,15 +145,15 @@ export function Map({
className="!relative aspect-[1027/560] object-cover"
fill
/>
{Array.from(
getProjectsGroupedByCities(
projects.filter(({ city }) => city !== 'Dubai' && city !== 'Абу-Даби')
).entries()
).map(([city, projects]) => {
{Array.from(groupedProjects).map(([city, projects], index) => {
const point = cities.find(({ title }) => title === city)!;
return point ? (
<CityPoint
active={
currentHovered === index ||
(cityPoint.title === city && currentHovered === null)
}
key={city}
chooseCity={chooseCity}
title={city}
@@ -142,7 +164,7 @@ export function Map({
}
x={point.x ?? 0}
y={point.y ?? 0}
extra={projects.length > 3 ? projects.length - 3 : 0}
{...{ setCurrentHovered, index }}
/>
) : null;
})}
@@ -156,16 +178,30 @@ export function CityPoint({
x,
y,
chooseCity,
}: ICityProjects & { chooseCity: (_: ICityProjects) => void }) {
active,
setCurrentHovered,
index,
}: ICityProjects & {
chooseCity: (_: ICityProjects) => void;
active: boolean;
setCurrentHovered: Dispatch<SetStateAction<number | null>>;
index: number;
}) {
const companiesWithMapIcon = companies.filter((company) => company.mapIcon);
const { cityPoint } = useCityPointStore();
const ref = useRef<HTMLDivElement>(null);
const hovered = useHover(ref);
useEffect(() => {
setCurrentHovered(hovered ? index : null);
}, [hovered, index, setCurrentHovered]);
return (
<div className="absolute" style={{ top: y + '%', left: x + '%' }}>
<div className="absolute" style={{ top: y + '%', left: x + '%' }} ref={ref}>
<div
className={`p-[11px] rounded-full bg-[#37393B99] hover:bg-transparent transition-colors relative z-10 peer ${
cityPoint.title === title ? 'bg-transparent' : ''
className={`p-[11px] rounded-full bg-[#37393B99] transition-colors relative z-10 ${
active ? 'bg-transparent' : ''
}`}
onClick={() => chooseCity({ title, x, y, companies })}
>
@@ -173,14 +209,14 @@ export function CityPoint({
</div>
<div
className={`transition-all duration-400 backdrop-blur-xl rounded-r-lg rounded-t-lg p-2 z-[20] min-w-[132px] ${
cityPoint.title === title ? 'block' : 'hidden'
} peer-hover:block bg-[#37393B99] absolute translate-x-5 -translate-y-full space-y-0.5`}
active ? 'block' : 'hidden'
} bg-[#37393B99] absolute translate-x-5 -translate-y-full space-y-0.5`}
style={{
left: x + '%',
top: y + '%',
}}
>
<p className="font-medium text-xs w-full leading-4">{title}</p>
<p className="font-medium text2 w-full leading-4">{title}</p>
<div className="flex py-px relative">
{companiesWithMapIcon
.slice(0, 3)
@@ -199,6 +235,7 @@ export function CityPoint({
src={process.env.NEXT_PUBLIC_S3_BUCKET + '' + mapIcon}
alt={title}
className="!relative object-cover"
sizes=""
fill
/>
)}
@@ -259,7 +296,7 @@ export function ProjectsSlider({
title: name,
company,
description,
tags: devices,
tags,
image,
stage = 1,
}) => (
@@ -277,19 +314,19 @@ export function ProjectsSlider({
className="w-2 h-2 rounded-full"
style={{ backgroundColor: company.color }}
/>
<p className="font-medium text-xs">{company.title}</p>
<p className="font-medium tbns">{company.title}</p>
</div>
)}
<div className="rounded-[17px] border border-[#37393B] text-xs flex items-center gap-x-1 px-2 py-1.5">
<div className="rounded-[17px] border border-[#37393B] btns flex items-center gap-x-1 px-2 py-1.5">
<ProgressPie value={Math.round((100 / 6) * stage)} />
{Math.round((100 / 6) * stage)}%
</div>
{devices.map((device) => (
{tags.map((tag) => (
<div
key={device}
className="rounded-[17px] border border-[#37393B] px-2 py-1.5"
key={tag}
className="rounded-[17px] border border-[#37393B] px-2 py-1.5 btns"
>
{device}
{tag}
</div>
))}
</div>
@@ -301,9 +338,10 @@ export function ProjectsSlider({
fill
className="!relative rounded object-cover aspect-[290/301]"
alt={name}
sizes=""
/>
</div>
<p className="text-[#ffffff99]">{description}</p>
<p className="text-[#ffffff99] text2">{description}</p>
</div>
</div>
)
@@ -311,7 +349,7 @@ export function ProjectsSlider({
</div>
</div>
<div className="bg-[linear-gradient(to_left_top,#7A7A7A66,transparent)] rounded-2xl p-6 flex justify-between items-center relative bottom-0">
<p className="text-[#ffffff99] font-medium text-sm">
<p className="text-[#ffffff99] font-medium text1">
<span className="text-white">{current}</span> из {count} в {city}
</p>
<div className="flex gap-x-1">
+14 -12
View File
@@ -32,11 +32,11 @@ export function Streaming() {
))}
<div className="border border-[#3D425C] w-1/4 rounded-2xl flex justify-center items-center p-6">
<div className="space-y-6 flex flex-col items-center">
<p className="text-center font-medium text-xl">
<p className="text-center font-medium heading2">
Расскажем и покажем как это работает на&nbsp;созвоне
</p>
<Button
className="rounded-lg"
className="rounded-lg btnl px-6 py-4"
onClick={() => setModal(<ModalWithForm />, 'form')}
>
Оставить заявку
@@ -77,16 +77,18 @@ function StreamingProject({
style={{ backgroundImage: `url(${image})` }}
>
<div className="space-y-3 font-medium">
<p className="text-xl">{title}</p>
<p className="heading1">{title}</p>
<div className="flex">
<div className="px-2 py-1.5 flex gap-1 items-center rounded-2xl bg-[#37393B99] text-xs">
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: company.color }}
/>
{company.title}
</div>
<p className="px-3 py-[7px] bg-[#37393B99] rounded-2xl text-xs">
{company && (
<div className="px-2 py-1.5 flex gap-1 items-center rounded-2xl bg-[#37393B99] btns">
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: company.color }}
/>
{company.title}
</div>
)}
<p className="px-3 py-[7px] bg-[#37393B99] rounded-2xl btns">
{city}
</p>
</div>
@@ -95,7 +97,7 @@ function StreamingProject({
href={href}
className="hidden group-hover:block absolute w-full h-full left-0 bottom-0 rounded-2xl font-medium bg-[radial-gradient(#000000CC,transparent)] backdrop-blur-[3px] content-center text-center"
>
<p className="flex gap-2 justify-center">
<p className="flex gap-2 justify-center btnl">
Начать демонстрацию <ArrowMoreIcon />
</p>
</Link>
@@ -16,7 +16,9 @@ export function ProjectCard(project: IProject) {
});
const stagePercentage = Math.round((100 / 6) * stage!);
const params = useSearchParams();
const chosenDevices = params.getAll('devices');
return (
@@ -38,11 +40,11 @@ export function ProjectCard(project: IProject) {
/>
<div className="absolute top-0 left-0 w-full h-full bg-gradient-card" />
<div className="relative flex flex-col gap-4">
<p className="lg:h4 h3 font-medium">{title}</p>
<p className="heading1 font-medium">{title}</p>
<div className="flex flex-wrap gap-2">
{company && (
<div className="px-2 py-1.5 font-medium rounded-2xl bg-[#37393B99] [backdrop-filter:blur(4px)] flex items-center gap-1">
<div className="px-2 py-1.5 font-medium rounded-2xl bg-[#37393B99] [backdrop-filter:blur(4px)] flex items-center gap-1 btns">
<div
className="w-2 h-2 rounded-full"
style={{
@@ -53,7 +55,7 @@ export function ProjectCard(project: IProject) {
</div>
)}
<div className="px-2 py-1.5 font-medium rounded-2xl bg-[#37393B99] [backdrop-filter:blur(4px)] flex items-center gap-1">
<div className="px-2 py-1.5 font-medium rounded-2xl bg-[#37393B99] [backdrop-filter:blur(4px)] flex items-center gap-1 btns">
{city}
</div>
@@ -67,7 +69,7 @@ export function ProjectCard(project: IProject) {
{stage! < 6 && (
<div className="bg-[#37393B99] px-3 py-2 rounded-full w-fit flex items-center gap-1 [backdrop-filter:blur(4px)]">
<p className="l-caption font-semibold">{stagePercentage}%</p>
<p className="btns font-semibold">{stagePercentage}%</p>
<ProgressPie value={stagePercentage} />
</div>
)}
@@ -1,6 +1,7 @@
'use client';
import { api } from '@/api';
import { projectsTags } from '@/consts/projectsTags';
import { IProject } from '@/types/IProject';
import { useQuery } from '@tanstack/react-query';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
@@ -28,24 +29,20 @@ export function ProjectsList({
});
return (
<div className="grid grid-cols-[1fr_4fr_1fr]">
<div className="space-y-2">
{[
'Все',
'Интерактивные презентации',
'Удаленная демонстрация',
'Архитектурная визуализации',
'Создание сайтов',
'Тур по 360-сферам',
].map((tag) => (
<div className="grid grid-cols-[1fr_4fr_1fr] items-start gap-x-[34px] relative">
<div className="space-y-2 sticky top-[50%] left-0 [align-self:start] row-span-full h-fit">
{['Все', ...projectsTags].map((tag) => (
<TagFilter
key={tag}
text={tag}
active={(tags.includes(tag) && tag !== 'Все') || tags.length === 0}
active={
(tags.includes(tag) && tag !== 'Все') ||
(tags.length === 0 && tag === 'Все')
}
/>
))}
</div>
<div className="col-span-4">
<div className="col-start-2">
{projects ? (
<ProjectsSection projects={projects} />
) : (
@@ -65,9 +62,11 @@ function TagFilter({ text, active }: { text: string; active?: boolean }) {
return (
<div
className={`px-5 py-2 ${
active ? 'bg-[#FFFFFF] text-black' : 'bg-[#37393B99] text-white'
} rounded-xl w-fit`}
className={`px-5 py-3 ${
active
? 'bg-[#FFFFFF] text-black'
: 'bg-[#37393B99] text-white text-nowrap'
} rounded-xl w-fit cursor-pointer font-medium btns`}
onClick={() => {
if (text === 'Все') params.delete('tags');
else if (params.getAll('tags').includes(text))
@@ -43,8 +43,8 @@ export function ProjectsPageHeader() {
}
return (
<div className="lg:space-y-10 space-y-5">
<Title className="text-center">
<div className="lg:space-y-10 space-y-5 relative">
<Title className="text-center" headerLevel={2}>
За 15 лет работы мы реализовали
<br />
{getProjectsCount(count ?? 0)} для застройщиков
@@ -48,14 +48,20 @@ export function ProjectsSection({ projects }: { projects: IProject[] }) {
}
return (
<div className="grid xl:grid-cols-[repeat(4,calc(25vw-24px))] md:grid-cols-3 sm:grid-cols-2 gap-4 pt-8 border-b border-[#3D425C] py-10">
<div
className={`grid ${
pathname === '/projects'
? 'grid-cols-3'
: 'xl:grid-cols-[repeat(4,calc(25vw-24px))]'
} md:grid-cols-3 sm:grid-cols-2 gap-4`}
>
{projects.map((project) => (
<ProjectCard key={project.id} {...project} />
))}
{pathname === '/' && (
<Link
href={'/projects'}
className="rounded-xl bg-[#232425] aspect-square h-fit self-center p-5 opacity-60 hover:opacity-100 transition-opacity flex items-center justify-center text-base font-medium gap-2"
className="rounded-xl bg-[#232425] aspect-square h-fit self-center p-5 opacity-60 hover:opacity-100 transition-opacity flex items-center justify-center btnl font-medium gap-2"
>
Смотреть все
<ArrowRightIcon />
-1
View File
@@ -1,7 +1,6 @@
'use client';
import { QueryClientProvider } from '@tanstack/react-query';
// import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';
import { getQueryClient } from './queryClient';
+1 -1
View File
@@ -12,7 +12,7 @@ function makeQueryClient() {
},
dehydrate: {
// include pending queries in dehydration
shouldDehydrateQuery: query =>
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) ||
query.state.status === 'pending',
},
-1
View File
@@ -5,5 +5,4 @@ export interface ICityProjects {
y: number;
title: string;
companies: ICompany[];
extra?: number;
}
+3 -5
View File
@@ -8,13 +8,11 @@ const manrope = Manrope({ subsets: ['latin'] });
export function NewFigure({
percent,
range: [from, to],
text,
className,
}: {
text: string;
percent: number;
range: [number, number];
className?: string;
}) {
const root = useRef<HTMLDivElement>(null);
@@ -47,9 +45,9 @@ export function NewFigure({
className
}
>
<p className="text-sm font-medium w-2/3">{text}</p>
<p className="font-medium text-[32px]">
<span className="text-8xl" ref={figureRef}>
<p className="text1 font-medium w-2/3">{text}</p>
<p className="font-medium accent">
<span className="line1 font-medium" ref={figureRef}>
{percent}
</span>
%
+13 -2
View File
@@ -4,6 +4,17 @@ export function Title({
className = '',
headerLevel = 2,
children,
}: PropsWithChildren<{ className?: string; headerLevel?: 1 | 2 }>) {
return <h1 className={`h${headerLevel} ` + className}>{children}</h1>;
}: PropsWithChildren<{
className?: string;
headerLevel?: 1 | 2;
}>) {
return (
<h1
className={
`font-medium ${headerLevel === 1 ? 'line1' : 'line2'} ` + className
}
>
{children}
</h1>
);
}
+7 -3
View File
@@ -6,6 +6,10 @@ const config: Config = {
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
theme: {
extend: {
// fontSize: {
// line1: 'line1',
// line2: 'line2',
// },
screens: {
'desktop-figma': '1600px',
},
@@ -45,10 +49,10 @@ const config: Config = {
const preflightStyles = postcss.parse(
fs.readFileSync(
require.resolve('tailwindcss/lib/css/preflight.css'),
'utf8',
),
'utf8'
)
);
preflightStyles.walkRules(rule => {
preflightStyles.walkRules((rule) => {
rule.selector = '.no-tailwind-base ' + rule.selector;
});
addBase(preflightStyles.nodes);