a29c72b307
Инструкции вынесены в отдельное окно Electron; токены игроков не пропадают при смене сцены; предпросмотр и плейлисты адаптированы; история свёрнута по умолчанию. Co-authored-by: Cursor <cursoragent@cursor.com>
119 lines
4.2 KiB
TypeScript
119 lines
4.2 KiB
TypeScript
import { useEffect } from 'react';
|
||
import { createPortal } from 'react-dom';
|
||
|
||
import { APP_DISPLAY_NAME_EN, APP_DISPLAY_NAME_RU } from '../../../shared/appBranding';
|
||
import { Button } from '../../shared/ui/controls';
|
||
import styles from '../EditorApp.module.css';
|
||
import { useEditorI18n } from '../i18n/EditorI18nContext';
|
||
|
||
const ABOUT_SPONSORS = [
|
||
{ kind: 'text', name: 'Тифлинг бард 😈❤️' },
|
||
{
|
||
kind: 'link',
|
||
name: 'Клуб настольных и бизнес-игр "КвестИя" - Подчуфарова Евгения Александровна',
|
||
url: 'https://vk.ru/club210361604',
|
||
},
|
||
{ kind: 'link', name: 'Старые кости', url: 'https://boosty.to/tabletoppodcasts' },
|
||
{ kind: 'text', name: 'Майер С.В. (Клуб «Серый Май»)' },
|
||
{ kind: 'link', name: 'true_rus(КАВ)', url: 'https://truecorp.su/' },
|
||
{ kind: 'text', name: 'Макс LonelyFlame Чурсин' },
|
||
] as const;
|
||
|
||
type AppAboutModalProps = {
|
||
open: boolean;
|
||
onClose: () => void;
|
||
appVersion: string | null;
|
||
};
|
||
|
||
export function AppAboutModal({ open, onClose, appVersion }: AppAboutModalProps) {
|
||
const { t, locale } = useEditorI18n();
|
||
const appName = locale === 'ru' ? APP_DISPLAY_NAME_RU : APP_DISPLAY_NAME_EN;
|
||
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
const onKey = (e: KeyboardEvent) => {
|
||
if (e.key === 'Escape') onClose();
|
||
};
|
||
window.addEventListener('keydown', onKey);
|
||
return () => window.removeEventListener('keydown', onKey);
|
||
}, [onClose, open]);
|
||
|
||
if (!open) return null;
|
||
|
||
return createPortal(
|
||
<>
|
||
<button
|
||
type="button"
|
||
aria-label={t('common.close')}
|
||
onClick={onClose}
|
||
className={styles.modalBackdrop}
|
||
/>
|
||
<div role="dialog" aria-modal="true" className={styles.modalDialog}>
|
||
<div className={styles.modalHeader}>
|
||
<div className={styles.modalTitle}>{t('app.about.title')}</div>
|
||
<button
|
||
type="button"
|
||
aria-label={t('common.close')}
|
||
onClick={onClose}
|
||
className={styles.modalClose}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
<div className={styles.aboutBody}>
|
||
<div className={styles.aboutAppName}>{appName}</div>
|
||
<div className={styles.aboutTagline}>{t('app.about.tagline')}</div>
|
||
<p className={styles.aboutParagraph}>{t('app.about.description')}</p>
|
||
<div className={styles.aboutMetaGrid}>
|
||
<div className={styles.fieldLabel}>{t('app.about.versionLabel')}</div>
|
||
<div>{appVersion ?? '—'}</div>
|
||
<div className={styles.fieldLabel}>{t('app.about.developerLabel')}</div>
|
||
<div>{t('app.about.developer')}</div>
|
||
<div className={styles.fieldLabel}>{t('app.about.supportLabel')}</div>
|
||
<div>
|
||
<a className={styles.aboutLink} href={`mailto:${t('app.about.supportEmail')}`}>
|
||
{t('app.about.supportEmail')}
|
||
</a>
|
||
</div>
|
||
<div className={styles.fieldLabel}>{t('app.about.websiteLabel')}</div>
|
||
<div>
|
||
<a
|
||
className={styles.aboutLink}
|
||
href={t('app.about.websiteUrl')}
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
>
|
||
{t('app.about.websiteUrl')}
|
||
</a>
|
||
</div>
|
||
<div className={styles.fieldLabel}>{t('app.about.sponsorsLabel')}</div>
|
||
<div className={styles.aboutSponsorsList}>
|
||
{ABOUT_SPONSORS.map((sponsor) =>
|
||
sponsor.kind === 'link' ? (
|
||
<a
|
||
key={sponsor.name}
|
||
className={styles.aboutLink}
|
||
href={sponsor.url}
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
>
|
||
{sponsor.name}
|
||
</a>
|
||
) : (
|
||
<div key={sponsor.name}>{sponsor.name}</div>
|
||
),
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className={styles.modalFooter}>
|
||
<Button variant="primary" onClick={onClose}>
|
||
{t('common.close')}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</>,
|
||
document.body,
|
||
);
|
||
}
|