Author SHA1 Message Date
Joe_Peach 6ef1e381e4 Merge pull request 'Feat/api logic' (#1) from feat/api-logic into dev
Reviewed-on: #1
2026-09-25 16:54:22 +00:00
Joe_Peach 3a530300ba create slice and store 2026-09-25 18:40:34 +03:00
Joe_Peach 9de3b363be wrote request 2026-09-25 18:40:00 +03:00
14 changed files with 120 additions and 239 deletions

No files matched your search

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 KiB

@@ -1,100 +0,0 @@
.card {
position: relative;
display: grid;
min-height: 220px;
gap: var(--space-4);
align-content: start;
padding: var(--space-8);
overflow: hidden;
text-align: left;
color: var(--color-text);
cursor: pointer;
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0)),
var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
transition:
border-color var(--duration-fast) var(--ease-standard),
box-shadow var(--duration-fast) var(--ease-standard),
transform var(--duration-fast) var(--ease-standard);
}
.card::before {
position: absolute;
inset: auto -28px -58px auto;
width: 180px;
height: 180px;
content: "";
background: radial-gradient(circle, var(--card-accent), transparent 66%);
opacity: 0.24;
}
.card:hover {
border-color: var(--card-accent);
box-shadow: var(--shadow-lg);
transform: translateY(-4px);
}
.card:focus-visible {
outline: 3px solid var(--card-focus);
outline-offset: 3px;
}
.cyan {
--card-accent: var(--color-primary);
--card-focus: var(--color-primary-soft);
}
.violet {
--card-accent: var(--color-accent);
--card-focus: rgba(143, 84, 255, 0.24);
}
.kicker {
width: fit-content;
padding: var(--space-2) var(--space-3);
font-size: 12px;
font-weight: 800;
line-height: 1;
color: var(--color-black);
text-transform: uppercase;
letter-spacing: 0;
background: var(--card-accent);
border-radius: var(--radius-sm);
}
.title {
max-width: 12ch;
font-size: clamp(28px, 5vw, 48px);
font-weight: 900;
line-height: 1;
}
.description {
max-width: 34ch;
color: var(--color-text-muted);
}
.action {
display: inline-flex;
width: fit-content;
align-items: center;
padding-block: var(--space-2);
margin-top: auto;
font-weight: 800;
color: var(--card-accent);
}
.action::after {
margin-left: var(--space-2);
content: "->";
}
@media (max-width: 640px) {
.card {
min-height: 190px;
padding: var(--space-6);
}
}
@@ -1,31 +0,0 @@
import styles from "./GeneratorCard.module.css";
interface GeneratorCardProps{
title: string;
description: string;
label: string;
accent: "cyan" | "violet";
onClick: () => void;
};
export default function GeneratorCard({
title,
description,
label,
accent,
onClick,
}: GeneratorCardProps) {
return (
<button
className={`${styles.card} ${styles[accent]}`}
type="button"
onClick={onClick}
aria-label={label}
>
<span className={styles.kicker}>{label}</span>
<span className={styles.title}>{title}</span>
<span className={styles.description}>{description}</span>
<span className={styles.action}>Открыть</span>
</button>
);
}
+5
View File
@@ -2,4 +2,9 @@ export interface Request{
id: number; id: number;
text: string; text: string;
files: File[]; files: File[];
}
export interface Response{
test: string;
files: File[];
} }
+1 -1
View File
@@ -1,4 +1,4 @@
.layout { .layout {
min-height: 100vh; min-height: 100vh;
/*padding-block: var(--space-8);*/ padding-block: var(--space-8);
} }
+2 -1
View File
@@ -1,4 +1,5 @@
import { Outlet } from "react-router-dom"; import { Navigate, Outlet } from "react-router-dom";
import { useAppSelector } from "../../store/hooks";
import styles from "./Layout.module.css"; import styles from "./Layout.module.css";
export default function Layout() { export default function Layout() {
+5 -38
View File
@@ -1,48 +1,15 @@
.page { .page {
display: grid; display: grid;
gap: var(--space-12);
min-height: 100vh;
padding: var(--space-10);
align-content: center;
background: var(--color-bg);
}
.hero {
display: grid;
justify-items: center;
gap: var(--space-4); gap: var(--space-4);
text-align: center;
}
.logo {
width: min(540px, 86vw);
height: auto;
object-fit: contain;
filter: drop-shadow(0 24px 56px rgba(53, 216, 255, 0.2));
} }
.title { .title {
margin: 0; margin: 0;
font-size: clamp(40px, 8vw, 88px); font-size: 32px;
font-weight: 900; line-height: 1.15;
line-height: 0.95;
color: var(--color-text);
text-shadow: 0 0 32px rgba(53, 216, 255, 0.28);
} }
.cards { .text {
display: grid; margin: 0;
grid-template-columns: repeat(2, minmax(0, 1fr)); color: var(--color-text-muted);
gap: var(--space-6);
}
@media (max-width: 760px) {
.page {
gap: var(--space-8);
padding-block: var(--space-8);
}
.cards {
grid-template-columns: 1fr;
}
} }
+5 -32
View File
@@ -1,42 +1,15 @@
import { useNavigate } from "react-router-dom";
import GeneratorCard from "../../components/GeneratorCard/GeneratorCard";
import logo from "../../assets/common/HITGENERATOR-Logo.png";
import styles from "./Main.module.css"; import styles from "./Main.module.css";
import {useNavigate} from "react-router-dom";
export default function Main() { export default function Main() {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<main className={styles.page}> <main className={styles.page}>
<section className={styles.hero} aria-labelledby="main-title"> <h1 className={styles.title}>Главная</h1>
<img <p className={styles.text}>Базовая страница приложения готова.</p>
className={styles.logo} <button onClick={() => navigate('/picture')}>Go to Picture</button>
src={logo} <button onClick={() => navigate('/video')}>Go to Video</button>
alt="ShitGenerator"
width="1080"
height="608"
/>
<h1 className={styles.title} id="main-title">
ShitGenerator
</h1>
</section>
<section className={styles.cards} aria-label="Выбор генератора">
<GeneratorCard
title="Генерация видео"
description="Собирай видеоролики из идеи, промпта и нужного визуального направления."
label="Видео"
accent="cyan"
onClick={() => navigate("/video")}
/>
<GeneratorCard
title="Генерация картинок"
description="Создавай изображения под задачу, стиль и настроение за пару кликов."
label="Картинки"
accent="violet"
onClick={() => navigate("/picture")}
/>
</section>
</main> </main>
); );
} }
+15
View File
@@ -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;
}
+61
View File
@@ -1 +1,62 @@
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;
}
},
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} = requestSlice.actions;
export default requestSlice.reducer;
+2 -2
View File
@@ -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
View File
@@ -1,9 +1,5 @@
body { body {
background-image: url('../assets/common/background.jpeg'); background: var(--color-bg);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
color: var(--color-text); color: var(--color-text);
font-family: var(--font-family-base); font-family: var(--font-family-base);
line-height: 1.5; line-height: 1.5;
+23 -29
View File
@@ -6,21 +6,18 @@
sans-serif; sans-serif;
--color-white: #ffffff; --color-white: #ffffff;
--color-black: #030610; --color-black: #0b1220;
--color-gray-50: #f6f8ff; --color-gray-50: #f8fafc;
--color-gray-100: #ecf2ff; --color-gray-100: #f1f5f9;
--color-gray-200: #d8e3fb; --color-gray-200: #e2e8f0;
--color-gray-300: #b8c7e7; --color-gray-300: #cbd5e1;
--color-gray-500: #7888aa; --color-gray-500: #64748b;
--color-gray-600: #52617f; --color-gray-600: #475569;
--color-gray-700: #243452; --color-gray-700: #334155;
--color-gray-900: #07142d; --color-gray-900: #0f172a;
--color-cyan-50: #e9fbff; --color-teal-50: #ecfeff;
--color-cyan-400: #35d8ff; --color-teal-600: #0d9488;
--color-cyan-600: #0899d8; --color-teal-700: #0f766e;
--color-violet-500: #8f54ff;
--color-coral-500: #ff6b79;
--color-gold-400: #ffc96b;
--color-rose-50: #fff1f2; --color-rose-50: #fff1f2;
--color-rose-200: #fecdd3; --color-rose-200: #fecdd3;
--color-rose-700: #be123c; --color-rose-700: #be123c;
@@ -28,18 +25,15 @@
--color-green-200: #bbf7d0; --color-green-200: #bbf7d0;
--color-green-800: #166534; --color-green-800: #166534;
--color-bg: #050914; --color-bg: var(--color-gray-50);
--color-surface: rgba(9, 18, 42, 0.86); --color-surface: var(--color-white);
--color-surface-muted: rgba(17, 32, 67, 0.78); --color-surface-muted: var(--color-gray-100);
--color-text: #f7fbff; --color-text: var(--color-gray-900);
--color-text-muted: #aab8d8; --color-text-muted: var(--color-gray-600);
--color-border: rgba(82, 208, 255, 0.22); --color-border: var(--color-gray-200);
--color-primary: var(--color-cyan-400); --color-primary: var(--color-teal-700);
--color-primary-hover: var(--color-gold-400); --color-primary-hover: #115e59;
--color-primary-soft: rgba(53, 216, 255, 0.16); --color-primary-soft: rgba(13, 148, 136, 0.12);
--color-accent: var(--color-violet-500);
--color-accent-warm: var(--color-coral-500);
--color-accent-gold: var(--color-gold-400);
--color-danger-bg: var(--color-rose-50); --color-danger-bg: var(--color-rose-50);
--color-danger-border: var(--color-rose-200); --color-danger-border: var(--color-rose-200);
--color-danger-text: var(--color-rose-700); --color-danger-text: var(--color-rose-700);
@@ -61,8 +55,8 @@
--radius-md: 8px; --radius-md: 8px;
--radius-lg: 12px; --radius-lg: 12px;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.24); --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.08);
--shadow-lg: 0 24px 80px rgba(0, 0, 0, 0.36); --shadow-lg: 0 24px 80px rgba(15, 23, 42, 0.14);
--duration-fast: 160ms; --duration-fast: 160ms;
--ease-standard: ease; --ease-standard: ease;