create video page

This commit is contained in:
Joe_Peach committed 2026-09-29 17:24:03 +03:00
1 parent 1501e4bf62
commit 7e62cfc815
2 files changed
+303 -2

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
.videoMark::before {
inset: 18px 15px 18px 22px;
background: var(--color-accent);
border: 0;
border-radius: 0;
clip-path: polygon(0 0, 100% 50%, 0 100%);
}
+296 -2
View File
@@ -1,6 +1,300 @@
import { useEffect, useState, type ChangeEvent, type FormEvent } from "react";
import { useNavigate } from "react-router-dom";
import logo from "../../assets/common/HITGENERATOR-Logo.png";
import { useAppDispatch, useAppSelector } from "../../store/hooks";
import { clearRequestError, request, resetState } from "../../store/slices/slice";
import pictureStyles from "../Picture/Picture.module.css";
import styles from "./VideoPage.module.css";
type AspectRatio = "1:1" | "4:5" | "16:9";
const aspectRatios: AspectRatio[] = ["1:1", "4:5", "16:9"];
export default function Video() {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const { loading, error: requestError } = useAppSelector((state) => state.request);
const [prompt, setPrompt] = useState("");
const [characters, setCharacters] = useState<File[]>([]);
const [scenes, setScenes] = useState<File[]>([]);
const [screenplays, setScreenplays] = useState<File[]>([]);
const [aspectRatio, setAspectRatio] = useState<AspectRatio>("16:9");
const [formError, setFormError] = useState<string | null>(null);
const [isSubmitted, setIsSubmitted] = useState(false);
useEffect(() => {
dispatch(clearRequestError());
}, [dispatch]);
function resetErrors() {
setFormError(null);
dispatch(clearRequestError());
}
function addImageFiles(
event: ChangeEvent<HTMLInputElement>,
setFiles: React.Dispatch<React.SetStateAction<File[]>>,) {
const nextFiles = event.currentTarget.files?.[0] ?? null;
if (nextFiles && !nextFiles.type.startsWith("image/")) {
setFormError("Выберите файлы изображений.");
return;
}
resetErrors();
setFiles((currentFiles) => nextFiles ? [...currentFiles, nextFiles] : currentFiles);
setIsSubmitted(false);
}
function addScreenplayFiles(event: ChangeEvent<HTMLInputElement>) {
const nextFiles = Array.from(event.currentTarget.files ?? []);
const isTextFile = (file: File) => {
return (file.type.startsWith("text/") || /\.(txt|md)$/i.test(file.name)) || file.type.startsWith("image/");
};
if (nextFiles.some((file) => !isTextFile(file))) {
setFormError("Выберите текстовые файлы в формате TXT или MD.");
return;
}
resetErrors();
setScreenplays((currentFiles) => [...currentFiles, ...nextFiles]);
event.currentTarget.value = "";
setIsSubmitted(false);
}
function removeFile(
index: number,
setFiles: React.Dispatch<React.SetStateAction<File[]>>,
) {
setFiles((currentFiles) => currentFiles.filter((_, fileIndex) => fileIndex !== index));
setIsSubmitted(false);
}
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const text = prompt.trim();
if (!text) {
setFormError("Добавьте описание видео.");
return;
}
resetErrors();
setIsSubmitted(false);
await dispatch(
request({
id: Date.now(),
text,
files: [...characters, ...scenes, ...screenplays],
ratio: aspectRatio,
}),
).unwrap();
setIsSubmitted(true);
}
const previewClassName = [
pictureStyles.previewCanvas,
aspectRatio === "1:1"
? pictureStyles.square
: aspectRatio === "4:5"
? pictureStyles.portrait
: pictureStyles.wide,
].join(" ");
const status = loading ? "Генерируем" : isSubmitted ? "Запрос принят" : "Ожидание";
//const attachmentsCount = characters.length + scenes.length + screenplays.length;
return (
<div>video</div>
)
<div className={pictureStyles.page}>
<header className={pictureStyles.toolbar}>
<div className={pictureStyles.brand}>
<button
className={pictureStyles.backButton}
type="button"
aria-label="На главную"
title="На главную"
onClick={() => {
navigate("/");
resetState();
}}
>
<span aria-hidden="true">&larr;</span>
</button>
<img className={pictureStyles.logo} src={logo} alt="ShitGenerator" />
</div>
<span className={pictureStyles.mode}>VIDEO</span>
</header>
<main className={pictureStyles.workspace}>
<form className={pictureStyles.controls} onSubmit={handleSubmit}>
<div className={pictureStyles.heading}>
<p className={pictureStyles.eyebrow}>VIDEO LAB</p>
<h1>Генерация видео</h1>
</div>
<label className={pictureStyles.field}>
<span className={pictureStyles.labelRow}>
<span>Описание</span>
<span>{prompt.length}/800</span>
</span>
<textarea
value={prompt}
maxLength={800}
placeholder="Опишите видео"
onChange={(event) => {
resetErrors();
setPrompt(event.target.value);
setIsSubmitted(false);
}}
/>
</label>
<section className={pictureStyles.field} aria-labelledby="characters-label">
<span className={pictureStyles.labelRow} id="characters-label">
<span>Персонажи</span>
<span>Изображения</span>
</span>
{characters.map((file, index) => (
<div className={pictureStyles.fileSelected} key={`${file.name}-${index}`}>
<span className={pictureStyles.fileName}>{file.name}</span>
<button
className={pictureStyles.removeFileButton}
type="button"
aria-label={`Удалить файл ${file.name}`}
title="Удалить файл"
onClick={() => removeFile(index, setCharacters)}
>
&times;
</button>
</div>
))}
<label className={pictureStyles.uploadButton}>
<input
type="file"
accept="image/*"
onChange={(file) => addImageFiles(file, setCharacters)}
/>
<span>Добавить файлы</span>
</label>
</section>
<section className={pictureStyles.field} aria-labelledby="scenes-label">
<span className={pictureStyles.labelRow} id="scenes-label">
<span>Сцены и локации</span>
<span>Изображения</span>
</span>
{scenes.map((file, index) => (
<div className={pictureStyles.fileSelected} key={`${file.name}-${index}`}>
<span className={pictureStyles.fileName}>{file.name}</span>
<button
className={pictureStyles.removeFileButton}
type="button"
aria-label={`Удалить файл ${file.name}`}
title="Удалить файл"
onClick={() => removeFile(index, setScenes)}
>
&times;
</button>
</div>
))}
<label className={pictureStyles.uploadButton}>
<input
type="file"
accept="image/*"
multiple
onChange={(event) => addImageFiles(event, setScenes)}
/>
<span>Добавить файлы</span>
</label>
</section>
<section className={pictureStyles.field} aria-labelledby="screenplays-label">
<span className={pictureStyles.labelRow} id="screenplays-label">
<span>Сценарий</span>
<span>TXT, MD</span>
</span>
{screenplays.map((file, index) => (
<div className={pictureStyles.fileSelected} key={`${file.name}-${index}`}>
<span className={pictureStyles.fileName}>{file.name}</span>
<button
className={pictureStyles.removeFileButton}
type="button"
aria-label={`Удалить файл ${file.name}`}
title="Удалить файл"
onClick={() => removeFile(index, setScreenplays)}
>
&times;
</button>
</div>
))}
<label className={pictureStyles.uploadButton}>
<input
type="file"
accept=".txt,.md,text/plain,text/markdown,image/*"
multiple
onChange={addScreenplayFiles}
/>
<span>Добавить файлы</span>
</label>
</section>
<fieldset className={pictureStyles.field}>
<legend>Формат</legend>
<div className={pictureStyles.segmentedControl}>
{aspectRatios.map((ratio) => (
<button
className={ratio === aspectRatio ? pictureStyles.selected : undefined}
type="button"
key={ratio}
aria-pressed={ratio === aspectRatio}
onClick={() => {
setAspectRatio(ratio);
setIsSubmitted(false);
}}
>
{ratio}
</button>
))}
</div>
</fieldset>
{(formError || requestError) && (
<p className={pictureStyles.error} role="alert">
{formError || requestError}
</p>
)}
<button className={pictureStyles.generateButton} type="submit" disabled={loading}>
{loading ? "Генерируем..." : "Сгенерировать"}
</button>
</form>
<section className={pictureStyles.previewPanel} aria-label="Область результата">
<div className={pictureStyles.previewHeader}>
<span>{status}</span>
<span className={pictureStyles.statusDot} data-active={loading || isSubmitted} aria-hidden="true" />
</div>
<div className={pictureStyles.previewArea}>
<div className={previewClassName}>
<div className={pictureStyles.emptyState}>
<span className={`${pictureStyles.emptyMark} ${styles.videoMark}`} aria-hidden="true" />
<span>{loading ? "Генерируем" : isSubmitted ? "Запрос принят" : "Ожидаемый ролик"}</span>
</div>
{loading && <span className={pictureStyles.scanLine} aria-hidden="true" />}
</div>
</div>
<footer className={pictureStyles.previewFooter}>
<span>{aspectRatio}</span>
</footer>
</section>
</main>
</div>
);
}