TelegramPWA/src/components/modals/birthday/BirthdaySetupModal.tsx
2025-12-22 22:53:43 +01:00

241 lines
6.5 KiB
TypeScript

import { memo, useMemo, useRef, useState } from '@teact';
import { getActions } from '../../../global';
import type { TabState } from '../../../global/types';
import { SettingsScreens } from '../../../types';
import buildClassName from '../../../util/buildClassName';
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
import Button from '../../ui/Button';
import DropdownMenu from '../../ui/DropdownMenu';
import InputText from '../../ui/InputText';
import Link from '../../ui/Link';
import MenuItem from '../../ui/MenuItem';
import Modal from '../../ui/Modal';
import styles from './BirthdaySetupModal.module.scss';
export type OwnProps = {
modal: TabState['birthdaySetupModal'];
};
const STICKER_SIZE = 120;
const MAX_AGE = 150;
const CURRENT_YEAR = new Date().getFullYear();
const MIN_YEAR = CURRENT_YEAR - MAX_AGE;
type MonthIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
const MONTH_INDEXES = Array.from({ length: 12 }, (_, index) => index + 1) as MonthIndex[];
const BirthdaySetupModal = ({ modal }: OwnProps) => {
const { closeBirthdaySetupModal, openSettingsScreen, updateBirthday } = getActions();
const { currentBirthday } = modal || {};
const dialogRef = useRef<HTMLDivElement>();
const [day, setDay] = useState<number | undefined>(currentBirthday?.day);
const [month, setMonth] = useState<MonthIndex | undefined>(currentBirthday?.month as MonthIndex | undefined);
const [year, setYear] = useState<number | undefined>(currentBirthday?.year);
const lang = useLang();
const handleClose = useLastCallback(() => {
closeBirthdaySetupModal();
});
const handleRemove = useLastCallback(() => {
updateBirthday({
birthday: undefined,
});
closeBirthdaySetupModal();
});
const handlePrivacyClick = useLastCallback(() => {
openSettingsScreen({ screen: SettingsScreens.PrivacyBirthday });
closeBirthdaySetupModal();
});
const maxDay = getMaxMonthDay(month, year);
const handleDayChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target.value) {
setDay(undefined);
return;
}
const value = Number(e.target.value.replace(/[^\d]+/g, ''));
if (!value) {
e.preventDefault();
return;
}
if (value > maxDay) {
setDay(maxDay);
return;
}
setDay(Math.max(value, 1));
});
const handleMonthUpdate = useLastCallback((value: MonthIndex) => {
setMonth(value);
if (day) setDay(Math.min(day, getMaxMonthDay(value, year)));
});
const handleYearChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target.value) {
setYear(undefined);
return;
}
const value = Number(e.target.value.replace(/[^\d]+/g, ''));
if (!value) {
e.preventDefault();
return;
}
if (value > CURRENT_YEAR) {
setYear(CURRENT_YEAR);
return;
}
setYear(value);
if (day) setDay(Math.min(day, getMaxMonthDay(month, value)));
});
const handleYearBlur = useLastCallback(() => {
if (!year) {
setYear(undefined);
return;
}
if (year < 100) {
setYear(1900 + year);
return;
}
if (year < MIN_YEAR) {
setYear(MIN_YEAR);
return;
}
});
const handleSubmit = useLastCallback(() => {
if (!day || !month) return;
updateBirthday({
birthday: {
day,
month,
year,
},
});
closeBirthdaySetupModal();
});
const MonthTrigger = useMemo(() => {
return ({ onTrigger, isOpen }: { onTrigger: () => void; isOpen?: boolean }) => (
<InputText
label={lang('BirthdayInputMonth')}
className={buildClassName(styles.input, styles.month, isOpen && 'active')}
value={month ? lang(`Month${month}`) : ''}
onClick={onTrigger}
inputMode="numeric"
teactExperimentControlled
/>
);
}, [lang, month]);
return (
<Modal
isOpen={Boolean(modal)}
hasCloseButton
hasAbsoluteCloseButton
isSlim
dialogRef={dialogRef}
contentClassName={styles.content}
onClose={handleClose}
>
<div className={styles.header}>
<AnimatedIconWithPreview
tgsUrl={LOCAL_TGS_URLS.DuckCake}
size={STICKER_SIZE}
className="section-icon"
/>
<h3 className={styles.title}>{lang('BirthdaySetupTitle')}</h3>
</div>
<div className={styles.inputs}>
<InputText
label={lang('BirthdayInputDay')}
className={styles.input}
value={day?.toString()}
onChange={handleDayChange}
maxLength={2}
inputMode="numeric"
/>
<DropdownMenu
className={buildClassName(styles.monthDropdown, 'with-menu-transitions')}
bubbleClassName={styles.monthBubble}
autoClose
positionY="bottom"
trigger={MonthTrigger}
>
{MONTH_INDEXES.map((index: MonthIndex) => (
<MenuItem key={index} onClick={() => handleMonthUpdate(index)}>
{lang(`Month${index}`)}
</MenuItem>
))}
</DropdownMenu>
<InputText
label={lang('BirthdayInputYear')}
className={styles.input}
value={year?.toString()}
onBlur={handleYearBlur}
onChange={handleYearChange}
maxLength={4}
inputMode="numeric"
/>
</div>
<div className={styles.footer}>
<span className={styles.privacySuggestion}>
{lang('BirthdayPrivacySuggestion', {
link: <Link isPrimary onClick={handlePrivacyClick}>{lang('BirthdayPrivacySuggestionLink')}</Link>,
}, { withNodes: true })}
</span>
{currentBirthday && (
<Button isText onClick={handleRemove}>
{lang('BirthdayRemove')}
</Button>
)}
<Button
disabled={!day || !month}
onClick={handleSubmit}
>
{lang('Save')}
</Button>
</div>
</Modal>
);
};
const getMaxMonthDay = (month?: number, year?: number) => {
if (!month) return 31;
if (month === 2) {
const isLeapYear = year ? year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) : true;
return isLeapYear ? 29 : 28;
}
if (month === 4 || month === 6 || month === 9 || month === 11) {
return 30;
}
return 31;
};
export default memo(BirthdaySetupModal);