id
24
absolete_frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
11
absolete_frontend/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 5173
|
||||
CMD ["npm", "run", "dev"]
|
||||
113
absolete_frontend/REACT.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Vontor CZ
|
||||
|
||||
Welcome to **Vontor CZ**!
|
||||
|
||||
## frontend Folder Overview
|
||||
|
||||
The `frontend` folder contains all code and assets for the client-side React application. Its structure typically includes:
|
||||
|
||||
- **src/**
|
||||
Tady se ukladají věci, které uživateli se nepředavají přímo spíš takové stavební kostky.
|
||||
- **api/**
|
||||
TypeScript/JS soubory které se starají o API a o JWT tokeny.
|
||||
Čistě pracují s backendem
|
||||
- **context/**
|
||||
Kontext si načte data které mu předáš a můžeš si je předávat mezi komponenty rychleji.
|
||||
- **hooks/**
|
||||
Pracuje s API a formátují to do výstupu který potřebujeme.
|
||||
|
||||
- **components/**
|
||||
Konktrétní komponenty které se vykreslují na stránce.
|
||||
|
||||
Už využívají už hooky a contexty pro vykreslení informaci bez složite logiky (ať je komponenta hezky čistá a né moc obsáhla).
|
||||
|
||||
- **features/**
|
||||
Nejsou to celé stránky, ale hotové komponenty.
|
||||
|
||||
Obsahuje všechny komponenty plus její hooky, API, state a utils potřebné pro jednu konkrétní funkcionalitu aplikace. (použijí se jenom jednou)
|
||||
|
||||
Features zajišťují modularitu a přehlednost aplikace.
|
||||
|
||||
Příklad: komponenta košík, která zahrnuje API volání, state management a UI komponenty.
|
||||
|
||||
---
|
||||
|
||||
- **layouts/**
|
||||
Tohle je jenom komponenta, která načte další komponenty, ale používá se jako layout kde jsou například navigace footer a ostatní se opakující prvky stránky, ale hlavní obsah ještě není součastí! Ten se načte skrz <outlet/> v pages.
|
||||
- **pages/**
|
||||
Tady se jde do finále tady se vkládají samostatné komponenty a tvoří se už hlavní obsah stránky a vkládají se komponenty a tvoří se logika mezi ně.
|
||||
- **routes/**
|
||||
tady se ukládají routy které například zabraňuji načtení stránky nepříhlášeným uživatelům nebo jenom pro ty s určitou roli/oprávněním... tyhle route komponenty se pak využívají v
|
||||
|
||||
---
|
||||
|
||||
- **assets/**
|
||||
Obrázky, fonty, a ostatní statické soubory.
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
- **App.tsx**
|
||||
Root komponenta pro aplikaci, kde se nastavují routy pro jednotlivé stránky.
|
||||
Pozor nemyslím ty routy ze složky routes/ ... to jsou jenom obaly pro konktrétní routy pro jednotlivé stránky.
|
||||
- **main.tsx**
|
||||
Vstupní bod pro načítaní aplikace (načíta se první komponenta App.jsx)
|
||||
|
||||
---
|
||||
|
||||
- **utils/**
|
||||
Sběrné místo pro pomocné funkce, které nejsou přímo komponenty nebo hooky, ale jsou znovupoužitelné napříč aplikací.
|
||||
|
||||
---
|
||||
|
||||
- **index.css**
|
||||
globální styly
|
||||
- **App.css**
|
||||
obsahuje stylovaní layoutu, navigace, footer, error okna atd. takové věci které zůstavjí vždy stejně
|
||||
|
||||
- **public/**
|
||||
Složka public obsahuje statické soubory dostupné přímo přes URL (např. index.html, favicon, obrázky), které React přímo nereenderuje ani neoptimalizuje.
|
||||
|
||||
- **package.json**
|
||||
něco jak requirements.txt v pythonu
|
||||
|
||||
- **vite.config.js / vite.config.ts**
|
||||
Vite konfigurace pro building a serving frontend aplikace.
|
||||
|
||||
- **Dockerfile**
|
||||
Konfigurace pro Docker
|
||||
|
||||
## Getting Started :3
|
||||
|
||||
1. **Instalace balíčku (bere z package.json):**
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Start dev server:**
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
4. **Build pro produkci(finále):**
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
5. **Preview production build:**
|
||||
```sh
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## Creating a New React Project with Vite
|
||||
|
||||
If you want to start a new project:
|
||||
|
||||
```sh
|
||||
npm create vite@latest
|
||||
# Choose 'react' or 'react-ts' for TypeScript
|
||||
cd your-project
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
23
absolete_frontend/eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { globalIgnores } from 'eslint/config'
|
||||
|
||||
export default tseslint.config([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
14
absolete_frontend/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="stylesheet" href="reset.css">
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4267
absolete_frontend/package-lock.json
generated
Normal file
36
absolete_frontend/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@types/react-router": "^5.1.20",
|
||||
"axios": "^1.13.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^7.8.1",
|
||||
"tailwindcss": "^4.1.16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.33.0",
|
||||
"@types/axios": "^0.9.36",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.3.0",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.39.1",
|
||||
"vite": "^7.1.2"
|
||||
}
|
||||
}
|
||||
4
absolete_frontend/public/PUBLIC.md
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
# Anything you import in JS/CSS → src/assets/
|
||||
|
||||
# Anything you reference directly in HTML → public/ something like: <img src="/images/foo.png">
|
||||
0
absolete_frontend/public/favicon.ico
Normal file
BIN
absolete_frontend/public/portfolio/davo1.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
absolete_frontend/public/portfolio/epinger.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
absolete_frontend/public/portfolio/perlica.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
76
absolete_frontend/reset.css
Normal file
@@ -0,0 +1,76 @@
|
||||
/*https://www.joshwcomeau.com/css/custom-css-reset/*/
|
||||
|
||||
|
||||
/* 1. Use a more-intuitive box-sizing model */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 2. Remove default margin */
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
/* 3. Add accessible line-height */
|
||||
line-height: 1.5;
|
||||
/* 4. Improve text rendering */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
ul, li{
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 5. Improve media defaults */
|
||||
img,
|
||||
picture,
|
||||
video,
|
||||
canvas,
|
||||
svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* 6. Inherit fonts for form controls */
|
||||
input,
|
||||
button,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
/* 7. Avoid text overflows */
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
padding-bottom: 0.5ch;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* 8. Improve line wrapping */
|
||||
p {
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
/*
|
||||
9. Create a root stacking context
|
||||
*/
|
||||
#root,
|
||||
#__next {
|
||||
isolation: isolate;
|
||||
}
|
||||
42
absolete_frontend/src/App.css
Normal file
@@ -0,0 +1,42 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
33
absolete_frontend/src/App.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { BrowserRouter as Router, Routes, Route, Link, Outlet } from "react-router-dom"
|
||||
import Home from "./pages/home/home";
|
||||
import HomeLayout from "./layouts/HomeLayout";
|
||||
import Downloader from "./pages/downloader/Downloader";
|
||||
|
||||
import PrivateRoute from "./routes/PrivateRoute";
|
||||
|
||||
import { UserContextProvider } from "./context/UserContext";
|
||||
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Router>
|
||||
<UserContextProvider>
|
||||
|
||||
{/* Layout route */}
|
||||
<Route path="/" element={<HomeLayout />}>
|
||||
<Route index element={<Home />} />
|
||||
<Route path="downloader" element={<Downloader />} />
|
||||
</Route>
|
||||
|
||||
<Route element={<PrivateRoute />}>
|
||||
{/* Protected routes go here */}
|
||||
<Route path="/" element={<HomeLayout />} >
|
||||
<Route path="protected-downloader" element={<Downloader />} />
|
||||
</Route>
|
||||
|
||||
</Route>
|
||||
|
||||
</UserContextProvider>
|
||||
</Router>
|
||||
)
|
||||
}
|
||||
275
absolete_frontend/src/api/Client.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import axios from "axios";
|
||||
|
||||
// --- ENV CONFIG ---
|
||||
const API_BASE_URL =
|
||||
import.meta.env.VITE_API_BASE_URL || "http://localhost:8000";
|
||||
|
||||
const LOGIN_PATH = import.meta.env.VITE_LOGIN_PATH || "/login";
|
||||
|
||||
|
||||
// --- ERROR EVENT BUS ---
|
||||
const ERROR_EVENT = "api:error";
|
||||
type ApiErrorDetail = {
|
||||
message: string;
|
||||
status?: number;
|
||||
url?: string;
|
||||
data?: unknown;
|
||||
};
|
||||
|
||||
// Use interface instead of arrow function types for readability
|
||||
interface ApiErrorHandler {
|
||||
(e: CustomEvent<ApiErrorDetail>): void;
|
||||
}
|
||||
|
||||
function notifyError(detail: ApiErrorDetail) {
|
||||
window.dispatchEvent(new CustomEvent<ApiErrorDetail>(ERROR_EVENT, { detail }));
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("[API ERROR]", detail);
|
||||
}
|
||||
function onError(handler: ApiErrorHandler) {
|
||||
const wrapped = handler as EventListener;
|
||||
window.addEventListener(ERROR_EVENT, wrapped as EventListener);
|
||||
|
||||
return () => window.removeEventListener(ERROR_EVENT, wrapped);
|
||||
}
|
||||
|
||||
// --- AXIOS INSTANCES ---
|
||||
// Always send cookies. Django will set auth cookies; browser will include them automatically.
|
||||
function createAxios(baseURL: string): any {
|
||||
const instance = axios.create({
|
||||
baseURL,
|
||||
withCredentials: true, // cookies
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
timeout: 20000,
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Use a single behavior for both: cookies are always sent
|
||||
const apiPublic = createAxios(API_BASE_URL);
|
||||
const apiAuth = createAxios(API_BASE_URL);
|
||||
|
||||
|
||||
// --- REQUEST INTERCEPTOR (PUBLIC) ---
|
||||
// Ensure no Authorization header is ever sent by the public client
|
||||
apiPublic.interceptors.request.use(function (config: any) {
|
||||
if (config?.headers && (config.headers as any).Authorization) {
|
||||
delete (config.headers as any).Authorization;
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
// --- REQUEST INTERCEPTOR (AUTH) ---
|
||||
// Do not attach Authorization header; rely on cookies set by Django.
|
||||
apiAuth.interceptors.request.use(function (config: any) {
|
||||
(config as any)._retryCount = (config as any)._retryCount || 0;
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
// --- RESPONSE INTERCEPTOR (AUTH) ---
|
||||
// Simplified: on 401, redirect to login. Server manages refresh via cookies.
|
||||
apiAuth.interceptors.response.use(
|
||||
function (response: any) {
|
||||
return response;
|
||||
},
|
||||
async function (error: any) {
|
||||
if (!error.response) {
|
||||
alert("Backend connection is unavailable. Please check your network.");
|
||||
|
||||
notifyError({
|
||||
message: "Network error or backend unavailable",
|
||||
url: error.config?.url,
|
||||
});
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const status = error.response.status;
|
||||
if (status === 401) {
|
||||
ClearTokens();
|
||||
window.location.assign(LOGIN_PATH);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
notifyError({
|
||||
message:
|
||||
(error.response.data as any)?.detail ||
|
||||
(error.response.data as any)?.message ||
|
||||
`Request failed with status ${status}`,
|
||||
status,
|
||||
url: error.config?.url,
|
||||
data: error.response.data,
|
||||
});
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// --- PUBLIC CLIENT: still emits errors and alerts on network failure ---
|
||||
apiPublic.interceptors.response.use(
|
||||
function (response: any) {
|
||||
return response;
|
||||
},
|
||||
async function (error: any) {
|
||||
if (!error.response) {
|
||||
alert("Backend connection is unavailable. Please check your network.");
|
||||
notifyError({
|
||||
message: "Network error or backend unavailable",
|
||||
url: error.config?.url,
|
||||
});
|
||||
return Promise.reject(error);
|
||||
}
|
||||
notifyError({
|
||||
message:
|
||||
(error.response.data as any)?.detail ||
|
||||
(error.response.data as any)?.message ||
|
||||
`Request failed with status ${error.response.status}`,
|
||||
status: error.response.status,
|
||||
url: error.config?.url,
|
||||
data: error.response.data,
|
||||
});
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
function Logout() {
|
||||
try {
|
||||
const LogOutResponse = apiAuth.post("/api/logout/");
|
||||
|
||||
if (LogOutResponse.body.detail != "Logout successful") {
|
||||
throw new Error("Logout failed");
|
||||
}
|
||||
|
||||
ClearTokens();
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error during logout:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function ClearTokens(){
|
||||
document.cookie = "access_token=; Max-Age=0; path=/";
|
||||
document.cookie = "refresh_token=; Max-Age=0; path=/";
|
||||
}
|
||||
|
||||
|
||||
// --- EXPORT DEFAULT API WRAPPER ---
|
||||
const Client = {
|
||||
// Axios instances
|
||||
auth: apiAuth,
|
||||
public: apiPublic,
|
||||
|
||||
Logout,
|
||||
|
||||
// Error subscription
|
||||
onError,
|
||||
};
|
||||
|
||||
export default Client;
|
||||
|
||||
/**
|
||||
USAGE EXAMPLES (TypeScript/React)
|
||||
|
||||
Import the client
|
||||
--------------------------------------------------
|
||||
import Client from "@/api/Client";
|
||||
|
||||
|
||||
Login: obtain tokens and persist to cookies
|
||||
--------------------------------------------------
|
||||
async function login(username: string, password: string) {
|
||||
// SimpleJWT default login endpoint (adjust if your backend differs)
|
||||
// Example backend endpoint: POST /api/token/ -> { access, refresh }
|
||||
const res = await Client.public.post("/api/token/", { username, password });
|
||||
const { access, refresh } = res.data;
|
||||
Client.setTokens(access, refresh);
|
||||
// After this, Client.auth will automatically attach Authorization header
|
||||
// and refresh when receiving a 401 (up to 2 retries).
|
||||
}
|
||||
|
||||
|
||||
Public request (no cookies, no Authorization)
|
||||
--------------------------------------------------
|
||||
// The public client does NOT send cookies or Authorization.
|
||||
async function listPublicItems() {
|
||||
const res = await Client.public.get("/api/public/items/");
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
Authenticated requests (auto Bearer header + refresh on 401)
|
||||
--------------------------------------------------
|
||||
async function fetchProfile() {
|
||||
const res = await Client.auth.get("/api/users/me/");
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async function updateProfile(payload: { first_name?: string; last_name?: string }) {
|
||||
const res = await Client.auth.patch("/api/users/me/", payload);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
Global error handling (UI notifications)
|
||||
--------------------------------------------------
|
||||
import { useEffect } from "react";
|
||||
|
||||
function useApiErrors(showToast: (msg: string) => void) {
|
||||
useEffect(function () {
|
||||
const unsubscribe = Client.onError(function (e) {
|
||||
const { message, status } = e.detail;
|
||||
showToast(status ? String(status) + ": " + message : message);
|
||||
});
|
||||
return unsubscribe;
|
||||
}, [showToast]);
|
||||
}
|
||||
|
||||
// Note: Network connectivity issues trigger an alert and also dispatch api:error.
|
||||
// All errors are logged to console for developers.
|
||||
|
||||
|
||||
Logout
|
||||
--------------------------------------------------
|
||||
function logout() {
|
||||
Client.clearTokens();
|
||||
window.location.assign("/login");
|
||||
}
|
||||
|
||||
|
||||
Route protection (PrivateRoute)
|
||||
--------------------------------------------------
|
||||
// If you created src/routes/PrivateRoute.tsx, wrap your protected routes with it.
|
||||
// PrivateRoute checks for "access_token" cookie presence and redirects to /login if missing.
|
||||
|
||||
// Example:
|
||||
// <Routes>
|
||||
// <Route element={<PrivateRoute />} >
|
||||
// <Route element={<MainLayout />}>
|
||||
// <Route path="/" element={<Dashboard />} />
|
||||
// <Route path="/profile" element={<Profile />} />
|
||||
// </Route>
|
||||
// </Route>
|
||||
// <Route path="/login" element={<Login />} />
|
||||
// </Routes>
|
||||
|
||||
|
||||
Refresh and retry flow (what happens on 401)
|
||||
--------------------------------------------------
|
||||
// 1) Client.auth request receives 401 from backend
|
||||
// 2) Client tries to refresh access token using refresh_token cookie
|
||||
// 3) If refresh succeeds, original request is retried (max 2 times)
|
||||
// 4) If still 401 (or no refresh token), tokens are cleared and user is redirected to /login
|
||||
|
||||
|
||||
Environment variables (optional overrides)
|
||||
--------------------------------------------------
|
||||
// VITE_API_BASE_URL default: "http://localhost:8000"
|
||||
// VITE_API_REFRESH_URL default: "/api/token/refresh/"
|
||||
// VITE_LOGIN_PATH default: "/login"
|
||||
*/
|
||||
114
absolete_frontend/src/api/apps/Downloader.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import Client from "../Client";
|
||||
|
||||
// Available output containers (must match backend)
|
||||
export const FORMAT_EXTS = ["mp4", "mkv", "webm", "flv", "mov", "avi", "ogg"] as const;
|
||||
export type FormatExt = (typeof FORMAT_EXTS)[number];
|
||||
|
||||
export type InfoResponse = {
|
||||
title: string | null;
|
||||
duration: number | null;
|
||||
thumbnail: string | null;
|
||||
video_resolutions: string[]; // e.g. ["2160p", "1440p", "1080p", ...]
|
||||
audio_resolutions: string[]; // e.g. ["320kbps", "160kbps", ...]
|
||||
};
|
||||
|
||||
// GET info for a URL
|
||||
export async function fetchInfo(url: string): Promise<InfoResponse> {
|
||||
const res = await Client.public.get("/api/downloader/download/", {
|
||||
params: { url },
|
||||
});
|
||||
return res.data as InfoResponse;
|
||||
}
|
||||
|
||||
// POST to stream binary immediately; returns { blob, filename }
|
||||
export async function downloadImmediate(args: {
|
||||
url: string;
|
||||
ext: FormatExt;
|
||||
videoResolution?: string | number; // "1080p" or 1080
|
||||
audioResolution?: string | number; // "160kbps" or 160
|
||||
}): Promise<{ blob: Blob; filename: string }> {
|
||||
const video_quality = toHeight(args.videoResolution);
|
||||
const audio_quality = toKbps(args.audioResolution);
|
||||
|
||||
if (video_quality == null || audio_quality == null) {
|
||||
throw new Error("Please select both video and audio quality.");
|
||||
}
|
||||
|
||||
const res = await Client.public.post(
|
||||
"/api/downloader/download/",
|
||||
{
|
||||
url: args.url,
|
||||
ext: args.ext,
|
||||
video_quality,
|
||||
audio_quality,
|
||||
},
|
||||
{ responseType: "blob" }
|
||||
);
|
||||
|
||||
const cd = res.headers?.["content-disposition"] as string | undefined;
|
||||
const xfn = res.headers?.["x-filename"] as string | undefined;
|
||||
const filename =
|
||||
parseContentDispositionFilename(cd) ||
|
||||
(xfn && xfn.trim()) ||
|
||||
inferFilenameFromUrl(args.url, res.headers?.["content-type"] as string | undefined) ||
|
||||
`download.${args.ext}`;
|
||||
|
||||
return { blob: res.data as Blob, filename };
|
||||
}
|
||||
|
||||
// Helpers
|
||||
export function parseContentDispositionFilename(cd?: string): string | null {
|
||||
if (!cd) return null;
|
||||
const utf8Match = cd.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
|
||||
if (utf8Match?.[1]) return decodeURIComponent(utf8Match[1]);
|
||||
const plainMatch = cd.match(/filename\s*=\s*"([^"]+)"/i) || cd.match(/filename\s*=\s*([^;]+)/i);
|
||||
return plainMatch?.[1]?.trim() || null;
|
||||
}
|
||||
|
||||
function inferFilenameFromUrl(url: string, contentType?: string): string {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
const last = u.pathname.split("/").filter(Boolean).pop();
|
||||
if (last) return last;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (contentType) {
|
||||
const ext = contentTypeToExt(contentType);
|
||||
return `download${ext ? `.${ext}` : ""}`;
|
||||
}
|
||||
return "download.bin";
|
||||
}
|
||||
|
||||
function contentTypeToExt(ct?: string): string | null {
|
||||
if (!ct) return null;
|
||||
const map: Record<string, string> = {
|
||||
"video/mp4": "mp4",
|
||||
"video/x-matroska": "mkv",
|
||||
"video/webm": "webm",
|
||||
"video/x-flv": "flv",
|
||||
"video/quicktime": "mov",
|
||||
"video/x-msvideo": "avi",
|
||||
"video/ogg": "ogg",
|
||||
"application/octet-stream": "bin",
|
||||
};
|
||||
return map[ct] || null;
|
||||
}
|
||||
|
||||
function toHeight(v?: string | number): number | undefined {
|
||||
if (typeof v === "number") return v || undefined;
|
||||
if (!v) return undefined;
|
||||
const m = /^(\d{2,4})p$/i.exec(v.trim());
|
||||
if (m) return parseInt(m[1], 10);
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? (n as number) : undefined;
|
||||
}
|
||||
|
||||
function toKbps(v?: string | number): number | undefined {
|
||||
if (typeof v === "number") return v || undefined;
|
||||
if (!v) return undefined;
|
||||
const m = /^(\d{2,4})\s*kbps$/i.exec(v.trim());
|
||||
if (m) return parseInt(m[1], 10);
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? (n as number) : undefined;
|
||||
}
|
||||
41
absolete_frontend/src/api/get_chocies.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import Client from "./Client";
|
||||
|
||||
/**
|
||||
* Loads enum values from an OpenAPI schema for a given path, method, and field (e.g., category).
|
||||
*
|
||||
* @param path - API path, e.g., "/api/service-tickets/"
|
||||
* @param method - HTTP method
|
||||
* @param field - field name in parameters or request
|
||||
* @param schemaUrl - URL of the JSON schema, default "/api/schema/?format=json"
|
||||
* @returns Promise<Array<{ value: string; label: string }>>
|
||||
*/
|
||||
export async function fetchEnumFromSchemaJson(
|
||||
path: string,
|
||||
method: "get" | "post" | "patch" | "put",
|
||||
field: string,
|
||||
schemaUrl: string = "/schema/?format=json"
|
||||
): Promise<Array<{ value: string; label: string }>> {
|
||||
try {
|
||||
const schema = await Client.public.get(schemaUrl);
|
||||
|
||||
const methodDef = schema.paths?.[path]?.[method];
|
||||
if (!methodDef) {
|
||||
throw new Error(`Method ${method.toUpperCase()} for ${path} not found in schema.`);
|
||||
}
|
||||
|
||||
// Search in "parameters" (e.g., GET query parameters)
|
||||
const param = methodDef.parameters?.find((p: any) => p.name === field);
|
||||
|
||||
if (param?.schema?.enum) {
|
||||
return param.schema.enum.map((val: string) => ({
|
||||
value: val,
|
||||
label: val,
|
||||
}));
|
||||
}
|
||||
|
||||
throw new Error(`Field '${field}' does not contain enum`);
|
||||
} catch (error) {
|
||||
console.error("Error loading enum values:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
82
absolete_frontend/src/api/models/User.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
// frontend/src/api/model/user.js
|
||||
// User API model for searching users by username
|
||||
// Structure matches other model files (see order.js for reference)
|
||||
|
||||
import Client from '../Client';
|
||||
|
||||
const API_BASE_URL = "/account/users";
|
||||
|
||||
const userAPI = {
|
||||
/**
|
||||
* Get current authenticated user
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
async getCurrentUser() {
|
||||
const response = await Client.auth.get(`${API_BASE_URL}/me/`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all users
|
||||
* @returns {Promise<Array<User>>}
|
||||
*/
|
||||
async getUsers(params: Object) {
|
||||
const response = await Client.auth.get(`${API_BASE_URL}/`, { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a single user by ID
|
||||
* @param {number|string} id
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
async getUser(id: number) {
|
||||
const response = await Client.auth.get(`${API_BASE_URL}/${id}/`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a user by ID
|
||||
* @param {number|string} id
|
||||
* @param {Object} data
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
async updateUser(id: number, data: Object) {
|
||||
const response = await Client.auth.patch(`${API_BASE_URL}/${id}/`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a user by ID
|
||||
* @param {number|string} id
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async deleteUser(id: number) {
|
||||
const response = await Client.auth.delete(`${API_BASE_URL}/${id}/`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new user
|
||||
* @param {Object} data
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
async createUser(data: Object) {
|
||||
const response = await Client.auth.post(`${API_BASE_URL}/`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Search users by username (partial match)
|
||||
* @param {Object} params - { username: string }
|
||||
* @returns {Promise<Array<User>>}
|
||||
*/
|
||||
async searchUsers(params: { username: string }) {
|
||||
// Adjust the endpoint as needed for your backend
|
||||
const response = await Client.auth.get(`${API_BASE_URL}/`, { params });
|
||||
console.log("User search response:", response.data);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export default userAPI;
|
||||
19
absolete_frontend/src/api/websockets/WebSocketClient.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
const wsUri = "ws://127.0.0.1/";
|
||||
|
||||
const websocket = new WebSocket(wsUri);
|
||||
|
||||
websocket.onopen = function (event) {
|
||||
console.log("WebSocket is open now.", event);
|
||||
};
|
||||
|
||||
websocket.onmessage = function (event) {
|
||||
console.log("WebSocket message received:", event.data);
|
||||
};
|
||||
|
||||
websocket.onclose = function (event) {
|
||||
console.log("WebSocket is closed now.", event.reason);
|
||||
};
|
||||
|
||||
websocket.onerror = function (event) {
|
||||
console.error("WebSocket error observed:", event);
|
||||
};
|
||||
BIN
absolete_frontend/src/assets/fonts/windows-98/ms_sans_serif.woff
Normal file
BIN
absolete_frontend/src/assets/img/cursor/Sata.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
absolete_frontend/src/assets/img/cursor/omagad.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
absolete_frontend/src/assets/img/cursor/pointing(inactive).png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
absolete_frontend/src/assets/img/cursor/pointing.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
absolete_frontend/src/assets/img/cursor/pointing2.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
absolete_frontend/src/assets/img/errors/403.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
absolete_frontend/src/assets/img/errors/404.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
absolete_frontend/src/assets/img/errors/500.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
absolete_frontend/src/assets/img/errors/error_icon.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
absolete_frontend/src/assets/img/job.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
24
absolete_frontend/src/assets/img/svg/default-background.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<svg viewBox="0 0 640 360" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- sky -->
|
||||
<rect x="0" y="0" width="640" height="360" fill="#A9A9A9"/>
|
||||
<!-- hill -->
|
||||
<path d="M 0 240 Q 320 60 640 240 L 640 360 L 0 360 Z" fill="#696969"/>
|
||||
<!-- trees -->
|
||||
<g fill="#696969" stroke="#696969">
|
||||
<circle cx="100" cy="192" r="26"/>
|
||||
<circle cx="112" cy="176" r="29"/>
|
||||
<circle cx="126" cy="208" r="22"/>
|
||||
<circle cx="496" cy="184" r="32"/>
|
||||
<circle cx="528" cy="168" r="26"/>
|
||||
<circle cx="560" cy="208" r="29"/>
|
||||
</g>
|
||||
<!-- clouds -->
|
||||
<g fill="#696969" stroke="#696969">
|
||||
<circle cx="90" cy="60" r="25"/>
|
||||
<circle cx="130" cy="80" r="30"/>
|
||||
<circle cx="170" cy="50" r="35"/>
|
||||
<circle cx="300" cy="40" r="28"/>
|
||||
<circle cx="340" cy="70" r="32"/>
|
||||
<circle cx="380" cy="60" r="25"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 823 B |
10
absolete_frontend/src/assets/img/svg/default-chat.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0"?>
|
||||
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<!-- Created with SVG-edit - https://github.com/SVG-Edit/svgedit-->
|
||||
|
||||
<g class="layer">
|
||||
<title>Layer 1</title>
|
||||
<rect fill="#e0e0e0" height="100" id="svg_3" stroke="#000000" stroke-width="0" width="100" x="0" y="0"/>
|
||||
<path d="m15,23.23l0,0c0,-4.55 3.6,-8.23 8.04,-8.23l3.65,0l0,0l17.54,0l32.88,0c2.13,0 4.18,0.87 5.68,2.41c1.51,1.54 2.35,3.64 2.35,5.82l0,20.57l0,0l0,12.34l0,0c0,4.55 -3.6,8.23 -8.04,8.23l-32.88,0l-22.91,20.93l5.37,-20.93l-3.65,0c-4.44,0 -8.04,-3.68 -8.04,-8.23l0,0l0,-12.34l0,0z" fill="#696969" id="svg_1" stroke="#000000" stroke-width="0"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 705 B |
10
absolete_frontend/src/assets/img/svg/default-pfp.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
|
||||
<!-- Background -->
|
||||
<rect width="100%" height="100%" fill="#e0e0e0"/>
|
||||
|
||||
<!-- Head -->
|
||||
<circle cx="100" cy="70" r="40" fill="#bdbdbd"/>
|
||||
|
||||
<!-- Shoulders with rounded top edges -->
|
||||
<rect x="40" y="100" width="120" height="80" rx="20" ry="20" fill="#9e9e9e"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 366 B |
17
absolete_frontend/src/assets/img/svg/menu-symbol.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="800px"
|
||||
height="800px"
|
||||
viewBox="0 0 24.75 24.75"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<path fill="#000000" d="M0,3.875c0-1.104,0.896-2,2-2h20.75c1.104,0,2,0.896,2,2s-0.896,2-2,2H2C0.896,5.875,0,4.979,0,3.875z M22.75,10.375H2
|
||||
c-1.104,0-2,0.896-2,2c0,1.104,0.896,2,2,2h20.75c1.104,0,2-0.896,2-2C24.75,11.271,23.855,10.375,22.75,10.375z M22.75,18.875H2
|
||||
c-1.104,0-2,0.896-2,2s0.896,2,2,2h20.75c1.104,0,2-0.896,2-2S23.855,18.875,22.75,18.875z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 722 B |
BIN
absolete_frontend/src/assets/img/test.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
11
absolete_frontend/src/assets/robots.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
Sitemap: https://vontor.com/sitemap.xml
|
||||
Disallow: /admin/
|
||||
Allow: /
|
||||
Allow: /social/public/
|
||||
Allow: /social/post/
|
||||
Allow: /social/community/
|
||||
Allow: /social/main/
|
||||
Allow: /social/profile/
|
||||
Allow: /social/login/
|
||||
Allow: /social/register/
|
||||
Crawl-delay: 10
|
||||
40
absolete_frontend/src/components/Footer/footer.module.css
Normal file
@@ -0,0 +1,40 @@
|
||||
footer a{
|
||||
color: var(--c-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
footer a i{
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
footer a:hover i{
|
||||
color: var(--c-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
footer{
|
||||
font-family: "Roboto Mono", monospace;
|
||||
|
||||
background-color: var(--c-boxes);
|
||||
|
||||
margin-top: 2em;
|
||||
display: flex;
|
||||
|
||||
color: white;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
footer address{
|
||||
padding: 1em;
|
||||
font-style: normal;
|
||||
}
|
||||
footer .contacts{
|
||||
font-size: 2em;
|
||||
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 990px){
|
||||
footer{
|
||||
flex-direction: column;
|
||||
padding-bottom: 1em;
|
||||
padding-top: 1em;
|
||||
}
|
||||
}
|
||||
76
absolete_frontend/src/components/Footer/footer.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import styles from "./footer.module.css"
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer id="contacts">
|
||||
<div>
|
||||
<h1>vontor.cz</h1>
|
||||
</div>
|
||||
|
||||
<address>
|
||||
Written by <b>David Bruno Vontor | © 2025</b>
|
||||
<br />
|
||||
<p>
|
||||
Tel.:{" "}
|
||||
<a href="tel:+420605512624">
|
||||
<u>+420 605 512 624</u>
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
E-mail:{" "}
|
||||
<a href="mailto:brunovontor@gmail.com">
|
||||
<u>brunovontor@gmail.com</u>
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
IČO:{" "}
|
||||
<a
|
||||
href="https://www.rzp.cz/verejne-udaje/cs/udaje/vyber-subjektu;ico=21613109;"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<u>21613109</u>
|
||||
</a>
|
||||
</p>
|
||||
</address>
|
||||
|
||||
<div className="contacts">
|
||||
<a
|
||||
href="https://github.com/Brunobrno"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i className="fa fa-github"></i>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.instagram.com/brunovontor/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i className="fa fa-instagram"></i>
|
||||
</a>
|
||||
<a
|
||||
href="https://twitter.com/BVontor"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i className="fa-brands fa-x-twitter"></i>
|
||||
</a>
|
||||
<a
|
||||
href="https://steamcommunity.com/id/Brunobrno/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i className="fa-brands fa-steam"></i>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.youtube.com/@brunovontor"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i className="fa-brands fa-youtube"></i>
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import React, { useState, useRef } from "react"
|
||||
import styles from "./contact-me.module.css"
|
||||
import { LuMousePointerClick } from "react-icons/lu";
|
||||
|
||||
export default function ContactMeForm() {
|
||||
const [opened, setOpened] = useState(false)
|
||||
const [contentMoveUp, setContentMoveUp] = useState(false)
|
||||
const [openingBehind, setOpeningBehind] = useState(false)
|
||||
const [success, setSuccess] = useState(false)
|
||||
const openingRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
function handleSubmit() {
|
||||
// form submission logic here
|
||||
}
|
||||
|
||||
const toggleOpen = () => {
|
||||
if (!opened) {
|
||||
setOpened(true)
|
||||
setOpeningBehind(false)
|
||||
setContentMoveUp(false)
|
||||
// Wait for the rotate-opening animation to finish before moving content up
|
||||
// The actual moveUp will be handled in onTransitionEnd
|
||||
} else {
|
||||
setContentMoveUp(false)
|
||||
setOpeningBehind(false)
|
||||
setTimeout(() => setOpened(false), 1000) // match transition duration
|
||||
}
|
||||
}
|
||||
|
||||
const handleTransitionEnd = (e: React.TransitionEvent<HTMLDivElement>) => {
|
||||
if (opened && e.propertyName === "transform") {
|
||||
setContentMoveUp(true)
|
||||
// Move the opening behind after the animation
|
||||
setTimeout(() => setOpeningBehind(true), 10)
|
||||
}
|
||||
if (!opened && e.propertyName === "transform") {
|
||||
setOpeningBehind(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles["contact-me"]}>
|
||||
<div
|
||||
ref={openingRef}
|
||||
className={
|
||||
[
|
||||
styles.opening,
|
||||
opened ? styles["rotate-opening"] : "",
|
||||
openingBehind ? styles["opening-behind"] : ""
|
||||
].filter(Boolean).join(" ")
|
||||
}
|
||||
onClick={toggleOpen}
|
||||
onTransitionEnd={handleTransitionEnd}
|
||||
>
|
||||
<LuMousePointerClick/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={
|
||||
contentMoveUp
|
||||
? `${styles.content} ${styles["content-moveup"]}`
|
||||
: styles.content
|
||||
}
|
||||
>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Váš email"
|
||||
required
|
||||
/>
|
||||
<textarea
|
||||
name="message"
|
||||
placeholder="Vaše zpráva"
|
||||
required
|
||||
/>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className={styles.cover}></div>
|
||||
<div className={styles.triangle}></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
.contact-me {
|
||||
margin: 5em auto;
|
||||
position: relative;
|
||||
|
||||
aspect-ratio: 16 / 9;
|
||||
|
||||
background-color: #c8c8c8;
|
||||
max-width: 100vw;
|
||||
}
|
||||
.contact-me + .mail-box{
|
||||
|
||||
}
|
||||
|
||||
.contact-me .opening {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
z-index: 2;
|
||||
transform-origin: top;
|
||||
|
||||
padding-top: 4em;
|
||||
|
||||
clip-path: polygon(0 0, 100% 0, 50% 50%);
|
||||
background-color: #d2d2d2;
|
||||
|
||||
transition: all 1s ease;
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
.rotate-opening{
|
||||
background-color: #c8c8c8;
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
|
||||
.opening svg{
|
||||
margin: auto;
|
||||
font-size: 3em;
|
||||
margin-top: -0.5em;
|
||||
}
|
||||
|
||||
|
||||
.contact-me .content {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
transition: all 1s ease-out;
|
||||
}
|
||||
.content-moveup{
|
||||
transform: translateY(-70%);
|
||||
}
|
||||
.content-moveup-index {
|
||||
z-index: 2 !important;
|
||||
}
|
||||
|
||||
.contact-me .content form{
|
||||
width: 80%;
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
background-color: #deefff;
|
||||
padding: 1em;
|
||||
border: 0.5em dashed #88d4ed;
|
||||
border-radius: 0.25em;
|
||||
}
|
||||
.contact-me .content form div{
|
||||
width: -webkit-fill-available;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.contact-me .content form input[type=submit]{
|
||||
margin: auto;
|
||||
border: none;
|
||||
background: #4ca4d5;
|
||||
color: #ffffff;
|
||||
padding: 1em 1.5em;
|
||||
cursor: pointer;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
.contact-me .content form input[type=text],
|
||||
.contact-me .content form input[type=email],
|
||||
.contact-me .content form textarea{
|
||||
background-color: #bfe8ff;
|
||||
border: none;
|
||||
border-bottom: 0.15em solid #064c7d;
|
||||
padding: 0.5em;
|
||||
|
||||
}
|
||||
|
||||
.opening-behind { z-index: 0 !important; }
|
||||
|
||||
.contact-me .cover {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
clip-path: polygon(0 0, 50% 50%, 100% 0, 100% 100%, 0 100%);
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.contact-me .triangle{
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 3;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
clip-path: polygon(100% 0, 0 100%, 100% 100%);
|
||||
background-color: rgb(255 255 255);
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0% { transform: translateX(0); }
|
||||
25% { transform: translateX(-2px) rotate(-8deg); }
|
||||
50% { transform: translateX(2px) rotate(4deg); }
|
||||
75% { transform: translateX(-1px) rotate(-2deg); }
|
||||
100% { transform: translateX(0); }
|
||||
}
|
||||
|
||||
|
||||
.contact-me .opening i {
|
||||
color: #797979;
|
||||
font-size: 5em;
|
||||
display: inline-block;
|
||||
animation: 0.4s ease-in-out 2s infinite normal none running shake;
|
||||
animation-delay: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media only screen and (max-width: 990px){
|
||||
.contact-me{
|
||||
aspect-ratio: unset;
|
||||
margin-top: 7ch;
|
||||
}
|
||||
}
|
||||
BIN
absolete_frontend/src/components/Forms/ContactMe/readme.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
90
absolete_frontend/src/components/ads/Drone/Drone.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React, { useEffect, useRef } from "react"
|
||||
import styles from "./drone.module.css"
|
||||
|
||||
export default function Drone() {
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null)
|
||||
const sourceRef = useRef<HTMLSourceElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
function setVideoDroneQuality() {
|
||||
if (!sourceRef.current || !videoRef.current) return
|
||||
|
||||
const videoSources = {
|
||||
fullHD: "static/home/video/drone-background-video-1080p.mp4", // For desktops (1920x1080)
|
||||
hd: "static/home/video/drone-background-video-720p.mp4", // For tablets/smaller screens (1280x720)
|
||||
lowRes: "static/home/video/drone-background-video-480p.mp4" // For mobile devices or low performance (854x480)
|
||||
}
|
||||
|
||||
const screenWidth = window.innerWidth
|
||||
|
||||
// Pick appropriate source
|
||||
if (screenWidth >= 1920) {
|
||||
sourceRef.current.src =
|
||||
"https://vontor-cz.s3.eu-central-1.amazonaws.com/" + videoSources.fullHD
|
||||
} else if (screenWidth >= 1280) {
|
||||
sourceRef.current.src =
|
||||
"https://vontor-cz.s3.eu-central-1.amazonaws.com/" + videoSources.hd
|
||||
} else {
|
||||
sourceRef.current.src =
|
||||
"https://vontor-cz.s3.eu-central-1.amazonaws.com/" + videoSources.lowRes
|
||||
}
|
||||
|
||||
// Reload video
|
||||
videoRef.current.load()
|
||||
console.log("Drone video set!")
|
||||
}
|
||||
|
||||
// Run once on mount
|
||||
setVideoDroneQuality()
|
||||
|
||||
// Optional: rerun on resize
|
||||
window.addEventListener("resize", setVideoDroneQuality)
|
||||
return () => {
|
||||
window.removeEventListener("resize", setVideoDroneQuality)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={`${styles.drone}`}>
|
||||
<video
|
||||
ref={videoRef}
|
||||
id="drone-video"
|
||||
className={styles.videoBackground}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
>
|
||||
<source ref={sourceRef} id="video-source" type="video/mp4" />
|
||||
Your browser does not support video.
|
||||
</video>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h1>Letecké záběry, co zaujmou</h1>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<h2>Oprávnění</h2>
|
||||
<p>Oprávnění A1/A2/A3 + radiostanice. Bezpečný provoz i v omezených zónách, povolení zajistím.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Cena</h2>
|
||||
<p>Paušál 3 000 Kč. Ostrava zdarma; mimo 10 Kč/km. Cena se může lišit dle povolení.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Výstup</h2>
|
||||
<p>Krátký sestřih nebo surové záběry — podle potřeby.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div>
|
||||
<a href="#contacts">Zájem?</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
103
absolete_frontend/src/components/ads/Drone/drone.module.css
Normal file
@@ -0,0 +1,103 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap');
|
||||
|
||||
|
||||
|
||||
|
||||
.drone{
|
||||
margin-top: -4em;
|
||||
font-style: normal;
|
||||
|
||||
width: 100%;
|
||||
position: relative;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
.drone .videoBackground {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
object-fit: cover;
|
||||
z-index: -1;
|
||||
|
||||
clip-path: polygon(0 3%, 15% 0, 30% 7%, 42% 3%, 61% 1%, 82% 5%, 100% 1%, 100% 94%, 82% 100%, 65% 96%, 47% 99%, 30% 90%, 14% 98%, 0 94%);
|
||||
}
|
||||
|
||||
|
||||
.drone article{
|
||||
padding: 5em;
|
||||
|
||||
display: flex;
|
||||
|
||||
border-radius: 2em;
|
||||
padding: 3em;
|
||||
gap: 2em;
|
||||
|
||||
position: relative;
|
||||
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
.drone article header h1{
|
||||
font-size: 4em;
|
||||
|
||||
font-weight: 300;
|
||||
}
|
||||
.drone article header{
|
||||
flex: 1;
|
||||
}
|
||||
.drone article main{
|
||||
width: 90%;
|
||||
display: flex;
|
||||
font-size: 1em;
|
||||
/* width: 60%; */
|
||||
flex: 2;
|
||||
flex-direction: row;
|
||||
|
||||
font-weight: 400;
|
||||
|
||||
gap: 2em;
|
||||
/* flex-wrap: wrap; */
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
.drone a{
|
||||
color: white;
|
||||
}
|
||||
.drone article div{
|
||||
display: flex;
|
||||
flex: 1;
|
||||
font-size: 1.25em;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width: 990px) {
|
||||
.drone article header h1{
|
||||
font-size: 2.3em;
|
||||
|
||||
font-weight: 200;
|
||||
}
|
||||
.drone article header{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.drone article main{
|
||||
flex-direction: column;
|
||||
font-size: 1em;
|
||||
}
|
||||
.drone article{
|
||||
height: auto;
|
||||
}
|
||||
.drone article div{
|
||||
margin: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
.drone video{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
BIN
absolete_frontend/src/components/ads/Drone/readme.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,168 @@
|
||||
.portfolio {
|
||||
margin: auto;
|
||||
margin-top: 10em;
|
||||
width: 80%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
color: white;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.portfolio div .door {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #c2a67d;
|
||||
color: #5e5747;
|
||||
|
||||
border-radius: 1em;
|
||||
|
||||
transform-origin: bottom;
|
||||
transition: transform 0.5s ease-in-out;
|
||||
|
||||
transform: skew(-5deg);
|
||||
z-index: 3;
|
||||
|
||||
box-shadow: #000000 5px 5px 15px;
|
||||
|
||||
}
|
||||
.portfolio div span svg{
|
||||
font-size: 5em;
|
||||
cursor: pointer;
|
||||
animation: shake 0.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@keyframes shake {
|
||||
0% { transform: translateX(0); }
|
||||
25% { transform: translateX(-2px) rotate(-8deg); }
|
||||
50% { transform: translateX(2px) rotate(4deg); }
|
||||
75% { transform: translateX(-1px) rotate(-2deg); }
|
||||
100% { transform: translateX(0); }
|
||||
}
|
||||
|
||||
.door i{
|
||||
color: #5e5747;
|
||||
font-size: 5em;
|
||||
display: inline-block;
|
||||
animation: shake 0.4s ease-in-out infinite;
|
||||
animation-delay: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.portfolio .door-open{
|
||||
transform: rotateX(90deg) skew(-2deg) !important;
|
||||
}
|
||||
|
||||
.portfolio>header {
|
||||
width: fit-content;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
top: -3.7em;
|
||||
left: 0;
|
||||
padding: 1em 3em;
|
||||
padding-bottom: 0;
|
||||
background-color: #cdc19c;
|
||||
color: #5e5747;
|
||||
border-top-left-radius: 1em;
|
||||
border-top-right-radius: 1em;
|
||||
}
|
||||
|
||||
.portfolio>header h1 {
|
||||
font-size: 2.5em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.portfolio>header i {
|
||||
font-size: 6em;
|
||||
}
|
||||
|
||||
.portfolio article{
|
||||
position: relative;
|
||||
}
|
||||
.portfolio article::after{
|
||||
clip-path: polygon(0% 0%, 11% 12.5%, 0% 25%, 11% 37.5%, 0% 50%, 11% 62.5%, 0% 75%, 11% 87.5%, 0% 100%, 100% 100%, 84% 87.5%, 98% 75%, 86% 62.5%, 100% 50%, 86% 37.5%, 100% 25%, 93% 12.5%, 100% 0%);
|
||||
content: "";
|
||||
bottom: 0;
|
||||
right: -2em;
|
||||
|
||||
height: 2em;
|
||||
width: 6em;
|
||||
transform: rotate(-45deg);
|
||||
|
||||
position: absolute;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.portfolio article::before{
|
||||
clip-path: polygon(0% 0%, 11% 12.5%, 0% 25%, 11% 37.5%, 0% 50%, 11% 62.5%, 0% 75%, 11% 87.5%, 0% 100%, 100% 100%, 84% 87.5%, 98% 75%, 86% 62.5%, 100% 50%, 86% 37.5%, 100% 25%, 93% 12.5%, 100% 0%);
|
||||
content: "";
|
||||
top: 0;
|
||||
left: -2em;
|
||||
|
||||
height: 2em;
|
||||
width: 6em;
|
||||
transform: rotate(-45deg);
|
||||
|
||||
position: absolute;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.portfolio article header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.portfolio div {
|
||||
width: 100%;
|
||||
padding: 3em;
|
||||
background-color: #cdc19c;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
gap: 5em;
|
||||
|
||||
border-radius: 1em;
|
||||
border-top-left-radius: 0;
|
||||
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.portfolio div article {
|
||||
display: flex;
|
||||
border-radius: 0em;
|
||||
background-color: #9c885c;
|
||||
width: 30%;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.portfolio div article header a img {
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@media only screen and (max-width: 990px) {
|
||||
.portfolio div{
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.portfolio div article{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
86
absolete_frontend/src/components/ads/Portfolio/Portfolio.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React, { useState } from "react"
|
||||
import styles from "./Portfolio.module.css"
|
||||
import { LuMousePointerClick } from "react-icons/lu";
|
||||
|
||||
interface PortfolioItem {
|
||||
href: string
|
||||
src: string
|
||||
alt: string
|
||||
// Optional per-item styling (prefer Tailwind utility classes in className/imgClassName)
|
||||
className?: string
|
||||
imgClassName?: string
|
||||
style?: React.CSSProperties
|
||||
imgStyle?: React.CSSProperties
|
||||
}
|
||||
|
||||
const portfolioItems: PortfolioItem[] = [
|
||||
{
|
||||
href: "https://davo1.cz",
|
||||
src: "/portfolio/davo1.png",
|
||||
alt: "davo1.cz logo",
|
||||
imgClassName: "bg-black rounded-lg p-4",
|
||||
//className: "bg-white/5 rounded-lg p-4",
|
||||
},
|
||||
{
|
||||
href: "https://perlica.cz",
|
||||
src: "/portfolio/perlica.png",
|
||||
alt: "Perlica logo",
|
||||
imgClassName: "rounded-lg",
|
||||
// imgClassName: "max-h-12",
|
||||
},
|
||||
{
|
||||
href: "http://epinger2.cz",
|
||||
src: "/portfolio/epinger.png",
|
||||
alt: "Epinger2 logo",
|
||||
imgClassName: "bg-white rounded-lg",
|
||||
// imgClassName: "max-h-12",
|
||||
},
|
||||
]
|
||||
|
||||
export default function Portfolio() {
|
||||
const [doorOpen, setDoorOpen] = useState(false)
|
||||
|
||||
const toggleDoor = () => setDoorOpen((prev) => !prev)
|
||||
|
||||
return (
|
||||
<div className={styles.portfolio} id="portfolio">
|
||||
<header>
|
||||
<h1>Portfolio</h1>
|
||||
</header>
|
||||
|
||||
<div>
|
||||
|
||||
<span
|
||||
className={
|
||||
doorOpen
|
||||
? `${styles.door} ${styles["door-open"]}`
|
||||
: styles.door
|
||||
}
|
||||
onClick={toggleDoor}
|
||||
>
|
||||
<LuMousePointerClick/>
|
||||
</span>
|
||||
|
||||
{portfolioItems.map((item, index) => (
|
||||
<article
|
||||
key={index}
|
||||
className={`${styles.article} ${item.className ?? ""}`}
|
||||
style={item.style}
|
||||
>
|
||||
<header>
|
||||
<a href={item.href} target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
src={item.src}
|
||||
alt={item.alt}
|
||||
className={item.imgClassName}
|
||||
style={item.imgStyle}
|
||||
/>
|
||||
</a>
|
||||
</header>
|
||||
<main></main>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
BIN
absolete_frontend/src/components/ads/Portfolio/readme.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
0
absolete_frontend/src/components/auth/LogOut.tsx
Normal file
0
absolete_frontend/src/components/auth/LoginForm.tsx
Normal file
304
absolete_frontend/src/components/navbar/HomeNav.module.css
Normal file
@@ -0,0 +1,304 @@
|
||||
nav{
|
||||
padding: 1.1em;
|
||||
|
||||
font-family: "Roboto Mono", monospace;
|
||||
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0; /* required */
|
||||
|
||||
transition: top 1s ease-in-out, border-radius 1s ease-in-out;
|
||||
|
||||
|
||||
|
||||
z-index: 5;
|
||||
padding-left: 2em;
|
||||
padding-right: 2em;
|
||||
width: max-content;
|
||||
|
||||
background: var(--c-boxes);
|
||||
/*background: -moz-linear-gradient(117deg, rgba(34,34,34,1) 0%, rgba(59,54,54,1) 100%);
|
||||
background: -webkit-linear-gradient(117deg, rgba(34,34,34,1) 0%, rgba(59,54,54,1) 100%);
|
||||
background: linear-gradient(117deg, rgba(34,34,34,1) 0%, rgba(59,54,54,1) 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#222222",endColorstr="#3b3636",GradientType=1);*/
|
||||
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
|
||||
margin: auto;
|
||||
|
||||
border-radius: 2em;
|
||||
}
|
||||
nav.isSticky-nav{
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
nav ul #nav-logo{
|
||||
border-right: 0.2em solid var(--c-lines);
|
||||
}
|
||||
/* Add class alias for logo used in TSX */
|
||||
.logo {
|
||||
border-right: 0.2em solid var(--c-lines);
|
||||
}
|
||||
nav ul #nav-logo span{
|
||||
line-height: 0.75;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
nav a{
|
||||
color: #fff;
|
||||
transition: color 1s;
|
||||
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
}
|
||||
nav a:hover{
|
||||
color: #fff;
|
||||
}
|
||||
/* Unify link/summary layout to prevent distortion */
|
||||
nav a,
|
||||
nav summary {
|
||||
color: #fff;
|
||||
transition: color 1s;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
display: inline-block; /* ensure consistent inline sizing */
|
||||
vertical-align: middle; /* align with neighbors */
|
||||
padding: 0; /* keep padding controlled by li */
|
||||
}
|
||||
|
||||
nav a::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: #fff;
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
nav a:hover::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
nav summary:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* underline effect shared for links and summary */
|
||||
nav a::before,
|
||||
nav summary::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: #fff;
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
nav a:hover::before,
|
||||
nav summary:hover::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
/* Submenu support */
|
||||
.hasSubmenu {
|
||||
position: relative;
|
||||
vertical-align: middle; /* align with other inline items */
|
||||
}
|
||||
|
||||
/* Keep details inline to avoid breaking the first row flow */
|
||||
.hasSubmenu details {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Ensure "Services" and caret stay on the same line */
|
||||
.hasSubmenu details > summary {
|
||||
display: inline-flex; /* horizontal layout */
|
||||
align-items: center; /* vertical alignment */
|
||||
gap: 0.5em; /* space between text and icon */
|
||||
white-space: nowrap; /* prevent wrapping */
|
||||
}
|
||||
|
||||
/* Hide native disclosure icon/marker on summary */
|
||||
.hasSubmenu details > summary {
|
||||
list-style: none;
|
||||
outline: none;
|
||||
}
|
||||
.hasSubmenu details > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
.hasSubmenu details > summary::marker {
|
||||
content: "";
|
||||
}
|
||||
|
||||
/* Reusable caret for submenu triggers */
|
||||
.caret {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
/* Rotate caret when submenu is open */
|
||||
.hasSubmenu details[open] .caret {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Submenu box: place directly under nav with a tiny gap (no overlap) */
|
||||
.submenu {
|
||||
list-style: none;
|
||||
margin: 1em 0;
|
||||
padding: 0.5em 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(100% + 0.25em);
|
||||
display: none;
|
||||
background: var(--c-background-light);
|
||||
border: 1px solid var(--c-lines);
|
||||
border-radius: 0.75em;
|
||||
min-width: max-content;
|
||||
text-align: left;
|
||||
z-index: 10;
|
||||
}
|
||||
.submenu li {
|
||||
display: block;
|
||||
padding: 0;
|
||||
}
|
||||
.submenu a {
|
||||
display: inline-block;
|
||||
padding: 0; /* remove padding so underline equals text width */
|
||||
margin: 0.35em 0; /* spacing without affecting underline width */
|
||||
}
|
||||
|
||||
/* Show submenu when open */
|
||||
.hasSubmenu details[open] .submenu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Hamburger toggle class (used by TSX) */
|
||||
.toggle {
|
||||
display: none;
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
.toggleRotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Bridge TSX classnames to existing rules */
|
||||
.navList {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.navList li {
|
||||
display: inline;
|
||||
padding: 0 3em;
|
||||
}
|
||||
.navList li a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
display: inline;
|
||||
padding: 0 3em;
|
||||
}
|
||||
|
||||
nav ul li a {
|
||||
text-decoration: none;
|
||||
}
|
||||
#toggle-nav{
|
||||
display: none;
|
||||
|
||||
-webkit-transition: transform 0.5s ease;
|
||||
-moz-transition: transform 0.5s ease;
|
||||
-o-transition: transform 0.5s ease;
|
||||
-ms-transition: transform 0.5s ease;
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
.toggle-nav-rotated {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
.nav-open{
|
||||
max-height: 20em;
|
||||
}
|
||||
@media only screen and (max-width: 990px){
|
||||
#toggle-nav{
|
||||
margin-top: 0.25em;
|
||||
margin-left: 0.75em;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
display: block;
|
||||
font-size: 2em;
|
||||
}
|
||||
nav{
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 1em;
|
||||
border-bottom-right-radius: 1em;
|
||||
overflow: hidden;
|
||||
}
|
||||
nav ul {
|
||||
margin-top: 1em;
|
||||
gap: 2em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
-webkit-transition: max-height 1s ease;
|
||||
-moz-transition: max-height 1s ease;
|
||||
-o-transition: max-height 1s ease;
|
||||
-ms-transition: max-height 1s ease;
|
||||
transition: max-height 1s ease;
|
||||
|
||||
max-height: 2em;
|
||||
}
|
||||
/* When TSX adds styles.open to the UL, expand it */
|
||||
.open {
|
||||
max-height: 20em;
|
||||
}
|
||||
|
||||
nav ul:last-child{
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
nav ul #nav-logo {
|
||||
margin: auto;
|
||||
padding-bottom: 0.5em;
|
||||
margin-bottom: -1em;
|
||||
border-bottom: 0.2em solid var(--c-lines);
|
||||
border-right: none;
|
||||
}
|
||||
/* Show hamburger on mobile */
|
||||
.toggle {
|
||||
margin-top: 0.25em;
|
||||
margin-left: 0.75em;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
display: block;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
/* Submenu stacks inline under the parent item on mobile */
|
||||
.submenu {
|
||||
position: static;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
padding: 0 0 0.5em 0.5em;
|
||||
min-width: unset;
|
||||
}
|
||||
.submenu a {
|
||||
display: inline-block;
|
||||
padding: 0; /* keep no padding on mobile too */
|
||||
margin: 0.25em 0.5em; /* spacing via margin */
|
||||
}
|
||||
}
|
||||
52
absolete_frontend/src/components/navbar/HomeNav.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { useState, useContext } from "react"
|
||||
import styles from "./HomeNav.module.css"
|
||||
import { FaBars, FaChevronDown } from "react-icons/fa";
|
||||
|
||||
import { UserContext } from "../../context/UserContext";
|
||||
|
||||
export default function HomeNav() {
|
||||
const [navOpen, setNavOpen] = useState(false)
|
||||
|
||||
const toggleNav = () => setNavOpen((prev) => !prev)
|
||||
|
||||
const { user } = useContext(UserContext);
|
||||
|
||||
return (
|
||||
<nav className={styles.nav}>
|
||||
<FaBars
|
||||
className={`${styles.toggle} ${navOpen ? styles.toggleRotated : ""}`}
|
||||
onClick={toggleNav}
|
||||
aria-label="Toggle navigation"
|
||||
aria-expanded={navOpen}
|
||||
/>
|
||||
|
||||
<ul className={`${styles.navList} ${navOpen ? styles.open : ""}`}>
|
||||
<li id="nav-logo" className={styles.logo}>
|
||||
<span>vontor.cz</span>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/">Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#portfolio">Portfolio</a>
|
||||
</li>
|
||||
<li className={styles.hasSubmenu}>
|
||||
<details>
|
||||
<summary>
|
||||
Services
|
||||
<FaChevronDown className={`${styles.caret} ml-2 inline-block`} aria-hidden="true" />
|
||||
</summary>
|
||||
<ul className={styles.submenu}>
|
||||
<li><a href="#web">Web development</a></li>
|
||||
<li><a href="#integration">Integrations</a></li>
|
||||
<li><a href="#support">Support</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#contactme-form">Contact me</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
30
absolete_frontend/src/context/Context.md
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
# EXAMPLE USAGE OF CONTEXT IN A COMPONENT:
|
||||
|
||||
## Wrap your app tree with the provider (e.g., in App.tsx)
|
||||
|
||||
```tsx
|
||||
import { UserContextProvider } from "../context/UserContext";
|
||||
function App() {
|
||||
return (
|
||||
<UserContextProvider>
|
||||
<YourRoutes />
|
||||
</UserContextProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Consume in any child component
|
||||
```tsx
|
||||
import React, { useContext } from "react"
|
||||
import { UserContext } from '../context/UserContext';
|
||||
|
||||
export default function ExampleComponent() {
|
||||
const { user, setUser } = useContext(UserContext);
|
||||
|
||||
|
||||
return ...;
|
||||
}
|
||||
```
|
||||
0
absolete_frontend/src/context/SettingsContext.tsx
Normal file
75
absolete_frontend/src/context/UserContext.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React, { createContext, useState, useContext, useEffect } from 'react';
|
||||
|
||||
import userAPI from '../api/models/User';
|
||||
|
||||
// definice uživatele
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
// určíme typ kontextu
|
||||
interface GlobalContextType {
|
||||
user: User | null;
|
||||
setUser: React.Dispatch<React.SetStateAction<User | null>>;
|
||||
}
|
||||
|
||||
// vytvoříme a exportneme kontext !!!
|
||||
export const UserContext = createContext<GlobalContextType | null>(null);
|
||||
|
||||
|
||||
// hook pro použití kontextu
|
||||
// zabal routy do téhle komponenty!!!
|
||||
export const UserContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
const currentUser = await userAPI.getCurrentUser();
|
||||
setUser(currentUser);
|
||||
} catch (error) {
|
||||
console.error('Failed to load user:', error);
|
||||
setUser(null);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUser();
|
||||
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={{ user, setUser }}>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
EXAMPLE USAGE OF CONTEXT IN A COMPONENT:
|
||||
|
||||
// Wrap your app tree with the provider (e.g., in App.tsx)
|
||||
// import { UserContextProvider } from "../context/UserContext";
|
||||
// function App() {
|
||||
// return (
|
||||
// <UserContextProvider>
|
||||
// <YourRoutes />
|
||||
// </UserContextProvider>
|
||||
// );
|
||||
// }
|
||||
|
||||
// Consume in any child component
|
||||
import React, { useContext } from "react"
|
||||
import { UserContext } from '../context/UserContext';
|
||||
|
||||
export default function ExampleComponent() {
|
||||
const { user, setUser } = useContext(UserContext);
|
||||
|
||||
|
||||
return ...;
|
||||
}
|
||||
|
||||
*/
|
||||
67
absolete_frontend/src/index.css
Normal file
@@ -0,0 +1,67 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
--c-background: #031D44; /*background*/
|
||||
--c-background-light: #04395E; /*background-highlight*/
|
||||
--c-boxes: #24719f;; /*boxes*/
|
||||
--c-lines: #87a9da; /*lines*/
|
||||
--c-text: #CAF0F8; /*text*/
|
||||
--c-other: #70A288; /*other*/
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
0
absolete_frontend/src/layouts/AuthLayout.tsx
Normal file
28
absolete_frontend/src/layouts/Default.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import Footer from "../components/Footer/footer";
|
||||
import ContactMeForm from "../components/Forms/ContactMe/ContactMeForm";
|
||||
import HomeNav from "../components/navbar/HomeNav";
|
||||
import Drone from "../components/ads/Drone/Drone";
|
||||
import Portfolio from "../components/ads/Portfolio/Portfolio";
|
||||
import Home from "../pages/home/home";
|
||||
import { Outlet } from "react-router";
|
||||
|
||||
export default function HomeLayout(){
|
||||
return(
|
||||
<>
|
||||
{/* Example usage of imported components, adjust as needed */}
|
||||
|
||||
<HomeNav />
|
||||
|
||||
<Home /> {/*page*/}
|
||||
<div style={{margin: "10em 0"}}>
|
||||
<Drone />
|
||||
</div>
|
||||
<Outlet />
|
||||
<Portfolio />
|
||||
<div style={{ margin: "6em auto", marginTop: "15em", maxWidth: "80vw" }}>
|
||||
<ContactMeForm />
|
||||
</div>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
28
absolete_frontend/src/layouts/HomeLayout.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import Footer from "../components/Footer/footer";
|
||||
import ContactMeForm from "../components/Forms/ContactMe/ContactMeForm";
|
||||
import HomeNav from "../components/navbar/HomeNav";
|
||||
import Drone from "../components/ads/Drone/Drone";
|
||||
import Portfolio from "../components/ads/Portfolio/Portfolio";
|
||||
import Home from "../pages/home/home";
|
||||
import { Outlet } from "react-router";
|
||||
|
||||
export default function HomeLayout(){
|
||||
return(
|
||||
<>
|
||||
{/* Example usage of imported components, adjust as needed */}
|
||||
|
||||
<HomeNav />
|
||||
|
||||
<Home /> {/*page*/}
|
||||
<div style={{margin: "10em 0"}}>
|
||||
<Drone />
|
||||
</div>
|
||||
<Outlet />
|
||||
<Portfolio />
|
||||
<div style={{ margin: "6em auto", marginTop: "15em", maxWidth: "80vw" }}>
|
||||
<ContactMeForm />
|
||||
</div>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
69
absolete_frontend/src/layouts/LAYOUTS.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Layouts in React Router
|
||||
|
||||
## 📌 What is a Layout?
|
||||
A **layout** in React Router is just a **React component** that wraps multiple pages with shared structure or styling (e.g., header, footer, sidebar).
|
||||
|
||||
Layouts usually contain:
|
||||
- Global UI elements (navigation, footer, etc.)
|
||||
- An `<Outlet />` component where nested routes will render their content
|
||||
|
||||
---
|
||||
|
||||
## 📂 Folder Structure Example
|
||||
|
||||
src/
|
||||
layouts/
|
||||
├── MainLayout.jsx
|
||||
└── AdminLayout.jsx
|
||||
pages/
|
||||
├── HomePage.jsx
|
||||
├── AboutPage.jsx
|
||||
└── DashboardPage.jsx
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🛠 How Layouts Are Used in Routes
|
||||
|
||||
### 1. Layout as a Parent Route
|
||||
Use the layout component as the `element` of a **parent route** and place **pages** inside as nested routes.
|
||||
|
||||
```jsx
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import MainLayout from "./layouts/MainLayout";
|
||||
import HomePage from "./pages/HomePage";
|
||||
import AboutPage from "./pages/AboutPage";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<MainLayout />}>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/about" element={<AboutPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
### 2. Inside the MainLayout.jsx
|
||||
|
||||
```jsx
|
||||
import { Outlet } from "react-router-dom";
|
||||
|
||||
export default function MainLayout() {
|
||||
return (
|
||||
<>
|
||||
<header>Header</header>
|
||||
<main>
|
||||
<Outlet />
|
||||
</main>
|
||||
<footer>Footer</footer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
10
absolete_frontend/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
206
absolete_frontend/src/pages/downloader/Downloader.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
fetchInfo,
|
||||
downloadImmediate,
|
||||
FORMAT_EXTS,
|
||||
type InfoResponse,
|
||||
parseContentDispositionFilename,
|
||||
} from "../../api/apps/Downloader";
|
||||
|
||||
export default function Downloader() {
|
||||
const [url, setUrl] = useState("");
|
||||
const [probing, setProbing] = useState(false);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [info, setInfo] = useState<InfoResponse | null>(null);
|
||||
|
||||
const [ext, setExt] = useState<typeof FORMAT_EXTS[number]>("mp4");
|
||||
const [videoRes, setVideoRes] = useState<string | undefined>(undefined);
|
||||
const [audioRes, setAudioRes] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (info?.video_resolutions?.length && !videoRes) {
|
||||
setVideoRes(info.video_resolutions[0]);
|
||||
}
|
||||
if (info?.audio_resolutions?.length && !audioRes) {
|
||||
setAudioRes(info.audio_resolutions[0]);
|
||||
}
|
||||
}, [info]);
|
||||
|
||||
async function onProbe(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setInfo(null);
|
||||
setProbing(true);
|
||||
try {
|
||||
const res = await fetchInfo(url);
|
||||
setInfo(res);
|
||||
// reset selections from fresh info
|
||||
setVideoRes(res.video_resolutions?.[0]);
|
||||
setAudioRes(res.audio_resolutions?.[0]);
|
||||
} catch (e: any) {
|
||||
setError(
|
||||
e?.response?.data?.error ||
|
||||
e?.response?.data?.detail ||
|
||||
e?.message ||
|
||||
"Failed to get info."
|
||||
);
|
||||
} finally {
|
||||
setProbing(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onDownload() {
|
||||
setError(null);
|
||||
setDownloading(true);
|
||||
try {
|
||||
const { blob, filename } = await downloadImmediate({
|
||||
url,
|
||||
ext,
|
||||
videoResolution: videoRes,
|
||||
audioResolution: audioRes,
|
||||
});
|
||||
const name = filename || parseContentDispositionFilename("") || `download.${ext}`;
|
||||
const href = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = href;
|
||||
a.download = name;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(href);
|
||||
} catch (e: any) {
|
||||
setError(
|
||||
e?.response?.data?.error ||
|
||||
e?.response?.data?.detail ||
|
||||
e?.message ||
|
||||
"Download failed."
|
||||
);
|
||||
} finally {
|
||||
setDownloading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const canDownload = useMemo(
|
||||
() => !!url && !!ext && !!videoRes && !!audioRes,
|
||||
[url, ext, videoRes, audioRes]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto p-4 space-y-4">
|
||||
<h1 className="text-2xl font-semibold">Downloader</h1>
|
||||
|
||||
{error && <div className="rounded border border-red-300 bg-red-50 text-red-700 p-2">{error}</div>}
|
||||
|
||||
<form onSubmit={onProbe} className="grid gap-3">
|
||||
<label className="grid gap-1">
|
||||
<span className="text-sm font-medium">URL</span>
|
||||
<input
|
||||
type="url"
|
||||
required
|
||||
placeholder="https://example.com/video"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
className="w-full border rounded p-2"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!url || probing}
|
||||
className="px-3 py-2 rounded bg-blue-600 text-white disabled:opacity-50"
|
||||
>
|
||||
{probing ? "Probing..." : "Get info"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDownload}
|
||||
disabled={!canDownload || downloading}
|
||||
className="px-3 py-2 rounded bg-emerald-600 text-white disabled:opacity-50"
|
||||
>
|
||||
{downloading ? "Downloading..." : "Download"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{info && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-3">
|
||||
{info.thumbnail && (
|
||||
<img
|
||||
src={info.thumbnail}
|
||||
alt={info.title || "thumbnail"}
|
||||
className="w-40 h-24 object-cover rounded border"
|
||||
/>
|
||||
)}
|
||||
<div className="text-sm text-gray-800 space-y-1">
|
||||
<div>
|
||||
<span className="font-medium">Title:</span> {info.title || "-"}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Duration:</span>{" "}
|
||||
{info.duration ? `${Math.round(info.duration)} s` : "-"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-3">
|
||||
<label className="grid gap-1">
|
||||
<span className="text-sm font-medium">Container</span>
|
||||
<select
|
||||
value={ext}
|
||||
onChange={(e) => setExt(e.target.value as any)}
|
||||
className="border rounded p-2"
|
||||
>
|
||||
{FORMAT_EXTS.map((x) => (
|
||||
<option key={x} value={x}>
|
||||
{x.toUpperCase()}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-1">
|
||||
<span className="text-sm font-medium">Video resolution</span>
|
||||
<select
|
||||
value={videoRes || ""}
|
||||
onChange={(e) => setVideoRes(e.target.value || undefined)}
|
||||
className="border rounded p-2"
|
||||
>
|
||||
{info.video_resolutions?.length ? (
|
||||
info.video_resolutions.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{r}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option value="">-</option>
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-1">
|
||||
<span className="text-sm font-medium">Audio bitrate</span>
|
||||
<select
|
||||
value={audioRes || ""}
|
||||
onChange={(e) => setAudioRes(e.target.value || undefined)}
|
||||
className="border rounded p-2"
|
||||
>
|
||||
{info.audio_resolutions?.length ? (
|
||||
info.audio_resolutions.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{r}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option value="">-</option>
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
240
absolete_frontend/src/pages/home/Home.module.css
Normal file
@@ -0,0 +1,240 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Doto:wght@100..900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Doto:wght@300&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap');
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Exo:ital,wght@0,100..900;1,100..900&display=swap');
|
||||
|
||||
:root{
|
||||
--c-background: #031D44; /*background*/
|
||||
--c-background-light: #04395E; /*background-highlight*/
|
||||
--c-boxes: #24719f;; /*boxes*/
|
||||
--c-lines: #87a9da; /*lines*/
|
||||
--c-text: #CAF0F8; /*text*/
|
||||
--c-other: #70A288; /*other*/
|
||||
}
|
||||
|
||||
html{
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body{
|
||||
font-family: "Exo", serif;
|
||||
|
||||
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.doto-font{
|
||||
font-family: "Doto", serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-variation-settings: "ROND" 0;
|
||||
}
|
||||
.bebas-neue-regular {
|
||||
font-family: "Bebas Neue", sans-serif;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
|
||||
.introduction {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--c-text);
|
||||
|
||||
padding-bottom: 10em;
|
||||
margin-top: 6em;
|
||||
|
||||
width: 100%;
|
||||
position: relative;
|
||||
top:0;
|
||||
|
||||
/* gap: 4em;*/
|
||||
}
|
||||
|
||||
.introduction h1{
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.introduction article {
|
||||
/*background-color: cadetblue;*/
|
||||
|
||||
padding: 2em;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
.introduction article header {}
|
||||
|
||||
.introduction article:nth-child(1) {
|
||||
width: 100%;
|
||||
/* transform: rotate(5deg); */
|
||||
align-self: center;
|
||||
text-align: center;
|
||||
font-size: 2em;
|
||||
}
|
||||
/*
|
||||
.introduction article:nth-child(2) {
|
||||
width: 50%;
|
||||
transform: rotate(3deg);
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.introduction article:nth-child(3) {
|
||||
width: 50%;
|
||||
transform: rotate(-2deg);
|
||||
align-self: flex-start;
|
||||
}*/
|
||||
|
||||
|
||||
|
||||
|
||||
.animation-introduction{
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
.animation-introduction ul{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/*overflow: hidden; ZAPNOUT KDYŽ NECHCEŠ ANIMACI PŘECHÁZET DO OSTATNÍCH DIVŮ*/
|
||||
}
|
||||
|
||||
.animation-introduction ul li{
|
||||
position: absolute;
|
||||
display: block;
|
||||
list-style: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: rgba(255, 255, 255, 35%);
|
||||
animation: animate 4s linear infinite;
|
||||
bottom: -150px;
|
||||
|
||||
}
|
||||
|
||||
.animation-introduction ul li:nth-child(1){
|
||||
left: 25%;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
|
||||
.animation-introduction ul li:nth-child(2){
|
||||
left: 10%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation-delay: 2s;
|
||||
animation-duration: 12s;
|
||||
}
|
||||
|
||||
.animation-introduction ul li:nth-child(3){
|
||||
left: 70%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
.animation-introduction ul li:nth-child(4){
|
||||
left: 40%;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
animation-delay: 0s;
|
||||
animation-duration: 18s;
|
||||
}
|
||||
|
||||
.animation-introduction ul li:nth-child(5){
|
||||
left: 65%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.animation-introduction ul li:nth-child(6){
|
||||
left: 75%;
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
animation-delay: 3s;
|
||||
}
|
||||
|
||||
.animation-introduction ul li:nth-child(7){
|
||||
left: 35%;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
animation-delay: 7s;
|
||||
}
|
||||
|
||||
.animation-introduction ul li:nth-child(8){
|
||||
left: 50%;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
animation-delay: 15s;
|
||||
animation-duration: 45s;
|
||||
}
|
||||
|
||||
.animation-introduction ul li:nth-child(9){
|
||||
left: 20%;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
animation-delay: 2s;
|
||||
animation-duration: 35s;
|
||||
}
|
||||
|
||||
.animation-introduction ul li:nth-child(10){
|
||||
left: 85%;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
animation-delay: 0s;
|
||||
animation-duration: 11s;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@keyframes animate {
|
||||
|
||||
0%{
|
||||
transform: translateY(0) rotate(0deg);
|
||||
opacity: 1;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
100%{
|
||||
transform: translateY(-1000px) rotate(720deg);
|
||||
opacity: 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width: 990px) {
|
||||
.animation-introduction ul li:nth-child(6){
|
||||
left: 67%;
|
||||
}
|
||||
.animation-introduction ul li:nth-child(10) {
|
||||
left: 60%;
|
||||
}
|
||||
.introduction {
|
||||
margin: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.introduction article {
|
||||
width: auto !important;
|
||||
transform: none !important;
|
||||
align-self: none !important;
|
||||
}
|
||||
}
|
||||
49
absolete_frontend/src/pages/home/home.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, { useEffect } from "react"
|
||||
import styles from "./Home.module.css"
|
||||
|
||||
|
||||
|
||||
export default function Home() {
|
||||
useEffect(() => {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
const randomId = "spark-" + Math.floor(Math.random() * 100000)
|
||||
|
||||
const spark = document.createElement("div")
|
||||
spark.className = "spark-cursor"
|
||||
spark.id = randomId
|
||||
document.body.appendChild(spark)
|
||||
|
||||
// pozice a barva
|
||||
spark.style.top = `${event.pageY}px`
|
||||
spark.style.left = `${event.pageX}px`
|
||||
spark.style.filter = `hue-rotate(${Math.random() * 360}deg)`
|
||||
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const span = document.createElement("span")
|
||||
span.style.transform = `rotate(${i * 45}deg)`
|
||||
spark.appendChild(span)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
spark.querySelectorAll("span").forEach((s) => {
|
||||
(s as HTMLElement).classList.add("animate")
|
||||
})
|
||||
}, 10)
|
||||
|
||||
setTimeout(() => {
|
||||
spark.remove()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
document.body.addEventListener("click", handleClick)
|
||||
|
||||
// cleanup když komponenta zmizí
|
||||
return () => {
|
||||
document.body.removeEventListener("click", handleClick)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
192
absolete_frontend/src/pages/home/introduction.css
Normal file
@@ -0,0 +1,192 @@
|
||||
.introduction {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--c-text);
|
||||
|
||||
padding-bottom: 10em;
|
||||
margin-top: 6em;
|
||||
|
||||
width: 100%;
|
||||
position: relative;
|
||||
top:0;
|
||||
|
||||
/* gap: 4em;*/
|
||||
}
|
||||
.introduction h1{
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.introduction article {
|
||||
/*background-color: cadetblue;*/
|
||||
|
||||
padding: 2em;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
.introduction article header {}
|
||||
|
||||
.introduction article:nth-child(1) {
|
||||
width: 100%;
|
||||
/* transform: rotate(5deg); */
|
||||
align-self: center;
|
||||
text-align: center;
|
||||
font-size: 2em;
|
||||
}
|
||||
/*
|
||||
.introduction article:nth-child(2) {
|
||||
width: 50%;
|
||||
transform: rotate(3deg);
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.introduction article:nth-child(3) {
|
||||
width: 50%;
|
||||
transform: rotate(-2deg);
|
||||
align-self: flex-start;
|
||||
}*/
|
||||
|
||||
|
||||
|
||||
|
||||
.animation-introduction{
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
.animation-introduction ul{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/*overflow: hidden; ZAPNOUT KDYŽ NECHCEŠ ANIMACI PŘECHÁZET DO OSTATNÍCH DIVŮ*/
|
||||
}
|
||||
|
||||
.animation-introduction ul li{
|
||||
position: absolute;
|
||||
display: block;
|
||||
list-style: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: rgba(255, 255, 255, 35%);
|
||||
animation: animate 4s linear infinite;
|
||||
bottom: -150px;
|
||||
|
||||
}
|
||||
|
||||
.animation-introduction ul li:nth-child(1){
|
||||
left: 25%;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
|
||||
.animation-introduction ul li:nth-child(2){
|
||||
left: 10%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation-delay: 2s;
|
||||
animation-duration: 12s;
|
||||
}
|
||||
|
||||
.animation-introduction ul li:nth-child(3){
|
||||
left: 70%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
.animation-introduction ul li:nth-child(4){
|
||||
left: 40%;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
animation-delay: 0s;
|
||||
animation-duration: 18s;
|
||||
}
|
||||
|
||||
.animation-introduction ul li:nth-child(5){
|
||||
left: 65%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.animation-introduction ul li:nth-child(6){
|
||||
left: 75%;
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
animation-delay: 3s;
|
||||
}
|
||||
|
||||
.animation-introduction ul li:nth-child(7){
|
||||
left: 35%;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
animation-delay: 7s;
|
||||
}
|
||||
|
||||
.animation-introduction ul li:nth-child(8){
|
||||
left: 50%;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
animation-delay: 15s;
|
||||
animation-duration: 45s;
|
||||
}
|
||||
|
||||
.animation-introduction ul li:nth-child(9){
|
||||
left: 20%;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
animation-delay: 2s;
|
||||
animation-duration: 35s;
|
||||
}
|
||||
|
||||
.animation-introduction ul li:nth-child(10){
|
||||
left: 85%;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
animation-delay: 0s;
|
||||
animation-duration: 11s;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@keyframes animate {
|
||||
|
||||
0%{
|
||||
transform: translateY(0) rotate(0deg);
|
||||
opacity: 1;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
100%{
|
||||
transform: translateY(-1000px) rotate(720deg);
|
||||
opacity: 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width: 990px) {
|
||||
.animation-introduction ul li:nth-child(6){
|
||||
left: 67%;
|
||||
}
|
||||
.animation-introduction ul li:nth-child(10) {
|
||||
left: 60%;
|
||||
}
|
||||
.introduction {
|
||||
margin: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.introduction article {
|
||||
width: auto !important;
|
||||
transform: none !important;
|
||||
align-self: none !important;
|
||||
}
|
||||
}
|
||||
2
absolete_frontend/src/pages/home/jquery-3.7.1.js
vendored
Normal file
17
absolete_frontend/src/pages/home/nav.js
Normal file
@@ -0,0 +1,17 @@
|
||||
$(document).ready(function() {
|
||||
const $stickyElm = $('nav');
|
||||
const stickyOffset = $stickyElm.offset().top;
|
||||
|
||||
$(window).on('scroll', function() {
|
||||
|
||||
const isSticky = $(window).scrollTop() > stickyOffset;
|
||||
//console.log("sticky: " + isSticky);
|
||||
|
||||
$stickyElm.toggleClass('isSticky-nav', isSticky);
|
||||
});
|
||||
|
||||
$('#toggle-nav').click(function () {
|
||||
$('nav ul').toggleClass('nav-open');
|
||||
$('#toggle-nav').toggleClass('toggle-nav-rotated');
|
||||
});
|
||||
});
|
||||
22
absolete_frontend/src/routes/PrivateRoute.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Navigate, Outlet, useLocation } from "react-router-dom";
|
||||
|
||||
function getCookie(name: string): string | null {
|
||||
const nameEQ = name + "=";
|
||||
const ca = document.cookie.split(";").map((c) => c.trim());
|
||||
for (const c of ca) {
|
||||
if (c.indexOf(nameEQ) === 0) return decodeURIComponent(c.substring(nameEQ.length));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const ACCESS_COOKIE = "access_token";
|
||||
|
||||
export default function PrivateRoute() {
|
||||
const location = useLocation();
|
||||
const isLoggedIn = !!getCookie(ACCESS_COOKIE);
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return <Navigate to="/login" replace state={{ from: location }} />;
|
||||
}
|
||||
return <Outlet />;
|
||||
}
|
||||
71
absolete_frontend/src/routes/ROUTES.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Routes Folder
|
||||
|
||||
This folder contains the route definitions and components used to manage routing in the React application. It includes public and private routes, as well as nested layouts.
|
||||
|
||||
## File Structure
|
||||
routes/
|
||||
├── PrivateRoute.jsx
|
||||
├── AppRoutes.jsx
|
||||
└── index.js
|
||||
|
||||
|
||||
### `PrivateRoute.jsx`
|
||||
|
||||
`PrivateRoute` is a wrapper component that restricts access to certain routes based on the user's authentication status. Only logged-in users can access routes wrapped inside `PrivateRoute`.
|
||||
|
||||
#### Example Usage
|
||||
|
||||
```jsx
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
import { useAuth } from "../auth"; // custom hook to get auth status
|
||||
|
||||
const PrivateRoute = () => {
|
||||
const { isLoggedIn } = useAuth();
|
||||
|
||||
return isLoggedIn ? <Outlet /> : <Navigate to="/login" />;
|
||||
};
|
||||
|
||||
export default PrivateRoute;
|
||||
```
|
||||
|
||||
` <Outlet /> ` allows nested routes to be rendered inside the PrivateRoute.
|
||||
|
||||
### AppRoutes.jsx
|
||||
|
||||
This file contains all the route definitions example of the app. It can use layouts from the layouts folder to wrap sections of the app.
|
||||
|
||||
Example Usage
|
||||
```jsx
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import PrivateRoute from "./PrivateRoute";
|
||||
|
||||
// Layouts
|
||||
import MainLayout from "../layouts/MainLayout";
|
||||
import AuthLayout from "../layouts/AuthLayout";
|
||||
|
||||
// Pages
|
||||
import Dashboard from "../pages/Dashboard";
|
||||
import Profile from "../pages/Profile";
|
||||
import Login from "../pages/Login";
|
||||
|
||||
const AppRoutes = () => {
|
||||
return (
|
||||
<Routes>
|
||||
{/* Public Routes */}
|
||||
<Route element={<AuthLayout />}>
|
||||
<Route path="/login" element={<Login />} />
|
||||
</Route>
|
||||
|
||||
{/* Private Routes */}
|
||||
<Route element={<PrivateRoute />}>
|
||||
<Route element={<MainLayout />}>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppRoutes;
|
||||
```
|
||||
1
absolete_frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
27
absolete_frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
absolete_frontend/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
25
absolete_frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
11
absolete_frontend/vite.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss()
|
||||
],
|
||||
})
|
||||