marketing/src/components/Family/PersonModal.tsx
2026-09-05 21:48:01 +03:00

404 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {useEffect, useState} from 'react';
import styled from 'styled-components';
import type {Gender, User} from '../../types/user';
import {Tag, TagsInput, TagsWrap} from './TagsInput';
import {reachGoal} from "../../hooks/useYM";
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) {
reachGoal('ac_family_edit');
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 => (*/}
{availableTags.map(tag => (
<Tag
key={tag}
$type={showExpanded}
$active={(showExpanded == 'favorite' ? form.favorite : form.avoided).indexOf(tag) > -1}
onClick={() => {
if ((showExpanded == 'favorite' ? form.favorite : form.avoided).indexOf(tag) > -1) {
(showExpanded == 'favorite' ? handleFavoriteChange : handleAvoidChange)(
((showExpanded == 'favorite' ? form.favorite : form.avoided) || []).filter(item => item != tag)
)
} else {
(showExpanded == 'favorite' ? handleFavoriteChange : handleAvoidChange)(
((showExpanded == 'favorite' ? form.favorite : form.avoided) || []).concat(tag)
)
}
}}
>
{tag}
</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`
height: 100svh;
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;
height: 44px;
&::placeholder {
color: #A3ADA2;
}
&:hover {
border-color: ${({theme}) => theme.colors.gray};
}
&: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: 16px;
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;
height: 44px;
&:hover {
outline: 1px solid #5EA12D;
border: 1px solid #5EA12D;
}
`;
const PrefsGrid = styled.div`
display: flex;
flex-direction: column;
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;
}
`;