This commit is contained in:
2025-10-21 22:25:26 +02:00
parent b9c74e12bc
commit e4048f90e5
31 changed files with 872 additions and 62 deletions

View File

@@ -3,7 +3,7 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import HomeLayout from "./layout/Home";
import DashboardLayout from "./layout/Dashboard";
import Home from "./pages/Home";
import Home from "./pages/Home/Home";
import Dashboard from "./pages/Dashboard";
import PrivateRoute from "./routes/Privateroute";

View File

@@ -0,0 +1,21 @@
import { useTranslation } from 'react-i18next';
export default function LanguageSwitcher() {
const { i18n } = useTranslation();
const changeLang = (e: React.ChangeEvent<HTMLSelectElement>) => {
void i18n.changeLanguage(e.target.value);
};
return (
<select
aria-label="Language selector"
onChange={changeLang}
value={i18n.resolvedLanguage}
className="bg-transparent text-white/80 border border-white/20 rounded-md px-2 py-1 text-sm hover:bg-white/10"
>
<option value="cs">CS</option>
<option value="en">EN</option>
</select>
);
}

View File

@@ -0,0 +1,122 @@
import { useState } from "react";
import { Link, NavLink } from "react-router-dom";
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
import LanguageSwitcher from "./LanguageSwitcher";
export default function Nav() {
const [open, setOpen] = useState(false);
const { t } = useTranslation();
const linkBase =
"px-3 py-2 rounded-md text-sm font-medium transition-colors text-white/80 hover:text-white";
const active = ({ isActive }: { isActive: boolean }) =>
`${linkBase} ${isActive ? "text-white" : ""}`;
return (
<motion.header
initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.4 }}
className="sticky top-0 z-50 w-full backdrop-blur bg-black/70 border-b border-gray-800"
>
<nav className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex h-16 items-center justify-between">
{/* Brand */}
<Link to="/" className="flex items-center m-0 p-0" aria-label={t('nav.brandAlt')}>
<img
src="/public/images/logo.png"
alt={t('nav.brandAlt')}
className="block h-16 w-auto m-0 p-0 object-contain select-none"
draggable={false}
/>
</Link>
{/* Desktop nav */}
<div className="hidden md:flex items-center gap-6">
<NavLink to="/" className={active} end>
{t('nav.home')}
</NavLink>
<NavLink to="/dashboard" className={active}>
{t('nav.dashboard')}
</NavLink>
<a href="#services" className={`${linkBase}`}>
{t('nav.services')}
</a>
<a href="#contact" className={`${linkBase}`}>
{t('nav.contact')}
</a>
<Link
to="/dashboard"
className="ml-2 inline-flex items-center gap-2 rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-700"
>
{t('nav.bookService')}
</Link>
<LanguageSwitcher />
</div>
{/* Mobile toggle */}
<button
className="md:hidden inline-flex items-center justify-center rounded-md p-2 text-white/80 hover:bg-white/10"
aria-label="Toggle navigation"
onClick={() => setOpen((v) => !v)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6"
>
{open ? (
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
) : (
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
)}
</svg>
</button>
</div>
{/* Mobile menu */}
{open && (
<div className="md:hidden pb-4">
<div className="flex flex-col gap-2">
<NavLink to="/" className={active} end onClick={() => setOpen(false)}>
{t('nav.home')}
</NavLink>
<NavLink to="/dashboard" className={active} onClick={() => setOpen(false)}>
{t('nav.dashboard')}
</NavLink>
<a href="#services" className={`${linkBase}`} onClick={() => setOpen(false)}>
{t('nav.services')}
</a>
<a href="#contact" className={`${linkBase}`} onClick={() => setOpen(false)}>
{t('nav.contact')}
</a>
<Link
to="/dashboard"
className="mt-2 inline-flex items-center justify-center rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-700"
onClick={() => setOpen(false)}
>
{t('nav.bookService')}
</Link>
<div className="pt-2">
<LanguageSwitcher />
</div>
</div>
</div>
)}
</nav>
</motion.header>
);
}

