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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -87,3 +87,6 @@ venv
|
||||
.venv
|
||||
|
||||
certs
|
||||
|
||||
# large video assets — host externally (S3)
|
||||
frontend/public/portfolio/*.mp4
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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>
|
||||
<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",
|
||||
}}>
|
||||
|
||||
{/* 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>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -122,7 +122,7 @@ export default function HeroSection() {
|
||||
WebkitTextFillColor: "transparent",
|
||||
backgroundClip: "text",
|
||||
}}>
|
||||
Bruno Novotný
|
||||
David Bruno Vontor
|
||||
</span>
|
||||
</motion.h1>
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,274 @@
|
||||
export default function DroneServisSection() {
|
||||
return (
|
||||
<article id="drone-servis" className="section">
|
||||
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";
|
||||
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,249 @@
|
||||
export default function KinematografieSection() {
|
||||
return (
|
||||
<article id="kinematografie" className="section">
|
||||
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";
|
||||
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -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, ať už 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 }}>
|
||||
Ať 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user