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

57
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,57 @@
# Copilot Instructions for This Repo
This workspace has a small Flask backend and a Vite + React + TypeScript frontend. Use these notes to be productive immediately and follow the codebases patterns.
## Big picture
- Frontend: `frontend/` using Vite, React Router v7, Tailwind v4, Framer Motion, i18next.
- Backend: `backend/` small Flask service for form email with reCAPTCHA.
- Integration: The current frontend API client expects a JWT/auth API under `/api/...` using `VITE_BACKEND_URL`, but the Flask app only implements `/send-incentive`. Treat the API client as scaffolding unless those endpoints exist.
## Frontend architecture & conventions
- Entrypoint: `src/main.tsx` (imports `./i18n` to enable translations). App routes defined in `src/App.tsx`.
- Routing: React Router v7 with layouts in `src/layout/` and pages in `src/pages/`. Protected routes via `src/routes/Privateroute.tsx` using `ApiClient.checkAuth()`.
- Styling: Tailwind CSS v4 (via `@tailwindcss/vite`). Utility classes throughout; minimal custom CSS in `src/styles/`.
- Animations: Framer Motion used for subtle motion in Nav, Footer, and sections.
- i18n: i18next + react-i18next + language detector. Resources in `src/locales/<lng>/translation.json`. Initialize in `src/i18n.ts`. Global language switcher at `src/components/main/LanguageSwitcher.tsx` and embedded in `Nav`.
- Pattern: Category strings in `categoriesData.ts` use the Czech text as translation keys; `CategoryCard` calls `t(title)` and `t(description)`. If you change data text, update both `cs` and `en` JSON files.
- Assets: Put static assets in `frontend/public`. Reference them as absolute paths (`/images/...`, `/assets/...`, `/videos/...`). Do not import from `/public/...` at runtime.
## API client & data flow
- Client: `src/api/Client.ts` wraps Axios with two instances: `apiClient` (auth) and `publicClient` (CSRF for anonymous calls).
- Base URL: `${import.meta.env.VITE_BACKEND_URL}/api` — set `VITE_BACKEND_URL` in an environment file (e.g., `.env`) for Vite.
- Auth flow: Intercepts 401s to refresh tokens via `ApiClient.refreshToken()`; on failure redirects to `/login`.
- Helpers: `ApiClient.request(method, endpoint, data, config)` returns `response.data` and normalizes params vs body.
- Current backend mismatch: Only `/send-incentive` exists in Flask. If you consume `ApiClient` endpoints (`/account/...`), ensure the backend implements them or mock responses.
## Developer workflows
- Frontend
- Dev: `npm run dev` in `frontend/` (Vite server)
- Build: `npm run build` (outputs to `frontend/dist`)
- Env: Use `VITE_*` variables (e.g., `VITE_BACKEND_URL=http://localhost:5000`). CRA-style vars like `REACT_APP_*` are ignored by Vite.
- Backend
- Setup: `pip install -r backend/requirements.txt`
- Run: `python backend/app.py` (defaults to `http://0.0.0.0:5000`)
- Env: `backend/.env` contains `RECAPTCHA_SECRET`, email credentials, SMTP settings.
## Patterns to follow
- Routing: Place route shells in `src/layout/` and pages in `src/pages/`; nest via `<Outlet />`.
- Protected routes: Wrap sections with `<Route element={<PrivateRoute />} />` and place guarded routes inside.
- i18n usage: `const { t } = useTranslation();` then `t('nav.home')`, etc. Add new keys to both `cs` and `en` JSON files.
- Category copy: Keep `categoriesData.ts` as the single source of keys (Czech text), and ensure translations exist for other languages.
- Axios calls: Prefer `ApiClient.request('get', '/resource', {q: 'x'})` for consistency and interceptor benefits.
## Examples
- Add a new translatable label:
1) Put `"nav.profile": "Profile"` in `en/translation.json` and `"nav.profile": "Profil"` in `cs/translation.json`.
2) Use in code: `{t('nav.profile')}`.
- Call backend API:
```ts
const data = await ApiClient.request('get', '/items', { page: 1 });
```
## Gotchas
- Vite env vars must start with `VITE_`. `.env-frontend` with `REACT_APP_*` keys wont be read by Vite.
- Public asset paths are absolute from web root (e.g., `/assets/logo.png`), not `/public/...`.
- If i18n keys are missing, youll see the raw key in UI; add it to both locale files.
If anything here looks off or incomplete (e.g., backend endpoints you actually have but arent in this repo), tell me and Ill refine these rules.

View File

