This commit is contained in:
2025-11-06 01:40:00 +01:00
parent de5f54f4bc
commit 602c5a40f1
108 changed files with 9859 additions and 1382 deletions

View File

@@ -1,42 +0,0 @@
#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;
}

View File

@@ -1,33 +0,0 @@
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>
)
}

View File

@@ -1,275 +0,0 @@
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"
*/

View File

@@ -1,114 +0,0 @@
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;
}

View File

@@ -1,41 +0,0 @@
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;
}
}

View File

@@ -1,82 +0,0 @@
// 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;

View File

@@ -1,19 +0,0 @@
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);
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -1,24 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 823 B

View File

@@ -1,10 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 705 B

View File

@@ -1,10 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 366 B

View File

@@ -1,17 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 722 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -1,11 +0,0 @@
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

View File

@@ -0,0 +1,149 @@
import React, { useState } from 'react';
import { Heart, ShoppingCart, Star } from 'lucide-react';
const DonateShop: React.FC = () => {
const [donatedItems, setDonatedItems] = useState<Set<number>>(new Set());
const products = [
{
id: 1,
name: 'Coffee Support',
price: 5,
originalPrice: 10,
image: 'https://images.unsplash.com/photo-1509042239860-f550ce710b93?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&q=80',
description: 'Fuel my coding sessions with a virtual coffee',
rating: 4.8,
reviews: 124
},
{
id: 2,
name: 'Meal Contribution',
price: 15,
originalPrice: 25,
image: 'https://images.unsplash.com/photo-1565299624946-b28f40a0ca4b?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&q=80',
description: 'Help me focus on creating amazing projects',
rating: 4.9,
reviews: 89
},
{
id: 3,
name: 'Project Boost',
price: 25,
originalPrice: 40,
image: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&q=80',
description: 'Support the development of new features',
rating: 5.0,
reviews: 67
},
{
id: 4,
name: 'Monthly Patron',
price: 50,
originalPrice: 75,
image: 'https://images.unsplash.com/photo-1552664730-d307ca884978?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&q=80',
description: 'Ongoing support for continuous improvement',
rating: 4.7,
reviews: 45
}
];
const handleDonate = (productId: number) => {
setDonatedItems(prev => new Set(prev).add(productId));
// Here you would integrate with a payment processor like Stripe
alert(`Thank you for your donation of $${products.find(p => p.id === productId)?.price}!`);
};
return (
<section className="py-16 bg-background">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-text sm:text-4xl">
Support My Creative Journey
</h2>
<p className="mt-4 text-lg text-lines max-w-2xl mx-auto">
Instead of buying products, consider donating to support my creative journey
</p>
</div>
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
{products.map((product) => (
<div
key={product.id}
className="bg-background-light rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-2 group border border-lines overflow-hidden"
>
<div className="relative overflow-hidden">
<img
className="w-full h-48 object-cover group-hover:scale-110 transition-transform duration-300"
src={product.image}
alt={product.name}
loading="lazy"
/>
<div className="absolute top-4 left-4">
<div className="bg-other text-background px-2 py-1 rounded-full text-xs font-medium">
Donation
</div>
</div>
<div className="absolute top-4 right-4 flex items-center space-x-1">
<Star className="h-4 w-4 text-other fill-current" />
<span className="text-text text-sm font-medium bg-background/70 px-1 rounded">
{product.rating}
</span>
</div>
</div>
<div className="p-6">
<h3 className="text-xl font-semibold text-text mb-2">
{product.name}
</h3>
<p className="text-lines mb-4 text-sm">
{product.description}
</p>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<span className="text-2xl font-bold text-text">
${product.price}
</span>
<span className="text-sm text-lines line-through">
${product.originalPrice}
</span>
</div>
<span className="text-sm text-lines">
({product.reviews} reviews)
</span>
</div>
<button
onClick={() => handleDonate(product.id)}
disabled={donatedItems.has(product.id)}
className={`w-full flex items-center justify-center px-4 py-3 rounded-lg text-sm font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-other focus:ring-offset-2 ${
donatedItems.has(product.id)
? 'bg-other/20 text-other cursor-not-allowed'
: 'bg-other text-background hover:bg-lines transform hover:scale-105'
}`}
>
{donatedItems.has(product.id) ? (
<>
<Heart className="h-5 w-5 mr-2 fill-current" />
Donated!
</>
) : (
<>
<ShoppingCart className="h-5 w-5 mr-2" />
Donate Now
</>
)}
</button>
</div>
</div>
))}
</div>
<div className="text-center mt-12">
<p className="text-sm text-lines">
All donations go towards improving my portfolio and creating more amazing projects
</p>
</div>
</div>
</section>
);
};
export default DonateShop;

View File

