Revamp footer, navbar, drone section and routes

- Replace heavy drone assets with S3-hosted video (excluded from git via .gitignore).
- Add routes for /services/web and /services/film in App.tsx.
- Rewrite Footer with framer-motion, structured social/contact/service arrays and improved responsive layout.
- Redesign DroneSection: full-bleed background video, layered panels/vignette, updated copy and styling.
- Minor content change in HeroSection (name updated to "David Bruno Vontor").
- Major Navbar refactor: improved mobile drawer with backdrop, accordion submenus, better state handling and accessibility.
- ChatSidebar: add optional onClose/onSelectChat callbacks, close button.
- ContactMe mobile CSS: tweak sizing and margins for small screens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 00:43:38 +02:00
parent 492e42bfdb
commit 46bc131a56
17 changed files with 1668 additions and 485 deletions

3
.gitignore vendored
View File

@@ -87,3 +87,6 @@ venv
.venv .venv
certs certs
# large video assets — host externally (S3)
frontend/public/portfolio/*.mp4

View File

@@ -6,6 +6,8 @@ import ChatLayout from "./layouts/social/Chat";
import Downloader from "./pages/downloader/Downloader"; import Downloader from "./pages/downloader/Downloader";
import Home from "./pages/home/home"; import Home from "./pages/home/home";
import DroneServisSection from "./pages/home/components/Services/droneServis"; import DroneServisSection from "./pages/home/components/Services/droneServis";
import WebServicePage from "./pages/home/components/Services/webs";
import FilmServicePage from "./pages/home/components/Services/kinematografie";
import PrivateRoute from "./routes/PrivateRoute"; import PrivateRoute from "./routes/PrivateRoute";
import PublicOnlyRoute from "./routes/PublicOnlyRoute"; import PublicOnlyRoute from "./routes/PublicOnlyRoute";
@@ -47,7 +49,8 @@ export default function App() {
<Route path="contact" element={<ContactPage />} /> <Route path="contact" element={<ContactPage />} />
<Route path="apps/downloader" element={<Downloader />} /> <Route path="apps/downloader" element={<Downloader />} />
<Route path="services/drone" element={<DroneServisSection />} /> <Route path="services/drone" element={<DroneServisSection />} />
<Route path="services/web" element={<Downloader />} /> <Route path="services/web" element={<WebServicePage />} />
<Route path="services/film" element={<FilmServicePage />} />
<Route path="test/sounds" element={<RetroSoundTest />} /> <Route path="test/sounds" element={<RetroSoundTest />} />
</Route> </Route>

View File

@@ -147,15 +147,8 @@
@media only screen and (max-width: 990px){ @media only screen and (max-width: 990px){
.contact-me{ .contact-me {
aspect-ratio: unset; width: min(30em, 100%);
margin-top: 3ch; margin: 11rem auto 1.5rem;
background: transparent;
border: none;
} }
.contact-me .opening,
.contact-me .cover,
.contact-me .triangle { display: none; }
.contact-me .content { position: relative; width: 100%; transform: none !important; }
.contact-me .content form { backdrop-filter: none; border: 1px solid var(--c-lines); }
} }

View File

@@ -1,106 +1 @@
footer a{ /* styles migrated to inline — file kept to avoid import errors */
color: var(--c-text);
text-decoration: none;
color: white;
}
footer{
padding: 2em;
font-family: "Roboto Mono", monospace;
background-color: var(--c-boxes);
margin-top: 2em;
display: flex;
color: white;
align-items: flex-start;
justify-content: space-evenly;
}
footer .logo{
font-size: 3em;
transition: all 0.3s ease-in-out;
&:hover{
text-shadow: 0.25em 0.25em 0.2em var(--c-text);
}
}
footer address{
display: flex;
flex-direction: column;
align-items: flex-start;
font-size: 1.2em;
font-style: normal;
gap: 0.2em;
}
footer address h2{
font-size: 1.5em;
}
footer address p{
display: flex;
flex-direction: row;
align-items: center;
gap: 1em;
}
footer address p > svg{
font-size: 1.2em;
}
footer address a{
display: inline-block;
color: var(--c-text);
}
footer .services{
display: flex;
flex-direction: column;
align-items: flex-start;
font-size: 1.2em;
font-style: normal;
gap: 0.2em;
}
footer .services h2{
font-size: 1.5em;
}
footer .services p{
display: flex;
flex-direction: row;
align-items: center;
gap: 1em;
}
footer .services p > svg{
font-size: 1.2em;
}
footer .services a{
display: inline-block;
color: var(--c-text);
}
footer .links{
margin-top: 0.5em;
font-size: 2em;
display: flex;
flex-direction: row;
justify-content: space-around;
gap: 0.8em;
}
footer .links a{
transition: all 0.2s ease-in-out;
&:hover{
transform: scale(1.5);
}
}
@media only screen and (max-width: 990px){
footer{
flex-direction: column;
padding-bottom: 1em;
padding-top: 1em;
gap: 2em;
}
}

View File

@@ -1,55 +1,215 @@
import { FaGitAlt , FaInstagram, FaYoutube, FaLinkedin, FaSteam, FaXTwitter, FaClapperboard } from "react-icons/fa6"; import { motion } from "framer-motion";
import {
FaGitAlt, FaInstagram, FaYoutube, FaLinkedin, FaSteam, FaXTwitter, FaClapperboard,
} from "react-icons/fa6";
import { FaPhoneAlt, FaEnvelope, FaTeamspeak, FaGlobe } from "react-icons/fa"; import { FaPhoneAlt, FaEnvelope, FaTeamspeak, FaGlobe } from "react-icons/fa";
import { GiAutoRepair } from "react-icons/gi"; import { GiAutoRepair } from "react-icons/gi";
import styles from "./footer.module.css"; const SOCIALS = [
{ Icon: FaGitAlt, href: "https://git.vontor.cz/Brunobrno", label: "Gitea" },
{ Icon: FaInstagram, href: "https://www.instagram.com/brunovontor/", label: "Instagram" },
{ Icon: FaXTwitter, href: "https://twitter.com/BVontor", label: "X" },
{ Icon: FaSteam, href: "https://steamcommunity.com/id/Brunobrno/", label: "Steam" },
{ Icon: FaYoutube, href: "https://www.youtube.com/@brunovontor", label: "YouTube" },
{ Icon: FaLinkedin, href: "https://www.linkedin.com/in/brunobrno/?skipRedirect=true", label: "LinkedIn" },
];
const CONTACTS = [
{ Icon: FaPhoneAlt, href: "tel:+420605512624", label: "+420 605 512 624" },
{ Icon: FaEnvelope, href: "mailto:brunovontor@gmail.com", label: "brunovontor@gmail.com" },
{ Icon: FaTeamspeak, href: undefined, label: "teamspeak.vontor.cz:4926" },
];
const SERVICES = [
{ Icon: FaGlobe, href: "/services/web", label: "Weby" },
{ Icon: FaClapperboard, href: "/services/film", label: "Filmařina" },
{ Icon: GiAutoRepair, href: "/services/drone", label: "Servis dronu" },
];
const fade = {
initial: { opacity: 0, y: 24 },
animate: { opacity: 1, y: 0, transition: { duration: 0.6, ease: "easeOut" } },
};
export default function Footer() { export default function Footer() {
return ( return (
<footer id="contacts" className={styles.footer}> <footer id="contacts" style={{
<div className={styles.logo}> position: "relative",
<h1>vontor.cz</h1> background: `
radial-gradient(ellipse 80% 60% at 10% 80%, color-mix(in hsl, var(--c-background-light), transparent 55%), transparent 65%),
linear-gradient(180deg, color-mix(in hsl, var(--c-background), black 10%) 0%, var(--c-background) 100%)
`,
borderTop: "1px solid color-mix(in hsl, var(--c-lines), transparent 75%)",
overflow: "hidden",
}}>
{/* Subtle grid overlay */}
<div style={{
position: "absolute", inset: 0, opacity: 0.03, pointerEvents: "none",
backgroundImage: "linear-gradient(var(--c-lines) 1px, transparent 1px), linear-gradient(90deg, var(--c-lines) 1px, transparent 1px)",
backgroundSize: "60px 60px",
}} />
<div style={{ position: "relative", maxWidth: 1200, margin: "0 auto", padding: "4rem 1.5rem 2rem" }}>
{/* Top grid */}
<motion.div
variants={{ animate: { transition: { staggerChildren: 0.12 } } }}
initial="initial"
whileInView="animate"
viewport={{ once: true }}
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))",
gap: "3rem",
marginBottom: "3rem",
}}
>
{/* Logo + tagline */}
<motion.div variants={fade}>
<h2 style={{
fontSize: "clamp(1.8rem, 4vw, 2.4rem)", fontWeight: 900,
margin: "0 0 0.5rem", letterSpacing: "-0.02em",
background: "linear-gradient(135deg, var(--c-other) 0%, var(--c-lines) 50%, var(--c-text) 100%)",
WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", backgroundClip: "text",
display: "inline-block",
}}>
vontor.cz
</h2>
<p style={{
color: "color-mix(in hsl, var(--c-text), transparent 45%)",
fontSize: "0.88rem", lineHeight: 1.65, marginTop: "0.5rem", maxWidth: 220,
}}>
Weby, filmařina a servis dronů od nápadu po výsledek.
</p>
</motion.div>
{/* Kontakty */}
<motion.div variants={fade}>
<h3 style={{
fontSize: "0.7rem", fontWeight: 700, letterSpacing: "0.12em",
textTransform: "uppercase", color: "var(--c-other)",
margin: "0 0 1.2rem",
}}>
Kontakty
</h3>
<address style={{ fontStyle: "normal", display: "flex", flexDirection: "column", gap: "0.75rem" }}>
{CONTACTS.map(({ Icon, href, label }) => (
<ContactRow key={label} Icon={Icon} href={href} label={label} />
))}
<div style={{
display: "flex", alignItems: "center", gap: "0.75rem",
fontSize: "0.9rem", color: "color-mix(in hsl, var(--c-text), transparent 30%)",
}}>
<span style={{ opacity: 0.6, fontSize: "0.85em", minWidth: 16, textAlign: "center" }}>IČO</span>
<a
href="https://www.rzp.cz/verejne-udaje/cs/udaje/vyber-subjektu;ico=21613109;"
target="_blank" rel="noopener noreferrer"
style={{ color: "var(--c-text)", transition: "color 0.2s" }}
onMouseEnter={e => (e.currentTarget.style.color = "var(--c-other)")}
onMouseLeave={e => (e.currentTarget.style.color = "var(--c-text)")}
>
21613109
</a>
</div>
</address>
</motion.div>
{/* Služby */}
<motion.div variants={fade}>
<h3 style={{
fontSize: "0.7rem", fontWeight: 700, letterSpacing: "0.12em",
textTransform: "uppercase", color: "var(--c-other)",
margin: "0 0 1.2rem",
}}>
Služby
</h3>
<nav style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
{SERVICES.map(({ Icon, href, label }) => (
<ContactRow key={label} Icon={Icon} href={href} label={label} />
))}
</nav>
</motion.div>
</motion.div>
{/* Divider */}
<div style={{
height: 1,
background: "linear-gradient(90deg, transparent, color-mix(in hsl, var(--c-lines), transparent 60%), transparent)",
marginBottom: "1.75rem",
}} />
{/* Bottom row */}
<motion.div
initial={{ opacity: 0 }} whileInView={{ opacity: 1 }} viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.3 }}
style={{
display: "flex", alignItems: "center", justifyContent: "space-between",
flexWrap: "wrap", gap: "1.2rem",
}}
>
<p style={{ margin: 0, fontSize: "0.8rem", color: "color-mix(in hsl, var(--c-text), transparent 55%)" }}>
© {new Date().getFullYear()} vontor.cz David Bruno Vontor
</p>
{/* Social icons */}
<div style={{ display: "flex", gap: "1.2rem", alignItems: "center" }}>
{SOCIALS.map(({ Icon, href, label }) => (
<a
key={label}
href={href}
aria-label={label}
target="_blank" rel="noopener noreferrer"
style={{
display: "flex",
color: "color-mix(in hsl, var(--c-text), transparent 40%)",
fontSize: "1.25rem",
transition: "transform 0.2s ease, color 0.2s ease",
}}
onMouseEnter={e => {
e.currentTarget.style.transform = "scale(1.5)";
e.currentTarget.style.color = "var(--c-other)";
}}
onMouseLeave={e => {
e.currentTarget.style.transform = "scale(1)";
e.currentTarget.style.color = "color-mix(in hsl, var(--c-text), transparent 40%)";
}}
>
<Icon />
</a>
))}
</div>
</motion.div>
</div> </div>
<address>
<h2><b>Kontakty</b></h2>
<p><FaPhoneAlt /><a href="tel:+420 605 512 624"><u>+420 605 512 624</u></a></p>
<p><FaEnvelope /> <a href="mailto:brunovontor@gmail.com"><u>brunovontor@gmail.com</u></a></p>
<p><FaTeamspeak /> teamspeak.vontor.cz:4926</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 className={styles.services}>
<h2><b>Služby</b></h2>
<p><FaGlobe /><a href="/services/web"><u>Weby</u></a></p>
<p><FaClapperboard /> <a href="/services/film"><u>Filmařina</u></a></p>
<p><GiAutoRepair /> <a href="/services/drone-service"><u>Servis dronu</u></a></p>
<p></p>
</div>
<div className={styles.links}>
<a href="https://git.vontor.cz/Brunobrno">
<FaGitAlt />
</a>
<a href="https://www.instagram.com/brunovontor/">
<FaInstagram />
</a>
<a href="https://twitter.com/BVontor">
<FaXTwitter />
</a>
<a href="https://steamcommunity.com/id/Brunobrno/">
<FaSteam />
</a>
<a href="www.youtube.com/@brunovontor">
<FaYoutube />
</a>
<a href="https://www.linkedin.com/in/brunobrno/?skipRedirect=true">
<FaLinkedin />
</a>
</div>
</footer> </footer>
); );
} }
function ContactRow({ Icon, href, label }: { Icon: React.ComponentType; href?: string; label: string }) {
const content = (
<span style={{
display: "flex", alignItems: "center", gap: "0.75rem",
fontSize: "0.9rem",
color: href ? "var(--c-text)" : "color-mix(in hsl, var(--c-text), transparent 30%)",
transition: "color 0.2s",
}}>
<span style={{ opacity: 0.55, fontSize: "0.9em", minWidth: 16, textAlign: "center" }}>
<Icon />
</span>
{label}
</span>
);
if (!href) return <div>{content}</div>;
return (
<a
href={href}
style={{ textDecoration: "none" }}
onMouseEnter={e => { const s = e.currentTarget.querySelector("span") as HTMLElement; if (s) s.style.color = "var(--c-other)"; }}
onMouseLeave={e => { const s = e.currentTarget.querySelector("span") as HTMLElement; if (s) s.style.color = "var(--c-text)"; }}
>
{content}
</a>
);
}

