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": {
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
||||
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2"
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
@@ -3154,11 +3154,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/fdir": {
|
||||
"version": "6.4.6",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
||||
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
@@ -3300,18 +3303,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz",
|
||||
"integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==",
|
||||
"version": "7.1.7",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz",
|
||||
"integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.6",
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"rollup": "^4.43.0",
|
||||
"tinyglobby": "^0.2.14"
|
||||
"tinyglobby": "^0.2.15"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
@@ -3375,11 +3378,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/fdir": {
|
||||
"version": "6.4.6",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
||||
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { useState } from 'react'
|
||||
import './App.css'
|
||||
import { BrowserRouter as Router, Routes, Route, Link, Outlet } from "react-router-dom"
|
||||
import Home from "./pages/home/home";
|
||||
import HomeLayout from "./layouts/HomeLayout";
|
||||
|
||||
|
||||
function App() {
|
||||
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%;
|
||||
width: 100%;
|
||||
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 {
|
||||
width: fit-content;
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
z-index: 0;
|
||||
top: -4.7em;
|
||||
left: 0;
|
||||
padding: 1em 3em;
|
||||
padding: 0 3em;
|
||||
padding-bottom: 0;
|
||||
background-color: #cdc19c;
|
||||
color: #5e5747;
|
||||
@@ -112,6 +112,7 @@
|
||||
|
||||
|
||||
.portfolio div {
|
||||
width: 100%;
|
||||
padding: 3em;
|
||||
background-color: #cdc19c;
|
||||
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,44 +34,9 @@ body{
|
||||
font-weight: 400;
|
||||
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;
|
||||
flex-direction: column;
|
||||
color: var(--c-text);
|
||||
@@ -85,6 +50,7 @@ footer .contacts{
|
||||
|
||||
/* gap: 4em;*/
|
||||
}
|
||||
|
||||
.introduction h1{
|
||||
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