View File

@@ -1,18 +1,115 @@
import { motion } from "framer-motion";
import styles from "./footer.module.css";
import { FaPhone, FaEnvelope, FaInstagram, FaMapMarkerAlt, FaUser } from "react-icons/fa";
import { useTranslation } from "react-i18next";
export default function Footer() {
const { t } = useTranslation();
return (
<motion.footer
initial={{ y: 80, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.4, delay: 0.2 }}
className={`${styles.footer} bg-white shadow-inner py-4 px-6 mt-8 text-center text-gray-500`}
className={`${styles.footer} bg-gradient-to-t from-zinc-950 to-black border-t border-gray-800 pt-10 px-6 mt-8 text-white/80`}
>
<span>
&copy; {new Date().getFullYear()} MyDashboard. All rights reserved.
</span>
<div className="mx-auto max-w-7xl">
{/* Top content: contacts + address + map */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-start">
{/* Left column: Kontakt + Adresa stacked */}
<div className="space-y-8">
{/* Contacts */}
<section aria-labelledby="footer-contacts">
<h3 id="footer-contacts" className="text-white text-lg font-semibold flex items-center gap-2">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-red-600" />
{t('footer.contacts')}
</h3>
<ul className="mt-4 space-y-5 md:space-y-6 text-sm w-fit">
{/* Contact block: Rostislav (stacked) */}
<li className="rounded-lg bg-white/5 ring-1 ring-white/10 p-3 md:p-4">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<FaUser className="text-white/70" />
<span>Rostislav Pecha</span>
</div>
<a href="tel:+420777309595" className="flex items-center gap-2 hover:text-white w-fit">
<FaPhone className="text-white/70" />
<span className="whitespace-nowrap">+420 777 309 595</span>
</a>
</div>
</li>
{/* Contact block: Lukáš (stacked) */}
<li className="rounded-lg bg-white/5 ring-1 ring-white/10 p-3 md:p-4">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<FaUser className="text-white/70" />
<span>Lukáš Pecha</span>
</div>
<a href="tel:+420775309595" className="flex items-center gap-2 hover:text-white w-fit">
<FaPhone className="text-white/70" />
<span className="whitespace-nowrap">+420 775 309 595</span>
</a>
</div>
</li>
<li className="flex items-center gap-2">
<FaEnvelope className="text-white/70" />
<a href="mailto:prevodovky@centrum.cz" className="hover:text-white">prevodovky@centrum.cz</a>
</li>
<li className="flex items-center gap-2">
<FaInstagram className="text-white/70" />
<a
href="https://instagram.com/at_automatic_transmision_cz"
target="_blank"
rel="noopener noreferrer"
className="hover:text-white"
>
@at_automatic_transmision_cz
</a>
</li>
</ul>
</section>
{/* Address */}
<section aria-labelledby="footer-address">
<h3 id="footer-address" className="text-white text-lg font-semibold flex items-center gap-2">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-red-600" />
{t('footer.address')}
</h3>
<div className="mt-3 flex items-start gap-3 text-sm">
<FaMapMarkerAlt className="mt-1 text-white/70" />
<address className="not-italic">
U haldy 60/32
<br />
700 30 Ostrava-Hrabůvka
<br />
Czech Republic
</address>
</div>
</section>
</div>
{/* Right column: Map */}
<section aria-labelledby="footer-map">
<h3 id="footer-map" className="sr-only">{t('footer.map')}</h3>
<div className="w-full overflow-hidden rounded-lg ring-1 ring-gray-800 shadow-sm">
<iframe
title={t('footer.mapTitle')}
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d453.46808205152774!2d18.270652943594587!3d49.79286407473225!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x4713e4b479250855%3A0x5ac735eecc1a7dd4!2sAutomatic%20Transmission%20CZ%20s.r.o.%20-%20Automatick%C3%A9%20p%C5%99evodovky%20Ostrava%20Hrab%C5%AFvka!5e1!3m2!1scs!2scz!4v1761069490812!5m2!1scs!2scz"
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
allowFullScreen
className="block w-full h-[280px] sm:h-[340px] lg:h-[420px]"
style={{ border: 0 }}
/>
</div>
</section>
</div>
{/* Bottom bar */}
<div className="mt-8 border-t border-gray-800/80 py-4 text-center text-s text-white/60">
{new Date().getFullYear()} © <a style={{textDecoration: "underline"}} href="https://vontor.cz">vontor.cz</a>. {t('footer.rights')}
</div>
</div>
</motion.footer>
);
}

33
frontend/src/i18n.ts Normal file
View File

@@ -0,0 +1,33 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
// Import translation resources
import en from './locales/en/translation.json';
import cs from './locales/cs/translation.json';
void i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
en: { translation: en },
cs: { translation: cs },
},
fallbackLng: 'cs',
supportedLngs: ['en', 'cs'],
debug: false,
interpolation: {
escapeValue: false,
},
detection: {
// Use querystring, localStorage and navigator to detect language
order: ['querystring', 'localStorage', 'navigator', 'htmlTag', 'path', 'subdomain'],
caches: ['localStorage'],
},
react: {
useSuspense: false,
},
});
export default i18n;