@@ -0,0 +1,99 @@
import React, { useState, useEffect } from 'react';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
const DroneVideoCarousel: React.FC = () => {
const [currentIndex, setCurrentIndex] = useState(0);
// Placeholder YouTube video IDs - replace with actual drone video IDs
const videos = [
{ id: 'dQw4w9WgXcQ', title: 'Drone Footage 1' },
{ id: 'dQw4w9WgXcQ', title: 'Drone Footage 2' },
{ id: 'dQw4w9WgXcQ', title: 'Drone Footage 3' },
{ id: 'dQw4w9WgXcQ', title: 'Drone Footage 4' }
];
const nextSlide = () => {
setCurrentIndex((prevIndex) => (prevIndex + 1) % videos.length);
};
const prevSlide = () => {
setCurrentIndex((prevIndex) => (prevIndex - 1 + videos.length) % videos.length);
};
useEffect(() => {
const timer = setInterval(nextSlide, 5000);
return () => clearInterval(timer);
}, []);
return (
<section className="py-20 bg-background">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-text sm:text-4xl">
Drone Videography
</h2>
<p className="mt-4 text-lg text-lines max-w-2xl mx-auto">
Capturing stunning aerial perspectives through professional drone footage
</p>
</div>
<div className="relative max-w-4xl mx-auto">
<div className="aspect-video bg-background-light rounded-xl overflow-hidden shadow-2xl border border-lines">
<AnimatePresence mode="wait">
<motion.div
key={currentIndex}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
className="w-full h-full"
>
<iframe
src={`https://www.youtube.com/embed/${videos[currentIndex].id}?autoplay=0&mute=1&loop=1&playlist=${videos[currentIndex].id}`}
title={videos[currentIndex].title}
className="w-full h-full"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
</motion.div>
</AnimatePresence>
</div>
<button
onClick={prevSlide}
className="absolute left-4 top-1/2 transform -translate-y-1/2 bg-boxes hover:bg-other text-text p-3 rounded-full transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-lines focus:ring-offset-2 focus:ring-offset-background"
aria-label="Previous video"
>
<ChevronLeft className="h-6 w-6" />
</button>
<button
onClick={nextSlide}
className="absolute right-4 top-1/2 transform -translate-y-1/2 bg-boxes hover:bg-other text-text p-3 rounded-full transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-lines focus:ring-offset-2 focus:ring-offset-background"
aria-label="Next video"
>
<ChevronRight className="h-6 w-6" />
</button>
<div className="flex justify-center mt-6 space-x-2">
{videos.map((_, index) => (
<button
key={index}
onClick={() => setCurrentIndex(index)}
className={`w-3 h-3 rounded-full transition-all duration-200 ${
index === currentIndex
? 'bg-other'
: 'bg-lines hover:bg-boxes'
}`}
aria-label={`Go to video ${index + 1}`}
/>
))}
</div>
</div>
</div>
</section>
);
};
export default DroneVideoCarousel;

View File

@@ -0,0 +1,127 @@
import React from 'react';
import { Instagram, Twitter, Youtube, Github, Linkedin, Gamepad2, Mail, Phone } from 'lucide-react';
const Footer: React.FC = () => {
const socialLinks = [
{ name: 'Instagram', icon: Instagram, href: '#', color: 'hover:text-other' },
{ name: 'Twitter', icon: Twitter, href: '#', color: 'hover:text-other' },
{ name: 'YouTube', icon: Youtube, href: '#', color: 'hover:text-other' },
{ name: 'GitHub', icon: Github, href: '#', color: 'hover:text-text' },
{ name: 'LinkedIn', icon: Linkedin, href: '#', color: 'hover:text-other' },
{ name: 'Steam', icon: Gamepad2, href: '#', color: 'hover:text-lines' }
];
const footerLinks = [
{
title: 'Portfolio',
links: [
{ name: 'Web Development', href: '/portfolio/web' },
{ name: 'Mobile Apps', href: '/portfolio/mobile' },
{ name: 'UI/UX Design', href: '/portfolio/design' }
]
},
{
title: 'Services',
links: [
{ name: 'Frontend Development', href: '/services/frontend' },
{ name: 'Backend Development', href: '/services/backend' },
{ name: 'Consulting', href: '/services/consulting' }
]
},
{
title: 'Company',
links: [
{ name: 'About', href: '/about' },
{ name: 'Blog', href: '/blog' },
{ name: 'Contact', href: '/contact' }
]
}
];
return (
<footer className="bg-background text-text border-t border-lines">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{/* Brand Section */}
<div className="lg:col-span-1">
<div className="flex items-center">
<span className="text-2xl font-bold text-other">
Portfolio
</span>
</div>
<p className="mt-4 text-lines text-sm">
Creating exceptional digital experiences through innovative web development and beautiful design.
</p>
<div className="mt-6 flex space-x-4">
{socialLinks.map((social) => {
const IconComponent = social.icon;
return (
<a
key={social.name}
href={social.href}
className={`text-lines ${social.color} transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-other focus:ring-offset-2 focus:ring-offset-background rounded`}
aria-label={social.name}
>
<IconComponent className="h-5 w-5" />
</a>
);
})}
</div>
<div className="mt-6 space-y-2">
<div className="flex items-center text-lines text-sm">
<Mail className="h-4 w-4 mr-2" />
hello@example.com
</div>
<div className="flex items-center text-lines text-sm">
<Phone className="h-4 w-4 mr-2" />
+1 (555) 123-4567
</div>
</div>
</div>
{/* Links Sections */}
{footerLinks.map((section) => (
<div key={section.title}>
<h3 className="text-sm font-semibold text-text uppercase tracking-wider">
{section.title}
</h3>
<ul className="mt-4 space-y-2">
{section.links.map((link) => (
<li key={link.name}>
<a
href={link.href}
className="text-lines hover:text-other transition-colors duration-200 text-sm focus:outline-none focus:ring-2 focus:ring-other focus:ring-offset-2 focus:ring-offset-background rounded"
>
{link.name}
</a>
</li>
))}
</ul>
</div>
))}
</div>
<div className="mt-8 pt-8 border-t border-boxes">
<div className="flex flex-col md:flex-row justify-between items-center">
<p className="text-lines text-sm">
© 2025 Portfolio. All rights reserved.
</p>
<div className="mt-4 md:mt-0 flex space-x-6">
<a href="/privacy" className="text-lines hover:text-text text-sm transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-other focus:ring-offset-2 focus:ring-offset-background rounded">
Privacy Policy
</a>
<a href="/terms" className="text-lines hover:text-text text-sm transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-other focus:ring-offset-2 focus:ring-offset-background rounded">
Terms of Service
</a>
</div>
<p className="text-lines text-sm mt-4 md:mt-0">
Built with by <a rel="nofollow" target="_blank" href="https://meku.dev" className="text-other hover:text-lines transition-colors duration-200">Meku.dev</a>
</p>
</div>
</div>
</div>
</footer>
);
};
export default Footer;

