Refactor email system and add contact form backend

Refactored email sending to use a single HTML template with a base layout, removed plain text email templates, and updated all related backend logic. Introduced a new ContactMe model, serializer, Celery task, and API endpoints for handling contact form submissions, including email notifications. Renamed ShopConfiguration to SiteConfiguration throughout the backend for consistency. Updated frontend to remove unused components, add a new Services section, and adjust navigation and contact form integration.
This commit is contained in:
2025-12-12 01:52:41 +01:00
parent df83288591
commit 564418501c
34 changed files with 433 additions and 327 deletions

View File

@@ -73,6 +73,7 @@ export default function ContactMeForm() {
placeholder="Vaše zpráva"
required
/>
<input type="hidden" name="state" />
<input type="submit"/>
</form>
</div>

View File

@@ -1,8 +0,0 @@
import { useEffect, useState } from "react";
export default function HeroCarousel() {
return (
<>
</>
);
}

View File

@@ -1,21 +0,0 @@
export default function HostingSecuritySection() {
return (
<section id="hosting" className="py-20">
<div className="container mx-auto px-4">
<h2 className="text-3xl md:text-4xl font-bold mb-6 text-rainbow">Hosting & Protection</h2>
<div className="card p-6 md:p-8">
<p className="text-brand-text opacity-80">We host our applications ourselves, which reduces hosting costs as projects scale.</p>
<p className="text-brand-text opacity-80 mt-2">All websites are protected by Cloudflare and optimized for performance.</p>
<div className="grid grid-cols-3 md:grid-cols-6 gap-4 text-center text-sm mt-6">
{['Server', 'Cloudflare', 'Docker', 'SSL', 'Monitoring', 'Scaling'].map(item => (
<div key={item} className="p-3 rounded-lg bg-brandGradient text-white shadow-glow transition-transform duration-200 hover:scale-105 flex flex-col items-center justify-center">
<span className="text-xs tracking-wide">{item}</span>
</div>
))}
</div>
</div>
</div>
</section>
);
}

View File

@@ -113,7 +113,6 @@ export default function Navbar({ user, onLogin, onLogout }: NavbarProps) {
</div>
<a className={styles.linkSimple} href="#contacts"><FaGlobe className={styles.iconSmall}/> Kontakt</a>
<a className={styles.linkSimple} href="/projects"><FaProjectDiagram className={styles.iconSmall}/> Projekty</a>
{/* right: user area */}
{!user ? (
@@ -136,9 +135,9 @@ export default function Navbar({ user, onLogin, onLogout }: NavbarProps) {
</button>
<div className={styles.dropdown} role="menu" aria-label="Uživatelské menu">
<a href="/profile" role="menuitem">Profil</a>
<a href="/billing" role="menuitem">Nastavení</a>
<a href="/billing" role="menuitem">Platby</a>
<a href="/me/profile" role="menuitem">Profil</a>
<a href="/me/settings" role="menuitem">Nastavení</a>
<a href="/me/billing" role="menuitem">Platby</a>
<button className={styles.logoutBtn} onClick={onLogout} role="menuitem">
<FaSignOutAlt className={styles.iconSmall} /> Odhlásit se

View File

@@ -1,5 +1,6 @@
.navbar {
width: 80%;
width: 50%;
width: max-content;
margin: auto;
padding: 0 2em;
background-color: var(--c-boxes);
@@ -11,7 +12,7 @@
position: sticky;
top: 0;
z-index: 50;
gap: 1rem;
gap: 1em;
border-bottom-left-radius: 2em;
border-bottom-right-radius: 2em;
@@ -56,7 +57,7 @@
/* Links container */
.links {
display: flex;
gap: 1.6rem;
gap: 3em;
align-items: center;
justify-content: space-around;
width: -webkit-fill-available;

View File

@@ -0,0 +1,115 @@
import { FaLaptopCode, FaVideo, FaCog } from "react-icons/fa";
import { MdLaunch, MdFolder, MdPhone } from "react-icons/md";
import styles from "./services.module.css";
type ServiceItem = {
title: string;
subtitle: string;
description: string;
icon: React.ComponentType<{ className?: string }>;
demoLink?: string;
portfolioLink?: string;
contactLink?: string;
align: "left" | "right";
};
const services: ServiceItem[] = [
{
title: "Weby",
subtitle: "Od prezentačních až po komplexní",
description:
"Tvorba webových stránek všech velikostí - od jednoduchých prezentačních po složité aplikace s databází, e-shopy a portály.",
icon: FaLaptopCode,
demoLink: "/demo/websites",
portfolioLink: "/portfolio#websites",
contactLink: "/contact?service=web",
align: "left",
},
{
title: "Filmařina",
subtitle: "Natáčení, drony, střih",
description:
"Natáčení na místě, drony, reels/TikTok videa a samozřejmě střih. Kompletní video produkce od námětu po finální video.",
icon: FaVideo,
portfolioLink: "/portfolio#videos",
contactLink: "/contact?service=video",
align: "right",
},
{
title: "Servis Dronů",
subtitle: "Opravy a údržba",
description:
"Profesionální servis a údržba dronů. Diagnostika, opravy, kalibrace a poradenství pro bezpečný provoz.",
icon: FaCog,
contactLink: "/contact?service=drone",
align: "left",
},
];export default function Services() {
return (
<section className={`section ${styles.wrapper}`} aria-labelledby="sluzby-heading">
<div className="container">
<header className={styles.header}>
<h2 id="sluzby-heading" className="text-3xl md:text-4xl font-bold tracking-tight">
Služby, které nabízím
</h2>
<p className="mt-3 text-neutral-300 max-w-2xl">
Od návrhu po produkční nasazení. Stavím udržitelné systémy s důrazem na
výkon, bezpečnost a spolehlivost. Základem jsou jasné procesy, čistý kód a
měřitelné výsledky.
</p>
</header>
<div className={styles.servicesStack}>
{services.map((service, idx) => {
const IconComponent = service.icon;
return (
<article
key={service.title}
className={`${styles.card} ${styles[service.align]} ${styles[`card${(idx % 3) + 1}`]}`}
>
<div className={styles.cardBg} />
<div className={styles.cardInner}>
<div className={styles.cardHeader}>
<div className={styles.iconWrapper}>
<IconComponent className={styles.icon} />
</div>
<h3 className="text-xl md:text-2xl font-semibold">{service.title}</h3>
<span className="text-sm md:text-base opacity-80">{service.subtitle}</span>
</div>
<p className="mt-3 text-sm md:text-base text-neutral-300">{service.description}</p>
<div className={styles.actionButtons}>
{service.demoLink && (
<a href={service.demoLink} className={styles.button}>
<MdLaunch className={styles.buttonIcon} />
Demo
</a>
)}
{service.portfolioLink && (
<a href={service.portfolioLink} className={styles.button}>
<MdFolder className={styles.buttonIcon} />
Portfolio
</a>
)}
{service.contactLink && (
<a href={service.contactLink} className={styles.buttonPrimary}>
<MdPhone className={styles.buttonIcon} />
Kontakt
</a>
)}
</div>
</div>
</article>
);
})}
</div> <footer className={styles.cta}>
<a href="/contact" className="glass px-5 py-3 rounded-lg font-medium">
Pojďme to probrat
</a>
</footer>
</div>
</section>
);
}

View File

@@ -0,0 +1,93 @@
/* Services section styles with clip-path animations */
.wrapper {
position: relative;
padding-top: 4rem;
padding-bottom: 4rem;
}
.header {
display: grid;
gap: 0.75rem;
margin-bottom: 3rem;
text-align: center;
}
.servicesStack {
display: flex;
flex-direction: column;
gap: 2rem;
max-width: 800px;
margin: 0 auto;
}.card {
position: relative;
isolation: isolate;
border-radius: 16px;
overflow: hidden;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
backdrop-filter: saturate(140%) blur(8px);
}
.cardBg {
position: absolute;
inset: 0;
z-index: -1;
background: radial-gradient(1200px 400px at 10% 10%, rgba(99, 102, 241, 0.25), transparent 60%),
radial-gradient(900px 300px at 90% 90%, rgba(16, 185, 129, 0.25), transparent 60%);
clip-path: polygon(0% 0%, 60% 0%, 100% 40%, 100% 100%, 0% 100%);
animation: floatClip 10s ease-in-out infinite;
}
.cardInner {
padding: 1rem;
}
.cardHeader {
display: grid;
gap: 0.25rem;
}
.icon {
font-size: 2rem;
line-height: 1;
margin-bottom: 0.5rem;
}/* Removed tags styles as not needed for simplified version */
/* Variant hues per card for subtle differentiation */
.card1 .cardBg {
background: radial-gradient(1200px 400px at 10% 10%, rgba(99, 102, 241, 0.35), transparent 60%),
radial-gradient(900px 300px at 90% 90%, rgba(56, 189, 248, 0.25), transparent 60%);
}
.card2 .cardBg {
background: radial-gradient(1200px 400px at 10% 10%, rgba(16, 185, 129, 0.35), transparent 60%),
radial-gradient(900px 300px at 90% 90%, rgba(245, 158, 11, 0.25), transparent 60%);
}
.card3 .cardBg {
background: radial-gradient(1200px 400px at 10% 10%, rgba(236, 72, 153, 0.35), transparent 60%),
radial-gradient(900px 300px at 90% 90%, rgba(59, 130, 246, 0.25), transparent 60%);
}
/* Removed card4 as we only have 3 services now */@keyframes floatClip {
0% {
clip-path: polygon(0% 0%, 60% 0%, 100% 40%, 100% 100%, 0% 100%);
transform: translate3d(0, 0, 0);
}
50% {
clip-path: polygon(0% 0%, 55% 0%, 100% 35%, 100% 100%, 0% 100%);
transform: translate3d(0, -6px, 0);
}
100% {
clip-path: polygon(0% 0%, 60% 0%, 100% 40%, 100% 100%, 0% 100%);
transform: translate3d(0, 0, 0);
}
}
.cta {
display: flex;
justify-content: center;
margin-top: 2rem;
}

View File

@@ -1,35 +0,0 @@
const categories: { name: string; items: string[] }[] = [
{ name: 'Experience', items: ['Freelance projects', 'Collaborations', 'Open-source contributions'] },
{ name: 'Backend', items: ['Django', 'Python', 'REST API', 'PostgreSQL', 'Celery', 'Docker'] },
{ name: 'Frontend', items: ['React', 'Tailwind', 'Vite', 'TypeScript', 'ShadCN', 'Bootstrap'] },
{ name: 'DevOps / Hosting', items: ['Nginx', 'Docker Compose', 'SSL', 'Cloudflare', 'Self-hosting'] },
{ name: 'Other Tools', items: ['Git', 'VSCode', 'WebRTC', 'ESP32', 'Automation'] },
];
export default function SkillsSection() {
return (
<section id="skills" className="py-20 bg-gradient-to-b from-brand-bg to-brand-bgLight">
<div className="container mx-auto px-4">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold mb-4 text-rainbow">Skills</h2>
<p className="text-brand-text/80 max-w-2xl mx-auto">Core technologies and tools I use across backend, frontend, infrastructure and hardware.</p>
</div>
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{categories.map(cat => (
<div key={cat.name} className="card p-6">
<h3 className="font-semibold mb-4 text-rainbow tracking-wide">{cat.name}</h3>
<ul className="space-y-2 text-sm">
{cat.items.map(i => (
<li key={i} className="flex items-center gap-2 text-brand-text/80">
<span className="inline-block w-2 h-2 rounded-full bg-brand-accent" />
<span className="group-hover:text-brand-text transition-colors">{i}</span>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</section>
);
}