/
guselnikov
/
inst
Обзор
Документация
Войти
/
guselnikov
/
inst
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
admin/src/components/post-editor/AddLayerMenu.tsx
117 строк
4 KB
Viktor Guselnikov
first_commit
07 июл 2026, 11:54
07 июл 2026, 11:54
098126a
Код
Авторство
О чём код?
import { useRef, useState, type ChangeEvent } from 'react'; import { Image, Layers, Shapes, Square, Type } from 'lucide-react'; import { uploadTemplateAsset } from '../../api/client'; import { prefetchTemplateAsset } from './assetUtils'; import type { Layer } from './design-types'; import type { DesignEditor } from './hooks/useDesignDocument'; import { measureImageFile } from './imageMeasure'; import styles from './AddLayerMenu.module.css'; const LOGO_ACCEPT = 'image/svg+xml,image/png,image/jpeg,image/jpg,image/webp'; const ADD_LAYERS: { type: Layer['type']; label: string; icon: typeof Type; needsTemplate?: boolean }[] = [ { type: 'text', label: 'Текст', icon: Type }, { type: 'shape', label: 'Шейп', icon: Shapes }, { type: 'image', label: 'Фото', icon: Image }, { type: 'logo', label: 'Лого', icon: Square, needsTemplate: true }, ]; interface AddLayerMenuProps { editor: DesignEditor; templateId: string | null; templateHint?: string; } export function AddLayerMenu({ editor, templateId, templateHint }: AddLayerMenuProps) { const { addLayer, addLogoLayer } = editor; const [addOpen, setAddOpen] = useState(false); const [uploadingLogo, setUploadingLogo] = useState(false); const [error, setError] = useState<string | null>(null); const logoInputRef = useRef<HTMLInputElement>(null); function handleAdd(type: Layer['type']) { if (type === 'logo') { if (!templateId) { setError(templateHint ?? 'Сначала сохраните шаблон или примените его к слайду'); setAddOpen(false); return; } setError(null); setAddOpen(false); logoInputRef.current?.click(); return; } addLayer(type); setAddOpen(false); setError(null); } async function handleLogoFile(event: ChangeEvent<HTMLInputElement>) { const file = event.target.files?.[0]; event.target.value = ''; if (!file || !templateId) return; setUploadingLogo(true); setError(null); try { const { width, height } = await measureImageFile(file); const asset = await uploadTemplateAsset(templateId, file, 'logo'); prefetchTemplateAsset(asset.id, file); addLogoLayer(asset.id, width, height); } catch (err) { setError(err instanceof Error ? err.message : 'Не удалось загрузить лого'); } finally { setUploadingLogo(false); } } return ( <div className={styles.root}> <input ref={logoInputRef} type="file" accept={LOGO_ACCEPT} hidden onChange={(event) => void handleLogoFile(event)} /> <div className={styles.addLayer}> <button type="button" className={styles.addToggle} aria-expanded={addOpen} disabled={uploadingLogo} onClick={() => setAddOpen((value) => !value)} > <Layers size={16} aria-hidden /> {uploadingLogo ? 'Загрузка лого…' : 'Добавить слой'} </button> {addOpen ? ( <div className={styles.addMenu}> {ADD_LAYERS.map(({ type, label, icon: Icon, needsTemplate }) => { const disabled = needsTemplate && !templateId; return ( <button key={type} type="button" className={styles.addItem} disabled={disabled} title={disabled ? (templateHint ?? 'Нужен шаблон для загрузки') : undefined} onClick={() => handleAdd(type)} > <Icon size={16} aria-hidden /> {label} </button> ); })} </div> ) : null} </div> {error ? <p className={styles.error}>{error}</p> : null} </div> ); }