Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ef1e381e4 | ||
|
|
3a530300ba | ||
|
|
9de3b363be |
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -3,3 +3,8 @@ export interface Request{
|
|||||||
text: string;
|
text: string;
|
||||||
files: File[];
|
files: File[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Response{
|
||||||
|
test: string;
|
||||||
|
files: File[];
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
.layout {
|
.layout {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
/*padding-block: var(--space-8);*/
|
padding-block: var(--space-8);
|
||||||
}
|
}
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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,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
@@ -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,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
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user