# Kompletní Technická Dokumentace - Systém pro Správu Fotbalového Klubu **Autor:** Tomáš Dvořák **Verze:** 1.0 **Datum:** Říjen 2025 **Účel:** Maturitní projekt - Prezentace komplexního webového systému pro správu fotbalového klubu --- ## 📋 Obsah 1. [Úvod a Motivace](#1-úvod-a-motivace) 2. [Přehled Projektu](#2-přehled-projektu) 3. [Technologický Stack](#3-technologický-stack) 4. [Architektura Systému](#4-architektura-systému) 5. [Struktura Projektu](#5-struktura-projektu) 6. [Klíčové Funkce](#6-klíčové-funkce) 7. [API a Integrace](#7-api-a-integrace) 8. [Bezpečnost](#8-bezpečnost) 9. [Minimální Požadavky](#9-minimální-požadavky) 10. [Instalace a Konfigurace](#10-instalace-a-konfigurace) 11. [Příklady Kódu](#11-příklady-kódu) 12. [Závěr](#12-závěr) --- ## 1. Úvod a Motivace ### 1.1 Problematika Moderní sportovní kluby, zejména fotbalové, čelí výzvě efektivní správy obsahu, komunikace s fanoušky a prezentace klubových aktivit. Mnoho klubů, zejména na amatérské úrovni, používá zastaralé nebo rozdrobené systémy, které neumožňují komplexní správu všech aspektů klubového života. ### 1.2 Cíl Projektu Vytvořit **moderní, plně funkční webový systém** pro správu fotbalového klubu, který: - **Centralizuje** všechny informace o klubu na jednom místě - **Automatizuje** získávání výsledků a tabulek z oficiálních zdrojů (FAČR) - **Usnadňuje** komunikaci s fanoušky prostřednictvím newsletteru a kontaktních formulářů - **Poskytuje** intuitivní administrační rozhraní pro správu obsahu - **Zajišťuje** bezpečnost a škálovatelnost pomocí moderních technologií - **Respektuje** legislativu (GDPR, cookies consent) ### 1.3 Klíčové Požadavky - **Responsivní design** - plná funkčnost na mobilních zařízeních - **Real-time aktualizace** - živé skóre zápasů, okamžitá synchronizace dat - **Multimédia** - galerie, videa z YouTube, klubové loga - **Analytics** - sledování návštěvnosti a chování uživatelů - **Personalizace** - přizpůsobení barev a vzhledu dle klubové identity - **Modulární architektura** - snadná rozšiřitelnost o nové funkce --- ## 2. Přehled Projektu ### 2.1 Popis Systému **Fotbal Club** je full-stack webová aplikace určená pro kompletní správu fotbalového klubu. Systém kombinuje veřejnou část pro fanoušky a návštěvníky s pokročilým administrátorským rozhraním pro správu obsahu. ### 2.2 Hlavní Komponenty ``` ┌─────────────────────────────────────────────────────────────┐ │ FRONTEND (React SPA) │ │ • Veřejný web • Admin dashboard • Setup wizard │ └──────────────────────────┬──────────────────────────────────┘ │ REST API (JSON) ┌──────────────────────────┴──────────────────────────────────┐ │ BACKEND (Go + Gin) │ │ • API Controllers • Auth/JWT • Business Logic │ └──────────────────────────┬──────────────────────────────────┘ │ GORM ┌──────────────────────────┴──────────────────────────────────┐ │ DATABASE (PostgreSQL) │ │ • Články • Týmy • Zápasy • Uživatelé • Settings │ └─────────────────────────────────────────────────────────────┘ ``` ### 2.3 Funkční Oblasti | Oblast | Funkce | |--------|--------| | **Obsah** | Blog/články, o klubu, galerie, videa | | **Sport** | Zápasy, tabulky, hráči, týmy, výsledky | | **Komunikace** | Kontaktní formulář, newsletter, notifikace | | **Marketing** | Sponzoři, bannery, SEO optimalizace | | **Správa** | Uživatelé, role, nastavení, analytika | | **Integrace** | FAČR API, YouTube, Zonerama, Umami Analytics | --- ## 3. Technologický Stack ### 3.1 Backend Technologie #### **Programovací Jazyk: Go (Golang) 1.23+** **Odůvodnění volby:** - **Vysoký výkon** - kompilovaný jazyk s nízkou latencí - **Concurrency** - nativní podpora paralelního zpracování (goroutines) - **Typ bezpečnost** - silně typovaný jazyk eliminuje mnoho chyb - **Jednoduchost** - čistá syntax, snadná údržba - **Ekosystém** - bohaté knihovny pro web development #### **Web Framework: Gin** ```go // Příklad Gin routeru func main() { r := gin.Default() // Middleware r.Use(securityHeaders()) r.Use(corsMiddleware()) // Routy api := r.Group("/api/v1") { api.GET("/articles", controllers.GetArticles) api.POST("/articles", middleware.JWTAuth(), controllers.CreateArticle) } r.Run(":8080") } ``` **Vlastnosti:** - Rychlost (40x rychlejší než Martini) - HTTP/2 podpora - Middleware systém - JSON validace - Error management #### **ORM: GORM** ```go // Definice modelu type Article struct { ID uint `gorm:"primarykey" json:"id"` Title string `gorm:"size:255;not null" json:"title"` Content string `gorm:"type:text" json:"content"` Published bool `gorm:"default:false" json:"published"` PublishedAt *time.Time `json:"published_at"` CategoryID uint `json:"category_id"` Category Category `gorm:"foreignKey:CategoryID" json:"category"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } ``` **Výhody:** - Auto-migrace databázových schémat - Vztahy mezi tabulkami (has-one, has-many, many-to-many) - Hooks (before/after create, update, delete) - Transaction management - Query builder s bezpečnými parametry #### **Databáze: PostgreSQL 15** **Klíčové vlastnosti:** - ACID compliance - Pokročilé indexování - Full-text search - JSON/JSONB podpora - Replikace a high availability #### **Další Backend Knihovny** | Knihovna | Účel | |----------|------| | `golang-jwt/jwt` | JWT token generování a validace | | `golang.org/x/crypto` | Bcrypt hashing hesel | | `gopkg.in/mail.v2` | SMTP odesílání emailů | | `joho/godotenv` | Správa environment variables | | `PuerkitoBio/goquery` | HTML parsing (web scraping FAČR) | | `vanng822/go-premailer` | Inline CSS pro HTML emaily | ### 3.2 Frontend Technologie #### **Framework: React 18+** **Architektura:** - **Single Page Application (SPA)** - rychlé přechody bez reload - **Funkcionální komponenty** - hooks-based přístup - **TypeScript** - typová bezpečnost na frontendu #### **UI Knihovna: Chakra UI** ```tsx // Příklad Chakra UI komponenty import { Box, Button, Heading, Stack } from '@chakra-ui/react'; function ArticleCard({ article }) { return ( {article.title} ); } ``` **Výhody:** - Přístupnost (a11y) out-of-the-box - Responzivní props systém - Theming a customizace - Dark mode podpora - Komponenty již vyzkoušené v produkci #### **State Management** | Nástroj | Použití | |---------|---------| | **React Query** | Server state, cache, synchronizace | | **React Context** | Globální state (auth, theme) | | **Local State** | Lokální stav komponent | ```tsx // React Query příklad import { useQuery } from '@tanstack/react-query'; function ArticlesList() { const { data, isLoading, error } = useQuery({ queryKey: ['articles'], queryFn: () => api.get('/articles'), staleTime: 5 * 60 * 1000, // 5 minut cache }); if (isLoading) return ; if (error) return {error.message}; return ; } ``` #### **Routing: React Router v6** - Deklarativní routing - Protected routes s autorizací - Lazy loading stránek - Nested routes ### 3.3 DevOps a Nástroje #### **Kontejnerizace: Docker** ```dockerfile # Multi-stage build pro optimalizaci FROM golang:1.23-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o main . FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --from=builder /app/main . EXPOSE 8080 CMD ["./main"] ``` **Výhody:** - Izolované prostředí - Reprodukovatelné buildy - Snadné nasazení - Konzistentní development/production #### **Orchestrace: Docker Compose** - Současné spuštění backend + frontend + databáze - Automatické volume mapování - Health checks - Network izolace ### 3.4 Externí Služby a API | Služba | Účel | Integrace | Poznámka | |--------|------|-----------|----------| | **FAČR API*** | Oficiální výsledky a tabulky českého fotbalu | REST API scraping | *Custom wrapper vytvořen pro tento projekt | | **YouTube Data API v3** | Klubová videa | iframe embed + data API | [Docs](https://developers.google.com/youtube/v3) | | **Zonerama*** | Fotogalerie | HTML scraping | *Custom scraper vytvořen (Zonerama nemá veřejné API) | | **Umami Analytics** | Web analytics (privacy-first) | JavaScript tracking SDK + REST API | Self-hosted, [Docs](https://umami.is/docs) | | **OpenRouter AI** | AI generování článků | REST API (GPT modely) | [Docs](https://openrouter.ai/docs) | | **Google Maps Embed** | Lokace klubu | Maps Embed API | [Docs](https://developers.google.com/maps/documentation/embed) | **Poznámka:** API označená hvězdičkou (*) byly vytvořeny specificky pro tento projekt jako custom wrappery/scrapery, protože oficiální veřejné API není dostupné nebo je omezené. --- ## 4. Architektura Systému ### 4.1 Celková Architektura Aplikace využívá **třívrstvou architekturu** s jasným oddělením zodpovědností: ``` ┌─────────────────────────────────────────────────────────────────┐ │ PREZENTAČNÍ VRSTVA │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Veřejný │ │ Admin │ │ Setup │ │ │ │ Web │ │ Dashboard │ │ Wizard │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ React Components + Chakra UI + React Router │ └────────────────────────────┬────────────────────────────────────┘ │ REST API (JSON) JWT Authentication │ ┌────────────────────────────┴────────────────────────────────────┐ │ APLIKAČNÍ VRSTVA │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Controllers │ │ Middleware │ │ Services │ │ │ │ (Handlers) │ │ Auth/CORS │ │ (Business) │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ Gin Framework + GORM + Custom Logic │ └────────────────────────────┬────────────────────────────────────┘ │ ORM (GORM) SQL Queries │ ┌────────────────────────────┴────────────────────────────────────┐ │ DATOVÁ VRSTVA │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ PostgreSQL │ │ Cache │ │ Uploads │ │ │ │ Database │ │ (JSON) │ │ (Files) │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` ### 4.2 Backend Architektura (Go) #### **Vrstvová struktura:** 1. **Controllers (Ovladače)** - Zpracování HTTP požadavků - Validace vstupních dat - Volání business logiky - Formátování odpovědí 2. **Middleware** - Autentizace (JWT) - Autorizace (role-based) - Rate limiting - CORS - Security headers 3. **Services (Služby)** - Business logika - Komplexní operace - Integrace s externími API - Background joby 4. **Models (Modely)** - Datové struktury - Databázové schéma - Vztahy mezi tabulkami #### **Příklad Request Flow:** ``` 1. HTTP Request → Gin Router 2. Router → Middleware Stack ├─ CORS Middleware ├─ Security Headers ├─ JWT Authentication └─ Rate Limiting 3. Middleware → Controller 4. Controller → Service (Business Logic) 5. Service → GORM (Database) 6. GORM → PostgreSQL 7. Response: PostgreSQL → GORM → Service → Controller → JSON ``` ### 4.3 Frontend Architektura (React) #### **Komponentová architektura:** ``` src/ ├── components/ # Znovupoužitelné komponenty │ ├── common/ # Obecné UI prvky (Button, Card, etc.) │ ├── admin/ # Admin-specifické komponenty │ ├── layout/ # Layout komponenty (Header, Footer) │ └── widgets/ # Složitější widgety ├── pages/ # Stránky (route komponenty) │ ├── admin/ # Admin stránky │ └── legal/ # Právní stránky ├── contexts/ # React Context (global state) ├── hooks/ # Custom React hooks ├── services/ # API klienti └── layouts/ # Layout wrappery ``` #### **State Management Pattern:** ```tsx // 1. Server State (React Query) const { data, isLoading } = useQuery({ queryKey: ['articles'], queryFn: fetchArticles, }); // 2. Global State (React Context) const { user, isAuthenticated } = useAuth(); // 3. Local State (useState) const [isOpen, setIsOpen] = useState(false); ``` ### 4.4 Datový Model #### **Základní Entity:** ```sql -- Uživatelé a autentizace users (id, email, password_hash, role, created_at) password_resets (id, user_id, token, expires_at) -- Obsah articles (id, title, content, published, category_id, created_at) categories (id, name, slug, color) -- Sport teams (id, name, age_category, division) players (id, name, position, number, team_id) matches (external_id, home_team, away_team, score, date) match_overrides (external_match_id, home_name, away_name, home_logo, away_logo) -- Galerie gallery_albums (id, title, cover_url, photo_count, zonerama_id) -- Komunikace contact_messages (id, name, email, subject, message, status, read_at) newsletter_subscriptions (id, email, status, preferences, subscribed_at) -- Konfigurace settings (id, club_name, club_colors, smtp_config, newsletter_enabled) competition_aliases (code, display_name, priority, hidden) -- Analytics visitor_events (id, event_type, page_url, user_agent, ip_hash, created_at) ``` #### **Vztahy:** ``` Article N:1 Category Article N:1 User (author) Player N:1 Team Match N:1 Competition NewsletterSubscription 1:N EmailLog ContactMessage 1:1 ContactCategory ``` ### 4.5 API Design Pattern Aplikace využívá **RESTful API** s konzistentním názvoslovím: | Metoda | Endpoint | Popis | Auth | |--------|----------|-------|------| | GET | `/api/v1/articles` | Seznam článků | Ne | | GET | `/api/v1/articles/:id` | Detail článku | Ne | | POST | `/api/v1/articles` | Vytvoření článku | Ano (JWT) | | PUT | `/api/v1/articles/:id` | Aktualizace článku | Ano (JWT) | | DELETE | `/api/v1/articles/:id` | Smazání článku | Ano (JWT + Admin) | **Response formát:** ```json // Úspěch { "success": true, "data": { "id": 1, "title": "Název článku" }, "message": "Article created successfully" } // Chyba { "success": false, "error": "Validation failed", "details": { "title": "Title is required" } } ``` --- ## 5. Struktura Projektu ### 5.1 Backend Struktura ``` fotbal-club/ ├── main.go # Vstupní bod aplikace ├── go.mod # Go dependencies ├── go.sum # Dependency checksums ├── .env # Environment variables ├── Dockerfile # Production build ├── Dockerfile.dev # Development build ├── docker-compose.yml # Multi-container orchestrace ├── Makefile # Build & run skripty │ ├── internal/ # Privátní aplikační kód │ ├── config/ # Konfigurace │ │ └── config.go # Načítání ENV variables │ │ │ ├── controllers/ # HTTP handlery │ │ ├── base_controller.go # CRUD operace │ │ ├── auth_controller.go # Autentizace │ │ ├── contact_controller.go # Kontaktní formulář │ │ ├── facr_controller.go # FAČR integrace │ │ ├── gallery_controller.go # Galerie │ │ ├── ai_controller.go # AI generování │ │ ├── analytics_controller.go # Analytika │ │ └── ... │ │ │ ├── middleware/ # Middleware funkce │ │ ├── auth.go # JWT ověření │ │ ├── admin.go # Role kontrola │ │ └── ratelimit.go # Rate limiting │ │ │ ├── models/ # Databázové modely │ │ ├── models.go # Společné modely (User, Article, etc.) │ │ ├── contact.go # Kontaktní modely │ │ ├── scoreboard.go # Scoreboard state │ │ └── ... │ │ │ ├── routes/ # Route definice │ │ ├── routes.go # Hlavní router │ │ ├── analytics_routes.go │ │ └── ... │ │ │ └── services/ # Business logika │ ├── prefetch.go # Background cache refresh │ ├── newsletter_automation.go # Auto newslettery │ └── gallery_service.go │ ├── pkg/ # Znovupoužitelné balíčky │ ├── database/ # DB utilities │ │ ├── db.go # Inicializace │ │ ├── migrate.go # Migrace │ │ └── seed.go # Seed data │ ├── email/ # Email service │ │ └── email.go │ ├── logger/ # Logging │ └── utils/ # Helper funkce │ ├── database/ # SQL migrace │ └── migrations/ │ ├── templates/ # HTML šablony pro emaily │ └── emails/ │ ├── newsletter.html │ ├── contact_confirm.html │ └── password_reset.html │ ├── static/ # Statické soubory │ └── dist/ │ ├── uploads/ # Nahrané soubory │ ├── articles/ │ ├── sponsors/ │ └── players/ │ └── cache/ # JSON cache soubory ├── articles.json ├── matches.json └── standings.json ``` ### 5.2 Frontend Struktura ``` frontend/ ├── public/ # Statické public soubory │ ├── index.html │ ├── favicon.ico │ └── manifest.json │ ├── src/ │ ├── index.tsx # Vstupní bod React app │ ├── App.tsx # Hlavní komponenta s routingem │ ├── config.ts # Frontend konfigurace │ │ │ ├── components/ # React komponenty │ │ ├── common/ # Sdílené komponenty │ │ │ ├── Button.tsx │ │ │ ├── Card.tsx │ │ │ └── Modal.tsx │ │ ├── admin/ # Admin komponenty │ │ │ ├── ArticleForm.tsx │ │ │ ├── UserManager.tsx │ │ │ └── ... │ │ ├── layout/ # Layout komponenty │ │ │ ├── Navbar.tsx │ │ │ ├── Footer.tsx │ │ │ └── Sidebar.tsx │ │ ├── seo/ # SEO komponenty │ │ │ └── DefaultSEO.tsx │ │ └── widgets/ # Specializované widgety │ │ ├── MatchCard.tsx │ │ ├── ArticleGrid.tsx │ │ └── ... │ │ │ ├── pages/ # Stránkové komponenty │ │ ├── HomePage.tsx │ │ ├── BlogPage.tsx │ │ ├── ContactPage.tsx │ │ ├── admin/ │ │ │ ├── AdminDashboardPage.tsx │ │ │ ├── ArticlesAdminPage.tsx │ │ │ └── ... │ │ └── legal/ │ │ ├── PrivacyPolicyPage.tsx │ │ └── TermsPage.tsx │ │ │ ├── contexts/ # React Context providers │ │ ├── AuthContext.tsx # Autentizace state │ │ └── ClubThemeContext.tsx # Klubové barvy │ │ │ ├── hooks/ # Custom React hooks │ │ ├── useAuth.ts │ │ ├── useFacrApi.ts │ │ ├── useSettings.ts │ │ └── useUmami.ts # Analytics tracking │ │ │ ├── services/ # API klienti │ │ ├── api.ts # Axios instance │ │ ├── auth.service.ts │ │ ├── articles.service.ts │ │ └── ... │ │ │ ├── layouts/ # Layout wrappery │ │ ├── AdminLayout.tsx │ │ └── PublicLayout.tsx │ │ │ ├── assets/ # Obrázky, fonty, styly │ │ ├── images/ │ │ ├── fonts/ │ │ └── styles/ │ │ │ └── types/ # TypeScript typy │ └── index.d.ts │ ├── package.json # NPM dependencies ├── tsconfig.json # TypeScript konfigurace └── .env # Environment variables ``` ### 5.3 Klíčové Konfigurační Soubory #### **Backend: `.env`** ```bash # Application APP_ENV=development PORT=8080 DEBUG=true # Database DATABASE_URL=postgres://user:pass@localhost:5432/fotbal_club # JWT JWT_SECRET=your_secret_key_here JWT_EXPIRATION_HOURS=24 # Email (SMTP) SMTP_HOST=smtp.example.com SMTP_PORT=587 SMTP_USER=your_email@example.com SMTP_PASSWORD=password # File Uploads UPLOAD_DIR=./uploads MAX_UPLOAD_SIZE=10485760 # 10MB # External APIs OPENROUTER_API_KEY=sk-or-v1-... UMAMI_URL=https://analytics.example.com ``` #### **Frontend: `.env`** ```bash REACT_APP_API_URL=http://localhost:8080/api/v1 REACT_APP_FACR_API_BASE_URL=http://localhost:8080/api/v1/facr REACT_APP_HOMEPAGE_LAYOUT=classic ``` --- ## 6. Klíčové Funkce ### 6.1 Systém Článků a Blogu **Popis:** Komplexní systém pro správu klubového obsahu s podporou kategorií, náhledů, publikování a SEO. #### **Hlavní Vlastnosti:** - ✅ Rich text editor s podporou obrázků a YouTube videí - ✅ Kategorizace článků (zápasy, události, novinky) - ✅ Plánované publikování (draft → published) - ✅ Featured články na homepage - ✅ Automatické generování slugů pro SEO-friendly URL - ✅ Sledování počtu přečtení - ✅ Propojení článku s konkrétním zápasem #### **Technická Implementace:** ```go // Backend Model type Article struct { ID uint `gorm:"primarykey" json:"id"` Title string `gorm:"size:255;not null" json:"title"` Slug string `gorm:"size:255;uniqueIndex" json:"slug"` Excerpt string `gorm:"type:text" json:"excerpt"` Content string `gorm:"type:text" json:"content"` FeaturedImage string `json:"featured_image"` Published bool `gorm:"default:false" json:"published"` PublishedAt *time.Time `json:"published_at"` ViewCount int `gorm:"default:0" json:"view_count"` CategoryID uint `json:"category_id"` Category Category `gorm:"foreignKey:CategoryID" json:"category"` AuthorID uint `json:"author_id"` Author User `gorm:"foreignKey:AuthorID" json:"author"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } ``` **Frontend Flow:** 1. Admin vytvoří článek přes formulář s WYSIWYG editorem 2. Nahraje featured obrázek přes drag-and-drop 3. Vybere kategorii a nastaví publikování 4. Článek se zobrazí na blogu a homepage (pokud je featured) ### 6.2 FAČR Integrace - Automatické Výsledky **Popis:** Systém automaticky stahuje a zobrazuje výsledky, rozpisy a tabulky z oficiálního webu Fotbalové asociace České republiky. #### **Jak to funguje:** ``` ┌─────────────┐ │ Frontend │ │ Request │ ───────┐ └─────────────┘ │ ▼ ┌─────────────────┐ │ Backend Proxy │ │ (FACR Ctrl) │ └─────────────────┘ │ ▼ ┌─────────────────┐ │ Web Scraping │ │ (goquery HTML) │ └─────────────────┘ │ ▼ ┌─────────────────┐ │ facr.fotbal.cz │ │ (Official API) │ └─────────────────┘ ``` #### **Klíčové Komponenty:** **1. Club Search** ```go // Vyhledání klubu podle názvu GET /api/v1/facr/club/search?q=Sokol Response: [ { "id": "123", "name": "TJ Sokol Praha", "type": "klub" } ] ``` **2. Match Schedule** ```go // Získání rozpisů zápasů GET /api/v1/facr/club/klub/123 Response: { "matches": [ { "date": "2025-10-15T15:00:00Z", "home_team": "TJ Sokol Praha", "away_team": "FC Sparta B", "competition": "III. třída - sk. A" } ] } ``` **3. Competition Tables** ```go // Tabulky soutěží GET /api/v1/facr/club/klub/123/table Response: { "tables": [ { "competition": "III. třída - sk. A", "rows": [ { "position": 1, "team": "TJ Sokol Praha", "played": 10, "wins": 8, "points": 24 } ] } ] } ``` #### **Cache Systém:** - **Interval:** Každých 30 minut automatický refresh - **Storage:** JSON soubory v `./cache/` adresáři - **Důvod:** Minimalizace zátěže na FAČR server + rychlé načítání ```go // Prefetch service func StartPrefetcher(targetURL string) { ticker := time.NewTicker(30 * time.Minute) go func() { for range ticker.C { fetchAndCacheEndpoint(targetURL + "/matches") fetchAndCacheEndpoint(targetURL + "/standings") } }() } ``` #### **Aliasy Soutěží:** Administrátor může upravit názvy soutěží pro lepší zobrazení: | Originální název | Alias | Priorita | |------------------|-------|----------| | III. třída - skupina A | Třetí třída A | 1 | | Přebor dorostu U19 | Dorost U19 | 2 | ### 6.3 Newsletter a Email Marketing **Popis:** Plnohodnotný newsletter systém s automatickými digesty, segmentací odběratelů a sledováním metrik. #### **Typy Newsletterů:** 1. **Týdenní Digest** - Automaticky každý pátek v 18:00 - Obsahuje nové články z týdne - Nadcházející zápasy 2. **Match Alerts** - 24h před zápasem - Info o soupeři, místě konání - Odkaz na mapy 3. **Blog Notifications** - Při publikování nového článku - Pouze pro odběratele "blog" kategorie 4. **Results Digest** - Po víkendu s výsledky - Aktuální tabulka #### **Technická Implementace:** ```go // Newsletter Automation Service type NewsletterAutomation struct { db *gorm.DB emailService *email.EmailService ticker *time.Ticker } func (na *NewsletterAutomation) Start() { // Denní kontrola v 18:00 na.ticker = time.NewTicker(1 * time.Hour) go func() { for range na.ticker.C { na.checkAndSendScheduled() } }() } func (na *NewsletterAutomation) checkAndSendScheduled() { now := time.Now() // Týdenní digest (pátek 18:00) if now.Weekday() == time.Friday && now.Hour() == 18 { na.sendWeeklyDigest() } // Match alerts (24h před zápasem) upcomingMatches := na.getMatchesIn24Hours() for _, match := range upcomingMatches { na.sendMatchAlert(match) } } ``` #### **Email Templates:** ```html
Logo

