upgrade
@@ -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>
|
||||
4267
absolete_frontend/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>,
|
||||
)
|
||||
@@ -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 (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
],
|
||||
})
|
||||
@@ -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:
|
||||
@@ -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,9 +15,6 @@ export default defineConfig([
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
|
||||
@@ -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 self‑hosted 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="Full‑stack 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>
|
||||
5377
frontend/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
45
frontend/src/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 823 B After Width: | Height: | Size: 823 B |
|
Before Width: | Height: | Size: 705 B After Width: | Height: | Size: 705 B |
|
Before Width: | Height: | Size: 366 B After Width: | Height: | Size: 366 B |
|
Before Width: | Height: | Size: 722 B After Width: | Height: | Size: 722 B |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
39
frontend/src/components/Footer/footer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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() {
|
||||
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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() {
|
||||
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
33
frontend/src/components/common/Modal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
10
frontend/src/components/common/ScrollToTop.tsx
Normal 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;
|
||||
}
|
||||
34
frontend/src/components/donate/DonationShop.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
62
frontend/src/components/hero/HeroCarousel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
18
frontend/src/components/hosting/HostingSecuritySection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
73
frontend/src/components/navbar/SiteNav.tsx
Normal 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%)]";
|
||||
79
frontend/src/components/portfolio/PortfolioGrid.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
35
frontend/src/components/skills/SkillsSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
15
frontend/src/components/trading/TradingGraph.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
13
frontend/src/layouts/HomeLayout.tsx
Normal 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
@@ -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)
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
14
frontend/src/pages/contact/ContactPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
4
frontend/src/pages/donate/DonateShopPage.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
import DonationShop from "../../components/donate/DonationShop";
|
||||
export default function DonateShopPage(){
|
||||
return <DonationShop />;
|
||||
}
|
||||