This commit is contained in:
2025-01-20 19:36:31 +05:00
parent f54368e10c
commit fbd5d8043f
29 changed files with 500 additions and 305 deletions
+1
View File
@@ -25,6 +25,7 @@
"react": "^18",
"react-circular-progressbar": "^2.1.0",
"react-dom": "^18",
"react-dropzone": "^14.3.5",
"react-hook-form": "^7.53.0",
"react-input-mask": "^2.0.4",
"react-phone-number-input": "^3.4.5",
+8 -4
View File
@@ -22,13 +22,13 @@ export function ItemActions({ item }: { item: IProject | ICompany }) {
const { company, ...project } = item;
setModal(
<ProjectFormModal action="edit" defaultValues={project} />,
'editProject'
'editProjectForm'
);
} else {
const { projects, ...company } = item;
setModal(
<CompanyFormModal action="edit" defaultValues={company} />,
'editCompany'
'editCompanyForm'
);
}
}
@@ -51,13 +51,17 @@ export function ItemActions({ item }: { item: IProject | ICompany }) {
onClick={handleEdit}
className="relative px-3 py-2 transition-opacity bg-[#37393B99] backdrop-blur-sm rounded-full bg-opacity-60 hover:bg-opacity-70"
>
<ClassNameWrapper element={<EditIcon />} className="w-4 h-4" />
<ClassNameWrapper className="w-4 h-4">
<EditIcon />
</ClassNameWrapper>
</button>
<button
onClick={handleDelete}
className="relative px-3 py-2 transition-opacity bg-[#37393B99] backdrop-blur-sm rounded-full bg-opacity-60 hover:bg-opacity-70"
>
<ClassNameWrapper element={<DeleteIcon />} className="w-4 h-4" />
<ClassNameWrapper className="w-4 h-4">
<DeleteIcon />
</ClassNameWrapper>
</button>
</div>
)
+20 -9
View File
@@ -7,7 +7,13 @@ import { getExampleNumber } from 'libphonenumber-js';
import examples from 'libphonenumber-js/mobile/examples';
import Link from 'next/link';
import { useEffect, useMemo, useState } from 'react';
import { useForm, UseFormGetValues, UseFormSetValue } from 'react-hook-form';
import {
FieldValues,
Path,
useForm,
UseFormGetValues,
UseFormSetValue,
} from 'react-hook-form';
import ReactInputMask from 'react-input-mask';
import { Country } from 'react-phone-number-input';
import { SelectPhoneCode } from './SelectPhoneCode';
@@ -67,6 +73,7 @@ export function FeedbackForm() {
<ProductOption
key={index}
text={option}
fieldName="products"
setValue={setValue}
getValues={getValues}
/>
@@ -127,32 +134,36 @@ export function FeedbackForm() {
);
}
export function ProductOption({
export function ProductOption<T extends FieldValues>({
text,
setValue,
getValues,
fieldName,
}: {
text: Product;
setValue: UseFormSetValue<IInput>;
getValues: UseFormGetValues<IInput>;
setValue: UseFormSetValue<T>;
getValues: UseFormGetValues<T>;
fieldName: Path<T>;
}) {
const [chosen, setChosen] = useState(false);
useEffect(
() =>
setValue(
'products',
fieldName,
chosen
? [...getValues('products'), text]
: getValues('products').filter((product: string) => product !== text)
? [...getValues(fieldName), text]
: getValues(fieldName).filter((product: string) => product !== text)
),
[chosen, getValues, setValue, text]
[chosen, fieldName, getValues, setValue, text]
);
return (
<div
className={`cursor-pointer px-6 py-[17px] transition-colors rounded-2xl font-medium btnm select-none ${
className={`cursor-pointer transition-colors rounded-2xl font-medium select-none ${
chosen ? 'bg-white text-black' : 'bg-[#37393B99]'
} ${
fieldName === 'products' ? 'px-6 py-[17px] btnm' : 'px-3 py-[9px] btns'
}`}
onClick={() => setChosen(!chosen)}
>
+9 -12
View File
@@ -18,10 +18,9 @@ export function Footer() {
<p className="text-[#7A7A7A] text1 font-medium">Позвонить</p>
<div className="lg:line2 sm:heading1 line2 flex items-center font-medium">
8 800 770 00 67
<ClassNameWrapper
className="lg:w-20 lg:h-20 sm:w-8 sm:h-8 w-9 h-9"
element={<ArrowMoreIcon />}
/>
<ClassNameWrapper className="lg:w-20 lg:h-20 sm:w-8 sm:h-8 w-9 h-9">
<ArrowMoreIcon />
</ClassNameWrapper>
</div>
</Link>
<Link
@@ -31,10 +30,9 @@ export function Footer() {
<p className="text-[#7A7A7A] text1 font-medium">Написать</p>
<div className="lg:line2 sm:heading1 line2 flex items-center font-medium">
info@graff.tech
<ClassNameWrapper
className="lg:w-20 lg:h-20 sm:w-8 sm:h-8 w-9 h-9"
element={<ArrowMoreIcon />}
/>
<ClassNameWrapper className="lg:w-20 lg:h-20 sm:w-8 sm:h-8 w-9 h-9">
<ArrowMoreIcon />
</ClassNameWrapper>
</div>
</Link>
<div className="gap-y-2 justify-stretch sm:gap-x-2 gap-x-1 sm:flex-col flex">
@@ -83,10 +81,9 @@ export function ContactLink({
href={href}
className={`rounded-2xl bg-[#37393B99] p-[18px] hover:bg-white transition-colors hover:text-black flex justify-center w-full ${className}`}
>
<ClassNameWrapper
className="lg:w-5 lg:h-5 sm:w-4 sm:h-4 w-5 h-5"
element={children}
/>
<ClassNameWrapper className="lg:w-5 lg:h-5 sm:w-4 sm:h-4 w-5 h-5">
{children}
</ClassNameWrapper>
</Link>
);
}
+12 -13
View File
@@ -61,10 +61,9 @@ export function Header() {
return (
<header className="lg:mt-5 relative flex items-center px-5">
<Link href={'/'}>
<ClassNameWrapper
element={<LogoWithTextIcon />}
className="max-lg:hidden"
/>
<ClassNameWrapper className="max-lg:hidden">
<LogoWithTextIcon />
</ClassNameWrapper>
</Link>
<div className="relative flex justify-center flex-1 m-auto">
<AnimatePresence>
@@ -85,7 +84,9 @@ export function Header() {
href={'/'}
className="aspect-square p-4 lg:hidden hover:bg-[#232425] rounded-xl content-center"
>
<ClassNameWrapper element={<LogoIcon />} className={'w-4 h-4'} />
<ClassNameWrapper className={'w-4 h-4'}>
<LogoIcon />
</ClassNameWrapper>
</Link>
<div ref={productsBtnRef} className="max-md:hidden">
<button
@@ -126,10 +127,9 @@ export function Header() {
className="!border-none p-[18px] hover:bg-[#232425] rounded-2xl active:opacity-50 outline-none"
onClick={() => setBurgerOpened((prev) => !prev)}
>
<ClassNameWrapper
element={burgerOpened ? <CloseIcon /> : <BurgerIcon />}
className="w-4 h-4 text-white"
/>
<ClassNameWrapper className="w-4 h-4 text-white">
{burgerOpened ? <CloseIcon /> : <BurgerIcon />}
</ClassNameWrapper>
</button>
</div>
{auth && (
@@ -212,10 +212,9 @@ export function Header() {
<p className="btnm font-medium">Смотреть кейс</p>
</Link>
) : (
<ClassNameWrapper
element={<SkolkovoIcon />}
className="max-lg:hidden"
/>
<ClassNameWrapper className="max-lg:hidden">
<SkolkovoIcon />
</ClassNameWrapper>
)}
</header>
);
+3 -2
View File
@@ -19,11 +19,12 @@ export function ModalContainer() {
modal && (
<div
className={
'fixed left-0 z-[11] w-full h-full flex justify-center items-start transition-opacity' +
'fixed left-0 z-[14] w-full h-full flex justify-center items-start transition-opacity' +
(name === 'video' || name === 'form'
? ' bg-black bg-opacity-90 [backdrop-filter:blur(10px);]'
: '') +
(name === 'menu' ? ' lg:top-16 top-12' : ' top-0')
(name === 'menu' ? ' lg:top-16 top-12' : ' top-0') +
(name.endsWith('Form') ? ' overflow-auto' : '')
}
>
{modal}
+3 -4
View File
@@ -49,10 +49,9 @@ export function SelectPhoneCode({
sizes=""
/>
<p className="btnl font-medium">{currentPhoneCode}</p>
<ClassNameWrapper
className="max-sm:w-4 sm:max-lg:w-5 flex-1"
element={open ? <ChevronUpIcon /> : <ChevronDownIcon />}
/>
<ClassNameWrapper className="max-sm:w-4 sm:max-lg:w-5 flex-1">
{open ? <ChevronUpIcon /> : <ChevronDownIcon />}
</ClassNameWrapper>
</button>
{open && (
<div className="space-y-1 absolute z-10 bg-[#14161F] top-[100%] -left-1 border border-t-0 rounded-b-lg border-[#37393B] max-h-[300px] overflow-y-auto overflow-x-hidden">
+93 -30
View File
@@ -1,7 +1,13 @@
import { api } from '@/api';
import { ClassNameWrapper } from '@/hocs/ClassNameWrapper';
import { Button } from '@/ui/Button';
import Image from 'next/image';
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
import { ChangeEvent, useEffect, useRef, useState } from 'react';
import Dropzone from 'react-dropzone';
import { FieldValues, Path, PathValue, UseFormSetValue } from 'react-hook-form';
import { PlusIcon } from './icons/PlusIcon';
import { RestartIcon } from './icons/RestartIcon';
import { TrashIcon } from './icons/TrashIcon';
export function MediaUploader<T extends FieldValues>({
dest,
@@ -9,9 +15,7 @@ export function MediaUploader<T extends FieldValues>({
fieldName,
item,
label,
className,
width = 300,
height = 300,
className = '',
}: {
dest: string;
setValue: UseFormSetValue<T>;
@@ -19,8 +23,6 @@ export function MediaUploader<T extends FieldValues>({
item: Record<'img', string> & Record<'id', string>;
label: string;
className?: string;
width?: number;
height?: number;
}) {
const [file, setFile] = useState<File>();
const [previewFile, setPreviewFile] = useState('');
@@ -34,7 +36,7 @@ export function MediaUploader<T extends FieldValues>({
setPreviewFile(URL.createObjectURL(targetFile));
}
const uploadFile = useCallback(async () => {
async function uploadFile() {
if (!file) return;
const formData = new FormData();
@@ -51,36 +53,97 @@ export function MediaUploader<T extends FieldValues>({
alert(`Error: ${error.message}`);
}
}
}, [dest, fieldName, file, setValue]);
}
useEffect(() => {
uploadFile();
}, [file, uploadFile]);
const ref = useRef<HTMLInputElement>(null);
return (
<label
className={
'relative border border-dashed border-neutral-500 px-3 py-2 cursor-pointer rounded-lg flex flex-col gap-2 ' +
className
}
<Dropzone
onDrop={([file]) => {
setFile(file);
setPreviewFile(URL.createObjectURL(file));
}}
noClick
>
<input
type="file"
accept={'image/*'}
className="absolute opacity-0"
onChange={handleChangeFile}
/>
<p className="truncate">{label}</p>
{(previewFile || item.img) && (
<Image
src={previewFile || process.env.NEXT_PUBLIC_S3_BUCKET + item.img}
width={300}
height={300}
alt={''}
className="pointer-events-none"
sizes=""
/>
{({ getRootProps, getInputProps, inputRef }) => (
<div
{...getRootProps()}
className={`relative border border-[#37393B] aspect-[824/340] px-3 py-2 rounded-lg flex flex-col justify-center items-center gap-2${
className ? ' ' + className : ''
}`}
>
<input
type="file"
ref={inputRef}
tabIndex={0}
accept={'image/*'}
className=""
onChange={handleChangeFile}
{...getInputProps()}
/>
{previewFile || item.img ? (
<>
<Image
src={
previewFile || process.env.NEXT_PUBLIC_S3_BUCKET + item.img
}
alt={''}
className="pointer-events-none aspect-square object-cover"
fill
sizes="calc(292/824*100%)"
/>
<div className="absolute left-6 top-6 flex gap-2">
<Button
onClick={() => inputRef.current?.click()}
color="secondary"
className="px-3 py-2 bg-[#37393B99]"
rounded="xl"
icon={
<ClassNameWrapper className="w-4 h-4">
<RestartIcon />
</ClassNameWrapper>
}
>
Заменить
</Button>
<button
onClick={() => {
setValue(fieldName, undefined!);
setPreviewFile('');
}}
className="bg-[#37393B99] px-3 py-2 rounded-xl"
>
<ClassNameWrapper className="w-4 h-4">
<TrashIcon />
</ClassNameWrapper>
</button>
</div>
</>
) : (
<div className="flex flex-col gap-4 items-center">
<p className="text-[#7A7A7A] font-medium text1 text-center max-w-[calc(346/824*100%)]">
{label}
</p>
<Button
onClick={() => inputRef.current?.click()}
color="secondary"
className="bg-[#37393B99] rounded-xl px-3 py-[9px]"
icon={
<ClassNameWrapper className="w-4 h-4">
<PlusIcon />
</ClassNameWrapper>
}
>
Выбрать
</Button>
</div>
)}
</div>
)}
</label>
</Dropzone>
);
}
+19
View File
@@ -0,0 +1,19 @@
export function RestartIcon() {
return (
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_2282_11898)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.66902 2.33908L10.976 1.55797L10.4268 0.638916L7.31283 2.49991L9.19665 5.41377L10.0958 4.83247L9.11258 3.31166C11.6606 3.81054 13.5953 6.09224 13.5953 8.84614C13.5953 11.9673 11.1101 14.482 8.06347 14.482C5.01685 14.482 2.53162 11.9673 2.53162 8.84614C2.53162 7.19267 3.22972 5.70784 4.34082 4.67735L3.61274 3.89232C2.29004 5.11907 1.46094 6.88552 1.46094 8.84614C1.46094 12.5414 4.40845 15.5527 8.06347 15.5527C11.7185 15.5527 14.666 12.5414 14.666 8.84614C14.666 5.71099 12.5444 3.06824 9.66902 2.33908Z"
fill="white"
/>
</g>
<defs>
<clipPath id="clip0_2282_11898">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
);
}
@@ -41,7 +41,7 @@ export function ArticleContentEditorModal({
data.source +
'" muted="true" autoplay="true" loop="true" playsinline style="aspect-ratio: 16/9; width: 768px"></video>',
images_upload_credentials: true,
images_upload_handler: async blobInfo => {
images_upload_handler: async (blobInfo) => {
const formData = new FormData();
formData.append('files', blobInfo.blob(), blobInfo.filename());
formData.append('dest', 'blog');
@@ -51,7 +51,7 @@ export function ArticleContentEditorModal({
return process.env.NEXT_PUBLIC_S3_BUCKET + res.files[0];
},
file_picker_types: 'image media',
file_picker_callback: async cb => {
file_picker_callback: async (cb) => {
videoUploadRef.current!.onchange = async function () {
const reader = new FileReader();
const file = videoUploadRef.current?.files?.[0];
@@ -65,7 +65,7 @@ export function ArticleContentEditorModal({
formData.append(
'files',
blobInfo?.blob()!,
blobInfo?.filename(),
blobInfo?.filename()
);
formData.append('dest', 'blog');
const res = await api
@@ -120,13 +120,15 @@ export function ArticleContentEditorModal({
ref={videoUploadRef}
/>
<button
onClick={e => {
onClick={(e) => {
e.preventDefault();
setModal(null, '');
}}
className="absolute top-4 right-4 z-[2]"
>
<ClassNameWrapper element={<CloseIcon />} className="text-black" />
<ClassNameWrapper className="text-black">
<CloseIcon />
</ClassNameWrapper>
</button>
</div>
);
+1 -2
View File
@@ -27,7 +27,6 @@ export function CompanyFormModal<TAction extends 'create' | 'edit'>({
action,
defaultValues,
}: ICompanyFormModalProps<TAction>) {
console.log(defaultValues);
const { setModal } = useModalStore();
const { register, handleSubmit, setValue } = useForm<ICompanyFormInput>({
@@ -48,7 +47,7 @@ export function CompanyFormModal<TAction extends 'create' | 'edit'>({
});
return (
<div className="text-black bg-white p-4 rounded-lg relative top-[100px] space-y-4">
<div className="text-black bg-[#232425] 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">
{action === 'create' ? 'Добавление' : 'Редактирование'} компании
@@ -40,7 +40,6 @@ export function ModalWithFeedbackForm() {
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
sendMail();
}
@@ -192,15 +191,13 @@ export function ModalWithFeedbackForm() {
className="py-5 px-6 mt-[213px]"
icon={
isLoading ? (
<ClassNameWrapper
element={<LoaderIcon />}
className="relative w-6 h-6 animate-spin"
/>
<ClassNameWrapper className="relative w-6 h-6 animate-spin">
<LoaderIcon />
</ClassNameWrapper>
) : (
<ClassNameWrapper
element={<MailIcon />}
className="relative w-6 h-6"
/>
<ClassNameWrapper className="relative w-6 h-6">
<MailIcon />
</ClassNameWrapper>
)
}
>
@@ -229,10 +226,9 @@ export function ModalWithFeedbackForm() {
width="full"
className="py-5 px-6 absolute top-[50vh]"
icon={
<ClassNameWrapper
className="w-6 h-6"
element={<ArrowRightIcon />}
/>
<ClassNameWrapper className="w-6 h-6">
<ArrowRightIcon />
</ClassNameWrapper>
}
onClick={() => setModal(false, '')}
>
+185 -121
View File
@@ -2,14 +2,22 @@
import { api } from '@/api';
import { projectsTags } from '@/consts/projectsTags';
import { AddItemWrapper } from '@/hocs/AddItemWrapper';
import { ClassNameWrapper } from '@/hocs/ClassNameWrapper';
import { useGetCompaniesQuery } from '@/queries/getCompanies';
import { useModalStore } from '@/stores/useModalStore';
import { IProject } from '@/types/IProject';
import { Product } from '@/types/Product';
import { Button } from '@/ui/Button';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { SubmitHandler, useForm } from 'react-hook-form';
import { CloseIcon } from '../icons/CloseIcon';
import { useEffect, useRef } from 'react';
import { SubmitHandler, useForm, useWatch } from 'react-hook-form';
import { ProductOption } from '../Layout/Feedback';
import { MediaUploader } from '../MediaUploader';
import { CheckIcon } from '../icons/CheckIcon';
import { CloseIcon } from '../icons/CloseIcon';
import { PlusIcon } from '../icons/PlusIcon';
import { CompanyFormModal } from './CompanyFormModal';
export interface IProjectFormInput {
title: string;
@@ -19,7 +27,7 @@ export interface IProjectFormInput {
image: string;
stage: number;
releaseDate: string;
tags: string[];
tags: Product[];
}
interface IProjectFormModalProps<TAction extends 'create' | 'edit'> {
@@ -52,147 +60,203 @@ export function ProjectFormModal<TAction extends 'create' | 'edit'>({
},
});
const { register, handleSubmit, setValue } = useForm<IProjectFormInput>({
defaultValues:
action === 'create'
? {
tags: [],
stage: 1,
releaseDate: new Date().toISOString().split('T')[0],
}
: {
...defaultValues,
releaseDate: defaultValues?.releaseDate.split('T')[0],
},
});
const { register, handleSubmit, setValue, getValues, control } =
useForm<IProjectFormInput>({
defaultValues:
action === 'create'
? {
tags: [],
stage: 1,
releaseDate: new Date().toISOString().split('T')[0],
}
: {
...defaultValues,
releaseDate: defaultValues?.releaseDate.split('T')[0],
},
});
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
const description = useWatch({ control });
useEffect(() => {
if (textAreaRef.current) {
textAreaRef.current.style.height = 'auto';
textAreaRef.current.style.height =
textAreaRef.current.scrollHeight + 'px';
}
}, [textAreaRef, description]);
const { ref, ...descriptionRegister } = register('description');
return (
<div className="relative p-4 space-y-4 text-black bg-white rounded-lg top-10">
<div className="flex justify-between items-center border-b border-[#ccc] pb-4 gap-4">
<p className="text-xl">
{action === 'create' ? 'Создание проекта' : 'Изменение проекта'}
</p>
<div className="relative py-10 space-y-4 bg-[#232425] rounded-[28px] top-5 w-[calc(954/1440*100vw)] pl-[75px] pr-[55px] overflow-y-auto">
<div className="absolute flex justify-between top-3 left-4 right-4">
<button
onClick={() => setModal(null, '')}
className="p-2 transition-colors rounded-full hover:bg-white hover:bg-opacity-10"
className="w-fit p-4"
onClick={() => setModal(null, 'addProjectForm')}
>
<CloseIcon />
<ClassNameWrapper className="w-4 h-4">
<CloseIcon />
</ClassNameWrapper>
</button>
<Button
color="primary"
onClick={() => setModal(null, 'addProjectForm')}
icon={
<ClassNameWrapper className="w-4 h-4">
<CheckIcon />
</ClassNameWrapper>
}
>
Сохранить
</Button>
</div>
<form
onSubmit={handleSubmit(mutate as SubmitHandler<IProjectFormInput>)}
className="space-y-4"
className="space-y-10"
>
<div className="grid grid-cols-3 gap-4">
<div className="flex flex-col">
<label htmlFor="name">Название</label>
<input
required
autoFocus
type="text"
className="px-3 py-2 border rounded-lg outline-none border-neutral-500"
placeholder="Название"
{...register('title', { required: true })}
id="name"
/>
<div className="flex flex-col gap-4">
<label htmlFor="title" className="heading2 font-medium">
Название проекта
</label>
<input
required
autoFocus
type="text"
className="py-5 border-b bg-transparent outline-none border-[#37393B] placeholder:btnl placeholder:text-[#7A7A7A] placeholder:font-medium"
placeholder="Название"
{...register('title', { required: true })}
id="title"
/>
</div>
<div className="flex flex-col gap-4">
<label htmlFor="city" className="heading2 font-medium">
Город
</label>
<input
required
type="text"
className="py-5 border-b bg-transparent outline-none border-[#37393B] placeholder:btnl placeholder:text-[#7A7A7A] placeholder:font-medium"
{...register('city', { required: true })}
id="city"
placeholder="Екатеринбург"
/>
</div>
<div className="space-y-4">
<label htmlFor="tags" className="heading2 font-medium">
Выберете категории
</label>
<div className="flex flex-wrap gap-2">
{(projectsTags as Product[]).map((option, index) => (
<ProductOption
key={index}
text={option}
fieldName="tags"
setValue={setValue}
getValues={getValues}
/>
))}
</div>
<div className="flex flex-col">
<label htmlFor="city">Город</label>
<input
type="text"
className="px-3 py-2 border rounded-lg outline-none border-neutral-500"
{...register('city', { required: true })}
id="city"
placeholder="Город"
/>
</div>
<div className="flex flex-col">
<label htmlFor="releaseDate">Дата релиза</label>
<input
type="date"
{...register('releaseDate', {
valueAsDate: true,
required: true,
})}
defaultValue={defaultValues?.releaseDate.split('T')[0]}
id="releaseDate"
className="px-3 py-2 border rounded-lg outline-none border-neutral-500"
/>
</div>
<div className="flex flex-col">
<label htmlFor="description">Описание</label>
<textarea
className="px-3 py-2 border rounded-lg outline-none border-neutral-500"
placeholder="Описание"
{...register('description', { required: false })}
id="description"
/>
</div>
<div className="flex flex-col">
<label htmlFor="stage">Стадия</label>
<select
{...register('stage', { valueAsNumber: true })}
id="stage"
className="px-3 py-2 border rounded-lg outline-none border-neutral-500"
>
{Array.from({ length: 6 }, (_, i) => i + 1).map((stage) => (
<option key={stage} value={stage} label={stage.toString()} />
))}
</select>
</div>
<div className="flex flex-col">
<label htmlFor="company">Компания</label>
</div>
<MediaUploader
dest="projects"
setValue={setValue}
fieldName="image"
item={{ img: defaultValues?.image ?? '', id: '' }}
label="Загрузите или перетащите изображение для превью (рекомендованнный размер 1080/1080 px)"
/>
<div className="flex flex-col gap-4">
<label htmlFor="company" className="heading2 font-medium">
Выберете застройщика из списка или добавьте нового
</label>
<div className="flex gap-4">
<select
style={{
backgroundImage: `url("data:image/svg+xml;charset=UTF-8,<svg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M12.0001 17.707L19.7072 9.99992L18.293 8.58571L12.0001 14.8786L5.70718 8.58571L4.29297 9.99992L12.0001 17.707Z' fill='white'/></svg>")`,
}}
id="company"
{...register('companyId')}
className="col-start-3 px-3 py-2 border rounded-lg outline-none border-neutral-500 h-fit"
className="px-8 py-7 rounded-2xl outline-none bg-[#37393B99] bg-no-repeat bg-[right_32px_top_24px] h-fit font-medium btnl appearance-none"
>
<option value={undefined}>-</option>
<option value={undefined}>Не выбрано</option>
{companies?.map((company) => (
<option key={company.id} value={company.id} className="flex">
{company.title}
</option>
))}
</select>
</div>
<MediaUploader
dest="projects"
setValue={setValue}
fieldName="image"
item={{ img: defaultValues?.image ?? '', id: '' }}
label="Выберите изображение"
/>
<div className="flex flex-col col-start-2">
<label htmlFor="devices" className="w-fit">
Категории
</label>
{projectsTags.map((tag) => (
<div key={tag} className="space-x-1">
<input
type="checkbox"
{...register('tags')}
id={tag}
value={tag}
key={tag}
/>
<label htmlFor={tag} className="select-none">
{tag}
</label>
</div>
))}
<AddItemWrapper
modal={<CompanyFormModal action={'create'} />}
modalName="companyForm"
>
<Button
rounded="2xl"
color="secondary"
className="bg-[#37393B99] h-full border-none gap-2"
icon={
<ClassNameWrapper className="w-5 h-5">
<PlusIcon />
</ClassNameWrapper>
}
>
Добавить
</Button>
</AddItemWrapper>
</div>
</div>
<div className="flex justify-between">
<Button
className="text-white bg-black/40"
color="secondary"
onClick={() => setModal(null, '')}
<div className="flex flex-col gap-4">
<label htmlFor="description" className="heading2 font-medium">
О проекте
</label>
<textarea
className="py-5 border-b bg-transparent outline-none border-[#37393B] placeholder:btnl placeholder:text-[#7A7A7A] placeholder:font-medium h-auto max-h-[300px] resize-none"
placeholder="Описание"
{...descriptionRegister}
rows={1}
ref={(e) => {
ref(e);
textAreaRef.current = e;
}}
id="description"
/>
</div>
<div className="flex flex-col gap-4">
<label htmlFor="stage" className="heading2 font-medium">
Этап проекта
</label>
<select
style={{
backgroundImage: `url("data:image/svg+xml;charset=UTF-8,<svg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M12.0001 17.707L19.7072 9.99992L18.293 8.58571L12.0001 14.8786L5.70718 8.58571L4.29297 9.99992L12.0001 17.707Z' fill='white'/></svg>")`,
}}
{...register('stage', { valueAsNumber: true })}
id="stage"
className="px-8 py-7 rounded-lg outline-none bg-[#37393B99] bg-no-repeat bg-[right_32px_top_24px] appearance-none"
>
Отмена
</Button>
<Button className="text-white" type="submit">
{action === 'create' ? 'Добавить проект' : 'Сохранить изменения'}
</Button>
{Array.from({ length: 6 }, (_, i) => i + 1).map((stage) => (
<option
className=""
key={stage}
value={stage}
label={stage.toString()}
/>
))}
</select>
</div>
<div className="flex flex-col gap-4">
<label htmlFor="releaseDate" className="heading2 font-medium">
Дата релиза
</label>
<input
type="date"
{...register('releaseDate', {
valueAsDate: true,
required: true,
})}
defaultValue={defaultValues?.releaseDate.split('T')[0]}
id="releaseDate"
className="py-5 border-b border-[#37393B] rounded-lg outline-none bg-transparent text-[#7A7A7A] placeholder:text-[#7A7A7A]"
/>
</div>
</form>
</div>
@@ -1,6 +1,6 @@
'use client';
import { AddItemWrapper } from '@/hocs/AddItemButton';
import { AddItemWrapper } from '@/hocs/AddItemWrapper';
import { useCheckAuthQuery } from '@/queries/checkAuth';
import { Title } from '@/ui/Title';
@@ -149,15 +149,17 @@ export function Calculator() {
/>
<GradientButton
onClick={() => setCalculated(!calculated)}
active={calculated}
className="flex gap-x-3 items-center max-md:absolute top-0 max-md:-mt-7 left-[calc(50%-24px)]"
>
<ClassNameWrapper
element={<LogoIcon />}
className={
'lg:w-7 lg:h-7 w-5 h-5 ' +
(!calculated ? 'opacity-50' : 'opacity-100')
}
/>
>
<LogoIcon />
</ClassNameWrapper>
</GradientButton>
</div>
<div className="space-y-10 lg:max-w-[1040px] w-full max-lg:order-1">
@@ -72,10 +72,9 @@ export function ConsultationRange({
onMouseLeave={handleMouseUp}
onMouseUp={handleMouseUp}
>
<ClassNameWrapper
className="text-[#7A7A7A] select-none w-6 h-6"
element={<PanDotsIcon />}
/>
<ClassNameWrapper className="text-[#7A7A7A] select-none w-6 h-6">
<PanDotsIcon />
</ClassNameWrapper>
</div>
</div>
<p className="self-center right-8 font-medium text-[#7A7A7A] z-[1] btnl">
+4 -2
View File
@@ -3,7 +3,7 @@
import { PlusIcon } from '@/components/icons/PlusIcon';
import { ItemActions } from '@/components/ItemActions';
import { CompanyFormModal } from '@/components/modals/CompanyFormModal';
import { AddItemWrapper } from '@/hocs/AddItemButton';
import { AddItemWrapper } from '@/hocs/AddItemWrapper';
import { ClassNameWrapper } from '@/hocs/ClassNameWrapper';
import { useGetCompaniesQuery } from '@/queries/getCompanies';
import { GradientButton } from '@/ui/GradientButton';
@@ -53,7 +53,9 @@ export function Clients() {
className="aspect-square flex flex-col items-center justify-center gap-3"
>
<GradientButton>
<ClassNameWrapper element={<PlusIcon />} className="w-7 h-7" />
<ClassNameWrapper className="w-7 h-7">
<PlusIcon />
</ClassNameWrapper>
</GradientButton>
<p className="btnl font-medium">Добавить</p>
</AddItemWrapper>
@@ -71,14 +71,12 @@ export function InteractivePresentation() {
<div className="rounded-lg bg-[#B1EC52] text-black font-medium w-12 h-12 rotate-[-4deg] content-center text-center z-[2] relative">
2K
</div>
<ClassNameWrapper
element={<Flat1 />}
className="relative right-1 z-[1]"
/>
<ClassNameWrapper
element={<Flat2 />}
className="rotate-[-4deg] right-2 relative"
/>
<ClassNameWrapper className="relative right-1 z-[1]">
<Flat1 />
</ClassNameWrapper>
<ClassNameWrapper className="rotate-[-4deg] right-2 relative">
<Flat2 />
</ClassNameWrapper>
</div>
<p className="font-medium text1">
Клиент всегда видит актуальные данные об интересующем его лоте,
@@ -96,10 +94,9 @@ export function InteractivePresentation() {
sizes=""
/>
<div className="w-12 h-12 bg-[#37393B] rounded-lg flex justify-center items-center z-[1] relative right-1">
<ClassNameWrapper
className="rotate-[4deg]"
element={<MailIcon />}
/>
<ClassNameWrapper className="rotate-[4deg]">
<MailIcon />
</ClassNameWrapper>
</div>
<div className="w-12 h-12 bg-[#37393B] rounded-lg flex justify-center items-center rotate-[-4deg] relative right-2">
<PhoneIcon />
@@ -105,7 +105,9 @@ export function ProjectsSlider({
className="px-3 py-2 rounded-xl bg-[#37393B99]"
onClick={() => setCurrent((prev) => Math.max(prev - 1, 1))}
>
<ClassNameWrapper element={<ArrowLeftIcon />} className="w-4 h-4" />
<ClassNameWrapper className="w-4 h-4">
<ArrowLeftIcon />
</ClassNameWrapper>
</button>
<button
className="px-3 py-2 rounded-xl bg-[#37393B99]"
@@ -113,10 +115,9 @@ export function ProjectsSlider({
setCurrent((prev) => Math.min(projects.length, prev + 1))
}
>
<ClassNameWrapper
element={<ArrowRightIcon />}
className="w-4 h-4"
/>
<ClassNameWrapper className="w-4 h-4">
<ArrowRightIcon />
</ClassNameWrapper>
</button>
</div>
</div>
@@ -2,7 +2,7 @@
import { PlusIcon } from '@/components/icons/PlusIcon';
import { ProjectFormModal } from '@/components/modals/ProjectFormModal';
import { AddItemWrapper } from '@/hocs/AddItemButton';
import { AddItemWrapper } from '@/hocs/AddItemWrapper';
import { ClassNameWrapper } from '@/hocs/ClassNameWrapper';
import { useCheckAuthQuery } from '@/queries/checkAuth';
import { useGetProjectsCountQuery } from '@/queries/getProjectsCount';
@@ -24,7 +24,7 @@ export function ProjectsPageHeader() {
</Title>
{auth && (
<AddItemWrapper
modalName="addProject"
modalName="addProjectForm"
modal={<ProjectFormModal action={'create'} />}
className="btns sticky top-0"
>
@@ -32,7 +32,9 @@ export function ProjectsPageHeader() {
color="primary"
className="btns rounded-xl gap-2 py-2"
icon={
<ClassNameWrapper className="w-4 h-4" element={<PlusIcon />} />
<ClassNameWrapper className="w-4 h-4">
<PlusIcon />
</ClassNameWrapper>
}
>
Добавить проект
@@ -31,7 +31,9 @@ export function ProjectsSection({ projects }: { projects: IProject[] }) {
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"
>
Смотреть все
<ClassNameWrapper className="w-5 h-5" element={<ArrowRightIcon />} />
<ClassNameWrapper className="w-5 h-5">
<ArrowRightIcon />
</ClassNameWrapper>
</Link>
)}
</div>
@@ -82,7 +82,9 @@ export function TagsFilters({
className="bg-gradient rounded-2xl btnm flex items-center gap-2 pl-6 py-4 pr-4 font-medium z-[11] -mb-5 mt-5"
>
Применить
<ClassNameWrapper className="w-4 h-4" element={<CheckIcon />} />
<ClassNameWrapper className="w-4 h-4">
<CheckIcon />
</ClassNameWrapper>
</button>
</div>
</>
-43
View File
@@ -1,43 +0,0 @@
import { useCheckAuthQuery } from '@/queries/checkAuth';
import { useModalStore } from '@/stores/useModalStore';
import { PropsWithChildren, ReactNode, useEffect, useRef } from 'react';
export function AddItemWrapper({
modal,
modalName,
children,
className,
}: PropsWithChildren<{
modal: ReactNode;
modalName: string;
className?: string;
}>) {
const { data: auth } = useCheckAuthQuery();
const { setModal } = useModalStore();
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (auth) {
ref.current?.children.item(0)!.addEventListener('click', () => {
console.log('asd');
setModal(modal, modalName);
});
return () => {
ref.current?.children.item(0)!.removeEventListener('click', () => {
setModal(modal, modalName);
});
};
}
}, [auth]);
return (
auth && (
<div ref={ref} className={className}>
{children}
</div>
)
);
}
+38
View File
@@ -0,0 +1,38 @@
import { useCheckAuthQuery } from '@/queries/checkAuth';
import { useModalStore } from '@/stores/useModalStore';
import { PropsWithChildren, ReactNode } from 'react';
export function AddItemWrapper({
modal,
modalName,
children,
className,
}: PropsWithChildren<{
modal: ReactNode;
modalName: string;
className?: string;
}>) {
const { data: auth } = useCheckAuthQuery();
const { setModal } = useModalStore();
return (
auth && (
<div
ref={(e) => {
e?.children
.item(0)!
.addEventListener('click', () => setModal(modal, modalName));
return () =>
e?.children
.item(0)
?.removeEventListener('click', () => setModal(modal, modalName));
}}
className={className}
>
{children}
</div>
)
);
}
+8 -6
View File
@@ -1,22 +1,24 @@
'use client';
import { ReactNode, useEffect, useRef } from 'react';
import { PropsWithChildren, useEffect, useRef } from 'react';
interface Props {
element: ReactNode;
className: string;
}
export function ClassNameWrapper({ element, className }: Props) {
export function ClassNameWrapper({
className,
children,
}: PropsWithChildren<Props>) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!className.split(' ').length) return;
ref.current?.children.item(0)?.setAttributeNS(null, 'class', '');
ref.current?.children
.item(0)
?.classList.add(...className.split(' ').filter(Boolean));
}, [className, element]);
}, [className, children]);
return <div ref={ref}>{element}</div>;
return <div ref={ref}>{children}</div>;
}
+2 -1
View File
@@ -1,4 +1,5 @@
import { ICompany } from './ICompany';
import { Product } from './Product';
export type Device = 'Stream' | 'Touch' | 'Mobile' | 'VR';
@@ -12,5 +13,5 @@ export interface IProject {
image: string;
stage: number;
releaseDate: string;
tags: string[];
tags: Product[];
}
+12 -3
View File
@@ -2,18 +2,27 @@ import { PropsWithChildren } from 'react';
export function GradientButton({
children,
active,
onClick,
className,
}: PropsWithChildren<{ onClick?: () => void; className?: string }>) {
}: PropsWithChildren<{
onClick?: () => void;
className?: string;
active?: boolean;
}>) {
return (
<button
onClick={onClick}
className={`bg-gradient-to-bl p-px rounded-full from-[#BE69F5] to-[#798FFF00]${
className={`bg-gradient-to-bl outline-none p-px rounded-full from-[#BE69F5] to-[#798FFF00]${
className ? ' ' + className : ''
}`}
>
<div className="p-2 bg-black rounded-full">
<div className="p-[14px] rounded-full bg-[#37393B99] active:bg-gradient-to-r from-[#6078F2] to-[#C868F5]">
<div
className={`p-[14px] rounded-full bg-[#37393B99] active:bg-gradient-to-r from-[#6078F2] to-[#C868F5]${
active ? ' bg-gradient-to-r' : ''
}`}
>
{children}
</div>
</div>
+6 -6
View File
@@ -20,15 +20,15 @@ export function NavLink({
<Link
href={href}
className={
`relative btn-text font-medium text-[#9299BD] [&:not(:last-child)]:border-r px-10 gap-x-2 border-[#3D425C] hover:text-white ${pathname === href ? 'text-white ' : ''}py-6 card-gradient-2 active:bg-[#14161F]` +
className
`relative btn-text font-medium text-[#9299BD] [&:not(:last-child)]:border-r px-10 gap-x-2 border-[#3D425C] hover:text-white ${
pathname === href ? 'text-white ' : ''
}py-6 card-gradient-2 active:bg-[#14161F]` + className
}
>
{pathname === href && (
<ClassNameWrapper
element={<CubeIcon />}
className="text-[#798FFF] absolute left-5"
/>
<ClassNameWrapper className="text-[#798FFF] absolute left-5">
<CubeIcon />
</ClassNameWrapper>
)}
{children}
</Link>
+26
View File
@@ -642,6 +642,11 @@ ast-types-flow@^0.0.8:
resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6"
integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==
attr-accept@^2.2.4:
version "2.2.5"
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.5.tgz#d7061d958e6d4f97bf8665c68b75851a0713ab5e"
integrity sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==
autoprefixer@^10.4.19:
version "10.4.19"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.19.tgz#ad25a856e82ee9d7898c59583c1afeb3fa65f89f"
@@ -1447,6 +1452,13 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"
file-selector@^2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-2.1.2.tgz#fe7c7ee9e550952dfbc863d73b14dc740d7de8b4"
integrity sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==
dependencies:
tslib "^2.7.0"
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
@@ -2561,6 +2573,15 @@ react-dom@^18:
loose-envify "^1.1.0"
scheduler "^0.23.2"
react-dropzone@^14.3.5:
version "14.3.5"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.3.5.tgz#1a8bd312c8a353ec78ef402842ccb3589c225add"
integrity sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ==
dependencies:
attr-accept "^2.2.4"
file-selector "^2.1.0"
prop-types "^15.8.1"
react-hook-form@^7.53.0:
version "7.53.0"
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.53.0.tgz#3cf70951bf41fa95207b34486203ebefbd3a05ab"
@@ -3114,6 +3135,11 @@ tslib@^2.4.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0"
integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==
tslib@^2.7.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"