View File

@@ -1,40 +0,0 @@
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;
}
}

View File

@@ -1,76 +0,0 @@
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>
)
}

View File

@@ -1,85 +0,0 @@
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>
)
}

View File

@@ -1,145 +0,0 @@
.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;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -0,0 +1,186 @@
import React, { useState, useRef, useEffect } from 'react';
import { ChevronDown, Menu, X } from 'lucide-react';
const Header: React.FC = () => {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const handleMouseEnter = (menu: string) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
setActiveDropdown(menu);
};
const handleMouseLeave = () => {
timeoutRef.current = setTimeout(() => {
setActiveDropdown(null);
}, 150);
};
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
const navigationItems = [
{ name: 'Home', href: '/' },
{
name: 'Portfolio',
href: '/portfolio',
submenu: [
{ name: 'Web Development', href: '/portfolio/web' },
{ name: 'Mobile Apps', href: '/portfolio/mobile' },
{ name: 'UI/UX Design', href: '/portfolio/design' },
{ name: 'E-commerce', href: '/portfolio/ecommerce' }
]
},
{
name: 'Services',
href: '/services',
submenu: [
{ name: 'Frontend Development', href: '/services/frontend' },
{ name: 'Backend Development', href: '/services/backend' },
{ name: 'Full Stack Solutions', href: '/services/fullstack' },
{ name: 'Consulting', href: '/services/consulting' }
]
},
{
name: 'About',
href: '/about',
submenu: [
{ name: 'My Story', href: '/about/story' },
{ name: 'Skills', href: '/about/skills' },
{ name: 'Experience', href: '/about/experience' },
{ name: 'Testimonials', href: '/about/testimonials' }
]
},
{ name: 'Blog', href: '/blog' },
{ name: 'Contact', href: '/contact' }
];
return (
<header className="bg-background shadow-lg sticky top-0 z-50">
<nav className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8" role="navigation" aria-label="Main navigation">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<div className="flex-shrink-0">
<a href="/" className="text-2xl font-bold text-other hover:text-lines transition-colors">
Portfolio
</a>
</div>
{/* Desktop Navigation */}
<div className="hidden md:block">
<div className="ml-10 flex items-baseline space-x-4">
{navigationItems.map((item) => (
<div
key={item.name}
className="relative"
onMouseEnter={() => item.submenu && handleMouseEnter(item.name)}
onMouseLeave={handleMouseLeave}
>
<a
href={item.href}
className="flex items-center px-3 py-2 rounded-md text-sm font-medium text-text hover:text-other hover:bg-background-light transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-lines focus:ring-offset-2"
aria-haspopup={item.submenu ? 'true' : 'false'}
aria-expanded={activeDropdown === item.name ? 'true' : 'false'}
>
{item.name}
{item.submenu && (
<ChevronDown
className={`ml-1 h-4 w-4 transition-transform duration-200 ${
activeDropdown === item.name ? 'rotate-180' : ''
}`}
aria-hidden="true"
/>
)}
</a>
{/* Dropdown Menu */}
{item.submenu && (
<div
className={`absolute left-0 mt-2 w-56 rounded-md shadow-lg bg-background-light ring-1 ring-lines ring-opacity-30 transition-all duration-200 ${
activeDropdown === item.name
? 'opacity-100 visible transform translate-y-0'
: 'opacity-0 invisible transform -translate-y-2'
}`}
role="menu"
aria-orientation="vertical"
>
<div className="py-1">
{item.submenu.map((subItem) => (
<a
key={subItem.name}
href={subItem.href}
className="block px-4 py-2 text-sm text-text hover:bg-boxes hover:text-other transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-lines focus:ring-inset"
role="menuitem"
>
{subItem.name}
</a>
))}
</div>
</div>
)}
</div>
))}
</div>
</div>
{/* Mobile menu button */}
<div className="md:hidden">
<button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="inline-flex items-center justify-center p-2 rounded-md text-text hover:text-other hover:bg-background-light focus:outline-none focus:ring-2 focus:ring-inset focus:ring-lines transition-colors duration-200"
aria-expanded={isMobileMenuOpen}
aria-label="Toggle mobile menu"
>
{isMobileMenuOpen ? (
<X className="block h-6 w-6" aria-hidden="true" />
) : (
<Menu className="block h-6 w-6" aria-hidden="true" />
)}
</button>
</div>
</div>
{/* Mobile Navigation */}
<div className={`md:hidden transition-all duration-300 ease-in-out ${
isMobileMenuOpen ? 'max-h-screen opacity-100' : 'max-h-0 opacity-0 overflow-hidden'
}`}>
<div className="px-2 pt-2 pb-3 space-y-1 sm:px-3 bg-background-light rounded-lg mt-2">
{navigationItems.map((item) => (
<div key={item.name}>
<a
href={item.href}
className="block px-3 py-2 rounded-md text-base font-medium text-text hover:text-other hover:bg-boxes transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-lines focus:ring-offset-2"
>
{item.name}
</a>
{item.submenu && (
<div className="ml-4 space-y-1">
{item.submenu.map((subItem) => (
<a
key={subItem.name}
href={subItem.href}
className="block px-3 py-2 rounded-md text-sm text-lines hover:text-other hover:bg-boxes transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-lines focus:ring-offset-2"
>
{subItem.name}
</a>
))}
</div>
)}
</div>
))}
</div>
</div>
</nav>
</header>
);
};
export default Header;

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { ArrowRight, Download } from 'lucide-react';
const Hero: React.FC = () => {
return (
<section className="relative bg-background-light py-20 lg:py-32 overflow-hidden">
<div className="absolute inset-0 bg-grid-pattern opacity-5"></div>
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="lg:grid lg:grid-cols-12 lg:gap-8 items-center">
<div className="sm:text-center md:max-w-2xl md:mx-auto lg:col-span-6 lg:text-left">
<h1 className="text-4xl font-bold text-text sm:text-5xl lg:text-6xl">
<span className="block">Creative</span>
<span className="block text-other">
Developer
</span>
</h1>
<p className="mt-6 text-lg text-lines sm:text-xl max-w-3xl">
I craft exceptional digital experiences through innovative web development,
combining cutting-edge technology with beautiful design to bring your ideas to life.
</p>
<div className="mt-8 sm:max-w-lg sm:mx-auto sm:text-center lg:text-left lg:mx-0">
<div className="flex flex-col sm:flex-row gap-4">
<a
href="/portfolio"
className="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-lg text-background bg-other hover:bg-lines focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-other transition-all duration-200 transform hover:scale-105"
>
View My Work
<ArrowRight className="ml-2 h-5 w-5" aria-hidden="true" />
</a>
<a
href="/resume.pdf"
className="inline-flex items-center justify-center px-6 py-3 border-2 border-lines text-base font-medium rounded-lg text-text bg-transparent hover:bg-background hover:border-other hover:text-other focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-other transition-all duration-200"
>
<Download className="mr-2 h-5 w-5" aria-hidden="true" />
Download Resume
</a>
</div>
</div>
</div>
<div className="mt-12 relative sm:max-w-lg sm:mx-auto lg:mt-0 lg:max-w-none lg:mx-0 lg:col-span-6 lg:flex lg:items-center">
<div className="relative mx-auto w-full rounded-lg shadow-lg lg:max-w-md">
<div className="relative block w-full bg-boxes rounded-lg overflow-hidden">
<img
className="w-full h-96 object-cover"
src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=687&q=80"
alt="Professional developer portrait"
loading="lazy"
/>
<div className="absolute inset-0 bg-gradient-to-tr from-boxes/40 to-other/20"></div>
</div>
</div>
</div>
</div>
</div>
</section>
);
};
export default Hero;

