Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e62cfc815 | ||
|
|
1501e4bf62 | ||
|
|
32122c5112 | ||
|
|
78b2afb747 | ||
|
|
9ec5e852c6 | ||
|
|
6ef1e381e4 | ||
|
|
3a530300ba | ||
|
|
9de3b363be |
No files matched your search
@@ -2,4 +2,10 @@ export interface Request{
|
|||||||
id: number;
|
id: number;
|
||||||
text: string;
|
text: string;
|
||||||
files: File[];
|
files: File[];
|
||||||
|
ratio: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response{
|
||||||
|
test: string;
|
||||||
|
files: File[];
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,540 @@
|
|||||||
|
.page {
|
||||||
|
display: grid;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: var(--space-6) var(--space-10) var(--space-10);
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 52px;
|
||||||
|
padding-bottom: var(--space-4);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backButton,
|
||||||
|
.removeFileButton {
|
||||||
|
display: inline-grid;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
place-items: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--color-text);
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition:
|
||||||
|
color var(--duration-fast) var(--ease-standard),
|
||||||
|
border-color var(--duration-fast) var(--ease-standard),
|
||||||
|
background-color var(--duration-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backButton {
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backButton:hover,
|
||||||
|
.removeFileButton:hover {
|
||||||
|
color: var(--color-black);
|
||||||
|
background: var(--color-primary);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backButton:focus-visible,
|
||||||
|
.removeFileButton:focus-visible,
|
||||||
|
.uploadButton:focus-within,
|
||||||
|
.segmentedControl button:focus-visible,
|
||||||
|
.styleChoices button:focus-visible,
|
||||||
|
.generateButton:focus-visible {
|
||||||
|
outline: 3px solid var(--color-primary-soft);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 150px;
|
||||||
|
max-height: 38px;
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: left center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode,
|
||||||
|
.eyebrow,
|
||||||
|
.labelRow,
|
||||||
|
.previewHeader,
|
||||||
|
.previewFooter {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.2;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode,
|
||||||
|
.eyebrow {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(300px, 0.86fr) minmax(380px, 1.14fr);
|
||||||
|
gap: var(--space-6);
|
||||||
|
width: min(100%, 1120px);
|
||||||
|
margin: auto;
|
||||||
|
padding-block: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls,
|
||||||
|
.previewPanel {
|
||||||
|
min-width: 0;
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-6);
|
||||||
|
padding: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(30px, 4vw, 44px);
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field legend {
|
||||||
|
padding: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelRow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-3);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelRow span:first-child {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 132px;
|
||||||
|
padding: var(--space-4);
|
||||||
|
resize: vertical;
|
||||||
|
color: var(--color-text);
|
||||||
|
font: inherit;
|
||||||
|
line-height: 1.45;
|
||||||
|
background: rgba(3, 6, 16, 0.42);
|
||||||
|
border: 1px solid rgba(216, 227, 251, 0.16);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
outline: 0;
|
||||||
|
transition: border-color var(--duration-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field textarea::placeholder {
|
||||||
|
color: var(--color-gray-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field textarea:focus {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadButton,
|
||||||
|
.fileSelected {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 48px;
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
background: rgba(3, 6, 16, 0.42);
|
||||||
|
border: 1px dashed rgba(216, 227, 251, 0.22);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadButton {
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
color var(--duration-fast) var(--ease-standard),
|
||||||
|
border-color var(--duration-fast) var(--ease-standard),
|
||||||
|
background-color var(--duration-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadButton:hover {
|
||||||
|
color: var(--color-black);
|
||||||
|
background: var(--color-primary);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadButton input {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
clip-path: inset(50%);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileSelected {
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-3);
|
||||||
|
border-style: solid;
|
||||||
|
border-color: rgba(143, 84, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileName {
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 14px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.removeFileButton {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmentedControl,
|
||||||
|
.styleChoices {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmentedControl,
|
||||||
|
.styleChoices {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmentedControl button,
|
||||||
|
.styleChoices button {
|
||||||
|
min-width: 0;
|
||||||
|
padding: var(--space-3);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
background: rgba(3, 6, 16, 0.42);
|
||||||
|
border: 1px solid rgba(216, 227, 251, 0.16);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition:
|
||||||
|
color var(--duration-fast) var(--ease-standard),
|
||||||
|
border-color var(--duration-fast) var(--ease-standard),
|
||||||
|
background-color var(--duration-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmentedControl button:hover,
|
||||||
|
.styleChoices button:hover {
|
||||||
|
color: var(--color-text);
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmentedControl .selected,
|
||||||
|
.styleChoices .styleSelected {
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-primary-soft);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.styleChoices button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.synthesis {
|
||||||
|
background: var(--color-primary);
|
||||||
|
box-shadow: 10px 0 0 var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.neon {
|
||||||
|
background: var(--color-accent);
|
||||||
|
box-shadow: 10px 0 0 var(--color-accent-warm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cinema {
|
||||||
|
background: var(--color-accent-gold);
|
||||||
|
box-shadow: 10px 0 0 var(--color-accent-warm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
padding: var(--space-3);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-rose-200);
|
||||||
|
background: rgba(190, 18, 60, 0.16);
|
||||||
|
border: 1px solid rgba(254, 205, 211, 0.36);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.generateButton {
|
||||||
|
min-height: 52px;
|
||||||
|
padding: var(--space-3) var(--space-5);
|
||||||
|
margin-top: auto;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 900;
|
||||||
|
color: var(--color-black);
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--color-primary);
|
||||||
|
border: 1px solid var(--color-primary);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition:
|
||||||
|
background-color var(--duration-fast) var(--ease-standard),
|
||||||
|
border-color var(--duration-fast) var(--ease-standard),
|
||||||
|
transform var(--duration-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.generateButton:hover:not(:disabled) {
|
||||||
|
background: var(--color-primary-hover);
|
||||||
|
border-color: var(--color-primary-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.generateButton:disabled {
|
||||||
|
cursor: wait;
|
||||||
|
opacity: 0.66;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewPanel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewHeader,
|
||||||
|
.previewFooter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 56px;
|
||||||
|
padding-inline: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewHeader {
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewFooter {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusDot {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
background: var(--color-gray-500);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusDot[data-active="true"] {
|
||||||
|
background: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 5px var(--color-primary-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewArea {
|
||||||
|
display: grid;
|
||||||
|
min-height: 460px;
|
||||||
|
padding: var(--space-6);
|
||||||
|
overflow: auto;
|
||||||
|
place-items: center;
|
||||||
|
background:
|
||||||
|
linear-gradient(rgba(53, 216, 255, 0.04) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(53, 216, 255, 0.04) 1px, transparent 1px),
|
||||||
|
rgba(3, 6, 16, 0.3);
|
||||||
|
background-size: 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewCanvas {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
width: min(100%, 560px);
|
||||||
|
overflow: hidden;
|
||||||
|
place-items: center;
|
||||||
|
background: #0a1430;
|
||||||
|
border: 1px solid rgba(53, 216, 255, 0.34);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.square {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portrait {
|
||||||
|
width: min(80%, 430px);
|
||||||
|
aspect-ratio: 4 / 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wide {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referenceImage {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyState {
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyMark {
|
||||||
|
position: relative;
|
||||||
|
width: 74px;
|
||||||
|
height: 74px;
|
||||||
|
border: 2px solid var(--color-primary);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transform: rotate(8deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyMark::before,
|
||||||
|
.emptyMark::after {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyMark::before {
|
||||||
|
inset: 12px;
|
||||||
|
border: 2px solid var(--color-accent);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyMark::after {
|
||||||
|
right: -12px;
|
||||||
|
bottom: -12px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: var(--color-accent-warm);
|
||||||
|
border: 2px solid var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanLine {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 auto auto 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--color-primary);
|
||||||
|
box-shadow: 0 0 18px var(--color-primary);
|
||||||
|
animation: scan 1.8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scan {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateY(300px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.page {
|
||||||
|
padding: var(--space-4) var(--space-6) var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding-block: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
padding: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewArea {
|
||||||
|
min-height: 360px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.page {
|
||||||
|
padding-inline: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 118px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
gap: var(--space-5);
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.styleChoices button {
|
||||||
|
padding-inline: var(--space-2);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewHeader,
|
||||||
|
.previewFooter {
|
||||||
|
padding-inline: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewArea {
|
||||||
|
min-height: 300px;
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,212 @@
|
|||||||
|
import { useEffect, useRef, 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 styles from "./Picture.module.css";
|
||||||
|
|
||||||
|
type AspectRatio = "1:1" | "4:5" | "16:9";
|
||||||
|
|
||||||
|
const aspectRatios: AspectRatio[] = ["1:1", "4:5", "16:9"];
|
||||||
|
|
||||||
export default function Picture() {
|
export default function Picture() {
|
||||||
return (
|
const navigate = useNavigate();
|
||||||
<main>
|
const dispatch = useAppDispatch();
|
||||||
<h1>Picture</h1>
|
const { loading, error: requestError } = useAppSelector((state) => state.request);
|
||||||
</main>
|
|
||||||
);
|
const [prompt, setPrompt] = useState("");
|
||||||
|
const [reference, setReference] = useState<File[]>([]);
|
||||||
|
const [aspectRatio, setAspectRatio] = useState<AspectRatio>("1:1");
|
||||||
|
const [formError, setFormError] = useState<string | null>(null);
|
||||||
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(clearRequestError());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
function handleReferenceChange(event: ChangeEvent<HTMLInputElement>) {
|
||||||
|
const nextFile = event.currentTarget.files?.[0] ?? null;
|
||||||
|
|
||||||
|
if (nextFile && !nextFile.type.startsWith("image/")) {
|
||||||
|
setFormError("Выберите файл изображения.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormError(null);
|
||||||
|
//updateReference(nextFile);
|
||||||
|
setIsSubmitted(false);
|
||||||
|
setReference(nextFile ? [...reference, nextFile] : []);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeReference(index: number) {
|
||||||
|
//updateReference(null);
|
||||||
|
setIsSubmitted(false);
|
||||||
|
setReference(reference.filter((_, i) => i !== index));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const text = prompt.trim();
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
setFormError("Добавьте описание картинки.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormError(null);
|
||||||
|
setIsSubmitted(false);
|
||||||
|
|
||||||
|
await dispatch(
|
||||||
|
request({
|
||||||
|
id: Date.now(),
|
||||||
|
text,
|
||||||
|
files: reference ? reference : [],
|
||||||
|
ratio: aspectRatio,
|
||||||
|
}),
|
||||||
|
).unwrap();
|
||||||
|
setIsSubmitted(true);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewClassName = [
|
||||||
|
styles.previewCanvas,
|
||||||
|
aspectRatio === "1:1"
|
||||||
|
? styles.square
|
||||||
|
: aspectRatio === "4:5"
|
||||||
|
? styles.portrait
|
||||||
|
: styles.wide,
|
||||||
|
].join(" ");
|
||||||
|
const status = loading ? "Генерируем" : isSubmitted ? "Запрос принят" : "Ожидание";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.page}>
|
||||||
|
<header className={styles.toolbar}>
|
||||||
|
<div className={styles.brand}>
|
||||||
|
<button
|
||||||
|
className={styles.backButton}
|
||||||
|
type="button"
|
||||||
|
aria-label="На главную"
|
||||||
|
title="На главную"
|
||||||
|
onClick={() => {
|
||||||
|
navigate("/")
|
||||||
|
resetState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">←</span>
|
||||||
|
</button>
|
||||||
|
<img className={styles.logo} src={logo} alt="ShitGenerator" />
|
||||||
|
</div>
|
||||||
|
<span className={styles.mode}>PICTURE</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className={styles.workspace}>
|
||||||
|
<form className={styles.controls} onSubmit={handleSubmit}>
|
||||||
|
<div className={styles.heading}>
|
||||||
|
<p className={styles.eyebrow}>IMAGE LAB</p>
|
||||||
|
<h1>Генерация картинки</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className={styles.field}>
|
||||||
|
<span className={styles.labelRow}>
|
||||||
|
<span>Описание</span>
|
||||||
|
<span>{prompt.length}/800</span>
|
||||||
|
</span>
|
||||||
|
<textarea
|
||||||
|
value={prompt}
|
||||||
|
maxLength={800}
|
||||||
|
placeholder="Опишите изображение"
|
||||||
|
onChange={(event) => {
|
||||||
|
setFormError(null);
|
||||||
|
setPrompt(event.target.value);
|
||||||
|
setIsSubmitted(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<section className={styles.field} aria-labelledby="reference-label">
|
||||||
|
<span className={styles.labelRow} id="reference-label">
|
||||||
|
<span>Референс</span>
|
||||||
|
<span>Необязательно</span>
|
||||||
|
</span>
|
||||||
|
{reference.length > 0 && (
|
||||||
|
reference.map((file, index) => (
|
||||||
|
<div className={styles.fileSelected} key={index}>
|
||||||
|
<span className={styles.fileName}>{file.name}</span>
|
||||||
|
<button
|
||||||
|
className={styles.removeFileButton}
|
||||||
|
type="button"
|
||||||
|
aria-label="Удалить референс"
|
||||||
|
title="Удалить референс"
|
||||||
|
onClick={() => removeReference(index)}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
<label className={styles.uploadButton}>
|
||||||
|
<input type="file" accept="image/*" onChange={handleReferenceChange} />
|
||||||
|
<span>Выбрать файл</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<fieldset className={styles.field}>
|
||||||
|
<span className={styles.labelRow}>Формат</span>
|
||||||
|
<div className={styles.segmentedControl}>
|
||||||
|
{aspectRatios.map((ratio) => (
|
||||||
|
<button
|
||||||
|
className={ratio === aspectRatio ? styles.selected : undefined}
|
||||||
|
type="button"
|
||||||
|
key={ratio}
|
||||||
|
aria-pressed={ratio === aspectRatio}
|
||||||
|
onClick={() => {
|
||||||
|
setAspectRatio(ratio);
|
||||||
|
setIsSubmitted(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ratio}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
{(formError || requestError) && (
|
||||||
|
<p className={styles.error} role="alert">
|
||||||
|
{formError || requestError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button className={styles.generateButton} type="submit" disabled={loading}>
|
||||||
|
{loading ? "Генерируем..." : "Сгенерировать"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<section className={styles.previewPanel} aria-label="Область результата">
|
||||||
|
<div className={styles.previewHeader}>
|
||||||
|
<span>{status}</span>
|
||||||
|
<span className={styles.statusDot} data-active={loading || isSubmitted} aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.previewArea}>
|
||||||
|
<div className={previewClassName}>
|
||||||
|
<div className={styles.emptyState}>
|
||||||
|
<span className={styles.emptyMark} aria-hidden="true" />
|
||||||
|
<span>{loading ? "Генерируем" : isSubmitted ? "Запрос принят" : "Ожидаемое изображение"}</span>
|
||||||
|
</div>
|
||||||
|
{loading && <span className={styles.scanLine} aria-hidden="true" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer className={styles.previewFooter}>
|
||||||
|
<span>{aspectRatio}</span>
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -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%);
|
||||||
|
}
|
||||||
@@ -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() {
|
export default function Video() {
|
||||||
return (
|
const navigate = useNavigate();
|
||||||
<div>video</div>
|
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 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">←</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)}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</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)}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</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)}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1 +1,16 @@
|
|||||||
|
import apiClient from "../config/client";
|
||||||
|
import { Request, Response } from "../model/common";
|
||||||
|
|
||||||
|
export async function createRequest(data: Request): Promise<Response> {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("id", data.id.toString());
|
||||||
|
formData.append("text", data.text);
|
||||||
|
|
||||||
|
data.files.forEach((file) => {
|
||||||
|
formData.append("files", file);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await apiClient.post<Response>("/request", formData);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
@@ -1 +1,68 @@
|
|||||||
|
import {Request, Response} from '../../model/common';
|
||||||
|
import {createAsyncThunk, createSlice, PayloadAction} from '@reduxjs/toolkit';
|
||||||
|
import {createRequest} from '../../services/service';
|
||||||
|
|
||||||
|
interface RequestState {
|
||||||
|
files: File[];
|
||||||
|
text: string;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: RequestState = {
|
||||||
|
files: [],
|
||||||
|
text: '',
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const request = createAsyncThunk(
|
||||||
|
'request/create',
|
||||||
|
async (data: Request, {rejectWithValue}) => {
|
||||||
|
try{
|
||||||
|
const response = await createRequest(data);
|
||||||
|
return response;
|
||||||
|
} catch(error: any){
|
||||||
|
if(error.response?.data?.detail){
|
||||||
|
return rejectWithValue(error.response.data.detail);
|
||||||
|
} else {
|
||||||
|
return rejectWithValue(error instanceof Error ? error.message : "Ошибка создания запроса");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const requestSlice = createSlice({
|
||||||
|
name:'requests',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
clearRequestError(state){
|
||||||
|
state.error = null;
|
||||||
|
},
|
||||||
|
resetState(state){
|
||||||
|
state.files = [];
|
||||||
|
state.text = '';
|
||||||
|
state.loading = false;
|
||||||
|
state.error = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder.addCase(request.pending, (state) => {
|
||||||
|
state.loading = true;
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
builder.addCase(request.fulfilled, (state, action: PayloadAction<Response>) => {
|
||||||
|
state.loading = false;
|
||||||
|
state.error = null;
|
||||||
|
state.files = action.payload.files;
|
||||||
|
})
|
||||||
|
builder.addCase(request.rejected, (state, action) => {
|
||||||
|
state.loading = false;
|
||||||
|
state.error = (action.payload as string) || action.error.message || "Неизвестная ошибка";
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const {clearRequestError, resetState} = requestSlice.actions;
|
||||||
|
|
||||||
|
export default requestSlice.reducer;
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
import { configureStore } from '@reduxjs/toolkit'
|
import { configureStore } from '@reduxjs/toolkit'
|
||||||
//import requestReducer from './slices/slice'
|
import requestReducer from './slices/slice'
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
//request: requestReducer
|
request: requestReducer
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
:root {
|
:root {
|
||||||
color-scheme: light;
|
color-scheme: dark;
|
||||||
|
|
||||||
--font-family-base:
|
--font-family-base:
|
||||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||||
|
|||||||
Reference in new issue
Block a user