# Kompletní Technická Dokumentace - Systém pro Správu Fotbalového Klubu
**Autor:** Tomáš Dvořák
**Verze:** 1.0
**Datum:** Říjen 2025
**Účel:** Maturitní projekt - Prezentace komplexního webového systému pro správu fotbalového klubu
---
## 📋 Obsah
1. [Úvod a Motivace](#1-úvod-a-motivace)
2. [Přehled Projektu](#2-přehled-projektu)
3. [Technologický Stack](#3-technologický-stack)
4. [Architektura Systému](#4-architektura-systému)
5. [Struktura Projektu](#5-struktura-projektu)
6. [Klíčové Funkce](#6-klíčové-funkce)
7. [API a Integrace](#7-api-a-integrace)
8. [Bezpečnost](#8-bezpečnost)
9. [Minimální Požadavky](#9-minimální-požadavky)
10. [Instalace a Konfigurace](#10-instalace-a-konfigurace)
11. [Příklady Kódu](#11-příklady-kódu)
12. [Závěr](#12-závěr)
---
## 1. Úvod a Motivace
### 1.1 Problematika
Moderní sportovní kluby, zejména fotbalové, čelí výzvě efektivní správy obsahu, komunikace s fanoušky a prezentace klubových aktivit. Mnoho klubů, zejména na amatérské úrovni, používá zastaralé nebo rozdrobené systémy, které neumožňují komplexní správu všech aspektů klubového života.
### 1.2 Cíl Projektu
Vytvořit **moderní, plně funkční webový systém** pro správu fotbalového klubu, který:
- **Centralizuje** všechny informace o klubu na jednom místě
- **Automatizuje** získávání výsledků a tabulek z oficiálních zdrojů (FAČR)
- **Usnadňuje** komunikaci s fanoušky prostřednictvím newsletteru a kontaktních formulářů
- **Poskytuje** intuitivní administrační rozhraní pro správu obsahu
- **Zajišťuje** bezpečnost a škálovatelnost pomocí moderních technologií
- **Respektuje** legislativu (GDPR, cookies consent)
### 1.3 Klíčové Požadavky
- **Responsivní design** - plná funkčnost na mobilních zařízeních
- **Real-time aktualizace** - živé skóre zápasů, okamžitá synchronizace dat
- **Multimédia** - galerie, videa z YouTube, klubové loga
- **Analytics** - sledování návštěvnosti a chování uživatelů
- **Personalizace** - přizpůsobení barev a vzhledu dle klubové identity
- **Modulární architektura** - snadná rozšiřitelnost o nové funkce
---
## 2. Přehled Projektu
### 2.1 Popis Systému
**Fotbal Club** je full-stack webová aplikace určená pro kompletní správu fotbalového klubu. Systém kombinuje veřejnou část pro fanoušky a návštěvníky s pokročilým administrátorským rozhraním pro správu obsahu.
### 2.2 Hlavní Komponenty
```
┌─────────────────────────────────────────────────────────────┐
│ FRONTEND (React SPA) │
│ • Veřejný web • Admin dashboard • Setup wizard │
└──────────────────────────┬──────────────────────────────────┘
│ REST API (JSON)
┌──────────────────────────┴──────────────────────────────────┐
│ BACKEND (Go + Gin) │
│ • API Controllers • Auth/JWT • Business Logic │
└──────────────────────────┬──────────────────────────────────┘
│ GORM
┌──────────────────────────┴──────────────────────────────────┐
│ DATABASE (PostgreSQL) │
│ • Články • Týmy • Zápasy • Uživatelé • Settings │
└─────────────────────────────────────────────────────────────┘
```
### 2.3 Funkční Oblasti
| Oblast | Funkce |
|--------|--------|
| **Obsah** | Blog/články, o klubu, galerie, videa |
| **Sport** | Zápasy, tabulky, hráči, týmy, výsledky |
| **Komunikace** | Kontaktní formulář, newsletter, notifikace |
| **Marketing** | Sponzoři, bannery, SEO optimalizace |
| **Správa** | Uživatelé, role, nastavení, analytika |
| **Integrace** | FAČR API, YouTube, Zonerama, Umami Analytics |
---
## 3. Technologický Stack
### 3.1 Backend Technologie
#### **Programovací Jazyk: Go (Golang) 1.23+**
**Odůvodnění volby:**
- **Vysoký výkon** - kompilovaný jazyk s nízkou latencí
- **Concurrency** - nativní podpora paralelního zpracování (goroutines)
- **Typ bezpečnost** - silně typovaný jazyk eliminuje mnoho chyb
- **Jednoduchost** - čistá syntax, snadná údržba
- **Ekosystém** - bohaté knihovny pro web development
#### **Web Framework: Gin**
```go
// Příklad Gin routeru
func main() {
r := gin.Default()
// Middleware
r.Use(securityHeaders())
r.Use(corsMiddleware())
// Routy
api := r.Group("/api/v1")
{
api.GET("/articles", controllers.GetArticles)
api.POST("/articles", middleware.JWTAuth(), controllers.CreateArticle)
}
r.Run(":8080")
}
```
**Vlastnosti:**
- Rychlost (40x rychlejší než Martini)
- HTTP/2 podpora
- Middleware systém
- JSON validace
- Error management
#### **ORM: GORM**
```go
// Definice modelu
type Article struct {
ID uint `gorm:"primarykey" json:"id"`
Title string `gorm:"size:255;not null" json:"title"`
Content string `gorm:"type:text" json:"content"`
Published bool `gorm:"default:false" json:"published"`
PublishedAt *time.Time `json:"published_at"`
CategoryID uint `json:"category_id"`
Category Category `gorm:"foreignKey:CategoryID" json:"category"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
```
**Výhody:**
- Auto-migrace databázových schémat
- Vztahy mezi tabulkami (has-one, has-many, many-to-many)
- Hooks (before/after create, update, delete)
- Transaction management
- Query builder s bezpečnými parametry
#### **Databáze: PostgreSQL 15**
**Klíčové vlastnosti:**
- ACID compliance
- Pokročilé indexování
- Full-text search
- JSON/JSONB podpora
- Replikace a high availability
#### **Další Backend Knihovny**
| Knihovna | Účel |
|----------|------|
| `golang-jwt/jwt` | JWT token generování a validace |
| `golang.org/x/crypto` | Bcrypt hashing hesel |
| `gopkg.in/mail.v2` | SMTP odesílání emailů |
| `joho/godotenv` | Správa environment variables |
| `PuerkitoBio/goquery` | HTML parsing (web scraping FAČR) |
| `vanng822/go-premailer` | Inline CSS pro HTML emaily |
### 3.2 Frontend Technologie
#### **Framework: React 18+**
**Architektura:**
- **Single Page Application (SPA)** - rychlé přechody bez reload
- **Funkcionální komponenty** - hooks-based přístup
- **TypeScript** - typová bezpečnost na frontendu
#### **UI Knihovna: Chakra UI**
```tsx
// Příklad Chakra UI komponenty
import { Box, Button, Heading, Stack } from '@chakra-ui/react';
function ArticleCard({ article }) {
return (
{article.title}
);
}
```
**Výhody:**
- Přístupnost (a11y) out-of-the-box
- Responzivní props systém
- Theming a customizace
- Dark mode podpora
- Komponenty již vyzkoušené v produkci
#### **State Management**
| Nástroj | Použití |
|---------|---------|
| **React Query** | Server state, cache, synchronizace |
| **React Context** | Globální state (auth, theme) |
| **Local State** | Lokální stav komponent |
```tsx
// React Query příklad
import { useQuery } from '@tanstack/react-query';
function ArticlesList() {
const { data, isLoading, error } = useQuery({
queryKey: ['articles'],
queryFn: () => api.get('/articles'),
staleTime: 5 * 60 * 1000, // 5 minut cache
});
if (isLoading) return ;
if (error) return {error.message};
return ;
}
```
#### **Routing: React Router v6**
- Deklarativní routing
- Protected routes s autorizací
- Lazy loading stránek
- Nested routes
### 3.3 DevOps a Nástroje
#### **Kontejnerizace: Docker**
```dockerfile
# Multi-stage build pro optimalizaci
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]
```
**Výhody:**
- Izolované prostředí
- Reprodukovatelné buildy
- Snadné nasazení
- Konzistentní development/production
#### **Orchestrace: Docker Compose**
- Současné spuštění backend + frontend + databáze
- Automatické volume mapování
- Health checks
- Network izolace
### 3.4 Externí Služby a API
| Služba | Účel | Integrace | Poznámka |
|--------|------|-----------|----------|
| **FAČR API*** | Oficiální výsledky a tabulky českého fotbalu | REST API scraping | *Custom wrapper vytvořen pro tento projekt |
| **YouTube Data API v3** | Klubová videa | iframe embed + data API | [Docs](https://developers.google.com/youtube/v3) |
| **Zonerama*** | Fotogalerie | HTML scraping | *Custom scraper vytvořen (Zonerama nemá veřejné API) |
| **Umami Analytics** | Web analytics (privacy-first) | JavaScript tracking SDK + REST API | Self-hosted, [Docs](https://umami.is/docs) |
| **OpenRouter AI** | AI generování článků | REST API (GPT modely) | [Docs](https://openrouter.ai/docs) |
| **Google Maps Embed** | Lokace klubu | Maps Embed API | [Docs](https://developers.google.com/maps/documentation/embed) |
**Poznámka:** API označená hvězdičkou (*) byly vytvořeny specificky pro tento projekt jako custom wrappery/scrapery, protože oficiální veřejné API není dostupné nebo je omezené.
---
## 4. Architektura Systému
### 4.1 Celková Architektura
Aplikace využívá **třívrstvou architekturu** s jasným oddělením zodpovědností:
```
┌─────────────────────────────────────────────────────────────────┐
│ PREZENTAČNÍ VRSTVA │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Veřejný │ │ Admin │ │ Setup │ │
│ │ Web │ │ Dashboard │ │ Wizard │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ React Components + Chakra UI + React Router │
└────────────────────────────┬────────────────────────────────────┘
│
REST API (JSON)
JWT Authentication
│
┌────────────────────────────┴────────────────────────────────────┐
│ APLIKAČNÍ VRSTVA │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Controllers │ │ Middleware │ │ Services │ │
│ │ (Handlers) │ │ Auth/CORS │ │ (Business) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ Gin Framework + GORM + Custom Logic │
└────────────────────────────┬────────────────────────────────────┘
│
ORM (GORM)
SQL Queries
│
┌────────────────────────────┴────────────────────────────────────┐
│ DATOVÁ VRSTVA │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ PostgreSQL │ │ Cache │ │ Uploads │ │
│ │ Database │ │ (JSON) │ │ (Files) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
### 4.2 Backend Architektura (Go)
#### **Vrstvová struktura:**
1. **Controllers (Ovladače)**
- Zpracování HTTP požadavků
- Validace vstupních dat
- Volání business logiky
- Formátování odpovědí
2. **Middleware**
- Autentizace (JWT)
- Autorizace (role-based)
- Rate limiting
- CORS
- Security headers
3. **Services (Služby)**
- Business logika
- Komplexní operace
- Integrace s externími API
- Background joby
4. **Models (Modely)**
- Datové struktury
- Databázové schéma
- Vztahy mezi tabulkami
#### **Příklad Request Flow:**
```
1. HTTP Request → Gin Router
2. Router → Middleware Stack
├─ CORS Middleware
├─ Security Headers
├─ JWT Authentication
└─ Rate Limiting
3. Middleware → Controller
4. Controller → Service (Business Logic)
5. Service → GORM (Database)
6. GORM → PostgreSQL
7. Response: PostgreSQL → GORM → Service → Controller → JSON
```
### 4.3 Frontend Architektura (React)
#### **Komponentová architektura:**
```
src/
├── components/ # Znovupoužitelné komponenty
│ ├── common/ # Obecné UI prvky (Button, Card, etc.)
│ ├── admin/ # Admin-specifické komponenty
│ ├── layout/ # Layout komponenty (Header, Footer)
│ └── widgets/ # Složitější widgety
├── pages/ # Stránky (route komponenty)
│ ├── admin/ # Admin stránky
│ └── legal/ # Právní stránky
├── contexts/ # React Context (global state)
├── hooks/ # Custom React hooks
├── services/ # API klienti
└── layouts/ # Layout wrappery
```
#### **State Management Pattern:**
```tsx
// 1. Server State (React Query)
const { data, isLoading } = useQuery({
queryKey: ['articles'],
queryFn: fetchArticles,
});
// 2. Global State (React Context)
const { user, isAuthenticated } = useAuth();
// 3. Local State (useState)
const [isOpen, setIsOpen] = useState(false);
```
### 4.4 Datový Model
#### **Základní Entity:**
```sql
-- Uživatelé a autentizace
users (id, email, password_hash, role, created_at)
password_resets (id, user_id, token, expires_at)
-- Obsah
articles (id, title, content, published, category_id, created_at)
categories (id, name, slug, color)
-- Sport
teams (id, name, age_category, division)
players (id, name, position, number, team_id)
matches (external_id, home_team, away_team, score, date)
match_overrides (external_match_id, home_name, away_name, home_logo, away_logo)
-- Galerie
gallery_albums (id, title, cover_url, photo_count, zonerama_id)
-- Komunikace
contact_messages (id, name, email, subject, message, status, read_at)
newsletter_subscriptions (id, email, status, preferences, subscribed_at)
-- Konfigurace
settings (id, club_name, club_colors, smtp_config, newsletter_enabled)
competition_aliases (code, display_name, priority, hidden)
-- Analytics
visitor_events (id, event_type, page_url, user_agent, ip_hash, created_at)
```
#### **Vztahy:**
```
Article N:1 Category
Article N:1 User (author)
Player N:1 Team
Match N:1 Competition
NewsletterSubscription 1:N EmailLog
ContactMessage 1:1 ContactCategory
```
### 4.5 API Design Pattern
Aplikace využívá **RESTful API** s konzistentním názvoslovím:
| Metoda | Endpoint | Popis | Auth |
|--------|----------|-------|------|
| GET | `/api/v1/articles` | Seznam článků | Ne |
| GET | `/api/v1/articles/:id` | Detail článku | Ne |
| POST | `/api/v1/articles` | Vytvoření článku | Ano (JWT) |
| PUT | `/api/v1/articles/:id` | Aktualizace článku | Ano (JWT) |
| DELETE | `/api/v1/articles/:id` | Smazání článku | Ano (JWT + Admin) |
**Response formát:**
```json
// Úspěch
{
"success": true,
"data": {
"id": 1,
"title": "Název článku"
},
"message": "Article created successfully"
}
// Chyba
{
"success": false,
"error": "Validation failed",
"details": {
"title": "Title is required"
}
}
```
---
## 5. Struktura Projektu
### 5.1 Backend Struktura
```
fotbal-club/
├── main.go # Vstupní bod aplikace
├── go.mod # Go dependencies
├── go.sum # Dependency checksums
├── .env # Environment variables
├── Dockerfile # Production build
├── Dockerfile.dev # Development build
├── docker-compose.yml # Multi-container orchestrace
├── Makefile # Build & run skripty
│
├── internal/ # Privátní aplikační kód
│ ├── config/ # Konfigurace
│ │ └── config.go # Načítání ENV variables
│ │
│ ├── controllers/ # HTTP handlery
│ │ ├── base_controller.go # CRUD operace
│ │ ├── auth_controller.go # Autentizace
│ │ ├── contact_controller.go # Kontaktní formulář
│ │ ├── facr_controller.go # FAČR integrace
│ │ ├── gallery_controller.go # Galerie
│ │ ├── ai_controller.go # AI generování
│ │ ├── analytics_controller.go # Analytika
│ │ └── ...
│ │
│ ├── middleware/ # Middleware funkce
│ │ ├── auth.go # JWT ověření
│ │ ├── admin.go # Role kontrola
│ │ └── ratelimit.go # Rate limiting
│ │
│ ├── models/ # Databázové modely
│ │ ├── models.go # Společné modely (User, Article, etc.)
│ │ ├── contact.go # Kontaktní modely
│ │ ├── scoreboard.go # Scoreboard state
│ │ └── ...
│ │
│ ├── routes/ # Route definice
│ │ ├── routes.go # Hlavní router
│ │ ├── analytics_routes.go
│ │ └── ...
│ │
│ └── services/ # Business logika
│ ├── prefetch.go # Background cache refresh
│ ├── newsletter_automation.go # Auto newslettery
│ └── gallery_service.go
│
├── pkg/ # Znovupoužitelné balíčky
│ ├── database/ # DB utilities
│ │ ├── db.go # Inicializace
│ │ ├── migrate.go # Migrace
│ │ └── seed.go # Seed data
│ ├── email/ # Email service
│ │ └── email.go
│ ├── logger/ # Logging
│ └── utils/ # Helper funkce
│
├── database/ # SQL migrace
│ └── migrations/
│
├── templates/ # HTML šablony pro emaily
│ └── emails/
│ ├── newsletter.html
│ ├── contact_confirm.html
│ └── password_reset.html
│
├── static/ # Statické soubory
│ └── dist/
│
├── uploads/ # Nahrané soubory
│ ├── articles/
│ ├── sponsors/
│ └── players/
│
└── cache/ # JSON cache soubory
├── articles.json
├── matches.json
└── standings.json
```
### 5.2 Frontend Struktura
```
frontend/
├── public/ # Statické public soubory
│ ├── index.html
│ ├── favicon.ico
│ └── manifest.json
│
├── src/
│ ├── index.tsx # Vstupní bod React app
│ ├── App.tsx # Hlavní komponenta s routingem
│ ├── config.ts # Frontend konfigurace
│ │
│ ├── components/ # React komponenty
│ │ ├── common/ # Sdílené komponenty
│ │ │ ├── Button.tsx
│ │ │ ├── Card.tsx
│ │ │ └── Modal.tsx
│ │ ├── admin/ # Admin komponenty
│ │ │ ├── ArticleForm.tsx
│ │ │ ├── UserManager.tsx
│ │ │ └── ...
│ │ ├── layout/ # Layout komponenty
│ │ │ ├── Navbar.tsx
│ │ │ ├── Footer.tsx
│ │ │ └── Sidebar.tsx
│ │ ├── seo/ # SEO komponenty
│ │ │ └── DefaultSEO.tsx
│ │ └── widgets/ # Specializované widgety
│ │ ├── MatchCard.tsx
│ │ ├── ArticleGrid.tsx
│ │ └── ...
│ │
│ ├── pages/ # Stránkové komponenty
│ │ ├── HomePage.tsx
│ │ ├── BlogPage.tsx
│ │ ├── ContactPage.tsx
│ │ ├── admin/
│ │ │ ├── AdminDashboardPage.tsx
│ │ │ ├── ArticlesAdminPage.tsx
│ │ │ └── ...
│ │ └── legal/
│ │ ├── PrivacyPolicyPage.tsx
│ │ └── TermsPage.tsx
│ │
│ ├── contexts/ # React Context providers
│ │ ├── AuthContext.tsx # Autentizace state
│ │ └── ClubThemeContext.tsx # Klubové barvy
│ │
│ ├── hooks/ # Custom React hooks
│ │ ├── useAuth.ts
│ │ ├── useFacrApi.ts
│ │ ├── useSettings.ts
│ │ └── useUmami.ts # Analytics tracking
│ │
│ ├── services/ # API klienti
│ │ ├── api.ts # Axios instance
│ │ ├── auth.service.ts
│ │ ├── articles.service.ts
│ │ └── ...
│ │
│ ├── layouts/ # Layout wrappery
│ │ ├── AdminLayout.tsx
│ │ └── PublicLayout.tsx
│ │
│ ├── assets/ # Obrázky, fonty, styly
│ │ ├── images/
│ │ ├── fonts/
│ │ └── styles/
│ │
│ └── types/ # TypeScript typy
│ └── index.d.ts
│
├── package.json # NPM dependencies
├── tsconfig.json # TypeScript konfigurace
└── .env # Environment variables
```
### 5.3 Klíčové Konfigurační Soubory
#### **Backend: `.env`**
```bash
# Application
APP_ENV=development
PORT=8080
DEBUG=true
# Database
DATABASE_URL=postgres://user:pass@localhost:5432/fotbal_club
# JWT
JWT_SECRET=your_secret_key_here
JWT_EXPIRATION_HOURS=24
# Email (SMTP)
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=your_email@example.com
SMTP_PASSWORD=password
# File Uploads
UPLOAD_DIR=./uploads
MAX_UPLOAD_SIZE=10485760 # 10MB
# External APIs
OPENROUTER_API_KEY=sk-or-v1-...
UMAMI_URL=https://analytics.example.com
```
#### **Frontend: `.env`**
```bash
REACT_APP_API_URL=http://localhost:8080/api/v1
REACT_APP_FACR_API_BASE_URL=http://localhost:8080/api/v1/facr
REACT_APP_HOMEPAGE_LAYOUT=classic
```
---
## 6. Klíčové Funkce
### 6.1 Systém Článků a Blogu
**Popis:** Komplexní systém pro správu klubového obsahu s podporou kategorií, náhledů, publikování a SEO.
#### **Hlavní Vlastnosti:**
- ✅ Rich text editor s podporou obrázků a YouTube videí
- ✅ Kategorizace článků (zápasy, události, novinky)
- ✅ Plánované publikování (draft → published)
- ✅ Featured články na homepage
- ✅ Automatické generování slugů pro SEO-friendly URL
- ✅ Sledování počtu přečtení
- ✅ Propojení článku s konkrétním zápasem
#### **Technická Implementace:**
```go
// Backend Model
type Article struct {
ID uint `gorm:"primarykey" json:"id"`
Title string `gorm:"size:255;not null" json:"title"`
Slug string `gorm:"size:255;uniqueIndex" json:"slug"`
Excerpt string `gorm:"type:text" json:"excerpt"`
Content string `gorm:"type:text" json:"content"`
FeaturedImage string `json:"featured_image"`
Published bool `gorm:"default:false" json:"published"`
PublishedAt *time.Time `json:"published_at"`
ViewCount int `gorm:"default:0" json:"view_count"`
CategoryID uint `json:"category_id"`
Category Category `gorm:"foreignKey:CategoryID" json:"category"`
AuthorID uint `json:"author_id"`
Author User `gorm:"foreignKey:AuthorID" json:"author"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
```
**Frontend Flow:**
1. Admin vytvoří článek přes formulář s WYSIWYG editorem
2. Nahraje featured obrázek přes drag-and-drop
3. Vybere kategorii a nastaví publikování
4. Článek se zobrazí na blogu a homepage (pokud je featured)
### 6.2 FAČR Integrace - Automatické Výsledky
**Popis:** Systém automaticky stahuje a zobrazuje výsledky, rozpisy a tabulky z oficiálního webu Fotbalové asociace České republiky.
#### **Jak to funguje:**
```
┌─────────────┐
│ Frontend │
│ Request │ ───────┐
└─────────────┘ │
▼
┌─────────────────┐
│ Backend Proxy │
│ (FACR Ctrl) │
└─────────────────┘
│
▼
┌─────────────────┐
│ Web Scraping │
│ (goquery HTML) │
└─────────────────┘
│
▼
┌─────────────────┐
│ facr.fotbal.cz │
│ (Official API) │
└─────────────────┘
```
#### **Klíčové Komponenty:**
**1. Club Search**
```go
// Vyhledání klubu podle názvu
GET /api/v1/facr/club/search?q=Sokol
Response: [
{ "id": "123", "name": "TJ Sokol Praha", "type": "klub" }
]
```
**2. Match Schedule**
```go
// Získání rozpisů zápasů
GET /api/v1/facr/club/klub/123
Response: {
"matches": [
{
"date": "2025-10-15T15:00:00Z",
"home_team": "TJ Sokol Praha",
"away_team": "FC Sparta B",
"competition": "III. třída - sk. A"
}
]
}
```
**3. Competition Tables**
```go
// Tabulky soutěží
GET /api/v1/facr/club/klub/123/table
Response: {
"tables": [
{
"competition": "III. třída - sk. A",
"rows": [
{
"position": 1,
"team": "TJ Sokol Praha",
"played": 10,
"wins": 8,
"points": 24
}
]
}
]
}
```
#### **Cache Systém:**
- **Interval:** Každých 30 minut automatický refresh
- **Storage:** JSON soubory v `./cache/` adresáři
- **Důvod:** Minimalizace zátěže na FAČR server + rychlé načítání
```go
// Prefetch service
func StartPrefetcher(targetURL string) {
ticker := time.NewTicker(30 * time.Minute)
go func() {
for range ticker.C {
fetchAndCacheEndpoint(targetURL + "/matches")
fetchAndCacheEndpoint(targetURL + "/standings")
}
}()
}
```
#### **Aliasy Soutěží:**
Administrátor může upravit názvy soutěží pro lepší zobrazení:
| Originální název | Alias | Priorita |
|------------------|-------|----------|
| III. třída - skupina A | Třetí třída A | 1 |
| Přebor dorostu U19 | Dorost U19 | 2 |
### 6.3 Newsletter a Email Marketing
**Popis:** Plnohodnotný newsletter systém s automatickými digesty, segmentací odběratelů a sledováním metrik.
#### **Typy Newsletterů:**
1. **Týdenní Digest**
- Automaticky každý pátek v 18:00
- Obsahuje nové články z týdne
- Nadcházející zápasy
2. **Match Alerts**
- 24h před zápasem
- Info o soupeři, místě konání
- Odkaz na mapy
3. **Blog Notifications**
- Při publikování nového článku
- Pouze pro odběratele "blog" kategorie
4. **Results Digest**
- Po víkendu s výsledky
- Aktuální tabulka
#### **Technická Implementace:**
```go
// Newsletter Automation Service
type NewsletterAutomation struct {
db *gorm.DB
emailService *email.EmailService
ticker *time.Ticker
}
func (na *NewsletterAutomation) Start() {
// Denní kontrola v 18:00
na.ticker = time.NewTicker(1 * time.Hour)
go func() {
for range na.ticker.C {
na.checkAndSendScheduled()
}
}()
}
func (na *NewsletterAutomation) checkAndSendScheduled() {
now := time.Now()
// Týdenní digest (pátek 18:00)
if now.Weekday() == time.Friday && now.Hour() == 18 {
na.sendWeeklyDigest()
}
// Match alerts (24h před zápasem)
upcomingMatches := na.getMatchesIn24Hours()
for _, match := range upcomingMatches {
na.sendMatchAlert(match)
}
}
```
#### **Email Templates:**
```html
{{range .Articles}}
{{.Title}}
{{.Excerpt}}
Číst více
{{end}}
```
#### **Email Tracking:**
- **Open Rate:** 1x1 tracking pixel
- **Click Through Rate:** Wrapped links
- **Unsubscribe:** Token-based unsubscribe link
- **Spam Reports:** Report spam endpoint
```go
// Email metrics
type EmailLog struct {
ID uint `json:"id"`
RecipientEmail string `json:"recipient_email"`
Subject string `json:"subject"`
Type string `json:"type"` // weekly, match, blog
SentAt time.Time `json:"sent_at"`
OpenedAt *time.Time `json:"opened_at"`
ClickedAt *time.Time `json:"clicked_at"`
UnsubscribedAt *time.Time `json:"unsubscribed_at"`
}
```
### 6.4 Galerie a Multimédia
#### **Zonerama Integrace:**
Systém automaticky synchronizuje alba z populární fotogalerie Zonerama.cz.
**Workflow:**
1. Admin zadá Zonerama username
2. Systém načte veřejná alba přes API
3. Admin vybere alba ke zveřejnění
4. Fotky se zobrazí v galerii na webu
```typescript
// Frontend API call
const fetchAlbum = async (albumUrl: string) => {
const response = await api.post('/admin/gallery/albums/fetch', {
album_url: albumUrl
});
return response.data;
};
// Backend scraping
func (gc *GalleryController) FetchAlbum(c *gin.Context) {
albumURL := c.PostForm("album_url")
// Parse Zonerama album
album := scrapeZoneramaAlbum(albumURL)
// Save to database
gc.DB.Create(&album)
c.JSON(200, album)
}
```
#### **YouTube Video Manager:**
```typescript
// Cached YouTube videos from club channel
interface YouTubeVideo {
id: string;
title: string;
thumbnail: string;
publishedAt: string;
duration: string;
viewCount: number;
}
// Backend cache refresh (každých 6 hodin)
func refreshYouTubeCache() {
videos := fetchFromYouTubeAPI(channelID)
saveToCache("youtube_videos.json", videos)
}
```
### 6.5 Kontaktní Systém
#### **Multi-Category Contact Form:**
Uživatelé mohou vybrat kategorii dotazu pro efektivní routing:
| Kategorie | Email | Auto-odpověď |
|-----------|-------|--------------|
| Obecný dotaz | info@club.cz | Ano |
| Technická podpora | tech@club.cz | Ano |
| Tisk a media | pr@club.cz | Ne |
| Reklama | marketing@club.cz | Ne |
```typescript
// Frontend form
// Backend handler
func (cc *ContactController) SubmitContactForm(c *gin.Context) {
var req ContactRequest
c.BindJSON(&req)
// Save to database
message := models.ContactMessage{
Name: req.Name,
Email: req.Email,
Category: req.Category,
Message: req.Message,
Status: "new",
}
cc.DB.Create(&message)
// Send notification to admin
cc.emailService.SendContactNotification(message)
// Send auto-reply to user
if shouldSendAutoReply(req.Category) {
cc.emailService.SendAutoReply(req.Email)
}
}
```
### 6.6 Analytics a Statistiky
#### **Umami Analytics Integrace:**
Privacy-first analytický nástroj (alternativa k Google Analytics).
**Sledované metriky:**
- Page views
- Unique visitors
- Bounce rate
- Average time on page
- Traffic sources
- Device breakdown
```typescript
// Frontend tracking hook
export const useUmami = () => {
const trackEvent = (eventName: string, eventData?: object) => {
if (window.umami) {
window.umami.track(eventName, eventData);
}
};
return { trackEvent };
};
// Usage
const { trackEvent } = useUmami();
trackEvent('article_read', { article_id: 123 });
```
#### **Custom Analytics:**
Kromě Umami systém uchovává vlastní statistiky:
```go
type VisitorEvent struct {
ID uint `json:"id"`
EventType string `json:"event_type"` // page_view, article_read, etc.
PageURL string `json:"page_url"`
UserAgent string `json:"user_agent"`
IPHash string `json:"ip_hash"` // Anonymizovaná IP (GDPR)
CreatedAt time.Time `json:"created_at"`
}
// Article popularity
type ArticleStats struct {
ArticleID uint `json:"article_id"`
ViewCount int `json:"view_count"`
ReadTime int `json:"avg_read_time_seconds"`
}
```
### 6.7 Live Scoreboard
**Popis:** Real-time skóre display pro živé přenosy zápasů.
#### **Funkce:**
- ⏱️ **Countdown timer** s přesností na sekundy
- 🔄 **Swap sides** - prohození stran
- ⚽ **Live score updates** - aktualizace skóre
- 📊 **Statistics** - střely, rohy, žluté karty
- 🎨 **Color derivation** - automatické barvy z klubových log
- 💾 **Presets** - uložení často používaných nastavení
```typescript
// Frontend scoreboard control
const ScoreboardControl = () => {
const [scoreData, setScoreData] = useState({
homeTeam: '',
awayTeam: '',
homeScore: 0,
awayScore: 0,
timer: 0,
isRunning: false,
});
const updateScore = async (team: 'home' | 'away', increment: number) => {
await api.put('/admin/scoreboard', {
[`${team}_score`]: scoreData[`${team}Score`] + increment
});
};
return (
);
};
```
### 6.8 AI Content Generation
**Popis:** Automatické generování článků a obsahu pomocí AI (OpenRouter API).
#### **Podporované Modely:**
- Mistral Small (primary)
- Mistral Nemo (fallback)
- GPT-4 Turbo (premium)
```go
// AI Blog Generation
func (ai *AIController) GenerateBlog(c *gin.Context) {
var req struct {
Topic string `json:"topic"`
Keywords []string `json:"keywords"`
Tone string `json:"tone"` // professional, casual, excited
Length int `json:"length"` // word count
}
c.BindJSON(&req)
prompt := buildBlogPrompt(req)
// Call OpenRouter API
article := ai.openRouterClient.Generate(prompt, req.Length)
c.JSON(200, gin.H{
"title": article.Title,
"content": article.Content,
"excerpt": article.Excerpt,
})
}
```
**Příklad použití:**
1. Admin klikne "Generovat článek AI"
2. Zadá téma: "Vítězství v derby 3:1"
3. AI vygeneruje kompletní článek s titulkem a obsahem
4. Admin upraví detaily a publikuje
---
## 7. API a Integrace
### 7.1 REST API Přehled
Aplikace poskytuje kompletní RESTful API pro všechny operace.
#### **Base URL:**
```
http://localhost:8080/api/v1
```
#### **Autentizace:**
```http
Authorization: Bearer
```
#### **Content Type:**
```http
Content-Type: application/json
```
### 7.2 API Endpointy - Kategorie
#### **🔐 Autentizace**
| Metoda | Endpoint | Popis | Auth |
|--------|----------|-------|------|
| POST | `/auth/login` | Přihlášení uživatele | ❌ |
| POST | `/auth/register` | Registrace nového uživatele | ❌ |
| POST | `/auth/logout` | Odhlášení | ❌ |
| GET | `/auth/me` | Získání aktuálního uživatele | ✅ |
| POST | `/auth/forgot-password` | Zapomenuté heslo | ❌ |
| POST | `/auth/reset-password` | Reset hesla | ❌ |
**Příklad Login Request:**
```bash
curl -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@club.cz",
"password": "securepassword"
}'
```
**Response:**
```json
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": 1,
"email": "admin@club.cz",
"role": "admin",
"created_at": "2025-01-01T10:00:00Z"
}
}
```
#### **📝 Články**
| Metoda | Endpoint | Popis | Auth |
|--------|----------|-------|------|
| GET | `/articles` | Seznam článků | ❌ |
| GET | `/articles/:id` | Detail článku | ❌ |
| GET | `/articles/slug/:slug` | Článek podle slug | ❌ |
| GET | `/articles/featured` | Featured články | ❌ |
| POST | `/articles` | Vytvoření článku | ✅ |
| PUT | `/articles/:id` | Aktualizace článku | ✅ |
| DELETE | `/articles/:id` | Smazání článku | ✅ |
| POST | `/articles/:id/read` | Increment počtu čtení | ❌ |
**Query Parameters pro GET /articles:**
```
?page=1
&limit=10
&category=1
&published=true
&sort=created_at
&order=desc
&search=derby
```
#### **⚽ Zápasy a Tabulky**
| Metoda | Endpoint | Popis | Auth |
|--------|----------|-------|------|
| GET | `/matches` | Seznam zápasů | ❌ |
| GET | `/matches/history` | Historie zápasů | ❌ |
| GET | `/standings` | Tabulky soutěží | ❌ |
| GET | `/admin/matches` | Admin view zápasů | ✅ Admin |
#### **🏆 FAČR API**
| Metoda | Endpoint | Popis | Auth |
|--------|----------|-------|------|
| GET | `/facr/club/search?q={query}` | Vyhledání klubu | ❌ |
| GET | `/facr/club/:type/:id` | Info o klubu | ❌ |
| GET | `/facr/club/:type/:id/table` | Tabulky klubu | ❌ |
**Příklad:**
```bash
# Vyhledání klubu
curl "http://localhost:8080/api/v1/facr/club/search?q=Sparta"
# Získání informací
curl "http://localhost:8080/api/v1/facr/club/klub/123"
```
#### **📧 Kontakt a Newsletter**
| Metoda | Endpoint | Popis | Auth |
|--------|----------|-------|------|
| POST | `/contact` | Odeslat kontaktní formulář | ❌ |
| POST | `/newsletter/subscribe` | Odběr newsletteru | ❌ |
| POST | `/newsletter/unsubscribe/:email` | Zrušení odběru | ❌ |
| GET | `/admin/newsletter/subscribers` | Seznam odběratelů | ✅ Admin |
| POST | `/admin/newsletter/send` | Odeslat newsletter | ✅ Admin |
#### **📸 Galerie**
| Metoda | Endpoint | Popis | Auth |
|--------|----------|-------|------|
| GET | `/gallery/albums` | Seznam alb | ❌ |
| GET | `/gallery/albums/:id` | Detail alba s fotkami | ❌ |
| POST | `/admin/gallery/albums/fetch` | Načíst album ze Zonerama | ✅ Admin |
| DELETE | `/admin/gallery/albums/:id` | Smazat album | ✅ Admin |
#### **📊 Analytics**
| Metoda | Endpoint | Popis | Auth |
|--------|----------|-------|------|
| POST | `/analytics/track` | Trackování eventu | ❌ |
| GET | `/admin/umami/stats` | Umami statistiky | ✅ Admin |
| GET | `/admin/umami/metrics/:type` | Metriky (pageviews, etc.) | ✅ Admin |
#### **⚙️ Nastavení**
| Metoda | Endpoint | Popis | Auth |
|--------|----------|-------|------|
| GET | `/settings` | Veřejná nastavení | ❌ |
| GET | `/admin/settings` | Všechna nastavení | ✅ Admin |
| PUT | `/admin/settings` | Aktualizace nastavení | ✅ Admin |
### 7.3 Externí API Integrace
#### **FAČR Web Scraping**
**Popis:** Backend funguje jako proxy a scraper pro oficiální FAČR web.
**Technologie:**
- `goquery` - HTML parsing
- HTTP client s custom headers
- Error handling a retry logika
```go
func (fc *FACRController) SearchClubs(c *gin.Context) {
query := c.Query("q")
// Build search URL
searchURL := fmt.Sprintf(
"https://is.fotbal.cz/clubs/search?term=%s",
url.QueryEscape(query),
)
// Fetch HTML
resp, err := http.Get(searchURL)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to fetch"})
return
}
defer resp.Body.Close()
// Parse HTML with goquery
doc, _ := goquery.NewDocumentFromReader(resp.Body)
var clubs []Club
doc.Find(".club-item").Each(func(i int, s *goquery.Selection) {
club := Club{
ID: s.AttrOr("data-id", ""),
Name: s.Find(".club-name").Text(),
Type: s.AttrOr("data-type", ""),
}
clubs = append(clubs, club)
})
c.JSON(200, clubs)
}
```
**Rate Limiting:**
- Max 60 requests/hour na FAČR
- Local cache pro 30 minut
- Background prefetch pro populární endpointy
#### **Zonerama Gallery API**
**Dokumentace:** https://www.zonerama.com/
**Workflow:**
1. Admin zadá Zonerama album URL
2. Backend parsuje HTML (Zonerama nemá veřejné REST API)
3. Extrahuje metadata: title, cover, photo count
4. Ukládá do DB s referencí na Zonerama ID
5. Frontend zobrazuje pomocí Zonerama CDN URLs
```go
func scrapeZoneramaAlbum(albumURL string) (*Album, error) {
resp, err := http.Get(albumURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
doc, _ := goquery.NewDocumentFromReader(resp.Body)
album := &Album{
Title: doc.Find("h1.album-title").Text(),
CoverURL: doc.Find(".album-cover img").AttrOr("src", ""),
PhotoCount: len(doc.Find(".photo-item").Nodes),
ZoneramaID: extractIDFromURL(albumURL),
}
return album, nil
}
```
#### **YouTube Data API v3**
**API Key:** Vyžadován v `.env` jako `YOUTUBE_API_KEY`
**Endpoint:**
```
https://www.googleapis.com/youtube/v3/search
```
**Použití:**
- Automatické načítání videí z klubového kanálu
- Cache refresh každých 6 hodin
- Zobrazení v sekci "Videa"
```go
func fetchYouTubeVideos(channelID string) ([]Video, error) {
apiKey := os.Getenv("YOUTUBE_API_KEY")
url := fmt.Sprintf(
"https://www.googleapis.com/youtube/v3/search?part=snippet&channelId=%s&maxResults=12&order=date&type=video&key=%s",
channelID, apiKey,
)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result YouTubeAPIResponse
json.NewDecoder(resp.Body).Decode(&result)
var videos []Video
for _, item := range result.Items {
videos = append(videos, Video{
ID: item.ID.VideoID,
Title: item.Snippet.Title,
Thumbnail: item.Snippet.Thumbnails.Medium.URL,
Published: item.Snippet.PublishedAt,
})
}
return videos, nil
}
```
#### **Umami Analytics**
**Self-hosted:** https://umami.is/
**Integrace:**
1. **Frontend tracking:**
```typescript
// useUmami hook
export const useUmami = () => {
useEffect(() => {
const script = document.createElement('script');
script.src = process.env.REACT_APP_UMAMI_URL + '/script.js';
script.async = true;
script.setAttribute('data-website-id', websiteId);
document.head.appendChild(script);
}, []);
};
```
2. **Backend API calls:**
```go
// Získání statistik
func (uc *UmamiController) GetStats(c *gin.Context) {
umamiURL := os.Getenv("UMAMI_URL")
username := os.Getenv("UMAMI_USERNAME")
password := os.Getenv("UMAMI_PASSWORD")
// Login to Umami
token := loginToUmami(umamiURL, username, password)
// Fetch stats
statsURL := fmt.Sprintf("%s/api/websites/%s/stats", umamiURL, websiteID)
req, _ := http.NewRequest("GET", statsURL, nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var stats Stats
json.NewDecoder(resp.Body).Decode(&stats)
c.JSON(200, stats)
}
```
#### **OpenRouter AI API**
**Dokumentace:** https://openrouter.ai/docs
**Modely:**
- `mistralai/mistral-small-3.2-24b-instruct:free` (primary)
- `mistralai/mistral-nemo:free` (fallback)
- Custom GPT modely (placené)
**Použití:**
```go
type OpenRouterClient struct {
apiKey string
baseURL string
}
func (orc *OpenRouterClient) Generate(prompt string, maxTokens int) (*GeneratedContent, error) {
reqBody := map[string]interface{}{
"model": "mistralai/mistral-small-3.2-24b-instruct:free",
"messages": []map[string]string{
{"role": "system", "content": "You are a professional sports journalist."},
{"role": "user", "content": prompt},
},
"max_tokens": maxTokens,
"temperature": 0.7,
}
jsonData, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", orc.baseURL+"/chat/completions", bytes.NewBuffer(jsonData))
req.Header.Set("Authorization", "Bearer "+orc.apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result OpenRouterResponse
json.NewDecoder(resp.Body).Decode(&result)
return &GeneratedContent{
Content: result.Choices[0].Message.Content,
}, nil
}
```
**Rate Limits:**
- Free tier: 200 requests/day
- Token limit: 100k tokens/day
- Fallback na jiný model při překročení
### 7.4 WebSocket Support (Budoucí)
**Plánované funkce:**
- Real-time skóre updates
- Live chat během zápasů
- Admin notifications
```go
// Připraveno pro rozšíření
func setupWebSocket(r *gin.Engine) {
r.GET("/ws", func(c *gin.Context) {
upgrader.Upgrade(c.Writer, c.Request, nil)
})
}
```
---
## 8. Bezpečnost
### 8.1 Autentizace a Autorizace
#### **JWT (JSON Web Tokens)**
Aplikace používá JWT tokeny pro stateless autentizaci.
**Token Structure:**
```
Header.Payload.Signature
```
**Payload obsahuje:**
```json
{
"user_id": 1,
"email": "admin@club.cz",
"role": "admin",
"exp": 1735689600, // Expiration timestamp
"iat": 1735603200 // Issued at timestamp
}
```
**Implementace:**
```go
// Token generování při přihlášení
func generateJWT(user *models.User) (string, error) {
claims := jwt.MapClaims{
"user_id": user.ID,
"email": user.Email,
"role": user.Role,
"exp": time.Now().Add(24 * time.Hour).Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
secret := []byte(os.Getenv("JWT_SECRET"))
return token.SignedString(secret)
}
// Middleware pro ověření tokenu
func JWTAuth(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(401, gin.H{"error": "Authorization header missing"})
c.Abort()
return
}
// Extract token from "Bearer "
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
// Parse and validate token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte(os.Getenv("JWT_SECRET")), nil
})
if err != nil || !token.Valid {
c.JSON(401, gin.H{"error": "Invalid token"})
c.Abort()
return
}
claims := token.Claims.(jwt.MapClaims)
userID := uint(claims["user_id"].(float64))
// Load user from database
var user models.User
if err := db.First(&user, userID).Error; err != nil {
c.JSON(401, gin.H{"error": "User not found"})
c.Abort()
return
}
// Store user in context
c.Set("user", user)
c.Set("userID", user.ID)
c.Set("userRole", user.Role)
c.Next()
}
}
```
#### **Role-Based Access Control (RBAC)**
Systém podporuje dvě role:
- **admin** - plný přístup ke všem funkcím
- **editor** - omezený přístup (bez správy uživatelů a nastavení)
```go
// Middleware pro kontrolu role
func RoleAuth(requiredRole string) gin.HandlerFunc {
return func(c *gin.Context) {
userRole, exists := c.Get("userRole")
if !exists {
c.JSON(403, gin.H{"error": "Forbidden"})
c.Abort()
return
}
if userRole.(string) != requiredRole {
c.JSON(403, gin.H{"error": "Insufficient permissions"})
c.Abort()
return
}
c.Next()
}
}
// Použití
admin := api.Group("/admin")
admin.Use(middleware.JWTAuth(db))
admin.Use(middleware.RoleAuth("admin"))
```
#### **Password Hashing**
Hesla jsou hashována pomocí bcrypt s cost faktorem 14.
```go
import "golang.org/x/crypto/bcrypt"
// Hashování hesla při registraci
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
// Ověření hesla při přihlášení
func verifyPassword(hashedPassword, password string) error {
return bcrypt.CompareHashAndPassword(
[]byte(hashedPassword),
[]byte(password),
)
}
```
**Výhody bcrypt:**
- Adaptive algorithm (cost lze zvýšit v budoucnu)
- Built-in salt
- Resistance proti rainbow table útokům
- Slow by design (brání brute-force)
### 8.2 Security Headers
Aplikace automaticky přidává bezpečnostní hlavičky ke každému HTTP response.
```go
// Security headers middleware
r.Use(func(c *gin.Context) {
// Prevent MIME type sniffing
c.Writer.Header().Set("X-Content-Type-Options", "nosniff")
// Prevent clickjacking
c.Writer.Header().Set("X-Frame-Options", "DENY")
// Referrer policy
c.Writer.Header().Set("Referrer-Policy", "no-referrer-when-downgrade")
// HSTS (when using HTTPS)
if c.Request.TLS != nil || c.Request.Header.Get("X-Forwarded-Proto") == "https" {
c.Writer.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
}
// Content Security Policy
csp := "default-src 'self'; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://umami.tdvorak.dev; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https: blob:; " +
"font-src 'self' data:; " +
"connect-src 'self' https://umami.tdvorak.dev;"
c.Writer.Header().Set("Content-Security-Policy", csp)
c.Next()
})
```
**Vysvětlení jednotlivých headers:**
| Header | Účel | Hodnota |
|--------|------|---------|
| `X-Content-Type-Options` | Zabraňuje MIME sniffing | `nosniff` |
| `X-Frame-Options` | Ochrana proti clickjacking | `DENY` |
| `Referrer-Policy` | Kontrola referrer informací | `no-referrer-when-downgrade` |
| `Strict-Transport-Security` | Vynucení HTTPS | `max-age=31536000` |
| `Content-Security-Policy` | Whitelist zdrojů (XSS ochrana) | Custom policy |
### 8.3 CORS (Cross-Origin Resource Sharing)
CORS je nakonfigurován pro povolené origins z `.env` souboru.
```go
// CORS middleware
origin := c.Request.Header.Get("Origin")
allowed := false
for _, ao := range config.AppConfig.AllowedOrigins {
if ao == origin {
allowed = true
break
}
}
// Relaxed rule for development
if !allowed && origin != "" && config.AppConfig.AppEnv != "production" {
if strings.HasPrefix(origin, "http://localhost:") ||
strings.HasPrefix(origin, "http://127.0.0.1:") {
allowed = true
}
}
if allowed && origin != "" {
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
c.Writer.Header().Set("Vary", "Origin")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
}
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, Cache-Control, X-Requested-With")
// Handle preflight requests
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
```
### 8.4 Rate Limiting
Aplikace implementuje rate limiting pro ochranu proti abuse a DDoS útokům.
```go
// Rate limiting middleware
func RateLimit(maxRequests int, window time.Duration) gin.HandlerFunc {
type client struct {
count int
resetTime time.Time
}
clients := make(map[string]*client)
mu := sync.Mutex{}
return func(c *gin.Context) {
ip := c.ClientIP()
mu.Lock()
defer mu.Unlock()
now := time.Now()
// Clean up expired entries
if cl, exists := clients[ip]; exists && now.After(cl.resetTime) {
delete(clients, ip)
}
// Initialize or increment counter
if _, exists := clients[ip]; !exists {
clients[ip] = &client{
count: 1,
resetTime: now.Add(window),
}
} else {
clients[ip].count++
}
// Check limit
if clients[ip].count > maxRequests {
c.JSON(429, gin.H{
"error": "Too many requests",
"retry_after": clients[ip].resetTime.Sub(now).Seconds(),
})
c.Abort()
return
}
c.Next()
}
}
// Použití na citlivých endpointech
auth.POST("/login", middleware.RateLimit(15, time.Minute), authController.Login)
auth.POST("/register", middleware.RateLimit(5, time.Hour), authController.Register)
api.POST("/contact", middleware.RateLimit(10, time.Minute), contactController.SubmitContactForm)
```
**Limity:**
- Login: 15 pokusů / minutu
- Registrace: 5 pokusů / hodinu
- Kontaktní formulář: 10 odeslání / minutu
- Newsletter subscribe: 30 pokusů / minutu
### 8.5 Input Validation a Sanitizace
#### **Backend Validace**
```go
type CreateArticleRequest struct {
Title string `json:"title" binding:"required,min=3,max=255"`
Slug string `json:"slug" binding:"required,min=3,max=255,alphanum_hyphen"`
Content string `json:"content" binding:"required,min=10"`
CategoryID uint `json:"category_id" binding:"required,gt=0"`
Published bool `json:"published"`
FeaturedImage string `json:"featured_image" binding:"omitempty,url"`
}
func (bc *BaseController) CreateArticle(c *gin.Context) {
var req CreateArticleRequest
// Validace
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Sanitizace HTML obsahu
sanitizedContent := sanitizeHTML(req.Content)
// Create article...
}
```
#### **SQL Injection Prevence**
GORM automaticky escapuje parametry v SQL dotazech.
```go
// ✅ Bezpečné - parameterized query
db.Where("email = ?", userEmail).First(&user)
// ❌ NEBEZPEČNÉ - string concatenation
db.Where("email = '" + userEmail + "'").First(&user) // NIKDY NEPOUŽÍVAT!
```
#### **XSS Prevence**
Frontend používá React, který automaticky escapuje obsah. Pro HTML obsah z editoru:
```typescript
// Sanitizace HTML na frontendu
import DOMPurify from 'dompurify';
function ArticleContent({ html }) {
const sanitizedHTML = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'h1', 'h2', 'h3', 'ul', 'ol', 'li', 'a', 'img'],
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class'],
});
return ;
}
```
### 8.6 File Upload Security
```go
func (bc *BaseController) UploadImage(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": "No file uploaded"})
return
}
// 1. Kontrola velikosti
maxSize := int64(10 * 1024 * 1024) // 10MB
if file.Size > maxSize {
c.JSON(400, gin.H{"error": "File too large"})
return
}
// 2. Kontrola MIME typu
allowedTypes := map[string]bool{
"image/jpeg": true,
"image/png": true,
"image/gif": true,
"image/webp": true,
}
contentType := file.Header.Get("Content-Type")
if !allowedTypes[contentType] {
c.JSON(400, gin.H{"error": "Invalid file type"})
return
}
// 3. Kontrola magic bytes (skutečný typ souboru)
fileContent, _ := file.Open()
buffer := make([]byte, 512)
fileContent.Read(buffer)
detectedType := http.DetectContentType(buffer)
if !allowedTypes[detectedType] {
c.JSON(400, gin.H{"error": "File content mismatch"})
return
}
// 4. Generování bezpečného jména souboru
ext := filepath.Ext(file.Filename)
safeFilename := fmt.Sprintf("%d_%s%s",
time.Now().Unix(),
generateRandomString(16),
ext,
)
// 5. Uložení do izolovaného adresáře
uploadPath := filepath.Join("./uploads", safeFilename)
c.SaveUploadedFile(file, uploadPath)
c.JSON(200, gin.H{"url": "/uploads/" + safeFilename})
}
```
### 8.7 GDPR Compliance
#### **Souhlas s Cookies**
```typescript
// Cookie consent banner
const CookieBanner = () => {
const [consent, setConsent] = useState(null);
const handleAccept = (categories: string[]) => {
const consent = {
necessary: true,
analytics: categories.includes('analytics'),
marketing: categories.includes('marketing'),
timestamp: new Date().toISOString(),
};
localStorage.setItem('cookie_consent', JSON.stringify(consent));
setConsent(consent);
// Spustit analytics pouze pokud uživatel souhlasil
if (consent.analytics) {
initializeUmami();
}
};
return consent ? null : ;
};
```
#### **Anonymizace IP Adres**
```go
// Hash IP adresy pro analytics (GDPR compliant)
func hashIP(ip string) string {
h := sha256.New()
h.Write([]byte(ip + os.Getenv("IP_SALT")))
return fmt.Sprintf("%x", h.Sum(nil))[:16]
}
// Uložení trackingu s anonymizovanou IP
event := models.VisitorEvent{
IPHash: hashIP(c.ClientIP()),
// ... other fields
}
```
#### **Právo na Výmaz Dat**
```go
// Endpoint pro smazání uživatelských dat
func (uc *UserController) DeleteUserData(c *gin.Context) {
userID := c.Param("id")
// Smazat veškerá osobní data
db.Where("user_id = ?", userID).Delete(&models.Article{})
db.Where("email = ?", user.Email).Delete(&models.NewsletterSubscription{})
db.Where("user_id = ?", userID).Delete(&models.ContactMessage{})
db.Delete(&models.User{}, userID)
c.JSON(200, gin.H{"message": "User data deleted"})
}
```
### 8.8 Environment Variables Security
**Nikdy necommitovat:**
- `.env` soubor s produkčními credentials
- API klíče
- JWT secret
- SMTP hesla
**Best practices:**
```bash
# .gitignore
.env
.env.local
.env.production
# Commitovat pouze example
.env.example
```
**Production deployment:**
- Použít environment variables na serveru
- Nebo encrypted secrets (GitHub Secrets, Vault)
- Rotate API keys pravidelně
### 8.9 Production Security Checklist
- ✅ **JWT_SECRET** změněn z default hodnoty
- ✅ **HTTPS** enabled (přes reverse proxy)
- ✅ **HSTS** header aktivován
- ✅ **CORS** omezen na konkrétní domény
- ✅ **Rate limiting** na všech public endpointech
- ✅ **SQL injection** prevence (GORM parametry)
- ✅ **XSS** prevence (React + DOMPurify)
- ✅ **File upload** validace
- ✅ **Password hashing** (bcrypt cost 14)
- ✅ **GDPR** compliance (cookie consent, IP anonymizace)
- ✅ **Security headers** (CSP, X-Frame-Options, etc.)
- ✅ **Input validation** na frontendu i backendu
- ✅ **Error logging** bez citlivých dat
---
## 9. Minimální Požadavky
### 9.1 Hardwarové Požadavky
#### **Development (Lokální vývoj):**
- **CPU:** 2+ cores (Intel i3 nebo ekvivalent)
- **RAM:** 4 GB minimum, 8 GB doporučeno
- **Disk:** 5 GB volného místa (SSD doporučeno)
- **OS:** Windows 10/11, macOS 10.15+, Linux (Ubuntu 20.04+)
#### **Production (Server):**
- **CPU:** 2+ cores
- **RAM:** 2 GB minimum, 4 GB doporučeno pro 1000+ concurrent users
- **Disk:** 20 GB+ (závisí na množství uploadů a databáze)
- **Bandwidth:** 100 Mbps+ pro optimální výkon
### 9.2 Softwarové Požadavky
#### **Backend:**
| Software | Minimální Verze | Doporučená Verze |
|----------|-----------------|------------------|
| Go | 1.23.0 | 1.24.4+ |
| PostgreSQL | 12.0 | 15.0+ |
| Docker | 20.10 | 24.0+ |
| Docker Compose | 1.29 | 2.20+ |
#### **Frontend:**
| Software | Minimální Verze | Doporučená Verze |
|----------|-----------------|------------------|
| Node.js | 16.0 | 20.0+ LTS |
| npm | 8.0 | 10.0+ |
#### **Další nástroje:**
- Git 2.30+
- Text editor (VS Code, GoLand)
- Make (pro Makefile příkazy)
### 9.3 Prohlížeče (Frontend Kompatibilita)
| Prohlížeč | Minimální Verze |
|-----------|-----------------|
| Chrome | 90+ |
| Firefox | 88+ |
| Safari | 14+ |
| Edge | 90+ |
| Mobile Safari (iOS) | 14+ |
| Chrome Mobile (Android) | 90+ |
---
## 10. Instalace a Konfigurace
### 10.1 Rychlá Instalace (Docker - Doporučeno)
**Krok 1: Klonování repozitáře**
```bash
git clone
cd fotbal-club
```
**Krok 2: Konfigurace prostředí**
```bash
# Zkopírovat example soubor
cp .env.example .env
# Upravit .env soubor
# Minimálně změnit:
# - JWT_SECRET
# - SMTP_* konfigurace
# - ALLOWED_ORIGINS
```
**Krok 3: Spuštění aplikace**
```bash
docker-compose up -d
```
**Krok 4: Přístup do aplikace**
- Frontend: http://localhost:3000
- Backend API: http://localhost:8080
- Databáze: localhost:5432
**Krok 5: Průvodce nastavením**
1. Otevřete http://localhost:3000
2. Budete přesměrováni na setup wizard
3. Vytvořte admin účet
4. Nastavte klubové informace
5. Vyberte klubové barvy
6. Připojte FAČR klub
### 10.2 Manuální Instalace (Bez Dockeru)
#### **Backend Setup:**
```bash
# 1. Instalace Go dependencies
go mod download
# 2. Nastavení PostgreSQL
createdb fotbal_club
# 3. Konfigurace .env
cp .env.example .env
# Upravit DATABASE_URL, JWT_SECRET, etc.
# 4. Migrace databáze
make migrate
# 5. (Volitelně) Seed testovacích dat
make seed
# 6. Spuštění serveru
make run
# Nebo přímo:
go run main.go
```
**Backend běží na:** http://localhost:8080
#### **Frontend Setup:**
```bash
# 1. Přejít do frontend složky
cd frontend
# 2. Instalace dependencies
npm install
# 3. Konfigurace .env
cp .env.example .env
# Nastavit REACT_APP_API_URL=http://localhost:8080/api/v1
# 4. Spuštění dev serveru
npm start
```
**Frontend běží na:** http://localhost:3000
### 10.3 Konfigurační Soubory
#### **Backend .env (Kompletní)**
```bash
# ==========================================
# APPLICATION
# ==========================================
APP_NAME=FotbalClub
APP_ENV=development # development, staging, production
PORT=8080
DEBUG=true
# ==========================================
# DATABASE
# ==========================================
DB_HOST=db
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=fotbal_club
DATABASE_URL=postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=disable
# Database Migrations & Seeding
RUN_MIGRATIONS=true
SEED_DATABASE=false
# ==========================================
# JWT AUTHENTICATION
# ==========================================
JWT_SECRET=your_very_secret_key_change_in_production
JWT_EXPIRATION_HOURS=24
# ==========================================
# EMAIL (SMTP)
# ==========================================
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=your_email@example.com
SMTP_PASSWORD=your_password
SMTP_FROM=noreply@fotbalclub.com
SMTP_FROM_NAME="Fotbal Club"
SMTP_ENCRYPTION=tls # tls, ssl, or none
SMTP_AUTH=true
SMTP_SKIP_VERIFY=false
# Email Templates
EMAIL_TEMPLATE_DIR=./templates/emails
# Contact Form
CONTACT_EMAIL=help@example.com
ADMIN_EMAIL=admin@example.com
# ==========================================
# NEWSLETTER
# ==========================================
NEWSLETTER_ENABLED=true
# ==========================================
# FILE UPLOADS
# ==========================================
UPLOAD_DIR=./uploads
MAX_UPLOAD_SIZE=10485760 # 10MB in bytes
ALLOWED_FILE_TYPES=image/jpeg,image/png,image/gif,application/pdf
MAX_FILES=5
# ==========================================
# CORS
# ==========================================
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080
# ==========================================
# EXTERNAL APIs
# ==========================================
# OpenRouter (AI blog generation)
OPENROUTER_API_KEY=sk-or-v1-your-key-here
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
OPENROUTER_MODEL=mistralai/mistral-small-3.2-24b-instruct:free
OPENROUTER_FALLBACK_MODEL=mistralai/mistral-nemo:free
# Umami Analytics
UMAMI_URL=https://umami.example.com
UMAMI_USERNAME=admin
UMAMI_PASSWORD=your_password
UMAMI_WEBSITE_ID= # Auto-create on first run
# ==========================================
# LOGGING
# ==========================================
LOG_LEVEL=info # debug, info, warn, error
LOG_FORMAT=text # text or json
LOG_OUTPUT=stdout # stdout, stderr, or file path
# ==========================================
# SECURITY
# ==========================================
CONTENT_SECURITY_POLICY=default-src 'self'
IP_SALT=random_salt_for_hashing_ips
```
#### **Frontend .env**
```bash
# API Configuration
REACT_APP_API_URL=http://localhost:8080/api/v1
REACT_APP_API_BASE_URL=http://localhost:8080
# FACR API
REACT_APP_FACR_API_BASE_URL=http://localhost:8080/api/v1/facr
REACT_APP_FACR_API_TIMEOUT=5000
REACT_APP_FACR_CACHE_TTL=3600000 # 1 hour in ms
# Homepage Layout
REACT_APP_HOMEPAGE_LAYOUT=classic # classic or sparta
# Application Name
REACT_APP_NAME=Fotbal Club Manager
```
### 10.4 Databázové Migrace
Aplikace používá automatické migrace přes GORM.
**Spuštění migrací:**
```bash
# V .env souboru
RUN_MIGRATIONS=true
# Pak restart aplikace
docker-compose restart backend
# Nebo
make migrate
```
**Manuální migrace (SQL):**
```sql
-- Vytvořit novou migraci v database/migrations/
-- Pojmenování: 001_create_users_table.sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(50) DEFAULT 'editor',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
### 10.5 Seed Data (Testovací Data)
```bash
# V .env
SEED_DATABASE=true
# Restart
docker-compose restart backend
```
**Co se seeduje:**
- Admin uživatel (admin@club.cz / admin123)
- Základní kategorie článků
- Sample články
- Sample týmy a hráči
- Sponzoři
### 10.6 Production Deployment
#### **Doporučená Architektura:**
```
Internet
│
▼
[Nginx Reverse Proxy] ← HTTPS/SSL
│
├──► [Frontend Container (React SPA)]
│
├──► [Backend Container (Go API)]
│
└──► [PostgreSQL Container/Managed DB]
```
#### **Docker Production Build:**
```bash
# 1. Build production image
docker build -t fotbal-club:latest .
# 2. Run with production env
docker run -d \
--name fotbal-club \
-p 8080:8080 \
--env-file .env.production \
-v /path/to/uploads:/app/uploads \
fotbal-club:latest
```
#### **Nginx Konfigurace:**
```nginx
server {
listen 80;
server_name fotbalclub.cz;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name fotbalclub.cz;
ssl_certificate /etc/letsencrypt/live/fotbalclub.cz/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/fotbalclub.cz/privkey.pem;
# Frontend (SPA)
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Backend API
location /api/ {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Uploads
location /uploads/ {
alias /var/www/fotbalclub/uploads/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# Cache
location /cache/ {
alias /var/www/fotbalclub/cache/;
expires 30m;
}
}
```
#### **Environment Variables na Produkci:**
```bash
# Použít environment variables místo .env souboru
export APP_ENV=production
export JWT_SECRET=$(openssl rand -base64 64)
export DATABASE_URL=postgres://user:pass@db.example.com:5432/fotbal_club?sslmode=require
export ALLOWED_ORIGINS=https://fotbalclub.cz
# Nebo použít Docker secrets / Kubernetes secrets
```
#### **Backup Strategy:**
```bash
# Automatický denní backup databáze
0 2 * * * pg_dump fotbal_club > /backups/fotbal_club_$(date +\%Y\%m\%d).sql
# Backup uploadů
0 3 * * * rsync -av /var/www/fotbalclub/uploads/ /backups/uploads/
```
### 10.7 Troubleshooting
#### **Časté Problémy:**
**1. Backend se nespustí - "Failed to connect to database"**
```bash
# Zkontrolovat DB běží
docker ps | grep postgres
# Zkontrolovat DATABASE_URL v .env
echo $DATABASE_URL
# Restartovat DB
docker-compose restart db
```
**2. Frontend nemůže volat API - CORS error**
```bash
# Zkontrolovat ALLOWED_ORIGINS v backend .env
# Zkontrolovat REACT_APP_API_URL ve frontend .env
# Development:
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080
```
**3. Migrace selhaly**
```bash
# Vymazat databázi a začít znovu
docker-compose down -v
docker-compose up -d
# Nebo manuální reset
psql fotbal_club -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
```
**4. Upload souborů nefunguje**
```bash
# Zkontrolovat oprávnění
chmod -R 755 ./uploads
# Zkontrolovat UPLOAD_DIR v .env
# Zkontrolovat MAX_UPLOAD_SIZE
```
**5. JWT token expired příliš rychle**
```bash
# Upravit JWT_EXPIRATION_HOURS v .env
JWT_EXPIRATION_HOURS=168 # 7 dní
```
---
## 11. Příklady Kódu
### 11.1 Backend - Vytvoření Nového Endpointu
**Příklad: Endpoint pro získání statistik článků**
```go
// internal/controllers/stats_controller.go
package controllers
import (
"fotbal-club/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type StatsController struct {
DB *gorm.DB
}
// GetArticleStats vrací statistiky článků
func (sc *StatsController) GetArticleStats(c *gin.Context) {
var stats struct {
TotalArticles int64 `json:"total_articles"`
PublishedArticles int64 `json:"published_articles"`
TotalViews int `json:"total_views"`
AverageViews float64 `json:"average_views"`
MostViewedArticle *models.Article `json:"most_viewed_article"`
}
// Celkový počet článků
sc.DB.Model(&models.Article{}).Count(&stats.TotalArticles)
// Počet publikovaných
sc.DB.Model(&models.Article{}).Where("published = ?", true).Count(&stats.PublishedArticles)
// Celkový počet zhlédnutí
var articles []models.Article
sc.DB.Find(&articles)
for _, article := range articles {
stats.TotalViews += article.ViewCount
}
// Průměrný počet zhlédnutí
if stats.TotalArticles > 0 {
stats.AverageViews = float64(stats.TotalViews) / float64(stats.TotalArticles)
}
// Nejvíce zobrazovaný článek
sc.DB.Order("view_count DESC").First(&stats.MostViewedArticle)
c.JSON(200, gin.H{
"success": true,
"data": stats,
})
}
```
**Registrace route:**
```go
// internal/routes/routes.go
func SetupRoutes(api *gin.RouterGroup, db *gorm.DB) {
statsController := &controllers.StatsController{DB: db}
// Public stats
api.GET("/stats/articles", statsController.GetArticleStats)
}
```
### 11.2 Frontend - Vytvoření Nové Komponenty
**Příklad: Dashboard widget pro statistiky**
```typescript
// frontend/src/components/dashboard/ArticleStatsWidget.tsx
import React from 'react';
import { Box, Stat, StatLabel, StatNumber, StatHelpText, SimpleGrid } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import api from '../../services/api';
interface ArticleStats {
total_articles: number;
published_articles: number;
total_views: number;
average_views: number;
most_viewed_article: {
id: number;
title: string;
view_count: number;
};
}
const ArticleStatsWidget: React.FC = () => {
const { data, isLoading, error } = useQuery({
queryKey: ['article-stats'],
queryFn: async () => {
const response = await api.get('/stats/articles');
return response.data.data;
},
refetchInterval: 30000, // Refresh každých 30 sekund
});
if (isLoading) {
return Načítání...;
}
if (error) {
return Chyba při načítání statistik;
}
return (
Celkem článků
{data?.total_articles}
Všechny články v systému
Publikované
{data?.published_articles}
{((data?.published_articles / data?.total_articles) * 100).toFixed(1)}% z celku
Celková zobrazení
{data?.total_views.toLocaleString()}
Všechny články dohromady
Průměrná zobrazení
{data?.average_views.toFixed(1)}
Per článek
{data?.most_viewed_article && (
Nejčtenější článek: {data.most_viewed_article.title}
{data.most_viewed_article.view_count} zobrazení
)}
);
};
export default ArticleStatsWidget;
```
### 11.3 Použití Custom Hooks
**Příklad: useArticles hook**
```typescript
// frontend/src/hooks/useArticles.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import api from '../services/api';
import { Article, CreateArticleRequest } from '../types';
export const useArticles = (filters?: {
category?: number;
published?: boolean;
page?: number;
limit?: number;
}) => {
return useQuery({
queryKey: ['articles', filters],
queryFn: async () => {
const params = new URLSearchParams();
if (filters?.category) params.append('category', filters.category.toString());
if (filters?.published !== undefined) params.append('published', filters.published.toString());
if (filters?.page) params.append('page', filters.page.toString());
if (filters?.limit) params.append('limit', filters.limit.toString());
const response = await api.get(`/articles?${params.toString()}`);
return response.data.data;
},
});
};
export const useCreateArticle = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (article: CreateArticleRequest) => {
const response = await api.post('/articles', article);
return response.data;
},
onSuccess: () => {
// Invalidate and refetch articles
queryClient.invalidateQueries({ queryKey: ['articles'] });
},
});
};
export const useDeleteArticle = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (articleId: number) => {
await api.delete(`/articles/${articleId}`);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['articles'] });
},
});
};
```
**Použití v komponentě:**
```typescript
// frontend/src/pages/admin/ArticlesAdminPage.tsx
import React from 'react';
import { useArticles, useDeleteArticle } from '../../hooks/useArticles';
const ArticlesAdminPage: React.FC = () => {
const { data: articles, isLoading } = useArticles({ published: true });
const deleteMutation = useDeleteArticle();
const handleDelete = async (id: number) => {
if (window.confirm('Opravdu smazat článek?')) {
await deleteMutation.mutateAsync(id);
}
};
if (isLoading) return Načítání...
;
return (
{articles?.map((article) => (
{article.title}
))}
);
};
```
### 11.4 Background Job - Newsletter Automation
**Příklad: Automatické odesílání newsletteru**
```go
// internal/services/newsletter_automation.go
package services
import (
"fotbal-club/internal/models"
"fotbal-club/pkg/email"
"time"
"gorm.io/gorm"
"log"
)
type NewsletterAutomation struct {
db *gorm.DB
emailService *email.EmailService
ticker *time.Ticker
stopChan chan bool
}
func NewNewsletterAutomation(db *gorm.DB, emailSvc *email.EmailService) *NewsletterAutomation {
return &NewsletterAutomation{
db: db,
emailService: emailSvc,
stopChan: make(chan bool),
}
}
func (na *NewsletterAutomation) Start() {
// Check every hour
na.ticker = time.NewTicker(1 * time.Hour)
go func() {
for {
select {
case <-na.ticker.C:
na.checkAndSendScheduled()
case <-na.stopChan:
na.ticker.Stop()
return
}
}
}()
log.Println("Newsletter automation started")
}
func (na *NewsletterAutomation) Stop() {
na.stopChan <- true
log.Println("Newsletter automation stopped")
}
func (na *NewsletterAutomation) checkAndSendScheduled() {
now := time.Now()
// Weekly digest - Friday at 18:00
if now.Weekday() == time.Friday && now.Hour() == 18 {
na.sendWeeklyDigest()
}
// Match alerts - 24 hours before match
na.sendMatchAlerts()
// Results digest - Monday at 10:00
if now.Weekday() == time.Monday && now.Hour() == 10 {
na.sendResultsDigest()
}
}
func (na *NewsletterAutomation) sendWeeklyDigest() {
// Get articles from last 7 days
var articles []models.Article
sevenDaysAgo := time.Now().AddDate(0, 0, -7)
na.db.Where("published = ? AND created_at > ?", true, sevenDaysAgo).
Order("created_at DESC").
Limit(5).
Find(&articles)
if len(articles) == 0 {
return // No new content
}
// Get subscribers who want weekly digest
var subscribers []models.NewsletterSubscription
na.db.Where("status = ? AND preferences->>'weekly_digest' = 'true'", "active").
Find(&subscribers)
for _, sub := range subscribers {
templateData := map[string]interface{}{
"Email": sub.Email,
"Articles": articles,
"Type": "weekly_digest",
}
err := na.emailService.SendNewsletter(sub.Email, "Týdenní přehled", templateData)
if err != nil {
log.Printf("Failed to send newsletter to %s: %v", sub.Email, err)
} else {
log.Printf("Weekly digest sent to %s", sub.Email)
}
// Rate limiting - wait 100ms between emails
time.Sleep(100 * time.Millisecond)
}
}
func (na *NewsletterAutomation) sendMatchAlerts() {
// Get matches in next 24 hours
tomorrow := time.Now().Add(24 * time.Hour)
dayAfterTomorrow := time.Now().Add(48 * time.Hour)
// Implementation would fetch from FACR or database
// For brevity, simplified here
log.Println("Checking for match alerts...")
}
func (na *NewsletterAutomation) sendResultsDigest() {
log.Println("Sending results digest...")
}
```
### 11.5 Use Case: Kompletní Flow Vytvoření Článku
**1. Frontend - Formulář pro vytvoření článku:**
```typescript
// frontend/src/components/admin/ArticleForm.tsx
import React, { useState } from 'react';
import { useCreateArticle } from '../../hooks/useArticles';
import { useNavigate } from 'react-router-dom';
const ArticleForm: React.FC = () => {
const [formData, setFormData] = useState({
title: '',
content: '',
excerpt: '',
category_id: 1,
published: false,
featured_image: '',
});
const createMutation = useCreateArticle();
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await createMutation.mutateAsync(formData);
alert('Článek vytvořen!');
navigate('/admin/articles');
} catch (error) {
alert('Chyba při vytváření článku');
}
};
return (
);
};
```
**2. Backend - API handler:**
```go
// internal/controllers/base_controller.go
func (bc *BaseController) CreateArticle(c *gin.Context) {
var req struct {
Title string `json:"title" binding:"required,min=3,max=255"`
Content string `json:"content" binding:"required,min=10"`
Excerpt string `json:"excerpt"`
CategoryID uint `json:"category_id" binding:"required"`
Published bool `json:"published"`
FeaturedImage string `json:"featured_image"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Get user from context
userID := c.GetUint("userID")
// Generate slug
slug := generateSlug(req.Title)
// Create article
article := models.Article{
Title: req.Title,
Slug: slug,
Content: req.Content,
Excerpt: req.Excerpt,
CategoryID: req.CategoryID,
Published: req.Published,
FeaturedImage: req.FeaturedImage,
AuthorID: userID,
}
if req.Published {
now := time.Now()
article.PublishedAt = &now
}
if err := bc.DB.Create(&article).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to create article"})
return
}
// Preload relationships
bc.DB.Preload("Category").Preload("Author").First(&article, article.ID)
c.JSON(201, gin.H{
"success": true,
"data": article,
"message": "Article created successfully",
})
}
```
### 11.6 Performance Optimization - Caching
**Backend caching příklad:**
```go
// pkg/cache/cache.go
package cache
import (
"encoding/json"
"os"
"path/filepath"
"sync"
"time"
)
type Cache struct {
mu sync.RWMutex
data map[string]CacheEntry
}
type CacheEntry struct {
Data interface{}
ExpiresAt time.Time
}
var globalCache = &Cache{
data: make(map[string]CacheEntry),
}
func Set(key string, value interface{}, ttl time.Duration) {
globalCache.mu.Lock()
defer globalCache.mu.Unlock()
globalCache.data[key] = CacheEntry{
Data: value,
ExpiresAt: time.Now().Add(ttl),
}
}
func Get(key string) (interface{}, bool) {
globalCache.mu.RLock()
defer globalCache.mu.RUnlock()
entry, exists := globalCache.data[key]
if !exists {
return nil, false
}
if time.Now().After(entry.ExpiresAt) {
delete(globalCache.data, key)
return nil, false
}
return entry.Data, true
}
// Save to file for persistence
func SaveToFile(filename string) error {
globalCache.mu.RLock()
defer globalCache.mu.RUnlock()
data, err := json.Marshal(globalCache.data)
if err != nil {
return err
}
return os.WriteFile(filepath.Join("./cache", filename), data, 0644)
}
```
**Použití:**
```go
func (bc *BaseController) GetArticles(c *gin.Context) {
cacheKey := "articles_published"
// Try cache first
if cached, found := cache.Get(cacheKey); found {
c.JSON(200, cached)
return
}
// Fetch from database
var articles []models.Article
bc.DB.Where("published = ?", true).Find(&articles)
response := gin.H{"success": true, "data": articles}
// Cache for 5 minutes
cache.Set(cacheKey, response, 5*time.Minute)
c.JSON(200, response)
}
```
---
## 12. Závěr
### 12.1 Shrnutí Projektu
**Fotbal Club** je komplexní full-stack webová aplikace navržená pro efektivní správu fotbalového klubu. Projekt úspěšně kombinuje moderní technologie a best practices pro vytvoření robustního, škálovatelného a bezpečného systému.
#### **Klíčové Úspěchy:**
✅ **Moderní Tech Stack**
- Go backend s vysokým výkonem
- React frontend s TypeScript
- PostgreSQL databáze
- Docker pro snadné deployment
✅ **Komplexní Funkcionalita**
- Správa obsahu (články, galerie, videa)
- Automatická integrace s FAČR API
- Newsletter systém s automatizací
- Analytics a statistiky
- AI generování obsahu
✅ **Bezpečnost**
- JWT autentizace
- RBAC autorizace
- Rate limiting
- GDPR compliance
- Security headers
✅ **Developer Experience**
- Setup wizard pro snadnou instalaci
- Comprehensive API dokumentace
- Docker Compose pro one-command start
- Environment-based konfigurace
### 12.2 Technické Přednosti
| Oblast | Implementace |
|--------|--------------|
| **Performance** | Caching, lazy loading, optimalizované DB queries |
| **Scalability** | Stateless API, horizontální škálování možné |
| **Maintainability** | Čistá architektura, SOLID principy |
| **Testing** | Unit testy, integration testy připraveny |
| **Documentation** | Kompletní technická dokumentace |
### 12.3 Možnosti Rozšíření
Aplikace je navržena s ohledem na budoucí rozšíření:
**Krátk
odobé (1-3 měsíce):**
- [ ] WebSocket podpora pro real-time updates
- [ ] Push notifikace (PWA)
- [ ] Mobilní aplikace (React Native)
- [ ] Advanced SEO features (schema.org markup)
**Střednědobé (3-6 měsíců):**
- [ ] E-shop modul pro klubové produkty
- [ ] Ticketing systém pro zápasy
- [ ] Member portál s přihlášením hráčů
- [ ] Platební brána integrace
**Dlouhodobé (6-12 měsíců):**
- [ ] Multi-tenant architektura (více klubů)
- [ ] API pro třetí strany
- [ ] Machine learning predikce výsledků
- [ ] Video streaming integrace
### 12.4 Lessons Learned
**Co fungovalo dobře:**
- Docker drasticky zjednodušil development workflow
- React Query zjednodušil state management
- GORM AutoMigrate urychlil iterace
- JWT stateless autentizace škáluje výborně
**Co by se dalo zlepšit:**
- Implementovat GraphQL pro flexibilnější API
- Přidat více unit testů (současné pokrytí ~60%)
- Použít message queue pro newsletter (Redis/RabbitMQ)
- Implementovat circuit breaker pro externí API
### 12.5 Použité Zdroje a Reference
#### **12.5.1 Primární Technologie**
**Backend:**
- **Go (Golang)** - https://go.dev/
- Verze: 1.23.0+
- Dokumentace: https://go.dev/doc/
- Tour of Go: https://go.dev/tour/
- Effective Go: https://go.dev/doc/effective_go
- **Gin Web Framework** - https://gin-gonic.com/
- GitHub: https://github.com/gin-gonic/gin
- Dokumentace: https://gin-gonic.com/docs/
- Licence: MIT
- **GORM** - https://gorm.io/
- GitHub: https://github.com/go-gorm/gorm
- Dokumentace: https://gorm.io/docs/
- Licence: MIT
- **PostgreSQL** - https://www.postgresql.org/
- Verze: 15+
- Dokumentace: https://www.postgresql.org/docs/
- Licence: PostgreSQL License
**Frontend:**
- **React** - https://react.dev/
- GitHub: https://github.com/facebook/react
- Verze: 18+
- Dokumentace: https://react.dev/learn
- Licence: MIT
- **TypeScript** - https://www.typescriptlang.org/
- Verze: 5+
- Dokumentace: https://www.typescriptlang.org/docs/
- Licence: Apache 2.0
- **Chakra UI** - https://chakra-ui.com/
- GitHub: https://github.com/chakra-ui/chakra-ui
- Dokumentace: https://chakra-ui.com/docs/
- Licence: MIT
- **React Query (TanStack Query)** - https://tanstack.com/query/latest
- GitHub: https://github.com/TanStack/query
- Dokumentace: https://tanstack.com/query/latest/docs/react/overview
- Licence: MIT
- **React Router** - https://reactrouter.com/
- GitHub: https://github.com/remix-run/react-router
- Verze: 6+
- Dokumentace: https://reactrouter.com/en/main
- Licence: MIT
**DevOps:**
- **Docker** - https://www.docker.com/
- Dokumentace: https://docs.docker.com/
- Licence: Apache 2.0
- **Docker Compose** - https://docs.docker.com/compose/
- Dokumentace: https://docs.docker.com/compose/
- Licence: Apache 2.0
#### **12.5.2 Go Knihovny a Dependencies**
| Knihovna | Účel | Odkaz | Licence |
|----------|------|-------|---------|
| `golang-jwt/jwt/v5` | JWT autentizace | https://github.com/golang-jwt/jwt | MIT |
| `golang.org/x/crypto` | Bcrypt hashing | https://pkg.go.dev/golang.org/x/crypto | BSD-3 |
| `gopkg.in/mail.v2` | SMTP emaily | https://github.com/go-gomail/gomail | MIT |
| `joho/godotenv` | Environment variables | https://github.com/joho/godotenv | MIT |
| `PuerkitoBio/goquery` | HTML parsing | https://github.com/PuerkitoBio/goquery | BSD-3 |
| `vanng822/go-premailer` | Email CSS inlining | https://github.com/vanng822/go-premailer | MIT |
| `gorm.io/driver/postgres` | PostgreSQL driver | https://github.com/go-gorm/postgres | MIT |
#### **12.5.3 Frontend Knihovny a Dependencies**
| Knihovna | Účel | Odkaz | Licence |
|----------|------|-------|---------|
| `@chakra-ui/react` | UI komponenty | https://chakra-ui.com/ | MIT |
| `@tanstack/react-query` | Server state management | https://tanstack.com/query | MIT |
| `react-router-dom` | Routing | https://reactrouter.com/ | MIT |
| `axios` | HTTP client | https://axios-http.com/ | MIT |
| `dompurify` | XSS sanitizace | https://github.com/cure53/DOMPurify | Apache-2.0 |
| `react-icons` | Ikony | https://react-icons.github.io/react-icons/ | MIT |
| `framer-motion` | Animace | https://www.framer.com/motion/ | MIT |
#### **12.5.4 Externí API a Služby**
**FAČR (Fotbalová asociace České republiky):**
- **URL:** https://is.fotbal.cz/
- **Typ integrace:** Custom wrapper/scraper vytvořen pro tento projekt
- **Účel:** Automatické získávání výsledků, rozpisů a tabulek
- **Poznámka:** Oficiální API není veřejně dostupné, použit web scraping s `goquery`
- **Zdrojový web:** https://www.fotbal.cz/
**YouTube Data API v3:**
- **Dokumentace:** https://developers.google.com/youtube/v3
- **Reference:** https://developers.google.com/youtube/v3/docs
- **Účel:** Načítání videí z klubového YouTube kanálu
- **Licence:** Google APIs Terms of Service
- **Quota:** 10,000 units/day (free tier)
**Zonerama:**
- **URL:** https://www.zonerama.com/
- **Typ integrace:** Custom HTML scraper vytvořen pro tento projekt
- **Účel:** Synchronizace fotografických alb
- **Poznámka:** Zonerama nemá veřejné API, použit scraping
**Umami Analytics:**
- **Oficiální web:** https://umami.is/
- **GitHub:** https://github.com/umami-software/umami
- **Dokumentace:** https://umami.is/docs
- **Typ nasazení:** Self-hosted
- **Účel:** Privacy-first web analytics
- **Licence:** MIT
- **Alternativa k:** Google Analytics
**OpenRouter AI:**
- **Oficiální web:** https://openrouter.ai/
- **Dokumentace:** https://openrouter.ai/docs
- **API Reference:** https://openrouter.ai/docs/api-reference
- **Účel:** AI generování článků pomocí LLM modelů
- **Modely použité:**
- Mistral Small: https://mistral.ai/
- Mistral Nemo: https://mistral.ai/news/mistral-nemo/
- **Ceny:** Free tier dostupný, placené modely pro produkci
**Google Maps Embed API:**
- **Dokumentace:** https://developers.google.com/maps/documentation/embed
- **Účel:** Zobrazení lokace klubu na mapě
- **Licence:** Google Maps Platform Terms
#### **12.5.5 Vývojové Nástroje**
| Nástroj | Účel | Odkaz |
|---------|------|-------|
| **Visual Studio Code** | Code editor | https://code.visualstudio.com/ |
| **Git** | Version control | https://git-scm.com/ |
| **Postman** | API testing | https://www.postman.com/ |
| **pgAdmin** | PostgreSQL management | https://www.pgadmin.org/ |
| **Go Playground** | Go testing | https://go.dev/play/ |
| **npm** | Package manager | https://www.npmjs.com/ |
#### **12.5.6 Knihy a Články**
**Knihy:**
1. **"Clean Code: A Handbook of Agile Software Craftsmanship"**
- Autor: Robert C. Martin
- Vydavatel: Prentice Hall
- ISBN: 978-0132350884
- Použito pro: Best practices v architektuře kódu
2. **"Designing Data-Intensive Applications"**
- Autor: Martin Kleppmann
- Vydavatel: O'Reilly Media
- ISBN: 978-1449373320
- Použito pro: Databázový design a škálovatelnost
3. **"The Go Programming Language"**
- Autoři: Alan A. A. Donovan, Brian W. Kernighan
- Vydavatel: Addison-Wesley
- ISBN: 978-0134190440
- Použito pro: Go best practices
**Online Zdroje:**
- **Go by Example** - https://gobyexample.com/
- **React TypeScript Cheatsheet** - https://react-typescript-cheatsheet.netlify.app/
- **PostgreSQL Tutorial** - https://www.postgresqltutorial.com/
- **JWT.io** - https://jwt.io/ (JWT debugger a dokumentace)
- **MDN Web Docs** - https://developer.mozilla.org/ (Web standardy)
#### **12.5.7 Bezpečnostní Standardy a Specifikace**
- **OWASP Top 10** - https://owasp.org/www-project-top-ten/
- Použito pro: Identifikace a prevenci bezpečnostních rizik
- **GDPR (General Data Protection Regulation)**
- Oficiální text: https://gdpr-info.eu/
- Použito pro: Cookie consent, data privacy, právo na výmaz
- **JWT RFC 7519** - https://datatracker.ietf.org/doc/html/rfc7519
- Použito pro: JWT implementace
- **bcrypt** - https://en.wikipedia.org/wiki/Bcrypt
- Použito pro: Password hashing
#### **12.5.8 Inspirace a Podobné Projekty**
- **Hacker News Clone (Go + React)** - Inspirace pro strukturu projektu
- **Real World App** - https://github.com/gothinkster/realworld
- Použito pro: API design patterns
#### **12.5.9 Community a Support**
- **Stack Overflow** - https://stackoverflow.com/
- Tag: golang, reactjs, postgresql, docker
- **Go Forum** - https://forum.golangbridge.org/
- **React Discord** - https://discord.gg/react
- **r/golang** - https://www.reddit.com/r/golang/
- **r/reactjs** - https://www.reddit.com/r/reactjs/
#### **12.5.10 Citace a Poděkování**
Tento projekt využívá open-source software a byl vytvořen díky příspěvkům tisíců vývojářů v komunitách:
**Poděkování:**
- **Go Team** - Za výborný programovací jazyk a ekosystém
- **Facebook/Meta** - Za React a související nástroje
- **Chakra UI Team** - Za krásnou UI knihovnu
- **GORM Team** - Za powerful ORM pro Go
- **PostgreSQL Global Development Group** - Za spolehlivou databázi
- **Docker Inc.** - Za kontejnerizační platformu
- **Všem contributorom** open-source projektů použitých v této aplikaci
**Copyright Notice:**
Všechny použité technologie a knihovny jsou použity v souladu s jejich licencemi. Tento projekt je šířen pod MIT licencí a respektuje všechny licence závislostí.
**Akademické Použití:**
Tento projekt byl vytvořen pro vzdělávací účely jako součást maturitní zkoušky. Veškerý kód je původní dílo autora s výjimkou open-source knihoven a frameworků, které jsou řádně citovány výše.
### 12.6 Autor a Kontakt
**Projekt vytvořil:** Tomáš Dvořák
**Datum:** Říjen 2025
**Účel:** Maturitní projekt - Prezentace full-stack webového systému
**Licence:** MIT License
---
### 12.7 Poděkování
Tento projekt vznikl jako demonstrace moderních webových technologií a best practices v oblasti full-stack development. Děkuji všem, kteří přispěli k open-source nástrojům použitým v tomto projektu.
---
**Konec dokumentace**
*Verze 1.0 - Říjen 2025*