mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 18:52:56 +00:00
3487 lines
98 KiB
Markdown
3487 lines
98 KiB
Markdown
# 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 (
|
|
<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 |
|
|
|
|
```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 <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**
|
|
|
|
```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
|
|
<!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
|
|
|
|
```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
|
|
<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
|
|
|
|
```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 (
|
|
<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)
|
|
|
|
```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 <JWT_TOKEN>
|
|
```
|
|
|
|
#### **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 <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í)
|
|
|
|
```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 <div dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />;
|
|
}
|
|
```
|
|
|
|
### 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<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**
|
|
|
|
```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 <repository-url>
|
|
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<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**
|
|
|
|
```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<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ě:**
|
|
|
|
```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 <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**
|
|
|
|
```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 (
|
|
<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:**
|
|
|
|
```go
|
|
// internal/controllers/base_controller.go
|
|
func (bc *BaseController) CreateArticle(c *gin.Context) {
|
|
var req struct {
|
|
Title string `json:"title" binding:"required,min=3,max=255"`
|
|
Content string `json:"content" binding:"required,min=10"`
|
|
Excerpt string `json:"excerpt"`
|
|
CategoryID uint `json:"category_id" binding:"required"`
|
|
Published bool `json:"published"`
|
|
FeaturedImage string `json:"featured_image"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Get user from context
|
|
userID := c.GetUint("userID")
|
|
|
|
// Generate slug
|
|
slug := generateSlug(req.Title)
|
|
|
|
// Create article
|
|
article := models.Article{
|
|
Title: req.Title,
|
|
Slug: slug,
|
|
Content: req.Content,
|
|
Excerpt: req.Excerpt,
|
|
CategoryID: req.CategoryID,
|
|
Published: req.Published,
|
|
FeaturedImage: req.FeaturedImage,
|
|
AuthorID: userID,
|
|
}
|
|
|
|
if req.Published {
|
|
now := time.Now()
|
|
article.PublishedAt = &now
|
|
}
|
|
|
|
if err := bc.DB.Create(&article).Error; err != nil {
|
|
c.JSON(500, gin.H{"error": "Failed to create article"})
|
|
return
|
|
}
|
|
|
|
// Preload relationships
|
|
bc.DB.Preload("Category").Preload("Author").First(&article, article.ID)
|
|
|
|
c.JSON(201, gin.H{
|
|
"success": true,
|
|
"data": article,
|
|
"message": "Article created successfully",
|
|
})
|
|
}
|
|
```
|
|
|
|
### 11.6 Performance Optimization - Caching
|
|
|
|
**Backend caching příklad:**
|
|
|
|
```go
|
|
// pkg/cache/cache.go
|
|
package cache
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Cache struct {
|
|
mu sync.RWMutex
|
|
data map[string]CacheEntry
|
|
}
|
|
|
|
type CacheEntry struct {
|
|
Data interface{}
|
|
ExpiresAt time.Time
|
|
}
|
|
|
|
var globalCache = &Cache{
|
|
data: make(map[string]CacheEntry),
|
|
}
|
|
|
|
func Set(key string, value interface{}, ttl time.Duration) {
|
|
globalCache.mu.Lock()
|
|
defer globalCache.mu.Unlock()
|
|
|
|
globalCache.data[key] = CacheEntry{
|
|
Data: value,
|
|
ExpiresAt: time.Now().Add(ttl),
|
|
}
|
|
}
|
|
|
|
func Get(key string) (interface{}, bool) {
|
|
globalCache.mu.RLock()
|
|
defer globalCache.mu.RUnlock()
|
|
|
|
entry, exists := globalCache.data[key]
|
|
if !exists {
|
|
return nil, false
|
|
}
|
|
|
|
if time.Now().After(entry.ExpiresAt) {
|
|
delete(globalCache.data, key)
|
|
return nil, false
|
|
}
|
|
|
|
return entry.Data, true
|
|
}
|
|
|
|
// Save to file for persistence
|
|
func SaveToFile(filename string) error {
|
|
globalCache.mu.RLock()
|
|
defer globalCache.mu.RUnlock()
|
|
|
|
data, err := json.Marshal(globalCache.data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile(filepath.Join("./cache", filename), data, 0644)
|
|
}
|
|
```
|
|
|
|
**Použití:**
|
|
|
|
```go
|
|
func (bc *BaseController) GetArticles(c *gin.Context) {
|
|
cacheKey := "articles_published"
|
|
|
|
// Try cache first
|
|
if cached, found := cache.Get(cacheKey); found {
|
|
c.JSON(200, cached)
|
|
return
|
|
}
|
|
|
|
// Fetch from database
|
|
var articles []models.Article
|
|
bc.DB.Where("published = ?", true).Find(&articles)
|
|
|
|
response := gin.H{"success": true, "data": articles}
|
|
|
|
// Cache for 5 minutes
|
|
cache.Set(cacheKey, response, 5*time.Minute)
|
|
|
|
c.JSON(200, response)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 12. Závěr
|
|
|
|
### 12.1 Shrnutí Projektu
|
|
|
|
**Fotbal Club** je komplexní full-stack webová aplikace navržená pro efektivní správu fotbalového klubu. Projekt úspěšně kombinuje moderní technologie a best practices pro vytvoření robustního, škálovatelného a bezpečného systému.
|
|
|
|
#### **Klíčové Úspěchy:**
|
|
|
|
✅ **Moderní Tech Stack**
|
|
- Go backend s vysokým výkonem
|
|
- React frontend s TypeScript
|
|
- PostgreSQL databáze
|
|
- Docker pro snadné deployment
|
|
|
|
✅ **Komplexní Funkcionalita**
|
|
- Správa obsahu (články, galerie, videa)
|
|
- Automatická integrace s FAČR API
|
|
- Newsletter systém s automatizací
|
|
- Analytics a statistiky
|
|
- AI generování obsahu
|
|
|
|
✅ **Bezpečnost**
|
|
- JWT autentizace
|
|
- RBAC autorizace
|
|
- Rate limiting
|
|
- GDPR compliance
|
|
- Security headers
|
|
|
|
✅ **Developer Experience**
|
|
- Setup wizard pro snadnou instalaci
|
|
- Comprehensive API dokumentace
|
|
- Docker Compose pro one-command start
|
|
- Environment-based konfigurace
|
|
|
|
### 12.2 Technické Přednosti
|
|
|
|
| Oblast | Implementace |
|
|
|--------|--------------|
|
|
| **Performance** | Caching, lazy loading, optimalizované DB queries |
|
|
| **Scalability** | Stateless API, horizontální škálování možné |
|
|
| **Maintainability** | Čistá architektura, SOLID principy |
|
|
| **Testing** | Unit testy, integration testy připraveny |
|
|
| **Documentation** | Kompletní technická dokumentace |
|
|
|
|
### 12.3 Možnosti Rozšíření
|
|
|
|
Aplikace je navržena s ohledem na budoucí rozšíření:
|
|
|
|
**Krátk
|
|
|
|
odobé (1-3 měsíce):**
|
|
- [ ] WebSocket podpora pro real-time updates
|
|
- [ ] Push notifikace (PWA)
|
|
- [ ] Mobilní aplikace (React Native)
|
|
- [ ] Advanced SEO features (schema.org markup)
|
|
|
|
**Střednědobé (3-6 měsíců):**
|
|
- [ ] E-shop modul pro klubové produkty
|
|
- [ ] Ticketing systém pro zápasy
|
|
- [ ] Member portál s přihlášením hráčů
|
|
- [ ] Platební brána integrace
|
|
|
|
**Dlouhodobé (6-12 měsíců):**
|
|
- [ ] Multi-tenant architektura (více klubů)
|
|
- [ ] API pro třetí strany
|
|
- [ ] Machine learning predikce výsledků
|
|
- [ ] Video streaming integrace
|
|
|
|
### 12.4 Lessons Learned
|
|
|
|
**Co fungovalo dobře:**
|
|
- Docker drasticky zjednodušil development workflow
|
|
- React Query zjednodušil state management
|
|
- GORM AutoMigrate urychlil iterace
|
|
- JWT stateless autentizace škáluje výborně
|
|
|
|
**Co by se dalo zlepšit:**
|
|
- Implementovat GraphQL pro flexibilnější API
|
|
- Přidat více unit testů (současné pokrytí ~60%)
|
|
- Použít message queue pro newsletter (Redis/RabbitMQ)
|
|
- Implementovat circuit breaker pro externí API
|
|
|
|
### 12.5 Použité Zdroje a Reference
|
|
|
|
#### **12.5.1 Primární Technologie**
|
|
|
|
**Backend:**
|
|
- **Go (Golang)** - https://go.dev/
|
|
- Verze: 1.23.0+
|
|
- Dokumentace: https://go.dev/doc/
|
|
- Tour of Go: https://go.dev/tour/
|
|
- Effective Go: https://go.dev/doc/effective_go
|
|
|
|
- **Gin Web Framework** - https://gin-gonic.com/
|
|
- GitHub: https://github.com/gin-gonic/gin
|
|
- Dokumentace: https://gin-gonic.com/docs/
|
|
- Licence: MIT
|
|
|
|
- **GORM** - https://gorm.io/
|
|
- GitHub: https://github.com/go-gorm/gorm
|
|
- Dokumentace: https://gorm.io/docs/
|
|
- Licence: MIT
|
|
|
|
- **PostgreSQL** - https://www.postgresql.org/
|
|
- Verze: 15+
|
|
- Dokumentace: https://www.postgresql.org/docs/
|
|
- Licence: PostgreSQL License
|
|
|
|
**Frontend:**
|
|
- **React** - https://react.dev/
|
|
- GitHub: https://github.com/facebook/react
|
|
- Verze: 18+
|
|
- Dokumentace: https://react.dev/learn
|
|
- Licence: MIT
|
|
|
|
- **TypeScript** - https://www.typescriptlang.org/
|
|
- Verze: 5+
|
|
- Dokumentace: https://www.typescriptlang.org/docs/
|
|
- Licence: Apache 2.0
|
|
|
|
- **Chakra UI** - https://chakra-ui.com/
|
|
- GitHub: https://github.com/chakra-ui/chakra-ui
|
|
- Dokumentace: https://chakra-ui.com/docs/
|
|
- Licence: MIT
|
|
|
|
- **React Query (TanStack Query)** - https://tanstack.com/query/latest
|
|
- GitHub: https://github.com/TanStack/query
|
|
- Dokumentace: https://tanstack.com/query/latest/docs/react/overview
|
|
- Licence: MIT
|
|
|
|
- **React Router** - https://reactrouter.com/
|
|
- GitHub: https://github.com/remix-run/react-router
|
|
- Verze: 6+
|
|
- Dokumentace: https://reactrouter.com/en/main
|
|
- Licence: MIT
|
|
|
|
**DevOps:**
|
|
- **Docker** - https://www.docker.com/
|
|
- Dokumentace: https://docs.docker.com/
|
|
- Licence: Apache 2.0
|
|
|
|
- **Docker Compose** - https://docs.docker.com/compose/
|
|
- Dokumentace: https://docs.docker.com/compose/
|
|
- Licence: Apache 2.0
|
|
|
|
#### **12.5.2 Go Knihovny a Dependencies**
|
|
|
|
| Knihovna | Účel | Odkaz | Licence |
|
|
|----------|------|-------|---------|
|
|
| `golang-jwt/jwt/v5` | JWT autentizace | https://github.com/golang-jwt/jwt | MIT |
|
|
| `golang.org/x/crypto` | Bcrypt hashing | https://pkg.go.dev/golang.org/x/crypto | BSD-3 |
|
|
| `gopkg.in/mail.v2` | SMTP emaily | https://github.com/go-gomail/gomail | MIT |
|
|
| `joho/godotenv` | Environment variables | https://github.com/joho/godotenv | MIT |
|
|
| `PuerkitoBio/goquery` | HTML parsing | https://github.com/PuerkitoBio/goquery | BSD-3 |
|
|
| `vanng822/go-premailer` | Email CSS inlining | https://github.com/vanng822/go-premailer | MIT |
|
|
| `gorm.io/driver/postgres` | PostgreSQL driver | https://github.com/go-gorm/postgres | MIT |
|
|
|
|
#### **12.5.3 Frontend Knihovny a Dependencies**
|
|
|
|
| Knihovna | Účel | Odkaz | Licence |
|
|
|----------|------|-------|---------|
|
|
| `@chakra-ui/react` | UI komponenty | https://chakra-ui.com/ | MIT |
|
|
| `@tanstack/react-query` | Server state management | https://tanstack.com/query | MIT |
|
|
| `react-router-dom` | Routing | https://reactrouter.com/ | MIT |
|
|
| `axios` | HTTP client | https://axios-http.com/ | MIT |
|
|
| `dompurify` | XSS sanitizace | https://github.com/cure53/DOMPurify | Apache-2.0 |
|
|
| `react-icons` | Ikony | https://react-icons.github.io/react-icons/ | MIT |
|
|
| `framer-motion` | Animace | https://www.framer.com/motion/ | MIT |
|
|
|
|
#### **12.5.4 Externí API a Služby**
|
|
|
|
**FAČR (Fotbalová asociace České republiky):**
|
|
- **URL:** https://is.fotbal.cz/
|
|
- **Typ integrace:** Custom wrapper/scraper vytvořen pro tento projekt
|
|
- **Účel:** Automatické získávání výsledků, rozpisů a tabulek
|
|
- **Poznámka:** Oficiální API není veřejně dostupné, použit web scraping s `goquery`
|
|
- **Zdrojový web:** https://www.fotbal.cz/
|
|
|
|
**YouTube Data API v3:**
|
|
- **Dokumentace:** https://developers.google.com/youtube/v3
|
|
- **Reference:** https://developers.google.com/youtube/v3/docs
|
|
- **Účel:** Načítání videí z klubového YouTube kanálu
|
|
- **Licence:** Google APIs Terms of Service
|
|
- **Quota:** 10,000 units/day (free tier)
|
|
|
|
**Zonerama:**
|
|
- **URL:** https://www.zonerama.com/
|
|
- **Typ integrace:** Custom HTML scraper vytvořen pro tento projekt
|
|
- **Účel:** Synchronizace fotografických alb
|
|
- **Poznámka:** Zonerama nemá veřejné API, použit scraping
|
|
|
|
**Umami Analytics:**
|
|
- **Oficiální web:** https://umami.is/
|
|
- **GitHub:** https://github.com/umami-software/umami
|
|
- **Dokumentace:** https://umami.is/docs
|
|
- **Typ nasazení:** Self-hosted
|
|
- **Účel:** Privacy-first web analytics
|
|
- **Licence:** MIT
|
|
- **Alternativa k:** Google Analytics
|
|
|
|
**OpenRouter AI:**
|
|
- **Oficiální web:** https://openrouter.ai/
|
|
- **Dokumentace:** https://openrouter.ai/docs
|
|
- **API Reference:** https://openrouter.ai/docs/api-reference
|
|
- **Účel:** AI generování článků pomocí LLM modelů
|
|
- **Modely použité:**
|
|
- Mistral Small: https://mistral.ai/
|
|
- Mistral Nemo: https://mistral.ai/news/mistral-nemo/
|
|
- **Ceny:** Free tier dostupný, placené modely pro produkci
|
|
|
|
**Google Maps Embed API:**
|
|
- **Dokumentace:** https://developers.google.com/maps/documentation/embed
|
|
- **Účel:** Zobrazení lokace klubu na mapě
|
|
- **Licence:** Google Maps Platform Terms
|
|
|
|
#### **12.5.5 Vývojové Nástroje**
|
|
|
|
| Nástroj | Účel | Odkaz |
|
|
|---------|------|-------|
|
|
| **Visual Studio Code** | Code editor | https://code.visualstudio.com/ |
|
|
| **Git** | Version control | https://git-scm.com/ |
|
|
| **Postman** | API testing | https://www.postman.com/ |
|
|
| **pgAdmin** | PostgreSQL management | https://www.pgadmin.org/ |
|
|
| **Go Playground** | Go testing | https://go.dev/play/ |
|
|
| **npm** | Package manager | https://www.npmjs.com/ |
|
|
|
|
#### **12.5.6 Knihy a Články**
|
|
|
|
**Knihy:**
|
|
1. **"Clean Code: A Handbook of Agile Software Craftsmanship"**
|
|
- Autor: Robert C. Martin
|
|
- Vydavatel: Prentice Hall
|
|
- ISBN: 978-0132350884
|
|
- Použito pro: Best practices v architektuře kódu
|
|
|
|
2. **"Designing Data-Intensive Applications"**
|
|
- Autor: Martin Kleppmann
|
|
- Vydavatel: O'Reilly Media
|
|
- ISBN: 978-1449373320
|
|
- Použito pro: Databázový design a škálovatelnost
|
|
|
|
3. **"The Go Programming Language"**
|
|
- Autoři: Alan A. A. Donovan, Brian W. Kernighan
|
|
- Vydavatel: Addison-Wesley
|
|
- ISBN: 978-0134190440
|
|
- Použito pro: Go best practices
|
|
|
|
**Online Zdroje:**
|
|
- **Go by Example** - https://gobyexample.com/
|
|
- **React TypeScript Cheatsheet** - https://react-typescript-cheatsheet.netlify.app/
|
|
- **PostgreSQL Tutorial** - https://www.postgresqltutorial.com/
|
|
- **JWT.io** - https://jwt.io/ (JWT debugger a dokumentace)
|
|
- **MDN Web Docs** - https://developer.mozilla.org/ (Web standardy)
|
|
|
|
#### **12.5.7 Bezpečnostní Standardy a Specifikace**
|
|
|
|
- **OWASP Top 10** - https://owasp.org/www-project-top-ten/
|
|
- Použito pro: Identifikace a prevenci bezpečnostních rizik
|
|
|
|
- **GDPR (General Data Protection Regulation)**
|
|
- Oficiální text: https://gdpr-info.eu/
|
|
- Použito pro: Cookie consent, data privacy, právo na výmaz
|
|
|
|
- **JWT RFC 7519** - https://datatracker.ietf.org/doc/html/rfc7519
|
|
- Použito pro: JWT implementace
|
|
|
|
- **bcrypt** - https://en.wikipedia.org/wiki/Bcrypt
|
|
- Použito pro: Password hashing
|
|
|
|
#### **12.5.8 Inspirace a Podobné Projekty**
|
|
|
|
- **Hacker News Clone (Go + React)** - Inspirace pro strukturu projektu
|
|
- **Real World App** - https://github.com/gothinkster/realworld
|
|
- Použito pro: API design patterns
|
|
|
|
#### **12.5.9 Community a Support**
|
|
|
|
- **Stack Overflow** - https://stackoverflow.com/
|
|
- Tag: golang, reactjs, postgresql, docker
|
|
|
|
- **Go Forum** - https://forum.golangbridge.org/
|
|
|
|
- **React Discord** - https://discord.gg/react
|
|
|
|
- **r/golang** - https://www.reddit.com/r/golang/
|
|
|
|
- **r/reactjs** - https://www.reddit.com/r/reactjs/
|
|
|
|
#### **12.5.10 Citace a Poděkování**
|
|
|
|
Tento projekt využívá open-source software a byl vytvořen díky příspěvkům tisíců vývojářů v komunitách:
|
|
|
|
**Poděkování:**
|
|
- **Go Team** - Za výborný programovací jazyk a ekosystém
|
|
- **Facebook/Meta** - Za React a související nástroje
|
|
- **Chakra UI Team** - Za krásnou UI knihovnu
|
|
- **GORM Team** - Za powerful ORM pro Go
|
|
- **PostgreSQL Global Development Group** - Za spolehlivou databázi
|
|
- **Docker Inc.** - Za kontejnerizační platformu
|
|
- **Všem contributorom** open-source projektů použitých v této aplikaci
|
|
|
|
**Copyright Notice:**
|
|
Všechny použité technologie a knihovny jsou použity v souladu s jejich licencemi. Tento projekt je šířen pod MIT licencí a respektuje všechny licence závislostí.
|
|
|
|
**Akademické Použití:**
|
|
Tento projekt byl vytvořen pro vzdělávací účely jako součást maturitní zkoušky. Veškerý kód je původní dílo autora s výjimkou open-source knihoven a frameworků, které jsou řádně citovány výše.
|
|
|
|
### 12.6 Autor a Kontakt
|
|
|
|
**Projekt vytvořil:** Tomáš Dvořák
|
|
**Datum:** Říjen 2025
|
|
**Účel:** Maturitní projekt - Prezentace full-stack webového systému
|
|
|
|
**Licence:** MIT License
|
|
|
|
---
|
|
|
|
### 12.7 Poděkování
|
|
|
|
Tento projekt vznikl jako demonstrace moderních webových technologií a best practices v oblasti full-stack development. Děkuji všem, kteří přispěli k open-source nástrojům použitým v tomto projektu.
|
|
|
|
---
|
|
|
|
**Konec dokumentace**
|
|
|
|
*Verze 1.0 - Říjen 2025*
|
|
|