This commit is contained in:
2025-11-07 00:46:35 +01:00
parent 2118f002d1
commit c3f837b90f
143 changed files with 4223 additions and 9686 deletions

View File

@@ -1,14 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<link rel="stylesheet" href="reset.css">
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +0,0 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.16",
"@types/react-router": "^5.1.20",
"axios": "^1.13.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-icons": "^5.5.0",
"react-router-dom": "^7.8.1",
"tailwindcss": "^4.1.16"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@types/axios": "^0.9.36",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.0",
"eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.39.1",
"vite": "^7.1.2"
}
}

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,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,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,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

@@ -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,11 +0,0 @@
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
tailwindcss()
],
})

View File

@@ -100,6 +100,46 @@ The `frontend` folder contains all code and assets for the client-side React app
npm run preview
```
## Portfolio site structure (vontor.cz)
New top-level navigation and sections were added to build a modern portfolio:
- components/
- navbar/SiteNav.tsx sticky top navigation with dropdown
- hero/HeroCarousel.tsx YouTube carousel hero with overlay text
- portfolio/PortfolioGrid.tsx grid of projects with modal detail
- trading/TradingGraph.tsx placeholder for Trading212 live graph
- donate/DonationShop.tsx symbolic donation tiers styled as product cards
- skills/SkillsSection.tsx categorized skill lists
- hosting/HostingSecuritySection.tsx hosting & protection info panel
- common/Modal.tsx reusable accessible modal
- pages/
- home/home.tsx composes all sections on the homepage
- portfolio/PortfolioPage.tsx
- skills/SkillsPage.tsx
- hosting/HostingSecurityPage.tsx
- donate/DonateShopPage.tsx
- contact/ContactPage.tsx
- layouts/
- HomeLayout.tsx wraps pages with SiteNav and Footer
Routing (src/App.tsx):
- "/" → Home (all sections)
- "/portfolio"
- "/skills"
- "/hosting-security"
- "/donate"
- "/contact"
Styling:
- The global palette lives in `src/index.css` as CSS variables and Tailwind is available via `@import "tailwindcss"`.
- Reusable helpers: `.glass`, `.glow`, `.section`, `.container`, `.divider`.
Assets:
- Public project images are in `public/portfolio/` and used by `PortfolioGrid`.
## Creating a New React Project with Vite
If you want to start a new project:

View File

@@ -1,11 +1,11 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
import globals from 'globals'
import tseslint from 'typescript-eslint'
import { globalIgnores } from 'eslint/config'
export default defineConfig([
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
@@ -15,12 +15,9 @@ export default defineConfig([
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
},
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])
])

View File

@@ -1,26 +1,26 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<link rel="stylesheet" href="reset.css">
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="https://meku.dev/favicon.ico" />
<title>Modern Portfolio</title>
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="generator" content="Meku" />
<meta name="description" content="Modern Portfolio Generated with Meku" />
<meta name="author" content="Meku" />
<meta property="og:title" content="Modern Portfolio" />
<meta property="og:description" content="Modern Portfolio Generated with Meku" />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://meku.dev/images/meku.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@meku_dev" />
<meta name="twitter:image" content="https://meku.dev/images/meku.png" />
<title>Vontor.cz Creative Tech & Design Portfolio</title>
<meta name="description" content="Vontor.cz showcases creative technology, design engineering, drone visuals, web applications, and selfhosted infrastructure by Bruno Vontor." />
<meta name="theme-color" content="#031D44" />
<meta property="og:title" content="Vontor.cz Creative Tech & Design" />
<meta property="og:description" content="Fullstack development, infrastructure, automation, and visual technology projects." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://vontor.cz" />
<meta property="og:image" content="/portfolio/perlica.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Vontor.cz Creative Tech & Design" />
<meta name="twitter:description" content="Engineering + design portfolio: backend, frontend, infrastructure, drone and automation." />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" href="/portfolio/perlica.png" as="image" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/index.tsx"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{
"name": "modern-portfolio",
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
@@ -10,35 +10,29 @@
"preview": "vite preview"
},
"dependencies": {
"globals": "^16.4.0",
"@hookform/resolvers": "^5.2.1",
"@radix-ui/themes": "^3.2.1",
"@vitejs/plugin-react": "^5.0.2",
"autoprefixer": "^10.4.19",
"date-fns": "^3.6.0",
"framer-motion": "^12.12.2",
"lucide-react": "^0.462.0",
"postcss": "^8.4.38",
"@tailwindcss/vite": "^4.1.16",
"@types/react-router": "^5.1.20",
"axios": "^1.13.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-hook-form": "^7.53.0",
"react-resizable-panels": "^2.1.3",
"react-router-dom": "^6.23.0",
"react-toastify": "^11.0.5",
"recharts": "^2.12.7",
"tailwindcss": "^3.4.1",
"vite": "^7.1.6",
"zod": "^3.23.8",
"@supabase/supabase-js": "^2.57.4"
"react-icons": "^5.5.0",
"react-router-dom": "^7.8.1",
"tailwindcss": "^4.1.16"
},
"devDependencies": {
"@eslint/js": "^9.35.0",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"eslint": "^9.35.0",
"@eslint/js": "^9.33.0",
"@tailwindcss/postcss": "^4.1.17",
"@types/axios": "^0.9.36",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.0",
"autoprefixer": "^10.4.21",
"eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.43.0"
"typescript-eslint": "^8.39.1",
"vite": "^7.1.2"
}
}
}

View File

@@ -1,6 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
};

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

45
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,45 @@
import { BrowserRouter as Router, Routes, Route } 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";
// Pages
import PortfolioPage from "./pages/portfolio/PortfolioPage";
import SkillsPage from "./pages/skills/SkillsPage";
import HostingSecurityPage from "./pages/hosting/HostingSecurityPage";
import DonateShopPage from "./pages/donate/DonateShopPage";
import ContactPage from "./pages/contact/ContactPage";
import ScrollToTop from "./components/common/ScrollToTop";
export default function App() {
return (
<Router>
<UserContextProvider>
<ScrollToTop />
<Routes>
{/* Public routes */}
<Route path="/" element={<HomeLayout />}>
<Route index element={<Home />} />
<Route path="portfolio" element={<PortfolioPage />} />
<Route path="skills" element={<SkillsPage />} />
<Route path="hosting-security" element={<HostingSecurityPage />} />
<Route path="donate" element={<DonateShopPage />} />
<Route path="contact" element={<ContactPage />} />
{/* Utilities */}
<Route path="downloader" element={<Downloader />} />
</Route>
{/* Example protected route group (kept for future use) */}
<Route element={<PrivateRoute />}>
<Route path="/" element={<HomeLayout />}>
<Route path="protected-downloader" element={<Downloader />} />
</Route>
</Route>
</Routes>
</UserContextProvider>
</Router>
);
}

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 823 B

After

Width:  |  Height:  |  Size: 823 B

View File

Before

Width:  |  Height:  |  Size: 705 B

After

Width:  |  Height:  |  Size: 705 B

View File

Before

Width:  |  Height:  |  Size: 366 B

After

Width:  |  Height:  |  Size: 366 B

View File

Before

Width:  |  Height:  |  Size: 722 B

After

Width:  |  Height:  |  Size: 722 B

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

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

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

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

@@ -0,0 +1,39 @@
import { FaGithub, FaInstagram, FaYoutube, FaLinkedin, FaSteam, FaXTwitter } from "react-icons/fa6";
export default function Footer() {
return (
<footer id="contacts" className="mt-12 bg-[var(--c-background-light)]">
<div className="container py-8 grid gap-6 md:grid-cols-3">
<div>
<h3 className="text-xl font-semibold text-rainbow">vontor.cz</h3>
<p className="text-sm text-[var(--c-lines)] mt-2">© 2025 Vontor.cz</p>
</div>
<address className="not-italic text-sm">
<div><b>David Bruno Vontor</b></div>
<div className="mt-2">Tel.: <a className="hover:text-[var(--c-other)]" href="tel:+420605512624">+420 605 512 624</a></div>
<div>E-mail: <a className="hover:text-[var(--c-other)]" href="mailto:brunovontor@gmail.com">brunovontor@gmail.com</a></div>
<div className="mt-1">IČO: <a className="hover:text-[var(--c-other)]" href="https://www.rzp.cz/verejne-udaje/cs/udaje/vyber-subjektu;ico=21613109;" target="_blank" rel="noopener noreferrer">21613109</a></div>
</address>
<div className="flex items-center gap-4 text-2xl justify-start md:justify-end">
<Social href="https://github.com/Brunobrno" label="GitHub"><FaGithub /></Social>
<Social href="https://www.instagram.com/brunovontor/" label="Instagram"><FaInstagram /></Social>
<Social href="https://twitter.com/BVontor" label="X / Twitter"><FaXTwitter /></Social>
<Social href="https://www.youtube.com/@brunovontor" label="YouTube"><FaYoutube /></Social>
<Social href="https://www.linkedin.com/in/brunovontor/" label="LinkedIn"><FaLinkedin /></Social>
<Social href="https://steamcommunity.com/id/Brunobrno/" label="Steam"><FaSteam /></Social>
</div>
</div>
</footer>
);
}
function Social({ href, label, children }: { href: string; label: string; children: React.ReactNode }) {
return (
<a href={href} target="_blank" rel="noreferrer" aria-label={label}
className="rounded p-2 transition-transform duration-200 hover:scale-110 hover:text-[var(--c-other)]">
{children}
</a>
);
}

View File

@@ -6,7 +6,7 @@ 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 [success, setSuccess] = useState(false)
const openingRef = useRef<HTMLDivElement>(null)
function handleSubmit() {

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

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

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

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

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

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

@@ -1,83 +0,0 @@
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,4 +1,4 @@
import React, { useEffect, useRef } from "react"
import { useEffect, useRef } from "react"
import styles from "./drone.module.css"
export default function Drone() {

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -0,0 +1,33 @@
import { useEffect } from "react";
import type { ReactNode } from "react";
type Props = {
open: boolean;
onClose: () => void;
title?: string;
children: ReactNode;
};
export default function Modal({ open, onClose, title, children }: Props) {
useEffect(() => {
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
if (open) document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
}, [open, onClose]);
if (!open) return null;
return (
<div className="fixed inset-0 z-[100]">
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
<div role="dialog" aria-modal="true" className="absolute inset-0 flex items-center justify-center p-4">
<div className="glass max-w-3xl w-full p-4 md:p-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg md:text-xl font-semibold">{title}</h3>
<button onClick={onClose} aria-label="Close" className="px-2 py-1 glow"></button>
</div>
<div className="prose prose-invert max-w-none">{children}</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,10 @@
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
export default function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
}, [pathname]);
return null;
}

View File

@@ -0,0 +1,34 @@
import { FaCoffee, FaBatteryFull, FaMicrochip } from "react-icons/fa";
import type { ReactNode } from 'react';
type Tier = { id: string; title: string; price: number; desc: string; color: string; icon: ReactNode };
const tiers: Tier[] = [
{ id: 'coffee', title: 'Coffee', price: 3, desc: 'Fuel late-night coding sessions.', color: '#d97706', icon: <FaCoffee /> },
{ id: 'battery', title: 'Drone Battery', price: 30, desc: 'Extend aerial filming time.', color: '#16a34a', icon: <FaBatteryFull /> },
{ id: 'gpu', title: 'GPU Upgrade', price: 200, desc: 'Speed up rendering and ML tasks.', color: '#6366f1', icon: <FaMicrochip /> },
];
export default function DonationShop() {
return (
<section id="shop" className="section">
<div className="container">
<h2 className="text-3xl md:text-4xl font-bold mb-2 text-rainbow">Support My Creative Journey</h2>
<p className="text-gray-300">Instead of buying products, consider donating to support my creative journey.</p>
<div className="grid md:grid-cols-3 gap-6 mt-6">
{tiers.map(t => (
<div key={t.id} className="card p-5 flex flex-col">
<div className="text-5xl mb-3" style={{ color: t.color }}>{t.icon}</div>
<h3 className="text-xl font-semibold" style={{ color: t.color }}>{t.title}</h3>
<p className="text-[var(--c-lines)] mt-1">{t.desc}</p>
<div className="mt-auto flex items-center justify-between pt-4">
<span className="text-2xl font-bold" style={{ color: t.color }}>${t.price}</span>
<button className="px-4 py-2 rounded-lg font-semibold text-white bg-gradient-to-r from-fuchsia-600 to-orange-500 hover:shadow-lg hover:shadow-fuchsia-600/25 transition-all" onClick={() => alert(`Thank you for supporting with ${t.title}!`)}>Donate</button>
</div>
</div>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,62 @@
import { useEffect, useState } from "react";
const videos = ["dQw4w9WgXcQ", "M7lc1UVf-VE", "aqz-KE-bpKQ"]; // placeholder IDs
export default function HeroCarousel() {
const [index, setIndex] = useState(0);
useEffect(() => {
const id = setInterval(() => setIndex(i => (i + 1) % videos.length), 10000);
return () => clearInterval(id);
}, []);
return (
<section id="home" className="relative min-h-[80vh] md:min-h-[85vh] flex items-center justify-center overflow-hidden">
{/* Background Gradient and animated glows */}
<div className="absolute inset-0 bg-gradient-to-br from-slate-800 via-slate-900 to-black -z-10" />
<div className="absolute inset-0 -z-10 pointer-events-none">
<div className="absolute top-1/4 left-1/4 w-64 h-64 bg-fuchsia-600/10 rounded-full blur-3xl animate-pulse" />
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-orange-500/10 rounded-full blur-3xl animate-pulse delay-1000" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-80 h-80 bg-violet-400/10 rounded-full blur-3xl animate-pulse delay-2000" />
</div>
<div className="relative container mx-auto px-4 py-10 grid lg:grid-cols-2 gap-10 items-center">
{/* Text */}
<div className="text-center lg:text-left">
<h1 className="text-4xl md:text-6xl font-bold mb-4 leading-tight">
<span className="text-rainbow">Welcome to</span><br />
<span className="text-white">Vontor.cz</span>
</h1>
<p className="text-lg md:text-xl text-gray-300 mb-6">Creative Tech & Design by <span className="text-fuchsia-600 font-semibold">Bruno Vontor</span></p>
<div className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start">
<a href="#portfolio" className="px-8 py-3 bg-gradient-to-r from-fuchsia-600 to-orange-500 text-white font-semibold rounded-lg hover:shadow-lg hover:shadow-fuchsia-600/25 transition-all duration-300 transform hover:scale-105">View Portfolio</a>
<a href="#contact" className="px-8 py-3 border-2 border-violet-400 text-violet-400 font-semibold rounded-lg hover:bg-violet-400 hover:text-white transition-all duration-300">Get In Touch</a>
</div>
</div>
{/* Video carousel */}
<div className="relative">
<div className="relative aspect-video bg-slate-800 rounded-xl overflow-hidden shadow-2xl">
{videos.map((v,i) => (
<iframe
key={v}
src={`https://www.youtube.com/embed/${v}?autoplay=${i===index?1:0}&mute=1&loop=1&playlist=${v}`}
title={`Slide ${i+1}`}
className={`absolute inset-0 w-full h-full transition-opacity duration-700 ${i===index? 'opacity-100':'opacity-0'}`}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
))}
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none" />
</div>
{/* Indicators */}
<div className="flex justify-center mt-4 space-x-2">
{videos.map((_,i) => (
<button key={i} onClick={()=>setIndex(i)} aria-label={`Go to slide ${i+1}`} className={`w-3 h-3 rounded-full transition-all duration-300 ${i===index? 'bg-fuchsia-600':'bg-gray-600 hover:bg-gray-400'}`} />
))}
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,18 @@
export default function HostingSecuritySection() {
return (
<section id="hosting" className="py-20">
<div className="container mx-auto px-4">
<h2 className="text-3xl md:text-4xl font-bold mb-6 text-rainbow">Hosting & Protection</h2>
<div className="card p-6 md:p-8">
<p className="text-gray-300">We host our applications ourselves, which reduces hosting costs as projects scale.</p>
<p className="text-gray-300 mt-2">All websites are protected by Cloudflare and optimized for performance.</p>
<div className="grid grid-cols-3 md:grid-cols-6 gap-4 text-center text-sm mt-6">
{['Server', 'Cloudflare', 'Docker', 'SSL', 'Monitoring', 'Scaling'].map(item => (
<div key={item} className="p-3 rounded-lg bg-slate-700/50 text-violet-300 transition-transform duration-200 hover:scale-105">{item}</div>
))}
</div>
</div>
</div>
</section>
);
}

