Files
MyClub/DOCS/DOKUMENTACE.md
Tomáš Dvořák 12cba639b9 upload
2025-10-16 13:32:05 +02:00

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

  1. Úvod a Motivace
  2. Přehled Projektu
  3. Technologický Stack
  4. Architektura Systému
  5. Struktura Projektu
  6. Klíčové Funkce
  7. API a Integrace
  8. Bezpečnost
  9. Minimální Požadavky
  10. Instalace a Konfigurace
  11. Příklady Kódu
  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

// 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:

  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:

// 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:

  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

// 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ů:

  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:

// 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:

  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
// 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 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
// 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í:

  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:

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

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
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:

  1. 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);
  }, []);
};
  1. 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:

  • .env soubor 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

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:

# 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:

Frontend:

DevOps:

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:

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:

OpenRouter AI:

Google Maps Embed API:

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:

12.5.7 Bezpečnostní Standardy a Specifikace

12.5.8 Inspirace a Podobné Projekty

12.5.9 Community a Support

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