View File

@@ -0,0 +1,110 @@
import React from 'react';
import { ExternalLink, Github } from 'lucide-react';
const Portfolio: React.FC = () => {
const projects = [
{
title: 'E-Commerce Platform',
description: 'A full-stack e-commerce solution with React, Node.js, and Stripe integration.',
image: 'https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
tags: ['React', 'Node.js', 'MongoDB', 'Stripe'],
liveUrl: '#',
githubUrl: '#'
},
{
title: 'Task Management App',
description: 'A collaborative project management tool with real-time updates and team features.',
image: 'https://images.unsplash.com/photo-1611224923853-80b023f02d71?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
tags: ['Vue.js', 'Firebase', 'Tailwind CSS'],
liveUrl: '#',
githubUrl: '#'
},
{
title: 'Weather Dashboard',
description: 'A responsive weather application with location-based forecasts and data visualization.',
image: 'https://images.unsplash.com/photo-1504608524841-42fe6f032b4b?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
tags: ['React', 'TypeScript', 'Chart.js', 'API'],
liveUrl: '#',
githubUrl: '#'
}
];
return (
<section className="py-20 bg-background">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h2 className="text-3xl font-bold text-text sm:text-4xl">
Featured Projects
</h2>
<p className="mt-4 text-lg text-lines max-w-2xl mx-auto">
A showcase of my recent work and creative solutions
</p>
</div>
<div className="mt-16 grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
{projects.map((project, index) => (
<div
key={project.title}
className="bg-background-light rounded-xl shadow-lg overflow-hidden hover:shadow-xl transition-all duration-300 transform hover:-translate-y-2 group border border-lines"
>
<div className="relative overflow-hidden">
<img
className="w-full h-48 object-cover group-hover:scale-110 transition-transform duration-300"
src={project.image}
alt={project.title}
loading="lazy"
/>
<div className="absolute inset-0 bg-gradient-to-t from-background/80 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="absolute top-4 right-4 flex space-x-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<a
href={project.liveUrl}
className="p-2 bg-boxes rounded-full hover:bg-other transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-lines focus:ring-offset-2"
aria-label={`View ${project.title} live`}
>
<ExternalLink className="h-4 w-4 text-text" />
</a>
<a
href={project.githubUrl}
className="p-2 bg-boxes rounded-full hover:bg-other transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-lines focus:ring-offset-2"
aria-label={`View ${project.title} source code`}
>
<Github className="h-4 w-4 text-text" />
</a>
</div>
</div>
<div className="p-6">
<h3 className="text-xl font-semibold text-text mb-2">
{project.title}
</h3>
<p className="text-lines mb-4">
{project.description}
</p>
<div className="flex flex-wrap gap-2">
{project.tags.map((tag) => (
<span
key={tag}
className="px-3 py-1 text-xs font-medium bg-boxes text-other rounded-full"
>
{tag}
</span>
))}
</div>
</div>
</div>
))}
</div>
<div className="text-center mt-12">
<a
href="/portfolio"
className="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-lg text-background bg-other hover:bg-lines focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-other transition-all duration-200 transform hover:scale-105"
>
View All Projects
</a>
</div>
</div>
</section>
);
};
export default Portfolio;