View File

@@ -1,16 +1,12 @@
import React, { useState, useContext } from "react"
import { useState } 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

View File

@@ -0,0 +1,73 @@
import { useEffect, useState } from "react";
import { NavLink } from "react-router-dom";
import { FaBars, FaTimes, FaChevronDown } from "react-icons/fa";
/* Responsive sticky navigation bar using theme variables */
export default function SiteNav() {
const [open, setOpen] = useState(false);
const [servicesOpen, setServicesOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 50);
onScroll();
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
return (
<header className={`sticky top-0 z-50 transition-all ${scrolled ? 'bg-slate-800/95 backdrop-blur-md shadow-lg' : 'bg-transparent'}`}>
<nav className="relative container mx-auto px-4 flex items-center justify-between h-16 text-[var(--c-text)] font-medium">
<div className="text-xl tracking-wide font-semibold">
<NavLink to="/" className="inline-block px-2 py-1 rounded nav-item text-rainbow font-bold">vontor.cz</NavLink>
</div>
<button
aria-label="Toggle navigation"
onClick={() => setOpen(o => !o)}
className="md:hidden p-2 rounded glow focus:outline-none"
>
{open ? <FaTimes /> : <FaBars />}
</button>
<ul className={`md:flex md:items-center md:gap-8 md:static absolute left-0 w-full md:w-auto transition-all duration-300 ${open ? 'top-16 bg-slate-800/95 pb-6 rounded-lg backdrop-blur-md' : 'top-[-500px]'}`}>
<li className="flex"><NavLink to="/" onClick={()=>setOpen(false)} className={linkCls}>Home</NavLink></li>
<li className="flex"><NavLink to="/portfolio" onClick={()=>setOpen(false)} className={linkCls}>Portfolio</NavLink></li>
<li className="flex"><NavLink to="/skills" onClick={()=>setOpen(false)} className={linkCls}>Skills</NavLink></li>
<li className="flex"><NavLink to="/hosting-security" onClick={()=>setOpen(false)} className={linkCls}>Hosting & Security</NavLink></li>
<li className="flex"><NavLink to="/donate" onClick={()=>setOpen(false)} className={linkCls}>Donate / Shop</NavLink></li>
<li className="flex"><NavLink to="/contact" onClick={()=>setOpen(false)} className={linkCls}>Contact</NavLink></li>
<li className="relative">
<button
type="button"
onClick={() => setServicesOpen(s => !s)}
className={`nav-item px-3 py-2 flex items-center gap-1`}
aria-haspopup="true" aria-expanded={servicesOpen}
>
More <FaChevronDown className={`transition-transform ${servicesOpen ? 'rotate-180' : ''}`} />
</button>
{/* Mobile inline dropdown */}
<div className={`md:hidden w-full mt-2 ${servicesOpen ? 'block' : 'hidden'}`}>
<ul className="space-y-2 text-sm glass p-4">
<li><a href="#live" className={`${dropdownCls} nav-item`}>Live Performance</a></li>
<li><a href="#shop" className={`${dropdownCls} nav-item`}>Support Journey</a></li>
<li><a href="#portfolio" className={`${dropdownCls} nav-item`}>Featured Work</a></li>
</ul>
</div>
</li>
</ul>
{/* Desktop offset dropdown anchored to right under nav */}
{servicesOpen && (
<div className="hidden md:block absolute top-full right-4 translate-y-2 min-w-56 glass p-4 shadow-xl">
<ul className="space-y-2 text-sm">
<li><a href="#live" className={`${dropdownCls} nav-item`}>Live Performance</a></li>
<li><a href="#shop" className={`${dropdownCls} nav-item`}>Support Journey</a></li>
<li><a href="#portfolio" className={`${dropdownCls} nav-item`}>Featured Work</a></li>
</ul>
</div>
)}
</nav>
</header>
);
}
const linkCls = ({ isActive }: { isActive: boolean }) => `nav-item px-3 py-2 rounded transition-colors ${isActive ? 'active text-[var(--c-other)] font-semibold' : 'hover:text-[var(--c-other)]'}`;
const dropdownCls = "block px-2 py-1 rounded hover:bg-[color-mix(in_hsl,var(--c-other),transparent_85%)]";

