384 lines
11 KiB
TypeScript
384 lines
11 KiB
TypeScript
import {useEffect, useState} from 'react';
|
||
import styled from 'styled-components';
|
||
import type {Gender, User} from '../../types/user';
|
||
import {Tag, TagsInput, TagsWrap} 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;
|
||
avoided: string[];
|
||
favorite: string[];
|
||
}
|
||
|
||
const defaultForm: PersonForm = {
|
||
name: '',
|
||
gender: 'm',
|
||
age: '',
|
||
weight: '',
|
||
height: '',
|
||
avoided: [],
|
||
favorite: [],
|
||
};
|
||
|
||
export function PersonModal({
|
||
isOpen,
|
||
user,
|
||
availableTags,
|
||
onClose,
|
||
onSave,
|
||
}: PersonModalProps) {
|
||
const [form, setForm] = useState<PersonForm>(defaultForm);
|
||
const [showExpanded, setShowExpanded] = useState<'' | 'avoid' | 'favorite'>('');
|
||
|
||
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) : '',
|
||
avoided: user.avoided ?? [],
|
||
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,
|
||
avoided: tags,
|
||
favorite: current.favorite.filter(tag => !tags.includes(tag)),
|
||
}));
|
||
}
|
||
|
||
function handleFavoriteChange(tags: string[]) {
|
||
setForm(current => ({
|
||
...current,
|
||
favorite: tags,
|
||
avoided: current.avoided.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,
|
||
avoided: form.avoided,
|
||
favorite: form.favorite,
|
||
});
|
||
}
|
||
|
||
return (
|
||
<Overlay onClick={() => showExpanded ? setShowExpanded('') : onClose()}>
|
||
{showExpanded ? (
|
||
<Content onClick={event => event.stopPropagation()}>
|
||
<Header>
|
||
<h2>{showExpanded == 'favorite' ? 'Любимые' : 'Не добавлять'}</h2>
|
||
<CloseButton type="button" onClick={() => setShowExpanded('')}>
|
||
×
|
||
</CloseButton>
|
||
</Header>
|
||
<FormGroup>
|
||
<TagsWrap>
|
||
{((showExpanded == 'favorite' ? form.favorite : form.avoided) || []).map(tag => (
|
||
<Tag key={tag} $type={showExpanded}>
|
||
{tag}
|
||
<button type="button"
|
||
onClick={() => (showExpanded == 'favorite' ? handleFavoriteChange : handleAvoidChange)(
|
||
((showExpanded == 'favorite' ? form.favorite : form.avoided) || []).filter(item => item != tag)
|
||
)}>
|
||
×
|
||
</button>
|
||
</Tag>
|
||
))}
|
||
</TagsWrap>
|
||
</FormGroup>
|
||
</Content>
|
||
) : (
|
||
<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.avoided}
|
||
availableTags={availableTags}
|
||
excludedTags={form.favorite}
|
||
onChange={handleAvoidChange}
|
||
onExpand={() => setShowExpanded('avoid')}
|
||
/>
|
||
|
||
<TagsInput
|
||
title="Любимые"
|
||
type="favorite"
|
||
value={form.favorite}
|
||
availableTags={availableTags}
|
||
excludedTags={form.avoided}
|
||
onChange={handleFavoriteChange}
|
||
onExpand={() => setShowExpanded('favorite')}
|
||
/>
|
||
</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;
|
||
overflow: auto;
|
||
`;
|
||
|
||
const Content = styled.div`
|
||
background: #ffffff;
|
||
border-radius: 12px;
|
||
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: #CEDFD0;
|
||
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: 16px;
|
||
color: ${({theme}) => theme.colors.darkGreen};
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
input {
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
border: 1px solid #e1e1e1;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
outline: none;
|
||
transition: 0.2s;
|
||
|
||
&::placeholder {
|
||
color: #A3ADA2;
|
||
}
|
||
&:focus {
|
||
border-color: ${({theme}) => theme.colors.green};
|
||
}
|
||
}
|
||
`;
|
||
|
||
const FormRow = styled.div`
|
||
display: flex;
|
||
gap: 16px;
|
||
margin-bottom: 16px;
|
||
|
||
${FormGroup} {
|
||
margin-bottom: 0;
|
||
}
|
||
`;
|
||
|
||
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 ? '#5EA12D' : '#E7EDE7')};
|
||
border-radius: 8px;
|
||
background: ${({$active}) => ($active ? '#E6F696' : '#ffffff')};
|
||
outline: ${({$active}) => ($active ? '1px solid #5EA12D' : '1px solid transparent')};
|
||
font-size: 16px;
|
||
cursor: pointer;
|
||
transition: 0.1s;
|
||
font-weight: 370;
|
||
|
||
&:hover {
|
||
outline: 1px solid #5EA12D;
|
||
border: 1px solid #5EA12D;
|
||
}
|
||
`;
|
||
|
||
const PrefsGrid = styled.div`
|
||
display: flex;
|
||
gap: 16px;
|
||
margin-bottom: 20px;
|
||
border-top: 1px solid #f0f0f0;
|
||
padding-top: 16px;
|
||
min-height: 200px;
|
||
`;
|
||
|
||
const SaveButton = styled.button`
|
||
width: 100%;
|
||
padding: 12px;
|
||
background: ${({theme}) => theme.colors.green};
|
||
color: #fff;
|
||
height: 54px;
|
||
box-sizing: border-box;
|
||
border: none;
|
||
border-radius: 12px;
|
||
font-size: 16px;
|
||
font-weight: 370;
|
||
cursor: pointer;
|
||
transition: 0.2s;
|
||
|
||
&:hover {
|
||
background: #5b8f29;
|
||
}
|
||
`; |