View File

@@ -1,12 +1,13 @@
@import "tailwindcss";
/* Global baseline: white text, neutral link styles */
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
color-scheme: dark;
color: #ffffff;
background-color: #242424;
font-synthesis: none;
@@ -15,26 +16,19 @@
-moz-osx-font-smoothing: grayscale;
}
html, body {
margin: 0;
color: inherit;
background: inherit;
}
a {
font-weight: 500;
color: #646cff;
color: inherit;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
color: inherit;
}
button {
@@ -49,22 +43,9 @@ button {
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
border-color: #ffffff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -6,15 +6,15 @@ import { Link, Outlet } from "react-router-dom";
export default function DashboardLayout() {
return (
<div className="flex h-screen bg-gray-100 text-gray-900">
<div className="flex h-screen bg-black text-white">
{/* Sidebar */}
<motion.aside
initial={{ x: -250 }}
animate={{ x: 0 }}
transition={{ duration: 0.4 }}
className="w-64 bg-white shadow-xl flex flex-col"
className="w-64 bg-zinc-900 shadow-xl flex flex-col"
>
<div className="p-4 font-bold text-xl border-b border-gray-200">
<div className="p-4 font-bold text-xl border-b border-gray-800">
MyDashboard
</div>
<nav className="flex-1 p-4 space-y-2">
@@ -22,7 +22,7 @@ export default function DashboardLayout() {
<SidebarLink to="/profile" icon={<FaUser />} label="Profile" />
<SidebarLink to="/settings" icon={<FaCog />} label="Settings" />
</nav>
<div className="p-4 border-t border-gray-200">
<div className="p-4 border-t border-gray-800">
<SidebarLink to="/logout" icon={<FaSignOutAlt />} label="Logout" />
</div>
</motion.aside>
@@ -34,11 +34,11 @@ export default function DashboardLayout() {
initial={{ y: -80 }}
animate={{ y: 0 }}
transition={{ duration: 0.3 }}
className="bg-white shadow px-6 py-3 flex justify-between items-center"
className="bg-zinc-950 shadow px-6 py-3 flex justify-between items-center border-b border-gray-800"
>
<h1 className="text-lg font-semibold">Dashboard</h1>
<div className="flex items-center space-x-4">
<span className="text-gray-600">Welcome back!</span>
<span className="text-white/80">Welcome back!</span>
<img
src="https://ui-avatars.com/api/?name=User&background=0D8ABC&color=fff"
alt="avatar"
@@ -52,7 +52,7 @@ export default function DashboardLayout() {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
className="flex-1 overflow-y-auto p-6"
className="flex-1 overflow-y-auto p-6 bg-black"
>
<Outlet />
</motion.main>
@@ -71,7 +71,7 @@ function SidebarLink({ to, icon, label }: SidebarLinkProps) {
return (
<Link
to={to}
className="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-blue-100 hover:text-blue-700 transition-colors"
className="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-white/10 hover:text-white transition-colors"
>
<span className="text-lg">{icon}</span>
<span className="text-sm font-medium">{label}</span>

View File

@@ -1,15 +1,17 @@
import { Outlet } from "react-router-dom";
import Footer from "../components/main/footer/Footer.tsx";
import { motion } from "framer-motion";
import Nav from "../components/main/Nav";
export default function HomeLayout() {
return (
<div>
<Nav />
<motion.section
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="flex flex-col min-h-[80vh] justify-center items-center"
className="flex flex-col justify-center items-center"
>
<Outlet />
</motion.section>

View File

@@ -0,0 +1,50 @@
{
"nav": {
"home": "Domů",
"dashboard": "Administrace",
"services": "Služby",
"contact": "Kontakt",
"bookService": "Objednat servis",
"brandAlt": "Logo Automatic Transmission CZ"
},
"footer": {
"contacts": "Kontakty",
"address": "Adresa",
"map": "Mapa",
"mapTitle": "Mapa Automatic Transmission CZ",
"rights": "Všechna práva vyhrazena."
},
"home": {
"hero": {
"title": "Precizní péče o vůz, mechanici, kterým můžete věřit.",
"subtitle": "Od pravidelné údržby po složité opravy převodovek udržíme vás na cestě s transparentní cenou a možností vyřízení v ten samý den."
},
"cta": {
"book": "Objednat termín",
"services": "Naše služby"
},
"badges": {
"certified": "Certifikovaní technici",
"oem": "OEM díly",
"warranty": "Záruka v ceně"
},
"educated": {
"title": "Pravidelně školíme náš tým",
"subtitle": "Naši mechanici se neustále vzdělávají certifikace, nové technologie a osvědčené postupy přímo od výrobců."
}
},
"categories": {
"title": "AT Automatické převodovky (CZ)",
"subtitle": "Profesionální služby v oblasti automatických převodovek: od diagnostiky a oprav, přes repase měničů a řídicích jednotek, až po výměny kapalin a prodej kvalitních dílů."
},
"Opravy automatických převodovek": "Opravy automatických převodovek",
"Diagnostika, seřízení a kompletní generální opravy automatických převodovek všech běžných značek.": "Diagnostika, seřízení a kompletní generální opravy automatických převodovek všech běžných značek.",
"Hydrodynamické měniče": "Hydrodynamické měniče",
"Repase a výměny měničů točivého momentu včetně vyvážení a testování těsnosti.": "Repase a výměny měničů točivého momentu včetně vyvážení a testování těsnosti.",
"Řídící jednotky pro A.P.": "Řídící jednotky pro A.P.",
"Diagnostika a programování TCU/Mechatroniky, aktualizace SW a opravy elektroniky.": "Diagnostika a programování TCU/Mechatroniky, aktualizace SW a opravy elektroniky.",
"Výměny olejů a filtrů": "Výměny olejů a filtrů",
"Profesionální výměna převodových olejů metodou výměny za provozu (ATF) včetně filtrů.": "Profesionální výměna převodových olejů metodou výměny za provozu (ATF) včetně filtrů.",
"Prodej dílů do A.P.": "Prodej dílů do A.P.",
"Široká nabídka originálních i ověřených náhradních dílů pro automatické převodovky.": "Široká nabídka originálních i ověřených náhradních dílů pro automatické převodovky."
}

View File

@@ -0,0 +1,50 @@
{
"nav": {
"home": "Home",
"dashboard": "Dashboard",
"services": "Services",
"contact": "Contact",
"bookService": "Book service",
"brandAlt": "Automatic Transmission CZ logo"
},
"footer": {
"contacts": "Contacts",
"address": "Address",
"map": "Map",
"mapTitle": "Map Automatic Transmission CZ",
"rights": "All rights reserved."
},
"home": {
"hero": {
"title": "Precision car care, trusted mechanics.",
"subtitle": "From routine maintenance to complex transmission repairs, we keep you on the road with transparent pricing and sameday service options."
},
"cta": {
"book": "Book an appointment",
"services": "Our services"
},
"badges": {
"certified": "Certified technicians",
"oem": "OEM parts",
"warranty": "Warranty included"
},
"educated": {
"title": "We continuously train our team",
"subtitle": "Our technicians keep growing — certifications, new technologies, and best practices straight from OEMs."
}
},
"categories": {
"title": "AT Automatic transmissions (CZ)",
"subtitle": "Professional services in the field of automatic transmissions: from diagnostics and repairs, through torque converter and control unit overhauls, to fluid changes and sale of quality parts."
},
"Opravy automatických převodovek": "Automatic transmission repairs",
"Diagnostika, seřízení a kompletní generální opravy automatických převodovek všech běžných značek.": "Diagnostics, adjustment, and complete overhauls of automatic transmissions for all common brands.",
"Hydrodynamické měniče": "Hydrodynamic torque converters",
"Repase a výměny měničů točivého momentu včetně vyvážení a testování těsnosti.": "Overhaul and replacement of torque converters including balancing and leak testing.",
"Řídící jednotky pro A.P.": "Control units for A.T.",
"Diagnostika a programování TCU/Mechatroniky, aktualizace SW a opravy elektroniky.": "Diagnostics and programming of TCU/Mechatronics, software updates, and electronics repairs.",
"Výměny olejů a filtrů": "Oil and filter changes",
"Profesionální výměna převodových olejů metodou výměny za provozu (ATF) včetně filtrů.": "Professional transmission oil changes by dynamic exchange (ATF) including filters.",
"Prodej dílů do A.P.": "Spare parts for A.T.",
"Široká nabídka originálních i ověřených náhradních dílů pro automatické převodovky.": "Wide range of genuine and verified spare parts for automatic transmissions."
}

View File

@@ -1,6 +1,7 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import './i18n'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(

View File

@@ -1,16 +0,0 @@
// Home.tsx
import { motion } from "framer-motion";
export default function Home() {
return (
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="flex flex-col items-center justify-center min-h-[80vh]"
>
<h1 className="text-3xl font-bold">Welcome to home.</h1>
<p className="text-gray-600 mt-2">This is your home page content.</p>
</motion.div>
);
}

View File

@@ -0,0 +1,87 @@
// Home.tsx
import { motion } from "framer-motion";
import CategoriesSection from "./components/categories/CategoriesSection";
import EducatedTeam from "./components/educated_team/EducatedTeam";
import { useTranslation } from "react-i18next";
export default function Home() {
const { t } = useTranslation();
return (
<>
<section className="relative w-full min-h-[80vh] overflow-hidden rounded-xl shadow-sm">
{/* Background video */}
<video
className="absolute inset-0 h-full w-full object-cover grayscale brightness-75"
src="/videos/speedup.mp4"
autoPlay
muted
loop
playsInline
poster="/assets/garage-poster.jpg"
/>
{/* Overlay to ensure text readability */}
<div className="absolute inset-0 bg-gradient-to-b from-black/50 via-black/30 to-black/70" />
{/* Content */}
<div className="relative z-10 mx-auto flex max-w-7xl items-center px-6 py-16 sm:px-8 md:py-24 lg:py-28 min-h-[80vh]">
<div className="max-w-2xl text-white">
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-4xl font-extrabold tracking-tight sm:text-5xl"
>
{t('home.hero.title')}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.15 }}
className="mt-4 text-lg text-white/90 max-w-xl"
>
{t('home.hero.subtitle')}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="mt-8 flex flex-wrap items-center gap-3"
>
<a
href="#contact"
className="inline-flex items-center gap-2 rounded-md bg-red-600 px-6 py-3 text-sm font-semibold text-white shadow hover:bg-red-700"
>
{t('home.cta.book')}
</a>
<a
href="#services"
className="inline-flex items-center gap-2 rounded-md bg-white/10 px-6 py-3 text-sm font-semibold text-white ring-1 ring-inset ring-white/30 hover:bg-white/20"
>
{t('home.cta.services')}
</a>
</motion.div>
{/* Quick badges */}
<motion.ul
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.45 }}
className="mt-6 flex flex-wrap gap-2 text-xs text-gray-200"
>
<li className="rounded bg-black/30 px-2.5 py-1">{t('home.badges.certified')}</li>
<li className="rounded bg-black/30 px-2.5 py-1">{t('home.badges.oem')}</li>
<li className="rounded bg-black/30 px-2.5 py-1">{t('home.badges.warranty')}</li>
</motion.ul>
</div>
</div>
{/* Optional anchors for nav */}
<div id="contact" className="relative z-10" />
</section>
<CategoriesSection />
<EducatedTeam />
</>
);
}

View File

@@ -0,0 +1,12 @@
import CategoryCard from "./CategoryCard";
import { categories } from "./categoriesData";
export default function CategoriesGrid() {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
{categories.map((c) => (
<CategoryCard key={c.id} category={c} />
))}
</div>
);
}

View File

@@ -0,0 +1,26 @@
import { motion } from "framer-motion";
import CategoriesGrid from "./CategoriesGrid";
import { useTranslation } from "react-i18next";
export default function CategoriesSection() {
const { t } = useTranslation();
return (
<section id="services" className="relative mx-auto max-w-7xl px-6 sm:px-8 py-12 md:py-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.3 }}
transition={{ duration: 0.5 }}
className="mb-8 md:mb-10"
>
<h2 className="text-2xl md:text-3xl font-extrabold tracking-tight text-white">
{t('categories.title')}
</h2>
<p className="mt-2 text-white/70 max-w-2xl">
{t('categories.subtitle')}
</p>
</motion.div>
<CategoriesGrid />
</section>
);
}

View File

@@ -0,0 +1,38 @@
import { motion } from "framer-motion";
import type { Category } from "./types";
import { FaCogs } from "react-icons/fa";
import { useTranslation } from "react-i18next";
interface Props {
category: Category;
}
export default function CategoryCard({ category }: Props) {
const { t } = useTranslation();
const { title, description, icon: IconFromData } = category;
const Icon = IconFromData ?? FaCogs;
return (
<motion.article
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.2 }}
transition={{ duration: 0.4 }}
className="group relative overflow-hidden rounded-xl bg-zinc-900/80 ring-1 ring-zinc-800 hover:ring-red-600/60 p-5 sm:p-6"
>
<div className="absolute -right-8 -top-8 h-24 w-24 rounded-full bg-red-600/10 blur-xl transition-opacity group-hover:opacity-70" />
<div className="relative z-10 flex items-start gap-4">
<div className="grid h-12 w-12 place-items-center rounded-lg bg-red-600/20 text-2xl text-white">
<Icon aria-hidden className="h-6 w-6" />
</div>
<div>
<h3 className="text-white text-lg font-semibold leading-tight">
{t(title)}
</h3>
{description && (
<p className="mt-1 text-sm text-white/70">{t(description)}</p>
)}
</div>
</div>
</motion.article>
);
}

View File

@@ -0,0 +1,36 @@
import type { Category } from "./types";
import { FaTools, FaCogs, FaMicrochip, FaOilCan, FaShoppingCart } from "react-icons/fa";
// AT - Automatic transmission CZ
export const categories: Category[] = [
{
id: "opravy-prevodovek",
title: "Opravy automatických převodovek",
description: "Diagnostika, seřízení a kompletní generální opravy automatických převodovek všech běžných značek.",
icon: FaTools,
},
{
id: "menice",
title: "Hydrodynamické měniče",
description: "Repase a výměny měničů točivého momentu včetně vyvážení a testování těsnosti.",
icon: FaCogs,
},
{
id: "ridici-jednotky",
title: "Řídící jednotky pro A.P.",
description: "Diagnostika a programování TCU/Mechatroniky, aktualizace SW a opravy elektroniky.",
icon: FaMicrochip,
},
{
id: "vymeny-kapalin",
title: "Výměny olejů a filtrů",
description: "Profesionální výměna převodových olejů metodou výměny za provozu (ATF) včetně filtrů.",
icon: FaOilCan,
},
{
id: "prodej-dilu",
title: "Prodej dílů do A.P.",
description: "Široká nabídka originálních i ověřených náhradních dílů pro automatické převodovky.",
icon: FaShoppingCart,
},
];

View File

@@ -0,0 +1,8 @@
import type { IconType } from "react-icons";
export interface Category {
id: string;
title: string;
description?: string;
icon?: IconType; // react-icons component
}

View File

@@ -0,0 +1,64 @@
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
type Slide = {
src: string;
alt: string;
};
export default function EducatedTeam() {
const { t } = useTranslation();
const slides: Slide[] = [
{ src: "/images/Seminar/team.jpg", alt: "Team in training" },
{ src: "/images/Seminar/teacher.jpg", alt: "Seminar teacher" },
{ src: "/images/Seminar/poster.jpg", alt: "Training poster" },
];
const duplicated = [...slides, ...slides]; // seamless loop
return (
<section className="mx-auto my-16 w-full max-w-7xl px-6 sm:px-8">
<div className="relative overflow-hidden rounded-xl">
{/* Background continuous slider */}
<div className="relative h-72 w-full sm:h-80 md:h-[26rem]">
<motion.div
className="absolute inset-0 flex"
animate={{ x: ["0%", "-50%"] }}
transition={{ duration: 25, ease: "linear", repeat: Infinity }}
aria-hidden
>
{duplicated.map((s, i) => (
<div key={i} className="relative h-full w-full flex-shrink-0">
<img
src={s.src}
alt={s.alt}
className="h-full w-full object-cover"
loading={i === 0 ? "eager" : "lazy"}
/>
{/* Subtle vignette for readability */}
<div className="absolute inset-0 bg-gradient-to-t from-black/50 via-black/20 to-transparent" />
</div>
))}
</motion.div>
{/* Centered text overlay */}
<div className="relative z-10 flex h-full w-full items-center justify-center">
<div className="mx-auto max-w-2xl text-center text-white">
<h2 className="text-2xl font-extrabold tracking-tight sm:text-3xl md:text-4xl drop-shadow">
{t("home.educated.title")}
</h2>
<p className="mt-3 text-sm leading-6 text-white/90 sm:text-base md:text-lg drop-shadow">
{t("home.educated.subtitle")}
</p>
</div>
</div>
{/* Outer gradient edges */}
<div className="pointer-events-none absolute inset-0 bg-gradient-to-r from-black/30 via-transparent to-black/30" />
</div>
</div>
</section>
);
}

View File

@@ -38,7 +38,7 @@ export default function PrivateRoute() {
return (
<div className="flex items-center justify-center h-screen">
<motion.div
className="w-16 h-16 border-4 border-blue-500 border-t-transparent rounded-full"
className="w-16 h-16 border-4 border-white border-t-transparent rounded-full"
animate={{ rotate: 360 }}
transition={{ repeat: Infinity, duration: 1, ease: "linear" }}
/>