View File

@@ -0,0 +1,79 @@
import { useState } from "react";
import Modal from "../common/Modal";
type Project = {
id: string;
title: string;
image: string; // public/ path
description: string;
link?: string;
};
const projects: Project[] = [
{
id: "perlica",
title: "Perlica",
image: "/portfolio/perlica.png",
description: "E-commerce redesign with modern UI and Django backend integration.",
link: "#",
},
{
id: "epinger",
title: "Epinger",
image: "/portfolio/epinger.png",
description: "Landing page with responsive layout and animation system.",
link: "#",
},
{
id: "davo1",
title: "Davo",
image: "/portfolio/davo1.png",
description: "Portfolio template and component library built with Vite + Tailwind.",
link: "#",
},
];
export default function PortfolioGrid() {
const [active, setActive] = useState<Project | null>(null);
return (
<section id="portfolio" className="section">
<div className="container">
<h2 className="text-2xl md:text-3xl font-bold mb-2 text-rainbow">My Work</h2>
<p className="text-[var(--c-lines)] mb-6 max-w-2xl">Selected projects combining engineering, design systems, performance optimization and infrastructure. Click a tile for details.</p>
<div className="grid gap-6 md:grid-cols-3">
{projects.map((p) => (
<button
key={p.id}
className="card overflow-hidden text-left"
onClick={() => setActive(p)}
>
<div className="aspect-[16/10] w-full overflow-hidden">
<img src={p.image} alt={p.title} className="w-full h-full object-cover hover:scale-105 transition-transform" />
</div>
<div className="p-4">
<h3 className="text-lg font-semibold tracking-wide text-rainbow">{p.title}</h3>
<p className="text-xs text-[var(--c-lines)] mt-1 uppercase">View details </p>
</div>
</button>
))}
</div>
</div>
<Modal open={!!active} onClose={() => setActive(null)} title={active?.title}>
<div className="space-y-3">
{active && (
<>
<img src={active.image} alt={active.title} className="w-full rounded" />
<p>{active.description}</p>
{active.link && (
<a href={active.link} target="_blank" rel="noreferrer" className="inline-block px-4 py-2 glow border rounded">
Visit project
</a>
)}
</>
)}
</div>
</Modal>
</section>
);
}

