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

View File

@@ -6,6 +6,8 @@ import ChatLayout from "./layouts/social/Chat";
import Downloader from "./pages/downloader/Downloader";
import Home from "./pages/home/home";
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 PublicOnlyRoute from "./routes/PublicOnlyRoute";
@@ -47,7 +49,8 @@ export default function App() {
<Route path="contact" element={<ContactPage />} />
<Route path="apps/downloader" element={<Downloader />} />
<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>

View File

@@ -147,15 +147,8 @@
@media only screen and (max-width: 990px){
.contact-me{
aspect-ratio: unset;
margin-top: 3ch;
background: transparent;
border: none;
.contact-me {
width: min(30em, 100%);
margin: 11rem auto 1.5rem;
}
.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{
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;
}
}
/* styles migrated to inline — file kept to avoid import errors */

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 { 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() {
return (
<footer id="contacts" className={styles.footer}>
<div className={styles.logo}>
<h1>vontor.cz</h1>
</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>
<footer id="contacts" style={{
position: "relative",
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",
}}>
<div className={styles.services}>
<h2><b>Služby</b></h2>
{/* 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",
}} />
<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 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 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>
);
}
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 },
};
const fadeRight = {
initial: { opacity: 0, x: 50 },
whileInView: { opacity: 1, x: 0 },
transition: { duration: 0.7, ease: "easeOut" },
viewport: { once: true },
};
const stagger = {
initial: "hidden",
whileInView: "visible",
@@ -37,73 +30,68 @@ const staggerItem = {
export default function DroneSection() {
return (
<section id="drone" className="section" style={{ background: "color-mix(in hsl, var(--c-background), black 10%)" }}>
<div className="container" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "4rem", alignItems: "center" }}>
<section id="drone" className="section" style={{ position: "relative", overflow: "hidden", background: "#030a16" }}>
{/* Left: visual */}
<motion.div {...fadeLeft} style={{ position: "relative" }}>
<div className="glass" style={{
padding: "3.5rem 2rem",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
minHeight: "380px",
position: "relative",
overflow: "hidden",
}}>
{/* Rotating ring */}
<div className="animate-spin-slow" style={{
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",
}} />
{/* Full-bleed background video */}
<video
src="https://vontor-cz.s3.eu-central-1.amazonaws.com/media/DroneVideo_web.mp4"
autoPlay
loop
muted
playsInline
style={{
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
objectFit: "cover",
zIndex: 0,
}}
/>
{/* Drone icon */}
<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%))" }} />
{/* Base dark scrim */}
<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 }}>
DJI · Sony · Gyroscope Stabilized
</p>
{/* Blue diagonal clip-path panel */}
<div className="drone-blue-panel" style={{
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 */}
<div style={{
position: "absolute",
bottom: "1.2rem",
right: "1.2rem",
display: "flex",
alignItems: "center",
gap: "0.5rem",
color: "var(--c-other)",
fontSize: "0.8rem",
fontWeight: 600,
}}>
<FaPlay style={{ fontSize: "0.7rem" }} /> Showreel coming soon
</div>
</div>
{/* Right-side vignette for text readability */}
<div className="drone-vignette" style={{
position: "absolute",
inset: 0,
background: "linear-gradient(to left, rgba(3,8,22,0.60) 0%, transparent 55%)",
zIndex: 2,
}} />
<div className="container" style={{ position: "relative", zIndex: 3, display: "grid", gridTemplateColumns: "1fr 1.4fr", gap: "4rem", alignItems: "center" }}>
{/* 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" }}>
<MdFlightTakeoff style={{
fontSize: "7.5rem",
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>
{/* 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}>
<span style={{
display: "inline-block",
padding: "0.3em 0.9em",
borderRadius: "9999px",
background: "color-mix(in hsl, var(--c-other), transparent 80%)",
border: "1px solid color-mix(in hsl, var(--c-other), transparent 50%)",
background: "color-mix(in hsl, var(--c-other), transparent 75%)",
border: "1px solid color-mix(in hsl, var(--c-other), transparent 40%)",
color: "var(--c-other)",
fontSize: "0.78rem",
fontWeight: 700,
@@ -114,12 +102,12 @@ export default function DroneSection() {
</span>
</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 {" "}
<span className="text-rainbow">Ground to Sky</span>
</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.
</motion.p>
@@ -127,7 +115,7 @@ export default function DroneSection() {
<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: <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" },
].map(({ icon, label, sub }) => (
<div key={label} style={{ display: "flex", gap: "1rem", alignItems: "flex-start" }}>
@@ -135,8 +123,8 @@ export default function DroneSection() {
width: "2.4rem",
height: "2.4rem",
borderRadius: "0.6rem",
background: "color-mix(in hsl, var(--c-other), transparent 80%)",
border: "1px solid color-mix(in hsl, var(--c-other), transparent 55%)",
background: "color-mix(in hsl, var(--c-other), transparent 75%)",
border: "1px solid color-mix(in hsl, var(--c-other), transparent 45%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
@@ -147,8 +135,8 @@ export default function DroneSection() {
{icon}
</div>
<div>
<div style={{ fontWeight: 600, fontSize: "0.95rem" }}>{label}</div>
<div style={{ color: "color-mix(in hsl, var(--c-text), transparent 45%)", fontSize: "0.85rem" }}>{sub}</div>
<div style={{ fontWeight: 600, fontSize: "0.95rem", color: "#fff" }}>{label}</div>
<div style={{ color: "rgba(255,255,255,0.50)", fontSize: "0.85rem" }}>{sub}</div>
</div>
</div>
))}
@@ -156,15 +144,15 @@ export default function DroneSection() {
{/* Cert badges */}
<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={{
padding: "0.3em 0.8em",
borderRadius: "9999px",
background: "color-mix(in hsl, var(--c-boxes), transparent 70%)",
border: "1px solid color-mix(in hsl, var(--c-lines), transparent 50%)",
background: "rgba(255,255,255,0.08)",
border: "1px solid rgba(255,255,255,0.22)",
fontSize: "0.8rem",
fontWeight: 600,
color: "var(--c-lines)",
color: "rgba(255,255,255,0.78)",
}}>
{cert}
</span>
@@ -202,10 +190,29 @@ export default function DroneSection() {
</motion.div>
</div>
{/* Mobile responsive */}
<style>{`
@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>
</section>

View File

@@ -122,7 +122,7 @@ export default function HeroSection() {
WebkitTextFillColor: "transparent",
backgroundClip: "text",
}}>
Bruno Novotný
David Bruno Vontor
</span>
</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 {
FaSignOutAlt,
FaSignInAlt,
FaBars,
FaChevronDown,
FaGlobe,
FaWrench,
FaUsers,
FaTimes,
FaSignOutAlt, FaSignInAlt, FaBars, FaChevronDown,
FaGlobe, FaWrench, FaUsers, FaTimes,
} from "react-icons/fa";
import { FaClapperboard } from "react-icons/fa6";
import { useAuth } from "@/hooks/useAuth";
@@ -20,15 +14,13 @@ export default function Navbar() {
const navigate = useNavigate();
const location = useLocation();
const handleLogin = () => navigate("/social/login");
const handleLogout = async () => {
await logout();
navigate("/");
};
const handleLogin = () => navigate("/social/login");
const handleLogout = async () => { await logout(); navigate("/"); };
const [mobileMenu, setMobileMenu] = useState(false);
const [scrolled, setScrolled] = useState(false);
const navRef = useRef<HTMLElement | null>(null);
const [mobileMenu, setMobileMenu] = useState(false);
const [mobileServicesOpen, setMobileServicesOpen] = useState(false);
const [mobileUserOpen, setMobileUserOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 40);
@@ -36,96 +28,171 @@ export default function Navbar() {
return () => window.removeEventListener("scroll", onScroll);
}, []);
// Close mobile menu on route change
useEffect(() => {
setMobileMenu(false);
setMobileServicesOpen(false);
setMobileUserOpen(false);
}, [location.pathname]);
useEffect(() => {
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") setMobileMenu(false);
}
const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setMobileMenu(false); };
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);
return (
<nav
className={`${styles.navbar} ${mobileMenu ? styles.mobileNavOpen : ""} ${scrolled ? styles.scrolled : ""}`}
ref={navRef}
aria-label="Hlavní navigace"
>
{/* Brand */}
<div className={styles.logo}>
<Link to="/" aria-label="vontor.cz home">vontor.cz</Link>
</div>
useEffect(() => {
document.body.style.overflow = mobileMenu ? "hidden" : "";
return () => { document.body.style.overflow = ""; };
}, [mobileMenu]);
{/* Center links */}
<div className={`${styles.links} ${mobileMenu ? styles.show : ""}`} role="menubar">
{/* Služby dropdown */}
<div className={styles.dropdownItem}>
<button className={styles.linkButton} aria-haspopup="true">
Služby <FaChevronDown className={styles.chev} />
</button>
<div className={styles.dropdown} role="menu" aria-label="Služby submenu">
<Link to="/services/web" role="menuitem">
<FaGlobe className={styles.iconSmall} /> Weby
</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>
const closeAll = () => setMobileMenu(false);
return (
<>
<nav
className={`${styles.navbar} ${scrolled ? styles.scrolled : ""}`}
aria-label="Hlavní navigace"
>
{/* Brand */}
<div className={styles.logo}>
<Link to="/" aria-label="vontor.cz home">vontor.cz</Link>
</div>
<Link className={`${styles.linkSimple} nav-item`} to="/social/feed">
<FaUsers className={styles.iconSmall} /> Social
</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>
) : (
{/* Desktop links */}
<div className={styles.links} role="menubar">
<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} />
Služby <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 className={styles.dropdown} role="menu" aria-label="Služby submenu">
<Link to="/services/web" role="menuitem"><FaGlobe className={styles.iconSmall} /> Weby</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>
{/* Mobile burger — right side */}
<button
className={styles.burger}
onClick={() => setMobileMenu((p) => !p)}
aria-expanded={mobileMenu}
aria-label={mobileMenu ? "Zavřít menu" : "Otevřít menu"}
<Link className={styles.linkSimple} to="/social/feed">
<FaUsers className={styles.iconSmall} /> Social
</Link>
<Link className={styles.linkSimple} to="/contact">Kontakt</Link>
{!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 />}
</button>
</nav>
<div className={styles.drawerHeader}>
<Link to="/" className={styles.drawerLogo} onClick={closeAll}>vontor.cz</Link>
<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;
}
/* ── Mobile ── */
/* ── Mobile — hide links, show burger ── */
@media (max-width: 900px) {
.navbar {
width: 100%;
@@ -276,89 +276,222 @@
border-right: none;
border-top: none;
}
.logo {
border-right: none;
padding-right: 0;
flex: 1;
}
.links { display: none; }
.burger {
display: inline-flex;
align-items: center;
justify-content: center;
}
.links {
position: absolute;
left: 0;
right: 0;
top: 100%;
flex-direction: column;
align-items: stretch;
gap: 0.3rem;
padding: 0;
background: color-mix(in hsl, var(--c-background-light), transparent 5%);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid color-mix(in hsl, var(--c-lines), transparent 60%);
border-bottom-left-radius: 1.5rem;
border-bottom-right-radius: 1.5rem;
overflow: hidden;
max-height: 0;
opacity: 0;
transition: max-height 0.4s ease, opacity 0.3s ease, padding 0.3s ease;
}
.links.show {
max-height: 80vh;
opacity: 1;
padding: 0.75rem;
overflow-y: auto;
}
.dropdownItem {
width: 100%;
}
.linkButton {
width: 100%;
justify-content: space-between;
padding: 0.75em 1em;
border-radius: 0.75rem;
background: color-mix(in hsl, var(--c-background-light), transparent 40%);
color: var(--c-text);
}
.linkSimple {
width: 100%;
padding: 0.75em 1em;
border-radius: 0.75rem;
justify-content: flex-start;
}
.dropdown {
position: relative;
top: 0;
left: 0;
transform: none;
opacity: 1;
pointer-events: auto;
border: none;
box-shadow: none;
background: color-mix(in hsl, var(--c-background), transparent 20%);
border-radius: 0.75rem;
margin-top: 0.25rem;
}
.dropdownItem:hover .dropdown,
.dropdownItem:focus-within .dropdown {
transform: none;
}
.loginBtn {
width: 100%;
justify-content: center;
padding: 0.75em 1em;
}
}
/* ── Backdrop overlay ── */
.overlay {
display: none;
position: fixed;
inset: 0;
z-index: 199;
background: rgba(3, 29, 68, 0.55);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
opacity: 0;
transition: opacity 0.3s ease;
}
.overlayVisible {
display: block;
opacity: 1;
}
/* ── Mobile drawer ── */
.drawer {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: min(320px, 88vw);
z-index: 200;
background: color-mix(in hsl, var(--c-background-light), transparent 8%);
backdrop-filter: blur(24px) saturate(1.5);
-webkit-backdrop-filter: blur(24px) saturate(1.5);
border-left: 1px solid color-mix(in hsl, var(--c-lines), transparent 60%);
display: flex;
flex-direction: column;
transform: translateX(100%);
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
overflow-y: auto;
}
.drawerOpen {
transform: translateX(0);
}
/* ── Drawer header ── */
.drawerHeader {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.1rem 1.25rem;
border-bottom: 1px solid color-mix(in hsl, var(--c-lines), transparent 70%);
flex-shrink: 0;
}
.drawerLogo {
font-size: 1.4rem;
font-weight: 900;
letter-spacing: -0.02em;
background: linear-gradient(135deg, var(--c-other) 0%, var(--c-lines) 50%, var(--c-text) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-decoration: none;
}
.drawerClose {
background: none;
border: 1px solid color-mix(in hsl, var(--c-lines), transparent 65%);
border-radius: 0.6rem;
color: var(--c-text);
font-size: 1rem;
padding: 0.35em 0.5em;
cursor: pointer;
display: flex;
align-items: center;
transition: background 0.2s;
}
.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 { NavLink } from "react-router-dom";
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 Avatar from "@/components/ui/Avatar";
import Spinner from "@/components/ui/Spinner";
@@ -9,7 +9,12 @@ import EmptyState from "@/components/ui/EmptyState";
import IconButton from "@/components/ui/IconButton";
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 [query, setQuery] = useState("");
const [createOpen, setCreateOpen] = useState(false);
@@ -26,11 +31,23 @@ export default function ChatSidebar() {
<h2 className="text-sm font-semibold text-brand-text">
{t("chat.sidebar.title")}
</h2>
<IconButton
icon={<FiPlus size={16} />}
label={t("chat.sidebar.new")}
onClick={() => setCreateOpen(true)}
/>
<div className="flex items-center gap-1">
<IconButton
icon={<FiPlus size={16} />}
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>
<div className="relative px-3 py-2">
@@ -62,6 +79,7 @@ export default function ChatSidebar() {
<li key={chat.id}>
<NavLink
to={`/social/chats/${chat.id}`}
onClick={onSelectChat}
className={({ isActive }) =>
[
"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 { 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";
export default function ChatLayout() {
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 (
// Mobile: sidebar=100%+0 or sidebar=0+100%
// Desktop: sidebar=280px+1fr or sidebar=0+1fr
<div
className={[
"grid h-screen transition-[grid-template-columns] duration-200",
open ? "grid-cols-[280px_1fr]" : "grid-cols-[0px_1fr]",
"grid transition-[grid-template-columns] duration-200",
"h-full",
open
? "grid-cols-[100%_0] sm:grid-cols-[280px_1fr]"
: "grid-cols-[0_100%] sm:grid-cols-[0px_1fr]",
].join(" ")}
>
{/* Sidebar — hidden via overflow+width collapse, not unmount (keeps scroll pos) */}
{/* Sidebar */}
<div className="overflow-hidden">
<ChatSidebar />
<ChatSidebar onClose={() => setOpen(false)} onSelectChat={handleSelectChat} />
</div>
{/* Chat pane */}
<section className="relative flex h-full flex-col overflow-hidden">
{/* Sidebar toggle — sits at the top-left of the chat pane */}
<button
type="button"
onClick={() => setOpen((v) => !v)}
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={open ? "Hide sidebar" : "Show sidebar"}
>
{open ? <FiChevronsLeft size={15} /> : <FiMenu size={15} />}
</button>
{/* Show-sidebar button — only when sidebar is collapsed */}
{!open && (
<button
type="button"
onClick={() => setOpen(true)}
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"
>
<FiMenu size={15} />
</button>
)}
<Outlet />
</section>
</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 { useMemo } from "react";
import {
@@ -8,6 +8,7 @@ import {
FiUser,
FiBookmark,
FiLogOut,
FiArrowLeft,
} from "react-icons/fi";
import { useAuth } from "@/hooks/useAuth";
import { useApiSocialChatsList } from "@/api/generated/private/chat/chat";
@@ -21,10 +22,10 @@ interface NavItem {
function buildItems(username?: string): NavItem[] {
return [
{ to: "/social/feed", icon: <FiHome size={22} />, labelKey: "nav.feed" },
{ to: "/social/chats", icon: <FiMessageCircle size={22} />, labelKey: "nav.chats" },
{ to: "/social/hubs", icon: <FiUsers size={22} />, labelKey: "nav.hubs" },
{ to: "/social/saved", icon: <FiBookmark size={22} />, labelKey: "nav.saved" },
{ to: "/social/feed", icon: <FiHome size={22} />, labelKey: "nav.feed" },
{ to: "/social/chats", icon: <FiMessageCircle size={22} />, labelKey: "nav.chats" },
{ to: "/social/hubs", icon: <FiUsers size={22} />, labelKey: "nav.hubs" },
{ to: "/social/saved", icon: <FiBookmark size={22} />, labelKey: "nav.saved" },
{
to: username ? `/social/profile/${username}` : "/social/feed",
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() {
const { t } = useTranslation("social");
const { user } = useAuth();
@@ -46,14 +50,57 @@ export default function SocialLayout() {
);
return (
<div className="min-h-screen w-full">
<div className="mx-auto flex max-w-[1280px] gap-6 px-3 sm:px-6">
{/* Left rail */}
<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">
/*
* Full-height flex column:
* [mobile top bar — shrinks to content]
* [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="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
</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) => (
<NavLink
key={it.to}
@@ -93,20 +140,20 @@ export default function SocialLayout() {
</div>
</aside>
{/* Main column — expands to fill all available space in chat mode */}
{/* Main column */}
<main
className={
isChat
? "flex-1 h-screen overflow-hidden"
: "flex-1 border-x border-brand-lines/15 max-w-[640px] min-h-screen"
? "w-full md:flex-1 overflow-hidden h-full"
: "w-full md:flex-1 md:border-x border-brand-lines/15 md:max-w-[640px] overflow-y-auto"
}
>
<Outlet />
</main>
{/* Right rail — hidden in chat (chat needs the space) */}
{/* Right rail — desktop only, not in chat */}
{!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="font-semibold text-brand-text mb-2">
{t("nav.hubs")}
@@ -116,6 +163,59 @@ export default function SocialLayout() {
</aside>
)}
</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>
);
}

View File

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

View File

@@ -1,7 +1,274 @@
export default function DroneServisSection() {
return (
<article id="drone-servis" className="section">
</article>
);
}
import { motion } from "framer-motion";
import { Link } from "react-router-dom";
import { FaWrench, FaSearch, FaHeadset, FaSlidersH } from "react-icons/fa";
import { MdFlightTakeoff } from "react-icons/md";
import { GiAutoRepair } from "react-icons/gi";
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";
import { Link } from "react-router-dom";
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";
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 (
<article id="kinematografie" className="section">
</article>
<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() {
return (
<article id="web-services" className="section">
<h1>Weby</h1>
import { motion } from "framer-motion";
import { Link } from "react-router-dom";
import { FaCode, FaExternalLinkAlt } from "react-icons/fa";
import WebDevSection from "@/components/home/webdev/WebDevSection";
<p>
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.
Web lze hostovat na mém hostingu s rychlou odezvou a atraktivní cenou.
const PROJECTS = [
{
href: "https://davo1.cz",
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>
<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>
<h2>Trading 212 (API)</h2>
<small>(realné data z Tradingu 212)</small>
<div id="T212Graph">
{/* Trading graph component or placeholder */}
{/* ── Scope spectrum ── */}
<section style={{ padding: "4rem 1.5rem 2rem", background: "color-mix(in hsl, var(--c-background), black 5%)" }}>
<div className="container">
<motion.div
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>
</section>
</motion.div>
))}
</motion.div>
</div>
</section>
<a href="">Příklady</a> {/* reálné ukázky webů + dema + apps */}
<a href="">Ceník</a>
</article>
);
}
{/* ── Feature cards — reuse homepage section as-is ── */}
<WebDevSection />
{/* ── 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 (
<main>
<HeroSection />
<div className="divider" />
<DroneSection />
<div className="divider" />
<WebDevSection />
<div className="divider" />
<ProjectsSection />
@@ -83,8 +81,8 @@ export default function Home() {
</p>
</motion.div>
{/* Two-column: info left, form right */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1.5fr", gap: "4rem", alignItems: "center" }}>
{/* Two-column: info left, form right — Tailwind handles responsive stacking */}
<div className="grid grid-cols-1 items-center gap-10 md:grid-cols-[1fr_1.5fr] md:gap-16">
{/* Left — contact info */}
<motion.div
@@ -158,11 +156,6 @@ export default function Home() {
</div>
</div>
<style>{`
@media (max-width: 768px) {
#contact .container > div:last-child { grid-template-columns: 1fr !important; gap: 2rem !important; }
}
`}</style>
</section>
</main>
);