Compare commits
8
Commits
master
...
9ec5e852c6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ec5e852c6 | ||
|
|
6ef1e381e4 | ||
|
|
3a530300ba | ||
|
|
9de3b363be | ||
|
|
2048c17eee | ||
|
|
b98b531162 | ||
|
|
6770eba5b6 | ||
|
|
c6e4801ff6 |
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 193 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 450 KiB |
@@ -0,0 +1,100 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import Layout from '../pages/Layout/Layout';
|
||||
import Main from '../pages/Main/Main';
|
||||
import Picture from '../pages/Picture/Picture';
|
||||
import Video from '../pages/Video/VideoPage';
|
||||
|
||||
export default function AppRoutes(){
|
||||
return (
|
||||
<Routes>
|
||||
<Route path='/' element={<Layout />}>
|
||||
<Route index element={<Main />} />
|
||||
<Route path='picture' element={<Picture />} />
|
||||
<Route path="video" element={<Video />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
|
||||
@@ -2,4 +2,9 @@ export interface Request{
|
||||
id: number;
|
||||
text: string;
|
||||
files: File[];
|
||||
}
|
||||
|
||||
export interface Response{
|
||||
test: string;
|
||||
files: File[];
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
.layout {
|
||||
min-height: 100vh;
|
||||
padding-block: var(--space-8);
|
||||
/*padding-block: var(--space-8);*/
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
import { useAppSelector } from "../../store/hooks";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import styles from "./Layout.module.css";
|
||||
|
||||
export default function Layout() {
|
||||
|
||||
@@ -1,15 +1,48 @@
|
||||
.page {
|
||||
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);
|
||||
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 {
|
||||
margin: 0;
|
||||
font-size: 32px;
|
||||
line-height: 1.15;
|
||||
font-size: clamp(40px, 8vw, 88px);
|
||||
font-weight: 900;
|
||||
line-height: 0.95;
|
||||
color: var(--color-text);
|
||||
text-shadow: 0 0 32px rgba(53, 216, 255, 0.28);
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-6);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.page {
|
||||
gap: var(--space-8);
|
||||
padding-block: var(--space-8);
|
||||
}
|
||||
|
||||
.cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
+34
-2
@@ -1,10 +1,42 @@
|
||||
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";
|
||||
|
||||
export default function Main() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<main className={styles.page}>
|
||||
<h1 className={styles.title}>Главная</h1>
|
||||
<p className={styles.text}>Базовая страница приложения готова.</p>
|
||||
<section className={styles.hero} aria-labelledby="main-title">
|
||||
<img
|
||||
className={styles.logo}
|
||||
src={logo}
|
||||
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>
|
||||
);
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,8 @@
|
||||
|
||||
export default function Picture() {
|
||||
return (
|
||||
<main>
|
||||
<h1>Picture</h1>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,6 @@
|
||||
|
||||
export default function Video() {
|
||||
return (
|
||||
<div>video</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,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 requestReducer from './slices/slice'
|
||||
import requestReducer from './slices/slice'
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
//request: requestReducer
|
||||
request: requestReducer
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
body {
|
||||
background: var(--color-bg);
|
||||
background-image: url('../assets/common/background.jpeg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family-base);
|
||||
line-height: 1.5;
|
||||
|
||||
+29
-23
@@ -6,18 +6,21 @@
|
||||
sans-serif;
|
||||
|
||||
--color-white: #ffffff;
|
||||
--color-black: #0b1220;
|
||||
--color-gray-50: #f8fafc;
|
||||
--color-gray-100: #f1f5f9;
|
||||
--color-gray-200: #e2e8f0;
|
||||
--color-gray-300: #cbd5e1;
|
||||
--color-gray-500: #64748b;
|
||||
--color-gray-600: #475569;
|
||||
--color-gray-700: #334155;
|
||||
--color-gray-900: #0f172a;
|
||||
--color-teal-50: #ecfeff;
|
||||
--color-teal-600: #0d9488;
|
||||
--color-teal-700: #0f766e;
|
||||
--color-black: #030610;
|
||||
--color-gray-50: #f6f8ff;
|
||||
--color-gray-100: #ecf2ff;
|
||||
--color-gray-200: #d8e3fb;
|
||||
--color-gray-300: #b8c7e7;
|
||||
--color-gray-500: #7888aa;
|
||||
--color-gray-600: #52617f;
|
||||
--color-gray-700: #243452;
|
||||
--color-gray-900: #07142d;
|
||||
--color-cyan-50: #e9fbff;
|
||||
--color-cyan-400: #35d8ff;
|
||||
--color-cyan-600: #0899d8;
|
||||
--color-violet-500: #8f54ff;
|
||||
--color-coral-500: #ff6b79;
|
||||
--color-gold-400: #ffc96b;
|
||||
--color-rose-50: #fff1f2;
|
||||
--color-rose-200: #fecdd3;
|
||||
--color-rose-700: #be123c;
|
||||
@@ -25,15 +28,18 @@
|
||||
--color-green-200: #bbf7d0;
|
||||
--color-green-800: #166534;
|
||||
|
||||
--color-bg: var(--color-gray-50);
|
||||
--color-surface: var(--color-white);
|
||||
--color-surface-muted: var(--color-gray-100);
|
||||
--color-text: var(--color-gray-900);
|
||||
--color-text-muted: var(--color-gray-600);
|
||||
--color-border: var(--color-gray-200);
|
||||
--color-primary: var(--color-teal-700);
|
||||
--color-primary-hover: #115e59;
|
||||
--color-primary-soft: rgba(13, 148, 136, 0.12);
|
||||
--color-bg: #050914;
|
||||
--color-surface: rgba(9, 18, 42, 0.86);
|
||||
--color-surface-muted: rgba(17, 32, 67, 0.78);
|
||||
--color-text: #f7fbff;
|
||||
--color-text-muted: #aab8d8;
|
||||
--color-border: rgba(82, 208, 255, 0.22);
|
||||
--color-primary: var(--color-cyan-400);
|
||||
--color-primary-hover: var(--color-gold-400);
|
||||
--color-primary-soft: rgba(53, 216, 255, 0.16);
|
||||
--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-border: var(--color-rose-200);
|
||||
--color-danger-text: var(--color-rose-700);
|
||||
@@ -55,8 +61,8 @@
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.08);
|
||||
--shadow-lg: 0 24px 80px rgba(15, 23, 42, 0.14);
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
--shadow-lg: 0 24px 80px rgba(0, 0, 0, 0.36);
|
||||
|
||||
--duration-fast: 160ms;
|
||||
--ease-standard: ease;
|
||||
|
||||
Reference in new issue
Block a user