View File

@@ -0,0 +1,115 @@
import React from 'react';
import { Briefcase, Code, Database, Palette, Smartphone, Zap } from 'lucide-react';
const Skills: React.FC = () => {
const experience = [
{
title: 'Senior Full-Stack Developer',
company: 'Tech Innovations Inc.',
period: '2022 - Present',
description: 'Leading development of scalable web applications using React, Node.js, and cloud technologies.'
},
{
title: 'Frontend Developer',
company: 'Digital Solutions Ltd.',
period: '2020 - 2022',
description: 'Built responsive user interfaces and improved performance for e-commerce platforms.'
},
{
title: 'Junior Developer',
company: 'StartupXYZ',
period: '2019 - 2020',
description: 'Developed mobile applications and contributed to backend API development.'
}
];
const specificSkills = [
{ name: 'React', level: 95, icon: Code },
{ name: 'TypeScript', level: 90, icon: Code },
{ name: 'Node.js', level: 85, icon: Database },
{ name: 'Python', level: 80, icon: Code },
{ name: 'UI/UX Design', level: 75, icon: Palette },
{ name: 'Mobile Development', level: 70, icon: Smartphone }
];
return (
<section className="py-20 bg-background-light">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h2 className="text-3xl font-bold text-text sm:text-4xl">
Experience & Skills
</h2>
<p className="mt-4 text-lg text-lines max-w-2xl mx-auto">
A comprehensive overview of my professional journey and technical expertise
</p>
</div>
<div className="mt-16 grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Experience Section */}
<div>
<div className="flex items-center mb-8">
<div className="p-3 bg-other rounded-lg mr-4">
<Briefcase className="h-6 w-6 text-background" />
</div>
<h3 className="text-2xl font-bold text-text">Experience</h3>
</div>
<div className="space-y-6">
{experience.map((exp, index) => (
<div
key={index}
className="bg-background rounded-lg p-6 border border-lines"
>
<h4 className="text-lg font-semibold text-text mb-1">
{exp.title}
</h4>
<p className="text-other font-medium mb-2">
{exp.company} {exp.period}
</p>
<p className="text-lines text-sm">
{exp.description}
</p>
</div>
))}
</div>
</div>
{/* Specific Skills Section */}
<div>
<div className="flex items-center mb-8">
<div className="p-3 bg-boxes rounded-lg mr-4">
<Zap className="h-6 w-6 text-text" />
</div>
<h3 className="text-2xl font-bold text-text">Specific Skills</h3>
</div>
<div className="space-y-6">
{specificSkills.map((skill, index) => {
const IconComponent = skill.icon;
return (
<div key={skill.name} className="bg-background rounded-lg p-6 shadow-md border border-lines">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center">
<div className="p-2 bg-boxes rounded-lg mr-3">
<IconComponent className="h-5 w-5 text-other" />
</div>
<span className="font-semibold text-text">{skill.name}</span>
</div>
<span className="text-sm font-medium text-lines">{skill.level}%</span>
</div>
<div className="w-full bg-background-light rounded-full h-2">
<div
className="bg-other h-2 rounded-full transition-all duration-1000 ease-out"
style={{ width: `${skill.level}%` }}
></div>
</div>
</div>
);
})}
</div>
</div>
</div>
</div>
</section>
);
};
export default Skills;

View File