View File

@@ -0,0 +1,35 @@
const categories: { name: string; items: string[] }[] = [
{ name: 'Experience', items: ['Freelance projects', 'Collaborations', 'Open-source contributions'] },
{ name: 'Backend', items: ['Django', 'Python', 'REST API', 'PostgreSQL', 'Celery', 'Docker'] },
{ name: 'Frontend', items: ['React', 'Tailwind', 'Vite', 'TypeScript', 'ShadCN', 'Bootstrap'] },
{ name: 'DevOps / Hosting', items: ['Nginx', 'Docker Compose', 'SSL', 'Cloudflare', 'Self-hosting'] },
{ name: 'Other Tools', items: ['Git', 'VSCode', 'WebRTC', 'ESP32', 'Automation'] },
];
export default function SkillsSection() {
return (
<section id="skills" className="py-20 bg-gradient-to-b from-slate-900 to-slate-800">
<div className="container mx-auto px-4">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold mb-4 text-rainbow">Skills</h2>
<p className="text-gray-300 max-w-2xl mx-auto">Core technologies and tools I use across backend, frontend, infrastructure and hardware.</p>
</div>
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{categories.map(cat => (
<div key={cat.name} className="card p-6">
<h3 className="font-semibold mb-4 text-rainbow tracking-wide">{cat.name}</h3>
<ul className="space-y-2 text-sm">
{cat.items.map(i => (
<li key={i} className="flex items-center gap-2 text-gray-300">
<span className="inline-block w-2 h-2 rounded-full bg-fuchsia-600" />
<span className="group-hover:text-white transition-colors">{i}</span>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,15 @@
export default function TradingGraph() {
return (
<section id="live" className="py-20 bg-gradient-to-b from-slate-900 to-slate-800">
<div className="container mx-auto px-4">
<h2 className="text-3xl md:text-4xl font-bold mb-6 text-rainbow">Live Performance</h2>
<div className="card p-4 md:p-6">
<div className="mb-3 text-sm text-gray-400">Trading212 graph placeholder</div>
<div className="aspect-[16/9] w-full rounded border border-slate-700 bg-black/40 grid place-items-center">
<span className="text-gray-400">Graph will appear here</span>
</div>
</div>
</div>
</section>
);
}

View File

@@ -1,4 +1,4 @@
import React, { createContext, useState, useContext, useEffect } from 'react';
import React, { createContext, useState, useEffect } from 'react';
import userAPI from '../api/models/User';

159
frontend/src/index.css Normal file
View File

@@ -0,0 +1,159 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
@import "tailwindcss";
/* Theme variables */
: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; /* accent */
color-scheme: dark;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji";
color: var(--c-text);
background: radial-gradient(1200px 800px at 10% 10%, var(--c-background-light), transparent 60%),
radial-gradient(1000px 700px at 90% 20%, rgba(135,169,218,0.15), transparent 60%),
linear-gradient(180deg, var(--c-background) 0%, #02162f 100%);
}
a {
color: inherit;
text-decoration: none;
}
h1, h2, h3 {
line-height: 1.2;
}
button {
border-radius: 0.75rem;
border: 1px solid color-mix(in hsl, var(--c-lines), transparent 60%);
padding: 0.6em 1.1em;
font: inherit;
background-color: color-mix(in hsl, var(--c-background-light), black 15%);
color: var(--c-text);
cursor: pointer;
transition: transform .15s ease, box-shadow .2s ease, border-color .2s ease;
}
button:hover {
border-color: var(--c-other);
box-shadow: 0 0 0.5rem color-mix(in hsl, var(--c-other), transparent 60%);
}
button:active { transform: translateY(1px) }
/* Reusable helpers */
.glass {
background: linear-gradient(180deg, color-mix(in hsl, var(--c-background-light), transparent 30%), color-mix(in hsl, black, transparent 70%));
/* no border for a modern look */
border: none;
box-shadow: 0 0 1.2rem rgba(3, 29, 68, 0.35);
border-radius: 1rem;
}
.glow:hover {
box-shadow: 0 0 0.8rem color-mix(in hsl, var(--c-other), transparent 50%);
}
.section {
padding: 4rem 1rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.divider {
height: 1px;
background: linear-gradient(90deg, transparent, var(--c-lines), transparent);
margin: 2rem 0;
}
/* Nav underline animation */
.nav-item {
position: relative;
}
.nav-item::after {
content: "";
position: absolute;
left: 0;
bottom: -2px;
width: 0;
height: 2px;
background: var(--c-other);
transition: width .2s ease;
}
.nav-item:hover::after,
.nav-item.active::after {
width: 100%;
}
.nav-item:focus-visible {
outline: 2px solid color-mix(in hsl, var(--c-other), transparent 20%);
outline-offset: 2px;
}
/* Rainbow text using project palette */
.text-rainbow {
background: linear-gradient(90deg, var(--c-other), var(--c-lines), var(--c-boxes));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
/* Card hover/animation */
.card {
background: color-mix(in hsl, var(--c-background-light), transparent 15%);
border-radius: 1rem;
box-shadow: 0 10px 30px rgba(0,0,0,0.25);
transition: transform .25s ease, box-shadow .25s ease, background .25s ease;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 16px 40px color-mix(in hsl, var(--c-other), transparent 70%);
background: color-mix(in hsl, var(--c-background-light), transparent 5%);
}
@media (prefers-reduced-motion: reduce) {
.card { transition: none }
.card:hover { transform: none }
}
/* Scroll-reveal animations (applied to .section and [data-reveal]) */
.section,
[data-reveal] {
opacity: 0;
transform: translateY(16px);
will-change: opacity, transform;
transition: opacity .6s ease, transform .6s ease;
}
.reveal-visible {
opacity: 1;
transform: none;
}
@media (prefers-reduced-motion: reduce) {
.section,
[data-reveal] {
opacity: 1;
transform: none;
transition: none;
}
}

View File

@@ -0,0 +1,13 @@
import Footer from "../components/Footer/footer";
import { Outlet } from "react-router";
import SiteNav from "../components/navbar/SiteNav";
export default function HomeLayout(){
return(
<>
<SiteNav />
<Outlet />
<Footer />
</>
)
}

77
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,77 @@
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>,
)
// Lightweight scroll-reveal setup for elements with `.section` or `[data-reveal]`.
// Respects prefers-reduced-motion and observes dynamically added nodes.
function setupReveal() {
const w = window as unknown as { __revealSetupDone?: boolean }
if (w.__revealSetupDone) return
w.__revealSetupDone = true
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
const selector = '.section, [data-reveal]'
// If reduced motion is preferred, mark everything visible and skip observers
if (prefersReduced) {
document.querySelectorAll(selector).forEach((el) => el.classList.add('reveal-visible'))
return
}
const seen = new WeakSet<Element>()
const io = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.classList.add('reveal-visible')
io.unobserve(entry.target)
}
}
},
{ threshold: 0.12, rootMargin: '0px 0px -10% 0px' },
)
const observeExisting = () => {
document.querySelectorAll(selector).forEach((el) => {
if (!seen.has(el)) {
seen.add(el)
io.observe(el)
}
})
}
// Observe current DOM
observeExisting()
// Observe future DOM mutations (e.g., route changes render new sections)
const mo = new MutationObserver((mutations) => {
let needsScan = false
for (const m of mutations) {
if (m.type === 'childList' && (m.addedNodes?.length ?? 0) > 0) {
needsScan = true
}
if (needsScan) break
}
if (needsScan) observeExisting()
})
mo.observe(document.documentElement, { childList: true, subtree: true })
// Cleanup on HMR disposal (Vite dev) to avoid duplicate observers
if (import.meta && import.meta.hot) {
import.meta.hot.dispose(() => {
io.disconnect()
mo.disconnect()
w.__revealSetupDone = false
})
}
}
// Initialize after first render tick
queueMicrotask(setupReveal)

View File

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

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

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

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

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

@@ -0,0 +1,14 @@
export default function ContactPage(){
return (
<section className="section">
<div className="container">
<h2 className="text-2xl md:text-3xl font-bold mb-4">Get in Touch</h2>
<p className="text-[var(--c-lines)] mb-6">Reach out via email or phone. Social links are in the footer.</p>
<div className="glass p-6 max-w-lg">
<p><strong>Email:</strong> <a href="mailto:brunovontor@gmail.com" className="hover:text-[var(--c-other)]">brunovontor@gmail.com</a></p>
<p className="mt-2"><strong>Phone:</strong> <a href="tel:+420605512624" className="hover:text-[var(--c-other)]">+420 605 512 624</a></p>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,4 @@
import DonationShop from "../../components/donate/DonationShop";
export default function DonateShopPage(){
return <DonationShop />;
}

Some files were not shown because too many files have changed in this diff Show More