350 lines
9.2 KiB
TypeScript
350 lines
9.2 KiB
TypeScript
|
|
import { useEffect, useState } from 'react';
|
|||
|
|
import styled from 'styled-components';
|
|||
|
|
import type { Gender, User } from '../../types/user';
|
|||
|
|
import { TagsInput } from './TagsInput';
|
|||
|
|
|
|||
|
|
interface PersonModalProps {
|
|||
|
|
isOpen: boolean;
|
|||
|
|
user: User | null;
|
|||
|
|
availableTags: string[];
|
|||
|
|
onClose: () => void;
|
|||
|
|
onSave: (user: User) => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface PersonForm {
|
|||
|
|
name: string;
|
|||
|
|
gender: Gender;
|
|||
|
|
age: string;
|
|||
|
|
weight: string;
|
|||
|
|
height: string;
|
|||
|
|
avoid: string[];
|
|||
|
|
favorite: string[];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const defaultForm: PersonForm = {
|
|||
|
|
name: '',
|
|||
|
|
gender: 'm',
|
|||
|
|
age: '',
|
|||
|
|
weight: '',
|
|||
|
|
height: '',
|
|||
|
|
avoid: [],
|
|||
|
|
favorite: [],
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export function PersonModal({
|
|||
|
|
isOpen,
|
|||
|
|
user,
|
|||
|
|
availableTags,
|
|||
|
|
onClose,
|
|||
|
|
onSave,
|
|||
|
|
}: PersonModalProps) {
|
|||
|
|
const [form, setForm] = useState<PersonForm>(defaultForm);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!isOpen) return;
|
|||
|
|
|
|||
|
|
if (user) {
|
|||
|
|
setForm({
|
|||
|
|
name: user.name,
|
|||
|
|
gender: user.gender,
|
|||
|
|
age: String(user.age),
|
|||
|
|
weight: user.weight ? String(user.weight) : '',
|
|||
|
|
height: user.height ? String(user.height) : '',
|
|||
|
|
avoid: user.avoid ?? [],
|
|||
|
|
favorite: user.favorite ?? [],
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
setForm(defaultForm);
|
|||
|
|
}
|
|||
|
|
}, [isOpen, user]);
|
|||
|
|
|
|||
|
|
if (!isOpen) return null;
|
|||
|
|
|
|||
|
|
function updateForm<TName extends keyof PersonForm>(name: TName, value: PersonForm[TName]) {
|
|||
|
|
setForm(current => ({
|
|||
|
|
...current,
|
|||
|
|
[name]: value,
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleAvoidChange(tags: string[]) {
|
|||
|
|
setForm(current => ({
|
|||
|
|
...current,
|
|||
|
|
avoid: tags,
|
|||
|
|
favorite: current.favorite.filter(tag => !tags.includes(tag)),
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleFavoriteChange(tags: string[]) {
|
|||
|
|
setForm(current => ({
|
|||
|
|
...current,
|
|||
|
|
favorite: tags,
|
|||
|
|
avoid: current.avoid.filter(tag => !tags.includes(tag)),
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleSave() {
|
|||
|
|
const age = Number(form.age);
|
|||
|
|
|
|||
|
|
if (!form.name.trim()) {
|
|||
|
|
alert('Пожалуйста, введите имя.');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!age || age < 7) {
|
|||
|
|
alert('Возраст должен быть от 7 лет.');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onSave({
|
|||
|
|
id: user?.id,
|
|||
|
|
name: form.name.trim(),
|
|||
|
|
gender: form.gender,
|
|||
|
|
age,
|
|||
|
|
weight: form.weight ? Number(form.weight) : undefined,
|
|||
|
|
height: form.height ? Number(form.height) : undefined,
|
|||
|
|
avoid: form.avoid,
|
|||
|
|
favorite: form.favorite,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Overlay onClick={onClose}>
|
|||
|
|
<Content onClick={event => event.stopPropagation()}>
|
|||
|
|
<Header>
|
|||
|
|
<h2>{user ? 'Редактировать человека' : 'Добавить человека'}</h2>
|
|||
|
|
<CloseButton type="button" onClick={onClose}>
|
|||
|
|
×
|
|||
|
|
</CloseButton>
|
|||
|
|
</Header>
|
|||
|
|
|
|||
|
|
<FormGroup>
|
|||
|
|
<label>Имя</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
placeholder="Имя"
|
|||
|
|
value={form.name}
|
|||
|
|
onChange={event => updateForm('name', event.target.value)}
|
|||
|
|
/>
|
|||
|
|
</FormGroup>
|
|||
|
|
|
|||
|
|
<FormGroup>
|
|||
|
|
<label>Пол</label>
|
|||
|
|
<GenderToggles>
|
|||
|
|
<GenderButton
|
|||
|
|
type="button"
|
|||
|
|
$active={form.gender === 'f'}
|
|||
|
|
onClick={() => updateForm('gender', 'f')}
|
|||
|
|
>
|
|||
|
|
Женский
|
|||
|
|
</GenderButton>
|
|||
|
|
<GenderButton
|
|||
|
|
type="button"
|
|||
|
|
$active={form.gender === 'm'}
|
|||
|
|
onClick={() => updateForm('gender', 'm')}
|
|||
|
|
>
|
|||
|
|
Мужской
|
|||
|
|
</GenderButton>
|
|||
|
|
</GenderToggles>
|
|||
|
|
</FormGroup>
|
|||
|
|
|
|||
|
|
<FormRow>
|
|||
|
|
<FormGroup>
|
|||
|
|
<label>Возраст</label>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={form.age}
|
|||
|
|
onChange={event => updateForm('age', event.target.value)}
|
|||
|
|
/>
|
|||
|
|
<Hint>от 7 лет</Hint>
|
|||
|
|
</FormGroup>
|
|||
|
|
|
|||
|
|
<FormGroup>
|
|||
|
|
<label>Вес</label>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={form.weight}
|
|||
|
|
onChange={event => updateForm('weight', event.target.value)}
|
|||
|
|
/>
|
|||
|
|
</FormGroup>
|
|||
|
|
|
|||
|
|
<FormGroup>
|
|||
|
|
<label>Рост</label>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={form.height}
|
|||
|
|
onChange={event => updateForm('height', event.target.value)}
|
|||
|
|
/>
|
|||
|
|
</FormGroup>
|
|||
|
|
</FormRow>
|
|||
|
|
|
|||
|
|
<PrefsGrid>
|
|||
|
|
<TagsInput
|
|||
|
|
title="Запрещённые"
|
|||
|
|
type="avoid"
|
|||
|
|
value={form.avoid}
|
|||
|
|
availableTags={availableTags}
|
|||
|
|
excludedTags={form.favorite}
|
|||
|
|
onChange={handleAvoidChange}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<TagsInput
|
|||
|
|
title="Любимые"
|
|||
|
|
type="favorite"
|
|||
|
|
value={form.favorite}
|
|||
|
|
availableTags={availableTags}
|
|||
|
|
excludedTags={form.avoid}
|
|||
|
|
onChange={handleFavoriteChange}
|
|||
|
|
/>
|
|||
|
|
</PrefsGrid>
|
|||
|
|
|
|||
|
|
<SaveButton type="button" onClick={handleSave}>
|
|||
|
|
Сохранить
|
|||
|
|
</SaveButton>
|
|||
|
|
</Content>
|
|||
|
|
</Overlay>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const Overlay = styled.div`
|
|||
|
|
position: fixed;
|
|||
|
|
inset: 0;
|
|||
|
|
background: rgba(0, 0, 0, 0.4);
|
|||
|
|
backdrop-filter: blur(2px);
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
z-index: 1000;
|
|||
|
|
padding: 20px;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const Content = styled.div`
|
|||
|
|
background: #ffffff;
|
|||
|
|
border-radius: 24px;
|
|||
|
|
width: 100%;
|
|||
|
|
max-width: 440px;
|
|||
|
|
padding: 24px;
|
|||
|
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
|
|||
|
|
max-height: 95vh;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const Header = styled.div`
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
margin-bottom: 20px;
|
|||
|
|
|
|||
|
|
h2 {
|
|||
|
|
font-size: 20px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: ${({ theme }) => theme.colors.darkGreen};
|
|||
|
|
}
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const CloseButton = styled.button`
|
|||
|
|
font-size: 24px;
|
|||
|
|
color: #b0b0b0;
|
|||
|
|
cursor: pointer;
|
|||
|
|
line-height: 1;
|
|||
|
|
background: transparent;
|
|||
|
|
border: 0;
|
|||
|
|
|
|||
|
|
&:hover {
|
|||
|
|
color: ${({ theme }) => theme.colors.darkGreen};
|
|||
|
|
}
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const FormGroup = styled.div`
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
flex: 1;
|
|||
|
|
|
|||
|
|
label {
|
|||
|
|
display: block;
|
|||
|
|
font-size: 14px;
|
|||
|
|
color: ${({ theme }) => theme.colors.darkGreen};
|
|||
|
|
margin-bottom: 6px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
input {
|
|||
|
|
width: 100%;
|
|||
|
|
padding: 10px 12px;
|
|||
|
|
border: 1px solid #e1e1e1;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
outline: none;
|
|||
|
|
transition: 0.2s;
|
|||
|
|
|
|||
|
|
&:focus {
|
|||
|
|
border-color: ${({ theme }) => theme.colors.green};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const FormRow = styled.div`
|
|||
|
|
display: flex;
|
|||
|
|
gap: 16px;
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
|
|||
|
|
${FormGroup} {
|
|||
|
|
margin-bottom: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 600px) {
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const Hint = styled.span`
|
|||
|
|
display: block;
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: ${({ theme }) => theme.colors.gray};
|
|||
|
|
margin-top: 4px;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const GenderToggles = styled.div`
|
|||
|
|
display: flex;
|
|||
|
|
gap: 8px;
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const GenderButton = styled.button<{ $active: boolean }>`
|
|||
|
|
flex: 1;
|
|||
|
|
padding: 10px;
|
|||
|
|
border: 1px solid ${({ $active }) => ($active ? '#e9f6d9' : '#e1e1e1')};
|
|||
|
|
border-radius: 8px;
|
|||
|
|
background: ${({ $active }) => ($active ? '#e9f6d9' : '#ffffff')};
|
|||
|
|
font-size: 14px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: 0.2s;
|
|||
|
|
font-weight: ${({ $active }) => ($active ? 500 : 400)};
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const PrefsGrid = styled.div`
|
|||
|
|
display: flex;
|
|||
|
|
gap: 16px;
|
|||
|
|
margin-bottom: 20px;
|
|||
|
|
border-top: 1px solid #f0f0f0;
|
|||
|
|
padding-top: 16px;
|
|||
|
|
|
|||
|
|
@media (max-width: 600px) {
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const SaveButton = styled.button`
|
|||
|
|
width: 100%;
|
|||
|
|
padding: 12px;
|
|||
|
|
background: ${({ theme }) => theme.colors.green};
|
|||
|
|
color: #fff;
|
|||
|
|
border: none;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
font-size: 16px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: 0.2s;
|
|||
|
|
|
|||
|
|
&:hover {
|
|||
|
|
background: #5b8f29;
|
|||
|
|
}
|
|||
|
|
`;
|