View File

@@ -11,13 +11,6 @@ const fadeLeft = {
viewport: { once: true }, viewport: { once: true },
}; };
const fadeRight = {
initial: { opacity: 0, x: 50 },
whileInView: { opacity: 1, x: 0 },
transition: { duration: 0.7, ease: "easeOut" },
viewport: { once: true },
};
const stagger = { const stagger = {
initial: "hidden", initial: "hidden",
whileInView: "visible", whileInView: "visible",
@@ -37,73 +30,68 @@ const staggerItem = {
export default function DroneSection() { export default function DroneSection() {
return ( return (
<section id="drone" className="section" style={{ background: "color-mix(in hsl, var(--c-background), black 10%)" }}> <section id="drone" className="section" style={{ position: "relative", overflow: "hidden", background: "#030a16" }}>
<div className="container" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "4rem", alignItems: "center" }}>
{/* Left: visual */} {/* Full-bleed background video */}
<motion.div {...fadeLeft} style={{ position: "relative" }}> <video
<div className="glass" style={{ src="https://vontor-cz.s3.eu-central-1.amazonaws.com/media/DroneVideo_web.mp4"
padding: "3.5rem 2rem", autoPlay
display: "flex", loop
flexDirection: "column", muted
alignItems: "center", playsInline
justifyContent: "center", style={{
minHeight: "380px", position: "absolute",
position: "relative", inset: 0,
overflow: "hidden", width: "100%",
}}> height: "100%",
{/* Rotating ring */} objectFit: "cover",
<div className="animate-spin-slow" style={{ zIndex: 0,
position: "absolute", }}
width: "280px", />
height: "280px",
borderRadius: "50%",
border: "2px solid color-mix(in hsl, var(--c-other), transparent 65%)",
borderTopColor: "var(--c-other)",
}} />
<div className="animate-spin-slow" style={{
position: "absolute",
width: "220px",
height: "220px",
borderRadius: "50%",
border: "1px dashed color-mix(in hsl, var(--c-lines), transparent 60%)",
animationDirection: "reverse",
animationDuration: "14s",
}} />
{/* Drone icon */} {/* Base dark scrim */}
<MdFlightTakeoff style={{ fontSize: "6rem", color: "var(--c-text)", position: "relative", zIndex: 1, filter: "drop-shadow(0 0 1.5rem color-mix(in hsl, var(--c-other), transparent 40%))" }} /> <div style={{ position: "absolute", inset: 0, background: "rgba(3,8,22,0.48)", zIndex: 1 }} />
<p style={{ marginTop: "1.5rem", color: "var(--c-lines)", fontSize: "0.9rem", fontWeight: 500, position: "relative", zIndex: 1 }}> {/* Blue diagonal clip-path panel */}
DJI · Sony · Gyroscope Stabilized <div className="drone-blue-panel" style={{
</p> position: "absolute",
inset: 0,
background: "linear-gradient(160deg, color-mix(in hsl, var(--c-other), transparent 30%) 0%, color-mix(in hsl, var(--c-other), black 15%) 100%)",
clipPath: "polygon(0 0, 46% 0, 30% 100%, 0 100%)",
zIndex: 2,
}} />
{/* Play button overlay hint */} {/* Right-side vignette for text readability */}
<div style={{ <div className="drone-vignette" style={{
position: "absolute", position: "absolute",
bottom: "1.2rem", inset: 0,
right: "1.2rem", background: "linear-gradient(to left, rgba(3,8,22,0.60) 0%, transparent 55%)",
display: "flex", zIndex: 2,
alignItems: "center", }} />
gap: "0.5rem",
color: "var(--c-other)", <div className="container" style={{ position: "relative", zIndex: 3, display: "grid", gridTemplateColumns: "1fr 1.4fr", gap: "4rem", alignItems: "center" }}>
fontSize: "0.8rem",
fontWeight: 600, {/* Left: icon floating over blue panel */}
}}> <motion.div {...fadeLeft} className="drone-icon-col" style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: "1.5rem", padding: "2rem 0" }}>
<FaPlay style={{ fontSize: "0.7rem" }} /> Showreel coming soon <MdFlightTakeoff style={{
</div> fontSize: "7.5rem",
</div> color: "#fff",
filter: "drop-shadow(0 0 2.5rem rgba(255,255,255,0.22))",
}} />
<p style={{ color: "rgba(255,255,255,0.78)", fontSize: "0.9rem", fontWeight: 600, textAlign: "center", margin: 0 }}>
DJI · Sony · Gyroscope Stabilized
</p>
</motion.div> </motion.div>
{/* Right: text */} {/* Right: text */}
<motion.div {...stagger} style={{ display: "flex", flexDirection: "column", gap: "1.2rem" }}> <motion.div {...stagger} className="drone-text" style={{ display: "flex", flexDirection: "column", gap: "1.2rem", paddingLeft: "1.5rem" }}>
<motion.div {...staggerItem}> <motion.div {...staggerItem}>
<span style={{ <span style={{
display: "inline-block", display: "inline-block",
padding: "0.3em 0.9em", padding: "0.3em 0.9em",
borderRadius: "9999px", borderRadius: "9999px",
background: "color-mix(in hsl, var(--c-other), transparent 80%)", background: "color-mix(in hsl, var(--c-other), transparent 75%)",
border: "1px solid color-mix(in hsl, var(--c-other), transparent 50%)", border: "1px solid color-mix(in hsl, var(--c-other), transparent 40%)",
color: "var(--c-other)", color: "var(--c-other)",
fontSize: "0.78rem", fontSize: "0.78rem",
fontWeight: 700, fontWeight: 700,
@@ -114,12 +102,12 @@ export default function DroneSection() {
</span> </span>
</motion.div> </motion.div>
<motion.h2 {...staggerItem} style={{ fontSize: "clamp(1.8rem, 4vw, 2.8rem)", fontWeight: 800, lineHeight: 1.15, margin: 0 }}> <motion.h2 {...staggerItem} style={{ fontSize: "clamp(1.8rem, 4vw, 2.8rem)", fontWeight: 800, lineHeight: 1.15, margin: 0, color: "#fff" }}>
Stunning Visuals {" "} Stunning Visuals {" "}
<span className="text-rainbow">Ground to Sky</span> <span className="text-rainbow">Ground to Sky</span>
</motion.h2> </motion.h2>
<motion.p {...staggerItem} style={{ color: "color-mix(in hsl, var(--c-text), transparent 25%)", lineHeight: 1.75, fontSize: "1rem", margin: 0 }}> <motion.p {...staggerItem} style={{ color: "rgba(255,255,255,0.72)", lineHeight: 1.75, fontSize: "1rem", margin: 0 }}>
Professional gyroscope-stabilized camera rigs deliver buttery-smooth footage at ground level. Pair that with DJI drone aerials and you get a complete cinematic package from tracking shots through forests to sweeping panoramas at altitude. Professional gyroscope-stabilized camera rigs deliver buttery-smooth footage at ground level. Pair that with DJI drone aerials and you get a complete cinematic package from tracking shots through forests to sweeping panoramas at altitude.
</motion.p> </motion.p>
@@ -127,7 +115,7 @@ export default function DroneSection() {
<motion.div {...staggerItem} style={{ display: "flex", flexDirection: "column", gap: "0.8rem" }}> <motion.div {...staggerItem} style={{ display: "flex", flexDirection: "column", gap: "0.8rem" }}>
{[ {[
{ icon: <GiFilmProjector />, label: "3-axis gyroscope stabilization", sub: "Cinema-grade smooth ground footage" }, { icon: <GiFilmProjector />, label: "3-axis gyroscope stabilization", sub: "Cinema-grade smooth ground footage" },
{ icon: <MdFlightTakeoff />, label: "Licensed drone operator", sub: "EU A1 · A2 · A3 certified" }, { icon: <MdFlightTakeoff />, label: "Licensed drone operator", sub: "A1 · A2 · A3 certified" },
{ icon: <MdRadio />, label: "Omezený průkaz radiotelefonisty", sub: "Authorized for restricted & controlled airspaces" }, { icon: <MdRadio />, label: "Omezený průkaz radiotelefonisty", sub: "Authorized for restricted & controlled airspaces" },
].map(({ icon, label, sub }) => ( ].map(({ icon, label, sub }) => (
<div key={label} style={{ display: "flex", gap: "1rem", alignItems: "flex-start" }}> <div key={label} style={{ display: "flex", gap: "1rem", alignItems: "flex-start" }}>
@@ -135,8 +123,8 @@ export default function DroneSection() {
width: "2.4rem", width: "2.4rem",
height: "2.4rem", height: "2.4rem",
borderRadius: "0.6rem", borderRadius: "0.6rem",
background: "color-mix(in hsl, var(--c-other), transparent 80%)", background: "color-mix(in hsl, var(--c-other), transparent 75%)",
border: "1px solid color-mix(in hsl, var(--c-other), transparent 55%)", border: "1px solid color-mix(in hsl, var(--c-other), transparent 45%)",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
@@ -147,8 +135,8 @@ export default function DroneSection() {
{icon} {icon}
</div> </div>
<div> <div>
<div style={{ fontWeight: 600, fontSize: "0.95rem" }}>{label}</div> <div style={{ fontWeight: 600, fontSize: "0.95rem", color: "#fff" }}>{label}</div>
<div style={{ color: "color-mix(in hsl, var(--c-text), transparent 45%)", fontSize: "0.85rem" }}>{sub}</div> <div style={{ color: "rgba(255,255,255,0.50)", fontSize: "0.85rem" }}>{sub}</div>
</div> </div>
</div> </div>
))} ))}
@@ -156,15 +144,15 @@ export default function DroneSection() {
{/* Cert badges */} {/* Cert badges */}
<motion.div {...staggerItem} style={{ display: "flex", gap: "0.6rem", flexWrap: "wrap" }}> <motion.div {...staggerItem} style={{ display: "flex", gap: "0.6rem", flexWrap: "wrap" }}>
{["EU A1", "EU A2", "EU A3", "Restricted Airspace"].map((cert) => ( {["A1", "A2", "A3", "Restricted Airspace"].map((cert) => (
<span key={cert} style={{ <span key={cert} style={{
padding: "0.3em 0.8em", padding: "0.3em 0.8em",
borderRadius: "9999px", borderRadius: "9999px",
background: "color-mix(in hsl, var(--c-boxes), transparent 70%)", background: "rgba(255,255,255,0.08)",
border: "1px solid color-mix(in hsl, var(--c-lines), transparent 50%)", border: "1px solid rgba(255,255,255,0.22)",
fontSize: "0.8rem", fontSize: "0.8rem",
fontWeight: 600, fontWeight: 600,
color: "var(--c-lines)", color: "rgba(255,255,255,0.78)",
}}> }}>
{cert} {cert}
</span> </span>
@@ -202,10 +190,29 @@ export default function DroneSection() {
</motion.div> </motion.div>
</div> </div>
{/* Mobile responsive */}
<style>{` <style>{`
@media (max-width: 768px) { @media (max-width: 768px) {
#drone .container { grid-template-columns: 1fr !important; gap: 2rem !important; } #drone .container {
grid-template-columns: 1fr !important;
gap: 0 !important;
}
#drone .drone-icon-col {
display: none !important;
}
#drone .drone-blue-panel {
clip-path: polygon(0 0, 6px 0, 6px 100%, 0 100%) !important;
background: var(--c-other) !important;
opacity: 0.85;
}
#drone .drone-vignette {
display: none;
}
#drone .drone-text {
padding-left: 0 !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
#drone video { object-position: center; }
} }
`}</style> `}</style>
</section> </section>

View File

@@ -122,7 +122,7 @@ export default function HeroSection() {
WebkitTextFillColor: "transparent", WebkitTextFillColor: "transparent",
backgroundClip: "text", backgroundClip: "text",
}}> }}>
Bruno Novotný David Bruno Vontor
</span> </span>
</motion.h1> </motion.h1>

View File

@@ -1,14 +1,8 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useState } from "react";
import { Link, useNavigate, useLocation } from "react-router-dom"; import { Link, useNavigate, useLocation } from "react-router-dom";
import { import {
FaSignOutAlt, FaSignOutAlt, FaSignInAlt, FaBars, FaChevronDown,
FaSignInAlt, FaGlobe, FaWrench, FaUsers, FaTimes,
FaBars,
FaChevronDown,
FaGlobe,
FaWrench,
FaUsers,
FaTimes,
} from "react-icons/fa"; } from "react-icons/fa";
import { FaClapperboard } from "react-icons/fa6"; import { FaClapperboard } from "react-icons/fa6";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
@@ -20,15 +14,13 @@ export default function Navbar() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const handleLogin = () => navigate("/social/login"); const handleLogin = () => navigate("/social/login");
const handleLogout = async () => { const handleLogout = async () => { await logout(); navigate("/"); };
await logout();
navigate("/");
};
const [mobileMenu, setMobileMenu] = useState(false); const [mobileMenu, setMobileMenu] = useState(false);
const [scrolled, setScrolled] = useState(false); const [mobileServicesOpen, setMobileServicesOpen] = useState(false);
const navRef = useRef<HTMLElement | null>(null); const [mobileUserOpen, setMobileUserOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
useEffect(() => { useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 40); const onScroll = () => setScrolled(window.scrollY > 40);
@@ -36,96 +28,171 @@ export default function Navbar() {
return () => window.removeEventListener("scroll", onScroll); return () => window.removeEventListener("scroll", onScroll);
}, []); }, []);
// Close mobile menu on route change
useEffect(() => { useEffect(() => {
setMobileMenu(false); setMobileMenu(false);
setMobileServicesOpen(false);
setMobileUserOpen(false);
}, [location.pathname]); }, [location.pathname]);
useEffect(() => { useEffect(() => {
function onKey(e: KeyboardEvent) { const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setMobileMenu(false); };
if (e.key === "Escape") setMobileMenu(false);
}
window.addEventListener("keydown", onKey); window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey);
}, []); }, []);
return ( useEffect(() => {
<nav document.body.style.overflow = mobileMenu ? "hidden" : "";
className={`${styles.navbar} ${mobileMenu ? styles.mobileNavOpen : ""} ${scrolled ? styles.scrolled : ""}`} return () => { document.body.style.overflow = ""; };
ref={navRef} }, [mobileMenu]);
aria-label="Hlavní navigace"
>
{/* Brand */}
<div className={styles.logo}>
<Link to="/" aria-label="vontor.cz home">vontor.cz</Link>
</div>
{/* Center links */} const closeAll = () => setMobileMenu(false);
<div className={`${styles.links} ${mobileMenu ? styles.show : ""}`} role="menubar">
{/* Služby dropdown */} return (
<div className={styles.dropdownItem}> <>
<button className={styles.linkButton} aria-haspopup="true"> <nav
Služby <FaChevronDown className={styles.chev} /> className={`${styles.navbar} ${scrolled ? styles.scrolled : ""}`}
</button> aria-label="Hlavní navigace"
<div className={styles.dropdown} role="menu" aria-label="Služby submenu"> >
<Link to="/services/web" role="menuitem"> {/* Brand */}
<FaGlobe className={styles.iconSmall} /> Weby <div className={styles.logo}>
</Link> <Link to="/" aria-label="vontor.cz home">vontor.cz</Link>
<Link to="/services/film" role="menuitem">
<FaClapperboard className={styles.iconSmall} /> Filmařina
</Link>
<Link to="/services/drone" role="menuitem">
<FaWrench className={styles.iconSmall} /> Servis dronu
</Link>
</div>
</div> </div>
<Link className={`${styles.linkSimple} nav-item`} to="/social/feed"> {/* Desktop links */}
<FaUsers className={styles.iconSmall} /> Social <div className={styles.links} role="menubar">
</Link>
<Link className={`${styles.linkSimple} nav-item`} to="/contact">
Kontakt
</Link>
{/* User area */}
{!isAuthenticated || !user ? (
<button
type="button"
className={styles.loginBtn}
onClick={handleLogin}
aria-label="Přihlásit"
>
<FaSignInAlt /> Přihlásit
</button>
) : (
<div className={styles.dropdownItem}> <div className={styles.dropdownItem}>
<button className={styles.linkButton} aria-haspopup="true"> <button className={styles.linkButton} aria-haspopup="true">
<Avatar name={user.username || user.email} size={24} className={styles.avatar} /> Služby <FaChevronDown className={styles.chev} />
<span className={styles.username}>{user.username}</span>
<FaChevronDown className={styles.chev} />
</button> </button>
<div className={styles.dropdown} role="menu" aria-label="Uživatelské menu"> <div className={styles.dropdown} role="menu" aria-label="Služby submenu">
<Link to="/social/profile" role="menuitem">Profil</Link> <Link to="/services/web" role="menuitem"><FaGlobe className={styles.iconSmall} /> Weby</Link>
<Link to="/social/feed" role="menuitem">Feed</Link> <Link to="/services/film" role="menuitem"><FaClapperboard className={styles.iconSmall} /> Filmařina</Link>
<Link to="/social/chats" role="menuitem">Zprávy</Link> <Link to="/services/drone" role="menuitem"><FaWrench className={styles.iconSmall} /> Servis dronu</Link>
<button type="button" className={styles.logoutBtn} onClick={handleLogout} role="menuitem">
<FaSignOutAlt className={styles.iconSmall} /> Odhlásit se
</button>
</div> </div>
</div> </div>
)}
</div>
{/* Mobile burger — right side */} <Link className={styles.linkSimple} to="/social/feed">
<button <FaUsers className={styles.iconSmall} /> Social
className={styles.burger} </Link>
onClick={() => setMobileMenu((p) => !p)}
aria-expanded={mobileMenu} <Link className={styles.linkSimple} to="/contact">Kontakt</Link>
aria-label={mobileMenu ? "Zavřít menu" : "Otevřít menu"}
{!isAuthenticated || !user ? (
<button type="button" className={styles.loginBtn} onClick={handleLogin} aria-label="Přihlásit">
<FaSignInAlt /> Přihlásit
</button>
) : (
<div className={styles.dropdownItem}>
<button className={styles.linkButton} aria-haspopup="true">
<Avatar name={user.username || user.email} size={24} className={styles.avatar} />
<span className={styles.username}>{user.username}</span>
<FaChevronDown className={styles.chev} />
</button>
<div className={styles.dropdown} role="menu" aria-label="Uživatelské menu">
<Link to="/social/profile" role="menuitem">Profil</Link>
<Link to="/social/feed" role="menuitem">Feed</Link>
<Link to="/social/chats" role="menuitem">Zprávy</Link>
<button type="button" className={styles.logoutBtn} onClick={handleLogout} role="menuitem">
<FaSignOutAlt className={styles.iconSmall} /> Odhlásit se
</button>
</div>
</div>
)}
</div>
{/* Burger */}
<button
className={styles.burger}
onClick={() => setMobileMenu(p => !p)}
aria-expanded={mobileMenu}
aria-label={mobileMenu ? "Zavřít menu" : "Otevřít menu"}
>
{mobileMenu ? <FaTimes /> : <FaBars />}
</button>
</nav>
{/* Backdrop */}
<div
className={`${styles.overlay} ${mobileMenu ? styles.overlayVisible : ""}`}
onClick={closeAll}
aria-hidden="true"
/>
{/* Mobile drawer */}
<div
className={`${styles.drawer} ${mobileMenu ? styles.drawerOpen : ""}`}
aria-hidden={!mobileMenu}
> >
{mobileMenu ? <FaTimes /> : <FaBars />} <div className={styles.drawerHeader}>
</button> <Link to="/" className={styles.drawerLogo} onClick={closeAll}>vontor.cz</Link>
</nav> <button className={styles.drawerClose} onClick={closeAll} aria-label="Zavřít menu">
<FaTimes />
</button>
</div>
<nav className={styles.drawerNav}>
{/* Služby accordion */}
<div>
<button
className={styles.drawerAccordion}
onClick={() => setMobileServicesOpen(p => !p)}
aria-expanded={mobileServicesOpen}
>
Služby
<FaChevronDown className={`${styles.chev} ${mobileServicesOpen ? styles.chevOpen : ""}`} />
</button>
<div className={`${styles.drawerSub} ${mobileServicesOpen ? styles.drawerSubOpen : ""}`}>
<Link to="/services/web" className={styles.drawerSubItem} onClick={closeAll}><FaGlobe className={styles.iconSmall} /> Weby</Link>
<Link to="/services/film" className={styles.drawerSubItem} onClick={closeAll}><FaClapperboard className={styles.iconSmall} /> Filmařina</Link>
<Link to="/services/drone" className={styles.drawerSubItem} onClick={closeAll}><FaWrench className={styles.iconSmall} /> Servis dronu</Link>
</div>
</div>
<Link to="/social/feed" className={styles.drawerItem} onClick={closeAll}>
<FaUsers className={styles.iconSmall} /> Social
</Link>
<Link to="/contact" className={styles.drawerItem} onClick={closeAll}>
Kontakt
</Link>
<div className={styles.drawerDivider} />
{!isAuthenticated || !user ? (
<button
type="button"
className={styles.drawerLoginBtn}
onClick={() => { handleLogin(); closeAll(); }}
>
<FaSignInAlt /> Přihlásit se
</button>
) : (
<div>
<button
className={styles.drawerAccordion}
onClick={() => setMobileUserOpen(p => !p)}
aria-expanded={mobileUserOpen}
>
<Avatar name={user.username || user.email} size={28} className={styles.avatar} />
<span className={styles.username}>{user.username}</span>
<FaChevronDown className={`${styles.chev} ${mobileUserOpen ? styles.chevOpen : ""}`} />
</button>
<div className={`${styles.drawerSub} ${mobileUserOpen ? styles.drawerSubOpen : ""}`}>
<Link to="/social/profile" className={styles.drawerSubItem} onClick={closeAll}>Profil</Link>
<Link to="/social/feed" className={styles.drawerSubItem} onClick={closeAll}>Feed</Link>
<Link to="/social/chats" className={styles.drawerSubItem} onClick={closeAll}>Zprávy</Link>
<button
type="button"
className={`${styles.drawerSubItem} ${styles.drawerLogout}`}
onClick={() => { handleLogout(); closeAll(); }}
>
<FaSignOutAlt className={styles.iconSmall} /> Odhlásit se
</button>
</div>
</div>
)}
</nav>
</div>
</>
); );
} }

View File

@@ -264,7 +264,7 @@
transform: none; transform: none;
} }
/* ── Mobile ── */ /* ── Mobile — hide links, show burger ── */
@media (max-width: 900px) { @media (max-width: 900px) {
.navbar { .navbar {
width: 100%; width: 100%;
@@ -276,89 +276,222 @@
border-right: none; border-right: none;
border-top: none; border-top: none;
} }
.logo { .logo {
border-right: none; border-right: none;
padding-right: 0; padding-right: 0;
flex: 1; flex: 1;
} }
.links { display: none; }
.burger { .burger {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
}
.links {
position: absolute; /* ── Backdrop overlay ── */
left: 0; .overlay {
right: 0; display: none;
top: 100%; position: fixed;
flex-direction: column; inset: 0;
align-items: stretch; z-index: 199;
gap: 0.3rem; background: rgba(3, 29, 68, 0.55);
padding: 0; backdrop-filter: blur(4px);
background: color-mix(in hsl, var(--c-background-light), transparent 5%); -webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(20px); opacity: 0;
-webkit-backdrop-filter: blur(20px); transition: opacity 0.3s ease;
border-bottom: 1px solid color-mix(in hsl, var(--c-lines), transparent 60%); }
border-bottom-left-radius: 1.5rem; .overlayVisible {
border-bottom-right-radius: 1.5rem; display: block;
overflow: hidden; opacity: 1;
max-height: 0; }
opacity: 0;
transition: max-height 0.4s ease, opacity 0.3s ease, padding 0.3s ease; /* ── Mobile drawer ── */
} .drawer {
position: fixed;
.links.show { top: 0;
max-height: 80vh; right: 0;
opacity: 1; bottom: 0;
padding: 0.75rem; width: min(320px, 88vw);
overflow-y: auto; z-index: 200;
} background: color-mix(in hsl, var(--c-background-light), transparent 8%);
backdrop-filter: blur(24px) saturate(1.5);
.dropdownItem { -webkit-backdrop-filter: blur(24px) saturate(1.5);
width: 100%; border-left: 1px solid color-mix(in hsl, var(--c-lines), transparent 60%);
} display: flex;
flex-direction: column;
.linkButton { transform: translateX(100%);
width: 100%; transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
justify-content: space-between; overflow-y: auto;
padding: 0.75em 1em; }
border-radius: 0.75rem; .drawerOpen {
background: color-mix(in hsl, var(--c-background-light), transparent 40%); transform: translateX(0);
color: var(--c-text); }
}
/* ── Drawer header ── */
.linkSimple { .drawerHeader {
width: 100%; display: flex;
padding: 0.75em 1em; align-items: center;
border-radius: 0.75rem; justify-content: space-between;
justify-content: flex-start; padding: 1.1rem 1.25rem;
} border-bottom: 1px solid color-mix(in hsl, var(--c-lines), transparent 70%);
flex-shrink: 0;
.dropdown { }
position: relative; .drawerLogo {
top: 0; font-size: 1.4rem;
left: 0; font-weight: 900;
transform: none; letter-spacing: -0.02em;
opacity: 1; background: linear-gradient(135deg, var(--c-other) 0%, var(--c-lines) 50%, var(--c-text) 100%);
pointer-events: auto; -webkit-background-clip: text;
border: none; -webkit-text-fill-color: transparent;
box-shadow: none; background-clip: text;
background: color-mix(in hsl, var(--c-background), transparent 20%); text-decoration: none;
border-radius: 0.75rem; }
margin-top: 0.25rem; .drawerClose {
} background: none;
border: 1px solid color-mix(in hsl, var(--c-lines), transparent 65%);
.dropdownItem:hover .dropdown, border-radius: 0.6rem;
.dropdownItem:focus-within .dropdown { color: var(--c-text);
transform: none; font-size: 1rem;
} padding: 0.35em 0.5em;
cursor: pointer;
.loginBtn { display: flex;
width: 100%; align-items: center;
justify-content: center; transition: background 0.2s;
padding: 0.75em 1em; }
} .drawerClose:hover {
background: color-mix(in hsl, var(--c-boxes), transparent 60%);
}
/* ── Drawer nav items ── */
.drawerNav {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 1rem 0.75rem;
flex: 1;
}
.drawerItem {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.8rem 1rem;
border-radius: 0.75rem;
font-size: 1rem;
font-weight: 500;
color: color-mix(in hsl, var(--c-text), transparent 15%);
text-decoration: none;
transition: background 0.18s, color 0.18s;
}
.drawerItem:hover {
background: color-mix(in hsl, var(--c-boxes), transparent 65%);
color: white;
}
/* ── Accordion trigger ── */
.drawerAccordion {
display: flex;
align-items: center;
gap: 0.6rem;
width: 100%;
padding: 0.8rem 1rem;
border-radius: 0.75rem;
background: none;
border: none;
font-size: 1rem;
font-weight: 500;
color: color-mix(in hsl, var(--c-text), transparent 15%);
cursor: pointer;
text-align: left;
transition: background 0.18s, color 0.18s;
}
.drawerAccordion:hover {
background: color-mix(in hsl, var(--c-boxes), transparent 65%);
color: white;
transform: none;
box-shadow: none;
}
.drawerAccordion .chev {
margin-left: auto;
transition: transform 0.25s ease;
}
.chevOpen {
transform: rotate(180deg) !important;
}
/* ── Accordion sub-items ── */
.drawerSub {
display: flex;
flex-direction: column;
gap: 0.15rem;
overflow: hidden;
max-height: 0;
opacity: 0;
transition: max-height 0.3s ease, opacity 0.25s ease;
padding-left: 0.75rem;
}
.drawerSubOpen {
max-height: 300px;
opacity: 1;
}
.drawerSubItem {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.65rem 1rem;
border-radius: 0.65rem;
font-size: 0.9rem;
font-weight: 400;
color: color-mix(in hsl, var(--c-text), transparent 25%);
text-decoration: none;
background: none;
border: none;
cursor: pointer;
text-align: left;
width: 100%;
font-family: inherit;
transition: background 0.18s, color 0.18s;
}
.drawerSubItem:hover {
background: color-mix(in hsl, var(--c-boxes), transparent 70%);
color: white;
transform: none;
box-shadow: none;
}
.drawerLogout {
color: color-mix(in hsl, #ff6b6b, var(--c-text) 30%);
}
.drawerLogout:hover {
background: color-mix(in hsl, #ff6b6b, transparent 80%);
color: #ff9898;
}
/* ── Drawer divider ── */
.drawerDivider {
height: 1px;
margin: 0.5rem 0.25rem;
background: color-mix(in hsl, var(--c-lines), transparent 72%);
}
/* ── Drawer login button ── */
.drawerLoginBtn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
width: 100%;
padding: 0.85em 1em;
border-radius: 0.75rem;
background: linear-gradient(135deg, var(--c-other), color-mix(in hsl, var(--c-other), var(--c-boxes) 40%));
border: none;
color: #031D44;
font-size: 0.95rem;
font-weight: 700;
cursor: pointer;
transition: opacity 0.2s, transform 0.15s;
}
.drawerLoginBtn:hover {
opacity: 0.9;
transform: scale(1.02);
box-shadow: 0 0 1rem color-mix(in hsl, var(--c-other), transparent 50%);
} }

View File

@@ -1,7 +1,7 @@
import { useState, useMemo } from "react"; import { useState, useMemo } from "react";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FiSearch, FiPlus } from "react-icons/fi"; import { FiSearch, FiPlus, FiChevronsLeft } from "react-icons/fi";
import { useApiSocialChatsList } from "@/api/generated/private/chat/chat"; import { useApiSocialChatsList } from "@/api/generated/private/chat/chat";
import Avatar from "@/components/ui/Avatar"; import Avatar from "@/components/ui/Avatar";
import Spinner from "@/components/ui/Spinner"; import Spinner from "@/components/ui/Spinner";
@@ -9,7 +9,12 @@ import EmptyState from "@/components/ui/EmptyState";
import IconButton from "@/components/ui/IconButton"; import IconButton from "@/components/ui/IconButton";
import CreateChatModal from "./CreateChatModal"; import CreateChatModal from "./CreateChatModal";
export default function ChatSidebar() { interface Props {
onClose?: () => void;
onSelectChat?: () => void;
}
export default function ChatSidebar({ onClose, onSelectChat }: Props) {
const { t } = useTranslation("social"); const { t } = useTranslation("social");
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
@@ -26,11 +31,23 @@ export default function ChatSidebar() {
<h2 className="text-sm font-semibold text-brand-text"> <h2 className="text-sm font-semibold text-brand-text">
{t("chat.sidebar.title")} {t("chat.sidebar.title")}
</h2> </h2>
<IconButton <div className="flex items-center gap-1">
icon={<FiPlus size={16} />} <IconButton
label={t("chat.sidebar.new")} icon={<FiPlus size={16} />}
onClick={() => setCreateOpen(true)} label={t("chat.sidebar.new")}
/> onClick={() => setCreateOpen(true)}
/>
{onClose && (
<button
type="button"
onClick={onClose}
className="flex h-7 w-7 items-center justify-center rounded-full text-brand-text/50 hover:bg-brand-lines/15 hover:text-brand-text transition-colors"
aria-label="Hide sidebar"
>
<FiChevronsLeft size={16} />
</button>
)}
</div>
</header> </header>
<div className="relative px-3 py-2"> <div className="relative px-3 py-2">
@@ -62,6 +79,7 @@ export default function ChatSidebar() {
<li key={chat.id}> <li key={chat.id}>
<NavLink <NavLink
to={`/social/chats/${chat.id}`} to={`/social/chats/${chat.id}`}
onClick={onSelectChat}
className={({ isActive }) => className={({ isActive }) =>
[ [
"flex items-center gap-3 px-3 py-2.5 hover:bg-brand-lines/10 transition-colors", "flex items-center gap-3 px-3 py-2.5 hover:bg-brand-lines/10 transition-colors",

View File

@@ -1,34 +1,46 @@
import { useState } from "react"; import { useState } from "react";
import { Outlet } from "react-router-dom"; import { Outlet } from "react-router-dom";
import { FiMenu, FiChevronsLeft } from "react-icons/fi"; import { FiMenu } from "react-icons/fi";
import ChatSidebar from "@/components/social/chat/ChatSidebar"; import ChatSidebar from "@/components/social/chat/ChatSidebar";
export default function ChatLayout() { export default function ChatLayout() {
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
// Close sidebar on mobile when a chat is selected (called from NavLink onClick)
const handleSelectChat = () => {
if (window.innerWidth < 640) setOpen(false);
};
return ( return (
// Mobile: sidebar=100%+0 or sidebar=0+100%
// Desktop: sidebar=280px+1fr or sidebar=0+1fr
<div <div
className={[ className={[
"grid h-screen transition-[grid-template-columns] duration-200", "grid transition-[grid-template-columns] duration-200",
open ? "grid-cols-[280px_1fr]" : "grid-cols-[0px_1fr]", "h-full",
open
? "grid-cols-[100%_0] sm:grid-cols-[280px_1fr]"
: "grid-cols-[0_100%] sm:grid-cols-[0px_1fr]",
].join(" ")} ].join(" ")}
> >
{/* Sidebar — hidden via overflow+width collapse, not unmount (keeps scroll pos) */} {/* Sidebar */}
<div className="overflow-hidden"> <div className="overflow-hidden">
<ChatSidebar /> <ChatSidebar onClose={() => setOpen(false)} onSelectChat={handleSelectChat} />
</div> </div>
{/* Chat pane */}
<section className="relative flex h-full flex-col overflow-hidden"> <section className="relative flex h-full flex-col overflow-hidden">
{/* Sidebar toggle — sits at the top-left of the chat pane */} {/* Show-sidebar button — only when sidebar is collapsed */}
<button {!open && (
type="button" <button
onClick={() => setOpen((v) => !v)} type="button"
className="absolute left-3 top-4 z-20 flex h-7 w-7 items-center justify-center rounded-full bg-brand-bgLight/60 text-brand-text/60 hover:bg-brand-lines/20 hover:text-brand-text transition-colors" onClick={() => setOpen(true)}
aria-label={open ? "Hide sidebar" : "Show sidebar"} className="absolute left-3 top-4 z-20 flex h-7 w-7 items-center justify-center rounded-full bg-brand-bgLight/60 text-brand-text/60 hover:bg-brand-lines/20 hover:text-brand-text transition-colors"
> aria-label="Show sidebar"
{open ? <FiChevronsLeft size={15} /> : <FiMenu size={15} />} >
</button> <FiMenu size={15} />
</button>
)}
<Outlet /> <Outlet />
</section> </section>
</div> </div>

View File

@@ -1,4 +1,4 @@
import { NavLink, Outlet, useMatch } from "react-router-dom"; import { NavLink, Link, Outlet, useMatch } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useMemo } from "react"; import { useMemo } from "react";
import { import {
@@ -8,6 +8,7 @@ import {
FiUser, FiUser,
FiBookmark, FiBookmark,
FiLogOut, FiLogOut,
FiArrowLeft,
} from "react-icons/fi"; } from "react-icons/fi";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { useApiSocialChatsList } from "@/api/generated/private/chat/chat"; import { useApiSocialChatsList } from "@/api/generated/private/chat/chat";
@@ -21,10 +22,10 @@ interface NavItem {
function buildItems(username?: string): NavItem[] { function buildItems(username?: string): NavItem[] {
return [ return [
{ to: "/social/feed", icon: <FiHome size={22} />, labelKey: "nav.feed" }, { to: "/social/feed", icon: <FiHome size={22} />, labelKey: "nav.feed" },
{ to: "/social/chats", icon: <FiMessageCircle size={22} />, labelKey: "nav.chats" }, { to: "/social/chats", icon: <FiMessageCircle size={22} />, labelKey: "nav.chats" },
{ to: "/social/hubs", icon: <FiUsers size={22} />, labelKey: "nav.hubs" }, { to: "/social/hubs", icon: <FiUsers size={22} />, labelKey: "nav.hubs" },
{ to: "/social/saved", icon: <FiBookmark size={22} />, labelKey: "nav.saved" }, { to: "/social/saved", icon: <FiBookmark size={22} />, labelKey: "nav.saved" },
{ {
to: username ? `/social/profile/${username}` : "/social/feed", to: username ? `/social/profile/${username}` : "/social/feed",
icon: <FiUser size={22} />, icon: <FiUser size={22} />,
@@ -33,6 +34,9 @@ function buildItems(username?: string): NavItem[] {
]; ];
} }
// Height of the mobile bottom tab bar (keep in sync with the nav below)
const BOTTOM_NAV_H = "3.5rem";
export default function SocialLayout() { export default function SocialLayout() {
const { t } = useTranslation("social"); const { t } = useTranslation("social");
const { user } = useAuth(); const { user } = useAuth();
@@ -46,14 +50,57 @@ export default function SocialLayout() {
); );
return ( return (
<div className="min-h-screen w-full"> /*
<div className="mx-auto flex max-w-[1280px] gap-6 px-3 sm:px-6"> * Full-height flex column:
{/* Left rail */} * [mobile top bar — shrinks to content]
<aside className="sticky top-0 hidden h-screen w-[72px] flex-shrink-0 flex-col items-center justify-between py-6 md:flex md:w-[220px] md:items-start"> * [content row — flex-1, fills the rest]
* [bottom spacer — same height as the fixed bottom nav, mobile only]
*
* This ensures the middle row is always exactly the right height so
* nothing is hidden behind the fixed bottom nav.
*/
<div className="flex flex-col" style={{ height: "100svh" }}>
{/* ── Mobile top bar ── */}
<div
className="md:hidden flex-shrink-0 relative flex items-center px-4 py-2.5 border-b border-brand-lines/15"
style={{
background: "color-mix(in hsl, var(--c-background-light), transparent 15%)",
backdropFilter: "blur(12px)",
zIndex: 40,
}}
>
<Link
to="/"
className="flex items-center gap-1.5 text-brand-text/60 hover:text-brand-text transition-colors text-sm font-medium"
>
<FiArrowLeft size={15} />
vontor.cz
</Link>
{/* Absolutely centered title so it's always exactly in the middle */}
<span className="absolute left-1/2 -translate-x-1/2 text-sm font-bold text-rainbow pointer-events-none">
Social
</span>
</div>
{/* ── Content row ── */}
<div className={[
"flex-1 overflow-hidden",
"mx-auto flex w-full max-w-[1280px] gap-6 sm:px-6",
].join(" ")}>
{/* Left sidebar — desktop only */}
<aside className="sticky top-0 hidden h-full w-[72px] flex-shrink-0 flex-col items-center justify-between py-6 md:flex md:w-[220px] md:items-start">
<div className="flex w-full flex-col gap-1.5"> <div className="flex w-full flex-col gap-1.5">
<div className="mb-4 px-2 text-xl font-bold text-rainbow hidden md:block"> <div className="mb-1 px-2 text-xl font-bold text-rainbow hidden md:block">
vontor.cz vontor.cz
</div> </div>
<Link
to="/"
className="hidden md:flex items-center gap-2 px-2 mb-3 text-xs text-brand-text/50 hover:text-brand-text/80 transition-colors"
>
<FiArrowLeft size={12} /> Back to site
</Link>
{items.map((it) => ( {items.map((it) => (
<NavLink <NavLink
key={it.to} key={it.to}
@@ -93,20 +140,20 @@ export default function SocialLayout() {
</div> </div>
</aside> </aside>
{/* Main column — expands to fill all available space in chat mode */} {/* Main column */}
<main <main
className={ className={
isChat isChat
? "flex-1 h-screen overflow-hidden" ? "w-full md:flex-1 overflow-hidden h-full"
: "flex-1 border-x border-brand-lines/15 max-w-[640px] min-h-screen" : "w-full md:flex-1 md:border-x border-brand-lines/15 md:max-w-[640px] overflow-y-auto"
} }
> >
<Outlet /> <Outlet />
</main> </main>
{/* Right rail — hidden in chat (chat needs the space) */} {/* Right rail — desktop only, not in chat */}
{!isChat && ( {!isChat && (
<aside className="sticky top-0 hidden h-screen w-[300px] shrink-0 py-6 lg:block"> <aside className="sticky top-0 hidden h-full w-[300px] shrink-0 py-6 lg:block overflow-y-auto">
<div className="glass rounded-2xl p-4 text-sm text-brand-text/70"> <div className="glass rounded-2xl p-4 text-sm text-brand-text/70">
<div className="font-semibold text-brand-text mb-2"> <div className="font-semibold text-brand-text mb-2">
{t("nav.hubs")} {t("nav.hubs")}
@@ -116,6 +163,59 @@ export default function SocialLayout() {
</aside> </aside>
)} )}
</div> </div>
{/* ── Bottom spacer — pushes content above the fixed nav on mobile ── */}
<div
className="md:hidden flex-shrink-0"
style={{ height: BOTTOM_NAV_H }}
/>
{/* ── Fixed bottom tab bar — mobile only ── */}
<nav
className="md:hidden"
style={{
position: "fixed", bottom: 0, left: 0, right: 0, zIndex: 50,
height: BOTTOM_NAV_H,
background: "color-mix(in hsl, var(--c-background-light), transparent 8%)",
backdropFilter: "blur(20px)",
WebkitBackdropFilter: "blur(20px)",
borderTop: "1px solid color-mix(in hsl, var(--c-lines), transparent 65%)",
display: "flex",
}}
>
{items.map((it) => (
<NavLink
key={it.to}
to={it.to}
style={{
flex: 1, display: "flex", flexDirection: "column" as const,
alignItems: "center", justifyContent: "center",
padding: "0.5rem 0", gap: "0.2rem",
textDecoration: "none", position: "relative",
}}
className={({ isActive }) => isActive ? "bottom-nav-active" : "bottom-nav-item"}
>
{it.icon}
<span style={{ fontSize: "0.62rem", fontWeight: 500 }}>{t(it.labelKey)}</span>
{it.to === "/social/chats" && totalUnread > 0 && (
<span style={{
position: "absolute", top: "0.35rem", right: "calc(50% - 1.4rem)",
background: "var(--c-other)", color: "#031D44",
fontSize: "0.6rem", fontWeight: 700,
padding: "0.1em 0.4em", borderRadius: "9999px", lineHeight: 1.4,
}}>
{totalUnread > 99 ? "99+" : totalUnread}
</span>
)}
</NavLink>
))}
</nav>
<style>{`
.bottom-nav-item { color: color-mix(in hsl, var(--c-text), transparent 45%); transition: color 0.15s; }
.bottom-nav-active { color: var(--c-other); }
.bottom-nav-item:hover { color: var(--c-text); }
`}</style>
</div> </div>
); );
} }

View File

@@ -54,8 +54,8 @@ export default function ContactPage() {
</p> </p>
</motion.div> </motion.div>
{/* Two-column layout */} {/* Two-column layout — Tailwind handles responsive columns (inline styles can't be overridden by media queries) */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1.4fr", gap: "4rem", alignItems: "start" }}> <div className="grid grid-cols-1 items-start gap-10 md:grid-cols-[1fr_1.4fr] md:gap-16">
{/* Left — info */} {/* Left — info */}
<div style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}> <div style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
@@ -143,12 +143,6 @@ export default function ContactPage() {
</div> </div>
</div> </div>
{/* Responsive: stack on mobile */}
<style>{`
@media (max-width: 768px) {
.contact-grid { grid-template-columns: 1fr !important; gap: 2.5rem !important; }
}
`}</style>
</section> </section>
); );
} }

View File

@@ -1,7 +1,274 @@
export default function DroneServisSection() { import { motion } from "framer-motion";
return ( import { Link } from "react-router-dom";
<article id="drone-servis" className="section"> import { FaWrench, FaSearch, FaHeadset, FaSlidersH } from "react-icons/fa";
import { MdFlightTakeoff } from "react-icons/md";
import { GiAutoRepair } from "react-icons/gi";
</article> const SERVICES = [
); {
icon: <FaSearch />,
color: "#87a9da",
title: "Diagnostika",
desc: "Kompletní elektronická i mechanická diagnostika — přesně zjistíme, co je potřeba opravit.",
},
{
icon: <FaWrench />,
color: "#70A288",
title: "Opravy",
desc: "Výměna motorů, ESC, rámů, kamer a dalších komponentů. Originální i aftermarket díly.",
},
{
icon: <FaSlidersH />,
color: "#CAF0F8",
title: "Kalibrace",
desc: "IMU, gimbal, kompas — přesná kalibrace po opravě i při problémech s nerovnoměrným letem.",
},
{
icon: <FaHeadset />,
color: "#70A288",
title: "Poradenství",
desc: "Nevíte, jaký dron koupit nebo jak létat bezpečně? Poradím s výběrem, provozem i legislativou.",
},
];
const BRANDS = ["DJI", "Autel", "Parrot", "Skydio", "Holybro", "Custom builds"];
const cardVariants = {
hidden: { opacity: 0, y: 30 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: "easeOut" } },
};
export default function DroneServicePage() {
return (
<main>
{/* ── Hero ── */}
<section style={{
padding: "6rem 1.5rem 5rem",
position: "relative", overflow: "hidden",
background: "linear-gradient(180deg, color-mix(in hsl, var(--c-background), black 15%) 0%, var(--c-background) 100%)",
}}>
<div style={{
position: "absolute", inset: 0, opacity: 0.04, pointerEvents: "none",
backgroundImage: "linear-gradient(var(--c-lines) 1px, transparent 1px), linear-gradient(90deg, var(--c-lines) 1px, transparent 1px)",
backgroundSize: "60px 60px",
}} />
<div style={{
position: "absolute", top: "10%", right: "5%", width: "420px", height: "420px",
borderRadius: "50%", background: "color-mix(in hsl, var(--c-other), transparent 88%)",
filter: "blur(90px)", pointerEvents: "none",
}} />
<div className="container" style={{ position: "relative", display: "grid", gridTemplateColumns: "1fr 1fr", gap: "4rem", alignItems: "center" }}>
{/* Left — text */}
<motion.div
initial={{ opacity: 0, x: -30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.65, ease: "easeOut" }}
>
<span style={{
display: "inline-flex", alignItems: "center", gap: "0.4em",
padding: "0.35em 1em", borderRadius: "9999px",
border: "1px solid color-mix(in hsl, var(--c-other), transparent 45%)",
background: "color-mix(in hsl, var(--c-other), transparent 82%)",
color: "var(--c-other)", fontSize: "0.78rem", fontWeight: 700,
letterSpacing: "0.08em", textTransform: "uppercase" as const,
marginBottom: "1.5rem",
}}>
Služby
</span>
<h1 style={{
fontSize: "clamp(2.2rem, 5vw, 3.5rem)", fontWeight: 900,
margin: "0.5rem 0 1rem", letterSpacing: "-0.02em",
background: "linear-gradient(135deg, var(--c-other) 0%, var(--c-lines) 50%, var(--c-text) 100%)",
WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", backgroundClip: "text",
display: "inline-block",
}}>
Servis dronů
</h1>
<p style={{
color: "color-mix(in hsl, var(--c-text), transparent 35%)",
lineHeight: 1.75, fontSize: "1.05rem", margin: "0 0 2rem", maxWidth: "440px",
}}>
Profesionální servis a údržba dronů všech značek. Diagnostika, opravy, kalibrace a letíte zas.
</p>
<Link
to="/contact?service=drone"
style={{
display: "inline-flex", alignItems: "center", gap: "0.6rem",
padding: "0.85em 2em", borderRadius: "9999px", fontWeight: 700, fontSize: "0.95rem",
background: "linear-gradient(135deg, var(--c-other), color-mix(in hsl, var(--c-other), var(--c-boxes) 45%))",
color: "#031D44", textDecoration: "none",
boxShadow: "0 0 1.2rem color-mix(in hsl, var(--c-other), transparent 55%)",
transition: "transform 0.2s ease, box-shadow 0.2s ease",
}}
onMouseEnter={e => { e.currentTarget.style.transform = "scale(1.05)"; e.currentTarget.style.boxShadow = "0 0 2rem color-mix(in hsl, var(--c-other), transparent 35%)"; }}
onMouseLeave={e => { e.currentTarget.style.transform = "scale(1)"; e.currentTarget.style.boxShadow = "0 0 1.2rem color-mix(in hsl, var(--c-other), transparent 55%)"; }}
>
<FaWrench style={{ fontSize: "0.9rem" }} /> Objednat servis
</Link>
</motion.div>
{/* Right — icon */}
<motion.div
className="drone-servis-icon"
initial={{ opacity: 0, x: 30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.65, ease: "easeOut", delay: 0.15 }}
style={{ display: "flex", alignItems: "center", justifyContent: "center" }}
>
<GiAutoRepair style={{
fontSize: "clamp(8rem, 18vw, 14rem)",
color: "var(--c-other)",
filter: "drop-shadow(0 0 3rem color-mix(in hsl, var(--c-other), transparent 65%))",
opacity: 0.9,
}} />
</motion.div>
</div>
<style>{`
@media (max-width: 768px) {
#drone-servis-hero-grid { grid-template-columns: 1fr !important; }
.drone-servis-icon { display: none !important; }
}
`}</style>
</section>
{/* ── Service cards ── */}
<section style={{ padding: "5rem 1.5rem", background: "var(--c-background)" }}>
<div className="container">
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
style={{ textAlign: "center", marginBottom: "3rem" }}
>
<h2 style={{ fontSize: "clamp(1.8rem, 4vw, 2.6rem)", fontWeight: 800, margin: "0 0 0.75rem" }}>
Co nabízím
</h2>
<p style={{ color: "color-mix(in hsl, var(--c-text), transparent 40%)", maxWidth: "460px", margin: "0 auto", lineHeight: 1.7 }}>
Komplexní péče o váš dron od první diagnózy po finální test letu.
</p>
</motion.div>
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={{ visible: { transition: { staggerChildren: 0.1 } } }}
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(250px, 1fr))",
gap: "1.2rem",
}}
>
{SERVICES.map(({ icon, color, title, desc }) => (
<motion.div
key={title}
variants={cardVariants}
whileHover={{ scale: 1.03, y: -4 }}
transition={{ type: "spring", stiffness: 280, damping: 22 }}
className="glass"
style={{ padding: "1.8rem", display: "flex", flexDirection: "column" as const, gap: "1rem", cursor: "default" }}
>
<div style={{
width: "2.8rem", height: "2.8rem", borderRadius: "0.75rem",
background: `color-mix(in hsl, ${color}, transparent 80%)`,
border: `1px solid color-mix(in hsl, ${color}, transparent 55%)`,
display: "flex", alignItems: "center", justifyContent: "center",
fontSize: "1.25rem", color,
}}>
{icon}
</div>
<div>
<h3 style={{ margin: "0 0 0.5rem", fontSize: "1.05rem", fontWeight: 700 }}>{title}</h3>
<p style={{ margin: 0, color: "color-mix(in hsl, var(--c-text), transparent 35%)", fontSize: "0.9rem", lineHeight: 1.65 }}>{desc}</p>
</div>
</motion.div>
))}
</motion.div>
</div>
</section>
{/* ── Supported brands ── */}
<section style={{ padding: "3rem 1.5rem", background: "color-mix(in hsl, var(--c-background), black 5%)" }}>
<div className="container" style={{ textAlign: "center" }}>
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
>
<p style={{
color: "color-mix(in hsl, var(--c-text), transparent 50%)",
fontSize: "0.8rem", marginBottom: "1.2rem",
textTransform: "uppercase" as const, letterSpacing: "0.08em", fontWeight: 600,
}}>
Podporované značky
</p>
<div style={{ display: "flex", gap: "0.8rem", justifyContent: "center", flexWrap: "wrap" }}>
{BRANDS.map(brand => (
<span key={brand} style={{
padding: "0.4em 1em", borderRadius: "9999px",
background: "color-mix(in hsl, var(--c-background-light), transparent 30%)",
border: "1px solid color-mix(in hsl, var(--c-lines), transparent 65%)",
fontSize: "0.88rem", fontWeight: 500,
color: "color-mix(in hsl, var(--c-text), transparent 25%)",
}}>
{brand}
</span>
))}
</div>
</motion.div>
</div>
</section>
{/* ── CTA ── */}
<section style={{
padding: "5rem 1.5rem", textAlign: "center",
background: "color-mix(in hsl, var(--c-background), black 8%)",
position: "relative", overflow: "hidden",
}}>
<div style={{
position: "absolute", bottom: "-15%", left: "50%", transform: "translateX(-50%)",
width: "500px", height: "250px", borderRadius: "50%",
background: "color-mix(in hsl, var(--c-other), transparent 90%)", filter: "blur(70px)",
pointerEvents: "none",
}} />
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
style={{ position: "relative" }}
>
<MdFlightTakeoff style={{ fontSize: "3rem", color: "var(--c-other)", marginBottom: "1rem", filter: "drop-shadow(0 0 1rem color-mix(in hsl, var(--c-other), transparent 55%))" }} />
<h2 style={{ fontSize: "clamp(1.7rem, 3.5vw, 2.4rem)", fontWeight: 800, margin: "0 0 1rem" }}>
Dron čeká popište závadu
</h2>
<p style={{ color: "color-mix(in hsl, var(--c-text), transparent 40%)", margin: "0 auto 2.2rem", maxWidth: "400px", lineHeight: 1.7 }}>
Napište mi co se stalo a domluvíme termín a cenu bez závazků.
</p>
<Link
to="/contact?service=drone"
style={{
display: "inline-flex", alignItems: "center", gap: "0.6rem",
padding: "0.9em 2.4em", borderRadius: "9999px", fontWeight: 700, fontSize: "1rem",
background: "linear-gradient(135deg, var(--c-other), color-mix(in hsl, var(--c-other), var(--c-boxes) 45%))",
color: "#031D44", textDecoration: "none",
boxShadow: "0 0 1.5rem color-mix(in hsl, var(--c-other), transparent 55%)",
transition: "transform 0.2s ease, box-shadow 0.2s ease",
}}
onMouseEnter={e => { e.currentTarget.style.transform = "scale(1.06)"; e.currentTarget.style.boxShadow = "0 0 2.5rem color-mix(in hsl, var(--c-other), transparent 35%)"; }}
onMouseLeave={e => { e.currentTarget.style.transform = "scale(1)"; e.currentTarget.style.boxShadow = "0 0 1.5rem color-mix(in hsl, var(--c-other), transparent 55%)"; }}
>
<FaWrench style={{ fontSize: "0.9rem" }} /> Napsat nám
</Link>
</motion.div>
</section>
</main>
);
} }

View File

@@ -1,7 +1,249 @@
export default function KinematografieSection() { import { motion } from "framer-motion";
return ( import { Link } from "react-router-dom";
<article id="kinematografie" className="section"> import { FaPlay } from "react-icons/fa";
import { MdFlightTakeoff, MdRadio } from "react-icons/md";
import { GiFilmProjector } from "react-icons/gi";
import { FaVideo } from "react-icons/fa";
import DroneSection from "@/components/home/drone/DroneSection";
</article> const WHAT = [
{
icon: <MdFlightTakeoff />,
color: "var(--c-other)",
title: "Drony & aerials",
desc: "DJI s licencí A1/A2/A3 — sweeping panoramata, dynamické tracking shoty i záběry v omezených zónách.",
},
{
icon: <GiFilmProjector />,
color: "var(--c-lines)",
title: "Gyro-stabilizovaná kamera",
desc: "3-osý gimbal pro kinematograficky plynulé záběry na zemi bez použití dronu.",
},
{
icon: <FaVideo />,
color: "var(--c-text)",
title: "Střih & postprodukce",
desc: "Kompletní sestřih, barevná korekce, titulky — ready pro sociální sítě, web i firemní prezentaci.",
},
{
icon: <MdRadio />,
color: "var(--c-other)",
title: "Oprávnění radiotelefonisty",
desc: "Omezený průkaz radiotelefonisty — autorizace pro lety v řízených i omezených vzdušných prostorech.",
},
];
const PRICING = [
{ label: "Základní paušál", value: "3 000 Kč", note: "Polovina dne natáčení" },
{ label: "Doprava — Ostrava", value: "Zdarma", note: "V rámci města" },
{ label: "Doprava — mimo Ostravu", value: "10 Kč / km", note: "Dle vzdálenosti" },
{ label: "Výstup", value: "Dle dohody", note: "Krátký sestřih nebo surové záběry" },
];
const cardVariants = {
hidden: { opacity: 0, y: 30 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: "easeOut" } },
};
export default function FilmServicePage() {
return (
<main>
{/* ── Reuse the DroneSection hero — covers aerial + film perfectly ── */}
<DroneSection />
{/* ── What's included ── */}
<section style={{ padding: "5rem 1.5rem", background: "var(--c-background)" }}>
<div className="container">
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
style={{ textAlign: "center", marginBottom: "3rem" }}
>
<h2 style={{ fontSize: "clamp(1.8rem, 4vw, 2.6rem)", fontWeight: 800, margin: "0 0 0.75rem" }}>
Co dostanete
</h2>
<p style={{ color: "color-mix(in hsl, var(--c-text), transparent 40%)", maxWidth: "460px", margin: "0 auto", lineHeight: 1.7 }}>
Od prvního záběru po finální video kompletní produkce.
</p>
</motion.div>
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={{ visible: { transition: { staggerChildren: 0.1 } } }}
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(250px, 1fr))",
gap: "1.2rem",
}}
>
{WHAT.map(({ icon, color, title, desc }) => (
<motion.div
key={title}
variants={cardVariants}
whileHover={{ scale: 1.03, y: -4 }}
transition={{ type: "spring", stiffness: 280, damping: 22 }}
className="glass"
style={{ padding: "1.8rem", display: "flex", flexDirection: "column" as const, gap: "1rem", cursor: "default" }}
>
<div style={{
width: "2.8rem", height: "2.8rem", borderRadius: "0.75rem",
background: `color-mix(in hsl, ${color}, transparent 80%)`,
border: `1px solid color-mix(in hsl, ${color}, transparent 55%)`,
display: "flex", alignItems: "center", justifyContent: "center",
fontSize: "1.25rem", color,
}}>
{icon}
</div>
<div>
<h3 style={{ margin: "0 0 0.5rem", fontSize: "1.05rem", fontWeight: 700 }}>{title}</h3>
<p style={{ margin: 0, color: "color-mix(in hsl, var(--c-text), transparent 35%)", fontSize: "0.9rem", lineHeight: 1.65 }}>{desc}</p>
</div>
</motion.div>
))}
</motion.div>
</div>
</section>
{/* ── Pricing table ── */}
<section style={{ padding: "4rem 1.5rem", background: "color-mix(in hsl, var(--c-background), black 5%)" }}>
<div className="container" style={{ maxWidth: "600px" }}>
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
>
<h2 style={{ fontSize: "clamp(1.8rem, 4vw, 2.4rem)", fontWeight: 800, margin: "0 0 2rem", textAlign: "center" }}>
Ceník
</h2>
<div className="glass" style={{ borderRadius: "1rem", overflow: "hidden" }}>
{PRICING.map(({ label, value, note }, i) => (
<div
key={label}
style={{
display: "flex", justifyContent: "space-between", alignItems: "center",
padding: "1rem 1.5rem", gap: "1rem",
borderBottom: i < PRICING.length - 1
? "1px solid color-mix(in hsl, var(--c-lines), transparent 75%)"
: "none",
}}
>
<div>
<div style={{ fontWeight: 600, fontSize: "0.95rem" }}>{label}</div>
<div style={{ color: "color-mix(in hsl, var(--c-text), transparent 50%)", fontSize: "0.8rem", marginTop: "0.1rem" }}>{note}</div>
</div>
<div style={{ fontWeight: 700, color: "var(--c-other)", fontSize: "1.05rem", whiteSpace: "nowrap" as const }}>
{value}
</div>
</div>
))}
</div>
<p style={{
color: "color-mix(in hsl, var(--c-text), transparent 50%)",
fontSize: "0.8rem", marginTop: "0.75rem", textAlign: "center",
}}>
Cena se může lišit dle rozsahu projektu, povolení a lokace.
</p>
</motion.div>
</div>
</section>
{/* ── Cert badges strip ── */}
<section style={{ padding: "2.5rem 1.5rem", background: "var(--c-background)" }}>
<div className="container" style={{ textAlign: "center" }}>
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
>
<p style={{
color: "color-mix(in hsl, var(--c-text), transparent 50%)",
fontSize: "0.8rem", marginBottom: "1.1rem",
textTransform: "uppercase" as const, letterSpacing: "0.08em", fontWeight: 600,
}}>
Certifikace & oprávnění
</p>
<div style={{ display: "flex", gap: "0.7rem", justifyContent: "center", flexWrap: "wrap" }}>
{["A1", "A2", "A3", "Restricted Airspace", "Radiotelefonista"].map(cert => (
<span key={cert} style={{
padding: "0.35em 0.9em", borderRadius: "9999px",
background: "color-mix(in hsl, var(--c-other), transparent 85%)",
border: "1px solid color-mix(in hsl, var(--c-other), transparent 55%)",
fontSize: "0.82rem", fontWeight: 600, color: "var(--c-other)",
}}>
{cert}
</span>
))}
</div>
</motion.div>
</div>
</section>
{/* ── CTA ── */}
<section style={{
padding: "5rem 1.5rem", textAlign: "center",
background: "color-mix(in hsl, var(--c-background), black 8%)",
position: "relative", overflow: "hidden",
}}>
<div style={{
position: "absolute", bottom: "-15%", left: "50%", transform: "translateX(-50%)",
width: "500px", height: "250px", borderRadius: "50%",
background: "color-mix(in hsl, var(--c-other), transparent 90%)", filter: "blur(70px)",
pointerEvents: "none",
}} />
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
style={{ position: "relative" }}
>
<h2 style={{ fontSize: "clamp(1.7rem, 3.5vw, 2.4rem)", fontWeight: 800, margin: "0 0 1rem" }}>
Natočíme to spolu
</h2>
<p style={{ color: "color-mix(in hsl, var(--c-text), transparent 40%)", margin: "0 auto 2.2rem", maxWidth: "400px", lineHeight: 1.7 }}>
Napište, kde a co chcete natočit, a domluvíme se na termínu.
</p>
<div style={{ display: "flex", gap: "1rem", justifyContent: "center", flexWrap: "wrap" }}>
<Link
to="/contact?service=video"
style={{
display: "inline-flex", alignItems: "center", gap: "0.5rem",
padding: "0.9em 2.2em", borderRadius: "9999px", fontWeight: 700, fontSize: "1rem",
background: "linear-gradient(135deg, var(--c-other), color-mix(in hsl, var(--c-other), var(--c-boxes) 45%))",
color: "#031D44", textDecoration: "none",
boxShadow: "0 0 1.5rem color-mix(in hsl, var(--c-other), transparent 55%)",
transition: "transform 0.2s ease, box-shadow 0.2s ease",
}}
onMouseEnter={e => { e.currentTarget.style.transform = "scale(1.06)"; e.currentTarget.style.boxShadow = "0 0 2.5rem color-mix(in hsl, var(--c-other), transparent 35%)"; }}
onMouseLeave={e => { e.currentTarget.style.transform = "scale(1)"; e.currentTarget.style.boxShadow = "0 0 1.5rem color-mix(in hsl, var(--c-other), transparent 55%)"; }}
>
Mám zájem
</Link>
<Link
to="/portfolio"
style={{
display: "inline-flex", alignItems: "center", gap: "0.5rem",
padding: "0.9em 2.2em", borderRadius: "9999px", fontWeight: 600, fontSize: "1rem",
background: "color-mix(in hsl, var(--c-background-light), transparent 25%)",
backdropFilter: "blur(12px)",
color: "var(--c-text)", textDecoration: "none",
border: "1px solid color-mix(in hsl, var(--c-lines), transparent 55%)",
transition: "border-color 0.2s ease, transform 0.2s ease",
}}
onMouseEnter={e => { e.currentTarget.style.borderColor = "var(--c-other)"; e.currentTarget.style.transform = "scale(1.04)"; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = "color-mix(in hsl, var(--c-lines), transparent 55%)"; e.currentTarget.style.transform = "scale(1)"; }}
>
<FaPlay style={{ fontSize: "0.8rem" }} /> Portfolio
</Link>
</div>
</motion.div>
</section>
</main>
); );
} }

View File

@@ -1,25 +1,321 @@
export default function WebsiteServiceSection() { import { motion } from "framer-motion";
return ( import { Link } from "react-router-dom";
<article id="web-services" className="section"> import { FaCode, FaExternalLinkAlt } from "react-icons/fa";
<h1>Weby</h1> import WebDevSection from "@/components/home/webdev/WebDevSection";
<p> const PROJECTS = [
Udělám weby na míru podle vašich představ, jde o jednoduché statické stránky, e-shopy, správy systémů, nebo komplexní webové aplikace. {
Používám moderní technologie jako React, Next.js, a další, abych zajistil rychlý a responzivní design. href: "https://davo1.cz",
Web lze hostovat na mém hostingu s rychlou odezvou a atraktivní cenou. name: "davo1.cz",
img: "/portfolio/davo1.png",
imgBg: "#000",
desc: "Osobní portfolio & prezentace",
},
{
href: "https://perlica.cz",
name: "perlica.cz",
img: "/portfolio/perlica.png",
imgBg: undefined,
desc: "Firemní e-commerce platforma",
},
{
href: "http://epinger2.cz",
name: "epinger2.cz",
img: "/portfolio/epinger.png",
imgBg: "#fff",
desc: "Monitoring dostupnosti serverů",
},
];
const cardVariants = {
hidden: { opacity: 0, y: 30 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: "easeOut" } },
};
export default function WebServicePage() {
return (
<main>
{/* ── Hero ── */}
<section style={{
padding: "6rem 1.5rem 5rem",
position: "relative",
overflow: "hidden",
background: "var(--c-background)",
}}>
<div style={{
position: "absolute", inset: 0, opacity: 0.04, pointerEvents: "none",
backgroundImage: "linear-gradient(var(--c-lines) 1px, transparent 1px), linear-gradient(90deg, var(--c-lines) 1px, transparent 1px)",
backgroundSize: "60px 60px",
}} />
<div style={{
position: "absolute", top: "20%", right: "8%", width: "360px", height: "360px",
borderRadius: "50%", background: "color-mix(in hsl, var(--c-lines), transparent 88%)",
filter: "blur(80px)", pointerEvents: "none",
}} />
<div className="container" style={{ position: "relative", textAlign: "center" }}>
<motion.div
initial={{ opacity: 0, y: 28 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.65, ease: "easeOut" }}
>
<span style={{
display: "inline-flex", alignItems: "center", gap: "0.4em",
padding: "0.35em 1em", borderRadius: "9999px",
border: "1px solid color-mix(in hsl, var(--c-lines), transparent 45%)",
background: "color-mix(in hsl, var(--c-boxes), transparent 75%)",
color: "var(--c-lines)", fontSize: "0.78rem", fontWeight: 700,
letterSpacing: "0.08em", textTransform: "uppercase" as const,
marginBottom: "1.5rem",
}}>
<FaCode style={{ fontSize: "0.85em" }} /> Služby
</span>
<h1 style={{
fontSize: "clamp(2.5rem, 6vw, 4rem)", fontWeight: 900,
margin: "0.5rem 0 1rem", letterSpacing: "-0.02em",
background: "linear-gradient(135deg, var(--c-other) 0%, var(--c-lines) 45%, var(--c-text) 100%)",
WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", backgroundClip: "text",
display: "inline-block",
}}>
Tvorba Webů
</h1>
<p style={{
color: "color-mix(in hsl, var(--c-text), transparent 35%)",
maxWidth: "520px", margin: "0 auto 2.2rem", lineHeight: 1.75, fontSize: "1.05rem",
}}>
Od vizitky po komplexní webovou aplikaci full-stack vývoj na míru, spolehlivý hosting a platební brány.
</p> </p>
<div style={{ display: "flex", gap: "1rem", justifyContent: "center", flexWrap: "wrap" }}>
<Link
to="/contact?service=web"
style={{
padding: "0.85em 2.2em", borderRadius: "9999px",
background: "linear-gradient(135deg, var(--c-other), color-mix(in hsl, var(--c-other), var(--c-boxes) 45%))",
color: "#031D44", fontWeight: 700, fontSize: "0.95rem",
textDecoration: "none",
boxShadow: "0 0 1.2rem color-mix(in hsl, var(--c-other), transparent 55%)",
transition: "transform 0.2s ease, box-shadow 0.2s ease",
}}
onMouseEnter={e => { e.currentTarget.style.transform = "scale(1.05)"; e.currentTarget.style.boxShadow = "0 0 2rem color-mix(in hsl, var(--c-other), transparent 35%)"; }}
onMouseLeave={e => { e.currentTarget.style.transform = "scale(1)"; e.currentTarget.style.boxShadow = "0 0 1.2rem color-mix(in hsl, var(--c-other), transparent 55%)"; }}
>
Chci web
</Link>
<a
href="#portfolio"
style={{
padding: "0.85em 2.2em", borderRadius: "9999px",
background: "color-mix(in hsl, var(--c-background-light), transparent 25%)",
backdropFilter: "blur(12px)",
color: "var(--c-text)", fontWeight: 600, fontSize: "0.95rem",
textDecoration: "none",
border: "1px solid color-mix(in hsl, var(--c-lines), transparent 55%)",
transition: "border-color 0.2s ease, transform 0.2s ease",
}}
onMouseEnter={e => { e.currentTarget.style.borderColor = "var(--c-other)"; e.currentTarget.style.transform = "scale(1.04)"; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = "color-mix(in hsl, var(--c-lines), transparent 55%)"; e.currentTarget.style.transform = "scale(1)"; }}
>
Ukázky práce
</a>
</div>
</motion.div>
</div>
</section>
<section> {/* ── Scope spectrum ── */}
<h2>Trading 212 (API)</h2> <section style={{ padding: "4rem 1.5rem 2rem", background: "color-mix(in hsl, var(--c-background), black 5%)" }}>
<small>(realné data z Tradingu 212)</small> <div className="container">
<div id="T212Graph"> <motion.div
{/* Trading graph component or placeholder */} initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
style={{ textAlign: "center", marginBottom: "2.5rem" }}
>
<h2 style={{ fontSize: "clamp(1.5rem, 3.5vw, 2.2rem)", fontWeight: 800, margin: "0 0 0.6rem" }}>
Od vizitky po komplexní platformu
</h2>
<p style={{ color: "color-mix(in hsl, var(--c-text), transparent 40%)", maxWidth: "500px", margin: "0 auto", lineHeight: 1.7 }}>
potřebujete jednoduchou prezentaci nebo real-time aplikaci s tisíci uživateli zvládnu oboje.
</p>
</motion.div>
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={{ visible: { transition: { staggerChildren: 0.08 } } }}
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))",
gap: "0",
borderRadius: "1rem",
overflow: "hidden",
border: "1px solid color-mix(in hsl, var(--c-lines), transparent 70%)",
}}
>
{[
{
step: "01",
label: "Prezentační web",
examples: "Portfolio, vizitka, firemní web",
color: "var(--c-lines)",
},
{
step: "02",
label: "E-shop & portál",
examples: "Stripe, ČSOB, správa obsahu",
color: "var(--c-other)",
},
{
step: "03",
label: "Komplexní aplikace",
examples: "Real-time, WebSocket, vícevrstvá architektura",
color: "var(--c-other)",
},
].map(({ step, label, examples, color }, i, arr) => (
<motion.div
key={step}
variants={{ hidden: { opacity: 0, y: 16 }, visible: { opacity: 1, y: 0, transition: { duration: 0.45, ease: "easeOut" } } }}
style={{
padding: "1.75rem 1.5rem",
background: i === 1
? "color-mix(in hsl, var(--c-background-light), transparent 30%)"
: "color-mix(in hsl, var(--c-background), transparent 0%)",
borderRight: i < arr.length - 1 ? "1px solid color-mix(in hsl, var(--c-lines), transparent 70%)" : "none",
display: "flex",
flexDirection: "column" as const,
gap: "0.5rem",
}}
>
<span style={{ fontSize: "0.7rem", fontWeight: 700, color, letterSpacing: "0.1em", textTransform: "uppercase" as const }}>
{step}
</span>
<div style={{ fontWeight: 700, fontSize: "1.05rem" }}>{label}</div>
<div style={{ color: "color-mix(in hsl, var(--c-text), transparent 45%)", fontSize: "0.85rem", lineHeight: 1.55 }}>
{examples}
</div> </div>
</section> </motion.div>
))}
</motion.div>
</div>
</section>
<a href="">Příklady</a> {/* reálné ukázky webů + dema + apps */} {/* ── Feature cards — reuse homepage section as-is ── */}
<a href="">Ceník</a> <WebDevSection />
</article>
); {/* ── Portfolio ── */}
<section id="portfolio" style={{ padding: "5rem 1.5rem", background: "var(--c-background)" }}>
<div className="container">
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
style={{ textAlign: "center", marginBottom: "3rem" }}
>
<h2 style={{ fontSize: "clamp(1.8rem, 4vw, 2.6rem)", fontWeight: 800, margin: "0 0 0.75rem" }}>
Ukázky práce
</h2>
<p style={{ color: "color-mix(in hsl, var(--c-text), transparent 40%)", maxWidth: "460px", margin: "0 auto", lineHeight: 1.7 }}>
Reálné projekty klikni pro návštěvu webu.
</p>
</motion.div>
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={{ visible: { transition: { staggerChildren: 0.1 } } }}
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(260px, 1fr))",
gap: "1.2rem",
}}
>
{PROJECTS.map(({ href, name, img, imgBg, desc }) => (
<motion.a
key={name}
href={href}
target="_blank"
rel="noopener noreferrer"
variants={cardVariants}
whileHover={{ scale: 1.03, y: -4 }}
transition={{ type: "spring", stiffness: 280, damping: 22 }}
className="glass"
style={{
display: "flex", flexDirection: "column" as const,
padding: "1.5rem", gap: "1rem",
textDecoration: "none", color: "inherit",
borderRadius: "1rem",
}}
>
<div style={{
borderRadius: "0.75rem", overflow: "hidden",
background: imgBg ?? "color-mix(in hsl, var(--c-background-light), transparent 20%)",
display: "flex", alignItems: "center", justifyContent: "center",
minHeight: "90px", padding: "1rem",
}}>
<img src={img} alt={name} style={{ maxHeight: "60px", maxWidth: "100%", objectFit: "contain" }} />
</div>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: "0.5rem" }}>
<div>
<div style={{ fontWeight: 700, fontSize: "0.95rem" }}>{name}</div>
<div style={{ color: "color-mix(in hsl, var(--c-text), transparent 45%)", fontSize: "0.85rem", marginTop: "0.2rem" }}>{desc}</div>
</div>
<FaExternalLinkAlt style={{ fontSize: "0.8rem", opacity: 0.4, flexShrink: 0, marginTop: "0.25rem" }} />
</div>
</motion.a>
))}
</motion.div>
</div>
</section>
{/* ── CTA ── */}
<section style={{
padding: "5rem 1.5rem", textAlign: "center",
background: "color-mix(in hsl, var(--c-background), black 8%)",
position: "relative", overflow: "hidden",
}}>
<div style={{
position: "absolute", bottom: "-15%", left: "50%", transform: "translateX(-50%)",
width: "500px", height: "250px", borderRadius: "50%",
background: "color-mix(in hsl, var(--c-other), transparent 90%)", filter: "blur(70px)",
pointerEvents: "none",
}} />
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
style={{ position: "relative" }}
>
<h2 style={{ fontSize: "clamp(1.7rem, 3.5vw, 2.4rem)", fontWeight: 800, margin: "0 0 1rem" }}>
Začněme váš projekt
</h2>
<p style={{ color: "color-mix(in hsl, var(--c-text), transparent 40%)", margin: "0 auto 2rem", maxWidth: "400px", lineHeight: 1.7 }}>
Napište mi a probereme, co potřebujete bez závazků.
</p>
<Link
to="/contact?service=web"
style={{
display: "inline-flex", alignItems: "center", gap: "0.5rem",
padding: "0.9em 2.4em", borderRadius: "9999px", fontWeight: 700, fontSize: "1rem",
background: "linear-gradient(135deg, var(--c-other), color-mix(in hsl, var(--c-other), var(--c-boxes) 45%))",
color: "#031D44", textDecoration: "none",
boxShadow: "0 0 1.5rem color-mix(in hsl, var(--c-other), transparent 55%)",
transition: "transform 0.2s ease, box-shadow 0.2s ease",
}}
onMouseEnter={e => { e.currentTarget.style.transform = "scale(1.06)"; e.currentTarget.style.boxShadow = "0 0 2.5rem color-mix(in hsl, var(--c-other), transparent 35%)"; }}
onMouseLeave={e => { e.currentTarget.style.transform = "scale(1)"; e.currentTarget.style.boxShadow = "0 0 1.5rem color-mix(in hsl, var(--c-other), transparent 55%)"; }}
>
Kontaktovat
</Link>
</motion.div>
</section>
</main>
);
} }

View File

@@ -36,9 +36,7 @@ export default function Home() {
return ( return (
<main> <main>
<HeroSection /> <HeroSection />
<div className="divider" />
<DroneSection /> <DroneSection />
<div className="divider" />
<WebDevSection /> <WebDevSection />
<div className="divider" /> <div className="divider" />
<ProjectsSection /> <ProjectsSection />
@@ -83,8 +81,8 @@ export default function Home() {
</p> </p>
</motion.div> </motion.div>
{/* Two-column: info left, form right */} {/* Two-column: info left, form right — Tailwind handles responsive stacking */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1.5fr", gap: "4rem", alignItems: "center" }}> <div className="grid grid-cols-1 items-center gap-10 md:grid-cols-[1fr_1.5fr] md:gap-16">
{/* Left — contact info */} {/* Left — contact info */}
<motion.div <motion.div
@@ -158,11 +156,6 @@ export default function Home() {
</div> </div>
</div> </div>
<style>{`
@media (max-width: 768px) {
#contact .container > div:last-child { grid-template-columns: 1fr !important; gap: 2rem !important; }
}
`}</style>
</section> </section>
</main> </main>
); );