98 KiB
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
- Úvod a Motivace
- Přehled Projektu
- Technologický Stack
- Architektura Systému
- Struktura Projektu
- Klíčové Funkce
- API a Integrace
- Bezpečnost
- Minimální Požadavky
- Instalace a Konfigurace
- Příklady Kódu
- 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
// 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
// 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
// Příklad Chakra UI komponenty
import { Box, Button, Heading, Stack } from '@chakra-ui/react';
function ArticleCard({ article }) {
return (
<Box
bg="white"
p={6}
rounded="lg"
shadow="md"
_hover={{ shadow: 'lg' }}
>
<Heading size="md" mb={4}>{article.title}</Heading>
<Button colorScheme="brand">Číst více</Button>
</Box>
);
}
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 |
// 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 <Spinner />;
if (error) return <Alert status="error">{error.message}</Alert>;
return <ArticlesGrid articles={data} />;
}
Routing: React Router v6
- Deklarativní routing
- Protected routes s autorizací
- Lazy loading stránek
- Nested routes
3.3 DevOps a Nástroje
Kontejnerizace: Docker
# 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 |
| 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 |
| OpenRouter AI | AI generování článků | REST API (GPT modely) | Docs |
| Google Maps Embed | Lokace klubu | Maps Embed API | Docs |
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:
-
Controllers (Ovladače)
- Zpracování HTTP požadavků
- Validace vstupních dat
- Volání business logiky
- Formátování odpovědí
-
Middleware
- Autentizace (JWT)
- Autorizace (role-based)
- Rate limiting
- CORS
- Security headers
-
Services (Služby)
- Business logika
- Komplexní operace
- Integrace s externími API
- Background joby
-
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:
// 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:
-- 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:
// Ú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
# 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
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:
// 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:
- Admin vytvoří článek přes formulář s WYSIWYG editorem
- Nahraje featured obrázek přes drag-and-drop
- Vybere kategorii a nastaví publikování
- Č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
// 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
// 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
// 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í
// 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ů:
-
Týdenní Digest
- Automaticky každý pátek v 18:00
- Obsahuje nové články z týdne
- Nadcházející zápasy
-
Match Alerts
- 24h před zápasem
- Info o soupeři, místě konání
- Odkaz na mapy
-
Blog Notifications
- Při publikování nového článku
- Pouze pro odběratele "blog" kategorie
-
Results Digest
- Po víkendu s výsledky
- Aktuální tabulka
Technická Implementace:
// 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:
<!DOCTYPE html>
<html>
<head>
<style>
/* Inline CSS pro kompatibilitu */
.header { background: {{.ClubPrimaryColor}}; }
.button { background: {{.ClubSecondaryColor}}; }
</style>
</head>
<body>
<div class="header">
<img src="{{.ClubLogo}}" alt="Logo">
<h1>{{.ClubName}} Newsletter</h1>
</div>
<div class="content">
{{range .Articles}}
<div class="article">
<h2>{{.Title}}</h2>
<p>{{.Excerpt}}</p>
<a href="{{.URL}}" class="button">Číst více</a>
</div>
{{end}}
</div>
<!-- Tracking pixel -->
<img src="{{.TrackingPixelURL}}" width="1" height="1">
</body>
</html>
Email Tracking:
- Open Rate: 1x1 tracking pixel
- Click Through Rate: Wrapped links
- Unsubscribe: Token-based unsubscribe link
- Spam Reports: Report spam endpoint
// 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:
- Admin zadá Zonerama username
- Systém načte veřejná alba přes API
- Admin vybere alba ke zveřejnění
- Fotky se zobrazí v galerii na webu
// 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:
// 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 | 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 |
// Frontend form
<Select name="category">
<option value="general">Obecný dotaz</option>
<option value="support">Podpora</option>
<option value="media">Media</option>
</Select>
// 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
// 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:
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í
// 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 (
<ScoreboardControls
data={scoreData}
onUpdate={updateScore}
/>
);
};
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)
// 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í:
- Admin klikne "Generovat článek AI"
- Zadá téma: "Vítězství v derby 3:1"
- AI vygeneruje kompletní článek s titulkem a obsahem
- 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:
Authorization: Bearer <JWT_TOKEN>
Content Type:
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:
curl -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@club.cz",
"password": "securepassword"
}'
Response:
{
"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:
# 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
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:
- Admin zadá Zonerama album URL
- Backend parsuje HTML (Zonerama nemá veřejné REST API)
- Extrahuje metadata: title, cover, photo count
- Ukládá do DB s referencí na Zonerama ID
- Frontend zobrazuje pomocí Zonerama CDN URLs
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"
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:
- Frontend tracking:
// 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);
}, []);
};
- Backend API calls:
// 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í:
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
// 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:
{
"user_id": 1,
"email": "admin@club.cz",
"role": "admin",
"exp": 1735689600, // Expiration timestamp
"iat": 1735603200 // Issued at timestamp
}
Implementace:
// 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 <token>"
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í)
// 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.
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.
// 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.
// 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.
// 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
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.
// ✅ 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:
// 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 <div dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />;
}
8.6 File Upload Security
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
// Cookie consent banner
const CookieBanner = () => {
const [consent, setConsent] = useState<CookieConsent | null>(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 : <CookieConsentModal onAccept={handleAccept} />;
};
Anonymizace IP Adres
// 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
// 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:
.envsoubor s produkčními credentials- API klíče
- JWT secret
- SMTP hesla
Best practices:
# .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
git clone <repository-url>
cd fotbal-club
Krok 2: Konfigurace prostředí
# 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
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
- Otevřete http://localhost:3000
- Budete přesměrováni na setup wizard
- Vytvořte admin účet
- Nastavte klubové informace
- Vyberte klubové barvy
- Připojte FAČR klub
10.2 Manuální Instalace (Bez Dockeru)
Backend Setup:
# 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:
# 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í)
# ==========================================
# 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
# 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í:
# V .env souboru
RUN_MIGRATIONS=true
# Pak restart aplikace
docker-compose restart backend
# Nebo
make migrate
Manuální migrace (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)
# 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:
# 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:
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:
# 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:
# 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"
# 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
# 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
# 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
# Zkontrolovat oprávnění
chmod -R 755 ./uploads
# Zkontrolovat UPLOAD_DIR v .env
# Zkontrolovat MAX_UPLOAD_SIZE
5. JWT token expired příliš rychle
# 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ů
// 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:
// 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
// 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<ArticleStats>({
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 <Box>Načítání...</Box>;
}
if (error) {
return <Box>Chyba při načítání statistik</Box>;
}
return (
<Box bg="white" p={6} rounded="lg" shadow="md">
<SimpleGrid columns={{ base: 1, md: 2, lg: 4 }} spacing={6}>
<Stat>
<StatLabel>Celkem článků</StatLabel>
<StatNumber>{data?.total_articles}</StatNumber>
<StatHelpText>Všechny články v systému</StatHelpText>
</Stat>
<Stat>
<StatLabel>Publikované</StatLabel>
<StatNumber>{data?.published_articles}</StatNumber>
<StatHelpText>
{((data?.published_articles / data?.total_articles) * 100).toFixed(1)}% z celku
</StatHelpText>
</Stat>
<Stat>
<StatLabel>Celková zobrazení</StatLabel>
<StatNumber>{data?.total_views.toLocaleString()}</StatNumber>
<StatHelpText>Všechny články dohromady</StatHelpText>
</Stat>
<Stat>
<StatLabel>Průměrná zobrazení</StatLabel>
<StatNumber>{data?.average_views.toFixed(1)}</StatNumber>
<StatHelpText>Per článek</StatHelpText>
</Stat>
</SimpleGrid>
{data?.most_viewed_article && (
<Box mt={4} p={3} bg="gray.50" rounded="md">
<strong>Nejčtenější článek:</strong> {data.most_viewed_article.title}
<br />
<small>{data.most_viewed_article.view_count} zobrazení</small>
</Box>
)}
</Box>
);
};
export default ArticleStatsWidget;
11.3 Použití Custom Hooks
Příklad: useArticles hook
// 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<Article[]>({
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ě:
// 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 <div>Načítání...</div>;
return (
<div>
{articles?.map((article) => (
<div key={article.id}>
<h3>{article.title}</h3>
<button onClick={() => handleDelete(article.id)}>Smazat</button>
</div>
))}
</div>
);
};
11.4 Background Job - Newsletter Automation
Příklad: Automatické odesílání newsletteru
// 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:
// 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 (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Název článku"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
required
/>
<textarea
placeholder="Obsah článku"
value={formData.content}
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
required
/>
<button type="submit" disabled={createMutation.isPending}>
{createMutation.isPending ? 'Ukládání...' : 'Vytvořit článek'}
</button>
</form>
);
};
2. Backend - API handler:
// 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:
// 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í:
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:
-
"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
-
"Designing Data-Intensive Applications"
- Autor: Martin Kleppmann
- Vydavatel: O'Reilly Media
- ISBN: 978-1449373320
- Použito pro: Databázový design a škálovatelnost
-
"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