fixed components
This commit is contained in:
58
.github/copilot-instructions.md
vendored
Normal file
58
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Copilot Instructions for Vontor CZ
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This monorepo contains a Django backend and a Vite/React frontend, orchestrated via Docker Compose. The project is designed for a Czech e-marketplace, with custom payment integrations and real-time features.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
- **backend/**: Django project (`vontor_cz`), custom `account` app, and third-party payment integrations (`thirdparty/`).
|
||||||
|
- Uses Django REST Framework, Channels (ASGI), Celery, and S3/static/media via `django-storages`.
|
||||||
|
- Custom user model: `account.CustomUser`.
|
||||||
|
- API docs: DRF Spectacular (`/api/schema/`).
|
||||||
|
- **frontend/**: Vite + React + TypeScript app.
|
||||||
|
- Organized by `src/api/`, `components/`, `features/`, `layouts/`, `pages/`, `routes/`.
|
||||||
|
- Uses React Router layouts and nested routes (see `src/layouts/`, `src/routes/`).
|
||||||
|
- **docker-compose.yml**: Orchestrates backend, frontend, Redis, and Postgres for local/dev.
|
||||||
|
|
||||||
|
## Developer Workflows
|
||||||
|
- **Backend**
|
||||||
|
- Local dev: `python manage.py runserver` (or use Docker Compose)
|
||||||
|
- Migrations: `python manage.py makemigrations && python manage.py migrate`
|
||||||
|
- Celery: `celery -A vontor_cz worker -l info`
|
||||||
|
- Channels: Daphne/ASGI (see Docker Compose command)
|
||||||
|
- Env config: `.env` files in `backend/` (see `.gitignore` for secrets)
|
||||||
|
- **Frontend**
|
||||||
|
- Install: `npm install`
|
||||||
|
- Dev server: `npm run dev`
|
||||||
|
- Build: `npm run build`
|
||||||
|
- Preview: `npm run preview`
|
||||||
|
- Static assets: `src/assets/` (import in JS/CSS), `public/` (referenced in HTML)
|
||||||
|
|
||||||
|
## Conventions & Patterns
|
||||||
|
- **Backend**
|
||||||
|
- Use environment variables for secrets and config (see `settings.py`).
|
||||||
|
- Static/media files: S3 in production, local in dev (see `settings.py`).
|
||||||
|
- API versioning and docs: DRF Spectacular config in `settings.py`.
|
||||||
|
- Custom permissions, filters, and serializers in each app.
|
||||||
|
- **Frontend**
|
||||||
|
- Use React Router layouts for shared UI (see `src/layouts/`, `LAYOUTS.md`).
|
||||||
|
- API calls and JWT handling in `src/api/`.
|
||||||
|
- Route definitions and guards in `src/routes/` (`ROUTES.md`).
|
||||||
|
- Use TypeScript strict mode (see `tsconfig.*.json`).
|
||||||
|
- Linting: ESLint config in `eslint.config.js`.
|
||||||
|
|
||||||
|
## Integration Points
|
||||||
|
- **Payments**: `thirdparty/` contains custom integrations for Stripe, GoPay, Trading212.
|
||||||
|
- **Real-time**: Django Channels (ASGI, Redis) for websockets.
|
||||||
|
- **Task queue**: Celery + Redis for async/background jobs.
|
||||||
|
- **API**: REST endpoints, JWT auth, API key support.
|
||||||
|
|
||||||
|
## References
|
||||||
|
- [frontend/REACT.md](../frontend/REACT.md): Frontend structure, workflows, and conventions.
|
||||||
|
- [frontend/src/layouts/LAYOUTS.md](../frontend/src/layouts/LAYOUTS.md): Layout/component patterns.
|
||||||
|
- [frontend/src/routes/ROUTES.md](../frontend/src/routes/ROUTES.md): Routing conventions.
|
||||||
|
- [backend/vontor_cz/settings.py](../backend/vontor_cz/settings.py): All backend config, env, and integration details.
|
||||||
|
- [docker-compose.yml](../docker-compose.yml): Service orchestration and dev workflow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**When in doubt, check the referenced markdown files and `settings.py` for project-specific logic and patterns.**
|
||||||
38
frontend/package-lock.json
generated
38
frontend/package-lock.json
generated
@@ -3137,14 +3137,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.14",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
|
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.5.0",
|
||||||
"picomatch": "^4.0.2"
|
"picomatch": "^4.0.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
@@ -3154,11 +3154,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby/node_modules/fdir": {
|
"node_modules/tinyglobby/node_modules/fdir": {
|
||||||
"version": "6.4.6",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"picomatch": "^3 || ^4"
|
"picomatch": "^3 || ^4"
|
||||||
},
|
},
|
||||||
@@ -3300,18 +3303,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz",
|
||||||
"integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==",
|
"integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.6",
|
"fdir": "^6.5.0",
|
||||||
"picomatch": "^4.0.3",
|
"picomatch": "^4.0.3",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"rollup": "^4.43.0",
|
"rollup": "^4.43.0",
|
||||||
"tinyglobby": "^0.2.14"
|
"tinyglobby": "^0.2.15"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
@@ -3375,11 +3378,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite/node_modules/fdir": {
|
"node_modules/vite/node_modules/fdir": {
|
||||||
"version": "6.4.6",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"picomatch": "^3 || ^4"
|
"picomatch": "^3 || ^4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import { useState } from 'react'
|
import { BrowserRouter as Router, Routes, Route, Link, Outlet } from "react-router-dom"
|
||||||
import './App.css'
|
import Home from "./pages/home/home";
|
||||||
|
import HomeLayout from "./layouts/HomeLayout";
|
||||||
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
/* */
|
<Router>
|
||||||
|
<Routes>
|
||||||
|
{/* Layout route */}
|
||||||
|
<Route path="/" element={<HomeLayout />}>
|
||||||
|
<Route index element={<Home />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
<footer id="contacts">
|
|
||||||
<div class="logo">
|
|
||||||
<h1>vontor.cz</h1>
|
|
||||||
</div>
|
|
||||||
<address>
|
|
||||||
Written by <b>David Bruno Vontor</b><br>
|
|
||||||
<p>Tel.: <a href="tel:+420 605 512 624"><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;"><u>21613109</u></a></p>
|
|
||||||
</address>
|
|
||||||
<div class="contacts">
|
|
||||||
<a href="https://github.com/Brunobrno">
|
|
||||||
<i class="fa fa-github"></i>
|
|
||||||
</a>
|
|
||||||
<a href="https://www.instagram.com/brunovontor/">
|
|
||||||
<i class="fa fa-instagram"></i>
|
|
||||||
</a>
|
|
||||||
<a href="https://twitter.com/BVontor">
|
|
||||||
<i class="fa-brands fa-x-twitter"></i>
|
|
||||||
</a>
|
|
||||||
<a href="https://steamcommunity.com/id/Brunobrno/">
|
|
||||||
<i class="fa-brands fa-steam"></i>
|
|
||||||
</a>
|
|
||||||
<a href="www.youtube.com/@brunovontor">
|
|
||||||
<i class="fa-brands fa-youtube"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
36
frontend/src/components/Footer/footer.module.css
Normal file
36
frontend/src/components/Footer/footer.module.css
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
footer a{
|
||||||
|
color: var(--c-text);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
footer a i{
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
footer{
|
||||||
|
font-family: "Roboto Mono", monospace;
|
||||||
|
|
||||||
|
background-color: var(--c-boxes);
|
||||||
|
|
||||||
|
margin-top: 2em;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
footer address{
|
||||||
|
padding: 1em;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
footer .contacts{
|
||||||
|
font-size: 2em;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 990px){
|
||||||
|
footer{
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
padding-top: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import styles from "./footer.module.css"
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
return (
|
||||||
|
<footer id="contacts">
|
||||||
|
<div>
|
||||||
|
<h1>vontor.cz</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<address>
|
||||||
|
Written by <b>David Bruno Vontor | © 2025</b>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
Tel.:{" "}
|
||||||
|
<a href="tel:+420605512624">
|
||||||
|
<u>+420 605 512 624</u>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
E-mail:{" "}
|
||||||
|
<a href="mailto:brunovontor@gmail.com">
|
||||||
|
<u>brunovontor@gmail.com</u>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
IČO:{" "}
|
||||||
|
<a
|
||||||
|
href="https://www.rzp.cz/verejne-udaje/cs/udaje/vyber-subjektu;ico=21613109;"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<u>21613109</u>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</address>
|
||||||
|
|
||||||
|
<div className="contacts">
|
||||||
|
<a
|
||||||
|
href="https://github.com/Brunobrno"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<i className="fa fa-github"></i>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://www.instagram.com/brunovontor/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<i className="fa fa-instagram"></i>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://twitter.com/BVontor"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<i className="fa-brands fa-x-twitter"></i>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://steamcommunity.com/id/Brunobrno/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<i className="fa-brands fa-steam"></i>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://www.youtube.com/@brunovontor"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<i className="fa-brands fa-youtube"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import React, { useState } from "react"
|
||||||
|
import styles from "./contact-me.module.css"
|
||||||
|
|
||||||
|
interface ContactFormResponse {
|
||||||
|
success: boolean
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ContactMeForm() {
|
||||||
|
const [opened, setOpened] = useState(false)
|
||||||
|
const [formData, setFormData] = useState({ message: "", email: "" })
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [success, setSuccess] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const toggleOpen = () => {
|
||||||
|
setOpened((prev) => !prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||||
|
) => {
|
||||||
|
const { name, value } = e.target
|
||||||
|
setFormData((prev) => ({ ...prev, [name]: value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/submit-contactme/", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-CSRFToken": getCSRFToken(),
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data: ContactFormResponse = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setSuccess(true)
|
||||||
|
setFormData({ message: "", email: "" })
|
||||||
|
} else {
|
||||||
|
setError("Zpráva nebyla odeslaná.")
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError("Chyba připojení nebo serveru.")
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility to read CSRF token from cookies (Django)
|
||||||
|
const getCSRFToken = (): string => {
|
||||||
|
const match = document.cookie.match(/csrftoken=([^;]+)/)
|
||||||
|
return match ? match[1] : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles["contact-me"]}>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
opened
|
||||||
|
? `${styles.opening} ${styles["rotate-opening"] || ""}`
|
||||||
|
: styles.opening
|
||||||
|
}
|
||||||
|
onClick={toggleOpen}
|
||||||
|
>
|
||||||
|
<i className="fa-solid fa-arrow-pointer" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
opened
|
||||||
|
? `${styles.content} ${styles["content-moveup"] || ""}`
|
||||||
|
: styles.content
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
placeholder="Váš email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
name="message"
|
||||||
|
placeholder="Vaše zpráva"
|
||||||
|
value={formData.message}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input type="submit" value={loading ? "Odesílám..." : "Odeslat"} />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<div className={styles.successFormAlert}>
|
||||||
|
<span>Zpráva odeslaná!</span>
|
||||||
|
<button onClick={() => setSuccess(false)}>×</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{error && <div className={styles.errorFormAlert}>{error}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.cover}></div>
|
||||||
|
<div className={styles.triangle}></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
36
frontend/src/components/navbar/HomeNav.tsx
Normal file
36
frontend/src/components/navbar/HomeNav.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import React, { useState } from "react"
|
||||||
|
import styles from "./HomeNav.module.css"
|
||||||
|
|
||||||
|
export default function HomeNav() {
|
||||||
|
const [navOpen, setNavOpen] = useState(false)
|
||||||
|
|
||||||
|
const toggleNav = () => setNavOpen((prev) => !prev)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className={styles.nav}>
|
||||||
|
<i
|
||||||
|
id="toggle-nav"
|
||||||
|
className={`fa-solid fa-bars ${styles.toggle}`}
|
||||||
|
onClick={toggleNav}
|
||||||
|
></i>
|
||||||
|
|
||||||
|
<ul className={`${styles.navList} ${navOpen ? styles.open : ""}`}>
|
||||||
|
<li id="nav-logo" className={styles.logo}>
|
||||||
|
<span>vontor.cz</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/">Home</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#portfolio">Portfolio</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#services">Services</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#contactme-form">Contact me</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<nav>
|
|
||||||
<i id="toggle-nav" class="fa-solid fa-bars"></i>
|
|
||||||
<ul>
|
|
||||||
<li id="nav-logo"><span>vontor.cz</span></li>
|
|
||||||
<li><a href="{% url "home" %}">Home</a></li>
|
|
||||||
<li><a href="#portfolio">Portfolio</a></li>
|
|
||||||
<li><a href="#services">Services</a></li>
|
|
||||||
<li><a href="#contactme-form">Contact me</a></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
{% load static %}
|
|
||||||
<div class="drone only-desktop">
|
|
||||||
|
|
||||||
<video id="drone-video" class="video-background" autoplay muted loop playsinline>
|
|
||||||
<source id="video-source" type="video/mp4">
|
|
||||||
Your browser does not support video.
|
|
||||||
</video>
|
|
||||||
|
|
||||||
<article>
|
|
||||||
<header>
|
|
||||||
<h1>Letecké snímky dronem</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<section>
|
|
||||||
<h2>Opravnění</h2>
|
|
||||||
|
|
||||||
|
|
||||||
A1, A2, A3 a průkaz na vysílačku!
|
|
||||||
|
|
||||||
Mohu garantovat bezpečný provoz dronu i ve složitějších podmínkách. Mám také možnost žádat o povolení k letu v blízkosti letišť!
|
|
||||||
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h2>Cena</h2>
|
|
||||||
|
|
||||||
Nabízím letecké záběry dronem <br>za cenu <u>3 000 Kč</u>.
|
|
||||||
|
|
||||||
Pokud se nacházíte v Ostravě, doprava je zdarma. Pro oblasti mimo Ostravu účtuji 10 Kč/km.
|
|
||||||
|
|
||||||
Cena se může odvíjet ještě podle složitosti získaní povolení.*
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h2>Výstup</h2>
|
|
||||||
|
|
||||||
Rád Vám připravím jednoduchý sestřih videa, který můžete rychle použít, nebo Vám mohu poskytnout samotné záběry k vlastní editaci. <br>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<div>
|
|
||||||
V případě zájmu mě neváhejte<br><a href="#contacts">kontaktovat!</a>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<script src="{% static 'home/js/drone.js' %}"></script>
|
|
||||||
</div>
|
|
||||||
<!--<button id="debug-drone">force reload</button>-->
|
|
||||||
|
|
||||||
|
|
||||||
drone.js:
|
|
||||||
|
|
||||||
$(document).ready(function () {
|
|
||||||
function setVideoDroneQuality() {
|
|
||||||
$sourceElement = $("#video-source");
|
|
||||||
|
|
||||||
const videoSources = {
|
|
||||||
fullHD: 'static/home/video/drone-background-video-1080p.mp4', // For desktops (1920x1080)
|
|
||||||
hd: 'static/home/video/drone-background-video-720p.mp4', // For tablets/smaller screens (1280x720)
|
|
||||||
lowRes: 'static/home/video/drone-background-video-480p.mp4' // For mobile devices or low performance (854x480)
|
|
||||||
};
|
|
||||||
|
|
||||||
const screenWidth = $(window).width(); // Get screen width
|
|
||||||
|
|
||||||
// Determine the appropriate video source
|
|
||||||
if (screenWidth >= 1920) {
|
|
||||||
$sourceElement.attr('src', "https://vontor-cz.s3.eu-central-1.amazonaws.com/" + videoSources.fullHD);
|
|
||||||
} else if (screenWidth >= 1280) {
|
|
||||||
$sourceElement.attr('src', "https://vontor-cz.s3.eu-central-1.amazonaws.com/" + videoSources.hd);
|
|
||||||
} else {
|
|
||||||
$sourceElement.attr('src', "https://vontor-cz.s3.eu-central-1.amazonaws.com/" + videoSources.lowRes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reload the video
|
|
||||||
$('#drone-video')[0].load();
|
|
||||||
|
|
||||||
console.log("video set!");
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(1000);
|
|
||||||
|
|
||||||
setVideoDroneQuality();
|
|
||||||
//$("#debug-drone").click(setVideoDroneQuality);
|
|
||||||
});
|
|
||||||
108
frontend/src/features/ads/Drone/Drone.tsx
Normal file
108
frontend/src/features/ads/Drone/Drone.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import React, { useEffect, useRef } from "react"
|
||||||
|
import styles from "./drone.module.css"
|
||||||
|
|
||||||
|
export default function Drone() {
|
||||||
|
const videoRef = useRef<HTMLVideoElement | null>(null)
|
||||||
|
const sourceRef = useRef<HTMLSourceElement | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function setVideoDroneQuality() {
|
||||||
|
if (!sourceRef.current || !videoRef.current) return
|
||||||
|
|
||||||
|
const videoSources = {
|
||||||
|
fullHD: "static/home/video/drone-background-video-1080p.mp4", // For desktops (1920x1080)
|
||||||
|
hd: "static/home/video/drone-background-video-720p.mp4", // For tablets/smaller screens (1280x720)
|
||||||
|
lowRes: "static/home/video/drone-background-video-480p.mp4" // For mobile devices or low performance (854x480)
|
||||||
|
}
|
||||||
|
|
||||||
|
const screenWidth = window.innerWidth
|
||||||
|
|
||||||
|
// Pick appropriate source
|
||||||
|
if (screenWidth >= 1920) {
|
||||||
|
sourceRef.current.src =
|
||||||
|
"https://vontor-cz.s3.eu-central-1.amazonaws.com/" + videoSources.fullHD
|
||||||
|
} else if (screenWidth >= 1280) {
|
||||||
|
sourceRef.current.src =
|
||||||
|
"https://vontor-cz.s3.eu-central-1.amazonaws.com/" + videoSources.hd
|
||||||
|
} else {
|
||||||
|
sourceRef.current.src =
|
||||||
|
"https://vontor-cz.s3.eu-central-1.amazonaws.com/" + videoSources.lowRes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload video
|
||||||
|
videoRef.current.load()
|
||||||
|
console.log("Drone video set!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run once on mount
|
||||||
|
setVideoDroneQuality()
|
||||||
|
|
||||||
|
// Optional: rerun on resize
|
||||||
|
window.addEventListener("resize", setVideoDroneQuality)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", setVideoDroneQuality)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.drone}`}>
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
id="drone-video"
|
||||||
|
className={styles.videoBackground}
|
||||||
|
autoPlay
|
||||||
|
muted
|
||||||
|
loop
|
||||||
|
playsInline
|
||||||
|
>
|
||||||
|
<source ref={sourceRef} id="video-source" type="video/mp4" />
|
||||||
|
Your browser does not support video.
|
||||||
|
</video>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<h1>Letecké snímky dronem</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section>
|
||||||
|
<h2>Oprávnění</h2>
|
||||||
|
<p>
|
||||||
|
A1, A2, A3 a průkaz na vysílačku!
|
||||||
|
<br />
|
||||||
|
Mohu garantovat bezpečný provoz dronu i ve složitějších podmínkách.
|
||||||
|
Mám také možnost žádat o povolení k letu v blízkosti letišť!
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Cena</h2>
|
||||||
|
<p>
|
||||||
|
Nabízím letecké záběry dronem <br />
|
||||||
|
za cenu <u>3 000 Kč</u>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Pokud se nacházíte v Ostravě, doprava je zdarma. Pro oblasti mimo Ostravu účtuji 10 Kč/km.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Cena se může odvíjet ještě podle složitosti získaní povolení.*
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Výstup</h2>
|
||||||
|
<p>
|
||||||
|
Rád Vám připravím jednoduchý sestřih videa, který můžete rychle použít,
|
||||||
|
nebo Vám mohu poskytnout samotné záběry k vlastní editaci.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
V případě zájmu mě neváhejte <br />
|
||||||
|
<a href="#contacts">kontaktovat!</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.drone .video-background {
|
.drone .videoBackground {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
{% load static %}
|
|
||||||
<div class="portfolio" id="portfolio">
|
|
||||||
<header>
|
|
||||||
<h1>Portfolio</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span class="door"><i class="fa-solid fa-arrow-pointer"></i></span>
|
|
||||||
<article>
|
|
||||||
<header>
|
|
||||||
<a href="https://davo1.cz"><img src="{% static 'home\img\portfolio\DAVO_logo_2024_bile.png' %}" alt="davo1.cz logo"></a>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
</main>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<header>
|
|
||||||
<a href="https://perlica.cz"><img src="{% static 'home\img\portfolio\perlica-3.webp' %}" alt="Perlica logo"></a>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
</main>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<header>
|
|
||||||
<a href="http://epinger2.cz"><img src="{% static 'home\img\portfolio\logo_epinger.svg' %}" alt="Epinger2 logo"></a>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
</main>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<script src="{% static 'home/js/portfolio.js' %}"></script>
|
|
||||||
|
|
||||||
|
|
||||||
portfolio.js:
|
|
||||||
|
|
||||||
$(document).ready(function () {
|
|
||||||
var doorOpen= false;
|
|
||||||
|
|
||||||
$(".door").click(function(){
|
|
||||||
doorOpen = !doorOpen;//převrátí hodnotu
|
|
||||||
|
|
||||||
if ($(".door").hasClass('door-open')){
|
|
||||||
$(".door").removeClass('door-open');
|
|
||||||
}else{
|
|
||||||
$(".door").addClass('door-open');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -54,10 +54,10 @@
|
|||||||
.portfolio>header {
|
.portfolio>header {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 5;
|
z-index: 0;
|
||||||
top: -4.7em;
|
top: -4.7em;
|
||||||
left: 0;
|
left: 0;
|
||||||
padding: 1em 3em;
|
padding: 0 3em;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
background-color: #cdc19c;
|
background-color: #cdc19c;
|
||||||
color: #5e5747;
|
color: #5e5747;
|
||||||
@@ -112,6 +112,7 @@
|
|||||||
|
|
||||||
|
|
||||||
.portfolio div {
|
.portfolio div {
|
||||||
|
width: 100%;
|
||||||
padding: 3em;
|
padding: 3em;
|
||||||
background-color: #cdc19c;
|
background-color: #cdc19c;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
65
frontend/src/features/ads/Portfolio/Portfolio.tsx
Normal file
65
frontend/src/features/ads/Portfolio/Portfolio.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import React, { useState } from "react"
|
||||||
|
import styles from "./Portfolio.module.css"
|
||||||
|
|
||||||
|
interface PortfolioItem {
|
||||||
|
href: string
|
||||||
|
src: string
|
||||||
|
alt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const portfolioItems: PortfolioItem[] = [
|
||||||
|
{
|
||||||
|
href: "https://davo1.cz",
|
||||||
|
src: "/home/img/portfolio/DAVO_logo_2024_bile.png",
|
||||||
|
alt: "davo1.cz logo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: "https://perlica.cz",
|
||||||
|
src: "/home/img/portfolio/perlica-3.webp",
|
||||||
|
alt: "Perlica logo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: "http://epinger2.cz",
|
||||||
|
src: "/home/img/portfolio/logo_epinger.svg",
|
||||||
|
alt: "Epinger2 logo",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function Portfolio() {
|
||||||
|
const [doorOpen, setDoorOpen] = useState(false)
|
||||||
|
|
||||||
|
const toggleDoor = () => setDoorOpen((prev) => !prev)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.portfolio} id="portfolio">
|
||||||
|
<header>
|
||||||
|
<h1>Portfolio</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
doorOpen
|
||||||
|
? `${styles.door} ${styles["door-open"]}`
|
||||||
|
: styles.door
|
||||||
|
}
|
||||||
|
onClick={toggleDoor}
|
||||||
|
>
|
||||||
|
<i className="fa-solid fa-arrow-pointer">fix missing font awesome</i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{portfolioItems.map((item, index) => (
|
||||||
|
<article key={index} className={styles.article}>
|
||||||
|
<header>
|
||||||
|
<a href={item.href} target="_blank" rel="noopener noreferrer">
|
||||||
|
<img src={item.src} alt={item.alt} />
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
<main></main>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import Footer from "../components/Footer/footer";
|
||||||
|
import ContactMeForm from "../components/Forms/ContactMe/ContactMeForm";
|
||||||
|
import HomeNav from "../components/navbar/HomeNav";
|
||||||
|
import Drone from "../features/ads/Drone/Drone";
|
||||||
|
import Portfolio from "../features/ads/Portfolio/Portfolio";
|
||||||
|
import Home from "../pages/home/home";
|
||||||
|
|
||||||
|
export default function HomeLayout(){
|
||||||
|
return(
|
||||||
|
<>
|
||||||
|
{/* Example usage of imported components, adjust as needed */}
|
||||||
|
<HomeNav />
|
||||||
|
<Home />
|
||||||
|
<ContactMeForm />
|
||||||
|
<Portfolio />
|
||||||
|
<Drone />
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -34,42 +34,7 @@ body{
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
footer a{
|
|
||||||
color: var(--c-text);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
footer a i{
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
footer{
|
|
||||||
font-family: "Roboto Mono", monospace;
|
|
||||||
|
|
||||||
background-color: var(--c-boxes);
|
|
||||||
|
|
||||||
margin-top: 2em;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
color: white;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
}
|
|
||||||
footer address{
|
|
||||||
padding: 1em;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
footer .contacts{
|
|
||||||
font-size: 2em;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 990px){
|
|
||||||
footer{
|
|
||||||
flex-direction: column;
|
|
||||||
padding-bottom: 1em;
|
|
||||||
padding-top: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.introduction {
|
.introduction {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -85,6 +50,7 @@ footer .contacts{
|
|||||||
|
|
||||||
/* gap: 4em;*/
|
/* gap: 4em;*/
|
||||||
}
|
}
|
||||||
|
|
||||||
.introduction h1{
|
.introduction h1{
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import styles from "./Home.module.css";
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<h1 className={styles.title}>Vítejte na hlavní stránce</h1>
|
|
||||||
<p>Toto je obsah jen pro home page.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$(document).ready(function () {
|
|
||||||
|
|
||||||
$("body").click(function(event){
|
|
||||||
var randomId = "spark-" + Math.floor(Math.random() * 100000);
|
|
||||||
var $spark = $("<div>").addClass("spark-cursor").attr("id", randomId);
|
|
||||||
$("body").append($spark);
|
|
||||||
|
|
||||||
// Nastavení pozice
|
|
||||||
$spark.css({
|
|
||||||
"top": event.pageY + "px",
|
|
||||||
"left": event.pageX + "px",
|
|
||||||
"filter": "hue-rotate(" + Math.random() * 360 + "deg)"
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let index = 0; index < 8; index++) {
|
|
||||||
let $span = $("<span>");
|
|
||||||
$span.css("transform", 'rotate(' + (index * 45) +"deg)" );
|
|
||||||
$spark.append($span);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
$spark.find("span").addClass("animate");
|
|
||||||
}, 10);
|
|
||||||
|
|
||||||
setTimeout(function(){
|
|
||||||
$("#" + randomId).remove();
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
49
frontend/src/pages/home/home.tsx
Normal file
49
frontend/src/pages/home/home.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import React, { useEffect } from "react"
|
||||||
|
import styles from "./Home.module.css"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClick = (event: MouseEvent) => {
|
||||||
|
const randomId = "spark-" + Math.floor(Math.random() * 100000)
|
||||||
|
|
||||||
|
const spark = document.createElement("div")
|
||||||
|
spark.className = "spark-cursor"
|
||||||
|
spark.id = randomId
|
||||||
|
document.body.appendChild(spark)
|
||||||
|
|
||||||
|
// pozice a barva
|
||||||
|
spark.style.top = `${event.pageY}px`
|
||||||
|
spark.style.left = `${event.pageX}px`
|
||||||
|
spark.style.filter = `hue-rotate(${Math.random() * 360}deg)`
|
||||||
|
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const span = document.createElement("span")
|
||||||
|
span.style.transform = `rotate(${i * 45}deg)`
|
||||||
|
spark.appendChild(span)
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
spark.querySelectorAll("span").forEach((s) => {
|
||||||
|
(s as HTMLElement).classList.add("animate")
|
||||||
|
})
|
||||||
|
}, 10)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
spark.remove()
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.addEventListener("click", handleClick)
|
||||||
|
|
||||||
|
// cleanup když komponenta zmizí
|
||||||
|
return () => {
|
||||||
|
document.body.removeEventListener("click", handleClick)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<></>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user