{{.ClubName}} Newsletter

{{range .Articles}}

{{.Title}}

{{.Excerpt}}

Číst více
{{end}}
``` #### **Email Tracking:** - **Open Rate:** 1x1 tracking pixel - **Click Through Rate:** Wrapped links - **Unsubscribe:** Token-based unsubscribe link - **Spam Reports:** Report spam endpoint ```go // Email metrics type EmailLog struct { ID uint `json:"id"` RecipientEmail string `json:"recipient_email"` Subject string `json:"subject"` Type string `json:"type"` // weekly, match, blog SentAt time.Time `json:"sent_at"` OpenedAt *time.Time `json:"opened_at"` ClickedAt *time.Time `json:"clicked_at"` UnsubscribedAt *time.Time `json:"unsubscribed_at"` } ``` ### 6.4 Galerie a Multimédia #### **Zonerama Integrace:** Systém automaticky synchronizuje alba z populární fotogalerie Zonerama.cz. **Workflow:** 1. Admin zadá Zonerama username 2. Systém načte veřejná alba přes API 3. Admin vybere alba ke zveřejnění 4. Fotky se zobrazí v galerii na webu ```typescript // Frontend API call const fetchAlbum = async (albumUrl: string) => { const response = await api.post('/admin/gallery/albums/fetch', { album_url: albumUrl }); return response.data; }; // Backend scraping func (gc *GalleryController) FetchAlbum(c *gin.Context) { albumURL := c.PostForm("album_url") // Parse Zonerama album album := scrapeZoneramaAlbum(albumURL) // Save to database gc.DB.Create(&album) c.JSON(200, album) } ``` #### **YouTube Video Manager:** ```typescript // Cached YouTube videos from club channel interface YouTubeVideo { id: string; title: string; thumbnail: string; publishedAt: string; duration: string; viewCount: number; } // Backend cache refresh (každých 6 hodin) func refreshYouTubeCache() { videos := fetchFromYouTubeAPI(channelID) saveToCache("youtube_videos.json", videos) } ``` ### 6.5 Kontaktní Systém #### **Multi-Category Contact Form:** Uživatelé mohou vybrat kategorii dotazu pro efektivní routing: | Kategorie | Email | Auto-odpověď | |-----------|-------|--------------| | Obecný dotaz | info@club.cz | Ano | | Technická podpora | tech@club.cz | Ano | | Tisk a media | pr@club.cz | Ne | | Reklama | marketing@club.cz | Ne | ```typescript // Frontend form // Backend handler func (cc *ContactController) SubmitContactForm(c *gin.Context) { var req ContactRequest c.BindJSON(&req) // Save to database message := models.ContactMessage{ Name: req.Name, Email: req.Email, Category: req.Category, Message: req.Message, Status: "new", } cc.DB.Create(&message) // Send notification to admin cc.emailService.SendContactNotification(message) // Send auto-reply to user if shouldSendAutoReply(req.Category) { cc.emailService.SendAutoReply(req.Email) } } ``` ### 6.6 Analytics a Statistiky #### **Umami Analytics Integrace:** Privacy-first analytický nástroj (alternativa k Google Analytics). **Sledované metriky:** - Page views - Unique visitors - Bounce rate - Average time on page - Traffic sources - Device breakdown ```typescript // Frontend tracking hook export const useUmami = () => { const trackEvent = (eventName: string, eventData?: object) => { if (window.umami) { window.umami.track(eventName, eventData); } }; return { trackEvent }; }; // Usage const { trackEvent } = useUmami(); trackEvent('article_read', { article_id: 123 }); ``` #### **Custom Analytics:** Kromě Umami systém uchovává vlastní statistiky: ```go type VisitorEvent struct { ID uint `json:"id"` EventType string `json:"event_type"` // page_view, article_read, etc. PageURL string `json:"page_url"` UserAgent string `json:"user_agent"` IPHash string `json:"ip_hash"` // Anonymizovaná IP (GDPR) CreatedAt time.Time `json:"created_at"` } // Article popularity type ArticleStats struct { ArticleID uint `json:"article_id"` ViewCount int `json:"view_count"` ReadTime int `json:"avg_read_time_seconds"` } ``` ### 6.7 Live Scoreboard **Popis:** Real-time skóre display pro živé přenosy zápasů. #### **Funkce:** - ⏱️ **Countdown timer** s přesností na sekundy - 🔄 **Swap sides** - prohození stran - ⚽ **Live score updates** - aktualizace skóre - 📊 **Statistics** - střely, rohy, žluté karty - 🎨 **Color derivation** - automatické barvy z klubových log - 💾 **Presets** - uložení často používaných nastavení ```typescript // Frontend scoreboard control const ScoreboardControl = () => { const [scoreData, setScoreData] = useState({ homeTeam: '', awayTeam: '', homeScore: 0, awayScore: 0, timer: 0, isRunning: false, }); const updateScore = async (team: 'home' | 'away', increment: number) => { await api.put('/admin/scoreboard', { [`${team}_score`]: scoreData[`${team}Score`] + increment }); }; return ( ); }; ``` ### 6.8 AI Content Generation **Popis:** Automatické generování článků a obsahu pomocí AI (OpenRouter API). #### **Podporované Modely:** - Mistral Small (primary) - Mistral Nemo (fallback) - GPT-4 Turbo (premium) ```go // AI Blog Generation func (ai *AIController) GenerateBlog(c *gin.Context) { var req struct { Topic string `json:"topic"` Keywords []string `json:"keywords"` Tone string `json:"tone"` // professional, casual, excited Length int `json:"length"` // word count } c.BindJSON(&req) prompt := buildBlogPrompt(req) // Call OpenRouter API article := ai.openRouterClient.Generate(prompt, req.Length) c.JSON(200, gin.H{ "title": article.Title, "content": article.Content, "excerpt": article.Excerpt, }) } ``` **Příklad použití:** 1. Admin klikne "Generovat článek AI" 2. Zadá téma: "Vítězství v derby 3:1" 3. AI vygeneruje kompletní článek s titulkem a obsahem 4. Admin upraví detaily a publikuje --- ## 7. API a Integrace ### 7.1 REST API Přehled Aplikace poskytuje kompletní RESTful API pro všechny operace. #### **Base URL:** ``` http://localhost:8080/api/v1 ``` #### **Autentizace:** ```http Authorization: Bearer ``` #### **Content Type:** ```http Content-Type: application/json ``` ### 7.2 API Endpointy - Kategorie #### **🔐 Autentizace** | Metoda | Endpoint | Popis | Auth | |--------|----------|-------|------| | POST | `/auth/login` | Přihlášení uživatele | ❌ | | POST | `/auth/register` | Registrace nového uživatele | ❌ | | POST | `/auth/logout` | Odhlášení | ❌ | | GET | `/auth/me` | Získání aktuálního uživatele | ✅ | | POST | `/auth/forgot-password` | Zapomenuté heslo | ❌ | | POST | `/auth/reset-password` | Reset hesla | ❌ | **Příklad Login Request:** ```bash curl -X POST http://localhost:8080/api/v1/auth/login \ -H "Content-Type: application/json" \ -d '{ "email": "admin@club.cz", "password": "securepassword" }' ``` **Response:** ```json { "success": true, "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "user": { "id": 1, "email": "admin@club.cz", "role": "admin", "created_at": "2025-01-01T10:00:00Z" } } ``` #### **📝 Články** | Metoda | Endpoint | Popis | Auth | |--------|----------|-------|------| | GET | `/articles` | Seznam článků | ❌ | | GET | `/articles/:id` | Detail článku | ❌ | | GET | `/articles/slug/:slug` | Článek podle slug | ❌ | | GET | `/articles/featured` | Featured články | ❌ | | POST | `/articles` | Vytvoření článku | ✅ | | PUT | `/articles/:id` | Aktualizace článku | ✅ | | DELETE | `/articles/:id` | Smazání článku | ✅ | | POST | `/articles/:id/read` | Increment počtu čtení | ❌ | **Query Parameters pro GET /articles:** ``` ?page=1 &limit=10 &category=1 &published=true &sort=created_at &order=desc &search=derby ``` #### **⚽ Zápasy a Tabulky** | Metoda | Endpoint | Popis | Auth | |--------|----------|-------|------| | GET | `/matches` | Seznam zápasů | ❌ | | GET | `/matches/history` | Historie zápasů | ❌ | | GET | `/standings` | Tabulky soutěží | ❌ | | GET | `/admin/matches` | Admin view zápasů | ✅ Admin | #### **🏆 FAČR API** | Metoda | Endpoint | Popis | Auth | |--------|----------|-------|------| | GET | `/facr/club/search?q={query}` | Vyhledání klubu | ❌ | | GET | `/facr/club/:type/:id` | Info o klubu | ❌ | | GET | `/facr/club/:type/:id/table` | Tabulky klubu | ❌ | **Příklad:** ```bash # Vyhledání klubu curl "http://localhost:8080/api/v1/facr/club/search?q=Sparta" # Získání informací curl "http://localhost:8080/api/v1/facr/club/klub/123" ``` #### **📧 Kontakt a Newsletter** | Metoda | Endpoint | Popis | Auth | |--------|----------|-------|------| | POST | `/contact` | Odeslat kontaktní formulář | ❌ | | POST | `/newsletter/subscribe` | Odběr newsletteru | ❌ | | POST | `/newsletter/unsubscribe/:email` | Zrušení odběru | ❌ | | GET | `/admin/newsletter/subscribers` | Seznam odběratelů | ✅ Admin | | POST | `/admin/newsletter/send` | Odeslat newsletter | ✅ Admin | #### **📸 Galerie** | Metoda | Endpoint | Popis | Auth | |--------|----------|-------|------| | GET | `/gallery/albums` | Seznam alb | ❌ | | GET | `/gallery/albums/:id` | Detail alba s fotkami | ❌ | | POST | `/admin/gallery/albums/fetch` | Načíst album ze Zonerama | ✅ Admin | | DELETE | `/admin/gallery/albums/:id` | Smazat album | ✅ Admin | #### **📊 Analytics** | Metoda | Endpoint | Popis | Auth | |--------|----------|-------|------| | POST | `/analytics/track` | Trackování eventu | ❌ | | GET | `/admin/umami/stats` | Umami statistiky | ✅ Admin | | GET | `/admin/umami/metrics/:type` | Metriky (pageviews, etc.) | ✅ Admin | #### **⚙️ Nastavení** | Metoda | Endpoint | Popis | Auth | |--------|----------|-------|------| | GET | `/settings` | Veřejná nastavení | ❌ | | GET | `/admin/settings` | Všechna nastavení | ✅ Admin | | PUT | `/admin/settings` | Aktualizace nastavení | ✅ Admin | ### 7.3 Externí API Integrace #### **FAČR Web Scraping** **Popis:** Backend funguje jako proxy a scraper pro oficiální FAČR web. **Technologie:** - `goquery` - HTML parsing - HTTP client s custom headers - Error handling a retry logika ```go func (fc *FACRController) SearchClubs(c *gin.Context) { query := c.Query("q") // Build search URL searchURL := fmt.Sprintf( "https://is.fotbal.cz/clubs/search?term=%s", url.QueryEscape(query), ) // Fetch HTML resp, err := http.Get(searchURL) if err != nil { c.JSON(500, gin.H{"error": "Failed to fetch"}) return } defer resp.Body.Close() // Parse HTML with goquery doc, _ := goquery.NewDocumentFromReader(resp.Body) var clubs []Club doc.Find(".club-item").Each(func(i int, s *goquery.Selection) { club := Club{ ID: s.AttrOr("data-id", ""), Name: s.Find(".club-name").Text(), Type: s.AttrOr("data-type", ""), } clubs = append(clubs, club) }) c.JSON(200, clubs) } ``` **Rate Limiting:** - Max 60 requests/hour na FAČR - Local cache pro 30 minut - Background prefetch pro populární endpointy #### **Zonerama Gallery API** **Dokumentace:** https://www.zonerama.com/ **Workflow:** 1. Admin zadá Zonerama album URL 2. Backend parsuje HTML (Zonerama nemá veřejné REST API) 3. Extrahuje metadata: title, cover, photo count 4. Ukládá do DB s referencí na Zonerama ID 5. Frontend zobrazuje pomocí Zonerama CDN URLs ```go func scrapeZoneramaAlbum(albumURL string) (*Album, error) { resp, err := http.Get(albumURL) if err != nil { return nil, err } defer resp.Body.Close() doc, _ := goquery.NewDocumentFromReader(resp.Body) album := &Album{ Title: doc.Find("h1.album-title").Text(), CoverURL: doc.Find(".album-cover img").AttrOr("src", ""), PhotoCount: len(doc.Find(".photo-item").Nodes), ZoneramaID: extractIDFromURL(albumURL), } return album, nil } ``` #### **YouTube Data API v3** **API Key:** Vyžadován v `.env` jako `YOUTUBE_API_KEY` **Endpoint:** ``` https://www.googleapis.com/youtube/v3/search ``` **Použití:** - Automatické načítání videí z klubového kanálu - Cache refresh každých 6 hodin - Zobrazení v sekci "Videa" ```go func fetchYouTubeVideos(channelID string) ([]Video, error) { apiKey := os.Getenv("YOUTUBE_API_KEY") url := fmt.Sprintf( "https://www.googleapis.com/youtube/v3/search?part=snippet&channelId=%s&maxResults=12&order=date&type=video&key=%s", channelID, apiKey, ) resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() var result YouTubeAPIResponse json.NewDecoder(resp.Body).Decode(&result) var videos []Video for _, item := range result.Items { videos = append(videos, Video{ ID: item.ID.VideoID, Title: item.Snippet.Title, Thumbnail: item.Snippet.Thumbnails.Medium.URL, Published: item.Snippet.PublishedAt, }) } return videos, nil } ``` #### **Umami Analytics** **Self-hosted:** https://umami.is/ **Integrace:** 1. **Frontend tracking:** ```typescript // useUmami hook export const useUmami = () => { useEffect(() => { const script = document.createElement('script'); script.src = process.env.REACT_APP_UMAMI_URL + '/script.js'; script.async = true; script.setAttribute('data-website-id', websiteId); document.head.appendChild(script); }, []); }; ``` 2. **Backend API calls:** ```go // Získání statistik func (uc *UmamiController) GetStats(c *gin.Context) { umamiURL := os.Getenv("UMAMI_URL") username := os.Getenv("UMAMI_USERNAME") password := os.Getenv("UMAMI_PASSWORD") // Login to Umami token := loginToUmami(umamiURL, username, password) // Fetch stats statsURL := fmt.Sprintf("%s/api/websites/%s/stats", umamiURL, websiteID) req, _ := http.NewRequest("GET", statsURL, nil) req.Header.Set("Authorization", "Bearer "+token) resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() var stats Stats json.NewDecoder(resp.Body).Decode(&stats) c.JSON(200, stats) } ``` #### **OpenRouter AI API** **Dokumentace:** https://openrouter.ai/docs **Modely:** - `mistralai/mistral-small-3.2-24b-instruct:free` (primary) - `mistralai/mistral-nemo:free` (fallback) - Custom GPT modely (placené) **Použití:** ```go type OpenRouterClient struct { apiKey string baseURL string } func (orc *OpenRouterClient) Generate(prompt string, maxTokens int) (*GeneratedContent, error) { reqBody := map[string]interface{}{ "model": "mistralai/mistral-small-3.2-24b-instruct:free", "messages": []map[string]string{ {"role": "system", "content": "You are a professional sports journalist."}, {"role": "user", "content": prompt}, }, "max_tokens": maxTokens, "temperature": 0.7, } jsonData, _ := json.Marshal(reqBody) req, _ := http.NewRequest("POST", orc.baseURL+"/chat/completions", bytes.NewBuffer(jsonData)) req.Header.Set("Authorization", "Bearer "+orc.apiKey) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var result OpenRouterResponse json.NewDecoder(resp.Body).Decode(&result) return &GeneratedContent{ Content: result.Choices[0].Message.Content, }, nil } ``` **Rate Limits:** - Free tier: 200 requests/day - Token limit: 100k tokens/day - Fallback na jiný model při překročení ### 7.4 WebSocket Support (Budoucí) **Plánované funkce:** - Real-time skóre updates - Live chat během zápasů - Admin notifications ```go // Připraveno pro rozšíření func setupWebSocket(r *gin.Engine) { r.GET("/ws", func(c *gin.Context) { upgrader.Upgrade(c.Writer, c.Request, nil) }) } ``` --- ## 8. Bezpečnost ### 8.1 Autentizace a Autorizace #### **JWT (JSON Web Tokens)** Aplikace používá JWT tokeny pro stateless autentizaci. **Token Structure:** ``` Header.Payload.Signature ``` **Payload obsahuje:** ```json { "user_id": 1, "email": "admin@club.cz", "role": "admin", "exp": 1735689600, // Expiration timestamp "iat": 1735603200 // Issued at timestamp } ``` **Implementace:** ```go // Token generování při přihlášení func generateJWT(user *models.User) (string, error) { claims := jwt.MapClaims{ "user_id": user.ID, "email": user.Email, "role": user.Role, "exp": time.Now().Add(24 * time.Hour).Unix(), "iat": time.Now().Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) secret := []byte(os.Getenv("JWT_SECRET")) return token.SignedString(secret) } // Middleware pro ověření tokenu func JWTAuth(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.JSON(401, gin.H{"error": "Authorization header missing"}) c.Abort() return } // Extract token from "Bearer " tokenString := strings.TrimPrefix(authHeader, "Bearer ") // Parse and validate token token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { return []byte(os.Getenv("JWT_SECRET")), nil }) if err != nil || !token.Valid { c.JSON(401, gin.H{"error": "Invalid token"}) c.Abort() return } claims := token.Claims.(jwt.MapClaims) userID := uint(claims["user_id"].(float64)) // Load user from database var user models.User if err := db.First(&user, userID).Error; err != nil { c.JSON(401, gin.H{"error": "User not found"}) c.Abort() return } // Store user in context c.Set("user", user) c.Set("userID", user.ID) c.Set("userRole", user.Role) c.Next() } } ``` #### **Role-Based Access Control (RBAC)** Systém podporuje dvě role: - **admin** - plný přístup ke všem funkcím - **editor** - omezený přístup (bez správy uživatelů a nastavení) ```go // Middleware pro kontrolu role func RoleAuth(requiredRole string) gin.HandlerFunc { return func(c *gin.Context) { userRole, exists := c.Get("userRole") if !exists { c.JSON(403, gin.H{"error": "Forbidden"}) c.Abort() return } if userRole.(string) != requiredRole { c.JSON(403, gin.H{"error": "Insufficient permissions"}) c.Abort() return } c.Next() } } // Použití admin := api.Group("/admin") admin.Use(middleware.JWTAuth(db)) admin.Use(middleware.RoleAuth("admin")) ``` #### **Password Hashing** Hesla jsou hashována pomocí bcrypt s cost faktorem 14. ```go import "golang.org/x/crypto/bcrypt" // Hashování hesla při registraci func hashPassword(password string) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) return string(bytes), err } // Ověření hesla při přihlášení func verifyPassword(hashedPassword, password string) error { return bcrypt.CompareHashAndPassword( []byte(hashedPassword), []byte(password), ) } ``` **Výhody bcrypt:** - Adaptive algorithm (cost lze zvýšit v budoucnu) - Built-in salt - Resistance proti rainbow table útokům - Slow by design (brání brute-force) ### 8.2 Security Headers Aplikace automaticky přidává bezpečnostní hlavičky ke každému HTTP response. ```go // Security headers middleware r.Use(func(c *gin.Context) { // Prevent MIME type sniffing c.Writer.Header().Set("X-Content-Type-Options", "nosniff") // Prevent clickjacking c.Writer.Header().Set("X-Frame-Options", "DENY") // Referrer policy c.Writer.Header().Set("Referrer-Policy", "no-referrer-when-downgrade") // HSTS (when using HTTPS) if c.Request.TLS != nil || c.Request.Header.Get("X-Forwarded-Proto") == "https" { c.Writer.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload") } // Content Security Policy csp := "default-src 'self'; " + "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://umami.tdvorak.dev; " + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data: https: blob:; " + "font-src 'self' data:; " + "connect-src 'self' https://umami.tdvorak.dev;" c.Writer.Header().Set("Content-Security-Policy", csp) c.Next() }) ``` **Vysvětlení jednotlivých headers:** | Header | Účel | Hodnota | |--------|------|---------| | `X-Content-Type-Options` | Zabraňuje MIME sniffing | `nosniff` | | `X-Frame-Options` | Ochrana proti clickjacking | `DENY` | | `Referrer-Policy` | Kontrola referrer informací | `no-referrer-when-downgrade` | | `Strict-Transport-Security` | Vynucení HTTPS | `max-age=31536000` | | `Content-Security-Policy` | Whitelist zdrojů (XSS ochrana) | Custom policy | ### 8.3 CORS (Cross-Origin Resource Sharing) CORS je nakonfigurován pro povolené origins z `.env` souboru. ```go // CORS middleware origin := c.Request.Header.Get("Origin") allowed := false for _, ao := range config.AppConfig.AllowedOrigins { if ao == origin { allowed = true break } } // Relaxed rule for development if !allowed && origin != "" && config.AppConfig.AppEnv != "production" { if strings.HasPrefix(origin, "http://localhost:") || strings.HasPrefix(origin, "http://127.0.0.1:") { allowed = true } } if allowed && origin != "" { c.Writer.Header().Set("Access-Control-Allow-Origin", origin) c.Writer.Header().Set("Vary", "Origin") c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") } c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, Cache-Control, X-Requested-With") // Handle preflight requests if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204) return } ``` ### 8.4 Rate Limiting Aplikace implementuje rate limiting pro ochranu proti abuse a DDoS útokům. ```go // Rate limiting middleware func RateLimit(maxRequests int, window time.Duration) gin.HandlerFunc { type client struct { count int resetTime time.Time } clients := make(map[string]*client) mu := sync.Mutex{} return func(c *gin.Context) { ip := c.ClientIP() mu.Lock() defer mu.Unlock() now := time.Now() // Clean up expired entries if cl, exists := clients[ip]; exists && now.After(cl.resetTime) { delete(clients, ip) } // Initialize or increment counter if _, exists := clients[ip]; !exists { clients[ip] = &client{ count: 1, resetTime: now.Add(window), } } else { clients[ip].count++ } // Check limit if clients[ip].count > maxRequests { c.JSON(429, gin.H{ "error": "Too many requests", "retry_after": clients[ip].resetTime.Sub(now).Seconds(), }) c.Abort() return } c.Next() } } // Použití na citlivých endpointech auth.POST("/login", middleware.RateLimit(15, time.Minute), authController.Login) auth.POST("/register", middleware.RateLimit(5, time.Hour), authController.Register) api.POST("/contact", middleware.RateLimit(10, time.Minute), contactController.SubmitContactForm) ``` **Limity:** - Login: 15 pokusů / minutu - Registrace: 5 pokusů / hodinu - Kontaktní formulář: 10 odeslání / minutu - Newsletter subscribe: 30 pokusů / minutu ### 8.5 Input Validation a Sanitizace #### **Backend Validace** ```go type CreateArticleRequest struct { Title string `json:"title" binding:"required,min=3,max=255"` Slug string `json:"slug" binding:"required,min=3,max=255,alphanum_hyphen"` Content string `json:"content" binding:"required,min=10"` CategoryID uint `json:"category_id" binding:"required,gt=0"` Published bool `json:"published"` FeaturedImage string `json:"featured_image" binding:"omitempty,url"` } func (bc *BaseController) CreateArticle(c *gin.Context) { var req CreateArticleRequest // Validace if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } // Sanitizace HTML obsahu sanitizedContent := sanitizeHTML(req.Content) // Create article... } ``` #### **SQL Injection Prevence** GORM automaticky escapuje parametry v SQL dotazech. ```go // ✅ Bezpečné - parameterized query db.Where("email = ?", userEmail).First(&user) // ❌ NEBEZPEČNÉ - string concatenation db.Where("email = '" + userEmail + "'").First(&user) // NIKDY NEPOUŽÍVAT! ``` #### **XSS Prevence** Frontend používá React, který automaticky escapuje obsah. Pro HTML obsah z editoru: ```typescript // Sanitizace HTML na frontendu import DOMPurify from 'dompurify'; function ArticleContent({ html }) { const sanitizedHTML = DOMPurify.sanitize(html, { ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'h1', 'h2', 'h3', 'ul', 'ol', 'li', 'a', 'img'], ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class'], }); return
; } ``` ### 8.6 File Upload Security ```go func (bc *BaseController) UploadImage(c *gin.Context) { file, err := c.FormFile("file") if err != nil { c.JSON(400, gin.H{"error": "No file uploaded"}) return } // 1. Kontrola velikosti maxSize := int64(10 * 1024 * 1024) // 10MB if file.Size > maxSize { c.JSON(400, gin.H{"error": "File too large"}) return } // 2. Kontrola MIME typu allowedTypes := map[string]bool{ "image/jpeg": true, "image/png": true, "image/gif": true, "image/webp": true, } contentType := file.Header.Get("Content-Type") if !allowedTypes[contentType] { c.JSON(400, gin.H{"error": "Invalid file type"}) return } // 3. Kontrola magic bytes (skutečný typ souboru) fileContent, _ := file.Open() buffer := make([]byte, 512) fileContent.Read(buffer) detectedType := http.DetectContentType(buffer) if !allowedTypes[detectedType] { c.JSON(400, gin.H{"error": "File content mismatch"}) return } // 4. Generování bezpečného jména souboru ext := filepath.Ext(file.Filename) safeFilename := fmt.Sprintf("%d_%s%s", time.Now().Unix(), generateRandomString(16), ext, ) // 5. Uložení do izolovaného adresáře uploadPath := filepath.Join("./uploads", safeFilename) c.SaveUploadedFile(file, uploadPath) c.JSON(200, gin.H{"url": "/uploads/" + safeFilename}) } ``` ### 8.7 GDPR Compliance #### **Souhlas s Cookies** ```typescript // Cookie consent banner const CookieBanner = () => { const [consent, setConsent] = useState(null); const handleAccept = (categories: string[]) => { const consent = { necessary: true, analytics: categories.includes('analytics'), marketing: categories.includes('marketing'), timestamp: new Date().toISOString(), }; localStorage.setItem('cookie_consent', JSON.stringify(consent)); setConsent(consent); // Spustit analytics pouze pokud uživatel souhlasil if (consent.analytics) { initializeUmami(); } }; return consent ? null : ; }; ``` #### **Anonymizace IP Adres** ```go // Hash IP adresy pro analytics (GDPR compliant) func hashIP(ip string) string { h := sha256.New() h.Write([]byte(ip + os.Getenv("IP_SALT"))) return fmt.Sprintf("%x", h.Sum(nil))[:16] } // Uložení trackingu s anonymizovanou IP event := models.VisitorEvent{ IPHash: hashIP(c.ClientIP()), // ... other fields } ``` #### **Právo na Výmaz Dat** ```go // Endpoint pro smazání uživatelských dat func (uc *UserController) DeleteUserData(c *gin.Context) { userID := c.Param("id") // Smazat veškerá osobní data db.Where("user_id = ?", userID).Delete(&models.Article{}) db.Where("email = ?", user.Email).Delete(&models.NewsletterSubscription{}) db.Where("user_id = ?", userID).Delete(&models.ContactMessage{}) db.Delete(&models.User{}, userID) c.JSON(200, gin.H{"message": "User data deleted"}) } ``` ### 8.8 Environment Variables Security **Nikdy necommitovat:** - `.env` soubor s produkčními credentials - API klíče - JWT secret - SMTP hesla **Best practices:** ```bash # .gitignore .env .env.local .env.production # Commitovat pouze example .env.example ``` **Production deployment:** - Použít environment variables na serveru - Nebo encrypted secrets (GitHub Secrets, Vault) - Rotate API keys pravidelně ### 8.9 Production Security Checklist - ✅ **JWT_SECRET** změněn z default hodnoty - ✅ **HTTPS** enabled (přes reverse proxy) - ✅ **HSTS** header aktivován - ✅ **CORS** omezen na konkrétní domény - ✅ **Rate limiting** na všech public endpointech - ✅ **SQL injection** prevence (GORM parametry) - ✅ **XSS** prevence (React + DOMPurify) - ✅ **File upload** validace - ✅ **Password hashing** (bcrypt cost 14) - ✅ **GDPR** compliance (cookie consent, IP anonymizace) - ✅ **Security headers** (CSP, X-Frame-Options, etc.) - ✅ **Input validation** na frontendu i backendu - ✅ **Error logging** bez citlivých dat --- ## 9. Minimální Požadavky ### 9.1 Hardwarové Požadavky #### **Development (Lokální vývoj):** - **CPU:** 2+ cores (Intel i3 nebo ekvivalent) - **RAM:** 4 GB minimum, 8 GB doporučeno - **Disk:** 5 GB volného místa (SSD doporučeno) - **OS:** Windows 10/11, macOS 10.15+, Linux (Ubuntu 20.04+) #### **Production (Server):** - **CPU:** 2+ cores - **RAM:** 2 GB minimum, 4 GB doporučeno pro 1000+ concurrent users - **Disk:** 20 GB+ (závisí na množství uploadů a databáze) - **Bandwidth:** 100 Mbps+ pro optimální výkon ### 9.2 Softwarové Požadavky #### **Backend:** | Software | Minimální Verze | Doporučená Verze | |----------|-----------------|------------------| | Go | 1.23.0 | 1.24.4+ | | PostgreSQL | 12.0 | 15.0+ | | Docker | 20.10 | 24.0+ | | Docker Compose | 1.29 | 2.20+ | #### **Frontend:** | Software | Minimální Verze | Doporučená Verze | |----------|-----------------|------------------| | Node.js | 16.0 | 20.0+ LTS | | npm | 8.0 | 10.0+ | #### **Další nástroje:** - Git 2.30+ - Text editor (VS Code, GoLand) - Make (pro Makefile příkazy) ### 9.3 Prohlížeče (Frontend Kompatibilita) | Prohlížeč | Minimální Verze | |-----------|-----------------| | Chrome | 90+ | | Firefox | 88+ | | Safari | 14+ | | Edge | 90+ | | Mobile Safari (iOS) | 14+ | | Chrome Mobile (Android) | 90+ | --- ## 10. Instalace a Konfigurace ### 10.1 Rychlá Instalace (Docker - Doporučeno) **Krok 1: Klonování repozitáře** ```bash git clone cd fotbal-club ``` **Krok 2: Konfigurace prostředí** ```bash # Zkopírovat example soubor cp .env.example .env # Upravit .env soubor # Minimálně změnit: # - JWT_SECRET # - SMTP_* konfigurace # - ALLOWED_ORIGINS ``` **Krok 3: Spuštění aplikace** ```bash docker-compose up -d ``` **Krok 4: Přístup do aplikace** - Frontend: http://localhost:3000 - Backend API: http://localhost:8080 - Databáze: localhost:5432 **Krok 5: Průvodce nastavením** 1. Otevřete http://localhost:3000 2. Budete přesměrováni na setup wizard 3. Vytvořte admin účet 4. Nastavte klubové informace 5. Vyberte klubové barvy 6. Připojte FAČR klub ### 10.2 Manuální Instalace (Bez Dockeru) #### **Backend Setup:** ```bash # 1. Instalace Go dependencies go mod download # 2. Nastavení PostgreSQL createdb fotbal_club # 3. Konfigurace .env cp .env.example .env # Upravit DATABASE_URL, JWT_SECRET, etc. # 4. Migrace databáze make migrate # 5. (Volitelně) Seed testovacích dat make seed # 6. Spuštění serveru make run # Nebo přímo: go run main.go ``` **Backend běží na:** http://localhost:8080 #### **Frontend Setup:** ```bash # 1. Přejít do frontend složky cd frontend # 2. Instalace dependencies npm install # 3. Konfigurace .env cp .env.example .env # Nastavit REACT_APP_API_URL=http://localhost:8080/api/v1 # 4. Spuštění dev serveru npm start ``` **Frontend běží na:** http://localhost:3000 ### 10.3 Konfigurační Soubory #### **Backend .env (Kompletní)** ```bash # ========================================== # APPLICATION # ========================================== APP_NAME=FotbalClub APP_ENV=development # development, staging, production PORT=8080 DEBUG=true # ========================================== # DATABASE # ========================================== DB_HOST=db DB_PORT=5432 DB_USER=postgres DB_PASSWORD=postgres DB_NAME=fotbal_club DATABASE_URL=postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=disable # Database Migrations & Seeding RUN_MIGRATIONS=true SEED_DATABASE=false # ========================================== # JWT AUTHENTICATION # ========================================== JWT_SECRET=your_very_secret_key_change_in_production JWT_EXPIRATION_HOURS=24 # ========================================== # EMAIL (SMTP) # ========================================== SMTP_HOST=smtp.example.com SMTP_PORT=587 SMTP_USER=your_email@example.com SMTP_PASSWORD=your_password SMTP_FROM=noreply@fotbalclub.com SMTP_FROM_NAME="Fotbal Club" SMTP_ENCRYPTION=tls # tls, ssl, or none SMTP_AUTH=true SMTP_SKIP_VERIFY=false # Email Templates EMAIL_TEMPLATE_DIR=./templates/emails # Contact Form CONTACT_EMAIL=help@example.com ADMIN_EMAIL=admin@example.com # ========================================== # NEWSLETTER # ========================================== NEWSLETTER_ENABLED=true # ========================================== # FILE UPLOADS # ========================================== UPLOAD_DIR=./uploads MAX_UPLOAD_SIZE=10485760 # 10MB in bytes ALLOWED_FILE_TYPES=image/jpeg,image/png,image/gif,application/pdf MAX_FILES=5 # ========================================== # CORS # ========================================== ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080 # ========================================== # EXTERNAL APIs # ========================================== # OpenRouter (AI blog generation) OPENROUTER_API_KEY=sk-or-v1-your-key-here OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 OPENROUTER_MODEL=mistralai/mistral-small-3.2-24b-instruct:free OPENROUTER_FALLBACK_MODEL=mistralai/mistral-nemo:free # Umami Analytics UMAMI_URL=https://umami.example.com UMAMI_USERNAME=admin UMAMI_PASSWORD=your_password UMAMI_WEBSITE_ID= # Auto-create on first run # ========================================== # LOGGING # ========================================== LOG_LEVEL=info # debug, info, warn, error LOG_FORMAT=text # text or json LOG_OUTPUT=stdout # stdout, stderr, or file path # ========================================== # SECURITY # ========================================== CONTENT_SECURITY_POLICY=default-src 'self' IP_SALT=random_salt_for_hashing_ips ``` #### **Frontend .env** ```bash # API Configuration REACT_APP_API_URL=http://localhost:8080/api/v1 REACT_APP_API_BASE_URL=http://localhost:8080 # FACR API REACT_APP_FACR_API_BASE_URL=http://localhost:8080/api/v1/facr REACT_APP_FACR_API_TIMEOUT=5000 REACT_APP_FACR_CACHE_TTL=3600000 # 1 hour in ms # Homepage Layout REACT_APP_HOMEPAGE_LAYOUT=classic # classic or sparta # Application Name REACT_APP_NAME=Fotbal Club Manager ``` ### 10.4 Databázové Migrace Aplikace používá automatické migrace přes GORM. **Spuštění migrací:** ```bash # V .env souboru RUN_MIGRATIONS=true # Pak restart aplikace docker-compose restart backend # Nebo make migrate ``` **Manuální migrace (SQL):** ```sql -- Vytvořit novou migraci v database/migrations/ -- Pojmenování: 001_create_users_table.sql CREATE TABLE users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, role VARCHAR(50) DEFAULT 'editor', created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); ``` ### 10.5 Seed Data (Testovací Data) ```bash # V .env SEED_DATABASE=true # Restart docker-compose restart backend ``` **Co se seeduje:** - Admin uživatel (admin@club.cz / admin123) - Základní kategorie článků - Sample články - Sample týmy a hráči - Sponzoři ### 10.6 Production Deployment #### **Doporučená Architektura:** ``` Internet │ ▼ [Nginx Reverse Proxy] ← HTTPS/SSL │ ├──► [Frontend Container (React SPA)] │ ├──► [Backend Container (Go API)] │ └──► [PostgreSQL Container/Managed DB] ``` #### **Docker Production Build:** ```bash # 1. Build production image docker build -t fotbal-club:latest . # 2. Run with production env docker run -d \ --name fotbal-club \ -p 8080:8080 \ --env-file .env.production \ -v /path/to/uploads:/app/uploads \ fotbal-club:latest ``` #### **Nginx Konfigurace:** ```nginx server { listen 80; server_name fotbalclub.cz; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name fotbalclub.cz; ssl_certificate /etc/letsencrypt/live/fotbalclub.cz/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/fotbalclub.cz/privkey.pem; # Frontend (SPA) location / { proxy_pass http://localhost:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # Backend API location /api/ { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # Uploads location /uploads/ { alias /var/www/fotbalclub/uploads/; expires 30d; add_header Cache-Control "public, immutable"; } # Cache location /cache/ { alias /var/www/fotbalclub/cache/; expires 30m; } } ``` #### **Environment Variables na Produkci:** ```bash # Použít environment variables místo .env souboru export APP_ENV=production export JWT_SECRET=$(openssl rand -base64 64) export DATABASE_URL=postgres://user:pass@db.example.com:5432/fotbal_club?sslmode=require export ALLOWED_ORIGINS=https://fotbalclub.cz # Nebo použít Docker secrets / Kubernetes secrets ``` #### **Backup Strategy:** ```bash # Automatický denní backup databáze 0 2 * * * pg_dump fotbal_club > /backups/fotbal_club_$(date +\%Y\%m\%d).sql # Backup uploadů 0 3 * * * rsync -av /var/www/fotbalclub/uploads/ /backups/uploads/ ``` ### 10.7 Troubleshooting #### **Časté Problémy:** **1. Backend se nespustí - "Failed to connect to database"** ```bash # Zkontrolovat DB běží docker ps | grep postgres # Zkontrolovat DATABASE_URL v .env echo $DATABASE_URL # Restartovat DB docker-compose restart db ``` **2. Frontend nemůže volat API - CORS error** ```bash # Zkontrolovat ALLOWED_ORIGINS v backend .env # Zkontrolovat REACT_APP_API_URL ve frontend .env # Development: ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080 ``` **3. Migrace selhaly** ```bash # Vymazat databázi a začít znovu docker-compose down -v docker-compose up -d # Nebo manuální reset psql fotbal_club -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" ``` **4. Upload souborů nefunguje** ```bash # Zkontrolovat oprávnění chmod -R 755 ./uploads # Zkontrolovat UPLOAD_DIR v .env # Zkontrolovat MAX_UPLOAD_SIZE ``` **5. JWT token expired příliš rychle** ```bash # Upravit JWT_EXPIRATION_HOURS v .env JWT_EXPIRATION_HOURS=168 # 7 dní ``` --- ## 11. Příklady Kódu ### 11.1 Backend - Vytvoření Nového Endpointu **Příklad: Endpoint pro získání statistik článků** ```go // internal/controllers/stats_controller.go package controllers import ( "fotbal-club/internal/models" "github.com/gin-gonic/gin" "gorm.io/gorm" ) type StatsController struct { DB *gorm.DB } // GetArticleStats vrací statistiky článků func (sc *StatsController) GetArticleStats(c *gin.Context) { var stats struct { TotalArticles int64 `json:"total_articles"` PublishedArticles int64 `json:"published_articles"` TotalViews int `json:"total_views"` AverageViews float64 `json:"average_views"` MostViewedArticle *models.Article `json:"most_viewed_article"` } // Celkový počet článků sc.DB.Model(&models.Article{}).Count(&stats.TotalArticles) // Počet publikovaných sc.DB.Model(&models.Article{}).Where("published = ?", true).Count(&stats.PublishedArticles) // Celkový počet zhlédnutí var articles []models.Article sc.DB.Find(&articles) for _, article := range articles { stats.TotalViews += article.ViewCount } // Průměrný počet zhlédnutí if stats.TotalArticles > 0 { stats.AverageViews = float64(stats.TotalViews) / float64(stats.TotalArticles) } // Nejvíce zobrazovaný článek sc.DB.Order("view_count DESC").First(&stats.MostViewedArticle) c.JSON(200, gin.H{ "success": true, "data": stats, }) } ``` **Registrace route:** ```go // internal/routes/routes.go func SetupRoutes(api *gin.RouterGroup, db *gorm.DB) { statsController := &controllers.StatsController{DB: db} // Public stats api.GET("/stats/articles", statsController.GetArticleStats) } ``` ### 11.2 Frontend - Vytvoření Nové Komponenty **Příklad: Dashboard widget pro statistiky** ```typescript // frontend/src/components/dashboard/ArticleStatsWidget.tsx import React from 'react'; import { Box, Stat, StatLabel, StatNumber, StatHelpText, SimpleGrid } from '@chakra-ui/react'; import { useQuery } from '@tanstack/react-query'; import api from '../../services/api'; interface ArticleStats { total_articles: number; published_articles: number; total_views: number; average_views: number; most_viewed_article: { id: number; title: string; view_count: number; }; } const ArticleStatsWidget: React.FC = () => { const { data, isLoading, error } = useQuery({ queryKey: ['article-stats'], queryFn: async () => { const response = await api.get('/stats/articles'); return response.data.data; }, refetchInterval: 30000, // Refresh každých 30 sekund }); if (isLoading) { return Načítání...; } if (error) { return Chyba při načítání statistik; } return ( Celkem článků {data?.total_articles} Všechny články v systému Publikované {data?.published_articles} {((data?.published_articles / data?.total_articles) * 100).toFixed(1)}% z celku Celková zobrazení {data?.total_views.toLocaleString()} Všechny články dohromady Průměrná zobrazení {data?.average_views.toFixed(1)} Per článek {data?.most_viewed_article && ( Nejčtenější článek: {data.most_viewed_article.title}
{data.most_viewed_article.view_count} zobrazení
)}
); }; export default ArticleStatsWidget; ``` ### 11.3 Použití Custom Hooks **Příklad: useArticles hook** ```typescript // frontend/src/hooks/useArticles.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import api from '../services/api'; import { Article, CreateArticleRequest } from '../types'; export const useArticles = (filters?: { category?: number; published?: boolean; page?: number; limit?: number; }) => { return useQuery({ queryKey: ['articles', filters], queryFn: async () => { const params = new URLSearchParams(); if (filters?.category) params.append('category', filters.category.toString()); if (filters?.published !== undefined) params.append('published', filters.published.toString()); if (filters?.page) params.append('page', filters.page.toString()); if (filters?.limit) params.append('limit', filters.limit.toString()); const response = await api.get(`/articles?${params.toString()}`); return response.data.data; }, }); }; export const useCreateArticle = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (article: CreateArticleRequest) => { const response = await api.post('/articles', article); return response.data; }, onSuccess: () => { // Invalidate and refetch articles queryClient.invalidateQueries({ queryKey: ['articles'] }); }, }); }; export const useDeleteArticle = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (articleId: number) => { await api.delete(`/articles/${articleId}`); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['articles'] }); }, }); }; ``` **Použití v komponentě:** ```typescript // frontend/src/pages/admin/ArticlesAdminPage.tsx import React from 'react'; import { useArticles, useDeleteArticle } from '../../hooks/useArticles'; const ArticlesAdminPage: React.FC = () => { const { data: articles, isLoading } = useArticles({ published: true }); const deleteMutation = useDeleteArticle(); const handleDelete = async (id: number) => { if (window.confirm('Opravdu smazat článek?')) { await deleteMutation.mutateAsync(id); } }; if (isLoading) return
Načítání...
; return (
{articles?.map((article) => (

{article.title}

))}
); }; ``` ### 11.4 Background Job - Newsletter Automation **Příklad: Automatické odesílání newsletteru** ```go // internal/services/newsletter_automation.go package services import ( "fotbal-club/internal/models" "fotbal-club/pkg/email" "time" "gorm.io/gorm" "log" ) type NewsletterAutomation struct { db *gorm.DB emailService *email.EmailService ticker *time.Ticker stopChan chan bool } func NewNewsletterAutomation(db *gorm.DB, emailSvc *email.EmailService) *NewsletterAutomation { return &NewsletterAutomation{ db: db, emailService: emailSvc, stopChan: make(chan bool), } } func (na *NewsletterAutomation) Start() { // Check every hour na.ticker = time.NewTicker(1 * time.Hour) go func() { for { select { case <-na.ticker.C: na.checkAndSendScheduled() case <-na.stopChan: na.ticker.Stop() return } } }() log.Println("Newsletter automation started") } func (na *NewsletterAutomation) Stop() { na.stopChan <- true log.Println("Newsletter automation stopped") } func (na *NewsletterAutomation) checkAndSendScheduled() { now := time.Now() // Weekly digest - Friday at 18:00 if now.Weekday() == time.Friday && now.Hour() == 18 { na.sendWeeklyDigest() } // Match alerts - 24 hours before match na.sendMatchAlerts() // Results digest - Monday at 10:00 if now.Weekday() == time.Monday && now.Hour() == 10 { na.sendResultsDigest() } } func (na *NewsletterAutomation) sendWeeklyDigest() { // Get articles from last 7 days var articles []models.Article sevenDaysAgo := time.Now().AddDate(0, 0, -7) na.db.Where("published = ? AND created_at > ?", true, sevenDaysAgo). Order("created_at DESC"). Limit(5). Find(&articles) if len(articles) == 0 { return // No new content } // Get subscribers who want weekly digest var subscribers []models.NewsletterSubscription na.db.Where("status = ? AND preferences->>'weekly_digest' = 'true'", "active"). Find(&subscribers) for _, sub := range subscribers { templateData := map[string]interface{}{ "Email": sub.Email, "Articles": articles, "Type": "weekly_digest", } err := na.emailService.SendNewsletter(sub.Email, "Týdenní přehled", templateData) if err != nil { log.Printf("Failed to send newsletter to %s: %v", sub.Email, err) } else { log.Printf("Weekly digest sent to %s", sub.Email) } // Rate limiting - wait 100ms between emails time.Sleep(100 * time.Millisecond) } } func (na *NewsletterAutomation) sendMatchAlerts() { // Get matches in next 24 hours tomorrow := time.Now().Add(24 * time.Hour) dayAfterTomorrow := time.Now().Add(48 * time.Hour) // Implementation would fetch from FACR or database // For brevity, simplified here log.Println("Checking for match alerts...") } func (na *NewsletterAutomation) sendResultsDigest() { log.Println("Sending results digest...") } ``` ### 11.5 Use Case: Kompletní Flow Vytvoření Článku **1. Frontend - Formulář pro vytvoření článku:** ```typescript // frontend/src/components/admin/ArticleForm.tsx import React, { useState } from 'react'; import { useCreateArticle } from '../../hooks/useArticles'; import { useNavigate } from 'react-router-dom'; const ArticleForm: React.FC = () => { const [formData, setFormData] = useState({ title: '', content: '', excerpt: '', category_id: 1, published: false, featured_image: '', }); const createMutation = useCreateArticle(); const navigate = useNavigate(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { await createMutation.mutateAsync(formData); alert('Článek vytvořen!'); navigate('/admin/articles'); } catch (error) { alert('Chyba při vytváření článku'); } }; return (
setFormData({ ...formData, title: e.target.value })} required />