mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-03 18:22:57 +00:00
update
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
VITE_API_BASE_URL=
|
||||
VITE_SITE_URL=https://example.com
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,46 @@
|
||||
# MyClub Landing
|
||||
|
||||
Samostatná publikovatelná landing page pro `MyClub` v root složce `/landing`.
|
||||
|
||||
## Stack
|
||||
|
||||
- React + TypeScript + Vite
|
||||
- Tailwind CSS v4
|
||||
- shadcn/ui primitives
|
||||
- Montserrat variable font
|
||||
- Form submission přes existující `POST /api/v1/contact`
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
cd landing
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Dev server běží na `http://localhost:4174`.
|
||||
|
||||
Výchozí proxy posílá `/api/*` na `http://localhost:8080`, takže při lokálním běhu backendu není potřeba nastavovat extra URL.
|
||||
|
||||
## Environment
|
||||
|
||||
Zkopírujte `.env.example` podle potřeby:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Podporované proměnné:
|
||||
|
||||
- `VITE_API_BASE_URL`: volitelné. Pokud je prázdné, používá se same-origin `/api/...`
|
||||
- `VITE_SITE_URL`: volitelné. Pokud je nastavené, doplní canonical a absolutní OG/Twitter URL
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cd landing
|
||||
npm run build
|
||||
npm run preview
|
||||
```
|
||||
|
||||
Výstup je v `landing/dist`.
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "radix-nova",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"rtl": false,
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"menuColor": "default",
|
||||
"menuAccent": "subtle",
|
||||
"registries": {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
rules: {
|
||||
'react-refresh/only-export-components': ['error', { allowConstantExport: true }],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/components/ui/button.tsx', 'src/components/ui/badge.tsx'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': 'off',
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -0,0 +1,34 @@
|
||||
<!doctype html>
|
||||
<html lang="cs">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="rgb(108 56 217)" />
|
||||
<meta name="color-scheme" content="light" />
|
||||
<meta
|
||||
name="description"
|
||||
content="MyClub spojuje klubový web, MyUIbrix builder, zápasy, obsah, partnery, newslettery i provozní workflow do jednoho přehledného systému pro sportovní kluby."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="MyClub" />
|
||||
<meta property="og:title" content="MyClub | Web, obsah a provoz klubu v jednom systému" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="MyClub spojuje klubový web, MyUIbrix builder, zápasy, obsah, partnery, newslettery i provozní workflow do jednoho přehledného systému pro sportovní kluby."
|
||||
/>
|
||||
<meta property="og:image" content="/og-cover.svg" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:title" content="MyClub | Web, obsah a provoz klubu v jednom systému" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="MyClub spojuje klubový web, MyUIbrix builder, zápasy, obsah, partnery, newslettery i provozní workflow do jednoho přehledného systému pro sportovní kluby."
|
||||
/>
|
||||
<meta name="twitter:image" content="/og-cover.svg" />
|
||||
<title>MyClub | Web, obsah a provoz klubu v jednom systému</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+9068
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "landing",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/montserrat": "^5.2.8",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"axios": "^1.13.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.577.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-hook-form": "^7.71.2",
|
||||
"shadcn": "^4.0.6",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@types/node": "^24.12.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.2.0",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.56.1",
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="MyClub">
|
||||
<rect width="128" height="128" rx="32" fill="rgb(255 255 255)" />
|
||||
<rect x="10" y="10" width="108" height="108" rx="28" fill="rgb(255 255 255)" stroke="rgba(17,24,39,0.14)" stroke-width="2" />
|
||||
<path d="M32 94V34h12l20 30 20-30h12v60H84V58L64 87 44 58v36H32Z" fill="rgb(108 56 217)" />
|
||||
<circle cx="98" cy="34" r="12" fill="rgba(255,153,51,0.24)" />
|
||||
<circle cx="98" cy="34" r="6" fill="rgb(255 153 51)" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 514 B |
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" role="img" aria-label="MyClub Open Graph Cover">
|
||||
<rect width="1200" height="630" fill="rgb(255 255 255)" />
|
||||
<rect x="42" y="42" width="1116" height="546" rx="36" fill="rgb(255 255 255)" stroke="rgba(17,24,39,0.1)" stroke-width="2" />
|
||||
<circle cx="180" cy="126" r="88" fill="rgba(108,56,217,0.14)" />
|
||||
<circle cx="1032" cy="128" r="72" fill="rgba(255,153,51,0.18)" />
|
||||
<path d="M110 454V182h68l112 168 112-168h68v272h-68V291L290 429 178 291v163h-68Z" fill="rgb(108 56 217)" />
|
||||
<rect x="552" y="170" width="520" height="290" rx="30" fill="rgba(108,56,217,0.05)" stroke="rgba(17,24,39,0.08)" />
|
||||
<rect x="590" y="210" width="230" height="132" rx="24" fill="rgb(255 255 255)" stroke="rgba(17,24,39,0.08)" />
|
||||
<rect x="848" y="210" width="186" height="60" rx="20" fill="rgba(255,153,51,0.18)" />
|
||||
<rect x="848" y="286" width="186" height="56" rx="20" fill="rgb(255 255 255)" stroke="rgba(17,24,39,0.08)" />
|
||||
<rect x="590" y="362" width="444" height="58" rx="20" fill="rgb(255 255 255)" stroke="rgba(17,24,39,0.08)" />
|
||||
<text x="552" y="520" fill="rgb(17 24 39)" font-family="Montserrat, sans-serif" font-size="58" font-weight="700" letter-spacing="-2">
|
||||
MyClub
|
||||
</text>
|
||||
<text x="552" y="574" fill="rgb(75 85 99)" font-family="Montserrat, sans-serif" font-size="28">
|
||||
Web, obsah a provoz klubu v jednom systému
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,429 @@
|
||||
import {
|
||||
ArrowRight,
|
||||
Check,
|
||||
ChevronRight,
|
||||
LayoutDashboard,
|
||||
MailPlus,
|
||||
Sparkles,
|
||||
Image as ImageIcon,
|
||||
} from 'lucide-react'
|
||||
|
||||
import {
|
||||
contactChecklist,
|
||||
faqItems,
|
||||
featureCards,
|
||||
navItems,
|
||||
operationalPillars,
|
||||
proofPoints,
|
||||
workflowSteps,
|
||||
} from '@/content'
|
||||
import { useLandingSeo } from '@/hooks/useLandingSeo'
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { ContactForm } from '@/components/landing/ContactForm'
|
||||
import { HeroVisual } from '@/components/landing/HeroVisual'
|
||||
import { SectionHeading } from '@/components/landing/SectionHeading'
|
||||
import { SiteHeader } from '@/components/landing/SiteHeader'
|
||||
import { VideoText } from '@/components/ui/video-text'
|
||||
|
||||
useLandingSeo()
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div id="top" className="landing-shell">
|
||||
<SiteHeader items={navItems} />
|
||||
|
||||
<main className="pb-16 pt-24 sm:pt-28">
|
||||
<section className="relative overflow-hidden pb-14 pt-8 sm:pb-18">
|
||||
<div className="section-shell grid gap-12 lg:grid-cols-[1.02fr_0.98fr] lg:items-center">
|
||||
<div className="relative z-10 flex flex-col gap-7">
|
||||
<Badge className="eyebrow-pill max-w-max">
|
||||
MyClub SaaS pro sportovní kluby
|
||||
</Badge>
|
||||
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="relative h-[120px] w-full overflow-hidden rounded-2xl">
|
||||
<VideoText
|
||||
src="https://cdn.magicui.design/ocean-small.webm"
|
||||
className="absolute inset-0"
|
||||
fontSize={18}
|
||||
fontWeight="800"
|
||||
>
|
||||
Jeden systém pro web, obsah a provoz vašeho klubu.
|
||||
</VideoText>
|
||||
</div>
|
||||
<p className="max-w-2xl text-lg leading-7 text-[rgb(75_85_99)] sm:text-xl sm:leading-8 break-words">
|
||||
MyClub spojuje MyUIbrix builder, zápasy, týmy, partnery, newslettery a
|
||||
klubovou administraci do jedné čisté vrstvy připravené na každodenní provoz.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<Button
|
||||
asChild
|
||||
className="h-12 rounded-full px-6 text-sm shadow-[0_18px_34px_rgba(17,24,39,0.12)]"
|
||||
>
|
||||
<a href="#kontakt">
|
||||
Domluvit ukázku
|
||||
<ArrowRight className="size-4" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="h-12 rounded-full border-[rgba(17,24,39,0.12)] bg-white px-6 text-sm hover:bg-[rgba(108,56,217,0.06)]"
|
||||
>
|
||||
<a href="#ukazka">
|
||||
Projít produktovou vrstvu
|
||||
<ChevronRight className="size-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
{[
|
||||
'MyUIbrix jako vestavěný editor homepage.',
|
||||
'Klubová data, obsah a komunikace bez pluginové skládačky.',
|
||||
'Připravené fungovat vedle stávajícího backendu a publikace.',
|
||||
].map((item) => (
|
||||
<div key={item} className="rounded-[1.55rem] border border-[rgba(17,24,39,0.08)] bg-white/88 p-5 backdrop-blur-sm transition-all duration-300 hover:shadow-lg hover:bg-white">
|
||||
<div className="mb-3 flex items-center gap-2 text-sm font-semibold text-[rgb(17_24_39)]">
|
||||
<Check className="size-4 text-[rgb(108_56_217)]" />
|
||||
Přímý benefit
|
||||
</div>
|
||||
<p className="text-sm leading-6 text-[rgb(75_85_99)]">{item}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HeroVisual />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="pb-16 pt-8">
|
||||
<div className="section-shell">
|
||||
<div className="grid gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
{proofPoints.map((item) => (
|
||||
<Card key={item.title} className="surface-panel-soft border-0 p-6 transition-all duration-300 hover:shadow-xl hover:scale-[1.02]">
|
||||
<CardHeader className="p-0 pb-4">
|
||||
<div className="mb-4 flex size-14 items-center justify-center rounded-2xl border border-[rgba(17,24,39,0.08)] bg-white shadow-sm">
|
||||
<item.icon className="size-6 text-[rgb(108_56_217)]" />
|
||||
</div>
|
||||
<CardDescription className="section-label mb-2">{item.eyebrow}</CardDescription>
|
||||
<CardTitle className="text-xl tracking-[-0.04em] leading-7 text-[rgb(17_24_39)]">{item.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<p className="text-sm leading-6 text-[rgb(75_85_99)]">{item.description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="platforma" className="scroll-mt-28 py-20">
|
||||
<div className="section-shell flex flex-col gap-12">
|
||||
<SectionHeading
|
||||
kicker="Platforma"
|
||||
title="Navržené pro klub, který nechce skládat pět různých systémů."
|
||||
description="Každá vrstva landingu vychází z reálných schopností projektu. Žádná obecná SaaS omáčka, ale konkrétní modulární klubový stack."
|
||||
/>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
{featureCards.map((item, index) => (
|
||||
<Card
|
||||
key={item.title}
|
||||
className={`
|
||||
${index % 3 === 1 ? 'surface-panel-secondary border-0' : 'surface-panel border-0'}
|
||||
p-6 transition-all duration-300 hover:shadow-xl hover:scale-[1.02]
|
||||
`}
|
||||
>
|
||||
<CardHeader className="p-0 pb-4">
|
||||
<div className="mb-4 flex size-14 items-center justify-center rounded-2xl border border-[rgba(17,24,39,0.08)] bg-white shadow-sm">
|
||||
<item.icon
|
||||
className={`
|
||||
${index % 3 === 1 ? 'size-6 text-[rgb(255_153_51)]' : 'size-6 text-[rgb(108_56_217)]'}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
<CardDescription className="section-label mb-2">{item.eyebrow}</CardDescription>
|
||||
<CardTitle className="text-xl tracking-[-0.04em] leading-7 text-[rgb(17_24_39)]">{item.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<p className="text-sm leading-6 text-[rgb(75_85_99)]">{item.description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="ukazka" className="scroll-mt-28 py-20">
|
||||
<div className="section-shell grid gap-12 xl:grid-cols-[1.08fr_0.92fr]">
|
||||
<div className="flex flex-col gap-12">
|
||||
<SectionHeading
|
||||
kicker="Ukázka produktu"
|
||||
title="Editorial sports-tech bez generické šablony."
|
||||
description="Landing používá bílý mód, výraznou typografii a kontrolovanou barevnost. Stejný přístup může nést i veřejný klubový web vystavěný v MyClubu."
|
||||
/>
|
||||
|
||||
<Card className="surface-panel border-0 py-0 transition-all duration-300 hover:shadow-xl">
|
||||
<CardHeader className="border-b border-[rgba(17,24,39,0.08)] pb-6">
|
||||
<CardDescription className="section-label">ukázková kompozice</CardDescription>
|
||||
<CardTitle className="text-2xl tracking-[-0.05em] leading-8">
|
||||
Homepage builder, který řídí rytmus celého klubu
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-6 px-6 py-8 sm:grid-cols-[0.95fr_1.05fr]">
|
||||
<div className="rounded-[1.5rem] border border-[rgba(17,24,39,0.08)] bg-white p-6">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<span className="section-label">bloky stránky</span>
|
||||
<Sparkles className="size-5 text-[rgb(108_56_217)]" />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
'Hero s hlavní kampaní',
|
||||
'Novinky a články',
|
||||
'Příští zápas + tabulka',
|
||||
'Sponzoři a bannery',
|
||||
'Kontakty a mapa',
|
||||
].map((label, index) => (
|
||||
<div
|
||||
key={label}
|
||||
className="flex items-center gap-4 rounded-[1.1rem] border border-[rgba(17,24,39,0.06)] bg-[rgba(17,24,39,0.02)] px-5 py-4 transition-all duration-200 hover:bg-[rgba(17,24,39,0.04)] hover:shadow-sm"
|
||||
>
|
||||
<span className="flex size-8 items-center justify-center rounded-full bg-[rgba(108,56,217,0.08)] text-xs font-semibold text-[rgb(108_56_217)]">
|
||||
0{index + 1}
|
||||
</span>
|
||||
<span className="font-medium text-[rgb(17_24_39)] leading-6">{label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[1.5rem] border border-[rgba(17,24,39,0.08)] bg-[linear-gradient(180deg,rgba(108,56,217,0.08),rgba(255,255,255,0.96))] p-6">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<span className="section-label">preview</span>
|
||||
<LayoutDashboard className="size-5 text-[rgb(17_24_39)]" />
|
||||
</div>
|
||||
<div className="grid gap-4">
|
||||
<div className="rounded-[1.4rem] bg-white p-5">
|
||||
<div className="h-3 w-2/3 rounded-full bg-[rgba(17,24,39,0.12)] mb-4" />
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="h-20 rounded-2xl bg-[rgba(108,56,217,0.09)] flex items-center justify-center">
|
||||
<ImageIcon className="size-6 text-[rgb(108_56_217)/20]" />
|
||||
</div>
|
||||
<div className="h-20 rounded-2xl bg-[rgba(255,153,51,0.16)] flex items-center justify-center">
|
||||
<ImageIcon className="size-6 text-[rgb(255_153_51)/20]" />
|
||||
</div>
|
||||
<div className="h-20 rounded-2xl bg-[rgba(17,24,39,0.06)] flex items-center justify-center">
|
||||
<ImageIcon className="size-6 text-[rgb(17_24_39)/15]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="rounded-[1.25rem] bg-white p-4">
|
||||
<div className="h-2.5 w-16 rounded-full bg-[rgba(17,24,39,0.12)] mb-4" />
|
||||
<div className="h-20 rounded-[1rem] bg-[rgba(108,56,217,0.08)] flex items-center justify-center">
|
||||
<ImageIcon className="size-5 text-[rgb(108_56_217)/15]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-[1.25rem] bg-white p-4">
|
||||
<div className="h-2.5 w-16 rounded-full bg-[rgba(17,24,39,0.12)] mb-4" />
|
||||
<div className="h-20 rounded-[1rem] bg-[rgba(255,153,51,0.16)] flex items-center justify-center">
|
||||
<ImageIcon className="size-5 text-[rgb(255_153_51)/15]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
{operationalPillars.map((item, index) => (
|
||||
<Card
|
||||
key={item.title}
|
||||
className={`
|
||||
${index === 1 ? 'surface-panel-secondary border-0' : 'surface-panel-soft border-0'}
|
||||
transition-all duration-300 hover:shadow-xl hover:scale-[1.02]
|
||||
`}
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="mb-4 flex size-14 items-center justify-center rounded-2xl border border-[rgba(17,24,39,0.08)] bg-white shadow-sm">
|
||||
<item.icon
|
||||
className={`
|
||||
${index === 1 ? 'size-6 text-[rgb(255_153_51)]' : 'size-6 text-[rgb(108_56_217)]'}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
<CardDescription className="section-label mb-2">{item.eyebrow}</CardDescription>
|
||||
<CardTitle className="text-xl tracking-[-0.04em] leading-7 text-[rgb(17_24_39)]">{item.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm leading-6 text-[rgb(75_85_99)]">{item.description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="workflow" className="scroll-mt-28 py-20">
|
||||
<div className="section-shell flex flex-col gap-12">
|
||||
<SectionHeading
|
||||
kicker="Jak to funguje"
|
||||
title="Od první identity klubu po každodenní publikaci."
|
||||
description="Workflow je navržené tak, aby produkt dával smysl vedení klubu, redakci i operativě okolo zápasového provozu."
|
||||
align="center"
|
||||
/>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-4">
|
||||
{workflowSteps.map((step, index) => (
|
||||
<Card
|
||||
key={step.step}
|
||||
className={`
|
||||
${index === 1 || index === 3 ? 'surface-panel-secondary border-0' : 'surface-panel border-0'}
|
||||
transition-all duration-300 hover:shadow-xl hover:scale-[1.02] relative overflow-hidden
|
||||
`}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-transparent via-white/5 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
|
||||
<CardHeader className="relative z-10">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="rounded-full bg-white px-4 py-2 text-xs font-semibold uppercase tracking-[0.2em] text-[rgb(75_85_99)] shadow-sm border border-[rgba(17,24,39,0.08)]">
|
||||
krok {step.step}
|
||||
</span>
|
||||
<span className="text-3xl font-semibold tracking-[-0.05em] text-[rgba(17,24,39,0.2)]">
|
||||
{step.step}
|
||||
</span>
|
||||
</div>
|
||||
<CardTitle className="text-xl tracking-[-0.04em] leading-7 text-[rgb(17_24_39)] mt-4">{step.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="relative z-10">
|
||||
<p className="text-sm leading-6 text-[rgb(75_85_99)]">{step.description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="faq" className="scroll-mt-28 py-20">
|
||||
<div className="section-shell grid gap-12 xl:grid-cols-[0.95fr_1.05fr]">
|
||||
<SectionHeading
|
||||
kicker="FAQ"
|
||||
title="Krátké odpovědi na věci, které padnou nejdřív."
|
||||
description="Landing je postavený jako publikovatelná poptávková vrstva. Níže jsou odpovědi, které drží směr produktu i technického nasazení."
|
||||
/>
|
||||
|
||||
<Card className="surface-panel border-0 transition-all duration-300 hover:shadow-xl">
|
||||
<CardContent className="px-8 py-6">
|
||||
<Accordion type="single" collapsible className="space-y-4">
|
||||
{faqItems.map((item) => (
|
||||
<AccordionItem
|
||||
key={item.question}
|
||||
value={item.question}
|
||||
className="border border-[rgba(17,24,39,0.08)] rounded-xl px-6 transition-all duration-200 hover:bg-[rgba(17,24,39,0.02]"
|
||||
>
|
||||
<AccordionTrigger className="text-base font-semibold text-[rgb(17_24_39)] hover:text-[rgb(108_56_217)] transition-colors duration-200">
|
||||
{item.question}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-sm leading-6 text-[rgb(75_85_99)] pt-4">
|
||||
{item.answer}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="kontakt" className="scroll-mt-28 py-20">
|
||||
<div className="section-shell">
|
||||
<div className="grid gap-12 rounded-[2rem] border border-[rgba(17,24,39,0.08)] bg-[linear-gradient(135deg,rgba(108,56,217,0.08),rgba(255,255,255,0.98),rgba(255,153,51,0.14))] p-8 shadow-[0_28px_80px_rgba(17,24,39,0.08)] lg:grid-cols-[0.9fr_1.1fr] lg:p-10">
|
||||
<div className="flex flex-col gap-8">
|
||||
<Badge className="eyebrow-pill max-w-max">CTA a poptávka</Badge>
|
||||
<div className="flex flex-col gap-6">
|
||||
<h2 className="section-title text-[clamp(2.2rem,4vw,4rem)] max-w-lg">
|
||||
Chcete si projít, jak bude MyClub fungovat pro váš klub?
|
||||
</h2>
|
||||
<p className="section-copy max-w-xl">
|
||||
Pošlete stručný kontext a landing použije existující veřejný endpoint projektu. Tím je hotová
|
||||
první publikovatelná vrstva bez dalších backend zásahů.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{contactChecklist.map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
className="flex items-start gap-4 rounded-[1.35rem] border border-[rgba(17,24,39,0.08)] bg-white/86 px-5 py-5 transition-all duration-300 hover:bg-white hover:shadow-md"
|
||||
>
|
||||
<span className="mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-full bg-[rgba(108,56,217,0.08)] text-[rgb(108_56_217)]">
|
||||
<Check className="size-5" />
|
||||
</span>
|
||||
<p className="text-sm leading-7 text-[rgb(17_24_39)]">{item}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="rounded-[1.45rem] border border-[rgba(17,24,39,0.08)] bg-white/88 p-6 transition-all duration-300 hover:bg-white hover:shadow-md">
|
||||
<div className="mb-3 flex items-center gap-3 text-sm font-semibold text-[rgb(17_24_39)]">
|
||||
<MailPlus className="size-5 text-[rgb(255_153_51)]" />
|
||||
Technická poznámka
|
||||
</div>
|
||||
<p className="text-sm leading-7 text-[rgb(75_85_99)]">
|
||||
V developmentu funguje formulář přes Vite proxy na <code className="px-2 py-1 bg-[rgba(108,56,217,0.08)] rounded-md">localhost:8080</code>. V produkci
|
||||
může zůstat same-origin nebo využít <code className="px-2 py-1 bg-[rgba(108,56,217,0.08)] rounded-md">VITE_API_BASE_URL</code>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="surface-panel border-0 bg-white/92 transition-all duration-300 hover:shadow-xl">
|
||||
<CardHeader>
|
||||
<CardDescription className="section-label">pracující formulář</CardDescription>
|
||||
<CardTitle className="text-2xl tracking-[-0.05em] leading-8">
|
||||
Pošlete poptávku přes stávající backend
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ContactForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer className="border-t border-[rgba(17,24,39,0.08)] py-12 bg-gradient-to-b from-white to-[rgba(108,56,217,0.02)]">
|
||||
<div className="section-shell flex flex-col gap-8 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-xl font-bold tracking-[-0.03em] text-[rgb(17_24_39)]">
|
||||
MyClub
|
||||
</p>
|
||||
<p className="text-sm leading-6 text-[rgb(75_85_99)] max-w-md">
|
||||
Web, obsah, partneři a klubový provoz bez generické CMS skládačky.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-6 text-sm font-medium text-[rgb(75_85_99)]">
|
||||
{navItems.map((item) => (
|
||||
<a
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="transition-all duration-200 hover:text-[rgb(108_56_217)] hover:translate-y-[-1px]"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import { useState } from 'react'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import axios from 'axios'
|
||||
import { ArrowRight, LoaderCircle } from 'lucide-react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { submitContactForm } from '@/lib/api'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
|
||||
const contactSchema = z.object({
|
||||
name: z.string().min(2, 'Zadejte prosím jméno nebo název klubu.'),
|
||||
email: z.email('Zadejte prosím platný e-mail.'),
|
||||
subject: z.string().min(3, 'Napište stručné téma poptávky.'),
|
||||
message: z.string().min(24, 'Doplňte prosím alespoň krátký kontext, co chcete řešit.'),
|
||||
})
|
||||
|
||||
type ContactValues = z.infer<typeof contactSchema>
|
||||
|
||||
const defaultValues: ContactValues = {
|
||||
name: '',
|
||||
email: '',
|
||||
subject: 'Mám zájem o ukázku MyClubu',
|
||||
message: '',
|
||||
}
|
||||
|
||||
type FormStatus =
|
||||
| { type: 'success'; message: string }
|
||||
| { type: 'error'; message: string }
|
||||
| null
|
||||
|
||||
export function ContactForm() {
|
||||
const [status, setStatus] = useState<FormStatus>(null)
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<ContactValues>({
|
||||
resolver: zodResolver(contactSchema),
|
||||
defaultValues,
|
||||
})
|
||||
|
||||
async function onSubmit(values: ContactValues) {
|
||||
setStatus(null)
|
||||
|
||||
try {
|
||||
await submitContactForm(values)
|
||||
reset(defaultValues)
|
||||
setStatus({
|
||||
type: 'success',
|
||||
message:
|
||||
'Poptávka byla odeslána. Pokud je backend správně nakonfigurovaný, zpráva právě dorazila do klubové kontaktní vrstvy.',
|
||||
})
|
||||
} catch (error) {
|
||||
let message =
|
||||
'Odeslání se nepodařilo. Zkontrolujte, že běží backend a že endpoint /api/v1/contact přijímá veřejné formuláře.'
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
const serverMessage =
|
||||
typeof error.response?.data?.error === 'string'
|
||||
? error.response.data.error
|
||||
: undefined
|
||||
|
||||
if (serverMessage) {
|
||||
if (/failed to save message/i.test(serverMessage)) {
|
||||
message =
|
||||
'Server zprávu přijal, ale nepodařilo se ji uložit. Zkontrolujte databázi a nastavení kontaktního formuláře na backendu.'
|
||||
} else if (/invalid payload/i.test(serverMessage)) {
|
||||
message = 'Server odmítl odeslaná data. Zkontrolujte prosím vyplněná pole.'
|
||||
} else if (/valid email is required/i.test(serverMessage)) {
|
||||
message = 'Zadejte prosím platný e-mail.'
|
||||
} else {
|
||||
message = serverMessage
|
||||
}
|
||||
} else if (error.code === 'ECONNABORTED') {
|
||||
message = 'Server neodpověděl včas. Zkuste odeslání znovu nebo zkontrolujte backend.'
|
||||
} else if (!error.response) {
|
||||
message = 'Nepodařilo se spojit s API. V developmentu ověřte proxy nebo VITE_API_BASE_URL.'
|
||||
}
|
||||
}
|
||||
|
||||
setStatus({ type: 'error', message })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="flex flex-col gap-5" onSubmit={handleSubmit(onSubmit)} noValidate>
|
||||
<div className="grid gap-5 sm:grid-cols-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-semibold text-[rgb(17_24_39)]" htmlFor="name">
|
||||
Jméno / klub
|
||||
</label>
|
||||
<Input
|
||||
id="name"
|
||||
placeholder="Například FK Example / Jan Novák"
|
||||
aria-invalid={errors.name ? true : undefined}
|
||||
className="h-12 rounded-2xl border-[rgba(17,24,39,0.1)] bg-white px-4 text-[rgb(17_24_39)] placeholder:text-[rgba(75,85,99,0.7)]"
|
||||
{...register('name')}
|
||||
/>
|
||||
{errors.name ? <p className="form-feedback">{errors.name.message}</p> : null}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-semibold text-[rgb(17_24_39)]" htmlFor="email">
|
||||
E-mail
|
||||
</label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="vas@email.cz"
|
||||
aria-invalid={errors.email ? true : undefined}
|
||||
className="h-12 rounded-2xl border-[rgba(17,24,39,0.1)] bg-white px-4 text-[rgb(17_24_39)] placeholder:text-[rgba(75,85,99,0.7)]"
|
||||
{...register('email')}
|
||||
/>
|
||||
{errors.email ? <p className="form-feedback">{errors.email.message}</p> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-semibold text-[rgb(17_24_39)]" htmlFor="subject">
|
||||
Co chcete řešit
|
||||
</label>
|
||||
<Input
|
||||
id="subject"
|
||||
aria-invalid={errors.subject ? true : undefined}
|
||||
className="h-12 rounded-2xl border-[rgba(17,24,39,0.1)] bg-white px-4 text-[rgb(17_24_39)] placeholder:text-[rgba(75,85,99,0.7)]"
|
||||
{...register('subject')}
|
||||
/>
|
||||
{errors.subject ? <p className="form-feedback">{errors.subject.message}</p> : null}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-semibold text-[rgb(17_24_39)]" htmlFor="message">
|
||||
Krátký kontext
|
||||
</label>
|
||||
<Textarea
|
||||
id="message"
|
||||
rows={6}
|
||||
placeholder="Popište klub, stávající web, jak dnes funguje obsah a co má nová platforma zjednodušit."
|
||||
aria-invalid={errors.message ? true : undefined}
|
||||
className="min-h-36 rounded-[1.35rem] border-[rgba(17,24,39,0.1)] bg-white px-4 py-3 text-[rgb(17_24_39)] placeholder:text-[rgba(75,85,99,0.7)]"
|
||||
{...register('message')}
|
||||
/>
|
||||
{errors.message ? <p className="form-feedback">{errors.message.message}</p> : null}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<Button
|
||||
type="submit"
|
||||
className="h-12 rounded-full px-6 text-sm shadow-[0_16px_32px_rgba(17,24,39,0.12)]"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<LoaderCircle className="size-4 animate-spin" />
|
||||
Odesílám
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Domluvit ukázku
|
||||
<ArrowRight className="size-4" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<p className="max-w-xs text-sm leading-6 text-[rgb(75_85_99)]">
|
||||
Formulář používá stávající veřejný endpoint projektu. V produkci stačí stejné origin API nebo
|
||||
nastavit <code>VITE_API_BASE_URL</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div aria-live="polite" className="min-h-6">
|
||||
{status ? (
|
||||
<p
|
||||
className={
|
||||
status.type === 'success'
|
||||
? 'rounded-3xl border border-[rgba(108,56,217,0.15)] bg-[rgba(108,56,217,0.07)] px-4 py-3 text-sm leading-6 text-[rgb(17_24_39)]'
|
||||
: 'rounded-3xl border border-[rgba(255,153,51,0.24)] bg-[rgba(255,153,51,0.12)] px-4 py-3 text-sm leading-6 text-[rgb(17_24_39)]'
|
||||
}
|
||||
>
|
||||
{status.message}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import {
|
||||
ArrowUpRight,
|
||||
CalendarRange,
|
||||
ChartColumnIncreasing,
|
||||
Layers3,
|
||||
MailPlus,
|
||||
Trophy,
|
||||
Image as ImageIcon,
|
||||
} from 'lucide-react'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { VideoText } from '@/components/ui/video-text'
|
||||
|
||||
export function HeroVisual() {
|
||||
return (
|
||||
<div className="relative mx-auto w-full max-w-[680px]">
|
||||
<div className="hero-halo hero-halo-primary absolute -left-16 top-16 size-56" />
|
||||
<div className="hero-halo hero-halo-secondary absolute -right-12 top-4 size-52" />
|
||||
|
||||
<Card className="surface-panel relative overflow-hidden rounded-[2rem] border-0 py-0 transition-all duration-500 hover:shadow-2xl">
|
||||
<CardHeader className="border-b border-[rgba(17,24,39,0.08)] px-8 py-6">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-[rgb(108_56_217)] mb-3">
|
||||
produktová ukázka
|
||||
</p>
|
||||
<CardTitle className="text-2xl tracking-[-0.04em] leading-8">
|
||||
Řídicí vrstva pro klubový web
|
||||
</CardTitle>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="hidden border-[rgba(17,24,39,0.08)] bg-white px-4 py-2 text-[rgb(75_85_99)] sm:inline-flex shadow-sm"
|
||||
>
|
||||
white mode only
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription className="max-w-xl text-[rgb(75_85_99)] mt-3 leading-6">
|
||||
MyUIbrix, klubový obsah, partneři i matchday přehled v jedné kompozici.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="grid gap-6 px-8 py-8 lg:grid-cols-[1.08fr_0.92fr]">
|
||||
<div className="space-y-5">
|
||||
<div className="mock-window dashboard-grid rounded-[1.75rem] p-5">
|
||||
<div className="mb-5 flex items-center justify-between gap-4">
|
||||
<div className="flex gap-3">
|
||||
<span className="size-3 rounded-full bg-[rgba(108,56,217,0.45)] shadow-sm" />
|
||||
<span className="size-3 rounded-full bg-[rgba(255,153,51,0.45)] shadow-sm" />
|
||||
<span className="size-3 rounded-full bg-[rgba(17,24,39,0.18)] shadow-sm" />
|
||||
</div>
|
||||
<div className="rounded-full bg-white px-4 py-2 text-[11px] font-semibold uppercase tracking-[0.2em] text-[rgb(75_85_99)] shadow-sm border border-[rgba(17,24,39,0.08)]">
|
||||
homepage builder
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="rounded-[1.3rem] border border-[rgba(108,56,217,0.16)] bg-[rgba(108,56,217,0.07)] p-5">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 text-sm font-semibold text-[rgb(17_24_39)]">
|
||||
<Layers3 className="size-5 text-[rgb(108_56_217)]" />
|
||||
Hero + novinky + zápasy
|
||||
</div>
|
||||
<span className="rounded-full bg-white px-3 py-1.5 text-[11px] font-semibold text-[rgb(108_56_217)] shadow-sm border border-[rgba(108,56,217,0.16)]">
|
||||
live preview
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="h-3 w-11/12 rounded-full bg-white/90 shadow-sm" />
|
||||
<div className="h-3 w-8/12 rounded-full bg-white/75 shadow-sm" />
|
||||
<div className="grid grid-cols-3 gap-3 pt-2">
|
||||
<div className="h-20 rounded-2xl bg-white/85 shadow-sm flex items-center justify-center relative overflow-hidden group">
|
||||
<VideoText
|
||||
src="https://cdn.magicui.design/ocean-small.webm"
|
||||
className="absolute inset-0"
|
||||
fontSize={8}
|
||||
>
|
||||
<ImageIcon className="size-8 text-white/80" />
|
||||
</VideoText>
|
||||
</div>
|
||||
<div className="h-20 rounded-2xl bg-white/70 shadow-sm flex items-center justify-center relative overflow-hidden group">
|
||||
<VideoText
|
||||
src="https://cdn.magicui.design/ocean-small.webm"
|
||||
className="absolute inset-0"
|
||||
fontSize={8}
|
||||
>
|
||||
<ImageIcon className="size-8 text-white/80" />
|
||||
</VideoText>
|
||||
</div>
|
||||
<div className="h-20 rounded-2xl bg-white/85 shadow-sm flex items-center justify-center relative overflow-hidden group">
|
||||
<VideoText
|
||||
src="https://cdn.magicui.design/ocean-small.webm"
|
||||
className="absolute inset-0"
|
||||
fontSize={8}
|
||||
>
|
||||
<ImageIcon className="size-8 text-white/80" />
|
||||
</VideoText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<div className="rounded-[1.2rem] border border-[rgba(17,24,39,0.08)] bg-white p-4 transition-all duration-300 hover:shadow-md hover:scale-[1.02]">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.2em] text-[rgb(75_85_99)] mb-3">
|
||||
články
|
||||
</div>
|
||||
<div className="mt-2 text-xl font-semibold tracking-[-0.04em] text-[rgb(17_24_39)]">
|
||||
redakce
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-[1.2rem] border border-[rgba(17,24,39,0.08)] bg-white p-4 transition-all duration-300 hover:shadow-md hover:scale-[1.02]">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.2em] text-[rgb(75_85_99)] mb-3">
|
||||
partneři
|
||||
</div>
|
||||
<div className="mt-2 text-xl font-semibold tracking-[-0.04em] text-[rgb(17_24_39)]">
|
||||
bannery
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-[1.2rem] border border-[rgba(17,24,39,0.08)] bg-white p-4 transition-all duration-300 hover:shadow-md hover:scale-[1.02]">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.2em] text-[rgb(75_85_99)] mb-3">
|
||||
komunikace
|
||||
</div>
|
||||
<div className="mt-2 text-xl font-semibold tracking-[-0.04em] text-[rgb(17_24_39)]">
|
||||
newsletter
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="metric-chip transition-all duration-300 hover:shadow-md hover:scale-[1.02]">
|
||||
<CalendarRange className="size-5 text-[rgb(108_56_217)]" />
|
||||
<span className="text-sm leading-5">Zápasy a tabulky v klubovém layoutu</span>
|
||||
</div>
|
||||
<div className="metric-chip transition-all duration-300 hover:shadow-md hover:scale-[1.02]">
|
||||
<MailPlus className="size-5 text-[rgb(255_153_51)]" />
|
||||
<span className="text-sm leading-5">Kontakty a newsletter bez externí skládačky</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
<div className="rounded-[1.75rem] border border-[rgba(17,24,39,0.08)] bg-white p-6 shadow-[0_20px_40px_rgba(17,24,39,0.06)] transition-all duration-300 hover:shadow-xl">
|
||||
<div className="mb-5 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 text-sm font-semibold text-[rgb(17_24_39)]">
|
||||
<Trophy className="size-5 text-[rgb(255_153_51)]" />
|
||||
Match center
|
||||
</div>
|
||||
<span className="text-xs font-semibold uppercase tracking-[0.18em] text-[rgb(75_85_99)]">
|
||||
připraveno na víkend
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
['1. tým', 'sobota 16:30'],
|
||||
['Dorost U19', 'neděle 10:15'],
|
||||
['Ženy', 'neděle 14:00'],
|
||||
].map(([label, time]) => (
|
||||
<div
|
||||
key={label}
|
||||
className="flex items-center justify-between rounded-[1.1rem] border border-[rgba(17,24,39,0.07)] bg-[rgba(17,24,39,0.02)] px-4 py-3 transition-all duration-200 hover:bg-[rgba(17,24,39,0.04)] hover:shadow-sm"
|
||||
>
|
||||
<span className="font-medium text-[rgb(17_24_39)]">{label}</span>
|
||||
<span className="text-sm text-[rgb(75_85_99)]">{time}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[1.75rem] border border-[rgba(17,24,39,0.08)] bg-[linear-gradient(180deg,rgba(255,153,51,0.12),rgba(255,255,255,0.98))] p-6 shadow-[0_20px_40px_rgba(17,24,39,0.06)] transition-all duration-300 hover:shadow-xl">
|
||||
<div className="mb-5 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 text-sm font-semibold text-[rgb(17_24_39)]">
|
||||
<ChartColumnIncreasing className="size-5 text-[rgb(108_56_217)]" />
|
||||
Přehled vedení
|
||||
</div>
|
||||
<ArrowUpRight className="size-4 text-[rgb(17_24_39)]" />
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="rounded-[1.2rem] bg-white/80 p-4 transition-all duration-300 hover:bg-white hover:shadow-sm min-h-[100px] flex flex-col justify-between">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.2em] text-[rgb(75_85_99)] mb-2">
|
||||
obsah
|
||||
</div>
|
||||
<div className="text-lg font-bold tracking-[-0.02em] text-[rgb(17_24_39)] leading-tight">
|
||||
pod kontrolou
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-[1.2rem] bg-white/80 p-4 transition-all duration-300 hover:bg-white hover:shadow-sm min-h-[100px] flex flex-col justify-between">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.2em] text-[rgb(75_85_99)] mb-2">
|
||||
partneři
|
||||
</div>
|
||||
<div className="text-lg font-bold tracking-[-0.02em] text-[rgb(17_24_39)] leading-tight">
|
||||
v publikaci
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type SectionHeadingProps = {
|
||||
kicker: string
|
||||
title: string
|
||||
description: string
|
||||
align?: 'left' | 'center'
|
||||
}
|
||||
|
||||
export function SectionHeading({
|
||||
kicker,
|
||||
title,
|
||||
description,
|
||||
align = 'left',
|
||||
}: SectionHeadingProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-4',
|
||||
align === 'center' && 'mx-auto max-w-3xl text-center'
|
||||
)}
|
||||
>
|
||||
<span className="section-kicker">{kicker}</span>
|
||||
<h2 className="section-title text-[clamp(2rem,4vw,3.55rem)]">{title}</h2>
|
||||
<p className="section-copy max-w-3xl">{description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { ArrowRight, Menu } from 'lucide-react'
|
||||
|
||||
import type { NavItem } from '@/content'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@/components/ui/sheet'
|
||||
|
||||
type SiteHeaderProps = {
|
||||
items: NavItem[]
|
||||
}
|
||||
|
||||
export function SiteHeader({ items }: SiteHeaderProps) {
|
||||
return (
|
||||
<header className="fixed inset-x-0 top-0 z-50 border-b border-[rgba(17,24,39,0.08)] bg-white/85 backdrop-blur-xl">
|
||||
<div className="section-shell flex h-18 items-center justify-between gap-6">
|
||||
<a href="#top" className="flex items-center gap-3 text-sm font-semibold text-[rgb(17_24_39)]">
|
||||
<span className="flex size-10 items-center justify-center rounded-2xl border border-[rgba(17,24,39,0.08)] bg-[rgba(108,56,217,0.08)] text-base font-bold text-[rgb(108_56_217)]">
|
||||
M
|
||||
</span>
|
||||
<span className="flex flex-col leading-none">
|
||||
<span className="text-base tracking-[-0.03em]">MyClub</span>
|
||||
<span className="text-xs font-medium text-[rgb(75_85_99)]">
|
||||
klubový web a provoz v jednom
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<nav className="hidden items-center gap-1 rounded-full border border-[rgba(17,24,39,0.08)] bg-white/80 p-1 md:flex">
|
||||
{items.map((item) => (
|
||||
<a
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="rounded-full px-4 py-2 text-sm font-medium text-[rgb(75_85_99)] transition-colors hover:bg-[rgba(108,56,217,0.08)] hover:text-[rgb(17_24_39)]"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="hidden md:block">
|
||||
<Button
|
||||
asChild
|
||||
className="h-11 rounded-full px-5 text-sm shadow-[0_16px_32px_rgba(17,24,39,0.12)]"
|
||||
>
|
||||
<a href="#kontakt">
|
||||
Domluvit ukázku
|
||||
<ArrowRight className="size-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="md:hidden">
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-lg"
|
||||
className="rounded-full border-[rgba(17,24,39,0.12)] bg-white"
|
||||
aria-label="Otevřít navigaci"
|
||||
>
|
||||
<Menu className="size-5" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent
|
||||
side="right"
|
||||
className="w-[86vw] border-l border-[rgba(17,24,39,0.08)] bg-white"
|
||||
>
|
||||
<SheetHeader className="px-6 pt-6">
|
||||
<SheetTitle>MyClub</SheetTitle>
|
||||
<SheetDescription>
|
||||
Klubový web, obsah a provoz v jednom editoru.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="flex flex-1 flex-col gap-2 px-6 pb-6">
|
||||
{items.map((item) => (
|
||||
<SheetClose asChild key={item.href}>
|
||||
<a
|
||||
href={item.href}
|
||||
className="rounded-3xl border border-[rgba(17,24,39,0.08)] px-4 py-3 text-base font-medium text-[rgb(17_24_39)] transition-colors hover:bg-[rgba(108,56,217,0.06)]"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
</SheetClose>
|
||||
))}
|
||||
|
||||
<SheetClose asChild>
|
||||
<Button asChild className="mt-4 h-11 rounded-full">
|
||||
<a href="#kontakt">
|
||||
Domluvit ukázku
|
||||
<ArrowRight className="size-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</SheetClose>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as React from "react"
|
||||
import { Accordion as AccordionPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
function Accordion({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return (
|
||||
<AccordionPrimitive.Root
|
||||
data-slot="accordion"
|
||||
className={cn("flex w-full flex-col", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("not-last:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring disabled:pointer-events-none disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon data-slot="accordion-trigger-icon" className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden" />
|
||||
<ChevronUpIcon data-slot="accordion-trigger-icon" className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up"
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-(--radix-accordion-content-height) pt-0 pb-2.5 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</AccordionPrimitive.Content>
|
||||
)
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
@@ -0,0 +1,49 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
||||
outline:
|
||||
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot.Root : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
data-variant={variant}
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
@@ -0,0 +1,67 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
outline:
|
||||
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
||||
icon: "size-8",
|
||||
"icon-xs":
|
||||
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm":
|
||||
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
||||
"icon-lg": "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,103 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn(
|
||||
"text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn(
|
||||
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
@@ -0,0 +1,144 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Dialog as SheetPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||
}
|
||||
|
||||
function SheetClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||
}
|
||||
|
||||
function SheetPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||
}
|
||||
|
||||
function SheetOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-[rgba(17,24,39,0.16)] duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
data-side={side}
|
||||
className={cn(
|
||||
"fixed z-50 flex flex-col gap-4 bg-background bg-clip-padding text-sm shadow-[0_24px_80px_rgba(17,24,39,0.14)] transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<SheetPrimitive.Close data-slot="sheet-close" asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="absolute top-3 right-3"
|
||||
size="icon-sm"
|
||||
>
|
||||
<XIcon
|
||||
/>
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={cn("flex flex-col gap-0.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn("text-base font-medium text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
@@ -0,0 +1,136 @@
|
||||
"use client"
|
||||
|
||||
import React, { useEffect, useState } from "react"
|
||||
import type { ElementType, ReactNode } from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface VideoTextProps {
|
||||
/**
|
||||
* The video source URL
|
||||
*/
|
||||
src: string
|
||||
/**
|
||||
* Additional className for the container
|
||||
*/
|
||||
className?: string
|
||||
/**
|
||||
* Whether to autoplay the video
|
||||
*/
|
||||
autoPlay?: boolean
|
||||
/**
|
||||
* Whether to mute the video
|
||||
*/
|
||||
muted?: boolean
|
||||
/**
|
||||
* Whether to loop the video
|
||||
*/
|
||||
loop?: boolean
|
||||
/**
|
||||
* Whether to preload the video
|
||||
*/
|
||||
preload?: "auto" | "metadata" | "none"
|
||||
/**
|
||||
* The content to display (will have the video "inside" it)
|
||||
*/
|
||||
children: ReactNode
|
||||
/**
|
||||
* Font size for the text mask (in viewport width units)
|
||||
* @default 10
|
||||
*/
|
||||
fontSize?: string | number
|
||||
/**
|
||||
* Font weight for the text mask
|
||||
* @default "bold"
|
||||
*/
|
||||
fontWeight?: string | number
|
||||
/**
|
||||
* Text anchor for the text mask
|
||||
* @default "middle"
|
||||
*/
|
||||
textAnchor?: string
|
||||
/**
|
||||
* Dominant baseline for the text mask
|
||||
* @default "middle"
|
||||
*/
|
||||
dominantBaseline?: string
|
||||
/**
|
||||
* Font family for the text mask
|
||||
* @default "sans-serif"
|
||||
*/
|
||||
fontFamily?: string
|
||||
/**
|
||||
* The element type to render for the text
|
||||
* @default "div"
|
||||
*/
|
||||
as?: ElementType
|
||||
}
|
||||
|
||||
export function VideoText({
|
||||
src,
|
||||
children,
|
||||
className = "",
|
||||
autoPlay = true,
|
||||
muted = true,
|
||||
loop = true,
|
||||
preload = "auto",
|
||||
fontSize = 15,
|
||||
fontWeight = "bold",
|
||||
textAnchor = "middle",
|
||||
dominantBaseline = "middle",
|
||||
fontFamily = "'Montserrat Variable', 'Montserrat', sans-serif",
|
||||
as: Component = "div",
|
||||
}: VideoTextProps) {
|
||||
const [svgMask, setSvgMask] = useState("")
|
||||
const content = React.Children.toArray(children).join("")
|
||||
|
||||
useEffect(() => {
|
||||
const updateSvgMask = () => {
|
||||
const responsiveFontSize =
|
||||
typeof fontSize === "number" ? `${fontSize}vw` : fontSize
|
||||
const newSvgMask = `<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%'><text x='50%' y='50%' font-size='${responsiveFontSize}' font-weight='${fontWeight}' text-anchor='${textAnchor}' dominant-baseline='${dominantBaseline}' font-family='${fontFamily}'>${content}</text></svg>`
|
||||
setSvgMask(newSvgMask)
|
||||
}
|
||||
|
||||
updateSvgMask()
|
||||
window.addEventListener("resize", updateSvgMask)
|
||||
return () => window.removeEventListener("resize", updateSvgMask)
|
||||
}, [content, fontSize, fontWeight, textAnchor, dominantBaseline, fontFamily])
|
||||
|
||||
const dataUrlMask = `url("data:image/svg+xml,${encodeURIComponent(svgMask)}")`
|
||||
|
||||
return (
|
||||
<Component className={cn(`relative size-full`, className)}>
|
||||
{/* Create a container that masks the video to only show within text */}
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
style={{
|
||||
maskImage: dataUrlMask,
|
||||
WebkitMaskImage: dataUrlMask,
|
||||
maskSize: "contain",
|
||||
WebkitMaskSize: "contain",
|
||||
maskRepeat: "no-repeat",
|
||||
WebkitMaskRepeat: "no-repeat",
|
||||
maskPosition: "center",
|
||||
WebkitMaskPosition: "center",
|
||||
}}
|
||||
>
|
||||
<video
|
||||
className="h-full w-full object-cover"
|
||||
autoPlay={autoPlay}
|
||||
muted={muted}
|
||||
loop={loop}
|
||||
preload={preload}
|
||||
playsInline
|
||||
>
|
||||
<source src={src} type="video/mp4" />
|
||||
<source src={src} type="video/webm" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
|
||||
{/* Add a backup text element for SEO/accessibility */}
|
||||
<span className="sr-only">{content}</span>
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import {
|
||||
BarChart3,
|
||||
Blocks,
|
||||
ChartColumnIncreasing,
|
||||
Flag,
|
||||
Images,
|
||||
MailPlus,
|
||||
Megaphone,
|
||||
Newspaper,
|
||||
ShieldCheck,
|
||||
Trophy,
|
||||
Users2,
|
||||
} from 'lucide-react'
|
||||
|
||||
export type NavItem = {
|
||||
label: string
|
||||
href: string
|
||||
}
|
||||
|
||||
export type ContentCard = {
|
||||
icon: LucideIcon
|
||||
eyebrow: string
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export type WorkflowStep = {
|
||||
step: string
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export type FaqItem = {
|
||||
question: string
|
||||
answer: string
|
||||
}
|
||||
|
||||
export const navItems: NavItem[] = [
|
||||
{ label: 'Platforma', href: '#platforma' },
|
||||
{ label: 'Ukázka', href: '#ukazka' },
|
||||
{ label: 'Jak to funguje', href: '#workflow' },
|
||||
{ label: 'FAQ', href: '#faq' },
|
||||
{ label: 'Kontakt', href: '#kontakt' },
|
||||
]
|
||||
|
||||
export const proofPoints: ContentCard[] = [
|
||||
{
|
||||
icon: Blocks,
|
||||
eyebrow: 'Web + CMS',
|
||||
title: 'Jedno prostředí pro veřejný web i správu obsahu',
|
||||
description: 'Stránky, články, galerie, bannery a homepage builder bez přepínání mezi nástroji.',
|
||||
},
|
||||
{
|
||||
icon: Trophy,
|
||||
eyebrow: 'Sportovní data',
|
||||
title: 'Zápasy, výsledky a tabulky v klubovém kontextu',
|
||||
description: 'Připravené pro klubový provoz, včetně hráčů, týmů, rozpisů a matchday obsahu.',
|
||||
},
|
||||
{
|
||||
icon: Users2,
|
||||
eyebrow: 'Klubová agenda',
|
||||
title: 'Týmy, hráči, kontakty a role přehledně pohromadě',
|
||||
description: 'Obsahový tým, vedení klubu i operativa pracují nad jedním zdrojem pravdy.',
|
||||
},
|
||||
{
|
||||
icon: MailPlus,
|
||||
eyebrow: 'Komunikace',
|
||||
title: 'Newsletter, kontaktní formuláře a engagement bez doplňků',
|
||||
description: 'Komunikace s fanoušky, členy a partnery vychází přímo z klubového obsahu.',
|
||||
},
|
||||
{
|
||||
icon: Megaphone,
|
||||
eyebrow: 'Partneři',
|
||||
title: 'Sponzoři, bannery a promo plochy bez ručního chaosu',
|
||||
description: 'Klubová monetizace se řídí stejně snadno jako redakční obsah.',
|
||||
},
|
||||
{
|
||||
icon: ChartColumnIncreasing,
|
||||
eyebrow: 'Přehled',
|
||||
title: 'Analytika a provozní kontrola v jednom dashboardu',
|
||||
description: 'Vidíte, co klub publikuje, co funguje a co má smysl dál posílit.',
|
||||
},
|
||||
]
|
||||
|
||||
export const featureCards: ContentCard[] = [
|
||||
{
|
||||
icon: Blocks,
|
||||
eyebrow: 'MyUIbrix uvnitř',
|
||||
title: 'Skládání homepage bez front-end sprintu',
|
||||
description: 'Hero sekce, novinky, zápasy, bannery, kontakty i další bloky poskládáte vizuálně a s okamžitým náhledem.',
|
||||
},
|
||||
{
|
||||
icon: Newspaper,
|
||||
eyebrow: 'Obsah',
|
||||
title: 'Redakce, články, galerie a videa v jednotném workflow',
|
||||
description: 'Klubový obsah má společná média, kategorie, vyhledávání i publikaci napříč veřejným webem.',
|
||||
},
|
||||
{
|
||||
icon: Trophy,
|
||||
eyebrow: 'Matchday',
|
||||
title: 'Sportovní sekce, které nejsou jen statická tabulka',
|
||||
description: 'Matches, standings, hráči a soutěže vypadají jako součást produktu, ne jako externí widget.',
|
||||
},
|
||||
{
|
||||
icon: Megaphone,
|
||||
eyebrow: 'Marketing',
|
||||
title: 'Sponzoři, bannery a kampaně navázané na klubový obsah',
|
||||
description: 'Promo plochy i partnerské sekce spravujete ve stejném systému jako články a homepage.',
|
||||
},
|
||||
{
|
||||
icon: MailPlus,
|
||||
eyebrow: 'Komunikace',
|
||||
title: 'Newslettery, kontakty, komentáře a ankety bez lepení pluginů',
|
||||
description: 'Fan engagement a klubové formuláře jsou součást platformy, ne další externí vrstva.',
|
||||
},
|
||||
{
|
||||
icon: BarChart3,
|
||||
eyebrow: 'Vedení klubu',
|
||||
title: 'Výsledky, aktivita a provozní signály na jednom místě',
|
||||
description: 'Obsah, návštěvnost a klubová agenda zůstávají čitelné i pro tým, který nechce složitou enterprise správu.',
|
||||
},
|
||||
]
|
||||
|
||||
export const operationalPillars: ContentCard[] = [
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
eyebrow: 'Pro management',
|
||||
title: 'Méně nástrojů, méně překlepů, méně ruční koordinace',
|
||||
description: 'Jedna platforma pro veřejný web, provozní data i komunikaci s partnery a komunitou.',
|
||||
},
|
||||
{
|
||||
icon: Flag,
|
||||
eyebrow: 'Pro operativu',
|
||||
title: 'Rychlá reakce na zápasový víkend i změny v klubu',
|
||||
description: 'Obsah, bannery, kontakty a homepage bloky upravíte bez čekání na nový deploy celé prezentace.',
|
||||
},
|
||||
{
|
||||
icon: Images,
|
||||
eyebrow: 'Pro obsahový tým',
|
||||
title: 'Média, články a vizuální skladba webu pod jednou střechou',
|
||||
description: 'Redaktor i admin pracují nad stejným systémem a neřeší, kam co ručně kopírovat.',
|
||||
},
|
||||
]
|
||||
|
||||
export const workflowSteps: WorkflowStep[] = [
|
||||
{
|
||||
step: '01',
|
||||
title: 'Nastavíte klubovou identitu',
|
||||
description: 'Barvy, navigaci, kontakty, role, mapu a základní klubový obsah připravíte bez ohýbání obecného CMS.',
|
||||
},
|
||||
{
|
||||
step: '02',
|
||||
title: 'Poskládáte veřejný web v MyUIbrixu',
|
||||
description: 'Homepage builder drží vzhled webu pod kontrolou i ve chvíli, kdy potřebujete rychle změnit pořadí bloků a akcenty.',
|
||||
},
|
||||
{
|
||||
step: '03',
|
||||
title: 'Napojíte sportovní a obsahové vrstvy',
|
||||
description: 'Zprávy, zápasy, tabulky, hráče, partnery i newsletter propojujete v jednom toku práce.',
|
||||
},
|
||||
{
|
||||
step: '04',
|
||||
title: 'Publikujete a dál ladíte provoz',
|
||||
description: 'Obsahový tým publikuje, vedení sleduje přehled a klub má jednu platformu připravenou pro další růst.',
|
||||
},
|
||||
]
|
||||
|
||||
export const faqItems: FaqItem[] = [
|
||||
{
|
||||
question: 'Je MyClub jen pro fotbalový klub?',
|
||||
answer: 'Ne. Repo sice vznikal ve fotbalovém kontextu, ale produktová vrstva je postavená jako klubová platforma pro sportovní organizace obecně. Fotbalové integrace jsou výhoda, ne omezení.',
|
||||
},
|
||||
{
|
||||
question: 'Musí každou změnu řešit vývojář?',
|
||||
answer: 'Nemusí. Právě proto je součástí produktu MyUIbrix, tedy vizuální builder pro homepage a obsahové sekce. Vývojář není potřeba pro běžný denní provoz a obsahové změny.',
|
||||
},
|
||||
{
|
||||
question: 'Co všechno v landingu myslíte pojmem provoz klubu?',
|
||||
answer: 'Obsah, veřejný web, kontakty, newslettery, bannery, sponzory, zápasy, tabulky, týmy, hráče a administrativní tok práce mezi lidmi v klubu.',
|
||||
},
|
||||
{
|
||||
question: 'Dá se MyClub nasadit vedle stávajícího backendu?',
|
||||
answer: 'Ano. Tato landing page je už připravená fungovat vedle současného backendu a odesílá poptávky přes existující endpoint. Stejný princip lze použít i pro širší produktové nasazení.',
|
||||
},
|
||||
{
|
||||
question: 'Jak vypadá první krok po odeslání formuláře?',
|
||||
answer: 'Ozvete se přes poptávkový formulář, projdou se vaše současné potřeby, veřejný web a provozní procesy, a podle toho se připraví produktová ukázka nebo návrh dalšího kroku.',
|
||||
},
|
||||
]
|
||||
|
||||
export const contactChecklist = [
|
||||
'Ukázka podle typu klubu a vašeho současného webu.',
|
||||
'Průchod obsahem, MyUIbrix builderem i provozní agendou.',
|
||||
'Doporučení, jak navázat na stávající backend a publikaci.',
|
||||
]
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { siteUrl } from '@/lib/api'
|
||||
|
||||
const seo = {
|
||||
title: 'MyClub | Web, obsah a provoz klubu v jednom systému',
|
||||
description:
|
||||
'MyClub spojuje klubový web, MyUIbrix builder, zápasy, obsah, partnery, newslettery i provozní workflow do jednoho přehledného systému pro sportovní kluby.',
|
||||
}
|
||||
|
||||
function ensureMeta(
|
||||
selector: string,
|
||||
attribute: 'name' | 'property',
|
||||
key: string,
|
||||
content: string
|
||||
) {
|
||||
let meta = document.head.querySelector(selector) as HTMLMetaElement | null
|
||||
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta')
|
||||
meta.setAttribute(attribute, key)
|
||||
document.head.appendChild(meta)
|
||||
}
|
||||
|
||||
meta.setAttribute('content', content)
|
||||
}
|
||||
|
||||
export function useLandingSeo() {
|
||||
useEffect(() => {
|
||||
document.title = seo.title
|
||||
|
||||
ensureMeta('meta[name="description"]', 'name', 'description', seo.description)
|
||||
ensureMeta('meta[name="theme-color"]', 'name', 'theme-color', 'rgb(108 56 217)')
|
||||
ensureMeta('meta[property="og:type"]', 'property', 'og:type', 'website')
|
||||
ensureMeta('meta[property="og:site_name"]', 'property', 'og:site_name', 'MyClub')
|
||||
ensureMeta('meta[property="og:title"]', 'property', 'og:title', seo.title)
|
||||
ensureMeta(
|
||||
'meta[property="og:description"]',
|
||||
'property',
|
||||
'og:description',
|
||||
seo.description
|
||||
)
|
||||
ensureMeta('meta[name="twitter:card"]', 'name', 'twitter:card', 'summary')
|
||||
ensureMeta('meta[name="twitter:title"]', 'name', 'twitter:title', seo.title)
|
||||
ensureMeta(
|
||||
'meta[name="twitter:description"]',
|
||||
'name',
|
||||
'twitter:description',
|
||||
seo.description
|
||||
)
|
||||
|
||||
if (!siteUrl) {
|
||||
return
|
||||
}
|
||||
|
||||
const canonicalHref = `${siteUrl}/`
|
||||
const ogImage = `${siteUrl}/og-cover.svg`
|
||||
|
||||
let canonical = document.head.querySelector(
|
||||
'link[rel="canonical"]'
|
||||
) as HTMLLinkElement | null
|
||||
|
||||
if (!canonical) {
|
||||
canonical = document.createElement('link')
|
||||
canonical.setAttribute('rel', 'canonical')
|
||||
document.head.appendChild(canonical)
|
||||
}
|
||||
|
||||
canonical.setAttribute('href', canonicalHref)
|
||||
|
||||
ensureMeta('meta[property="og:url"]', 'property', 'og:url', canonicalHref)
|
||||
ensureMeta('meta[property="og:image"]', 'property', 'og:image', ogImage)
|
||||
ensureMeta(
|
||||
'meta[property="og:image:type"]',
|
||||
'property',
|
||||
'og:image:type',
|
||||
'image/svg+xml'
|
||||
)
|
||||
ensureMeta('meta[name="twitter:image"]', 'name', 'twitter:image', ogImage)
|
||||
}, [])
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
@import '@fontsource-variable/montserrat';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: rgb(255 255 255);
|
||||
--foreground: rgb(17 24 39);
|
||||
--card: rgb(255 255 255);
|
||||
--card-foreground: rgb(17 24 39);
|
||||
--popover: rgb(255 255 255);
|
||||
--popover-foreground: rgb(17 24 39);
|
||||
--primary: rgb(108 56 217);
|
||||
--primary-foreground: rgb(255 255 255);
|
||||
--secondary: rgba(255, 153, 51, 0.14);
|
||||
--secondary-foreground: rgb(17 24 39);
|
||||
--muted: rgba(17, 24, 39, 0.04);
|
||||
--muted-foreground: rgb(75 85 99);
|
||||
--accent: rgba(108, 56, 217, 0.08);
|
||||
--accent-foreground: rgb(17 24 39);
|
||||
--destructive: rgb(255 153 51);
|
||||
--border: rgba(17, 24, 39, 0.12);
|
||||
--input: rgba(17, 24, 39, 0.1);
|
||||
--ring: rgba(108, 56, 217, 0.28);
|
||||
--chart-1: rgb(108 56 217);
|
||||
--chart-2: rgba(108, 56, 217, 0.7);
|
||||
--chart-3: rgb(255 153 51);
|
||||
--chart-4: rgba(255 153 51, 0.7);
|
||||
--chart-5: rgba(17, 24, 39, 0.7);
|
||||
--radius: 1.5rem;
|
||||
--sidebar: rgb(255 255 255);
|
||||
--sidebar-foreground: rgb(17 24 39);
|
||||
--sidebar-primary: rgb(108 56 217);
|
||||
--sidebar-primary-foreground: rgb(255 255 255);
|
||||
--sidebar-accent: rgba(108, 56, 217, 0.08);
|
||||
--sidebar-accent-foreground: rgb(17 24 39);
|
||||
--sidebar-border: rgba(17, 24, 39, 0.12);
|
||||
--sidebar-ring: rgba(108, 56, 217, 0.28);
|
||||
|
||||
/* Enhanced typography scale */
|
||||
--text-xs: 0.75rem;
|
||||
--text-sm: 0.875rem;
|
||||
--text-base: 1rem;
|
||||
--text-lg: 1.125rem;
|
||||
--text-xl: 1.25rem;
|
||||
--text-2xl: 1.5rem;
|
||||
--text-3xl: 1.875rem;
|
||||
--text-4xl: 2.25rem;
|
||||
--text-5xl: 3rem;
|
||||
--text-6xl: 3.75rem;
|
||||
|
||||
/* Line heights */
|
||||
--leading-tight: 1.25;
|
||||
--leading-normal: 1.5;
|
||||
--leading-relaxed: 1.625;
|
||||
--leading-loose: 2;
|
||||
|
||||
/* Spacing scale */
|
||||
--space-xs: 0.5rem;
|
||||
--space-sm: 0.75rem;
|
||||
--space-md: 1rem;
|
||||
--space-lg: 1.25rem;
|
||||
--space-xl: 1.5rem;
|
||||
--space-2xl: 2rem;
|
||||
--space-3xl: 3rem;
|
||||
--space-4xl: 4rem;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: 'Montserrat Variable', 'Montserrat', sans-serif;
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
--color-brand-primary: rgb(108 56 217);
|
||||
--color-brand-secondary: rgb(255 153 51);
|
||||
--color-brand-ink: rgb(17 24 39);
|
||||
--color-brand-muted: rgb(75 85 99);
|
||||
--radius-sm: calc(var(--radius) * 0.55);
|
||||
--radius-md: calc(var(--radius) * 0.76);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.28);
|
||||
--radius-2xl: calc(var(--radius) * 1.7);
|
||||
--radius-3xl: calc(var(--radius) * 2.15);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
|
||||
html {
|
||||
@apply bg-background font-sans text-foreground;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: 'Montserrat Variable', 'Montserrat', sans-serif;
|
||||
min-height: 100vh;
|
||||
background-image:
|
||||
linear-gradient(180deg, rgba(108, 56, 217, 0.08), rgba(255, 255, 255, 0) 18%),
|
||||
linear-gradient(120deg, rgba(255, 153, 51, 0.12), rgba(255, 255, 255, 0) 28%),
|
||||
linear-gradient(180deg, rgba(17, 24, 39, 0.02), rgba(255, 255, 255, 0) 55%);
|
||||
background-color: rgb(255 255 255);
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
linear-gradient(rgba(17, 24, 39, 0.035) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(17, 24, 39, 0.035) 1px, transparent 1px);
|
||||
background-size: 48px 48px;
|
||||
mask-image: linear-gradient(180deg, rgba(17, 24, 39, 0.28), transparent 70%);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Montserrat Variable', 'Montserrat', sans-serif;
|
||||
border-radius: 999px;
|
||||
background: rgba(108, 56, 217, 0.08);
|
||||
color: rgb(17 24 39);
|
||||
padding: 0.2rem 0.55rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: rgba(108, 56, 217, 0.18);
|
||||
color: rgb(17 24 39);
|
||||
}
|
||||
|
||||
#root {
|
||||
min-height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.landing-shell {
|
||||
@apply min-h-screen overflow-x-clip;
|
||||
}
|
||||
|
||||
.section-shell {
|
||||
width: min(1280px, calc(100% - 3rem));
|
||||
margin-inline: auto;
|
||||
padding-inline: var(--space-lg);
|
||||
}
|
||||
|
||||
.section-kicker {
|
||||
@apply text-xs font-bold uppercase tracking-[0.28em];
|
||||
color: rgb(108 56 217);
|
||||
font-size: var(--text-xs);
|
||||
line-height: var(--leading-tight);
|
||||
letter-spacing: 0.28em;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@apply font-semibold leading-[1.1] tracking-[-0.03em] text-[rgb(17_24_39)];
|
||||
font-size: clamp(2rem, 4vw, 3.5rem);
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.03em;
|
||||
margin-bottom: var(--space-lg);
|
||||
max-width: none;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.section-copy {
|
||||
@apply text-base leading-7 text-[rgb(75_85_99)] sm:text-lg sm:leading-8;
|
||||
font-size: var(--text-base);
|
||||
line-height: var(--leading-relaxed);
|
||||
color: rgb(75 85 99);
|
||||
max-width: 70ch;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.section-copy {
|
||||
font-size: var(--text-lg);
|
||||
line-height: var(--leading-relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
.section-label {
|
||||
@apply text-xs font-semibold uppercase tracking-[0.2em];
|
||||
color: rgb(75 85 99);
|
||||
font-size: var(--text-xs);
|
||||
line-height: var(--leading-normal);
|
||||
letter-spacing: 0.2em;
|
||||
margin-bottom: var(--space-xs);
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.eyebrow-pill {
|
||||
@apply inline-flex w-fit items-center rounded-full border px-4 py-2 text-xs font-semibold uppercase tracking-[0.24em];
|
||||
border-color: rgba(17, 24, 39, 0.08);
|
||||
background: rgba(108, 56, 217, 0.08);
|
||||
color: rgb(108 56 217);
|
||||
}
|
||||
|
||||
.surface-panel {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(108, 56, 217, 0.06)),
|
||||
rgb(255 255 255);
|
||||
border: 1px solid rgba(17, 24, 39, 0.1);
|
||||
box-shadow: 0 30px 70px rgba(17, 24, 39, 0.08);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.surface-panel:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 40px 80px rgba(17, 24, 39, 0.12);
|
||||
}
|
||||
|
||||
.surface-panel-soft {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(17, 24, 39, 0.02)),
|
||||
rgb(255 255 255);
|
||||
border: 1px solid rgba(17, 24, 39, 0.08);
|
||||
box-shadow: 0 24px 54px rgba(17, 24, 39, 0.06);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.surface-panel-soft:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 32px 64px rgba(17, 24, 39, 0.1);
|
||||
}
|
||||
|
||||
.surface-panel-secondary {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(255, 153, 51, 0.14)),
|
||||
rgb(255 255 255);
|
||||
border: 1px solid rgba(17, 24, 39, 0.1);
|
||||
box-shadow: 0 28px 62px rgba(17, 24, 39, 0.07);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.surface-panel-secondary:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 36px 72px rgba(255, 153, 51, 0.15);
|
||||
}
|
||||
|
||||
/* Enhanced animations and micro-interactions */
|
||||
.hero-halo {
|
||||
@apply pointer-events-none rounded-full blur-3xl;
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.hero-halo-primary {
|
||||
background: radial-gradient(circle, rgba(108, 56, 217, 0.22) 0%, rgba(108, 56, 217, 0) 72%);
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.hero-halo-secondary {
|
||||
background: radial-gradient(circle, rgba(255, 153, 51, 0.22) 0%, rgba(255, 153, 51, 0) 72%);
|
||||
animation-delay: 3s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-20px); }
|
||||
}
|
||||
|
||||
.metric-chip {
|
||||
@apply flex items-center gap-2 rounded-full border px-4 py-3 text-sm font-medium;
|
||||
border-color: rgba(17, 24, 39, 0.08);
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
color: rgb(17, 24, 39);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.metric-chip:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(17, 24, 39, 0.15);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.form-feedback {
|
||||
@apply text-sm font-medium;
|
||||
color: rgb(255 153 51);
|
||||
}
|
||||
|
||||
/* Scroll animations */
|
||||
.fade-in-up {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.fade-in-up.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Enhanced button interactions */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, rgb(108, 56, 217), rgb(139, 92, 246));
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 20px 40px rgba(108, 56, 217, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.mock-window {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(17, 24, 39, 0.02)),
|
||||
rgb(255 255 255);
|
||||
border: 1px solid rgba(17, 24, 39, 0.08);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
background-image:
|
||||
linear-gradient(rgba(17, 24, 39, 0.04) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(17, 24, 39, 0.04) 1px, transparent 1px);
|
||||
background-size: 28px 28px;
|
||||
}
|
||||
|
||||
.metric-chip {
|
||||
@apply flex items-center gap-2 rounded-full border px-4 py-3 text-sm font-medium;
|
||||
border-color: rgba(17, 24, 39, 0.08);
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
color: rgb(17 24 39);
|
||||
}
|
||||
|
||||
.form-feedback {
|
||||
@apply text-sm font-medium;
|
||||
color: rgb(255 153 51);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export type ContactPayload = {
|
||||
name: string
|
||||
email: string
|
||||
subject: string
|
||||
message: string
|
||||
}
|
||||
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL?.replace(/\/$/, '') ?? ''
|
||||
|
||||
export const siteUrl = import.meta.env.VITE_SITE_URL?.replace(/\/$/, '') ?? ''
|
||||
|
||||
export async function submitContactForm(payload: ContactPayload) {
|
||||
const response = await axios.post(`${apiBaseUrl}/api/v1/contact`, payload, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: 15000,
|
||||
})
|
||||
|
||||
return response.data
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 4174,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user