mirror of
https://github.com/Dvorinka/ClubLogos.git
synced 2026-06-03 19:42:58 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -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,29 @@
|
||||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the app
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:alpine
|
||||
|
||||
# Copy built files to nginx
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# Copy nginx configuration
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -0,0 +1,197 @@
|
||||
# 🇨🇿 Czech Clubs Logos API - Frontend
|
||||
|
||||
A beautiful, dark mode frontend for the Czech Clubs Logos API. Built with modern web technologies for a smooth and user-friendly experience.
|
||||
|
||||
## 🎨 Tech Stack
|
||||
|
||||
- **Vite** - Lightning-fast build tool
|
||||
- **Tailwind CSS** - Utility-first CSS framework
|
||||
- **GSAP** - Professional-grade animation library
|
||||
- **Vanilla JavaScript** - No framework overhead
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🌙 **Dark Mode** - Eye-friendly dark theme
|
||||
- 🎭 **Smooth Animations** - GSAP-powered transitions
|
||||
- 🔍 **Club Search** - Search Czech clubs by name
|
||||
- ⬆️ **Logo Upload** - Drag & drop or browse to upload
|
||||
- 📱 **Responsive** - Works on all device sizes
|
||||
- ⚡ **Fast** - Optimized with Vite
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js (v18 or higher)
|
||||
- npm or yarn
|
||||
|
||||
### Installation
|
||||
|
||||
1. Navigate to the frontend directory:
|
||||
```bash
|
||||
cd frontend
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Start the development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The app will open at `http://localhost:3000`
|
||||
|
||||
## 🏗️ Build for Production
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
This will create an optimized build in the `dist` folder.
|
||||
|
||||
To preview the production build:
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### API Endpoint
|
||||
|
||||
Update the backend API URL in `src/main.js`:
|
||||
|
||||
```javascript
|
||||
const API_BASE_URL = 'http://localhost:8080' // Update this to your backend URL
|
||||
```
|
||||
|
||||
### Styling
|
||||
|
||||
Customize colors and theme in `tailwind.config.js`:
|
||||
|
||||
```javascript
|
||||
colors: {
|
||||
'dark-bg': '#0a0e1a',
|
||||
'dark-card': '#131823',
|
||||
'accent-blue': '#3b82f6',
|
||||
'accent-green': '#10b981',
|
||||
}
|
||||
```
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── src/
|
||||
│ ├── main.js # Main JavaScript logic
|
||||
│ └── style.css # Global styles + Tailwind
|
||||
├── index.html # Main HTML file
|
||||
├── vite.config.js # Vite configuration
|
||||
├── tailwind.config.js # Tailwind configuration
|
||||
├── postcss.config.js # PostCSS configuration
|
||||
└── package.json # Dependencies
|
||||
```
|
||||
|
||||
## 🎯 Usage
|
||||
|
||||
### Search for Clubs
|
||||
|
||||
1. Click the "🔍 Search Clubs" button
|
||||
2. Type a club name (e.g., "Sparta", "Slavia")
|
||||
3. Browse results and copy UUIDs
|
||||
|
||||
### Upload a Logo
|
||||
|
||||
1. Click the "⬆️ Upload Logo" button
|
||||
2. Enter or paste a club UUID
|
||||
3. Drag & drop or click to select a logo file (SVG/PNG)
|
||||
4. Preview your logo
|
||||
5. Click "Upload Logo"
|
||||
|
||||
## 🌟 Features in Detail
|
||||
|
||||
### Animation System
|
||||
|
||||
Powered by GSAP with:
|
||||
- Smooth hero animations on page load
|
||||
- Scroll-triggered feature cards
|
||||
- Staggered API endpoint reveals
|
||||
- Interactive button feedback
|
||||
|
||||
### Search System
|
||||
|
||||
- Real-time search with debouncing
|
||||
- Demo data fallback when backend is unavailable
|
||||
- Click to auto-fill upload form
|
||||
- Copy UUID to clipboard
|
||||
|
||||
### Upload System
|
||||
|
||||
- Drag & drop support
|
||||
- File type validation (SVG/PNG only)
|
||||
- UUID format validation
|
||||
- Image preview before upload
|
||||
- Visual feedback notifications
|
||||
|
||||
## 🔌 Backend Integration
|
||||
|
||||
This frontend is designed to work with the Go backend API. Ensure the backend is running at the configured URL.
|
||||
|
||||
Expected endpoints:
|
||||
- `GET /clubs/search?q={query}` - Search clubs
|
||||
- `GET /clubs/:id` - Get club details
|
||||
- `POST /logos/:id` - Upload logo
|
||||
- `GET /logos/:id` - Get logo
|
||||
|
||||
## 🎨 Customization
|
||||
|
||||
### Colors
|
||||
|
||||
Edit `tailwind.config.js` to change the color scheme:
|
||||
|
||||
```javascript
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'dark-bg': '#your-color',
|
||||
'dark-card': '#your-color',
|
||||
// ... etc
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fonts
|
||||
|
||||
Change the font in `src/style.css`:
|
||||
|
||||
```css
|
||||
@import url('https://fonts.googleapis.com/css2?family=YourFont:wght@300;400;700&display=swap');
|
||||
```
|
||||
|
||||
### Animations
|
||||
|
||||
Adjust GSAP animations in `src/main.js`:
|
||||
|
||||
```javascript
|
||||
gsap.from('.hero-content', {
|
||||
duration: 1,
|
||||
opacity: 0,
|
||||
y: 50,
|
||||
// ... customize
|
||||
})
|
||||
```
|
||||
|
||||
## 📝 License
|
||||
|
||||
This project is part of the Czech Clubs Logos API system.
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
---
|
||||
|
||||
Built with ❤️ for Czech Football
|
||||
@@ -0,0 +1,203 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="cs" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin - České Kluby Loga API</title>
|
||||
<link rel="stylesheet" href="/src/style.css">
|
||||
</head>
|
||||
<body class="bg-dark-bg text-white min-h-screen">
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="border-b border-dark-border bg-dark-card/50 backdrop-blur-sm sticky top-0 z-50">
|
||||
<div class="container mx-auto px-6 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<a href="/" class="text-2xl font-bold gradient-text">České Kluby Loga</a>
|
||||
<div class="flex gap-4">
|
||||
<a href="/" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Domů</a>
|
||||
<a href="/api-docs.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">API Docs</a>
|
||||
<a href="/admin.html" class="nav-link px-4 py-2 rounded-lg bg-accent-blue/20 transition-smooth">Admin</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Admin Header -->
|
||||
<header class="border-b border-dark-border bg-dark-card">
|
||||
<div class="container mx-auto px-6 py-8">
|
||||
<h1 class="text-3xl font-bold gradient-text mb-2">Administrace</h1>
|
||||
<p class="text-gray-400">Vyhledejte kluby a nahrajte jejich loga</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container mx-auto px-6 py-12">
|
||||
|
||||
<!-- Club Search Section -->
|
||||
<section class="mb-12">
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
|
||||
<h2 class="text-2xl font-bold mb-6">🔍 Vyhledat Klub</h2>
|
||||
|
||||
<div class="relative mb-6">
|
||||
<input
|
||||
type="text"
|
||||
id="clubSearch"
|
||||
placeholder="Hledat české kluby (např. Sparta, Slavia)..."
|
||||
class="w-full bg-dark-bg border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Search Results -->
|
||||
<div id="searchResults" class="space-y-3">
|
||||
<!-- Výsledky naplněné JavaScriptem -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Upload Section -->
|
||||
<section id="uploadSection" class="hidden">
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
|
||||
<h2 class="text-2xl font-bold mb-6"><span style="font-size: 30px; display: inline-block; vertical-align: middle; line-height: 1;">⬆️</span> Nahrát Logo</h2>
|
||||
<form id="uploadForm" class="space-y-6">
|
||||
|
||||
<!-- Club UUID (Read-only) -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">
|
||||
UUID Klubu <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="clubUuid"
|
||||
readonly
|
||||
class="w-full bg-dark-bg/50 border border-dark-border rounded-lg px-4 py-3 text-gray-400 cursor-not-allowed"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Club Name (Required) -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">
|
||||
Název Klubu <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="clubName"
|
||||
required
|
||||
placeholder="AC Sparta Praha"
|
||||
class="w-full bg-dark-bg border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-1">Povinné: Nahrání bude zamítnuto bez názvu klubu</p>
|
||||
</div>
|
||||
|
||||
<!-- Club Type -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Typ Klubu</label>
|
||||
<select
|
||||
id="clubType"
|
||||
class="w-full bg-dark-bg border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
|
||||
>
|
||||
<option value="football">Fotbal</option>
|
||||
<option value="futsal">Futsal</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Club Website with Search -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">
|
||||
Web Klubu
|
||||
<button type="button" id="searchWebsite" class="ml-2 text-accent-blue hover:text-blue-400 text-xs">
|
||||
🔍 Hledat Online
|
||||
</button>
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
id="clubWebsite"
|
||||
placeholder="https://www.sparta.cz"
|
||||
class="w-full bg-dark-bg border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
|
||||
>
|
||||
<div id="websiteSearchResults" class="mt-2 hidden"></div>
|
||||
</div>
|
||||
|
||||
<!-- File Upload Area -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">
|
||||
Soubor Loga <span class="text-red-500">*</span>
|
||||
</label>
|
||||
|
||||
<!-- URL Upload -->
|
||||
<div class="mb-3">
|
||||
<input
|
||||
type="url"
|
||||
id="logoUrl"
|
||||
placeholder="Nebo vložte URL obrázku (https://...)"
|
||||
class="w-full bg-dark-bg border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
|
||||
>
|
||||
<button type="button" id="loadFromUrl" class="mt-2 px-4 py-2 bg-blue-600 rounded-lg hover:bg-blue-700 transition-smooth text-sm">
|
||||
📥 Načíst z URL
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-dark-border"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-2 bg-dark-card text-gray-400">nebo</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="uploadArea" class="upload-area rounded-lg p-12 text-center cursor-pointer border-2 border-dashed border-dark-border hover:border-accent-blue transition-smooth mt-3">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
|
||||
</svg>
|
||||
<p class="text-lg mb-2">Přetáhněte logo sem nebo <span class="text-accent-blue font-semibold">procházet</span></p>
|
||||
<p class="text-sm text-gray-500">SVG, PNG nebo PDF • Preferováno průhledné pozadí</p>
|
||||
<p class="text-xs text-gray-600 mt-2">SVG a PDF soubory budou automaticky převedeny na PNG</p>
|
||||
<input type="file" id="fileInput" accept=".svg,.png,.pdf" class="hidden" multiple>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500 mt-2">💡 Můžete vybrat více souborů najednou pro nahrání variant</p>
|
||||
</div>
|
||||
|
||||
<!-- Files Preview -->
|
||||
<div id="filesPreviewArea" class="hidden">
|
||||
<h3 class="text-lg font-semibold mb-3">Vybrané soubory</h3>
|
||||
<div id="filesPreviewList" class="space-y-3">
|
||||
<!-- Files will be listed here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Button -->
|
||||
<button
|
||||
type="submit"
|
||||
id="uploadSubmit"
|
||||
class="w-full px-6 py-4 bg-accent-green rounded-lg font-semibold hover:bg-green-600 transition-smooth disabled:opacity-50 disabled:cursor-not-allowed text-lg"
|
||||
>
|
||||
Nahrát Logo
|
||||
</button>
|
||||
|
||||
<!-- Requirements Notice -->
|
||||
<div class="bg-red-900/20 border border-red-800 rounded-lg p-4 text-sm">
|
||||
<p class="font-semibold text-red-400 mb-2">⚠️ Požadavky na nahrání:</p>
|
||||
<ul class="list-disc list-inside space-y-1 text-red-300/80">
|
||||
<li>Název klubu je povinný (automatické zamítnutí bez něj)</li>
|
||||
<li>UUID klubu musí být platné</li>
|
||||
<li>Akceptovány pouze SVG, PNG a PDF soubory</li>
|
||||
<li>Doporučeno průhledné pozadí</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="border-t border-dark-border mt-20">
|
||||
<div class="container mx-auto px-6 py-8 text-center text-gray-400">
|
||||
<p>🇨🇿 České Kluby Loga API | Administrace</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="/src/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,423 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="cs" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API Dokumentace - České Kluby Loga API</title>
|
||||
<link rel="stylesheet" href="/src/style.css">
|
||||
</head>
|
||||
<body class="bg-dark-bg text-white min-h-screen">
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="border-b border-dark-border bg-dark-card/50 backdrop-blur-sm sticky top-0 z-50">
|
||||
<div class="container mx-auto px-6 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<a href="/" class="text-2xl font-bold gradient-text">🇨🇿 České Kluby Loga</a>
|
||||
<div class="flex gap-4">
|
||||
<a href="/" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Domů</a>
|
||||
<a href="/api-docs.html" class="nav-link px-4 py-2 rounded-lg bg-accent-blue/20">API Docs</a>
|
||||
<a href="/admin.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Admin</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="border-b border-dark-border bg-dark-card">
|
||||
<div class="container mx-auto px-6 py-12">
|
||||
<h1 class="text-4xl font-bold gradient-text mb-3">📚 API Dokumentace</h1>
|
||||
<p class="text-xl text-gray-400">Kompletní referenční příručka pro České Kluby Loga API</p>
|
||||
<div class="mt-6 flex gap-4 items-center flex-wrap">
|
||||
<div>
|
||||
<span class="text-sm text-gray-400 mr-2">Frontend:</span>
|
||||
<code class="bg-dark-bg px-4 py-2 rounded text-accent-blue">http://localhost:3000</code>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm text-gray-400 mr-2">Backend API:</span>
|
||||
<code class="bg-dark-bg px-4 py-2 rounded text-accent-green">http://localhost:8080</code>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-400 mt-3">💡 Ve vývojovém prostředí používejte relativní cesty (např. <code class="text-accent-blue">/logos</code>), Vite proxy je přesměruje na backend</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="container mx-auto px-6 py-12">
|
||||
|
||||
<!-- Quick Start -->
|
||||
<section class="mb-16">
|
||||
<h2 class="text-3xl font-bold mb-6">🚀 Rychlý Start</h2>
|
||||
<div class="bg-gradient-to-br from-accent-green/10 to-accent-blue/10 rounded-xl p-6 border-2 border-accent-green/30">
|
||||
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
|
||||
<span class="text-2xl">⬆️</span>
|
||||
Nahrání loga klubu - Základní příkaz
|
||||
</h3>
|
||||
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm">curl -X POST http://localhost:8080/logos/{club-uuid} \
|
||||
-F "file=@logo.svg" \
|
||||
-F "club_name=Název Klubu"</code></pre>
|
||||
<div class="mt-4 space-y-2">
|
||||
<p class="text-sm text-gray-300"><strong class="text-accent-green">Povinné:</strong> Club UUID v URL, soubor loga (SVG/PNG/PDF), název klubu</p>
|
||||
<p class="text-sm text-gray-300"><strong class="text-accent-blue">Volitelné:</strong> club_type, club_website, club_city</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mt-6">
|
||||
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
|
||||
<span class="text-2xl">📥</span>
|
||||
Stažení loga klubu
|
||||
</h3>
|
||||
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm"># Přímo z backendu
|
||||
curl http://localhost:8080/logos/{uuid}
|
||||
|
||||
# Přes frontend proxy
|
||||
curl http://localhost:3000/api/logos/{uuid}</code></pre>
|
||||
<p class="text-gray-400 mt-3 text-sm">Vrátí PNG obrázek loga (SVG jako fallback)</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Endpoints -->
|
||||
<section class="mb-16">
|
||||
<h2 class="text-3xl font-bold mb-6">📡 Endpointy</h2>
|
||||
|
||||
<!-- List Logos -->
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<span class="px-3 py-1 bg-blue-600/20 text-blue-400 rounded font-mono text-sm">GET</span>
|
||||
<code class="text-lg">/logos</code>
|
||||
</div>
|
||||
<p class="text-gray-400 mb-4">Seznam všech nahraných log</p>
|
||||
|
||||
<div class="bg-dark-bg rounded-lg p-4">
|
||||
<h4 class="text-sm font-semibold text-gray-400 mb-2">Response 200:</h4>
|
||||
<pre class="text-sm overflow-x-auto"><code>[
|
||||
{
|
||||
"id": "uuid-here",
|
||||
"club_name": "AC Sparta Praha",
|
||||
"club_type": "football",
|
||||
"has_svg": true,
|
||||
"has_png": true,
|
||||
"logo_url": "http://localhost:8080/logos/uuid-here",
|
||||
"created_at": "2024-01-01T12:00:00Z"
|
||||
}
|
||||
]</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Get Logo File -->
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<span class="px-3 py-1 bg-blue-600/20 text-blue-400 rounded font-mono text-sm">GET</span>
|
||||
<code class="text-lg">/logos/:id</code>
|
||||
</div>
|
||||
<p class="text-gray-400 mb-4">Získání souboru loga (PNG preferováno, SVG jako fallback)</p>
|
||||
|
||||
<h4 class="text-sm font-semibold mb-2">Query Parameters (volitelné):</h4>
|
||||
<div class="bg-dark-bg rounded-lg p-4 mb-4">
|
||||
<code class="text-sm">format</code> <span class="text-gray-500">string</span> - "png" nebo "svg"
|
||||
</div>
|
||||
|
||||
<div class="bg-dark-bg rounded-lg p-4">
|
||||
<h4 class="text-sm font-semibold text-gray-400 mb-2">Response 200:</h4>
|
||||
<p class="text-sm text-gray-400">Binární data obrázku (image/png nebo image/svg+xml)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Get Logo Metadata -->
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<span class="px-3 py-1 bg-blue-600/20 text-blue-400 rounded font-mono text-sm">GET</span>
|
||||
<code class="text-lg">/logos/:id/json</code>
|
||||
</div>
|
||||
<p class="text-gray-400 mb-4">Získání metadat loga ve formátu JSON</p>
|
||||
|
||||
<div class="bg-dark-bg rounded-lg p-4">
|
||||
<h4 class="text-sm font-semibold text-gray-400 mb-2">Response 200:</h4>
|
||||
<pre class="text-sm overflow-x-auto"><code>{
|
||||
"id": "uuid-here",
|
||||
"club_name": "AC Sparta Praha",
|
||||
"club_type": "football",
|
||||
"club_website": "https://sparta.cz",
|
||||
"has_svg": true,
|
||||
"has_png": true,
|
||||
"primary_format": "png",
|
||||
"logo_url": "http://localhost:8080/logos/uuid-here",
|
||||
"logo_url_svg": "http://localhost:8080/logos/uuid-here?format=svg",
|
||||
"logo_url_png": "http://localhost:8080/logos/uuid-here?format=png",
|
||||
"file_size_svg": 12345,
|
||||
"file_size_png": 54321,
|
||||
"created_at": "2024-01-01T12:00:00Z",
|
||||
"updated_at": "2024-01-01T12:00:00Z"
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Logo -->
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6 border-2 border-accent-green/40">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<span class="px-3 py-1 bg-accent-green/20 text-accent-green rounded font-mono text-sm">POST</span>
|
||||
<code class="text-lg">/logos/:id</code>
|
||||
</div>
|
||||
<p class="text-gray-400 mb-4">Nahrání nového loga klubu s kompletními daty (ID klubu, název, logo soubory)</p>
|
||||
|
||||
<h4 class="text-sm font-semibold mb-2">URL Parameters:</h4>
|
||||
<div class="bg-dark-bg rounded-lg p-4 mb-4">
|
||||
<code class="text-sm">:id</code> <span class="text-red-400">*</span> <span class="text-gray-500">UUID</span> - Jedinečné ID klubu (např. <code class="text-xs">550e8400-e29b-41d4-a716-446655440000</code>)
|
||||
</div>
|
||||
|
||||
<h4 class="text-sm font-semibold mb-2">Content-Type:</h4>
|
||||
<div class="bg-dark-bg rounded-lg p-4 mb-4">
|
||||
<code class="text-sm">multipart/form-data</code>
|
||||
</div>
|
||||
|
||||
<h4 class="text-sm font-semibold mb-2">Form Data (Povinné pole):</h4>
|
||||
<div class="bg-dark-bg rounded-lg p-4 mb-4 space-y-3">
|
||||
<div class="border-l-2 border-red-400 pl-3">
|
||||
<code class="text-sm font-semibold text-red-400">file</code> <span class="text-red-400">*</span> <span class="text-gray-500">file (SVG nebo PNG)</span>
|
||||
<p class="text-xs text-gray-500 mt-1">Soubor loga. Podporované formáty: SVG (doporučeno), PNG, PDF</p>
|
||||
</div>
|
||||
<div class="border-l-2 border-red-400 pl-3">
|
||||
<code class="text-sm font-semibold text-red-400">club_name</code> <span class="text-red-400">*</span> <span class="text-gray-500">string</span>
|
||||
<p class="text-xs text-gray-500 mt-1">Název klubu (např. "AC Sparta Praha")</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="text-sm font-semibold mb-2">Form Data (Volitelné):</h4>
|
||||
<div class="bg-dark-bg rounded-lg p-4 mb-4 space-y-3">
|
||||
<div class="border-l-2 border-blue-400 pl-3">
|
||||
<code class="text-sm">club_type</code> <span class="text-gray-500">string</span>
|
||||
<p class="text-xs text-gray-500 mt-1">Typ klubu: <code>"football"</code> (výchozí) nebo <code>"futsal"</code></p>
|
||||
</div>
|
||||
<div class="border-l-2 border-blue-400 pl-3">
|
||||
<code class="text-sm">club_website</code> <span class="text-gray-500">string</span>
|
||||
<p class="text-xs text-gray-500 mt-1">URL webové stránky klubu (např. "https://sparta.cz")</p>
|
||||
</div>
|
||||
<div class="border-l-2 border-blue-400 pl-3">
|
||||
<code class="text-sm">club_city</code> <span class="text-gray-500">string</span>
|
||||
<p class="text-xs text-gray-500 mt-1">Město klubu (např. "Praha")</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark-bg rounded-lg p-4 mb-4">
|
||||
<h4 class="text-sm font-semibold text-gray-400 mb-2">Response 200 (Úspěch):</h4>
|
||||
<pre class="text-sm overflow-x-auto"><code>{
|
||||
"success": true,
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"club_name": "AC Sparta Praha",
|
||||
"has_svg": true,
|
||||
"has_png": true,
|
||||
"size_svg": 12543,
|
||||
"size_png": 45210,
|
||||
"message": "logo uploaded successfully"
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="bg-red-900/20 rounded-lg p-4 border border-red-600/30">
|
||||
<h4 class="text-sm font-semibold text-red-400 mb-2">Response 400 (Chyba):</h4>
|
||||
<pre class="text-sm overflow-x-auto"><code>{
|
||||
"error": "club_name is required"
|
||||
}</code></pre>
|
||||
<p class="text-xs text-gray-400 mt-2">Možné chyby: <code>"no file provided"</code>, <code>"invalid UUID format"</code>, <code>"only .svg, .png and .pdf files are allowed"</code></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Examples -->
|
||||
<section class="mb-16">
|
||||
<h2 class="text-3xl font-bold mb-6">💡 Příklady Použití - Nahrání Loga</h2>
|
||||
|
||||
<!-- cURL Example -->
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
|
||||
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
|
||||
<span>🔧</span> cURL (Terminal)
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold mb-2 text-accent-green">Minimální nahrání (pouze povinná pole):</h4>
|
||||
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm">curl -X POST http://localhost:8080/logos/550e8400-e29b-41d4-a716-446655440000 \
|
||||
-F "file=@sparta_logo.svg" \
|
||||
-F "club_name=AC Sparta Praha"</code></pre>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold mb-2 text-accent-blue">Kompletní nahrání (všechna data):</h4>
|
||||
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm">curl -X POST http://localhost:8080/logos/550e8400-e29b-41d4-a716-446655440000 \
|
||||
-F "file=@sparta_logo.svg" \
|
||||
-F "club_name=AC Sparta Praha" \
|
||||
-F "club_type=football" \
|
||||
-F "club_website=https://sparta.cz" \
|
||||
-F "club_city=Praha"</code></pre>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold mb-2 text-gray-400">Nahrání PNG místo SVG:</h4>
|
||||
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm">curl -X POST http://localhost:8080/logos/550e8400-e29b-41d4-a716-446655440000 \
|
||||
-F "file=@sparta_logo.png" \
|
||||
-F "club_name=AC Sparta Praha"</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript Example -->
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
|
||||
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
|
||||
<span>📜</span> JavaScript (Fetch API)
|
||||
</h3>
|
||||
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm">// Funkce pro nahrání loga s kompletními daty
|
||||
async function uploadClubLogo(clubId, file, clubData) {
|
||||
const formData = new FormData();
|
||||
|
||||
// Povinná pole
|
||||
formData.append('file', file);
|
||||
formData.append('club_name', clubData.name);
|
||||
|
||||
// Volitelná pole
|
||||
if (clubData.type) formData.append('club_type', clubData.type);
|
||||
if (clubData.website) formData.append('club_website', clubData.website);
|
||||
if (clubData.city) formData.append('club_city', clubData.city);
|
||||
|
||||
const response = await fetch(`http://localhost:8080/logos/${clubId}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// Použití s file input
|
||||
const fileInput = document.getElementById('logoFile');
|
||||
const clubId = '550e8400-e29b-41d4-a716-446655440000';
|
||||
|
||||
const result = await uploadClubLogo(clubId, fileInput.files[0], {
|
||||
name: 'AC Sparta Praha',
|
||||
type: 'football',
|
||||
website: 'https://sparta.cz',
|
||||
city: 'Praha'
|
||||
});
|
||||
|
||||
console.log('Upload successful:', result);</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- Python Example -->
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
|
||||
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
|
||||
<span>🐍</span> Python (requests)
|
||||
</h3>
|
||||
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm">import requests
|
||||
|
||||
def upload_club_logo(club_id, file_path, club_name, **optional_data):
|
||||
"""
|
||||
Nahraje logo klubu s kompletními daty
|
||||
|
||||
Args:
|
||||
club_id: UUID klubu
|
||||
file_path: Cesta k souboru loga
|
||||
club_name: Název klubu (povinný)
|
||||
**optional_data: club_type, club_website, club_city
|
||||
"""
|
||||
with open(file_path, 'rb') as f:
|
||||
files = {'file': f}
|
||||
data = {'club_name': club_name}
|
||||
data.update(optional_data)
|
||||
|
||||
response = requests.post(
|
||||
f"http://localhost:8080/logos/{club_id}",
|
||||
files=files,
|
||||
data=data
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
# Použití
|
||||
result = upload_club_logo(
|
||||
club_id='550e8400-e29b-41d4-a716-446655440000',
|
||||
file_path='sparta_logo.svg',
|
||||
club_name='AC Sparta Praha',
|
||||
club_type='football',
|
||||
club_website='https://sparta.cz',
|
||||
club_city='Praha'
|
||||
)
|
||||
|
||||
print(f"Upload úspěšný: {result['message']}")
|
||||
print(f"Has SVG: {result['has_svg']}, Has PNG: {result['has_png']}")</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- PowerShell Example -->
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
|
||||
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
|
||||
<span>💻</span> PowerShell
|
||||
</h3>
|
||||
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm"># Nahrání loga s kompletními daty
|
||||
$clubId = "550e8400-e29b-41d4-a716-446655440000"
|
||||
$logoFile = "C:\logos\sparta_logo.svg"
|
||||
|
||||
$form = @{
|
||||
file = Get-Item -Path $logoFile
|
||||
club_name = "AC Sparta Praha"
|
||||
club_type = "football"
|
||||
club_website = "https://sparta.cz"
|
||||
club_city = "Praha"
|
||||
}
|
||||
|
||||
$result = Invoke-RestMethod `
|
||||
-Uri "http://localhost:8080/logos/$clubId" `
|
||||
-Method Post `
|
||||
-Form $form
|
||||
|
||||
Write-Host "Upload úspěšný: $($result.message)" -ForegroundColor Green
|
||||
Write-Host "Club: $($result.club_name)" -ForegroundColor Cyan</code></pre>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Error Codes -->
|
||||
<section>
|
||||
<h2 class="text-3xl font-bold mb-6">⚠️ Chybové Kódy</h2>
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start gap-4">
|
||||
<span class="px-3 py-1 bg-green-600/20 text-green-400 rounded text-sm font-mono">200</span>
|
||||
<div>
|
||||
<h4 class="font-semibold">OK</h4>
|
||||
<p class="text-gray-400 text-sm">Požadavek úspěšně dokončen</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-4">
|
||||
<span class="px-3 py-1 bg-red-600/20 text-red-400 rounded text-sm font-mono">400</span>
|
||||
<div>
|
||||
<h4 class="font-semibold">Bad Request</h4>
|
||||
<p class="text-gray-400 text-sm">Neplatné parametry nebo chybějící povinná pole</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-4">
|
||||
<span class="px-3 py-1 bg-red-600/20 text-red-400 rounded text-sm font-mono">404</span>
|
||||
<div>
|
||||
<h4 class="font-semibold">Not Found</h4>
|
||||
<p class="text-gray-400 text-sm">Logo nebo klub nenalezen</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-4">
|
||||
<span class="px-3 py-1 bg-red-600/20 text-red-400 rounded text-sm font-mono">500</span>
|
||||
<div>
|
||||
<h4 class="font-semibold">Internal Server Error</h4>
|
||||
<p class="text-gray-400 text-sm">Interní chyba serveru</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="border-t border-dark-border mt-20">
|
||||
<div class="container mx-auto px-6 py-8 text-center text-gray-400">
|
||||
<p>🇨🇿 České Kluby Loga API</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,181 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="cs" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>🇨🇿 České Kluby Loga API</title>
|
||||
<link rel="stylesheet" href="/src/style.css">
|
||||
</head>
|
||||
<body class="bg-dark-bg text-white min-h-screen">
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="border-b border-dark-border bg-dark-card/50 backdrop-blur-sm sticky top-0 z-50">
|
||||
<div class="container mx-auto px-6 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<a href="/" class="text-2xl font-bold gradient-text">🇨🇿 České Kluby Loga</a>
|
||||
<div class="flex gap-4">
|
||||
<a href="/" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Domů</a>
|
||||
<a href="/api-docs.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">API Docs</a>
|
||||
<a href="/admin.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Admin</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<header class="relative overflow-hidden border-b border-dark-border">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-blue-600/10 to-green-600/10"></div>
|
||||
<div class="container mx-auto px-6 py-20 relative z-10">
|
||||
<div class="text-center hero-content max-w-4xl mx-auto">
|
||||
<h1 class="text-5xl md:text-7xl font-bold mb-6">
|
||||
<span class="gradient-text">České Kluby Loga CDN</span>
|
||||
</h1>
|
||||
<p class="text-xl text-gray-400 mb-8">
|
||||
Vysoce kvalitní loga českých fotbalových a futsalových klubů s průhledným pozadím.
|
||||
Založeno na UUID, API-first, připraveno pro produkci.
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-4 justify-center">
|
||||
<button id="browseBtn" class="px-8 py-4 bg-accent-blue rounded-lg font-semibold hover:bg-blue-600 transition-smooth text-lg">
|
||||
🔍 Procházet Loga
|
||||
</button>
|
||||
<a href="/admin.html" class="px-8 py-4 bg-accent-green rounded-lg font-semibold hover:bg-green-600 transition-smooth text-lg">
|
||||
⬆️ Nahrát Logo
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Logo Gallery -->
|
||||
<section class="container mx-auto px-6 py-16" id="logoGallery">
|
||||
<div class="mb-8">
|
||||
<h2 class="text-3xl font-bold mb-4">Dostupná Loga Klubů</h2>
|
||||
<input
|
||||
type="text"
|
||||
id="gallerySearch"
|
||||
placeholder="Filtrovat podle názvu klubu..."
|
||||
class="w-full max-w-md bg-dark-card border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div id="logoGrid" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-6">
|
||||
<!-- Logos will be loaded here -->
|
||||
</div>
|
||||
|
||||
<div id="loadingState" class="text-center py-16">
|
||||
<div class="spinner mx-auto"></div>
|
||||
<p class="mt-4 text-gray-400">Načítání log klubů...</p>
|
||||
</div>
|
||||
|
||||
<div id="emptyState" class="text-center py-16 hidden">
|
||||
<div class="text-6xl mb-4">⚽</div>
|
||||
<p class="text-xl text-gray-400 mb-4">Zatím nebyla nahrána žádná loga</p>
|
||||
<a href="/admin.html" class="px-6 py-3 bg-accent-green rounded-lg font-semibold hover:bg-green-600 transition-smooth inline-block">
|
||||
Nahrát První Logo
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- API Documentation Preview -->
|
||||
<section class="bg-dark-card border-y border-dark-border py-16">
|
||||
<div class="container mx-auto px-6">
|
||||
<h2 class="text-3xl font-bold mb-8 text-center">Rychlá Referenční API</h2>
|
||||
<div class="grid md:grid-cols-2 gap-6 max-w-4xl mx-auto">
|
||||
<div class="bg-dark-bg rounded-xl p-6 border border-dark-border">
|
||||
<div class="flex items-start gap-4">
|
||||
<span class="px-3 py-1 bg-accent-blue/20 text-accent-blue rounded-md text-sm font-mono">GET</span>
|
||||
<div class="flex-1">
|
||||
<p class="font-mono text-sm mb-2">/logos</p>
|
||||
<p class="text-gray-400 text-sm">Zobrazit všechna dostupná loga</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark-bg rounded-xl p-6 border border-dark-border">
|
||||
<div class="flex items-start gap-4">
|
||||
<span class="px-3 py-1 bg-accent-blue/20 text-accent-blue rounded-md text-sm font-mono">GET</span>
|
||||
<div class="flex-1">
|
||||
<p class="font-mono text-sm mb-2">/logos/:id</p>
|
||||
<p class="text-gray-400 text-sm">Získat logo podle UUID (PNG/SVG)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark-bg rounded-xl p-6 border border-dark-border">
|
||||
<div class="flex items-start gap-4">
|
||||
<span class="px-3 py-1 bg-accent-blue/20 text-accent-blue rounded-md text-sm font-mono">GET</span>
|
||||
<div class="flex-1">
|
||||
<p class="font-mono text-sm mb-2">/logos/:id/json</p>
|
||||
<p class="text-gray-400 text-sm">Získat metadata loga</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark-bg rounded-xl p-6 border border-dark-border">
|
||||
<div class="flex items-start gap-4">
|
||||
<span class="px-3 py-1 bg-accent-green/20 text-accent-green rounded-md text-sm font-mono">POST</span>
|
||||
<div class="flex-1">
|
||||
<p class="font-mono text-sm mb-2">/logos/:id</p>
|
||||
<p class="text-gray-400 text-sm">Nahrát nové logo</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features -->
|
||||
<section class="container mx-auto px-6 py-16">
|
||||
<h2 class="text-3xl font-bold mb-12 text-center">✨ Funkce</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
|
||||
|
||||
<div class="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
|
||||
<div class="text-3xl mb-4">⚽</div>
|
||||
<h3 class="text-xl font-semibold mb-2">Integrace s FAČR</h3>
|
||||
<p class="text-gray-400">Přímá integrace s oficiálním českým fotbalovým registrem</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
|
||||
<div class="text-3xl mb-4">🖼️</div>
|
||||
<h3 class="text-xl font-semibold mb-2">SVG & PNG</h3>
|
||||
<p class="text-gray-400">Nahrajte SVG, PNG se vygeneruje automaticky</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
|
||||
<div class="text-3xl mb-4">🔄</div>
|
||||
<h3 class="text-xl font-semibold mb-2">Založeno na UUID</h3>
|
||||
<p class="text-gray-400">Konzistentní identifikace napříč všemi platformami</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
|
||||
<div class="text-3xl mb-4">🌐</div>
|
||||
<h3 class="text-xl font-semibold mb-2">Připraveno pro CDN</h3>
|
||||
<p class="text-gray-400">Rychlé, cachovatelné, produkční API</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
|
||||
<div class="text-3xl mb-4">📝</div>
|
||||
<h3 class="text-xl font-semibold mb-2">Bohatá Metadata</h3>
|
||||
<p class="text-gray-400">Název klubu, město, typ, web v ceně</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
|
||||
<div class="text-3xl mb-4">🐳</div>
|
||||
<h3 class="text-xl font-semibold mb-2">Připraveno pro Docker</h3>
|
||||
<p class="text-gray-400">Nasazení jedním příkazem s Docker Compose</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="border-t border-dark-border mt-20">
|
||||
<div class="container mx-auto px-6 py-8 text-center text-gray-400">
|
||||
<p>🇨🇿 České Kluby Loga API | Vytvořeno s ❤️ pro český fotbal</p>
|
||||
<p class="text-sm mt-2">Poháněno FAČR Scraper API | Open Source MIT Licence</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="/src/home.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,162 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="cs" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Detail Loga - České Kluby Loga API</title>
|
||||
<link rel="stylesheet" href="/src/style.css">
|
||||
</head>
|
||||
<body class="bg-dark-bg text-white min-h-screen">
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="border-b border-dark-border bg-dark-card/50 backdrop-blur-sm sticky top-0 z-50">
|
||||
<div class="container mx-auto px-6 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<a href="/" class="text-2xl font-bold gradient-text">🇨🇿 České Kluby Loga</a>
|
||||
<div class="flex gap-4">
|
||||
<a href="/" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Domů</a>
|
||||
<a href="/api-docs.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">API Docs</a>
|
||||
<a href="/admin.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Admin</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="container mx-auto px-6 py-12">
|
||||
|
||||
<!-- Loading State -->
|
||||
<div id="loadingState" class="text-center py-12">
|
||||
<div class="spinner mx-auto mb-4"></div>
|
||||
<p class="text-gray-400">Načítání...</p>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div id="errorState" class="hidden text-center py-12">
|
||||
<svg class="mx-auto h-16 w-16 text-red-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<h2 class="text-2xl font-bold mb-2">Logo nenalezeno</h2>
|
||||
<p class="text-gray-400 mb-4">Logo s tímto UUID neexistuje</p>
|
||||
<a href="/" class="px-4 py-2 bg-accent-blue rounded-lg hover:bg-blue-600 transition-smooth inline-block">
|
||||
Zpět na hlavní stránku
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Logo Detail -->
|
||||
<div id="logoDetail" class="hidden">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-start justify-between mb-8">
|
||||
<div>
|
||||
<h1 id="clubName" class="text-4xl font-bold gradient-text mb-2"></h1>
|
||||
<p id="clubMeta" class="text-gray-400"></p>
|
||||
</div>
|
||||
<a href="/admin.html" class="px-4 py-2 bg-accent-blue rounded-lg hover:bg-blue-600 transition-smooth">
|
||||
✏️ Upravit
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Logo Preview -->
|
||||
<section class="mb-8">
|
||||
<div class="bg-dark-card rounded-xl p-8 border border-dark-border">
|
||||
<h2 class="text-2xl font-bold mb-6">📷 Náhled Loga</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Light Background -->
|
||||
<div class="bg-white rounded-lg p-8 flex items-center justify-center min-h-[300px]">
|
||||
<img id="logoPreviewLight" src="" alt="Logo na světlém pozadí" class="max-w-full max-h-64 object-contain">
|
||||
</div>
|
||||
<!-- Dark Background -->
|
||||
<div class="bg-gray-900 rounded-lg p-8 flex items-center justify-center min-h-[300px]">
|
||||
<img id="logoPreviewDark" src="" alt="Logo na tmavém pozadí" class="max-w-full max-h-64 object-contain">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Available Formats -->
|
||||
<section class="mb-8">
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
|
||||
<h2 class="text-2xl font-bold mb-6">💾 Dostupné Formáty</h2>
|
||||
<div id="formatsGrid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- Formats will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variants -->
|
||||
<section class="mb-8" id="variantsSection">
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
|
||||
<h2 class="text-2xl font-bold mb-6">🎨 Varianty Loga</h2>
|
||||
<div id="variantsGrid" class="space-y-4">
|
||||
<!-- Variants will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Metadata -->
|
||||
<section class="mb-8">
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
|
||||
<h2 class="text-2xl font-bold mb-6">ℹ️ Informace</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-2">UUID</h3>
|
||||
<p id="logoUuid" class="font-mono text-sm bg-dark-bg rounded px-3 py-2"></p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-2">Typ Klubu</h3>
|
||||
<p id="clubType" class="text-sm bg-dark-bg rounded px-3 py-2"></p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-2">Webová Stránka</h3>
|
||||
<p id="clubWebsite" class="text-sm bg-dark-bg rounded px-3 py-2"></p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-2">Datum Nahrání</h3>
|
||||
<p id="uploadDate" class="text-sm bg-dark-bg rounded px-3 py-2"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- API Usage -->
|
||||
<section>
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
|
||||
<h2 class="text-2xl font-bold mb-6">🔗 Použití API</h2>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-2">GET Logo (PNG preferováno)</h3>
|
||||
<div class="bg-dark-bg rounded px-4 py-3 font-mono text-sm flex items-center justify-between">
|
||||
<code id="apiUrlDefault"></code>
|
||||
<button onclick="copyToClipboard('apiUrlDefault')" class="px-3 py-1 bg-accent-blue rounded text-xs hover:bg-blue-600 transition-smooth">
|
||||
Kopírovat
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-2">GET Logo s Metadaty (JSON)</h3>
|
||||
<div class="bg-dark-bg rounded px-4 py-3 font-mono text-sm flex items-center justify-between">
|
||||
<code id="apiUrlJson"></code>
|
||||
<button onclick="copyToClipboard('apiUrlJson')" class="px-3 py-1 bg-accent-blue rounded text-xs hover:bg-blue-600 transition-smooth">
|
||||
Kopírovat
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="border-t border-dark-border mt-20">
|
||||
<div class="container mx-auto px-6 py-8 text-center text-gray-400">
|
||||
<p>🇨🇿 České Kluby Loga API</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="/src/logo.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,27 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
# Cache static assets
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# SPA fallback
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
}
|
||||
Generated
+2529
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "czech-clubs-logos-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"gsap": "^3.12.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"vite": "^5.2.11"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,547 @@
|
||||
import './style.css'
|
||||
import gsap from 'gsap'
|
||||
|
||||
// Configuration
|
||||
const API_BASE_URL = '/api' // Always use /api - Vite proxy will handle routing in dev mode
|
||||
const FACR_API_URL = 'https://facr.tdvorak.dev'
|
||||
|
||||
// ==================== Club Search ====================
|
||||
|
||||
const clubSearch = document.getElementById('clubSearch')
|
||||
const searchResults = document.getElementById('searchResults')
|
||||
const uploadSection = document.getElementById('uploadSection')
|
||||
|
||||
let searchTimeout
|
||||
|
||||
clubSearch.addEventListener('input', (e) => {
|
||||
clearTimeout(searchTimeout)
|
||||
const query = e.target.value.trim()
|
||||
|
||||
if (query.length < 2) {
|
||||
searchResults.innerHTML = ''
|
||||
return
|
||||
}
|
||||
|
||||
searchTimeout = setTimeout(() => {
|
||||
searchClubs(query)
|
||||
}, 300)
|
||||
})
|
||||
|
||||
async function searchClubs(query) {
|
||||
searchResults.innerHTML = '<div class="text-center py-4"><div class="spinner mx-auto"></div></div>'
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/clubs/search?q=${encodeURIComponent(query)}`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Vyhledávání selhalo')
|
||||
}
|
||||
|
||||
const clubs = await response.json()
|
||||
await displaySearchResults(clubs)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Search error:', error)
|
||||
searchResults.innerHTML = `
|
||||
<div class="text-center py-4 text-red-400">
|
||||
<p>Vyhledávání selhalo. Zkuste to prosím znovu.</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
async function displaySearchResults(clubs) {
|
||||
if (!clubs || clubs.length === 0) {
|
||||
searchResults.innerHTML = `
|
||||
<div class="text-center py-8 text-gray-400">
|
||||
<p>Žádné kluby nenalezeny</p>
|
||||
</div>
|
||||
`
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch logos from our API first
|
||||
let existingLogos = []
|
||||
try {
|
||||
const logosResponse = await fetch(`${API_BASE_URL}/logos`)
|
||||
if (logosResponse.ok) {
|
||||
const data = await logosResponse.json()
|
||||
existingLogos = data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Could not fetch existing logos:', error)
|
||||
}
|
||||
|
||||
searchResults.innerHTML = clubs.map(club => {
|
||||
// Check if we have this logo in our API
|
||||
const existingLogo = existingLogos.find(l => l.id === club.id)
|
||||
const logoUrl = existingLogo ? existingLogo.logo_url : (club.logo_url || '')
|
||||
|
||||
// Create logo HTML with fallback icon
|
||||
let logoHtml = ''
|
||||
if (logoUrl) {
|
||||
logoHtml = `
|
||||
<div class="flex-shrink-0 w-16 h-16 flex items-center justify-center bg-dark-border/30 rounded-lg p-2">
|
||||
<img src="${logoUrl}"
|
||||
alt="${club.name}"
|
||||
class="max-w-full max-h-full object-contain"
|
||||
onerror="this.parentElement.innerHTML='<svg class=\\'w-8 h-8 text-gray-500\\' fill=\\'none\\' stroke=\\'currentColor\\' viewBox=\\'0 0 24 24\\'><path stroke-linecap=\\'round\\' stroke-linejoin=\\'round\\' stroke-width=\\'2\\' d=\\'M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z\\'></path></svg>'">
|
||||
</div>
|
||||
`
|
||||
} else {
|
||||
logoHtml = `
|
||||
<div class="flex-shrink-0 w-16 h-16 flex items-center justify-center bg-dark-border/30 rounded-lg">
|
||||
<svg class="w-8 h-8 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="club-result bg-dark-bg rounded-lg p-4 border border-dark-border hover:border-accent-blue transition-smooth cursor-pointer" data-club='${JSON.stringify(club)}' data-logo-url='${logoUrl}'>
|
||||
<div class="flex items-center gap-4">
|
||||
${logoHtml}
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-semibold text-lg truncate">${club.name}</h3>
|
||||
<p class="text-sm text-gray-400">${club.type || 'football'}</p>
|
||||
<p class="text-xs text-gray-500 font-mono mt-1 truncate">${club.id}</p>
|
||||
${club.website ? `<p class="text-xs text-blue-400 mt-1 truncate">🌐 ${club.website}</p>` : ''}
|
||||
${existingLogo ? '<p class="text-xs text-green-400 mt-1">✓ Logo již nahráno</p>' : ''}
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 flex-shrink-0">
|
||||
${existingLogo ? `<a href="/logo.html?id=${club.id}" class="px-4 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 transition-smooth text-sm text-center" onclick="event.stopPropagation()">👁️ Detail</a>` : ''}
|
||||
<button class="select-club px-4 py-2 bg-accent-blue rounded-lg hover:bg-blue-600 transition-smooth text-sm">
|
||||
Vybrat
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}).join('')
|
||||
|
||||
// Animate results
|
||||
gsap.from('.club-result', {
|
||||
duration: 0.4,
|
||||
opacity: 0,
|
||||
y: 20,
|
||||
stagger: 0.08,
|
||||
ease: 'power2.out'
|
||||
})
|
||||
|
||||
// Add click handlers
|
||||
document.querySelectorAll('.club-result').forEach(result => {
|
||||
result.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('select-club') || e.target.closest('.select-club')) {
|
||||
const clubData = JSON.parse(result.dataset.club)
|
||||
selectClub(clubData)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function selectClub(club) {
|
||||
// Fill form
|
||||
document.getElementById('clubUuid').value = club.id
|
||||
document.getElementById('clubName').value = club.name
|
||||
document.getElementById('clubType').value = club.type || 'football'
|
||||
document.getElementById('clubWebsite').value = club.website || ''
|
||||
|
||||
// Show upload section
|
||||
uploadSection.classList.remove('hidden')
|
||||
|
||||
// Scroll to upload section
|
||||
uploadSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
|
||||
// Animate upload section
|
||||
gsap.from(uploadSection, {
|
||||
duration: 0.5,
|
||||
opacity: 0,
|
||||
y: 20,
|
||||
ease: 'power2.out'
|
||||
})
|
||||
|
||||
showNotification(`Vybráno: ${club.name}`, 'success')
|
||||
}
|
||||
|
||||
// ==================== Website Search ====================
|
||||
|
||||
const searchWebsiteBtn = document.getElementById('searchWebsite')
|
||||
const websiteSearchResults = document.getElementById('websiteSearchResults')
|
||||
|
||||
searchWebsiteBtn.addEventListener('click', async () => {
|
||||
const clubName = document.getElementById('clubName').value.trim()
|
||||
|
||||
if (!clubName) {
|
||||
showNotification('Nejprve zadejte název klubu', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
searchWebsiteBtn.innerHTML = '<div class="spinner inline-block w-4 h-4"></div>'
|
||||
searchWebsiteBtn.disabled = true
|
||||
|
||||
try {
|
||||
const searchQuery = encodeURIComponent(`${clubName} český fotbal oficiální web`)
|
||||
const searchUrl = `https://www.google.com/search?q=${searchQuery}`
|
||||
|
||||
websiteSearchResults.innerHTML = `
|
||||
<div class="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||
<p class="text-sm text-gray-400 mb-2">Vyhledat web klubu:</p>
|
||||
<a href="${searchUrl}" target="_blank" class="text-accent-blue hover:text-blue-400 text-sm">
|
||||
🔍 Hledat "${clubName}" na Google
|
||||
</a>
|
||||
<p class="text-xs text-gray-500 mt-2">Zkopírujte URL oficiálního webu a vložte jej výše</p>
|
||||
</div>
|
||||
`
|
||||
websiteSearchResults.classList.remove('hidden')
|
||||
|
||||
} catch (error) {
|
||||
console.error('Website search error:', error)
|
||||
} finally {
|
||||
searchWebsiteBtn.innerHTML = '🔍 Hledat Online'
|
||||
searchWebsiteBtn.disabled = false
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== File Upload ====================
|
||||
|
||||
const uploadArea = document.getElementById('uploadArea')
|
||||
const fileInput = document.getElementById('fileInput')
|
||||
const filesPreviewArea = document.getElementById('filesPreviewArea')
|
||||
const filesPreviewList = document.getElementById('filesPreviewList')
|
||||
const uploadForm = document.getElementById('uploadForm')
|
||||
|
||||
let selectedFiles = []
|
||||
|
||||
// Click to browse
|
||||
uploadArea.addEventListener('click', (e) => {
|
||||
if (e.target === uploadArea || e.target.closest('#uploadArea')) {
|
||||
fileInput.click()
|
||||
}
|
||||
})
|
||||
|
||||
// Drag and drop
|
||||
uploadArea.addEventListener('dragover', (e) => {
|
||||
e.preventDefault()
|
||||
uploadArea.classList.add('dragover', 'border-accent-blue')
|
||||
})
|
||||
|
||||
uploadArea.addEventListener('dragleave', () => {
|
||||
uploadArea.classList.remove('dragover', 'border-accent-blue')
|
||||
})
|
||||
|
||||
uploadArea.addEventListener('drop', (e) => {
|
||||
e.preventDefault()
|
||||
uploadArea.classList.remove('dragover', 'border-accent-blue')
|
||||
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
if (files.length > 0) {
|
||||
handleFilesSelect(files)
|
||||
}
|
||||
})
|
||||
|
||||
// File input change
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
handleFilesSelect(Array.from(e.target.files))
|
||||
}
|
||||
})
|
||||
|
||||
function handleFilesSelect(files) {
|
||||
// Validate and filter files
|
||||
const validFiles = []
|
||||
|
||||
for (const file of files) {
|
||||
const ext = file.name.split('.').pop().toLowerCase()
|
||||
if (ext === 'svg' || ext === 'png' || ext === 'pdf') {
|
||||
validFiles.push({
|
||||
file: file,
|
||||
ext: ext,
|
||||
name: '',
|
||||
description: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (validFiles.length === 0) {
|
||||
showNotification('Vyberte prosím SVG, PNG nebo PDF soubory', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
selectedFiles = validFiles
|
||||
displayFilesPreview()
|
||||
}
|
||||
|
||||
function displayFilesPreview() {
|
||||
if (selectedFiles.length === 0) {
|
||||
filesPreviewArea.classList.add('hidden')
|
||||
return
|
||||
}
|
||||
|
||||
filesPreviewArea.classList.remove('hidden')
|
||||
|
||||
filesPreviewList.innerHTML = selectedFiles.map((fileObj, index) => {
|
||||
const sizeKB = (fileObj.file.size / 1024).toFixed(2)
|
||||
const isPrimary = index === 0
|
||||
|
||||
return `
|
||||
<div class="bg-dark-bg rounded-lg p-4 border border-dark-border" data-file-index="${index}">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 w-16 h-16 bg-dark-border/30 rounded flex items-center justify-center">
|
||||
<span class="text-2xl">${fileObj.ext === 'svg' ? '📐' : fileObj.ext === 'pdf' ? '📄' : '🖼️'}</span>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h4 class="font-semibold">${fileObj.file.name}</h4>
|
||||
${isPrimary ? '<span class="px-2 py-0.5 bg-accent-blue rounded text-xs">Hlavní</span>' : ''}
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mb-3">${fileObj.ext.toUpperCase()} • ${sizeKB} KB</p>
|
||||
|
||||
<div class="space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Název varianty (volitelné)"
|
||||
value="${fileObj.name}"
|
||||
onchange="updateFileMetadata(${index}, 'name', this.value)"
|
||||
class="w-full bg-dark-card border border-dark-border rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-accent-blue transition-smooth"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Popis (volitelné)"
|
||||
value="${fileObj.description}"
|
||||
onchange="updateFileMetadata(${index}, 'description', this.value)"
|
||||
class="w-full bg-dark-card border border-dark-border rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-accent-blue transition-smooth"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onclick="removeFile(${index})" class="flex-shrink-0 p-2 text-red-400 hover:text-red-300 transition-smooth">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}).join('')
|
||||
|
||||
gsap.from('.bg-dark-bg[data-file-index]', {
|
||||
duration: 0.4,
|
||||
opacity: 0,
|
||||
y: 10,
|
||||
stagger: 0.05,
|
||||
ease: 'power2.out'
|
||||
})
|
||||
}
|
||||
|
||||
window.updateFileMetadata = function(index, field, value) {
|
||||
if (selectedFiles[index]) {
|
||||
selectedFiles[index][field] = value
|
||||
}
|
||||
}
|
||||
|
||||
window.removeFile = function(index) {
|
||||
selectedFiles.splice(index, 1)
|
||||
displayFilesPreview()
|
||||
|
||||
if (selectedFiles.length === 0) {
|
||||
fileInput.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// Form submission
|
||||
uploadForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const uuid = document.getElementById('clubUuid').value.trim()
|
||||
const clubName = document.getElementById('clubName').value.trim()
|
||||
const clubType = document.getElementById('clubType').value
|
||||
const clubWebsite = document.getElementById('clubWebsite').value.trim()
|
||||
|
||||
// Validation
|
||||
if (!uuid) {
|
||||
showNotification('Nejprve vyberte klub', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
if (!clubName) {
|
||||
showNotification('Název klubu je povinný', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedFiles.length === 0) {
|
||||
showNotification('Vyberte prosím soubor loga', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
// Validate UUID format
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
if (!uuidRegex.test(uuid)) {
|
||||
showNotification('Neplatný formát UUID', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
await uploadLogos(uuid, clubName, clubType, clubWebsite, selectedFiles)
|
||||
})
|
||||
|
||||
async function uploadLogos(uuid, clubName, clubType, clubWebsite, filesData) {
|
||||
const submitBtn = document.getElementById('uploadSubmit')
|
||||
const originalText = submitBtn.textContent
|
||||
submitBtn.disabled = true
|
||||
submitBtn.innerHTML = '<div class="spinner mx-auto"></div>'
|
||||
|
||||
try {
|
||||
let uploadedCount = 0
|
||||
|
||||
// Upload each file
|
||||
for (let i = 0; i < filesData.length; i++) {
|
||||
const fileData = filesData[i]
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append('file', fileData.file)
|
||||
formData.append('club_name', clubName)
|
||||
if (clubType) formData.append('club_type', clubType)
|
||||
if (clubWebsite) formData.append('club_website', clubWebsite)
|
||||
|
||||
// Add variant metadata if not the first file
|
||||
if (i > 0) {
|
||||
formData.append('variant', 'true')
|
||||
if (fileData.name) formData.append('variant_name', fileData.name)
|
||||
if (fileData.description) formData.append('variant_description', fileData.description)
|
||||
} else {
|
||||
// First file is primary
|
||||
if (fileData.name) formData.append('variant_name', fileData.name || 'Hlavní')
|
||||
if (fileData.description) formData.append('variant_description', fileData.description)
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/logos/${uuid}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Upload failed')
|
||||
}
|
||||
|
||||
uploadedCount++
|
||||
submitBtn.innerHTML = `<div class="spinner mx-auto"></div> ${uploadedCount}/${filesData.length}`
|
||||
}
|
||||
|
||||
showNotification(`${uploadedCount} ${uploadedCount === 1 ? 'logo' : 'loga'} úspěšně nahráno pro ${clubName}! ✓`, 'success')
|
||||
|
||||
// Reset form after delay
|
||||
setTimeout(() => {
|
||||
uploadForm.reset()
|
||||
filesPreviewArea.classList.add('hidden')
|
||||
selectedFiles = []
|
||||
uploadSection.classList.add('hidden')
|
||||
clubSearch.value = ''
|
||||
searchResults.innerHTML = ''
|
||||
fileInput.value = ''
|
||||
}, 2000)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error)
|
||||
showNotification(`Nahrání selhalo: ${error.message}`, 'error')
|
||||
} finally {
|
||||
submitBtn.disabled = false
|
||||
submitBtn.textContent = originalText
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Utility Functions ====================
|
||||
|
||||
function showNotification(message, type = 'info') {
|
||||
const notification = document.createElement('div')
|
||||
notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 ${
|
||||
type === 'success' ? 'bg-accent-green' :
|
||||
type === 'error' ? 'bg-red-500' :
|
||||
'bg-accent-blue'
|
||||
} text-white font-medium`
|
||||
notification.textContent = message
|
||||
|
||||
document.body.appendChild(notification)
|
||||
|
||||
gsap.from(notification, {
|
||||
duration: 0.3,
|
||||
opacity: 0,
|
||||
y: -20,
|
||||
ease: 'power2.out'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
gsap.to(notification, {
|
||||
duration: 0.3,
|
||||
opacity: 0,
|
||||
y: -20,
|
||||
ease: 'power2.in',
|
||||
onComplete: () => notification.remove()
|
||||
})
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
// ==================== Initialize ====================
|
||||
|
||||
console.log('🇨🇿 České Kluby Loga API - Administrace')
|
||||
console.log('Backend API:', API_BASE_URL)
|
||||
console.log('FAČR API:', FACR_API_URL)
|
||||
|
||||
// Load from URL functionality
|
||||
const loadFromUrlBtn = document.getElementById('loadFromUrl')
|
||||
const logoUrlInput = document.getElementById('logoUrl')
|
||||
|
||||
loadFromUrlBtn.addEventListener('click', async () => {
|
||||
const url = logoUrlInput.value.trim()
|
||||
|
||||
if (!url) {
|
||||
showNotification('Zadejte prosím URL obrázku', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
showNotification('URL musí začínat http:// nebo https://', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
loadFromUrlBtn.disabled = true
|
||||
loadFromUrlBtn.innerHTML = '<div class="spinner inline-block w-4 h-4"></div>'
|
||||
|
||||
try {
|
||||
// Fetch the image from URL
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) throw new Error('Nelze načíst obrázek')
|
||||
|
||||
const blob = await response.blob()
|
||||
|
||||
// Determine file extension from content type or URL
|
||||
let ext = 'png'
|
||||
const contentType = response.headers.get('content-type')
|
||||
if (contentType) {
|
||||
if (contentType.includes('svg')) ext = 'svg'
|
||||
else if (contentType.includes('pdf')) ext = 'pdf'
|
||||
else if (contentType.includes('png')) ext = 'png'
|
||||
} else {
|
||||
const urlExt = url.split('.').pop().toLowerCase().split('?')[0]
|
||||
if (['svg', 'png', 'pdf'].includes(urlExt)) ext = urlExt
|
||||
}
|
||||
|
||||
// Create a file from the blob
|
||||
const filename = `logo-${Date.now()}.${ext}`
|
||||
const file = new File([blob], filename, { type: blob.type })
|
||||
|
||||
handleFilesSelect([file])
|
||||
showNotification('Obrázek úspěšně načten z URL', 'success')
|
||||
|
||||
} catch (error) {
|
||||
console.error('Load from URL error:', error)
|
||||
showNotification(`Chyba načítání: ${error.message}`, 'error')
|
||||
} finally {
|
||||
loadFromUrlBtn.disabled = false
|
||||
loadFromUrlBtn.innerHTML = '📥 Načíst z URL'
|
||||
}
|
||||
})
|
||||
|
||||
// Show info notification
|
||||
setTimeout(() => {
|
||||
showNotification('Administrace: Vyhledejte kluby a nahrajte loga', 'info')
|
||||
}, 1000)
|
||||
@@ -0,0 +1,205 @@
|
||||
import './style.css'
|
||||
import gsap from 'gsap'
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger)
|
||||
|
||||
// Configuration
|
||||
const API_BASE_URL = '/api' // Always use /api - Vite proxy will handle routing in dev mode
|
||||
|
||||
// ==================== GSAP Animations ====================
|
||||
|
||||
// Hero animation on load
|
||||
gsap.from('.hero-content', {
|
||||
duration: 1,
|
||||
opacity: 0,
|
||||
y: 50,
|
||||
ease: 'power3.out',
|
||||
delay: 0.2
|
||||
})
|
||||
|
||||
// Animate feature cards on scroll
|
||||
gsap.utils.toArray('.feature-card').forEach((card, index) => {
|
||||
gsap.from(card, {
|
||||
scrollTrigger: {
|
||||
trigger: card,
|
||||
start: 'top 80%',
|
||||
toggleActions: 'play none none reverse'
|
||||
},
|
||||
duration: 0.6,
|
||||
opacity: 0,
|
||||
y: 30,
|
||||
delay: index * 0.1,
|
||||
ease: 'power2.out'
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Logo Gallery ====================
|
||||
|
||||
const logoGrid = document.getElementById('logoGrid')
|
||||
const loadingState = document.getElementById('loadingState')
|
||||
const emptyState = document.getElementById('emptyState')
|
||||
const gallerySearch = document.getElementById('gallerySearch')
|
||||
const browseBtn = document.getElementById('browseBtn')
|
||||
|
||||
let allLogos = []
|
||||
|
||||
// Load logos
|
||||
async function loadLogos() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/logos`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch logos')
|
||||
}
|
||||
|
||||
allLogos = await response.json()
|
||||
|
||||
loadingState.classList.add('hidden')
|
||||
|
||||
if (allLogos.length === 0) {
|
||||
emptyState.classList.remove('hidden')
|
||||
} else {
|
||||
displayLogos(allLogos)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading logos:', error)
|
||||
loadingState.classList.add('hidden')
|
||||
emptyState.classList.remove('hidden')
|
||||
}
|
||||
}
|
||||
|
||||
// Display logos in grid
|
||||
function displayLogos(logos) {
|
||||
logoGrid.innerHTML = logos.map(logo => `
|
||||
<div class="logo-card bg-dark-card rounded-xl p-4 border border-dark-border hover:border-accent-blue transition-smooth cursor-pointer group" data-logo-id="${logo.id}">
|
||||
<div class="aspect-square bg-dark-bg rounded-lg flex items-center justify-center mb-3 overflow-hidden">
|
||||
<img
|
||||
src="${logo.logo_url}"
|
||||
alt="${logo.club_name}"
|
||||
class="max-w-full max-h-full object-contain p-2 group-hover:scale-110 transition-transform duration-300"
|
||||
loading="lazy"
|
||||
onerror="this.parentElement.innerHTML='<svg class=\\'w-8 h-8 text-gray-500\\' fill=\\'none\\' stroke=\\'currentColor\\' viewBox=\\'0 0 24 24\\'><path stroke-linecap=\\'round\\' stroke-linejoin=\\'round\\' stroke-width=\\'2\\' d=\\'M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z\\'></path></svg>'"
|
||||
>
|
||||
</div>
|
||||
<h3 class="font-semibold text-sm truncate mb-1">${logo.club_name}</h3>
|
||||
<p class="text-xs text-gray-400 truncate">${logo.club_type || 'fotbal'}</p>
|
||||
<div class="flex gap-1 mt-2">
|
||||
${logo.has_svg ? '<span class="px-2 py-0.5 bg-blue-500/20 text-blue-400 rounded text-xs">SVG</span>' : ''}
|
||||
${logo.has_png ? '<span class="px-2 py-0.5 bg-green-500/20 text-green-400 rounded text-xs">PNG</span>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')
|
||||
|
||||
// Animate logo cards
|
||||
gsap.from('.logo-card', {
|
||||
duration: 0.5,
|
||||
opacity: 0,
|
||||
scale: 0.9,
|
||||
stagger: 0.05,
|
||||
ease: 'power2.out'
|
||||
})
|
||||
|
||||
// Add click handlers to navigate to logo detail page
|
||||
document.querySelectorAll('.logo-card').forEach((card, index) => {
|
||||
card.addEventListener('click', () => {
|
||||
const logo = logos[index]
|
||||
window.location.href = `/logo.html?id=${logo.id}`
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Filter logos
|
||||
function filterLogos(query) {
|
||||
const filtered = allLogos.filter(logo =>
|
||||
logo.club_name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
(logo.club_city && logo.club_city.toLowerCase().includes(query.toLowerCase()))
|
||||
)
|
||||
|
||||
displayLogos(filtered)
|
||||
|
||||
if (filtered.length === 0 && query) {
|
||||
logoGrid.innerHTML = `
|
||||
<div class="col-span-full text-center py-16">
|
||||
<p class="text-xl text-gray-400">No logos found matching "${query}"</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
// Copy logo URL
|
||||
function copyLogoURL(url, clubName) {
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
showNotification(`Logo URL copied for ${clubName}!`, 'success')
|
||||
}).catch(() => {
|
||||
showNotification('Failed to copy URL', 'error')
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== Event Handlers ====================
|
||||
|
||||
// Gallery search
|
||||
if (gallerySearch) {
|
||||
let searchTimeout
|
||||
gallerySearch.addEventListener('input', (e) => {
|
||||
clearTimeout(searchTimeout)
|
||||
searchTimeout = setTimeout(() => {
|
||||
filterLogos(e.target.value.trim())
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
||||
// Browse button - scroll to gallery
|
||||
if (browseBtn) {
|
||||
browseBtn.addEventListener('click', () => {
|
||||
document.getElementById('logoGallery').scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== Utility Functions ====================
|
||||
|
||||
function showNotification(message, type = 'info') {
|
||||
const notification = document.createElement('div')
|
||||
notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 ${
|
||||
type === 'success' ? 'bg-accent-green' :
|
||||
type === 'error' ? 'bg-red-500' :
|
||||
'bg-accent-blue'
|
||||
} text-white font-medium`
|
||||
notification.textContent = message
|
||||
|
||||
document.body.appendChild(notification)
|
||||
|
||||
gsap.from(notification, {
|
||||
duration: 0.3,
|
||||
opacity: 0,
|
||||
y: -20,
|
||||
ease: 'power2.out'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
gsap.to(notification, {
|
||||
duration: 0.3,
|
||||
opacity: 0,
|
||||
y: -20,
|
||||
ease: 'power2.in',
|
||||
onComplete: () => notification.remove()
|
||||
})
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
// ==================== Initialize ====================
|
||||
|
||||
console.log('🇨🇿 Czech Clubs Logos API - Home')
|
||||
console.log('Backend API:', API_BASE_URL)
|
||||
|
||||
// Load logos on page load
|
||||
loadLogos()
|
||||
|
||||
// Show welcome notification
|
||||
setTimeout(() => {
|
||||
showNotification('Welcome to Czech Clubs Logos API! 🇨🇿', 'info')
|
||||
}, 1000)
|
||||
@@ -0,0 +1,218 @@
|
||||
import './style.css'
|
||||
import gsap from 'gsap'
|
||||
|
||||
// Configuration
|
||||
const API_BASE_URL = window.location.hostname === 'localhost' ? '/api' : 'http://localhost:8080'
|
||||
|
||||
// Get UUID from URL
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const logoId = urlParams.get('id')
|
||||
|
||||
// DOM Elements
|
||||
const loadingState = document.getElementById('loadingState')
|
||||
const errorState = document.getElementById('errorState')
|
||||
const logoDetail = document.getElementById('logoDetail')
|
||||
|
||||
// Initialize
|
||||
if (!logoId) {
|
||||
showError()
|
||||
} else {
|
||||
loadLogoDetails(logoId)
|
||||
}
|
||||
|
||||
async function loadLogoDetails(id) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/logos/${id}/json`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Logo not found')
|
||||
}
|
||||
|
||||
const logo = await response.json()
|
||||
displayLogoDetails(logo)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading logo:', error)
|
||||
showError()
|
||||
}
|
||||
}
|
||||
|
||||
function displayLogoDetails(logo) {
|
||||
// Hide loading, show content
|
||||
loadingState.classList.add('hidden')
|
||||
logoDetail.classList.remove('hidden')
|
||||
|
||||
// Club Info
|
||||
document.getElementById('clubName').textContent = logo.club_name
|
||||
document.getElementById('clubMeta').textContent = `${logo.club_type || 'fotbal'}`
|
||||
|
||||
// Logo Previews
|
||||
const previewUrl = logo.logo_url || logo.logo_url_png || logo.logo_url_svg
|
||||
document.getElementById('logoPreviewLight').src = previewUrl
|
||||
document.getElementById('logoPreviewDark').src = previewUrl
|
||||
|
||||
// Formats
|
||||
const formatsGrid = document.getElementById('formatsGrid')
|
||||
const formats = []
|
||||
|
||||
if (logo.has_png && logo.logo_url_png) {
|
||||
formats.push({
|
||||
name: 'PNG',
|
||||
url: logo.logo_url_png,
|
||||
size: formatFileSize(logo.file_size_png),
|
||||
icon: '🖼️',
|
||||
color: 'bg-blue-600'
|
||||
})
|
||||
}
|
||||
|
||||
if (logo.has_svg && logo.logo_url_svg) {
|
||||
formats.push({
|
||||
name: 'SVG',
|
||||
url: logo.logo_url_svg,
|
||||
size: formatFileSize(logo.file_size_svg),
|
||||
icon: '📐',
|
||||
color: 'bg-green-600'
|
||||
})
|
||||
}
|
||||
|
||||
formatsGrid.innerHTML = formats.map(format => `
|
||||
<a href="${format.url}" download class="block bg-dark-bg rounded-lg p-4 border border-dark-border hover:border-accent-blue transition-smooth">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-2xl">${format.icon}</span>
|
||||
<span class="px-2 py-1 ${format.color} rounded text-xs font-semibold">${format.name}</span>
|
||||
</div>
|
||||
<h3 class="font-semibold mb-1">${format.name} Format</h3>
|
||||
<p class="text-sm text-gray-400">${format.size}</p>
|
||||
<div class="mt-3 flex items-center text-accent-blue text-sm">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
||||
</svg>
|
||||
Stáhnout
|
||||
</div>
|
||||
</a>
|
||||
`).join('')
|
||||
|
||||
// Variants (if supported)
|
||||
if (logo.variants && logo.variants.length > 0) {
|
||||
document.getElementById('variantsSection').classList.remove('hidden')
|
||||
const variantsGrid = document.getElementById('variantsGrid')
|
||||
|
||||
variantsGrid.innerHTML = logo.variants.map(variant => `
|
||||
<div class="bg-dark-bg rounded-lg p-4 border border-dark-border">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 w-20 h-20 bg-white rounded flex items-center justify-center p-2">
|
||||
<img src="${variant.url}" alt="${variant.name}" class="max-w-full max-h-full object-contain">
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-semibold mb-1">${variant.name || 'Varianta'}</h3>
|
||||
${variant.description ? `<p class="text-sm text-gray-400 mb-2">${variant.description}</p>` : ''}
|
||||
<div class="flex items-center gap-3 text-xs text-gray-500">
|
||||
<span>${variant.format.toUpperCase()}</span>
|
||||
<span>•</span>
|
||||
<span>${formatFileSize(variant.size)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="${variant.url}" download class="px-3 py-2 bg-accent-blue rounded-lg hover:bg-blue-600 transition-smooth text-sm">
|
||||
⬇️
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')
|
||||
} else {
|
||||
document.getElementById('variantsSection').classList.add('hidden')
|
||||
}
|
||||
|
||||
// Metadata
|
||||
document.getElementById('logoUuid').textContent = logo.id
|
||||
document.getElementById('clubType').textContent = logo.club_type || 'fotbal'
|
||||
|
||||
const website = logo.club_website || 'N/A'
|
||||
const websiteElement = document.getElementById('clubWebsite')
|
||||
if (logo.club_website) {
|
||||
websiteElement.innerHTML = `<a href="${logo.club_website}" target="_blank" class="text-accent-blue hover:underline">${logo.club_website}</a>`
|
||||
} else {
|
||||
websiteElement.textContent = website
|
||||
}
|
||||
|
||||
document.getElementById('uploadDate').textContent = formatDate(logo.created_at)
|
||||
|
||||
// API URLs
|
||||
const baseUrl = window.location.origin
|
||||
document.getElementById('apiUrlDefault').textContent = `${baseUrl}/logos/${logo.id}`
|
||||
document.getElementById('apiUrlJson').textContent = `${baseUrl}/logos/${logo.id}/json`
|
||||
|
||||
// Animate
|
||||
gsap.from('#logoDetail > *', {
|
||||
duration: 0.6,
|
||||
opacity: 0,
|
||||
y: 20,
|
||||
stagger: 0.1,
|
||||
ease: 'power2.out'
|
||||
})
|
||||
}
|
||||
|
||||
function showError() {
|
||||
loadingState.classList.add('hidden')
|
||||
errorState.classList.remove('hidden')
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (!bytes) return 'N/A'
|
||||
if (bytes < 1024) return bytes + ' B'
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB'
|
||||
return (bytes / (1024 * 1024)).toFixed(2) + ' MB'
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return 'N/A'
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('cs-CZ', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
window.copyToClipboard = function(elementId) {
|
||||
const element = document.getElementById(elementId)
|
||||
const text = element.textContent
|
||||
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showNotification('URL zkopírováno do schránky', 'success')
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy:', err)
|
||||
showNotification('Chyba při kopírování', 'error')
|
||||
})
|
||||
}
|
||||
|
||||
function showNotification(message, type = 'info') {
|
||||
const notification = document.createElement('div')
|
||||
notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 ${
|
||||
type === 'success' ? 'bg-accent-green' :
|
||||
type === 'error' ? 'bg-red-500' :
|
||||
'bg-accent-blue'
|
||||
} text-white font-medium`
|
||||
notification.textContent = message
|
||||
|
||||
document.body.appendChild(notification)
|
||||
|
||||
gsap.from(notification, {
|
||||
duration: 0.3,
|
||||
opacity: 0,
|
||||
y: -20,
|
||||
ease: 'power2.out'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
gsap.to(notification, {
|
||||
duration: 0.3,
|
||||
opacity: 0,
|
||||
y: -20,
|
||||
ease: 'power2.in',
|
||||
onComplete: () => notification.remove()
|
||||
})
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
console.log('🇨🇿 České Kluby Loga API - Detail Loga')
|
||||
console.log('Logo ID:', logoId)
|
||||
@@ -0,0 +1,424 @@
|
||||
import './style.css'
|
||||
import gsap from 'gsap'
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger)
|
||||
|
||||
// Configuration
|
||||
const API_BASE_URL = '/api' // Always use /api - Vite proxy will handle routing in dev mode
|
||||
const FACR_API_URL = 'https://facr.tdvorak.dev'
|
||||
|
||||
// ==================== GSAP Animations ====================
|
||||
|
||||
// Hero animation on load
|
||||
gsap.from('.hero-content', {
|
||||
duration: 1,
|
||||
opacity: 0,
|
||||
y: 50,
|
||||
ease: 'power3.out',
|
||||
delay: 0.2
|
||||
})
|
||||
|
||||
// Animate feature cards on scroll
|
||||
gsap.utils.toArray('.feature-card').forEach((card, index) => {
|
||||
gsap.from(card, {
|
||||
scrollTrigger: {
|
||||
trigger: card,
|
||||
start: 'top 80%',
|
||||
toggleActions: 'play none none reverse'
|
||||
},
|
||||
duration: 0.6,
|
||||
opacity: 0,
|
||||
y: 30,
|
||||
delay: index * 0.1,
|
||||
ease: 'power2.out'
|
||||
})
|
||||
})
|
||||
|
||||
// Animate API endpoint cards
|
||||
gsap.utils.toArray('.api-section .card-hover').forEach((card, index) => {
|
||||
gsap.from(card, {
|
||||
scrollTrigger: {
|
||||
trigger: card,
|
||||
start: 'top 85%',
|
||||
toggleActions: 'play none none reverse'
|
||||
},
|
||||
duration: 0.5,
|
||||
opacity: 0,
|
||||
x: -20,
|
||||
delay: index * 0.08,
|
||||
ease: 'power2.out'
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== UI State Management ====================
|
||||
|
||||
const searchSection = document.getElementById('searchSection')
|
||||
const uploadSection = document.getElementById('uploadSection')
|
||||
const searchBtn = document.getElementById('searchBtn')
|
||||
const uploadBtn = document.getElementById('uploadBtn')
|
||||
|
||||
// Section toggle handlers
|
||||
searchBtn.addEventListener('click', () => {
|
||||
gsap.to(searchSection, {
|
||||
duration: 0.5,
|
||||
opacity: 1,
|
||||
display: 'block',
|
||||
ease: 'power2.inOut'
|
||||
})
|
||||
gsap.to(uploadSection, {
|
||||
duration: 0.5,
|
||||
opacity: 0,
|
||||
display: 'none',
|
||||
ease: 'power2.inOut'
|
||||
})
|
||||
|
||||
// Smooth scroll to section
|
||||
searchSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
})
|
||||
|
||||
uploadBtn.addEventListener('click', () => {
|
||||
gsap.to(uploadSection, {
|
||||
duration: 0.5,
|
||||
opacity: 1,
|
||||
display: 'block',
|
||||
ease: 'power2.inOut'
|
||||
})
|
||||
gsap.to(searchSection, {
|
||||
duration: 0.5,
|
||||
opacity: 0,
|
||||
display: 'none',
|
||||
ease: 'power2.inOut'
|
||||
})
|
||||
|
||||
// Smooth scroll to section
|
||||
uploadSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
})
|
||||
|
||||
// ==================== Search Functionality ====================
|
||||
|
||||
const searchInput = document.getElementById('searchInput')
|
||||
const searchResults = document.getElementById('searchResults')
|
||||
let searchTimeout
|
||||
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
clearTimeout(searchTimeout)
|
||||
const query = e.target.value.trim()
|
||||
|
||||
if (query.length < 2) {
|
||||
searchResults.innerHTML = ''
|
||||
return
|
||||
}
|
||||
|
||||
// Debounce search
|
||||
searchTimeout = setTimeout(() => {
|
||||
searchClubs(query)
|
||||
}, 300)
|
||||
})
|
||||
|
||||
async function searchClubs(query) {
|
||||
searchResults.innerHTML = '<div class="text-center py-4"><div class="spinner mx-auto"></div></div>'
|
||||
|
||||
try {
|
||||
// Try to fetch from your backend API first
|
||||
// If backend is not ready, show demo data
|
||||
const response = await fetch(`${API_BASE_URL}/clubs/search?q=${encodeURIComponent(query)}`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Backend not available')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
displaySearchResults(data)
|
||||
|
||||
} catch (error) {
|
||||
console.log('Backend not available, showing demo data')
|
||||
// Demo data when backend is not ready
|
||||
displaySearchResults(getDemoClubs(query))
|
||||
}
|
||||
}
|
||||
|
||||
function getDemoClubs(query) {
|
||||
const demoClubs = [
|
||||
{
|
||||
id: '11111111-2222-3333-4444-555555555555',
|
||||
name: 'SK Slavia Praha',
|
||||
city: 'Praha',
|
||||
type: 'football'
|
||||
},
|
||||
{
|
||||
id: '22222222-3333-4444-5555-666666666666',
|
||||
name: 'AC Sparta Praha',
|
||||
city: 'Praha',
|
||||
type: 'football'
|
||||
},
|
||||
{
|
||||
id: '33333333-4444-5555-6666-777777777777',
|
||||
name: 'FC Viktoria Plzeň',
|
||||
city: 'Plzeň',
|
||||
type: 'football'
|
||||
},
|
||||
{
|
||||
id: '44444444-5555-6666-7777-888888888888',
|
||||
name: 'FC Baník Ostrava',
|
||||
city: 'Ostrava',
|
||||
type: 'football'
|
||||
}
|
||||
]
|
||||
|
||||
return demoClubs.filter(club =>
|
||||
club.name.toLowerCase().includes(query.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
function displaySearchResults(clubs) {
|
||||
if (clubs.length === 0) {
|
||||
searchResults.innerHTML = `
|
||||
<div class="text-center py-8 text-gray-400">
|
||||
<p>No clubs found</p>
|
||||
</div>
|
||||
`
|
||||
return
|
||||
}
|
||||
|
||||
searchResults.innerHTML = clubs.map(club => `
|
||||
<div class="club-result bg-dark-bg rounded-lg p-4 border border-dark-border hover:border-accent-blue transition-smooth cursor-pointer">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<h3 class="font-semibold text-lg">${club.name}</h3>
|
||||
<p class="text-sm text-gray-400">${club.city || 'N/A'} • ${club.type || 'football'}</p>
|
||||
<p class="text-xs text-gray-500 font-mono mt-1">${club.id}</p>
|
||||
</div>
|
||||
<button
|
||||
class="copy-uuid px-4 py-2 bg-accent-blue/20 text-accent-blue rounded-lg hover:bg-accent-blue/30 transition-smooth text-sm"
|
||||
data-uuid="${club.id}"
|
||||
>
|
||||
Copy UUID
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')
|
||||
|
||||
// Animate results
|
||||
gsap.from('.club-result', {
|
||||
duration: 0.4,
|
||||
opacity: 0,
|
||||
y: 20,
|
||||
stagger: 0.08,
|
||||
ease: 'power2.out'
|
||||
})
|
||||
|
||||
// Add copy UUID handlers
|
||||
document.querySelectorAll('.copy-uuid').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
const uuid = btn.dataset.uuid
|
||||
copyToClipboard(uuid)
|
||||
|
||||
// Visual feedback
|
||||
const originalText = btn.textContent
|
||||
btn.textContent = '✓ Copied!'
|
||||
setTimeout(() => {
|
||||
btn.textContent = originalText
|
||||
}, 2000)
|
||||
})
|
||||
})
|
||||
|
||||
// Add click handlers to fill upload form
|
||||
document.querySelectorAll('.club-result').forEach(result => {
|
||||
result.addEventListener('click', () => {
|
||||
const uuid = result.querySelector('.copy-uuid').dataset.uuid
|
||||
document.getElementById('clubUuid').value = uuid
|
||||
uploadBtn.click() // Switch to upload section
|
||||
|
||||
// Highlight the UUID input
|
||||
const uuidInput = document.getElementById('clubUuid')
|
||||
gsap.fromTo(uuidInput,
|
||||
{ backgroundColor: 'rgba(59, 130, 246, 0.2)' },
|
||||
{ backgroundColor: 'transparent', duration: 1, ease: 'power2.out' }
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== Upload Functionality ====================
|
||||
|
||||
const uploadArea = document.getElementById('uploadArea')
|
||||
const fileInput = document.getElementById('fileInput')
|
||||
const previewArea = document.getElementById('previewArea')
|
||||
const previewImage = document.getElementById('previewImage')
|
||||
const uploadSubmit = document.getElementById('uploadSubmit')
|
||||
const clubUuidInput = document.getElementById('clubUuid')
|
||||
|
||||
let selectedFile = null
|
||||
|
||||
// Click to browse
|
||||
uploadArea.addEventListener('click', () => {
|
||||
fileInput.click()
|
||||
})
|
||||
|
||||
// Drag and drop handlers
|
||||
uploadArea.addEventListener('dragover', (e) => {
|
||||
e.preventDefault()
|
||||
uploadArea.classList.add('dragover')
|
||||
})
|
||||
|
||||
uploadArea.addEventListener('dragleave', () => {
|
||||
uploadArea.classList.remove('dragover')
|
||||
})
|
||||
|
||||
uploadArea.addEventListener('drop', (e) => {
|
||||
e.preventDefault()
|
||||
uploadArea.classList.remove('dragover')
|
||||
|
||||
const files = e.dataTransfer.files
|
||||
if (files.length > 0) {
|
||||
handleFileSelect(files[0])
|
||||
}
|
||||
})
|
||||
|
||||
// File input change
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
handleFileSelect(e.target.files[0])
|
||||
}
|
||||
})
|
||||
|
||||
function handleFileSelect(file) {
|
||||
// Validate file type
|
||||
if (!file.type.match('image/(svg\\+xml|png)')) {
|
||||
showNotification('Please select an SVG or PNG file', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
selectedFile = file
|
||||
|
||||
// Show preview
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
previewImage.src = e.target.result
|
||||
|
||||
// Animate preview
|
||||
gsap.to(previewArea, {
|
||||
duration: 0.5,
|
||||
opacity: 1,
|
||||
display: 'block',
|
||||
ease: 'power2.out'
|
||||
})
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
// Upload submit
|
||||
uploadSubmit.addEventListener('click', async () => {
|
||||
const uuid = clubUuidInput.value.trim()
|
||||
|
||||
if (!uuid) {
|
||||
showNotification('Please enter a club UUID', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
if (!selectedFile) {
|
||||
showNotification('Please select a logo file', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
// Validate UUID format
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
if (!uuidRegex.test(uuid)) {
|
||||
showNotification('Invalid UUID format', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
await uploadLogo(uuid, selectedFile)
|
||||
})
|
||||
|
||||
async function uploadLogo(uuid, file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
// Disable button and show loading
|
||||
uploadSubmit.disabled = true
|
||||
uploadSubmit.innerHTML = '<div class="spinner mx-auto"></div>'
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/logos/${uuid}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Upload failed')
|
||||
}
|
||||
|
||||
showNotification('Logo uploaded successfully! ✓', 'success')
|
||||
|
||||
// Reset form
|
||||
setTimeout(() => {
|
||||
clubUuidInput.value = ''
|
||||
fileInput.value = ''
|
||||
selectedFile = null
|
||||
previewArea.style.display = 'none'
|
||||
}, 1500)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error)
|
||||
showNotification('Upload failed. Make sure the backend is running.', 'error')
|
||||
} finally {
|
||||
uploadSubmit.disabled = false
|
||||
uploadSubmit.textContent = 'Upload Logo'
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Utility Functions ====================
|
||||
|
||||
function copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showNotification('UUID copied to clipboard!', 'success')
|
||||
}).catch(() => {
|
||||
showNotification('Failed to copy UUID', 'error')
|
||||
})
|
||||
}
|
||||
|
||||
function showNotification(message, type = 'info') {
|
||||
// Create notification element
|
||||
const notification = document.createElement('div')
|
||||
notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 ${
|
||||
type === 'success' ? 'bg-accent-green' :
|
||||
type === 'error' ? 'bg-red-500' :
|
||||
'bg-accent-blue'
|
||||
} text-white font-medium`
|
||||
notification.textContent = message
|
||||
|
||||
document.body.appendChild(notification)
|
||||
|
||||
// Animate in
|
||||
gsap.from(notification, {
|
||||
duration: 0.3,
|
||||
opacity: 0,
|
||||
y: -20,
|
||||
ease: 'power2.out'
|
||||
})
|
||||
|
||||
// Remove after 3 seconds
|
||||
setTimeout(() => {
|
||||
gsap.to(notification, {
|
||||
duration: 0.3,
|
||||
opacity: 0,
|
||||
y: -20,
|
||||
ease: 'power2.in',
|
||||
onComplete: () => notification.remove()
|
||||
})
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
// ==================== Initialize ====================
|
||||
|
||||
console.log('🇨🇿 Czech Clubs Logos API Frontend')
|
||||
console.log('Backend API:', API_BASE_URL)
|
||||
console.log('FAČR API:', FACR_API_URL)
|
||||
|
||||
// Show a welcome notification
|
||||
setTimeout(() => {
|
||||
showNotification('Welcome to Czech Clubs Logos API! 🇨🇿', 'info')
|
||||
}, 1000)
|
||||
@@ -0,0 +1,99 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for dark mode */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #0a0e1a;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #1f2937;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
/* Smooth transitions */
|
||||
.transition-smooth {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Card hover effects */
|
||||
.card-hover {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Gradient text */
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #10b981 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Upload area styling */
|
||||
.upload-area {
|
||||
border: 2px dashed #374151;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.upload-area.dragover {
|
||||
border-color: #3b82f6;
|
||||
background-color: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in-up {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
/* Loading spinner */
|
||||
.spinner {
|
||||
border: 3px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
border-top-color: #3b82f6;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'dark-bg': '#0a0e1a',
|
||||
'dark-card': '#131823',
|
||||
'dark-border': '#1f2937',
|
||||
'accent-blue': '#3b82f6',
|
||||
'accent-green': '#10b981',
|
||||
},
|
||||
fontFamily: {
|
||||
'sans': ['Inter', 'system-ui', 'sans-serif'],
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
root: './',
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: resolve(__dirname, 'index.html'),
|
||||
admin: resolve(__dirname, 'admin.html'),
|
||||
apiDocs: resolve(__dirname, 'api-docs.html'),
|
||||
logo: resolve(__dirname, 'logo.html')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user