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:
115
frontend/src/components/services/Services.tsx
Normal file
115
frontend/src/components/services/Services.tsx
Normal 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 až 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>
|
||||
);
|
||||
}
|
||||
|
||||
93
frontend/src/components/services/services.module.css
Normal file
93
frontend/src/components/services/services.module.css
Normal 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user