@@ -10,6 +10,9 @@
"axios": "^1.12.2", "axios": "^1.12.2",
"babel-plugin-react-compiler": "^19.1.0-rc.3", "babel-plugin-react-compiler": "^19.1.0-rc.3",
"framer-motion": "^12.23.22", "framer-motion": "^12.23.22",
"i18next": "^25.6.0",
"i18next-browser-languagedetector": "^8.2.0",
"react-i18next": "^16.1.4",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router-dom": "^7.9.3", "react-router-dom": "^7.9.3",
"tailwindcss": "^4.1.14" "tailwindcss": "^4.1.14"
@@ -17,6 +20,7 @@
"devDependencies": { "devDependencies": {
"@types/axios": "^0.9.36", "@types/axios": "^0.9.36",
"@types/react": "^19.2.0", "@types/react": "^19.2.0",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.0.4" "@vitejs/plugin-react": "^5.0.4"
} }
}, },
@@ -252,6 +256,15 @@
"@babel/core": "^7.0.0-0" "@babel/core": "^7.0.0-0"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.27.2", "version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
@@ -1449,6 +1462,16 @@
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
}, },
"node_modules/@types/react-dom": {
"version": "19.2.2",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
}
},
"node_modules/@types/react-router": { "node_modules/@types/react-router": {
"version": "5.1.20", "version": "5.1.20",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
@@ -2014,6 +2037,55 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"license": "MIT",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/i18next": {
"version": "25.6.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.6.0.tgz",
"integrity": "sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6"
},
"peerDependencies": {
"typescript": "^5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz",
"integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/jiti": { "node_modules/jiti": {
"version": "2.6.1", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@@ -2479,6 +2551,32 @@
"react": "^19.2.0" "react": "^19.2.0"
} }
}, },
"node_modules/react-i18next": {
"version": "16.1.4",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.1.4.tgz",
"integrity": "sha512-0UUKZDHjKnLk6dfbYXEZ9CVqLMpNiul+dHbPVQo2z2t1GkdirkeHXb/TtdsNuv+nyNOTDl1Jp6F6uwf9M3DMcg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6",
"html-parse-stringify": "^3.0.1"
},
"peerDependencies": {
"i18next": ">= 25.5.2",
"react": ">= 16.8.0",
"typescript": "^5"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/react-icons": { "node_modules/react-icons": {
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
@@ -2783,6 +2881,15 @@
} }
} }
}, },
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@@ -5,6 +5,9 @@
"axios": "^1.12.2", "axios": "^1.12.2",
"babel-plugin-react-compiler": "^19.1.0-rc.3", "babel-plugin-react-compiler": "^19.1.0-rc.3",
"framer-motion": "^12.23.22", "framer-motion": "^12.23.22",
"i18next": "^25.6.0",
"i18next-browser-languagedetector": "^8.2.0",
"react-i18next": "^16.1.4",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router-dom": "^7.9.3", "react-router-dom": "^7.9.3",
"tailwindcss": "^4.1.14" "tailwindcss": "^4.1.14"
@@ -12,6 +15,7 @@
"devDependencies": { "devDependencies": {
"@types/axios": "^0.9.36", "@types/axios": "^0.9.36",
"@types/react": "^19.2.0", "@types/react": "^19.2.0",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.0.4" "@vitejs/plugin-react": "^5.0.4"
}, },
"scripts": { "scripts": {

View File

@@ -0,0 +1,12 @@
# Public assets
Place your site-wide static assets in this folder. These files are served as-is from the web root.
Required for navbar:
- Save the provided logo image as `logo-atcz.png` in this folder.
- Expected path in code: `/assets/logo-atcz.png`
- Recommended size: ~180240px wide, transparent PNG or optimized WEBP
- The navbar displays it at `h-9` and auto width.
Optional:
- Home video poster: `/assets/garage-poster.jpg`

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,17 @@
# Videos
Place your homepage background video here as `garage.mp4`.
Recommended:
- Format: MP4 (H.264, AAC)
- Resolution: 1920x1080 (or 1280x720 for smaller size)
- Duration: 1020s loop, trimmed and optimized
- File path: `/videos/garage.mp4`
Optional poster (fallback image):
- Add an image at `/assets/garage-poster.jpg` for the `<video poster>` attribute used in `src/pages/Home.tsx`.
Optimization tips:
- Compress with HandBrake or ffmpeg
- Aim for < 812 MB for fast loads
- Consider a shorter mobile-specific version

Binary file not shown.

View File

@@ -3,7 +3,7 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import HomeLayout from "./layout/Home"; import HomeLayout from "./layout/Home";
import DashboardLayout from "./layout/Dashboard"; import DashboardLayout from "./layout/Dashboard";
import Home from "./pages/Home"; import Home from "./pages/Home/Home";
import Dashboard from "./pages/Dashboard"; import Dashboard from "./pages/Dashboard";
import PrivateRoute from "./routes/Privateroute"; 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 { motion } from "framer-motion";
import styles from "./footer.module.css"; 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() { export default function Footer() {
const { t } = useTranslation();
return ( return (
<motion.footer <motion.footer
initial={{ y: 80, opacity: 0 }} initial={{ y: 80, opacity: 0 }}
animate={{ y: 0, opacity: 1 }} animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.4, delay: 0.2 }} 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> <div className="mx-auto max-w-7xl">
&copy; {new Date().getFullYear()} MyDashboard. All rights reserved. {/* Top content: contacts + address + map */}
</span> <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> </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"; @import "tailwindcss";
/* Global baseline: white text, neutral link styles */
:root { :root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5; line-height: 1.5;
font-weight: 400; font-weight: 400;
color-scheme: light dark; color-scheme: dark;
color: rgba(255, 255, 255, 0.87); color: #ffffff;
background-color: #242424; background-color: #242424;
font-synthesis: none; font-synthesis: none;
@@ -15,26 +16,19 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
html, body {
margin: 0;
color: inherit;
background: inherit;
}
a { a {
font-weight: 500; font-weight: 500;
color: #646cff; color: inherit;
text-decoration: inherit; text-decoration: inherit;
} }
a:hover { a:hover {
color: #535bf2; color: inherit;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
} }
button { button {
@@ -49,22 +43,9 @@ button {
transition: border-color 0.25s; transition: border-color 0.25s;
} }
button:hover { button:hover {
border-color: #646cff; border-color: #ffffff;
} }
button:focus, button:focus,
button:focus-visible { button:focus-visible {
outline: 4px auto -webkit-focus-ring-color; 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() { export default function DashboardLayout() {
return ( return (
<div className="flex h-screen bg-gray-100 text-gray-900"> <div className="flex h-screen bg-black text-white">
{/* Sidebar */} {/* Sidebar */}
<motion.aside <motion.aside
initial={{ x: -250 }} initial={{ x: -250 }}
animate={{ x: 0 }} animate={{ x: 0 }}
transition={{ duration: 0.4 }} 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 MyDashboard
</div> </div>
<nav className="flex-1 p-4 space-y-2"> <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="/profile" icon={<FaUser />} label="Profile" />
<SidebarLink to="/settings" icon={<FaCog />} label="Settings" /> <SidebarLink to="/settings" icon={<FaCog />} label="Settings" />
</nav> </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" /> <SidebarLink to="/logout" icon={<FaSignOutAlt />} label="Logout" />
</div> </div>
</motion.aside> </motion.aside>
@@ -34,11 +34,11 @@ export default function DashboardLayout() {
initial={{ y: -80 }} initial={{ y: -80 }}
animate={{ y: 0 }} animate={{ y: 0 }}
transition={{ duration: 0.3 }} 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> <h1 className="text-lg font-semibold">Dashboard</h1>
<div className="flex items-center space-x-4"> <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 <img
src="https://ui-avatars.com/api/?name=User&background=0D8ABC&color=fff" src="https://ui-avatars.com/api/?name=User&background=0D8ABC&color=fff"
alt="avatar" alt="avatar"
@@ -52,7 +52,7 @@ export default function DashboardLayout() {
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="flex-1 overflow-y-auto p-6" className="flex-1 overflow-y-auto p-6 bg-black"
> >
<Outlet /> <Outlet />
</motion.main> </motion.main>
@@ -71,7 +71,7 @@ function SidebarLink({ to, icon, label }: SidebarLinkProps) {
return ( return (
<Link <Link
to={to} 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-lg">{icon}</span>
<span className="text-sm font-medium">{label}</span> <span className="text-sm font-medium">{label}</span>

View File

@@ -1,15 +1,17 @@
import { Outlet } from "react-router-dom"; import { Outlet } from "react-router-dom";
import Footer from "../components/main/footer/Footer.tsx"; import Footer from "../components/main/footer/Footer.tsx";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import Nav from "../components/main/Nav";
export default function HomeLayout() { export default function HomeLayout() {
return ( return (
<div> <div>
<Nav />
<motion.section <motion.section
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="flex flex-col min-h-[80vh] justify-center items-center" className="flex flex-col justify-center items-center"
> >
<Outlet /> <Outlet />
</motion.section> </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 { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import './index.css' import './index.css'
import './i18n'
import App from './App.tsx' import App from './App.tsx'
createRoot(document.getElementById('root')!).render( 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 ( return (
<div className="flex items-center justify-center h-screen"> <div className="flex items-center justify-center h-screen">
<motion.div <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 }} animate={{ rotate: 360 }}
transition={{ repeat: Infinity, duration: 1, ease: "linear" }} transition={{ repeat: Infinity, duration: 1, ease: "linear" }}
/> />

View File

@@ -15,6 +15,7 @@
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"resolveJsonModule": true,
/* Linting */ /* Linting */
"strict": true, "strict": true,