marketing/src/components/Family/PersonModal.tsx

390 lines
12 KiB
TypeScript
Raw Normal View History

2026-08-30 11:47:34 +00:00
import {useEffect, useState} from 'react';
2026-08-24 10:07:16 +00:00
import styled from 'styled-components';
2026-08-30 11:47:34 +00:00
import type {Gender, User} from '../../types/user';
import {Tag, TagsInput, TagsWrap} from './TagsInput';
2026-09-03 10:50:12 +00:00
import ym from "react-yandex-metrika";
2026-08-24 10:07:16 +00:00
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;
2026-08-30 11:47:34 +00:00
avoided: string[];
2026-08-24 10:07:16 +00:00
favorite: string[];
}
const defaultForm: PersonForm = {
name: '',
gender: 'm',
age: '',
weight: '',
height: '',
2026-08-30 11:47:34 +00:00
avoided: [],
2026-08-24 10:07:16 +00:00
favorite: [],
};
export function PersonModal({
isOpen,
user,
availableTags,
onClose,
onSave,
}: PersonModalProps) {
const [form, setForm] = useState<PersonForm>(defaultForm);
2026-08-30 11:47:34 +00:00
const [showExpanded, setShowExpanded] = useState<'' | 'avoid' | 'favorite'>('');
2026-08-24 10:07:16 +00:00
useEffect(() => {
if (!isOpen) return;
if (user) {
2026-09-03 10:50:12 +00:00
ym('reachGoal', 'ac_family_edit');
2026-08-24 10:07:16 +00:00
setForm({
name: user.name,
gender: user.gender,
age: String(user.age),
weight: user.weight ? String(user.weight) : '',
height: user.height ? String(user.height) : '',
2026-08-30 11:47:34 +00:00
avoided: user.avoided ?? [],
2026-08-24 10:07:16 +00:00
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,
2026-08-30 11:47:34 +00:00
avoided: tags,
2026-08-24 10:07:16 +00:00
favorite: current.favorite.filter(tag => !tags.includes(tag)),
}));
}
function handleFavoriteChange(tags: string[]) {
setForm(current => ({
...current,
favorite: tags,
2026-08-30 11:47:34 +00:00
avoided: current.avoided.filter(tag => !tags.includes(tag)),
2026-08-24 10:07:16 +00:00
}));
}
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,
2026-08-30 11:47:34 +00:00
avoided: form.avoided,
2026-08-24 10:07:16 +00:00
favorite: form.favorite,
});
}
return (
2026-09-02 06:41:12 +00:00
<Overlay onClick={() => showExpanded ? setShowExpanded('') : onClose()}>
2026-08-30 11:47:34 +00:00
{showExpanded ? (
<Content onClick={event => event.stopPropagation()}>
<Header>
<h2>{showExpanded == 'favorite' ? 'Любимые' : 'Не добавлять'}</h2>
<CloseButton type="button" onClick={() => setShowExpanded('')}>
×
</CloseButton>
</Header>
2026-08-24 10:07:16 +00:00
<FormGroup>
2026-08-30 11:47:34 +00:00
<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>
2026-08-24 10:07:16 +00:00
</FormGroup>
2026-08-30 11:47:34 +00:00
</Content>
) : (
<Content onClick={event => event.stopPropagation()}>
<Header>
<h2>{user ? 'Редактировать человека' : 'Добавить человека'}</h2>
<CloseButton type="button" onClick={onClose}>
×
</CloseButton>
</Header>
2026-08-24 10:07:16 +00:00
<FormGroup>
2026-08-30 11:47:34 +00:00
<label>Имя</label>
2026-08-24 10:07:16 +00:00
<input
2026-08-30 11:47:34 +00:00
type="text"
placeholder="Имя"
value={form.name}
onChange={event => updateForm('name', event.target.value)}
2026-08-24 10:07:16 +00:00
/>
</FormGroup>
<FormGroup>
2026-08-30 11:47:34 +00:00
<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>
2026-08-24 10:07:16 +00:00
</FormGroup>
2026-08-30 11:47:34 +00:00
<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>
)}
2026-08-24 10:07:16 +00:00
</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;
2026-09-02 06:41:12 +00:00
overflow: auto;
2026-08-24 10:07:16 +00:00
`;
const Content = styled.div`
background: #ffffff;
2026-08-31 08:55:33 +00:00
border-radius: 12px;
2026-08-24 10:07:16 +00:00
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;
2026-08-30 11:47:34 +00:00
color: ${({theme}) => theme.colors.darkGreen};
2026-08-24 10:07:16 +00:00
}
`;
const CloseButton = styled.button`
font-size: 24px;
2026-08-31 08:55:33 +00:00
color: #CEDFD0;
2026-08-24 10:07:16 +00:00
cursor: pointer;
line-height: 1;
background: transparent;
border: 0;
&:hover {
2026-08-30 11:47:34 +00:00
color: ${({theme}) => theme.colors.darkGreen};
2026-08-24 10:07:16 +00:00
}
`;
const FormGroup = styled.div`
margin-bottom: 16px;
flex: 1;
label {
display: block;
2026-08-31 08:55:33 +00:00
font-size: 16px;
2026-08-30 11:47:34 +00:00
color: ${({theme}) => theme.colors.darkGreen};
2026-08-31 08:55:33 +00:00
margin-bottom: 8px;
2026-08-24 10:07:16 +00:00
}
input {
width: 100%;
padding: 10px 12px;
border: 1px solid #e1e1e1;
border-radius: 8px;
font-size: 14px;
outline: none;
transition: 0.2s;
2026-09-02 11:17:13 +00:00
height: 44px;
2026-08-24 10:07:16 +00:00
2026-08-31 08:55:33 +00:00
&::placeholder {
color: #A3ADA2;
}
2026-09-02 11:17:13 +00:00
&:hover {
border-color: ${({theme}) => theme.colors.gray};
}
2026-08-24 10:07:16 +00:00
&:focus {
2026-08-30 11:47:34 +00:00
border-color: ${({theme}) => theme.colors.green};
2026-08-24 10:07:16 +00:00
}
}
`;
const FormRow = styled.div`
display: flex;
gap: 16px;
margin-bottom: 16px;
${FormGroup} {
margin-bottom: 0;
}
`;
const Hint = styled.span`
display: block;
font-size: 12px;
2026-08-30 11:47:34 +00:00
color: ${({theme}) => theme.colors.gray};
2026-08-24 10:07:16 +00:00
margin-top: 4px;
`;
const GenderToggles = styled.div`
display: flex;
2026-09-02 11:17:13 +00:00
gap: 16px;
2026-08-24 10:07:16 +00:00
margin-bottom: 16px;
`;
const GenderButton = styled.button<{ $active: boolean }>`
flex: 1;
padding: 10px;
2026-08-31 08:55:33 +00:00
border: 1px solid ${({$active}) => ($active ? '#5EA12D' : '#E7EDE7')};
2026-08-24 10:07:16 +00:00
border-radius: 8px;
2026-08-31 08:55:33 +00:00
background: ${({$active}) => ($active ? '#E6F696' : '#ffffff')};
outline: ${({$active}) => ($active ? '1px solid #5EA12D' : '1px solid transparent')};
font-size: 16px;
2026-08-24 10:07:16 +00:00
cursor: pointer;
2026-08-31 08:55:33 +00:00
transition: 0.1s;
font-weight: 370;
&:hover {
outline: 1px solid #5EA12D;
border: 1px solid #5EA12D;
}
2026-08-24 10:07:16 +00:00
`;
const PrefsGrid = styled.div`
display: flex;
gap: 16px;
margin-bottom: 20px;
border-top: 1px solid #f0f0f0;
padding-top: 16px;
2026-08-30 11:47:34 +00:00
min-height: 200px;
2026-08-24 10:07:16 +00:00
`;
const SaveButton = styled.button`
width: 100%;
padding: 12px;
2026-08-30 11:47:34 +00:00
background: ${({theme}) => theme.colors.green};
2026-08-24 10:07:16 +00:00
color: #fff;
2026-08-31 08:55:33 +00:00
height: 54px;
box-sizing: border-box;
2026-08-24 10:07:16 +00:00
border: none;
border-radius: 12px;
font-size: 16px;
2026-08-31 08:55:33 +00:00
font-weight: 370;
2026-08-24 10:07:16 +00:00
cursor: pointer;
transition: 0.2s;
&:hover {
background: #5b8f29;
}
`;