@@ -0,0 +1,100 @@
import React from 'react';
import { TrendingUp, BarChart3 } from 'lucide-react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
// Placeholder data - replace with actual Trading212 API data
const data = [
{ date: '2024-01', value: 1000 },
{ date: '2024-02', value: 1200 },
{ date: '2024-03', value: 1100 },
{ date: '2024-04', value: 1400 },
{ date: '2024-05', value: 1300 },
{ date: '2024-06', value: 1600 }
];
const TradingGraph: React.FC = () => {
return (
<section className="py-16 bg-background-light">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-text sm:text-4xl">
Trading Performance
</h2>
<p className="mt-4 text-lg text-lines max-w-2xl mx-auto">
Real-time insights from Trading212 portfolio tracking
</p>
</div>
<div className="bg-background rounded-xl shadow-lg p-6 border border-lines">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-3">
<div className="p-2 bg-other rounded-lg">
<BarChart3 className="h-6 w-6 text-background" />
</div>
<div>
<h3 className="text-lg font-semibold text-text">Portfolio Value</h3>
<p className="text-sm text-lines">Trading212 Integration</p>
</div>
</div>
<div className="text-right">
<div className="text-2xl font-bold text-text">$1,600</div>
<div className="flex items-center text-sm text-other">
<TrendingUp className="h-4 w-4 mr-1" />
+12.5% this month
</div>
</div>
</div>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--c-lines)" />
<XAxis
dataKey="date"
stroke="var(--c-lines)"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="var(--c-lines)"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `$${value}`}
/>
<Tooltip
contentStyle={{
backgroundColor: 'var(--c-background-light)',
border: '1px solid var(--c-lines)',
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
color: 'var(--c-text)'
}}
labelStyle={{ color: 'var(--c-text)' }}
formatter={(value: number) => [`$${value}`, 'Value']}
/>
<Line
type="monotone"
dataKey="value"
stroke="var(--c-other)"
strokeWidth={3}
dot={{ fill: '#c026d3', strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: '#c026d3', strokeWidth: 2, fill: '#ffffff' }}
/>
</LineChart>
</ResponsiveContainer>
</div>
<div className="mt-6 text-center">
<p className="text-sm text-slate-500">
API integration pending - displaying sample data
</p>
</div>
</div>
</div>
</section>
);
};
export default TradingGraph;

View File

@@ -0,0 +1,83 @@
import React from 'react';
import { ExternalLink } from 'lucide-react';
const WebsiteScreenshots: React.FC = () => {
const websites = [
{
title: 'E-Commerce Platform',
image: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
url: '#',
description: 'Modern online store with seamless checkout'
},
{
title: 'Portfolio Website',
image: 'https://images.unsplash.com/photo-1467232004584-a241de8bcf5d?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
url: '#',
description: 'Creative showcase for digital artists'
},
{
title: 'Business Landing Page',
image: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
url: '#',
description: 'Professional B2B service presentation'
},
{
title: 'Blog Platform',
image: 'https://images.unsplash.com/photo-1486312338219-ce68e2c6f44d?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
url: '#',
description: 'Content management system for writers'
}
];
return (
<section className="py-16 bg-background-light">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-text sm:text-4xl">
Website Screenshots
</h2>
<p className="mt-4 text-lg text-lines max-w-2xl mx-auto">
A glimpse of recent web development projects and designs
</p>
</div>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
{websites.map((website, index) => (
<div
key={website.title}
className="group bg-background rounded-lg shadow-md hover:shadow-lg transition-all duration-300 transform hover:-translate-y-1 border border-lines overflow-hidden"
>
<div className="relative overflow-hidden">
<img
className="w-full h-32 object-cover group-hover:scale-105 transition-transform duration-300"
src={website.image}
alt={`${website.title} screenshot`}
loading="lazy"
/>
<div className="absolute inset-0 bg-gradient-to-t from-background/80 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-end justify-end p-2">
<a
href={website.url}
className="p-2 bg-boxes rounded-full hover:bg-other transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-lines focus:ring-offset-2"
aria-label={`View ${website.title}`}
>
<ExternalLink className="h-4 w-4 text-text" />
</a>
</div>
</div>
<div className="p-4">
<h3 className="text-sm font-semibold text-text mb-1">
{website.title}
</h3>
<p className="text-xs text-lines">
{website.description}
</p>
</div>
</div>
))}
</div>
</div>
</section>
);
};
export default WebsiteScreenshots;

View File

@@ -1,90 +0,0 @@
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 3000. Ostrava zdarma; mimo 10/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>
)
}

View File

@@ -1,103 +0,0 @@
@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;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -1,168 +0,0 @@
.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%;
}
}

View File

@@ -1,86 +0,0 @@
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>
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -1,304 +0,0 @@
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 */
}
}

View File

@@ -1,52 +0,0 @@
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>
)
}

View File

@@ -1,30 +0,0 @@
/*
# 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 ...;
}
```

View File

@@ -1,74 +0,0 @@
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
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 ...;
}
*/

View File

@@ -1,67 +0,0 @@
@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;
}
}

View File

@@ -1,28 +0,0 @@
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 />
</>
)
}

View File

@@ -1,28 +0,0 @@
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 />
</>
)
}

View File

