/
guselnikov
/
inst
Обзор
Документация
Войти
/
guselnikov
/
inst
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
admin/src/components/post-editor/PostCreateWorkspace.tsx
299 строк
10 KB
Viktor Guselnikov
first_commit
07 июл 2026, 11:54
07 июл 2026, 11:54
098126a
Код
Авторство
О чём код?
import { useEffect, useRef, useState } from 'react'; import { ArrowLeft, ArrowRight, CalendarOff, Save, Trash2 } from 'lucide-react'; import { Link } from 'react-router-dom'; import { Button } from '../ui/Button'; import { preloadPhotoFonts } from './fontLoader'; import { Textarea } from '../ui/Textarea'; import { ScheduleDestinations } from '../auto-post/ScheduleDestinations'; import type { ScheduleDestination } from '../auto-post/types'; import type { IgAccount } from '../../api/client'; import { AddLayerMenu } from './AddLayerMenu'; import { CanvasStage } from './CanvasStage'; import { CanvasToolbar } from './CanvasToolbar'; import { LayerPanel } from './LayerPanel'; import { PropertiesPanel } from './PropertiesPanel'; import { SlideStrip } from './SlideStrip'; import { TemplatePicker } from './TemplatePicker'; import type { TemplateAspectRatio, TextLayer } from './design-types'; import { useCanvasViewport } from './hooks/useCanvasViewport'; import { useEditorKeyboard } from './hooks/useEditorKeyboard'; import type { PostSlidesEditor } from './hooks/usePostSlides'; import { findTextOverflowLayers } from './measureText'; import styles from './PostCreateWorkspace.module.css'; export type PostCreateStep = 'design' | 'publish'; interface PostCreateWorkspaceProps { slidesEditor: PostSlidesEditor; aspectRatio: TemplateAspectRatio; aspectRatioLabel: string; step: PostCreateStep; isEditing?: boolean; isScheduled?: boolean; onStepChange: (step: PostCreateStep) => void; onContinue: () => void; accounts: IgAccount[]; destinations: ScheduleDestination[]; onDestinationsChange: (items: ScheduleDestination[]) => void; caption: string; onCaptionChange: (value: string) => void; saving?: boolean; onSave: (fromStep: PostCreateStep) => void | Promise<void>; onUnschedule?: () => void | Promise<void>; onDelete?: () => void | Promise<void>; } export function PostCreateWorkspace({ slidesEditor, aspectRatio, aspectRatioLabel, step, isEditing = false, isScheduled = false, onStepChange, onContinue, accounts, destinations, onDestinationsChange, caption, onCaptionChange, saving, onSave, onUnschedule, onDelete, }: PostCreateWorkspaceProps) { const { slides, activeSlideId, activeSlide, editor, selectSlide, applyTemplate, addSlide, removeSlide, reorderSlide, } = slidesEditor; const isDesignStep = step === 'design'; const [showSafeArea, setShowSafeArea] = useState(false); const viewportRef = useRef<HTMLDivElement>(null); useEffect(() => { preloadPhotoFonts(); }, []); const { document, selectedLayerId, selectLayer, setLayerTransform, rotateLayer, beginInteraction, endInteraction, undo, redo, canUndo, canRedo, } = editor; const { scale, zoomMode, zoomPercent, zoomIn, zoomOut, setZoom, setFit } = useCanvasViewport( viewportRef, document.artboard, ); useEditorKeyboard({ editor, enabled: isDesignStep }); const textLayers = document.layers.filter((layer): layer is TextLayer => layer.type === 'text'); const overflowLayers = findTextOverflowLayers(textLayers, document.artboard); const hasOverflow = overflowLayers.length > 0; const hasScheduledPublish = destinations.some((dest) => { if (!dest.scheduledAt?.trim()) return false; const date = new Date(dest.scheduledAt); return !Number.isNaN(date.getTime()) && date.getTime() > Date.now(); }); const saveLabel = saving ? 'Сохранение…' : hasScheduledPublish ? 'Запланировать' : 'Сохранить'; const assetTemplateId = activeSlide?.templateId ?? ''; return ( <div className={styles.shell}> <header className={styles.toolbar}> <div className={styles.toolbarLeft}> <Link to="/posts/queue" className={styles.backLink}> <ArrowLeft size={16} aria-hidden /> Посты </Link> <div className={styles.titleBlock}> <h1 className={styles.title}>{isEditing ? 'Редактирование' : 'Новый пост'}</h1> <span className={styles.badge}>{aspectRatioLabel}</span> {slides.length > 1 ? ( <span className={styles.badgeMuted}>Карусель · {slides.length}</span> ) : null} <span className={styles.stepBadge}> {isDesignStep ? 'Шаг 1 · Дизайн' : 'Шаг 2 · Публикация'} </span> </div> </div> <div className={styles.toolbarActions}> {isEditing && onDelete ? ( <Button variant="danger" disabled={saving} onClick={() => void onDelete()} > <Trash2 size={16} aria-hidden /> Удалить </Button> ) : null} {isDesignStep ? ( <> <Button variant="secondary" disabled={saving || hasOverflow} onClick={() => void onSave('design')} > <Save size={16} aria-hidden /> {saving ? 'Сохранение…' : 'Сохранить'} </Button> <Button variant="primary" disabled={hasOverflow} onClick={onContinue}> Далее <ArrowRight size={16} aria-hidden /> </Button> </> ) : ( <> <Button variant="ghost" onClick={() => onStepChange('design')}> Назад </Button> <Button variant="primary" disabled={saving || hasOverflow} onClick={() => void onSave('publish')} > <Save size={16} aria-hidden /> {saveLabel} </Button> </> )} </div> </header> {hasOverflow ? ( <div className={styles.overflowBanner} role="alert"> Текст выходит за край артборда: {overflowLayers.map((layer) => layer.name).join(', ')} </div> ) : null} <div className={`${styles.workspace} ${isDesignStep ? '' : styles.workspacePublish}`}> <div className={styles.leftCol}> <SlideStrip slides={slides} activeSlideId={activeSlideId} onSelect={selectSlide} onAdd={isDesignStep ? () => addSlide(aspectRatio) : undefined} onRemove={isDesignStep ? removeSlide : undefined} onReorder={isDesignStep ? reorderSlide : undefined} /> {isDesignStep && assetTemplateId ? ( <AddLayerMenu editor={editor} templateId={assetTemplateId} templateHint="Примените шаблон к слайду, чтобы добавить лого" /> ) : null} {isDesignStep ? ( <LayerPanel editor={editor} emptyHint={assetTemplateId ? undefined : 'Слои появятся после применения шаблона'} /> ) : null} </div> <div className={styles.canvasCol}> {isDesignStep ? ( <CanvasToolbar zoomPercent={zoomPercent} zoomMode={zoomMode} onZoomIn={zoomIn} onZoomOut={zoomOut} onSetZoom={setZoom} onFit={setFit} canUndo={canUndo} canRedo={canRedo} onUndo={undo} onRedo={redo} showSafeArea={showSafeArea} onToggleSafeArea={() => setShowSafeArea((value) => !value)} /> ) : ( <p className={styles.previewHint}>Предпросмотр слайдов</p> )} <div ref={viewportRef} className={styles.canvasViewport}> <CanvasStage document={document} selectedLayerId={isDesignStep ? selectedLayerId : null} scale={scale} zoomPercent={zoomPercent} showSafeArea={isDesignStep && showSafeArea} onSelectLayer={isDesignStep ? selectLayer : () => {}} onSetLayerTransform={isDesignStep ? setLayerTransform : () => {}} onRotateLayer={isDesignStep ? rotateLayer : () => {}} onBeginInteraction={isDesignStep ? beginInteraction : () => {}} onEndInteraction={isDesignStep ? endInteraction : () => {}} /> </div> </div> <aside className={styles.rightCol}> {isDesignStep ? ( <> <TemplatePicker aspectRatio={aspectRatio} onApply={applyTemplate} /> {assetTemplateId ? ( <PropertiesPanel editor={editor} templateId={assetTemplateId} showLayerPickerWhenEmpty /> ) : ( <div className={styles.hint}> Выберите шаблон и нажмите «Применить к слайду» — откроются все слои и настройки фона. </div> )} </> ) : ( <div className={styles.publishBlock}> <div className={styles.publishHeader}> <h2 className={styles.publishTitle}>Публикация</h2> <p className={styles.publishSubtitle}>Подпись и расписание — необязательно</p> </div> <Textarea label="Подпись" value={caption} onChange={onCaptionChange} maxLength={2200} placeholder="Текст, хештеги… (можно оставить пустым)" /> <ScheduleDestinations accounts={accounts} destinations={destinations} onChange={onDestinationsChange} /> {isScheduled && onUnschedule ? ( <Button type="button" variant="ghost" disabled={saving} onClick={() => void onUnschedule()} > <CalendarOff size={16} aria-hidden /> Снять с публикации </Button> ) : null} </div> )} </aside> </div> </div> ); }