new gui
This commit is contained in:
57
.github/copilot-instructions.md
vendored
Normal file
57
.github/copilot-instructions.md
vendored
Normal 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 codebase’s 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 won’t be read by Vite.
|
||||
- Public asset paths are absolute from web root (e.g., `/assets/logo.png`), not `/public/...`.
|
||||
- If i18n keys are missing, you’ll 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 aren’t in this repo), tell me and I’ll refine these rules.
|
||||
107
frontend/package-lock.json
generated
107
frontend/package-lock.json
generated
@@ -10,6 +10,9 @@
|
||||
"axios": "^1.12.2",
|
||||
"babel-plugin-react-compiler": "^19.1.0-rc.3",
|
||||
"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-router-dom": "^7.9.3",
|
||||
"tailwindcss": "^4.1.14"
|
||||
@@ -17,6 +20,7 @@
|
||||
"devDependencies": {
|
||||
"@types/axios": "^0.9.36",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.0.4"
|
||||
}
|
||||
},
|
||||
@@ -252,6 +256,15 @@
|
||||
"@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": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||
@@ -1449,6 +1462,16 @@
|
||||
"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": {
|
||||
"version": "5.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
|
||||
@@ -2014,6 +2037,55 @@
|
||||
"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": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
@@ -2479,6 +2551,32 @@
|
||||
"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": {
|
||||
"version": "5.5.0",
|
||||
"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": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"axios": "^1.12.2",
|
||||
"babel-plugin-react-compiler": "^19.1.0-rc.3",
|
||||
"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-router-dom": "^7.9.3",
|
||||
"tailwindcss": "^4.1.14"
|
||||
@@ -12,6 +15,7 @@
|
||||
"devDependencies": {
|
||||
"@types/axios": "^0.9.36",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.0.4"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
12
frontend/public/assets/README.md
Normal file
12
frontend/public/assets/README.md
Normal 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: ~180–240px wide, transparent PNG or optimized WEBP
|
||||
- The navbar displays it at `h-9` and auto width.
|
||||
|
||||
Optional:
|
||||
- Home video poster: `/assets/garage-poster.jpg`
|
||||
BIN
frontend/public/images/Seminar/poster.jpg
Normal file
BIN
frontend/public/images/Seminar/poster.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 135 KiB |
BIN
frontend/public/images/Seminar/teacher.jpg
Normal file
BIN
frontend/public/images/Seminar/teacher.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
BIN
frontend/public/images/Seminar/team.jpg
Normal file
BIN
frontend/public/images/Seminar/team.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
BIN
frontend/public/images/logo.png
Normal file
BIN
frontend/public/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
17
frontend/public/videos/README.md
Normal file
17
frontend/public/videos/README.md
Normal 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: 10–20s 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 < 8–12 MB for fast loads
|
||||
- Consider a shorter mobile-specific version
|
||||
BIN
frontend/public/videos/speedup.mp4
Normal file
BIN
frontend/public/videos/speedup.mp4
Normal file
Binary file not shown.
@@ -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";
|
||||
|
||||
21
frontend/src/components/main/LanguageSwitcher.tsx
Normal file
21
frontend/src/components/main/LanguageSwitcher.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
© {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
33
frontend/src/i18n.ts
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
50
frontend/src/locales/cs/translation.json
Normal file
50
frontend/src/locales/cs/translation.json
Normal 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."
|
||||
}
|
||||
50
frontend/src/locales/en/translation.json
Normal file
50
frontend/src/locales/en/translation.json
Normal 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 same‑day 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."
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
87
frontend/src/pages/Home/Home.tsx
Normal file
87
frontend/src/pages/Home/Home.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
8
frontend/src/pages/Home/components/categories/types.ts
Normal file
8
frontend/src/pages/Home/components/categories/types.ts
Normal 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
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" }}
|
||||
/>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"resolveJsonModule": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
|
||||
Reference in New Issue
Block a user