@@ -1,69 +0,0 @@
# 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>
</>
);
}
```

View File

@@ -1,10 +0,0 @@
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>,
)

View File

@@ -0,0 +1,26 @@
import React from 'react';
import Header from '../components/Header';
import Footer from '../components/Footer';
const About: React.FC = () => {
return (
<div className="min-h-screen">
<Header />
<main className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h1 className="text-4xl font-bold text-slate-800 sm:text-5xl">
About
</h1>
<p className="mt-4 text-lg text-slate-600 max-w-2xl mx-auto">
Ask Meku to generate content for this page.
</p>
</div>
</div>
</main>
<Footer />
</div>
);
};
export default About;

View File

@@ -0,0 +1,26 @@
import React from 'react';
import Header from '../components/Header';
import Footer from '../components/Footer';
const Blog: React.FC = () => {
return (
<div className="min-h-screen">
<Header />
<main className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h1 className="text-4xl font-bold text-slate-800 sm:text-5xl">
Blog
</h1>
<p className="mt-4 text-lg text-slate-600 max-w-2xl mx-auto">
Ask Meku to generate content for this page.
</p>
</div>
</div>
</main>
<Footer />
</div>
);
};
export default Blog;

View File

@@ -0,0 +1,26 @@
import React from 'react';
import Header from '../components/Header';
import Footer from '../components/Footer';
const Contact: React.FC = () => {
return (
<div className="min-h-screen">
<Header />
<main className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h1 className="text-4xl font-bold text-slate-800 sm:text-5xl">
Contact
</h1>
<p className="mt-4 text-lg text-slate-600 max-w-2xl mx-auto">
Ask Meku to generate content for this page.
</p>
</div>
</div>
</main>
<Footer />
</div>
);
};
export default Contact;

View File

@@ -0,0 +1,30 @@
import React from 'react';
import Header from '../components/Header';
import Hero from '../components/Hero';
import Skills from '../components/Skills';
import Portfolio from '../components/Portfolio';
import DroneVideoCarousel from '../components/DroneVideoCarousel';
import WebsiteScreenshots from '../components/WebsiteScreenshots';
import TradingGraph from '../components/TradingGraph';
import DonateShop from '../components/DonateShop';
import Footer from '../components/Footer';
const Home: React.FC = () => {
return (
<div className="min-h-screen">
<Header />
<main>
<Hero />
<Skills />
<Portfolio />
<DroneVideoCarousel />
<WebsiteScreenshots />
<TradingGraph />
<DonateShop />
</main>
<Footer />
</div>
);
};
export default Home;

View File

@@ -0,0 +1,179 @@
import React from 'react';
const NotFound: React.FC = () => {
return (
<section className="relative flex py-10 min-h-screen items-center justify-center overflow-hidden bg-black">
<div className="mx-auto relative z-30 w-full max-w-[600px] text-center px-4">
{/* Large 404 Text */}
<div className="mb-8">
<svg
width="472"
height="158"
viewBox="0 0 472 158"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M26.4028 0.5224C29.8616 0.5224 32.6655 3.3263 32.6655 6.7851V58.7187H75.8726V9.4306C75.8726 5.97182 78.6765 3.16794 82.1353 3.16791H102.236C105.694 3.16817 108.499 5.97196 108.499 9.4306V151.215C108.498 154.673 105.694 157.477 102.235 157.477H82.1353C78.6766 157.477 75.8727 154.673 75.8726 151.215V91.3437H23.0571C19.5983 91.3437 16.7944 88.5398 16.7944 85.081V78.1181H6.30322C2.84444 78.1181 0.0405444 75.3142 0.0405273 71.8554V6.7851C0.0405355 3.32631 2.84444 0.522409 6.30322 0.5224H26.4028Z"
fill="url(#paint0_linear_11881_2293)"
/>
<path
d="M262.328 82.4706C263.99 82.4708 265.338 83.8187 265.338 85.4814V97.8544H277.712C279.375 97.8546 280.723 99.2018 280.723 100.864V116.31C280.723 117.973 279.375 119.321 277.712 119.321H260.835C259.173 119.321 257.825 117.973 257.825 116.31V103.937H214.175V116.31C214.175 117.973 212.827 119.321 211.165 119.321H194.289C192.626 119.321 191.278 117.973 191.278 116.31V100.864C191.278 99.2017 192.626 97.8544 194.289 97.8544H207.02V85.4814C207.02 83.8186 208.368 82.4706 210.031 82.4706H262.328Z"
fill="url(#paint1_linear_11881_2293)"
/>
<path
d="M222.614 41.3251C224.277 41.3251 225.625 42.6731 225.625 44.3359V59.7812C225.625 61.4439 224.277 62.7919 222.614 62.7919H205.737C204.074 62.7917 202.727 61.4438 202.727 59.7812V44.3359C202.727 42.6733 204.074 41.3254 205.737 41.3251H222.614Z"
fill="url(#paint2_linear_11881_2293)"
/>
<path
d="M266.263 41.3251C267.926 41.3252 269.274 42.6732 269.274 44.3359V59.7812C269.274 61.4439 267.926 62.7918 266.263 62.7919H249.386C247.724 62.7918 246.375 61.4439 246.375 59.7812V44.3359C246.375 42.6732 247.724 41.3252 249.386 41.3251H266.263Z"
fill="url(#paint3_linear_11881_2293)"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M291.231 3.16693C313.322 3.16693 331.231 21.0755 331.231 43.1669V117.477L331.218 118.51C330.671 140.124 312.977 157.477 291.231 157.477H180.769C158.678 157.477 140.769 139.569 140.769 117.477V43.1669C140.769 21.0755 158.678 3.16693 180.769 3.16693H291.231ZM180.769 27.1669C171.932 27.1669 164.769 34.3304 164.769 43.1669V117.477C164.769 126.314 171.932 133.477 180.769 133.477H291.231C300.068 133.477 307.231 126.314 307.231 117.477V43.1669C307.231 34.3304 300.068 27.1669 291.231 27.1669H180.769Z"
fill="url(#paint4_linear_11881_2293)"
/>
<path
d="M389.865 0.5224C393.324 0.522421 396.127 3.32632 396.127 6.7851V58.7187H439.334V9.4306C439.334 5.9718 442.138 3.16791 445.597 3.16791H465.697C469.156 3.16791 471.959 5.9718 471.959 9.4306V151.215C471.959 154.673 469.155 157.477 465.697 157.477H445.597C442.138 157.477 439.335 154.673 439.334 151.215V91.3437H386.518C383.059 91.3436 380.255 88.5397 380.255 85.081V78.1181H369.765C366.306 78.1181 363.502 75.3142 363.502 71.8554V6.7851C363.502 3.3263 366.306 0.5224 369.765 0.5224H389.865Z"
fill="url(#paint5_linear_11881_2293)"
/>
<defs>
<linearGradient
id="paint0_linear_11881_2293"
x1="471.959"
y1="157.477"
x2="448.654"
y2="-49.8952"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#3D404D" />
<stop offset="1" stopColor="#8F95B2" />
</linearGradient>
<linearGradient
id="paint1_linear_11881_2293"
x1="471.959"
y1="157.477"
x2="448.654"
y2="-49.8952"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#3D404D" />
<stop offset="1" stopColor="#8F95B2" />
</linearGradient>
<linearGradient
id="paint2_linear_11881_2293"
x1="471.959"
y1="157.477"
x2="448.654"
y2="-49.8952"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#3D404D" />
<stop offset="1" stopColor="#8F95B2" />
</linearGradient>
<linearGradient
id="paint3_linear_11881_2293"
x1="471.959"
y1="157.477"
x2="448.654"
y2="-49.8952"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#3D404D" />
<stop offset="1" stopColor="#8F95B2" />
</linearGradient>
<linearGradient
id="paint4_linear_11881_2293"
x1="471.959"
y1="157.477"
x2="448.654"
y2="-49.8952"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#3D404D" />
<stop offset="1" stopColor="#8F95B2" />
</linearGradient>
<linearGradient
id="paint5_linear_11881_2293"
x1="471.959"
y1="157.477"
x2="448.654"
y2="-49.8952"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#3D404D" />
<stop offset="1" stopColor="#8F95B2" />
</linearGradient>
</defs>
</svg>
</div>
<h1 className="mb-4 text-3xl font-bold text-white sm:text-4xl">
OPPS! Page Not Found
</h1>
<p className="mb-8 text-base text-white/60 sm:text-lg">
We can&apos;t seem to find the page you are looking for!
</p>
<a
href={typeof window !== 'undefined' ? window.location.origin : '/'}
className="inline-flex items-center gap-2 rounded-full bg-white px-6 py-3 text-sm font-medium text-black transition-colors hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-black"
>
Back to homepage
</a>
{/* Footer */}
<div className="mt-16">
<p className="text-sm text-gray-600">
© {new Date().getFullYear()} - Meku.dev
</p>
</div>
</div>
<div className="absolute inset-0 bg-[url(https://meku.dev/images/grain.png)] bg-cover bg-center opacity-60 mix-blend-soft-light z-20"></div>
<div className="absolute bottom-0 left-0 right-0 z-10">
<svg
width="2192"
height="771"
viewBox="0 0 2192 771"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g opacity="0.35" filter="url(#filter0_f_3740_75)">
<path
d="M199.999 258.919C199.999 86.6144 601.152 347.404 1096 347.404C1590.85 347.404 1992 86.6146 1992 258.919C1992 431.223 1590.85 570.904 1096 570.904C601.152 570.904 199.999 431.223 199.999 258.919Z"
fill="#C0C2CF"
/>
</g>
<defs>
<filter
id="filter0_f_3740_75"
x="-0.000732422"
y="0.0515137"
width="2192"
height="770.852"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="BackgroundImageFix"
result="shape"
/>
<feGaussianBlur
stdDeviation="100"
result="effect1_foregroundBlur_3740_75"
/>
</filter>
</defs>
</svg>
</div>
</section>
);
};
export default NotFound;

View File

@@ -0,0 +1,26 @@
import React from 'react';
import Header from '../components/Header';
import Footer from '../components/Footer';
const Portfolio: React.FC = () => {
return (
<div className="min-h-screen">
<Header />
<main className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h1 className="text-4xl font-bold text-slate-800 sm:text-5xl">
Portfolio
</h1>
<p className="mt-4 text-lg text-slate-600 max-w-2xl mx-auto">
Ask Meku to generate content for this page.
</p>
</div>
</div>
</main>
<Footer />
</div>
);
};
export default Portfolio;

View File

@@ -0,0 +1,26 @@
import React from 'react';
import Header from '../components/Header';
import Footer from '../components/Footer';
const Services: React.FC = () => {
return (
<div className="min-h-screen">
<Header />
<main className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h1 className="text-4xl font-bold text-slate-800 sm:text-5xl">
Services
</h1>
<p className="mt-4 text-lg text-slate-600 max-w-2xl mx-auto">
Ask Meku to generate content for this page.
</p>
</div>
</div>
</main>
<Footer />
</div>
);
};
export default Services;

View File

@@ -1,206 +0,0 @@
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>
);
}

View File

@@ -1,240 +0,0 @@
@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;
}
}

View File

@@ -1,49 +0,0 @@
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 (
<></>
)
}

View File

@@ -1,192 +0,0 @@
.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;
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,17 +0,0 @@
$(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');
});
});

View File

@@ -1,22 +0,0 @@
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 />;
}

View File

@@ -1,71 +0,0 @@
# 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;
```

View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />