Enhance contact UI and email handling

Add an email template and improve contact flow: create backend/templates/email/contact_me.html and update backend/advertisement/tasks.py to use SiteConfiguration.contact_email with a fallback to brunovontor@gmail.com when sending contact form emails. Revamp frontend contact experience: ContactMeForm is now open by default, uses controlled email/message inputs, shows loading/success/error states and posts to /api/advertisement/contact-me/ via publicApi. Update ContactPage and Home to include richer contact sections (framer-motion animations, icons, social links and responsive layouts). Also add a PowerShell helper entry to .claude/settings.local.json.
This commit is contained in:
2026-06-07 23:17:10 +02:00
parent 3766b033bc
commit ad7f0fbe55
6 changed files with 385 additions and 59 deletions

View File

@@ -1,35 +1,50 @@
import React, { useState, useRef } from "react"
import styles from "./contact-me.module.css"
import { LuMousePointerClick } from "react-icons/lu";
import { publicApi } from "@/api/publicClient";
export default function ContactMeForm() {
const [opened, setOpened] = useState(false)
const [contentMoveUp, setContentMoveUp] = useState(false)
const [openingBehind, setOpeningBehind] = useState(false)
// const [success, setSuccess] = useState(false)
const [opened, setOpened] = useState(true)
const [contentMoveUp, setContentMoveUp] = useState(true)
const [openingBehind, setOpeningBehind] = useState(true)
const [email, setEmail] = useState("")
const [message, setMessage] = useState("")
const [loading, setLoading] = useState(false)
const [success, setSuccess] = useState(false)
const [error, setError] = useState("")
const openingRef = useRef<HTMLDivElement>(null)
function handleSubmit() {
// form submission logic here
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setLoading(true)
setError("")
try {
await publicApi.post("/api/advertisement/contact-me/", { email, message, hp: "" })
setSuccess(true)
setEmail("")
setMessage("")
} catch {
setError("Nepodařilo se odeslat zprávu. Zkuste to prosím znovu.")
} finally {
setLoading(false)
}
}
const toggleOpen = () => {
if (!opened) {
setOpened(true)
setOpeningBehind(false)
setContentMoveUp(false)
// Wait for the rotate-opening animation to finish before moving content up
// The actual moveUp will be handled in onTransitionEnd
setContentMoveUp(false)
} else {
setContentMoveUp(false)
setOpeningBehind(false)
setTimeout(() => setOpened(false), 1000) // match transition duration
setTimeout(() => setOpened(false), 1000)
}
}
const handleTransitionEnd = (e: React.TransitionEvent<HTMLDivElement>) => {
if (opened && e.propertyName === "transform") {
setContentMoveUp(true)
setContentMoveUp(true)
setTimeout(() => setOpeningBehind(true), 10)
}
if (!opened && e.propertyName === "transform") {
@@ -38,7 +53,6 @@ export default function ContactMeForm() {
}
return (
<div className={styles["contact-me"]}>
<div
ref={openingRef}
@@ -52,7 +66,7 @@ export default function ContactMeForm() {
onClick={toggleOpen}
onTransitionEnd={handleTransitionEnd}
>
<LuMousePointerClick/>
<LuMousePointerClick />
</div>
<div
@@ -61,21 +75,50 @@ export default function ContactMeForm() {
contentMoveUp ? styles["content-moveup"] : ''
].filter(Boolean).join(' ')}
>
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
placeholder="Váš email"
required
/>
<textarea
name="message"
placeholder="Vaše zpráva"
required
/>
<input type="hidden" name="state" />
<input type="submit"/>
</form>
{success ? (
<div style={{
display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center",
height: "100%", gap: "0.75rem", padding: "1.5rem", textAlign: "center",
}}>
<div style={{ fontSize: "2.5rem" }}></div>
<p style={{ color: "var(--c-other)", fontWeight: 700, margin: 0 }}>Zpráva odeslána!</p>
<p style={{ color: "color-mix(in hsl, var(--c-text), transparent 35%)", fontSize: "0.85rem", margin: 0 }}>
Ozvu se vám do 24 hodin.
</p>
<button
onClick={() => setSuccess(false)}
style={{
marginTop: "0.5rem", background: "none", border: "1px solid var(--c-lines)",
color: "var(--c-text)", padding: "0.4em 1.2em", borderRadius: "0.5em",
cursor: "pointer", fontSize: "0.82rem",
}}
>
Nová zpráva
</button>
</div>
) : (
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
placeholder="Váš email"
required
value={email}
onChange={e => setEmail(e.target.value)}
/>
<textarea
name="message"
placeholder="Vaše zpráva"
required
value={message}
onChange={e => setMessage(e.target.value)}
/>
{error && (
<p style={{ color: "#ff6b6b", fontSize: "0.8rem", margin: "0", textAlign: "center" }}>{error}</p>
)}
<input type="submit" value={loading ? "Odesílám…" : "Odeslat"} disabled={loading} />
</form>
)}
</div>
<div className={styles.cover}></div>

View File

@@ -1,39 +1,154 @@
import { motion } from "framer-motion";
import { FaEnvelope, FaPhone, FaGithub, FaLinkedin, FaInstagram } from "react-icons/fa";
import ContactMeForm from "../../components/home/ContactMe/ContactMeForm";
export default function ContactPage(){
const fadeUp = (delay = 0) => ({
initial: { opacity: 0, y: 28 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.6, ease: "easeOut", delay },
});
const CONTACT_ITEMS = [
{ icon: <FaEnvelope />, label: "Email", value: "brunovontor@gmail.com", href: "mailto:brunovontor@gmail.com", color: "var(--c-other)" },
{ icon: <FaPhone />, label: "Phone", value: "+420 605 512 624", href: "tel:+420605512624", color: "var(--c-lines)" },
];
const SOCIAL_LINKS = [
{ icon: <FaGithub />, href: "https://github.com/Brunobrno", label: "GitHub", color: "#e6edf3" },
{ icon: <FaLinkedin />, href: "https://linkedin.com", label: "LinkedIn", color: "#0a66c2" },
{ icon: <FaInstagram />,href: "https://instagram.com", label: "Instagram",color: "#e1306c" },
];
export default function ContactPage() {
return (
<section className="section">
<div className="container">
<h2 className="text-2xl md:text-3xl font-bold mb-2 text-rainbow">Get in Touch</h2>
<p className="text-brand-lines mb-6">Reach out via the form or use the details below.</p>
<section style={{ minHeight: "100svh", background: "var(--c-background)", padding: "5rem 1.5rem 4rem", position: "relative", overflow: "hidden" }}>
{/* Desktop/tablet: envelope animation + slide-out form */}
<div className="hidden md:block">
<ContactMeForm />
</div>
{/* Background glow blobs */}
<div style={{ position: "absolute", top: "10%", left: "-10%", width: "500px", height: "500px", borderRadius: "50%", background: "color-mix(in hsl, var(--c-other), transparent 88%)", filter: "blur(80px)", zIndex: 0, pointerEvents: "none" }} />
<div style={{ position: "absolute", bottom: "5%", right: "-5%", width: "400px", height: "400px", borderRadius: "50%", background: "color-mix(in hsl, var(--c-boxes), transparent 85%)", filter: "blur(80px)", zIndex: 0, pointerEvents: "none" }} />
{/* Mobile: simple card version without envelope */}
<div className="md:hidden">
<div className="card p-5">
<form className="space-y-3">
<div>
<label className="block text-sm text-brand-lines mb-1">Your email</label>
<input type="email" required className="w-full rounded-lg bg-brand-bgLight/40 border border-brand-lines/40 focus:outline-none focus:ring-2 focus:ring-brand-accent px-3 py-2 text-brand-text placeholder:text-brand-lines/70" placeholder="Enter your email" />
<div className="container" style={{ position: "relative", zIndex: 1 }}>
{/* Header */}
<motion.div {...fadeUp(0)} style={{ textAlign: "center", marginBottom: "4rem" }}>
<span style={{
display: "inline-block", padding: "0.3em 0.9em", borderRadius: "9999px",
background: "color-mix(in hsl, var(--c-other), transparent 82%)",
border: "1px solid color-mix(in hsl, var(--c-other), transparent 50%)",
color: "var(--c-other)", fontSize: "0.78rem", fontWeight: 700,
letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: "1rem",
}}>
Contact
</span>
<h1 style={{ fontSize: "clamp(2rem, 5vw, 3.5rem)", fontWeight: 900, margin: "0 0 0.8rem", lineHeight: 1.1 }}>
Let's{" "}
<span style={{
background: "linear-gradient(135deg, var(--c-other), var(--c-lines))",
WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", backgroundClip: "text",
}}>
Work Together
</span>
</h1>
<p style={{ color: "color-mix(in hsl, var(--c-text), transparent 40%)", maxWidth: "480px", margin: "0 auto", lineHeight: 1.7, fontSize: "1rem" }}>
Have a project in mind? Click the envelope to open a message, or reach out directly below.
</p>
</motion.div>
{/* Two-column layout */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1.4fr", gap: "4rem", alignItems: "start" }}>
{/* Left — info */}
<div style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
{/* Available badge */}
<motion.div {...fadeUp(0.1)}>
<div className="glass" style={{ padding: "1.2rem 1.5rem", display: "flex", alignItems: "center", gap: "1rem" }}>
<div style={{ width: "10px", height: "10px", borderRadius: "50%", background: "var(--c-other)", boxShadow: "0 0 8px var(--c-other)", flexShrink: 0, animation: "pulse-glow 2s ease-in-out infinite" }} />
<div>
<div style={{ fontWeight: 700, fontSize: "0.95rem" }}>Available for new projects</div>
<div style={{ fontSize: "0.8rem", color: "color-mix(in hsl, var(--c-text), transparent 45%)" }}>Usually responds within 24 hours</div>
</div>
</div>
<div>
<label className="block text-sm text-brand-lines mb-1">Message</label>
<textarea required rows={5} className="w-full rounded-lg bg-brand-bgLight/40 border border-brand-lines/40 focus:outline-none focus:ring-2 focus:ring-brand-accent px-3 py-2 text-brand-text placeholder:text-brand-lines/70" placeholder="How can I help?" />
</div>
<button type="submit" className="px-4 py-2 rounded-lg font-semibold text-white bg-brandGradient shadow-glow">Send</button>
</form>
</motion.div>
{/* Contact details */}
{CONTACT_ITEMS.map(({ icon, label, value, href, color }, i) => (
<motion.a
key={label}
{...fadeUp(0.15 + i * 0.1)}
href={href}
className="glass"
style={{
padding: "1.2rem 1.5rem", display: "flex", alignItems: "center", gap: "1rem",
textDecoration: "none", color: "inherit", transition: "transform 0.2s ease, box-shadow 0.2s ease",
}}
whileHover={{ y: -3, boxShadow: `0 8px 24px color-mix(in hsl, ${color}, transparent 70%)` }}
>
<div style={{
width: "2.6rem", height: "2.6rem", borderRadius: "0.65rem", flexShrink: 0,
background: `color-mix(in hsl, ${color}, transparent 82%)`,
border: `1px solid color-mix(in hsl, ${color}, transparent 55%)`,
display: "flex", alignItems: "center", justifyContent: "center",
fontSize: "1.1rem", color,
}}>
{icon}
</div>
<div>
<div style={{ fontSize: "0.72rem", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.06em", color: "color-mix(in hsl, var(--c-text), transparent 50%)", marginBottom: "0.15rem" }}>{label}</div>
<div style={{ fontWeight: 600, fontSize: "0.95rem" }}>{value}</div>
</div>
</motion.a>
))}
{/* Social links */}
<motion.div {...fadeUp(0.35)} style={{ display: "flex", gap: "0.75rem", paddingTop: "0.5rem" }}>
{SOCIAL_LINKS.map(({ icon, href, label, color }) => (
<a
key={label}
href={href}
target="_blank"
rel="noopener noreferrer"
title={label}
style={{
width: "2.8rem", height: "2.8rem", borderRadius: "0.75rem",
background: "color-mix(in hsl, var(--c-background-light), transparent 20%)",
border: "1px solid color-mix(in hsl, var(--c-lines), transparent 60%)",
display: "flex", alignItems: "center", justifyContent: "center",
fontSize: "1.2rem", color: "color-mix(in hsl, var(--c-text), transparent 30%)",
textDecoration: "none", transition: "color 0.2s ease, border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease",
}}
onMouseEnter={e => {
e.currentTarget.style.color = color;
e.currentTarget.style.borderColor = color;
e.currentTarget.style.transform = "translateY(-3px)";
e.currentTarget.style.boxShadow = `0 4px 16px color-mix(in hsl, ${color}, transparent 55%)`;
}}
onMouseLeave={e => {
e.currentTarget.style.color = "color-mix(in hsl, var(--c-text), transparent 30%)";
e.currentTarget.style.borderColor = "color-mix(in hsl, var(--c-lines), transparent 60%)";
e.currentTarget.style.transform = "translateY(0)";
e.currentTarget.style.boxShadow = "none";
}}
>
{icon}
</a>
))}
</motion.div>
</div>
</div>
<div className="glass p-6 max-w-lg mt-8">
<p><strong>Email:</strong> <a href="mailto:brunovontor@gmail.com" className="hover:text-[var(--c-other)]">brunovontor@gmail.com</a></p>
<p className="mt-2"><strong>Phone:</strong> <a href="tel:+420605512624" className="hover:text-[var(--c-other)]">+420 605 512 624</a></p>
{/* Right — envelope form (untouched) */}
<motion.div {...fadeUp(0.2)} style={{ display: "flex", justifyContent: "center" }}>
<ContactMeForm />
</motion.div>
</div>
</div>
{/* Responsive: stack on mobile */}
<style>{`
@media (max-width: 768px) {
.contact-grid { grid-template-columns: 1fr !important; gap: 2.5rem !important; }
}
`}</style>
</section>
);
}
}

View File

@@ -1,4 +1,7 @@
import { useEffect } from "react";
import { motion } from "framer-motion";
import { FaEnvelope, FaPhone } from "react-icons/fa";
import { Link } from "react-router-dom";
import HeroSection from "@/components/home/hero/HeroSection";
import DroneSection from "@/components/home/drone/DroneSection";
import WebDevSection from "@/components/home/webdev/WebDevSection";
@@ -7,7 +10,6 @@ import TechMarquee from "@/components/home/tech/TechMarquee";
import ContactMeForm from "@/components/home/ContactMe/ContactMeForm";
export default function Home() {
// Spark cursor on click
useEffect(() => {
const handleClick = (event: MouseEvent) => {
const spark = document.createElement("div");
@@ -43,7 +45,125 @@ export default function Home() {
<div className="divider" />
<TechMarquee />
<div className="divider" />
<ContactMeForm />
{/* Contact section */}
<section id="contact" style={{ background: "color-mix(in hsl, var(--c-background), black 10%)", padding: "5rem 1.5rem 6rem", position: "relative", overflow: "hidden" }}>
{/* Background glow */}
<div style={{ position: "absolute", bottom: "-10%", left: "50%", transform: "translateX(-50%)", width: "600px", height: "300px", borderRadius: "50%", background: "color-mix(in hsl, var(--c-other), transparent 90%)", filter: "blur(80px)", zIndex: 0, pointerEvents: "none" }} />
<div className="container" style={{ position: "relative", zIndex: 1 }}>
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 28 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
style={{ textAlign: "center", marginBottom: "3.5rem" }}
>
<span style={{
display: "inline-block", padding: "0.3em 0.9em", borderRadius: "9999px",
background: "color-mix(in hsl, var(--c-other), transparent 82%)",
border: "1px solid color-mix(in hsl, var(--c-other), transparent 50%)",
color: "var(--c-other)", fontSize: "0.78rem", fontWeight: 700,
letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: "1rem",
}}>
Contact
</span>
<h2 style={{ fontSize: "clamp(1.9rem, 4.5vw, 3rem)", fontWeight: 800, margin: "0 0 0.8rem" }}>
Let's{" "}
<span style={{
background: "linear-gradient(135deg, var(--c-other), var(--c-lines))",
WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", backgroundClip: "text",
}}>
Work Together
</span>
</h2>
<p style={{ color: "color-mix(in hsl, var(--c-text), transparent 40%)", maxWidth: "440px", margin: "0 auto", lineHeight: 1.7 }}>
Click the envelope to open it or reach out directly.
</p>
</motion.div>
{/* Two-column: info left, form right */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1.5fr", gap: "4rem", alignItems: "center" }}>
{/* Left — contact info */}
<motion.div
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
style={{ display: "flex", flexDirection: "column", gap: "1rem" }}
>
{[
{ icon: <FaEnvelope />, label: "Email", value: "brunovontor@gmail.com", href: "mailto:brunovontor@gmail.com", color: "var(--c-other)" },
{ icon: <FaPhone />, label: "Phone", value: "+420 605 512 624", href: "tel:+420605512624", color: "var(--c-lines)" },
].map(({ icon, label, value, href, color }) => (
<a
key={label}
href={href}
className="glass"
style={{
padding: "1rem 1.3rem", display: "flex", alignItems: "center", gap: "1rem",
textDecoration: "none", color: "inherit", borderRadius: "1rem",
transition: "transform 0.2s ease",
}}
onMouseEnter={e => (e.currentTarget.style.transform = "translateY(-3px)")}
onMouseLeave={e => (e.currentTarget.style.transform = "translateY(0)")}
>
<div style={{
width: "2.4rem", height: "2.4rem", borderRadius: "0.6rem", flexShrink: 0,
background: `color-mix(in hsl, ${color}, transparent 82%)`,
border: `1px solid color-mix(in hsl, ${color}, transparent 55%)`,
display: "flex", alignItems: "center", justifyContent: "center",
fontSize: "1rem", color,
}}>
{icon}
</div>
<div>
<div style={{ fontSize: "0.7rem", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.06em", color: "color-mix(in hsl, var(--c-text), transparent 50%)", marginBottom: "0.1rem" }}>{label}</div>
<div style={{ fontWeight: 600, fontSize: "0.9rem" }}>{value}</div>
</div>
</a>
))}
<Link
to="/contact"
style={{
marginTop: "0.5rem",
display: "inline-flex", alignItems: "center", gap: "0.5rem",
padding: "0.7em 1.6em", borderRadius: "9999px",
background: "color-mix(in hsl, var(--c-other), transparent 80%)",
border: "1px solid color-mix(in hsl, var(--c-other), transparent 50%)",
color: "var(--c-other)", fontWeight: 600, fontSize: "0.88rem",
textDecoration: "none", transition: "background 0.2s ease, transform 0.2s ease",
width: "fit-content",
}}
onMouseEnter={e => { e.currentTarget.style.background = "color-mix(in hsl, var(--c-other), transparent 60%)"; e.currentTarget.style.transform = "scale(1.04)"; }}
onMouseLeave={e => { e.currentTarget.style.background = "color-mix(in hsl, var(--c-other), transparent 80%)"; e.currentTarget.style.transform = "scale(1)"; }}
>
Full Contact Page
</Link>
</motion.div>
{/* Right — envelope form */}
<motion.div
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
viewport={{ once: true }}
style={{ display: "flex", justifyContent: "center" }}
>
<ContactMeForm />
</motion.div>
</div>
</div>
<style>{`
@media (max-width: 768px) {
#contact .container > div:last-child { grid-template-columns: 1fr !important; gap: 2rem !important; }
}
`}</style>
</section>
</main>
);
}