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

@@ -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;
}