dev day #92
@@ -1,29 +0,0 @@
|
||||
# Project Diagrams
|
||||
|
||||
This folder contains Mermaid diagrams for the project:
|
||||
|
||||
- ER Diagram of the database schema
|
||||
- System Architecture (frontend ↔ backend ↔ integrations)
|
||||
- Admin Module Map (grouped by navigation categories)
|
||||
- Frontpage Data Map (sections → data sources)
|
||||
|
||||
## Recommended extensions (VS Code)
|
||||
- Markdown Preview Mermaid Support (ID: bpruitt-goddard.vscode-mermaid-preview)
|
||||
- Alternative: Markdown Preview Enhanced (ID: shd101wyy.markdown-preview-enhanced)
|
||||
|
||||
## How to preview
|
||||
1) Install one of the extensions above.
|
||||
2) Open any .md file here (e.g., er-diagram.md).
|
||||
3) Press Ctrl+Shift+V (or Right click → Open Preview / Open Preview to the Side).
|
||||
4) If prompted to allow scripts for Mermaid, accept.
|
||||
|
||||
## Files
|
||||
- er-diagram.md — ER diagram of DB entities and relationships
|
||||
- system-architecture.md — high-level system flow
|
||||
- admin-map.md — map of admin sections
|
||||
- frontpage-data-map.md — frontpage sections → data sources
|
||||
|
||||
## Optional: Export as images
|
||||
- You can install Mermaid CLI to export to PNG/SVG: `npm i -g @mermaid-js/mermaid-cli`
|
||||
- For Markdown files in this folder, run: `mmdc -i er-diagram.md -o er-diagram.svg --inputType markdown`
|
||||
(If you extract the mermaid code to a standalone .mmd file, you can omit `--inputType`.)
|
||||
@@ -1,61 +0,0 @@
|
||||
# Admin Module Map
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph Zakladni
|
||||
ADASH[Nastenka]
|
||||
AANALYT[Analytika]
|
||||
end
|
||||
subgraph Sport
|
||||
TEAMS[Tymy]
|
||||
MATCHES[Zapasy]
|
||||
PLAYERS[Hraci]
|
||||
ALIASES[Alias_soutezi]
|
||||
SCORE[Tabule_Scoreboard]
|
||||
SCORE_R[Scoreboard_Remote]
|
||||
end
|
||||
subgraph Obsah
|
||||
ARTICLES[Clanky]
|
||||
ACTIVITIES[Aktivity]
|
||||
CATEGORIES[Kategorie]
|
||||
COMMENTS[Komentare]
|
||||
end
|
||||
subgraph Media
|
||||
VIDEOS[Videa]
|
||||
GALLERY[Galerie]
|
||||
FILES[Soubory]
|
||||
BANNERS[Bannery]
|
||||
end
|
||||
subgraph Komunikace
|
||||
MSGS[Zpravy]
|
||||
NEWSLTR[Zpravodaj]
|
||||
CONTACTS[Kontakty]
|
||||
end
|
||||
subgraph Marketing
|
||||
SPONSORS[Sponzori]
|
||||
MERCH[Obleceni]
|
||||
POLLS[Ankety]
|
||||
SWEEP[Souteze]
|
||||
ENGAGE[Odmeny_a_Uspechy]
|
||||
SHORT[Zkracene_odkazy]
|
||||
end
|
||||
subgraph Nastroje
|
||||
PREFETCH[Prefetch_a_Cache]
|
||||
ERRORS[Chyby]
|
||||
DOCS[Dokumentace]
|
||||
end
|
||||
subgraph Nastaveni
|
||||
SETTINGS[Nastaveni]
|
||||
USERS[Uzivatele]
|
||||
NAV[Navigace]
|
||||
ABOUT[O_klubu]
|
||||
end
|
||||
|
||||
ADASH --> Sport
|
||||
ADASH --> Obsah
|
||||
ADASH --> Media
|
||||
ADASH --> Komunikace
|
||||
ADASH --> Marketing
|
||||
ADASH --> Nastroje
|
||||
ADASH --> Nastaveni
|
||||
```
|
||||
@@ -1,57 +0,0 @@
|
||||
graph LR
|
||||
subgraph Zakladni
|
||||
ADASH[Nastenka]
|
||||
AANALYT[Analytika]
|
||||
end
|
||||
subgraph Sport
|
||||
TEAMS[Tymy]
|
||||
MATCHES[Zapasy]
|
||||
PLAYERS[Hraci]
|
||||
ALIASES[Alias_soutezi]
|
||||
SCORE[Tabule_Scoreboard]
|
||||
SCORE_R[Scoreboard_Remote]
|
||||
end
|
||||
subgraph Obsah
|
||||
ARTICLES[Clanky]
|
||||
ACTIVITIES[Aktivity]
|
||||
CATEGORIES[Kategorie]
|
||||
COMMENTS[Komentare]
|
||||
end
|
||||
subgraph Media
|
||||
VIDEOS[Videa]
|
||||
GALLERY[Galerie]
|
||||
FILES[Soubory]
|
||||
BANNERS[Bannery]
|
||||
end
|
||||
subgraph Komunikace
|
||||
MSGS[Zpravy]
|
||||
NEWSLTR[Zpravodaj]
|
||||
CONTACTS[Kontakty]
|
||||
end
|
||||
subgraph Marketing
|
||||
SPONSORS[Sponzori]
|
||||
MERCH[Obleceni]
|
||||
POLLS[Ankety]
|
||||
SWEEP[Souteze]
|
||||
ENGAGE[Odmeny_a_Uspechy]
|
||||
SHORT[Zkracene_odkazy]
|
||||
end
|
||||
subgraph Nastroje
|
||||
PREFETCH[Prefetch_a_Cache]
|
||||
ERRORS[Chyby]
|
||||
DOCS[Dokumentace]
|
||||
end
|
||||
subgraph Nastaveni
|
||||
SETTINGS[Nastaveni]
|
||||
USERS[Uzivatele]
|
||||
NAV[Navigace]
|
||||
ABOUT[O_klubu]
|
||||
end
|
||||
|
||||
ADASH --> Sport
|
||||
ADASH --> Obsah
|
||||
ADASH --> Media
|
||||
ADASH --> Komunikace
|
||||
ADASH --> Marketing
|
||||
ADASH --> Nastroje
|
||||
ADASH --> Nastaveni
|
||||
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,420 @@
|
||||
%%{init: {
|
||||
'theme': 'base',
|
||||
'securityLevel': 'loose',
|
||||
'flowchart': { 'curve': 'basis' },
|
||||
'themeVariables': {
|
||||
'primaryColor': '#0b5cff',
|
||||
'primaryTextColor': '#ffffff',
|
||||
'lineColor': '#64748b',
|
||||
'tertiaryColor': '#f8fafc',
|
||||
'fontSize': '12px'
|
||||
},
|
||||
'themeCSS': '.edgePath path { stroke-dasharray: 5 5; animation: dash 24s linear infinite; } @keyframes dash { to { stroke-dashoffset: 1000; } } .cluster rect { rx:8; ry:8; }'
|
||||
}}%%
|
||||
flowchart TB
|
||||
|
||||
classDef route stroke:#2563eb,fill:#eff6ff,color:#1e3a8a,stroke-width:1px;
|
||||
classDef page stroke:#0ea5e9,fill:#e0f7ff,color:#0b5cff,stroke-width:1px;
|
||||
classDef layout stroke:#6b7280,fill:#f8fafc,color:#111827,stroke-width:1px;
|
||||
classDef component stroke:#94a3b8,fill:#f1f5f9,color:#111827,stroke-width:1px;
|
||||
classDef svc stroke:#22c55e,fill:#ecfdf5,color:#065f46,stroke-width:1px;
|
||||
classDef adminsvc stroke:#16a34a,fill:#dcfce7,color:#064e3b,stroke-width:1px;
|
||||
classDef hook stroke:#f97316,fill:#fff7ed,color:#9a3412,stroke-width:1px;
|
||||
classDef ctx stroke:#8b5cf6,fill:#f5f3ff,color:#4c1d95,stroke-width:1px;
|
||||
classDef guard stroke:#ef4444,fill:#fef2f2,color:#991b1b,stroke-width:1px;
|
||||
classDef ext stroke:#a855f7,fill:#faf5ff,color:#6b21a8,stroke-width:1px;
|
||||
classDef infra stroke:#0f766e,fill:#ecfeff,color:#155e75,stroke-width:1px;
|
||||
classDef legacy stroke-dasharray:3 3,stroke:#9ca3af,fill:#fafafa,color:#6b7280;
|
||||
|
||||
%% Routing & Guards
|
||||
subgraph ROUTING [Routing]
|
||||
direction TB
|
||||
pr_admin[ProtectedRoute requiredRole=admin]:::guard
|
||||
pr_editor[ProtectedRoute requiredRole=editor]:::guard
|
||||
|
||||
subgraph ROUTES [Admin Routes]
|
||||
direction TB
|
||||
r_admin["/admin"]:::route
|
||||
r_docs["/admin/docs"]:::route
|
||||
r_about["/admin/o-klubu"]:::route
|
||||
r_videos["/admin/videa"]:::route
|
||||
r_gallery["/admin/galerie"]:::route
|
||||
r_merch["/admin/obleceni"]:::route
|
||||
r_sponsors["/admin/sponzori"]:::route
|
||||
r_matches["/admin/zapasy"]:::route
|
||||
r_players["/admin/hraci"]:::route
|
||||
r_teams["/admin/tymy"]:::route
|
||||
r_users["/admin/uzivatele"]:::route
|
||||
r_banners["/admin/bannery"]:::route
|
||||
r_messages["/admin/zpravy"]:::route
|
||||
r_settings["/admin/nastaveni"]:::route
|
||||
r_newsletter["/admin/newsletter"]:::route
|
||||
r_polls["/admin/ankety"]:::route
|
||||
r_aliases["/admin/aliasy-soutezi"]:::route
|
||||
r_prefetch["/admin/prefetch"]:::route
|
||||
r_reset["/admin/users/send-reset"]:::route
|
||||
r_score["/admin/scoreboard"]:::route
|
||||
r_score_remote["/admin/scoreboard/remote"]:::route
|
||||
r_analytics["/admin/analytika"]:::route
|
||||
r_shortlinks["/admin/shortlinks"]:::route
|
||||
r_files["/admin/soubory"]:::route
|
||||
r_contacts["/admin/kontakty"]:::route
|
||||
r_nav["/admin/navigace"]:::route
|
||||
r_comments["/admin/komentare"]:::route
|
||||
r_engagement["/admin/engagement"]:::route
|
||||
r_sweep["/admin/sweepstakes"]:::route
|
||||
r_sweep_visual["/admin/sweepstakes/:id/visual"]:::route
|
||||
r_articles["/admin/clanky"]:::route
|
||||
r_activities["/admin/aktivity"]:::route
|
||||
end
|
||||
|
||||
pr_admin --> r_admin
|
||||
pr_admin --> r_docs
|
||||
pr_admin --> r_about
|
||||
pr_admin --> r_videos
|
||||
pr_admin --> r_gallery
|
||||
pr_admin --> r_merch
|
||||
pr_admin --> r_sponsors
|
||||
pr_admin --> r_matches
|
||||
pr_admin --> r_players
|
||||
pr_admin --> r_teams
|
||||
pr_admin --> r_users
|
||||
pr_admin --> r_banners
|
||||
pr_admin --> r_messages
|
||||
pr_admin --> r_settings
|
||||
pr_admin --> r_newsletter
|
||||
pr_admin --> r_polls
|
||||
pr_admin --> r_aliases
|
||||
pr_admin --> r_prefetch
|
||||
pr_admin --> r_reset
|
||||
pr_admin --> r_score
|
||||
pr_admin --> r_score_remote
|
||||
pr_admin --> r_analytics
|
||||
pr_admin --> r_shortlinks
|
||||
pr_admin --> r_files
|
||||
pr_admin --> r_contacts
|
||||
pr_admin --> r_nav
|
||||
pr_admin --> r_comments
|
||||
pr_admin --> r_engagement
|
||||
pr_admin --> r_sweep
|
||||
pr_admin --> r_sweep_visual
|
||||
pr_editor --> r_articles
|
||||
pr_editor --> r_activities
|
||||
end
|
||||
|
||||
%% Admin UI Shell
|
||||
subgraph UI [Admin UI Shell]
|
||||
direction TB
|
||||
layout[AdminLayout]:::layout
|
||||
sidebar[AdminSidebar]:::component
|
||||
header[AdminHeader]:::component
|
||||
search[AdminSearchModal]:::component
|
||||
support[AdminSupportButton]:::component
|
||||
auth[AuthContext]:::ctx
|
||||
ups[usePublicSettings]:::hook
|
||||
|
||||
layout --> sidebar
|
||||
layout --> header
|
||||
layout --> search
|
||||
layout --> support
|
||||
layout --> auth
|
||||
layout --> ups
|
||||
end
|
||||
|
||||
%% Common Admin Hooks
|
||||
subgraph HOOKS [Common Admin Hooks]
|
||||
direction TB
|
||||
h_adminTable[useAdminTable]:::hook
|
||||
h_autoSave[useAutoSave]:::hook
|
||||
end
|
||||
|
||||
%% Admin Pages (each includes AdminLayout)
|
||||
subgraph PAGES [Admin Pages]
|
||||
direction TB
|
||||
p_dashboard[AdminDashboardPage]:::page
|
||||
p_docs[AdminDocsPage]:::page
|
||||
p_about[AboutAdminPage]:::page
|
||||
p_videos[AdminVideosPage]:::page
|
||||
p_gallery[GalleryAdminPage]:::page
|
||||
p_merch[AdminMerchPage]:::page
|
||||
p_sponsors[SponsorsAdminPage]:::page
|
||||
p_matches[MatchesAdminPage]:::page
|
||||
p_players[PlayersAdminPage]:::page
|
||||
p_teams[TeamsAdminPage]:::page
|
||||
p_users[UsersAdminPage]:::page
|
||||
p_banners[BannersAdminPage]:::page
|
||||
p_messages[MessagesAdminPage]:::page
|
||||
p_settings[SettingsAdminPage]:::page
|
||||
p_newsletter[NewsletterAdminPage]:::page
|
||||
p_polls[PollsAdminPage]:::page
|
||||
p_aliases[CompetitionAliasesAdminPage]:::page
|
||||
p_prefetch[PrefetchAdminPage]:::page
|
||||
p_reset[AdminResetPasswordPage]:::page
|
||||
p_score[ScoreboardAdminPage]:::page
|
||||
p_score_remote[MobileScoreboardControlPage]:::page
|
||||
p_analytics[AnalyticsAdminPage]:::page
|
||||
p_shortlinks[ShortlinksAdminPage]:::page
|
||||
p_files[FilesAdminPage]:::page
|
||||
p_contacts[ContactsAdminPage]:::page
|
||||
p_nav[NavigationAdminPage]:::page
|
||||
p_comments[CommentsAdminPage]:::page
|
||||
p_engagement[EngagementAdminPage]:::page
|
||||
p_sweep[SweepstakesAdminPage]:::page
|
||||
p_sweep_visual[SweepstakeVisualPage]:::page
|
||||
p_articles[ArticlesAdminPage]:::page
|
||||
p_activities[AdminActivitiesPage]:::page
|
||||
|
||||
%% Unrouted/Legacy admin pages present in codebase
|
||||
p_devdocs[DevDocsPage]:::legacy
|
||||
p_media[MediaAdminPage]:::legacy
|
||||
p_standings[StandingsAdminPage]:::legacy
|
||||
p_errors[ErrorsAdminPage]:::legacy
|
||||
p_docs_old[AdminDocsPage_Old]:::legacy
|
||||
p_dash_legacy[DashboardPage]:::legacy
|
||||
end
|
||||
|
||||
%% Route -> Page wiring
|
||||
r_admin --> p_dashboard
|
||||
r_docs --> p_docs
|
||||
r_about --> p_about
|
||||
r_videos --> p_videos
|
||||
r_gallery --> p_gallery
|
||||
r_merch --> p_merch
|
||||
r_sponsors --> p_sponsors
|
||||
r_matches --> p_matches
|
||||
r_players --> p_players
|
||||
r_teams --> p_teams
|
||||
r_users --> p_users
|
||||
r_banners --> p_banners
|
||||
r_messages --> p_messages
|
||||
r_settings --> p_settings
|
||||
r_newsletter --> p_newsletter
|
||||
r_polls --> p_polls
|
||||
r_aliases --> p_aliases
|
||||
r_prefetch --> p_prefetch
|
||||
r_reset --> p_reset
|
||||
r_score --> p_score
|
||||
r_score_remote --> p_score_remote
|
||||
r_analytics --> p_analytics
|
||||
r_shortlinks --> p_shortlinks
|
||||
r_files --> p_files
|
||||
r_contacts --> p_contacts
|
||||
r_nav --> p_nav
|
||||
r_comments --> p_comments
|
||||
r_engagement --> p_engagement
|
||||
r_sweep --> p_sweep
|
||||
r_sweep_visual --> p_sweep_visual
|
||||
r_articles --> p_articles
|
||||
r_activities --> p_activities
|
||||
|
||||
%% Pages include AdminLayout
|
||||
p_dashboard --> layout
|
||||
p_docs --> layout
|
||||
p_about --> layout
|
||||
p_videos --> layout
|
||||
p_gallery --> layout
|
||||
p_merch --> layout
|
||||
p_sponsors --> layout
|
||||
p_matches --> layout
|
||||
p_players --> layout
|
||||
p_teams --> layout
|
||||
p_users --> layout
|
||||
p_banners --> layout
|
||||
p_messages --> layout
|
||||
p_settings --> layout
|
||||
p_newsletter --> layout
|
||||
p_polls --> layout
|
||||
p_aliases --> layout
|
||||
p_prefetch --> layout
|
||||
p_reset --> layout
|
||||
p_score --> layout
|
||||
p_score_remote --> layout
|
||||
p_analytics --> layout
|
||||
p_shortlinks --> layout
|
||||
p_files --> layout
|
||||
p_contacts --> layout
|
||||
p_nav --> layout
|
||||
p_comments --> layout
|
||||
p_engagement --> layout
|
||||
p_sweep --> layout
|
||||
p_sweep_visual --> layout
|
||||
p_articles --> layout
|
||||
p_activities --> layout
|
||||
|
||||
%% Services
|
||||
subgraph SERVICES [Service Layer]
|
||||
direction TB
|
||||
s_api["api.ts (Axios + interceptors)"]:::infra
|
||||
s_settings[settings.ts]:::svc
|
||||
s_setup[setup.ts]:::svc
|
||||
s_seo[seo.ts]:::svc
|
||||
s_articles[articles.ts]:::svc
|
||||
s_files[files.ts]:::svc
|
||||
s_navigation[navigation.ts]:::svc
|
||||
s_players[players.ts]:::svc
|
||||
s_polls[polls.ts]:::svc
|
||||
s_shortlinks[shortlinks.ts]:::svc
|
||||
s_banners[banners.ts]:::svc
|
||||
s_clothing[clothing.ts]:::svc
|
||||
s_contacts[contactInfo.ts]:::svc
|
||||
s_comments_pub[comments.ts]:::svc
|
||||
s_events[eventService.ts]:::svc
|
||||
s_image[imageProcessing.ts]:::svc
|
||||
s_scoreboard[scoreboard.ts]:::svc
|
||||
s_sweep[sweepstakes.ts]:::svc
|
||||
s_youtube[youtube.ts]:::svc
|
||||
s_zonerama[zonerama.ts]:::svc
|
||||
s_analytics[analyticsService.ts]:::svc
|
||||
s_errors[errors.ts]:::svc
|
||||
s_facr_cache[facr/cache.ts]:::svc
|
||||
s_facr_api[facr/facrApi.ts]:::svc
|
||||
s_ai[ai.ts]:::svc
|
||||
|
||||
subgraph ADMIN_APIs [Admin API Modules]
|
||||
direction TB
|
||||
s_admin_comments[admin/comments.ts]:::adminsvc
|
||||
s_admin_msgs[admin/contactMessages.ts]:::adminsvc
|
||||
s_admin_eng[admin/engagement.ts]:::adminsvc
|
||||
s_admin_news[admin/newsletter.ts]:::adminsvc
|
||||
s_admin_prefetch[admin/prefetch.ts]:::adminsvc
|
||||
s_admin_matches[adminMatches.ts]:::adminsvc
|
||||
end
|
||||
end
|
||||
|
||||
%% External/Integrations
|
||||
subgraph EXTERNAL [External Systems]
|
||||
direction TB
|
||||
x_facr[FAČR API]:::ext
|
||||
x_youtube[YouTube]:::ext
|
||||
x_zonerama[Zonerama]:::ext
|
||||
x_logoapi[logoapi.sportcreative.eu]:::ext
|
||||
x_sportlogos[sportlogos.tdvorak.dev]:::ext
|
||||
x_smtp[SMTP / Email]:::ext
|
||||
x_errrev[Error Review Service]:::ext
|
||||
end
|
||||
|
||||
%% Infra
|
||||
s_api -->|interceptors, auth, csrf| s_api
|
||||
|
||||
%% Service <-> External mappings
|
||||
s_youtube --> x_youtube
|
||||
s_zonerama --> x_zonerama
|
||||
s_facr_api --> x_facr
|
||||
s_scoreboard --> x_logoapi
|
||||
s_scoreboard --> x_sportlogos
|
||||
s_admin_news --> x_smtp
|
||||
s_settings --> x_errrev
|
||||
|
||||
%% Page -> Service dependencies
|
||||
p_dashboard --> s_analytics
|
||||
p_dashboard --> s_facr_cache
|
||||
p_dashboard --> s_api
|
||||
p_docs --> s_api
|
||||
p_about --> s_settings
|
||||
p_about --> s_articles
|
||||
p_about --> s_api
|
||||
p_about --> s_ai
|
||||
p_videos --> s_settings
|
||||
p_videos --> s_admin_prefetch
|
||||
p_videos --> s_youtube
|
||||
p_gallery --> s_api
|
||||
p_gallery --> s_zonerama
|
||||
p_merch --> s_clothing
|
||||
p_sponsors --> s_sponsors
|
||||
p_sponsors --> s_articles
|
||||
p_matches --> s_admin_matches
|
||||
p_matches --> s_settings
|
||||
p_matches --> s_facr_cache
|
||||
p_matches --> s_facr_api
|
||||
p_matches --> s_comp_alias
|
||||
p_players --> s_players
|
||||
p_players --> s_image
|
||||
p_players --> s_articles
|
||||
p_teams --> s_admin_matches
|
||||
p_teams --> s_image
|
||||
p_teams --> s_facr_api
|
||||
p_teams --> s_scoreboard
|
||||
p_users --> s_admin_eng
|
||||
p_users --> s_api
|
||||
p_banners --> s_banners
|
||||
p_banners --> s_articles
|
||||
p_messages --> s_admin_msgs
|
||||
p_settings --> s_settings
|
||||
p_settings --> s_seo
|
||||
p_settings --> s_admin_prefetch
|
||||
p_settings --> s_articles
|
||||
p_newsletter --> s_admin_news
|
||||
p_newsletter --> s_settings
|
||||
p_polls --> s_polls
|
||||
p_polls --> s_categories
|
||||
p_polls --> s_events
|
||||
p_polls --> s_articles
|
||||
p_polls --> s_players
|
||||
p_aliases --> s_comp_alias
|
||||
p_aliases --> s_api
|
||||
p_prefetch --> s_admin_prefetch
|
||||
p_prefetch --> s_api
|
||||
p_reset --> s_api
|
||||
p_score --> s_scoreboard
|
||||
p_score --> s_sponsors
|
||||
p_score_remote --> s_scoreboard
|
||||
p_analytics --> s_analytics
|
||||
p_shortlinks --> s_shortlinks
|
||||
p_files --> s_files
|
||||
p_files --> s_api
|
||||
p_contacts --> s_contacts
|
||||
p_contacts --> s_settings
|
||||
p_contacts --> s_image
|
||||
p_contacts --> s_facr_cache
|
||||
p_nav --> s_navigation
|
||||
p_comments --> s_admin_comments
|
||||
p_comments --> s_comments_pub
|
||||
p_comments --> s_articles
|
||||
p_comments --> s_events
|
||||
p_comments --> s_youtube
|
||||
p_comments --> s_admin_eng
|
||||
p_engagement --> s_admin_eng
|
||||
p_sweep --> s_sweep
|
||||
p_sweep --> s_articles
|
||||
p_sweep_visual --> s_sweep
|
||||
p_sweep_visual --> s_settings
|
||||
p_articles --> s_articles
|
||||
p_articles --> s_youtube
|
||||
p_articles --> s_shortlinks
|
||||
p_articles --> s_zonerama
|
||||
p_articles --> s_settings
|
||||
p_articles --> s_facr_api
|
||||
p_articles --> s_events
|
||||
p_articles --> h_autoSave
|
||||
p_articles --> s_ai
|
||||
p_activities --> s_events
|
||||
p_activities --> s_articles
|
||||
p_activities --> s_youtube
|
||||
p_activities --> s_settings
|
||||
p_activities --> s_shortlinks
|
||||
p_activities --> s_facr_api
|
||||
p_activities --> h_autoSave
|
||||
|
||||
%% Missing simple aliases for a few symbols referenced above
|
||||
s_sponsors[sponsors.ts]:::svc
|
||||
s_categories[categories.ts]:::svc
|
||||
s_comp_alias[competitionAliases.ts]:::svc
|
||||
|
||||
%% Legacy pages light mappings
|
||||
p_errors --> s_errors
|
||||
p_errors --> s_settings
|
||||
p_media --> s_files
|
||||
p_media --> s_image
|
||||
p_standings --> s_facr_cache
|
||||
|
||||
%% UI / Guards dependencies
|
||||
sidebar --> s_navigation
|
||||
sidebar --> s_events
|
||||
sidebar --> s_settings
|
||||
pr_admin --> s_setup
|
||||
pr_editor --> s_setup
|
||||
|
||||
%% Notes:
|
||||
%% - All admin pages render AdminLayout which composes Sidebar, Header, Search modal and Support button.
|
||||
%% - Guards: routes under /admin require admin unless explicitly editor-accessible (/admin/clanky, /admin/aktivity).
|
||||
%% - Services use Axios instance with interceptors (api.ts), many pages use React Query for data fetching.
|
||||
@@ -0,0 +1,44 @@
|
||||
%%{init: {
|
||||
'theme': 'neutral'
|
||||
}}%%
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant U as User
|
||||
participant FE as Frontend (React)
|
||||
participant BE as Backend API (Gin)
|
||||
participant DB as Postgres
|
||||
|
||||
Note over FE,BE: Auth uses either HttpOnly cookie (auth_token) or Bearer token
|
||||
|
||||
U->>FE: Submit credentials (email/password)
|
||||
FE->>BE: POST /api/v1/auth/login {email, password}
|
||||
BE->>DB: Verify user by email
|
||||
DB-->>BE: User + password hash
|
||||
BE->>BE: Check password, issue JWT
|
||||
BE-->>FE: 200 OK + Set-Cookie auth_token=JWT (HttpOnly)
|
||||
|
||||
rect rgba(200, 255, 200, 0.15)
|
||||
Note over U,BE: Accessing protected endpoints
|
||||
FE->>BE: GET /api/v1/admin/... (with cookie or Authorization: Bearer)
|
||||
BE->>BE: JWTAuth parses token, loads user
|
||||
BE->>DB: SELECT users WHERE id=claims.userID
|
||||
DB-->>BE: User
|
||||
BE->>BE: RoleAuth("admin" or "editor")
|
||||
BE-->>FE: 200 OK (or 403/401)
|
||||
end
|
||||
|
||||
rect rgba(200, 200, 255, 0.15)
|
||||
Note over U,BE: Get current user
|
||||
FE->>BE: GET /api/v1/auth/me
|
||||
BE->>BE: JWTOptional (if present)
|
||||
BE-->>FE: 200 OK {user}
|
||||
end
|
||||
|
||||
rect rgba(255, 220, 200, 0.15)
|
||||
Note over U,BE: Logout
|
||||
FE->>BE: POST /api/v1/auth/logout
|
||||
BE-->>FE: 200/204 + Clear-Cookie auth_token
|
||||
end
|
||||
|
||||
Note over BE: Dev shortcuts (non-production)
|
||||
Note over BE: X-Admin-Token or X-Dev-Admin grant admin for local/dev only
|
||||
@@ -0,0 +1,42 @@
|
||||
%%{init: {
|
||||
'theme': 'forest',
|
||||
'flowchart': { 'curve': 'linear' },
|
||||
'themeCSS': '.edgePath path { stroke-dasharray: 6 4; animation: dash 20s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } }'
|
||||
}}%%
|
||||
flowchart LR
|
||||
|
||||
classDef job fill:#ecfdf5,stroke:#16a34a,color:#064e3b;
|
||||
classDef svc fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e;
|
||||
classDef ext fill:#faf5ff,stroke:#a855f7,color:#6b21a8;
|
||||
classDef api fill:#eef2ff,stroke:#6366f1,color:#312e81;
|
||||
|
||||
subgraph jobs["Background Jobs & Services"]
|
||||
direction TB
|
||||
j_prefetch["Prefetcher (StartPrefetcher)\nFetches public endpoints periodically"]:::job
|
||||
j_news_sched["NewsletterScheduler"]:::job
|
||||
j_news_auto["NewsletterAutomation\nWeekly, match alerts, blog notifications, results"]:::job
|
||||
j_sweep["SweepstakesScheduler\nFinalize & pick winners"]:::job
|
||||
j_err_auto["ErrorReview Auto-Register\nRegisters backend in monitors"]:::job
|
||||
j_filetrk["FileTracker\nScan/uploads tracking"]:::svc
|
||||
j_logo["LogoCache"]:::svc
|
||||
j_cache["CacheService"]:::svc
|
||||
j_imgopt["ImageOptimizer"]:::svc
|
||||
j_umami["UmamiService"]:::svc
|
||||
end
|
||||
|
||||
api_public["/api/v1 (public)"]:::api
|
||||
smtp["SMTP Provider"]:::ext
|
||||
err_recv["Error Receiver: errors.tdvorak.dev or local :8083"]:::ext
|
||||
facr["FACR Scraper/API"]:::ext
|
||||
umami["Umami Server"]:::ext
|
||||
|
||||
j_prefetch -.-> api_public
|
||||
j_news_sched --> j_news_auto
|
||||
j_news_auto --> smtp
|
||||
j_sweep --> smtp
|
||||
j_err_auto --> err_recv
|
||||
j_umami <---> umami
|
||||
|
||||
j_filetrk --> j_cache
|
||||
j_imgopt --> j_cache
|
||||
j_logo --> j_cache
|
||||
@@ -1,37 +0,0 @@
|
||||
graph LR
|
||||
subgraph Backend
|
||||
Router[API Router /api/v1]
|
||||
Middleware[Middleware JWT RateLimit CORS Gzip Recovery]
|
||||
Controllers[Controllers]
|
||||
Services[Services]
|
||||
Models[Models GORM]
|
||||
DB[PostgreSQL]
|
||||
Migrations[Migrations]
|
||||
Jobs[Background jobs Prefetcher Newsletter]
|
||||
Uploads[uploads static dist]
|
||||
end
|
||||
|
||||
subgraph Integrations
|
||||
FACR[FACR API]
|
||||
YT[YouTube API]
|
||||
ZON[Zonerama]
|
||||
SMTP[SMTP Email]
|
||||
MAPS[Google Maps]
|
||||
UMAMI[Umami Analytics]
|
||||
end
|
||||
|
||||
Router --> Middleware
|
||||
Router --> Controllers
|
||||
Controllers --> Services
|
||||
Services --> Models
|
||||
Models --> DB
|
||||
Migrations --> DB
|
||||
Jobs --> Services
|
||||
Jobs --> DB
|
||||
Controllers --> Uploads
|
||||
Controllers --> FACR
|
||||
Controllers --> YT
|
||||
Controllers --> ZON
|
||||
Controllers --> SMTP
|
||||
Controllers --> MAPS
|
||||
Controllers -. telemetry .-> UMAMI
|
||||
|
Before Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,64 @@
|
||||
%%{init: {
|
||||
'theme': 'base',
|
||||
'flowchart': { 'curve': 'linear' },
|
||||
'themeCSS': '.edgePath path { stroke-dasharray: 6 4; animation: dash 18s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } } .cluster rect { rx:8; ry:8; }'
|
||||
}}%%
|
||||
flowchart TB
|
||||
|
||||
classDef stage fill:#f1f5f9,stroke:#475569,color:#0f172a;
|
||||
classDef mid fill:#ecfeff,stroke:#0891b2,color:#0e7490;
|
||||
classDef api fill:#eef2ff,stroke:#6366f1,color:#312e81;
|
||||
classDef route fill:#ede9fe,stroke:#7c3aed,color:#4c1d95;
|
||||
classDef stat fill:#e2e8f0,stroke:#334155,color:#0f172a;
|
||||
|
||||
subgraph req["HTTP Request Pipeline"]
|
||||
direction TB
|
||||
client["Client"]:::stage
|
||||
router["Gin Router"]:::stage
|
||||
m_reqid["RequestID"]:::mid
|
||||
m_logger["RequestLogger"]:::mid
|
||||
m_recovery["CustomRecoveryWithReporter"]:::mid
|
||||
m_errstatus["ErrorStatusReporter"]:::mid
|
||||
m_sanitize["SanitizeHeaders"]:::mid
|
||||
m_dbctx["DBContext (with timeout)"]:::mid
|
||||
m_size["RequestSizeLimit (2MB)"]:::mid
|
||||
m_ct["ValidateContentType (JSON for mutating)"]:::mid
|
||||
m_sec["SecurityHeaders"]:::mid
|
||||
m_assets["AssetCacheControl"]:::mid
|
||||
m_cors["CORS Handler (reflect allowed origins)"]:::mid
|
||||
|
||||
client --> router
|
||||
router --> m_reqid --> m_logger --> m_recovery --> m_errstatus --> m_sanitize --> m_dbctx --> m_size --> m_ct --> m_sec --> m_assets --> m_cors
|
||||
end
|
||||
|
||||
subgraph endpoints["Endpoints"]
|
||||
direction TB
|
||||
api_v1["/api/v1"]:::api
|
||||
root["/robots.txt, /sitemap.xml, /s/:code, /r"]:::api
|
||||
|
||||
subgraph groups["API Groups"]
|
||||
direction TB
|
||||
g_public["Public"]:::route
|
||||
g_protected["Protected (JWTAuth + CSRF)"]:::route
|
||||
g_admin["Admin (Role: admin)"]:::route
|
||||
end
|
||||
|
||||
m_cors --> api_v1
|
||||
m_cors --> root
|
||||
api_v1 --> g_public --> g_protected --> g_admin
|
||||
end
|
||||
|
||||
subgraph static["Static & Assets"]
|
||||
direction TB
|
||||
s_cache["/cache -> ./cache"]:::stat
|
||||
s_uploads["/uploads -> UPLOAD_DIR"]:::stat
|
||||
s_dist["/dist -> ./static"]:::stat
|
||||
s_prem["/premium-assets -> ./pro"]:::stat
|
||||
s_metrics["/metrics (prometheus)"]:::stat
|
||||
end
|
||||
|
||||
m_cors --> s_cache
|
||||
m_cors --> s_uploads
|
||||
m_cors --> s_dist
|
||||
m_cors --> s_prem
|
||||
m_cors --> s_metrics
|
||||
@@ -0,0 +1,78 @@
|
||||
%%{init: {
|
||||
'theme': 'forest',
|
||||
'flowchart': { 'curve': 'linear' },
|
||||
'themeCSS': '.edgePath path { stroke-dasharray: 6 4; animation: dash 16s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } }'
|
||||
}}%%
|
||||
flowchart LR
|
||||
|
||||
classDef bin fill:#ffe4e6,stroke:#be123c,color:#7f1d1d;
|
||||
classDef internal fill:#e0f2fe,stroke:#0369a1,color:#0c4a6e;
|
||||
classDef pkg fill:#dcfce7,stroke:#16a34a,color:#065f46;
|
||||
classDef third fill:#ede9fe,stroke:#7c3aed,color:#4c1d95;
|
||||
|
||||
main["main.go"]:::bin
|
||||
|
||||
subgraph internal_pkgs["internal/* packages"]
|
||||
direction TB
|
||||
p_config["internal/config"]:::internal
|
||||
p_routes["internal/routes"]:::internal
|
||||
p_controllers["internal/controllers"]:::internal
|
||||
p_services["internal/services"]:::internal
|
||||
p_models["internal/models"]:::internal
|
||||
p_middleware["internal/middleware"]:::internal
|
||||
end
|
||||
|
||||
subgraph shared_pkgs["pkg/*"]
|
||||
direction TB
|
||||
p_db["pkg/database"]:::pkg
|
||||
p_email["pkg/email"]:::pkg
|
||||
p_logger["pkg/logger"]:::pkg
|
||||
end
|
||||
|
||||
subgraph third_party["third-party"]
|
||||
direction TB
|
||||
t_gin["github.com/gin-gonic/gin"]:::third
|
||||
t_gzip["github.com/gin-contrib/gzip"]:::third
|
||||
t_prom["github.com/prometheus/client_golang/promhttp"]:::third
|
||||
t_gorm["gorm.io/gorm"]:::third
|
||||
end
|
||||
|
||||
%% main dependencies
|
||||
main --> p_config
|
||||
main --> p_logger
|
||||
main --> p_db
|
||||
main --> p_models
|
||||
main --> p_middleware
|
||||
main --> p_services
|
||||
main --> p_routes
|
||||
main --> p_email
|
||||
main --> t_gin
|
||||
main --> t_gzip
|
||||
main --> t_prom
|
||||
|
||||
%% routes wiring
|
||||
p_routes --> p_controllers
|
||||
p_routes --> p_middleware
|
||||
p_routes --> p_services
|
||||
p_routes --> p_email
|
||||
p_routes --> t_gin
|
||||
p_routes --> t_gorm
|
||||
|
||||
%% controllers wiring
|
||||
p_controllers --> p_models
|
||||
p_controllers --> p_services
|
||||
|
||||
%% middleware wiring
|
||||
p_middleware --> p_config
|
||||
p_middleware --> t_gorm
|
||||
|
||||
%% services wiring
|
||||
p_services --> p_models
|
||||
p_services --> p_email
|
||||
|
||||
%% database wiring
|
||||
p_db --> p_config
|
||||
p_db --> t_gorm
|
||||
|
||||
%% logger
|
||||
p_logger --> main
|
||||
@@ -0,0 +1,113 @@
|
||||
%%{init: {"theme":"forest","flowchart":{"curve":"linear"},"themeCSS":".edgePath path { stroke-dasharray: 6 4; animation: dash 16s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } }" }}%%
|
||||
flowchart TB
|
||||
|
||||
classDef group fill:#eef7ff,stroke:#2b6cb0,color:#0b3a60;
|
||||
classDef sec fill:#fef9c3,stroke:#ca8a04,color:#7c2d12;
|
||||
classDef admin fill:#ecfdf5,stroke:#16a34a,color:#064e3b;
|
||||
classDef pub fill:#f1f5f9,stroke:#334155,color:#0f172a;
|
||||
classDef root fill:#f3e8ff,stroke:#6d28d9,color:#3b0764;
|
||||
classDef route fill:#e2e8f0,stroke:#475569,color:#111827;
|
||||
classDef ext fill:#faf5ff,stroke:#a855f7,color:#6b21a8;
|
||||
|
||||
client((Browser)):::ext
|
||||
api["/api/v1"]:::group
|
||||
rootgrp["Root"]:::group
|
||||
client ==>|HTTP| api
|
||||
client ==>|HTTP| rootgrp
|
||||
|
||||
subgraph PUBLIC["Public endpoints"]
|
||||
direction TB
|
||||
p_health["GET /health"]:::pub
|
||||
p_csrf["GET /csrf-token"]:::pub
|
||||
p_image_proxy["GET /proxy/image"]:::pub
|
||||
p_seo["GET /seo"]:::pub
|
||||
p_nav["GET /navigation, /social-links"]:::pub
|
||||
p_page_elems["GET /page-elements"]:::pub
|
||||
p_short_public["POST /shortlinks/public"]:::pub
|
||||
p_email["GET /email/open.gif | /email/click | /email/unsubscribe"]:::pub
|
||||
p_setup["GET /setup/status | POST /setup/initialize | POST /setup/validate-smtp"]:::pub
|
||||
p_errors_ingest["POST /errors (rate-limited)"]:::pub
|
||||
p_comments_list["GET /comments (JWT optional)"]:::pub
|
||||
p_eng_rewards["GET /engagement/rewards"]:::pub
|
||||
p_scoreboard_pub["GET /scoreboard | /scoreboard/sponsors | /scoreboard/qr"]:::pub
|
||||
p_settings["GET /settings"]:::pub
|
||||
p_comp_aliases["GET /competition-aliases"]:::pub
|
||||
p_team_logo_over["GET /public/team-logo-overrides"]:::pub
|
||||
p_articles["/articles: featured, list, slug/:slug, :id, read, track-view"]:::pub
|
||||
p_categories["GET /categories"]:::pub
|
||||
p_youtube["GET /youtube/videos"]:::pub
|
||||
p_about["GET /about"]:::pub
|
||||
p_teams["GET /teams, /teams/:id"]:::pub
|
||||
p_players["GET /players, /players/:id"]:::pub
|
||||
p_sponsors["GET /sponsors"]:::pub
|
||||
p_banners["GET /banners"]:::pub
|
||||
p_matches["GET /matches | /matches/history | /standings"]:::pub
|
||||
p_gallery["GET /gallery/albums | /gallery/albums/:id | /gallery/proxy-image"]:::pub
|
||||
p_zonerama["GET /zonerama/album | /zonerama-album | /zonerama/picks"]:::pub
|
||||
p_clothing["GET /clothing"]:::pub
|
||||
p_sweep_pub["GET /sweepstakes/current | /sweepstakes/:id/visual"]:::pub
|
||||
p_polls["GET /polls | /polls/:id | POST /polls/:id/vote | GET /polls/:id/results"]:::pub
|
||||
p_contact["POST /contact"]:::pub
|
||||
p_newsletter_pub["POST /newsletter/subscribe | /newsletter/unsubscribe/:email | /newsletter/setup | /newsletter/preferences"]:::pub
|
||||
p_newsletter_token["POST /newsletter/unsubscribe-token | GET /newsletter/preferences (by token)"]:::pub
|
||||
p_facr["/facr: club/search | club/:type/:id | table"]:::pub
|
||||
end
|
||||
|
||||
subgraph PROTECTED["Protected (JWTAuth + CSRF for state)"]
|
||||
direction TB
|
||||
prot_sweep["POST /sweepstakes/:id/enter | POST /sweepstakes/:id/played | GET /sweepstakes/my-winnings"]:::route
|
||||
prot_eng["Engagement: GET /leaderboard, /profile, /achievements, /transactions | POST /checkin, /article-read, /redeem | PATCH /profile, /avatar"]:::route
|
||||
prot_comments["Comments: POST /comments | PUT/DELETE /comments/:id | react/unreact | unban-request | report"]:::route
|
||||
prot_editor_preview["/editor: GET/POST /preview/:session_id | apply | delete | validate | GET /variants/:element_name"]:::sec
|
||||
prot_newsletter_me["GET /newsletter/token/me"]:::route
|
||||
prot_user["PUT /me | GET /me"]:::route
|
||||
prot_events["/events (editor): POST, PUT, DELETE"]:::sec
|
||||
prot_shortlinks["/shortlinks (editor): POST, GET"]:::sec
|
||||
prot_articles["/articles (editor): POST, PUT/:id, DELETE/:id, match-link PUT/DELETE"]:::sec
|
||||
prot_admin_groups["/admin/* groups (require admin role)"]:::admin
|
||||
end
|
||||
|
||||
subgraph ADMIN["Admin groups (JWT + Role: admin)"]
|
||||
direction TB
|
||||
ad_errors["/admin/errors: list, get, external proxies"]:::admin
|
||||
ad_comments["/admin/comments: list, status, bans, unban requests"]:::admin
|
||||
ad_comp_aliases["/admin/competition-aliases: CRUD + reorder"]:::admin
|
||||
ad_settings["/admin/settings: GET/PUT"]:::admin
|
||||
ad_about["/admin/about: GET/PUT/DELETE"]:::admin
|
||||
ad_scoreboard["/admin/scoreboard: GET/PUT + timer + sponsors + QR + presets"]:::admin
|
||||
ad_users["/admin/users: CRUD + send reset + reset by ID"]:::admin
|
||||
ad_matches["/admin/matches: merged list"]:::admin
|
||||
ad_overrides["/admin/*-overrides: GET/PUT/PATCH for match/team logos"]:::admin
|
||||
ad_contacts["/admin/contact-messages: list/get/read/forward/delete"]:::admin
|
||||
ad_newsletter["/admin/newsletter: send/test/preview/status + automation"]:::admin
|
||||
ad_notifications["/admin/notifications: competition, match"]:::admin
|
||||
ad_prefetch["/admin/prefetch: status/trigger"]:::admin
|
||||
ad_cache["/admin/cache: list/file"]:::admin
|
||||
ad_gallery["/admin/gallery: profile, fetch, refresh, delete"]:::admin
|
||||
ad_zonerama["/admin/zonerama: save-album, pick"]:::admin
|
||||
ad_seo["/admin/seo: GET/PUT"]:::admin
|
||||
ad_files["/admin/files: list/unused/duplicates/usage/usages/delete/scan/refresh-tracking"]:::admin
|
||||
ad_navigation["/admin/navigation + /admin/social-links: CRUD + reorder + seed"]:::admin
|
||||
ad_clothing["/admin/clothing: CRUD + reorder"]:::admin
|
||||
ad_polls["/admin/polls: CRUD + stats + votes"]:::admin
|
||||
ad_engagement["/admin/engagement: rewards CRUD, redemptions, leaderboard, transactions, adjust, profile"]:::admin
|
||||
ad_page_elements["/admin/page-elements: CRUD + batch"]:::admin
|
||||
ad_myuibrix["/admin/myuibrix: validate, batch-validate, preview, optimize-layout"]:::admin
|
||||
ad_shortlinks["/admin/shortlinks: create/list + stats"]:::admin
|
||||
ad_sweep["/admin/sweepstakes: CRUD + entries/winners/prizes + finalize"]:::admin
|
||||
end
|
||||
|
||||
subgraph ROOT["Root endpoints"]
|
||||
direction TB
|
||||
r_robots["GET /robots.txt"]:::root
|
||||
r_sitemap["GET /sitemap.xml"]:::root
|
||||
r_short["GET /s/:code"]:::root
|
||||
r_redirect["GET /r (tracked redirect)"]:::root
|
||||
end
|
||||
|
||||
api --> PUBLIC
|
||||
api --> PROTECTED
|
||||
api --> ADMIN
|
||||
rootgrp --> ROOT
|
||||
|
||||
note["Note: This overview groups related endpoints; see routes.go for exact definitions and middlewares."]:::route
|
||||
@@ -0,0 +1,72 @@
|
||||
%%{init: {'theme': 'neutral'}}%%
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant V as Visitor/User
|
||||
participant FE as Frontend
|
||||
participant BE as Backend API
|
||||
participant DB as Postgres
|
||||
|
||||
Note over FE,BE: Public list (JWT optional personalizes reactions)
|
||||
FE->>BE: GET /api/v1/comments
|
||||
BE->>BE: JWTOptional
|
||||
BE->>DB: Query comments + aggregates
|
||||
DB-->>BE: Rows
|
||||
BE-->>FE: 200 OK [comments]
|
||||
|
||||
rect rgba(220,255,220,0.2)
|
||||
Note over V,BE: Create/Edit/Delete comment (protected)
|
||||
FE->>BE: POST /api/v1/comments (RateLimit)
|
||||
BE->>BE: JWTAuth + CSRF
|
||||
BE->>DB: Insert Comment
|
||||
BE-->>FE: 201 Created
|
||||
|
||||
FE->>BE: PUT /api/v1/comments/:id
|
||||
BE->>BE: JWTAuth + CSRF
|
||||
BE->>DB: Update Comment
|
||||
BE-->>FE: 200 OK
|
||||
|
||||
FE->>BE: DELETE /api/v1/comments/:id
|
||||
BE->>BE: JWTAuth + CSRF
|
||||
BE->>DB: Delete Comment
|
||||
BE-->>FE: 204 No Content
|
||||
end
|
||||
|
||||
rect rgba(220,220,255,0.2)
|
||||
Note over V,BE: Reactions & unban/report actions
|
||||
FE->>BE: POST /api/v1/comments/:id/react | DELETE /comments/:id/react
|
||||
BE->>BE: JWTAuth + RateLimit
|
||||
BE->>DB: Insert/Delete reaction
|
||||
BE-->>FE: 200 OK
|
||||
|
||||
FE->>BE: POST /api/v1/comments/unban-request
|
||||
BE->>BE: JWTAuth + RateLimit
|
||||
BE->>DB: Insert UnbanRequest
|
||||
BE-->>FE: 200 OK
|
||||
|
||||
FE->>BE: POST /api/v1/comments/:id/report
|
||||
BE->>BE: JWTAuth + RateLimit
|
||||
BE->>DB: Insert CommentReport
|
||||
BE-->>FE: 200 OK
|
||||
end
|
||||
|
||||
rect rgba(255,240,220,0.2)
|
||||
Note over FE,BE: Admin moderation
|
||||
FE->>BE: GET /api/v1/admin/comments
|
||||
BE->>BE: JWTAuth + RoleAuth(admin)
|
||||
BE->>DB: List with filters
|
||||
BE-->>FE: 200 OK
|
||||
|
||||
FE->>BE: PATCH /api/v1/admin/comments/:id/status
|
||||
BE->>DB: Update status
|
||||
BE-->>FE: 200 OK
|
||||
|
||||
FE->>BE: POST /api/v1/admin/comments/ban
|
||||
BE->>DB: Insert CommentBan
|
||||
BE-->>FE: 200 OK
|
||||
|
||||
FE->>BE: GET /api/v1/admin/comments/bans
|
||||
FE->>BE: POST /api/v1/admin/comments/bans/:id/lift
|
||||
|
||||
FE->>BE: GET /api/v1/admin/comments/unban-requests
|
||||
FE->>BE: POST /api/v1/admin/comments/unban-requests/:id/resolve
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
%%{init: { 'theme': 'forest' }}%%
|
||||
erDiagram
|
||||
USER ||--o{ ARTICLE : author
|
||||
ARTICLE }o--|| CATEGORY : belongs_to
|
||||
USER ||--o{ COMMENT : writes
|
||||
COMMENT ||--o{ COMMENT_REACTION : has
|
||||
USER ||--o{ COMMENT_REACTION : reacts
|
||||
USER ||--o{ COMMENT_BAN : ban
|
||||
USER ||--o{ UNBAN_REQUEST : request
|
||||
COMMENT ||--o{ COMMENT_REPORT : reported
|
||||
|
||||
USER ||--o{ USER_PROFILE : has
|
||||
|
||||
POLL ||--o{ POLL_OPTION : has
|
||||
POLL ||--o{ POLL_VOTE : has
|
||||
USER ||--o{ POLL_VOTE : votes
|
||||
|
||||
SHORT_LINK ||--o{ LINK_CLICK : tracked
|
||||
|
||||
SWEEPSTAKE ||--o{ SWEEPSTAKE_PRIZE : has
|
||||
SWEEPSTAKE ||--o{ SWEEPSTAKE_ENTRY : has
|
||||
SWEEPSTAKE ||--o{ SWEEPSTAKE_WINNER : has
|
||||
|
||||
UPLOADED_FILE ||--o{ FILE_USAGE : used_in
|
||||
|
||||
NAVIGATION_ITEM ||--o{ ARTICLE : links
|
||||
SOCIAL_LINK ||--o{ NAVIGATION_ITEM : part_of
|
||||
|
||||
TEAM ||--o{ PLAYER : has
|
||||
COMPETITION_ALIAS ||--o{ TEAM : member_of
|
||||
SCOREBOARD_STATE ||..|| ARTICLE : references
|
||||
|
||||
CONTACT ||--o{ CONTACT_MESSAGE : has
|
||||
CONTACT_CATEGORY ||--o{ CONTACT : categorizes
|
||||
NEWSLETTER_SUBSCRIPTION ||..|| CONTACT : same_person
|
||||
|
||||
ERROR_EVENT ||..|| USER : context
|
||||
|
||||
%% Note: Names reflect models; exact FKs are simplified for overview
|
||||
@@ -0,0 +1,122 @@
|
||||
%%{init: {
|
||||
'theme': 'neutral',
|
||||
'flowchart': { 'curve': 'linear' },
|
||||
'themeCSS': '.edgePath path { stroke-dasharray: 6 4; animation: dash 18s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } }'
|
||||
}}%%
|
||||
flowchart TB
|
||||
|
||||
classDef grp fill:#0f172a,stroke:#334155,color:#e5e7eb;
|
||||
classDef model fill:#111827,stroke:#475569,color:#e5e7eb;
|
||||
|
||||
subgraph CORE["Core"]
|
||||
direction LR
|
||||
m_settings["Settings"]:::model
|
||||
m_user["User"]:::model
|
||||
m_user_profile["UserProfile"]:::model
|
||||
end
|
||||
|
||||
subgraph CONTENT["Content"]
|
||||
direction LR
|
||||
m_article["Article"]:::model
|
||||
m_category["Category"]:::model
|
||||
m_page_el["PageElementConfig"]:::model
|
||||
m_uploaded["UploadedFile"]:::model
|
||||
m_file_usage["FileUsage"]:::model
|
||||
end
|
||||
|
||||
subgraph NAV["Navigation"]
|
||||
direction LR
|
||||
m_nav_item["NavigationItem"]:::model
|
||||
m_social["SocialLink"]:::model
|
||||
m_short["ShortLink"]:::model
|
||||
m_click["LinkClick"]:::model
|
||||
end
|
||||
|
||||
subgraph MATCHES["Matches & Teams"]
|
||||
direction LR
|
||||
m_team["Team"]:::model
|
||||
m_player["Player"]:::model
|
||||
m_match_over["MatchOverride"]:::model
|
||||
m_team_logo_over["TeamLogoOverride"]:::model
|
||||
m_comp_alias["CompetitionAlias"]:::model
|
||||
m_score_state["ScoreboardState"]:::model
|
||||
end
|
||||
|
||||
subgraph POLLS["Polls"]
|
||||
direction LR
|
||||
m_poll["Poll"]:::model
|
||||
m_poll_opt["PollOption"]:::model
|
||||
m_poll_vote["PollVote"]:::model
|
||||
end
|
||||
|
||||
subgraph SWEEPSTAKES["Sweepstakes"]
|
||||
direction LR
|
||||
m_sw["Sweepstake"]:::model
|
||||
m_sw_prize["SweepstakePrize"]:::model
|
||||
m_sw_entry["SweepstakeEntry"]:::model
|
||||
m_sw_winner["SweepstakeWinner"]:::model
|
||||
end
|
||||
|
||||
subgraph COMMENTS["Comments & Moderation"]
|
||||
direction LR
|
||||
m_comment["Comment"]:::model
|
||||
m_comment_react["CommentReaction"]:::model
|
||||
m_comment_ban["CommentBan"]:::model
|
||||
m_unban_req["UnbanRequest"]:::model
|
||||
m_comment_rep["CommentReport"]:::model
|
||||
end
|
||||
|
||||
subgraph CONTACTS["Contacts & Newsletter"]
|
||||
direction LR
|
||||
m_contact_cat["ContactCategory"]:::model
|
||||
m_contact["Contact"]:::model
|
||||
m_contact_msg["ContactMessage"]:::model
|
||||
m_news_sub["NewsletterSubscription"]:::model
|
||||
m_email_log["EmailLog (models.email)"]:::model
|
||||
end
|
||||
|
||||
subgraph ENGAGE["Engagement & Rewards"]
|
||||
direction LR
|
||||
m_points_tx["PointsTransaction"]:::model
|
||||
m_ach["Achievement"]:::model
|
||||
m_user_ach["UserAchievement"]:::model
|
||||
m_reward_item["RewardItem"]:::model
|
||||
m_reward_red["RewardRedemption"]:::model
|
||||
end
|
||||
|
||||
subgraph SHOP["Shop"]
|
||||
direction LR
|
||||
m_cloth["Clothing"]:::model
|
||||
end
|
||||
|
||||
subgraph GALLERY["Gallery"]
|
||||
direction LR
|
||||
m_zonerama_album["ZoneramaAlbum (derived)"]:::model
|
||||
end
|
||||
|
||||
subgraph ERRORS["Error Tracking"]
|
||||
direction LR
|
||||
m_error["ErrorEvent"]:::model
|
||||
end
|
||||
|
||||
%% (Optional) Indicative relationships (no strict cardinalities)
|
||||
%% Using simple arrows to avoid ER notation parse issues
|
||||
m_article --> m_category
|
||||
m_article --> m_user
|
||||
m_file_usage --> m_uploaded
|
||||
m_click --> m_short
|
||||
m_comment_react --> m_comment
|
||||
m_comment_ban --> m_user
|
||||
m_unban_req --> m_user
|
||||
m_comment_rep --> m_comment
|
||||
m_user_profile --> m_user
|
||||
m_points_tx --> m_user
|
||||
m_user_ach --> m_user
|
||||
m_user_ach --> m_ach
|
||||
m_reward_red --> m_reward_item
|
||||
m_sw_entry --> m_sw
|
||||
m_sw_winner --> m_sw
|
||||
m_poll_opt --> m_poll
|
||||
m_poll_vote --> m_poll
|
||||
m_contact_msg --> m_contact
|
||||
m_nav_item --> m_article
|
||||
@@ -1,79 +0,0 @@
|
||||
# ER Diagram
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USERS ||--o{ ARTICLES : author_id
|
||||
CATEGORIES ||--o{ ARTICLES : category_id
|
||||
ARTICLES ||--o{ ARTICLE_TEAM_LINKS : article_id
|
||||
ARTICLES ||--o{ ARTICLE_MATCH_LINKS : article_id
|
||||
|
||||
TEAMS ||--o{ PLAYERS : team_id
|
||||
|
||||
USERS ||--o{ EVENTS : created_by_id
|
||||
EVENTS ||--o{ EVENT_ATTACHMENTS : event_id
|
||||
|
||||
POLLS ||--o{ POLL_OPTIONS : poll_id
|
||||
POLLS ||--o{ POLL_VOTES : poll_id
|
||||
POLL_OPTIONS ||--o{ POLL_VOTES : option_id
|
||||
USERS o{--o| POLL_VOTES : user_id
|
||||
CATEGORIES o{--o| POLLS : category_id
|
||||
ARTICLES o{--o| POLLS : related_article_id
|
||||
EVENTS o{--o| POLLS : related_event_id
|
||||
PLAYERS o{--o| POLL_OPTIONS : player_id
|
||||
|
||||
USERS ||--|| USER_PROFILES : user_id
|
||||
USERS ||--o{ PASSWORD_RESETS : user_id
|
||||
USERS ||--o{ COMMENTS : user_id
|
||||
COMMENTS o{--o| COMMENTS : parent_id
|
||||
USERS ||--o{ COMMENT_BANS : user_id
|
||||
USERS ||--o{ UNBAN_REQUESTS : user_id
|
||||
COMMENTS ||--o{ COMMENT_REACTIONS : comment_id
|
||||
COMMENTS ||--o{ COMMENT_REPORTS : comment_id
|
||||
|
||||
USERS o{--o| UPLOADED_FILES : uploaded_by_id
|
||||
UPLOADED_FILES ||--o{ FILE_USAGES : file_id
|
||||
|
||||
CONTACT_CATEGORIES o{--o| CONTACTS : category_id
|
||||
|
||||
SHORT_LINKS o{--o| LINK_CLICKS : short_link_id
|
||||
USERS o{--o| SHORT_LINKS : created_by_id
|
||||
|
||||
EMAIL_LOGS ||--o{ EMAIL_EVENTS : email_log_id
|
||||
ARTICLES ||--o{ BLOG_NOTIFICATIONS : article_id
|
||||
|
||||
SWEEPSTAKES ||--o{ SWEEPSTAKE_PRIZES : sweepstake_id
|
||||
SWEEPSTAKES ||--o{ SWEEPSTAKE_ENTRIES : sweepstake_id
|
||||
SWEEPSTAKES ||--o{ SWEEPSTAKE_WINNERS : sweepstake_id
|
||||
USERS ||--o{ SWEEPSTAKE_ENTRIES : user_id
|
||||
USERS ||--o{ SWEEPSTAKE_WINNERS : user_id
|
||||
SWEEPSTAKE_ENTRIES ||--o{ SWEEPSTAKE_WINNERS : entry_id
|
||||
SWEEPSTAKE_PRIZES |o--o{ SWEEPSTAKE_WINNERS : prize_id
|
||||
|
||||
USERS ||--o{ POINTS_TRANSACTIONS : user_id
|
||||
USERS ||--o{ USER_ACHIEVEMENTS : user_id
|
||||
ACHIEVEMENTS ||--o{ USER_ACHIEVEMENTS : achievement_id
|
||||
REWARD_ITEMS ||--o{ REWARD_REDEMPTIONS : reward_id
|
||||
USERS ||--o{ REWARD_REDEMPTIONS : user_id
|
||||
|
||||
USERS o{--o| AUDIT_LOGS : user_id
|
||||
USERS o{--o| ERROR_EVENTS : user_id
|
||||
USERS o{--o| VISITOR_EVENTS : user_id
|
||||
|
||||
SETUP_INFO ||--o{ CLUB_INFO : setup_info_id
|
||||
|
||||
NAVIGATION_ITEMS o{--o| NAVIGATION_ITEMS : parent_id
|
||||
|
||||
%% Standalone/core tables (configured/consumed by services)
|
||||
SETTINGS
|
||||
ABOUT_PAGES
|
||||
SPONSORS
|
||||
BANNERS
|
||||
CLOTHING
|
||||
COMPETITION_ALIASES
|
||||
MATCH_OVERRIDES
|
||||
TEAM_LOGO_OVERRIDES
|
||||
NEWSLETTER_SUBSCRIPTIONS
|
||||
NEWSLETTER_SENT_LOG
|
||||
MATCH_NOTIFICATIONS
|
||||
SCOREBOARD_STATES
|
||||
```
|
||||
@@ -1,75 +0,0 @@
|
||||
erDiagram
|
||||
USERS ||--o{ ARTICLES : author_id
|
||||
CATEGORIES ||--o{ ARTICLES : category_id
|
||||
ARTICLES ||--o{ ARTICLE_TEAM_LINKS : article_id
|
||||
ARTICLES ||--o{ ARTICLE_MATCH_LINKS : article_id
|
||||
|
||||
TEAMS ||--o{ PLAYERS : team_id
|
||||
|
||||
USERS ||--o{ EVENTS : created_by_id
|
||||
EVENTS ||--o{ EVENT_ATTACHMENTS : event_id
|
||||
|
||||
POLLS ||--o{ POLL_OPTIONS : poll_id
|
||||
POLLS ||--o{ POLL_VOTES : poll_id
|
||||
POLL_OPTIONS ||--o{ POLL_VOTES : option_id
|
||||
USERS |o--o{ POLL_VOTES : user_id
|
||||
CATEGORIES |o--o{ POLLS : category_id
|
||||
ARTICLES |o--o{ POLLS : related_article_id
|
||||
EVENTS |o--o{ POLLS : related_event_id
|
||||
PLAYERS |o--o{ POLL_OPTIONS : player_id
|
||||
|
||||
USERS ||--|| USER_PROFILES : user_id
|
||||
USERS ||--o{ PASSWORD_RESETS : user_id
|
||||
USERS ||--o{ COMMENTS : user_id
|
||||
COMMENTS |o--o{ COMMENTS : parent_id
|
||||
USERS ||--o{ COMMENT_BANS : user_id
|
||||
USERS ||--o{ UNBAN_REQUESTS : user_id
|
||||
COMMENTS ||--o{ COMMENT_REACTIONS : comment_id
|
||||
COMMENTS ||--o{ COMMENT_REPORTS : comment_id
|
||||
|
||||
USERS |o--o{ UPLOADED_FILES : uploaded_by_id
|
||||
UPLOADED_FILES ||--o{ FILE_USAGES : file_id
|
||||
|
||||
CONTACT_CATEGORIES |o--o{ CONTACTS : category_id
|
||||
|
||||
SHORT_LINKS |o--o{ LINK_CLICKS : short_link_id
|
||||
USERS |o--o{ SHORT_LINKS : created_by_id
|
||||
|
||||
EMAIL_LOGS ||--o{ EMAIL_EVENTS : email_log_id
|
||||
ARTICLES ||--o{ BLOG_NOTIFICATIONS : article_id
|
||||
|
||||
SWEEPSTAKES ||--o{ SWEEPSTAKE_PRIZES : sweepstake_id
|
||||
SWEEPSTAKES ||--o{ SWEEPSTAKE_ENTRIES : sweepstake_id
|
||||
SWEEPSTAKES ||--o{ SWEEPSTAKE_WINNERS : sweepstake_id
|
||||
USERS ||--o{ SWEEPSTAKE_ENTRIES : user_id
|
||||
USERS ||--o{ SWEEPSTAKE_WINNERS : user_id
|
||||
SWEEPSTAKE_ENTRIES ||--o{ SWEEPSTAKE_WINNERS : entry_id
|
||||
SWEEPSTAKE_PRIZES |o--o{ SWEEPSTAKE_WINNERS : prize_id
|
||||
|
||||
USERS ||--o{ POINTS_TRANSACTIONS : user_id
|
||||
USERS ||--o{ USER_ACHIEVEMENTS : user_id
|
||||
ACHIEVEMENTS ||--o{ USER_ACHIEVEMENTS : achievement_id
|
||||
REWARD_ITEMS ||--o{ REWARD_REDEMPTIONS : reward_id
|
||||
USERS ||--o{ REWARD_REDEMPTIONS : user_id
|
||||
|
||||
USERS |o--o{ AUDIT_LOGS : user_id
|
||||
USERS |o--o{ ERROR_EVENTS : user_id
|
||||
USERS |o--o{ VISITOR_EVENTS : user_id
|
||||
|
||||
SETUP_INFO ||--o{ CLUB_INFO : setup_info_id
|
||||
|
||||
NAVIGATION_ITEMS |o--o{ NAVIGATION_ITEMS : parent_id
|
||||
|
||||
%% Standalone/core tables (configured/consumed by services)
|
||||
SETTINGS
|
||||
ABOUT_PAGES
|
||||
SPONSORS
|
||||
BANNERS
|
||||
CLOTHING
|
||||
COMPETITION_ALIASES
|
||||
MATCH_OVERRIDES
|
||||
TEAM_LOGO_OVERRIDES
|
||||
NEWSLETTER_SUBSCRIPTIONS
|
||||
NEWSLETTER_SENT_LOG
|
||||
MATCH_NOTIFICATIONS
|
||||
SCOREBOARD_STATES
|
||||
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 91 KiB |
@@ -0,0 +1,29 @@
|
||||
%%{init: {'theme': 'neutral'}}%%
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant FE as Frontend (React)
|
||||
participant BE as Backend API
|
||||
participant ER as Error Receiver (errors.tdvorak.dev or :8083)
|
||||
participant EV as Error Review Admin UI
|
||||
|
||||
Note over FE: JS errors captured (window.onerror,<br/>unhandledrejection, manual report)
|
||||
FE->>BE: POST /api/v1/errors {event}
|
||||
BE->>BE: RateLimit(120/min)
|
||||
BE->>BE: Validate & normalize
|
||||
alt External ingest configured
|
||||
BE->>ER: POST /api/v1/errors (Bearer/X-Ingest-Token)
|
||||
ER-->>BE: 202 Accepted {request_id}
|
||||
else Local DB fallback
|
||||
BE->>BE: Store as ErrorEvent (DB)
|
||||
end
|
||||
BE-->>FE: 200 OK
|
||||
|
||||
Note over ER,EV: Admin inspects
|
||||
EV->>ER: GET /admin/api/errors
|
||||
ER-->>EV: List, details
|
||||
|
||||
rect rgba(240,240,255,0.2)
|
||||
Note over BE,EV: Auto-register monitor (background)
|
||||
BE->>ER: Register/heartbeat monitor (retry)
|
||||
EV->>ER: Autologin redirect injects token (local dev)
|
||||
end
|
||||
@@ -0,0 +1,108 @@
|
||||
%%{init: {
|
||||
'theme': 'forest',
|
||||
'flowchart': { 'curve': 'linear' },
|
||||
'themeCSS': '.edgePath path { stroke-dasharray: 6 4; animation: dash 16s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } }'
|
||||
}}%%
|
||||
flowchart LR
|
||||
|
||||
classDef svc fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e;
|
||||
classDef admin fill:#dcfce7,stroke:#16a34a,color:#065f46;
|
||||
classDef pub fill:#f1f5f9,stroke:#334155,color:#0f172a;
|
||||
classDef core fill:#ede9fe,stroke:#7c3aed,color:#4c1d95;
|
||||
|
||||
api_core["services/api.ts (Axios core)"]:::core
|
||||
|
||||
subgraph Services
|
||||
direction TB
|
||||
s_settings[settings.ts]:::svc
|
||||
s_page_elements[pageElements.ts]:::svc
|
||||
s_articles[articles.ts]:::svc
|
||||
s_players[players.ts]:::svc
|
||||
s_sponsors[sponsors.ts]:::svc
|
||||
s_banners[banners.ts]:::svc
|
||||
s_comp_alias[competitionAliases.ts]:::svc
|
||||
s_events[eventService.ts]:::svc
|
||||
s_setup[setup.ts]:::svc
|
||||
s_engagement[engagement.ts]:::svc
|
||||
s_action[actionLog.ts]:::svc
|
||||
s_facr[facr/facrApi.ts]:::svc
|
||||
s_files[files.ts]:::svc
|
||||
s_image[imageProcessing.ts]:::svc
|
||||
s_shortlinks[shortlinks.ts]:::svc
|
||||
s_scoreboard[scoreboard.ts]:::svc
|
||||
s_youtube[youtube.ts]:::svc
|
||||
s_zonerama[zonerama.ts]:::svc
|
||||
s_errors[errors.ts]:::svc
|
||||
s_contactInfo[contactInfo.ts]:::svc
|
||||
s_public[public.ts]:::svc
|
||||
s_editor[editorController.ts]:::svc
|
||||
end
|
||||
|
||||
subgraph AdminAPIs
|
||||
direction TB
|
||||
s_admin_comments[admin/comments.ts]:::admin
|
||||
s_admin_msgs[admin/contactMessages.ts]:::admin
|
||||
s_admin_eng[admin/engagement.ts]:::admin
|
||||
s_admin_news[admin/newsletter.ts]:::admin
|
||||
s_admin_prefetch[admin/prefetch.ts]:::admin
|
||||
s_admin_matches[adminMatches.ts]:::admin
|
||||
end
|
||||
|
||||
api_core --> Services
|
||||
api_core --> AdminAPIs
|
||||
|
||||
subgraph PublicEndpoints["Representative public endpoints"]
|
||||
direction TB
|
||||
e_articles["/articles, /articles/slug/:slug, /articles/:id"]:::pub
|
||||
e_featured["/articles/featured"]:::pub
|
||||
e_players["/players, /players/:id"]:::pub
|
||||
e_teams["/teams, /teams/:id"]:::pub
|
||||
e_scores["/matches, /standings, /matches/history"]:::pub
|
||||
e_gallery["/gallery/albums, /gallery/albums/:id"]:::pub
|
||||
e_youtube["/youtube/videos"]:::pub
|
||||
e_settings["/settings"]:::pub
|
||||
e_scoreboard_pub["/scoreboard, /scoreboard/sponsors, /scoreboard/qr"]:::pub
|
||||
e_contact["/contact"]:::pub
|
||||
e_short_pub["/shortlinks/public"]:::pub
|
||||
e_polls["/polls, /polls/:id, /polls/:id/vote, /polls/:id/results"]:::pub
|
||||
end
|
||||
|
||||
%% Map key services to endpoints
|
||||
s_articles --> e_articles
|
||||
s_articles --> e_featured
|
||||
s_players --> e_players
|
||||
s_settings --> e_settings
|
||||
s_page_elements --> e_settings
|
||||
s_comp_alias --> e_scores
|
||||
s_events --> e_scores
|
||||
s_zonerama --> e_gallery
|
||||
s_youtube --> e_youtube
|
||||
s_scoreboard --> e_scoreboard_pub
|
||||
s_contactInfo --> e_contact
|
||||
s_shortlinks --> e_short_pub
|
||||
s_public --> e_settings
|
||||
|
||||
subgraph AdminEndpoints["Representative admin endpoints"]
|
||||
direction TB
|
||||
a_settings["/admin/settings"]:::admin
|
||||
a_files["/admin/files"]:::admin
|
||||
a_nav["/admin/navigation, /admin/social-links"]:::admin
|
||||
a_comments["/admin/comments"]:::admin
|
||||
a_msgs["/admin/contact-messages"]:::admin
|
||||
a_news["/admin/newsletter"]:::admin
|
||||
a_matches["/admin/matches"]:::admin
|
||||
a_sweep["/admin/sweepstakes"]:::admin
|
||||
a_scoreboard["/admin/scoreboard"]:::admin
|
||||
a_shortlinks["/admin/shortlinks"]:::admin
|
||||
end
|
||||
|
||||
s_admin_comments --> a_comments
|
||||
s_admin_msgs --> a_msgs
|
||||
s_admin_eng --> a_settings
|
||||
s_admin_news --> a_news
|
||||
s_admin_prefetch --> a_settings
|
||||
s_admin_matches --> a_matches
|
||||
s_files --> a_files
|
||||
s_navigation[navigation.ts]:::svc --> a_nav
|
||||
s_shortlinks --> a_shortlinks
|
||||
s_scoreboard --> a_scoreboard
|
||||
@@ -0,0 +1,39 @@
|
||||
flowchart TD
|
||||
%% Provider & runtime architecture
|
||||
classDef infra fill:#1f2835,stroke:#5b6e8a,color:#e8eaf0;
|
||||
classDef ctx fill:#2b233f,stroke:#7a63a0,color:#e8eaf0;
|
||||
classDef comp fill:#1d2a2a,stroke:#3d7a6a,color:#e8eaf0;
|
||||
|
||||
subgraph Entry[index.tsx]
|
||||
RootDOM[(#root)]:::infra --> ErrorBoundary:::comp --> ColorModeScript:::infra --> AppLazy[App.lazy]:::infra
|
||||
ServiceWorker[serviceWorkerRegistration]:::infra
|
||||
ErrorReporter[services/errorReporter.installGlobalErrorHandlers]:::infra
|
||||
end
|
||||
|
||||
subgraph Providers
|
||||
Chakra[ChakraProvider]:::infra --> RQ[QueryClientProvider]:::infra --> Router[BrowserRouter]:::infra --> AuthProv[AuthProvider]:::ctx --> ClubThemeProv[ClubThemeProvider]:::ctx --> Helmet[HelmetProvider]:::infra --> Suspense:::infra --> Routes:::infra
|
||||
DefaultSEO:::comp
|
||||
CookieBanner:::comp
|
||||
end
|
||||
|
||||
AppLazy --> Chakra
|
||||
AppLazy --> DefaultSEO
|
||||
AppLazy --> CookieBanner
|
||||
|
||||
subgraph Routing
|
||||
Routes:::infra --> PublicRoutes
|
||||
Routes:::infra --> AdminRoutes
|
||||
ProtectedRoute:::comp
|
||||
end
|
||||
|
||||
AuthProv --> ProtectedRoute
|
||||
|
||||
subgraph PublicRoutes
|
||||
HomeRoute
|
||||
BlogRoute
|
||||
OtherPublic[(other public pages)]
|
||||
end
|
||||
|
||||
subgraph AdminRoutes
|
||||
AdminPages[(admin pages...)]
|
||||
end
|
||||
@@ -0,0 +1,225 @@
|
||||
%%{init: {"theme":"forest","flowchart":{"curve":"linear"},"themeCSS":".edgePath path { stroke-dasharray: 6 4; animation: dash 16s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } }" }}%%
|
||||
flowchart LR
|
||||
%% Everything Graph: combined overview of frontend
|
||||
classDef cluster fill:#eef7ff,stroke:#2b6cb0,color:#0b3a60;
|
||||
classDef page fill:#fff7ed,stroke:#f59e0b,color:#7c2d12;
|
||||
classDef comp fill:#ecfdf5,stroke:#16a34a,color:#064e3b;
|
||||
classDef ctx fill:#f3e8ff,stroke:#6d28d9,color:#3b0764;
|
||||
classDef hook fill:#fef9c3,stroke:#ca8a04,color:#7c2d12;
|
||||
classDef svc fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e;
|
||||
classDef util fill:#e2e8f0,stroke:#475569,color:#111827;
|
||||
classDef infra fill:#e3f2fd,stroke:#1e88e5,color:#0c4a6e;
|
||||
|
||||
%% Entry & Providers
|
||||
subgraph Entry[Entry / Boot]
|
||||
index[index.tsx]:::infra --> AppLazy[App.lazy.tsx]:::infra
|
||||
index --> ErrorBoundary:::comp
|
||||
index --> ServiceWorker[serviceWorkerRegistration]:::infra
|
||||
end
|
||||
|
||||
subgraph Providers[Providers]
|
||||
Chakra[ChakraProvider]:::infra --> RQ[QueryClientProvider]:::infra --> Router[BrowserRouter]:::infra --> AuthProv[AuthProvider]:::ctx --> ClubThemeProv[ClubThemeProvider]:::ctx --> Helmet[HelmetProvider]:::infra --> Suspense:::infra --> Routes:::infra
|
||||
DefaultSEO:::comp
|
||||
CookieBanner:::comp
|
||||
end
|
||||
|
||||
AppLazy --> Chakra
|
||||
AppLazy --> DefaultSEO
|
||||
AppLazy --> CookieBanner
|
||||
Router --> Routes
|
||||
|
||||
%% Routing
|
||||
subgraph Routing[Routes]
|
||||
HomeRoute:::comp --> HomePage
|
||||
BlogRoute:::comp --> BlogPage
|
||||
NotFoundRoute:::comp --> NotFoundPage
|
||||
ProtectedRoute:::comp
|
||||
end
|
||||
|
||||
AuthProv --> ProtectedRoute
|
||||
|
||||
%% Pages
|
||||
subgraph Pages
|
||||
HomePage:::page
|
||||
BlogPage:::page
|
||||
ArticleDetailPage:::page
|
||||
ActivityDetailPage:::page
|
||||
MatchDetailPage:::page
|
||||
ClubPage:::page
|
||||
AboutPage:::page
|
||||
CalendarPage:::page
|
||||
ActivitiesCalendarPage:::page
|
||||
TablesPage:::page
|
||||
MatchesPage:::page
|
||||
PlayersPage:::page
|
||||
PlayerDetailPage:::page
|
||||
SponsorsPage:::page
|
||||
ContactPage:::page
|
||||
GalleryPage:::page
|
||||
AlbumDetailPage:::page
|
||||
VideosPage:::page
|
||||
SearchPage:::page
|
||||
ClothingPage:::page
|
||||
PollsPage:::page
|
||||
OverlayScoreboardPage:::page
|
||||
OverlaySponsorsPage:::page
|
||||
ForbiddenPage:::page
|
||||
SetupPage:::page
|
||||
StylePreviewPage:::page
|
||||
AuthPage:::page
|
||||
RegisterPage:::page
|
||||
ForgotPasswordPage:::page
|
||||
ResetPasswordPage:::page
|
||||
NewsletterUnsubscribePage:::page
|
||||
NewsletterPreferencesPage:::page
|
||||
SemiAdminPage:::page
|
||||
end
|
||||
|
||||
%% Admin Pages
|
||||
subgraph Admin[Admin]
|
||||
AdminDashboardPage:::page
|
||||
AdminDocsPage:::page
|
||||
AboutAdminPage:::page
|
||||
AdminVideosPage:::page
|
||||
GalleryAdminPage:::page
|
||||
AdminMerchPage:::page
|
||||
SponsorsAdminPage:::page
|
||||
MatchesAdminPage:::page
|
||||
PlayersAdminPage:::page
|
||||
TeamsAdminPage:::page
|
||||
UsersAdminPage:::page
|
||||
BannersAdminPage:::page
|
||||
MessagesAdminPage:::page
|
||||
SettingsAdminPage:::page
|
||||
NewsletterAdminPage:::page
|
||||
PollsAdminPage:::page
|
||||
CompetitionAliasesAdminPage:::page
|
||||
PrefetchAdminPage:::page
|
||||
AdminResetPasswordPage:::page
|
||||
ScoreboardAdminPage:::page
|
||||
MobileScoreboardControlPage:::page
|
||||
AnalyticsAdminPage:::page
|
||||
ErrorsAdminPage:::page
|
||||
FilesAdminPage:::page
|
||||
ContactsAdminPage:::page
|
||||
NavigationAdminPage:::page
|
||||
CommentsAdminPage:::page
|
||||
ShortlinksAdminPage:::page
|
||||
EngagementAdminPage:::page
|
||||
SweepstakesAdminPage:::page
|
||||
SweepstakeVisualPage:::page
|
||||
end
|
||||
|
||||
%% Components (subset of key ones)
|
||||
subgraph Components
|
||||
MainLayout[components/layout/MainLayout]:::comp
|
||||
ClubHeroTopbar[components/home/ClubHeroTopbar]:::comp
|
||||
BannerDisplay[components/banners/BannerDisplay]:::comp
|
||||
BlogCardsScroller[components/home/BlogCardsScroller]:::comp
|
||||
BlogSwiper[components/home/BlogSwiper]:::comp
|
||||
VideosSection[components/home/VideosSection]:::comp
|
||||
MerchSection[components/home/MerchSection]:::comp
|
||||
PollsWidget[components/home/PollsWidget]:::comp
|
||||
GallerySection[components/home/GallerySection]:::comp
|
||||
NewsletterSubscribe[components/newsletter/NewsletterSubscribe]:::comp
|
||||
NewsList[components/pack/NewsList]:::comp
|
||||
StandingsCard[components/pack/StandingsCard]:::comp
|
||||
NextMatch[components/pack/NextMatch]:::comp
|
||||
MatchesSlider[components/pack/MatchesSlider]:::comp
|
||||
ActivitiesList[components/pack/ActivitiesList]:::comp
|
||||
TeamLogo[components/common/TeamLogo]:::comp
|
||||
SweepstakeWidget[components/sweepstakes/SweepstakeWidget]:::comp
|
||||
ClubModal[components/home/ClubModal]:::comp
|
||||
MatchModal[components/home/MatchModal]:::comp
|
||||
end
|
||||
|
||||
HomePage --> MainLayout
|
||||
HomePage --> ClubHeroTopbar
|
||||
HomePage --> BannerDisplay
|
||||
HomePage --> BlogCardsScroller
|
||||
HomePage --> BlogSwiper
|
||||
HomePage --> VideosSection
|
||||
HomePage --> MerchSection
|
||||
HomePage --> PollsWidget
|
||||
HomePage --> GallerySection
|
||||
HomePage --> NewsletterSubscribe
|
||||
HomePage --> NewsList
|
||||
HomePage --> StandingsCard
|
||||
HomePage --> NextMatch
|
||||
HomePage --> MatchesSlider
|
||||
HomePage --> ActivitiesList
|
||||
HomePage -. uses .- TeamLogo
|
||||
HomePage --> SweepstakeWidget
|
||||
HomePage --> ClubModal
|
||||
HomePage --> MatchModal
|
||||
|
||||
%% Contexts & Hooks
|
||||
subgraph Contexts
|
||||
AuthContext[contexts/AuthContext]:::ctx
|
||||
ClubThemeContext[contexts/ClubThemeContext]:::ctx
|
||||
end
|
||||
|
||||
subgraph Hooks
|
||||
usePublicSettings[hooks/usePublicSettings]:::hook
|
||||
useFontLoader[hooks/useFontLoader]:::hook
|
||||
useUmami[hooks/useUmami]:::hook
|
||||
usePageElementConfig[hooks/usePageElementConfig]:::hook
|
||||
useAllPageElementConfigs[hooks/usePageElementConfig.useAllPageElementConfigs]:::hook
|
||||
end
|
||||
|
||||
Providers --> Contexts
|
||||
Pages --> Contexts
|
||||
Pages --> Hooks
|
||||
|
||||
%% Services & Utils
|
||||
subgraph Services
|
||||
apiCore["services/api (API_URL)"]:::svc
|
||||
errorReporter[services/errorReporter]:::svc
|
||||
settingsSvc[services/settings]:::svc
|
||||
pageElementsSvc[services/pageElements]:::svc
|
||||
articlesSvc[services/articles]:::svc
|
||||
playersSvc[services/players]:::svc
|
||||
sponsorsSvc[services/sponsors]:::svc
|
||||
bannersSvc[services/banners]:::svc
|
||||
compAliasesSvc[services/competitionAliases]:::svc
|
||||
eventsSvc[services/eventService]:::svc
|
||||
setupSvc[services/setup]:::svc
|
||||
engagementSvc[services/engagement]:::svc
|
||||
actionLogSvc[services/actionLog]:::svc
|
||||
facrApi[services/facr/facrApi]:::svc
|
||||
end
|
||||
|
||||
subgraph Utils
|
||||
urlUtil[utils/url]:::util
|
||||
nationalityUtil[utils/nationality]:::util
|
||||
colorsUtil[utils/colors]:::util
|
||||
logosUtil[utils/sportLogosAPI]:::util
|
||||
end
|
||||
|
||||
Pages --> apiCore
|
||||
Pages --> errorReporter
|
||||
Pages --> settingsSvc
|
||||
Pages --> pageElementsSvc
|
||||
Pages --> articlesSvc
|
||||
Pages --> playersSvc
|
||||
Pages --> sponsorsSvc
|
||||
Pages --> bannersSvc
|
||||
Pages --> compAliasesSvc
|
||||
Pages --> eventsSvc
|
||||
ClubThemeContext --> facrApi
|
||||
ClubThemeContext --> colorsUtil
|
||||
ClubThemeContext --> logosUtil
|
||||
Pages --> urlUtil
|
||||
Pages --> nationalityUtil
|
||||
Hooks --> settingsSvc
|
||||
|
||||
%% Backends
|
||||
subgraph Backends
|
||||
Backend[(fotbal-club backend API)]:::infra
|
||||
ErrorIngest[(errors.tdvorak.dev)]:::infra
|
||||
FACR[(FACR APIs)]:::infra
|
||||
end
|
||||
|
||||
apiCore --> Backend
|
||||
errorReporter -. sends .- ErrorIngest
|
||||
facrApi --> FACR
|
||||
@@ -0,0 +1,69 @@
|
||||
flowchart LR
|
||||
%% Homepage composition (components and data)
|
||||
classDef page fill:#1c243a,stroke:#4b5b8a,color:#e8eaf0;
|
||||
classDef comp fill:#1d2a2a,stroke:#3d7a6a,color:#e8eaf0;
|
||||
classDef svc fill:#0b273f,stroke:#3a72a0,color:#e8eaf0;
|
||||
classDef ctx fill:#2b233f,stroke:#7a63a0,color:#e8eaf0;
|
||||
classDef hook fill:#2a2a1f,stroke:#9a8a3d,color:#e8eaf0;
|
||||
|
||||
HomePage:::page
|
||||
HomePage --> MainLayout[components/layout/MainLayout]:::comp
|
||||
HomePage --> ClubHeroTopbar[components/home/ClubHeroTopbar]:::comp
|
||||
HomePage --> BannerDisplay[components/banners/BannerDisplay]:::comp
|
||||
HomePage --> BlogCardsScroller[components/home/BlogCardsScroller]:::comp
|
||||
HomePage --> BlogSwiper[components/home/BlogSwiper]:::comp
|
||||
HomePage --> VideosSection[components/home/VideosSection]:::comp
|
||||
HomePage --> MerchSection[components/home/MerchSection]:::comp
|
||||
HomePage --> PollsWidget[components/home/PollsWidget]:::comp
|
||||
HomePage --> GallerySection[components/home/GallerySection]:::comp
|
||||
HomePage --> NewsletterSubscribe[components/newsletter/NewsletterSubscribe]:::comp
|
||||
HomePage --> NewsList[components/pack/NewsList]:::comp
|
||||
HomePage --> StandingsCard[components/pack/StandingsCard]:::comp
|
||||
HomePage --> NextMatch[components/pack/NextMatch]:::comp
|
||||
HomePage --> MatchesSlider[components/pack/MatchesSlider]:::comp
|
||||
HomePage --> ActivitiesList[components/pack/ActivitiesList]:::comp
|
||||
HomePage --> SweepstakeWidget[components/sweepstakes/SweepstakeWidget]:::comp
|
||||
HomePage --> ClubModal[components/home/ClubModal]:::comp
|
||||
HomePage --> MatchModal[components/home/MatchModal]:::comp
|
||||
HomePage -. uses .- TeamLogo[components/common/TeamLogo]:::comp
|
||||
|
||||
%% Data sources
|
||||
subgraph Services
|
||||
settingsSvc[services/settings.getPublicSettings]:::svc
|
||||
pageElementsSvc[services/pageElements.getPageElementConfigs]:::svc
|
||||
articlesSvc[services/articles]:::svc
|
||||
playersSvc[services/players]:::svc
|
||||
sponsorsSvc[services/sponsors]:::svc
|
||||
bannersSvc[services/banners]:::svc
|
||||
compAliasesSvc[services/competitionAliases]:::svc
|
||||
eventsSvc[services/eventService.getUpcomingEvents]:::svc
|
||||
facrApi[services/facr/facrApi]:::svc
|
||||
apiCore[services/api - API_URL]:::svc
|
||||
end
|
||||
|
||||
HomePage --> settingsSvc
|
||||
HomePage --> pageElementsSvc
|
||||
HomePage --> articlesSvc
|
||||
HomePage --> playersSvc
|
||||
HomePage --> sponsorsSvc
|
||||
HomePage --> bannersSvc
|
||||
HomePage --> compAliasesSvc
|
||||
HomePage --> eventsSvc
|
||||
HomePage --> facrApi
|
||||
HomePage --> apiCore
|
||||
|
||||
%% Contexts & Hooks
|
||||
subgraph Contexts
|
||||
AuthContext[contexts/AuthContext]:::ctx
|
||||
ClubThemeContext[contexts/ClubThemeContext]:::ctx
|
||||
end
|
||||
|
||||
subgraph Hooks
|
||||
useAllPageElementConfigs[hooks/usePageElementConfig.useAllPageElementConfigs]:::hook
|
||||
usePublicSettings[hooks/usePublicSettings]:::hook
|
||||
end
|
||||
|
||||
HomePage --> AuthContext
|
||||
HomePage --> ClubThemeContext
|
||||
HomePage --> useAllPageElementConfigs
|
||||
HomePage --> usePublicSettings
|
||||
@@ -0,0 +1,81 @@
|
||||
flowchart LR
|
||||
%% Modules and dependencies (key subset)
|
||||
classDef svc fill:#0b273f,stroke:#3a72a0,color:#e8eaf0;
|
||||
classDef util fill:#2b2f3f,stroke:#6a7aa0,color:#e8eaf0;
|
||||
classDef ctx fill:#2b233f,stroke:#7a63a0,color:#e8eaf0;
|
||||
classDef hook fill:#2a2a1f,stroke:#9a8a3d,color:#e8eaf0;
|
||||
classDef page fill:#1c243a,stroke:#4b5b8a,color:#e8eaf0;
|
||||
|
||||
subgraph Contexts
|
||||
AuthContext[contexts/AuthContext]:::ctx
|
||||
ClubThemeContext[contexts/ClubThemeContext]:::ctx
|
||||
end
|
||||
|
||||
subgraph Hooks
|
||||
usePublicSettings[hooks/usePublicSettings]:::hook
|
||||
usePageElementConfig[hooks/usePageElementConfig]:::hook
|
||||
useAllPageElementConfigs[hooks/usePageElementConfig.useAll]:::hook
|
||||
useUmami[hooks/useUmami]:::hook
|
||||
useFontLoader[hooks/useFontLoader]:::hook
|
||||
end
|
||||
|
||||
subgraph Services
|
||||
apiCore[services/api]:::svc
|
||||
errorReporter[services/errorReporter]:::svc
|
||||
settingsSvc[services/settings]:::svc
|
||||
pageElementsSvc[services/pageElements]:::svc
|
||||
articlesSvc[services/articles]:::svc
|
||||
playersSvc[services/players]:::svc
|
||||
sponsorsSvc[services/sponsors]:::svc
|
||||
bannersSvc[services/banners]:::svc
|
||||
compAliasesSvc[services/competitionAliases]:::svc
|
||||
eventsSvc[services/eventService]:::svc
|
||||
setupSvc[services/setup]:::svc
|
||||
engagementSvc[services/engagement]:::svc
|
||||
actionLogSvc[services/actionLog]:::svc
|
||||
facrApi[services/facr/facrApi]:::svc
|
||||
end
|
||||
|
||||
subgraph Utils
|
||||
urlUtil[utils/url]:::util
|
||||
nationalityUtil[utils/nationality]:::util
|
||||
colorsUtil[utils/colors]:::util
|
||||
logosUtil[utils/sportLogosAPI]:::util
|
||||
end
|
||||
|
||||
subgraph Pages
|
||||
HomePage:::page
|
||||
BlogPage:::page
|
||||
ArticleDetailPage:::page
|
||||
MatchDetailPage:::page
|
||||
ActivityDetailPage:::page
|
||||
AdminPages[(Admin Pages...)]:::page
|
||||
end
|
||||
|
||||
HomePage --> settingsSvc
|
||||
HomePage --> pageElementsSvc
|
||||
HomePage --> articlesSvc
|
||||
HomePage --> playersSvc
|
||||
HomePage --> sponsorsSvc
|
||||
HomePage --> bannersSvc
|
||||
HomePage --> compAliasesSvc
|
||||
HomePage --> eventsSvc
|
||||
HomePage --> facrApi
|
||||
|
||||
Pages --> apiCore
|
||||
Pages --> errorReporter
|
||||
Pages --> usePublicSettings
|
||||
Pages --> usePageElementConfig
|
||||
Pages --> useUmami
|
||||
Pages --> useFontLoader
|
||||
Pages --> urlUtil
|
||||
Pages --> nationalityUtil
|
||||
|
||||
ClubThemeContext --> usePublicSettings
|
||||
ClubThemeContext --> facrApi
|
||||
ClubThemeContext --> colorsUtil
|
||||
ClubThemeContext --> logosUtil
|
||||
|
||||
errorReporter -. sends .- ErrorIngest[(errors.tdvorak.dev)]
|
||||
apiCore -. REST .- Backend[(fotbal-club backend)]
|
||||
facrApi -. data .- FACR[(FACR APIs)]
|
||||
@@ -0,0 +1,236 @@
|
||||
%%{init: {"theme":"forest","flowchart":{"curve":"linear"},"themeCSS":".edgePath path { stroke-dasharray: 6 4; animation: dash 16s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } }" }}%%
|
||||
flowchart LR
|
||||
%% Overall Frontend Architecture
|
||||
classDef cluster fill:#eef7ff,stroke:#2b6cb0,color:#0b3a60;
|
||||
classDef svc fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e;
|
||||
classDef page fill:#fff7ed,stroke:#f59e0b,color:#7c2d12;
|
||||
classDef comp fill:#ecfdf5,stroke:#16a34a,color:#064e3b;
|
||||
classDef ctx fill:#f3e8ff,stroke:#6d28d9,color:#3b0764;
|
||||
classDef hook fill:#fef9c3,stroke:#ca8a04,color:#7c2d12;
|
||||
classDef infra fill:#e3f2fd,stroke:#1e88e5,color:#0c4a6e;
|
||||
|
||||
subgraph Client
|
||||
Browser((Browser)):::infra
|
||||
end
|
||||
|
||||
Browser --> index_tsx[index.tsx]:::infra
|
||||
index_tsx --> ErrorBoundary
|
||||
index_tsx --> AppLazy[App.lazy.tsx]
|
||||
index_tsx --> ServiceWorker[serviceWorkerRegistration.ts]
|
||||
index_tsx --> ErrorReporterSvc[services/errorReporter.ts]
|
||||
|
||||
subgraph Providers
|
||||
Chakra[ChakraProvider]:::infra --> RQ[QueryClientProvider]:::infra --> Router[BrowserRouter]:::infra --> AuthProv[AuthProvider]:::ctx --> ClubThemeProv[ClubThemeProvider]:::ctx --> Helmet[HelmetProvider]:::infra --> Suspense:::infra
|
||||
SEO[DefaultSEO]:::comp
|
||||
CookieBanner:::comp
|
||||
end
|
||||
AppLazy --> Chakra
|
||||
AppLazy --> SEO
|
||||
AppLazy --> CookieBanner
|
||||
Suspense --> Routes
|
||||
|
||||
subgraph Routing
|
||||
Routes:::infra
|
||||
ProtectedRoute:::comp
|
||||
end
|
||||
|
||||
Routes --> PublicRoutes
|
||||
Routes --> AdminRoutes
|
||||
|
||||
subgraph PublicRoutes[Public Pages]
|
||||
HomeRoute --> HomePage
|
||||
HomeRoute --> PremiumHomePage
|
||||
BlogRoute --> BlogPage
|
||||
BlogRoute --> PremiumBlogPage
|
||||
PublicOther[Other Public Routes]
|
||||
end
|
||||
|
||||
subgraph AdminRoutes[Admin Pages]
|
||||
AdminDashboardPage
|
||||
AdminContent[Other Admin Pages]
|
||||
end
|
||||
|
||||
Router --> Routes
|
||||
AuthProv --> ProtectedRoute
|
||||
|
||||
%% Pages and components
|
||||
subgraph Pages[Key Pages]
|
||||
HomePage:::page
|
||||
BlogPage:::page
|
||||
ArticleDetailPage:::page
|
||||
ActivityDetailPage:::page
|
||||
MatchDetailPage:::page
|
||||
ClubPage:::page
|
||||
AboutPage:::page
|
||||
CalendarPage:::page
|
||||
ActivitiesCalendarPage:::page
|
||||
TablesPage:::page
|
||||
MatchesPage:::page
|
||||
PlayersPage:::page
|
||||
PlayerDetailPage:::page
|
||||
SponsorsPage:::page
|
||||
ContactPage:::page
|
||||
GalleryPage:::page
|
||||
AlbumDetailPage:::page
|
||||
VideosPage:::page
|
||||
SearchPage:::page
|
||||
ClothingPage:::page
|
||||
PollsPage:::page
|
||||
OverlayScoreboardPage:::page
|
||||
OverlaySponsorsPage:::page
|
||||
NotFoundPage:::page
|
||||
ForbiddenPage:::page
|
||||
SetupPage:::page
|
||||
StylePreviewPage:::page
|
||||
AuthPage:::page
|
||||
RegisterPage:::page
|
||||
ForgotPasswordPage:::page
|
||||
ResetPasswordPage:::page
|
||||
NewsletterUnsubscribePage:::page
|
||||
NewsletterPreferencesPage:::page
|
||||
SemiAdminPage:::page
|
||||
ShortRedirectPage:::page
|
||||
PremiumHomePage:::page
|
||||
PremiumBlogPage:::page
|
||||
end
|
||||
|
||||
PublicRoutes --> Pages
|
||||
AdminRoutes --> AdminContent
|
||||
|
||||
subgraph AdminPages[Admin Management]
|
||||
AdminDashboardPage:::page
|
||||
AdminDocsPage:::page
|
||||
AboutAdminPage:::page
|
||||
AdminVideosPage:::page
|
||||
GalleryAdminPage:::page
|
||||
AdminMerchPage:::page
|
||||
SponsorsAdminPage:::page
|
||||
MatchesAdminPage:::page
|
||||
PlayersAdminPage:::page
|
||||
TeamsAdminPage:::page
|
||||
UsersAdminPage:::page
|
||||
BannersAdminPage:::page
|
||||
MessagesAdminPage:::page
|
||||
SettingsAdminPage:::page
|
||||
NewsletterAdminPage:::page
|
||||
PollsAdminPage:::page
|
||||
CompetitionAliasesAdminPage:::page
|
||||
PrefetchAdminPage:::page
|
||||
AdminResetPasswordPage:::page
|
||||
ScoreboardAdminPage:::page
|
||||
MobileScoreboardControlPage:::page
|
||||
AnalyticsAdminPage:::page
|
||||
ErrorsAdminPage:::page
|
||||
FilesAdminPage:::page
|
||||
ContactsAdminPage:::page
|
||||
NavigationAdminPage:::page
|
||||
CommentsAdminPage:::page
|
||||
ShortlinksAdminPage:::page
|
||||
EngagementAdminPage:::page
|
||||
SweepstakesAdminPage:::page
|
||||
SweepstakeVisualPage:::page
|
||||
end
|
||||
|
||||
AdminContent --> AdminPages
|
||||
|
||||
%% Components (subset)
|
||||
subgraph Components
|
||||
MainLayout[components/layout/MainLayout]:::comp
|
||||
ProtectedRoute:::comp
|
||||
CookieBanner:::comp
|
||||
DefaultSEO[components/seo/DefaultSEO]:::comp
|
||||
TeamLogo[components/common/TeamLogo]:::comp
|
||||
ClubHeroTopbar[components/home/ClubHeroTopbar]:::comp
|
||||
BannerDisplay[components/banners/BannerDisplay]:::comp
|
||||
BlogCardsScroller[components/home/BlogCardsScroller]:::comp
|
||||
BlogSwiper[components/home/BlogSwiper]:::comp
|
||||
VideosSection[components/home/VideosSection]:::comp
|
||||
MerchSection[components/home/MerchSection]:::comp
|
||||
PollsWidget[components/home/PollsWidget]:::comp
|
||||
GallerySection[components/home/GallerySection]:::comp
|
||||
NewsletterSubscribe[components/newsletter/NewsletterSubscribe]:::comp
|
||||
MyUIbrixEditor[components/editor/MyUIbrixEditor]:::comp
|
||||
MyUIbrixErrorBoundary[components/editor/MyUIbrixErrorBoundary]:::comp
|
||||
ClubModal[components/home/ClubModal]:::comp
|
||||
MatchModal[components/home/MatchModal]:::comp
|
||||
NewsList[components/pack/NewsList]:::comp
|
||||
StandingsCard[components/pack/StandingsCard]:::comp
|
||||
NextMatch[components/pack/NextMatch]:::comp
|
||||
MatchesSlider[components/pack/MatchesSlider]:::comp
|
||||
ActivitiesList[components/pack/ActivitiesList]:::comp
|
||||
SweepstakeWidget[components/sweepstakes/SweepstakeWidget]:::comp
|
||||
end
|
||||
|
||||
HomePage --> MainLayout
|
||||
HomePage --> ClubHeroTopbar
|
||||
HomePage --> BannerDisplay
|
||||
HomePage --> BlogCardsScroller
|
||||
HomePage --> BlogSwiper
|
||||
HomePage --> VideosSection
|
||||
HomePage --> MerchSection
|
||||
HomePage --> PollsWidget
|
||||
HomePage --> GallerySection
|
||||
HomePage --> NewsletterSubscribe
|
||||
HomePage --> NewsList
|
||||
HomePage --> StandingsCard
|
||||
HomePage --> NextMatch
|
||||
HomePage --> MatchesSlider
|
||||
HomePage --> ActivitiesList
|
||||
HomePage --> SweepstakeWidget
|
||||
HomePage --> ClubModal
|
||||
HomePage --> MatchModal
|
||||
TeamLogo -. logos .- HomePage
|
||||
|
||||
%% Contexts & Hooks
|
||||
subgraph Contexts
|
||||
AuthContext[contexts/AuthContext]:::ctx
|
||||
ClubThemeContext[contexts/ClubThemeContext]:::ctx
|
||||
end
|
||||
|
||||
subgraph Hooks
|
||||
usePublicSettings[hooks/usePublicSettings]:::hook
|
||||
usePageElementConfig[hooks/usePageElementConfig]:::hook
|
||||
useUmami[hooks/useUmami]:::hook
|
||||
useFontLoader[hooks/useFontLoader]:::hook
|
||||
end
|
||||
|
||||
AuthProv --> AuthContext
|
||||
ClubThemeProv --> ClubThemeContext
|
||||
Pages --> AuthContext
|
||||
Pages --> ClubThemeContext
|
||||
Pages --> Hooks
|
||||
|
||||
%% Services & Data
|
||||
subgraph Services
|
||||
settingsSvc[services/settings.ts]:::svc
|
||||
pageElementsSvc[services/pageElements.ts]:::svc
|
||||
articlesSvc[services/articles.ts]:::svc
|
||||
playersSvc[services/players.ts]:::svc
|
||||
sponsorsSvc[services/sponsors.ts]:::svc
|
||||
bannersSvc[services/banners.ts]:::svc
|
||||
compAliasesSvc[services/competitionAliases.ts]:::svc
|
||||
eventsSvc[services/eventService.ts]:::svc
|
||||
facrApi[services/facr/facrApi.ts]:::svc
|
||||
setupSvc[services/setup.ts]:::svc
|
||||
engagementSvc[services/engagement.ts]:::svc
|
||||
actionLogSvc[services/actionLog.ts]:::svc
|
||||
errorReporter[services/errorReporter.ts]:::svc
|
||||
apiCore[services/api.ts - API_URL]:::svc
|
||||
end
|
||||
|
||||
Components --> Services
|
||||
Pages --> Services
|
||||
Hooks --> Services
|
||||
|
||||
subgraph Backends
|
||||
Backend[(fotbal-club backend API)]:::infra
|
||||
ErrorIngest[(errors.tdvorak.dev)]:::infra
|
||||
FACR[(FACR APIs)]:::infra
|
||||
end
|
||||
|
||||
Services --> Backend
|
||||
errorReporter --> ErrorIngest
|
||||
facrApi --> FACR
|
||||
apiCore --> Backend
|
||||
|
||||
ServiceWorker -.->|PWA| Browser
|
||||
@@ -0,0 +1,94 @@
|
||||
%%{init: {"theme":"forest","flowchart":{"curve":"linear"},"themeCSS":".edgePath path { stroke-dasharray: 6 4; animation: dash 16s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } }" }}%%
|
||||
flowchart TD
|
||||
%% Routes to Pages Mapping (from App.lazy.tsx)
|
||||
classDef page fill:#fff7ed,stroke:#f59e0b,color:#7c2d12;
|
||||
classDef route fill:#e2e8f0,stroke:#475569,color:#111827;
|
||||
|
||||
Router[BrowserRouter]:::route --> Routes:::route
|
||||
|
||||
subgraph PublicRoutes[Public Routes]
|
||||
R0["/"]:::route --> HomeRoute:::route --> HomePage:::page
|
||||
R1["/blog"]:::route --> BlogRoute:::route --> BlogPage:::page
|
||||
R2["/hledat"]:::route --> SearchPage:::page
|
||||
R3["/search"]:::route --> SearchPage:::page
|
||||
R4["/overlay/scoreboard"]:::route --> OverlayScoreboardPage:::page
|
||||
R5["/overlay/sponsors"]:::route --> OverlaySponsorsPage:::page
|
||||
R6["/klub"]:::route --> ClubPage:::page
|
||||
R7["/o-klubu"]:::route --> AboutPage:::page
|
||||
R8["/kalendar"]:::route --> CalendarPage:::page
|
||||
R9["/aktivity"]:::route --> ActivitiesCalendarPage:::page
|
||||
R10["/tabulky"]:::route --> TablesPage:::page
|
||||
R11["/zapasy"]:::route --> MatchesPage:::page
|
||||
R12["/players"]:::route --> PlayersPage:::page
|
||||
R13["/hraci"]:::route --> PlayersPage:::page
|
||||
R14["/players/:id"]:::route --> PlayerDetailPage:::page
|
||||
R15["/hraci/:id"]:::route --> PlayerDetailPage:::page
|
||||
R16["/sponzori"]:::route --> SponsorsPage:::page
|
||||
R17["/kontakt"]:::route --> ContactPage:::page
|
||||
R18["/ankety"]:::route --> PollsPage:::page
|
||||
R19["/galerie"]:::route --> GalleryPage:::page
|
||||
R20["/galerie/album/:id"]:::route --> AlbumDetailPage:::page
|
||||
R21["/videa"]:::route --> VideosPage:::page
|
||||
R22["/obleceni"]:::route --> ClothingPage:::page
|
||||
|
||||
%% Legal
|
||||
R23["/pravidla-cookies"]:::route --> CookiePolicyPage:::page
|
||||
R24["/obchodni-podminky"]:::route --> TermsPage:::page
|
||||
R25["/zasady-ochrany-osobnich-udaju"]:::route --> PrivacyPolicyPage:::page
|
||||
|
||||
%% Articles and matches
|
||||
R26["/news"]:::route --> RedirectToBlog((Redirect -> /blog))
|
||||
R27["/news/:slug"]:::route --> ArticleDetailPage:::page
|
||||
R28["/articles/slug/:slug"]:::route --> ArticleDetailPage:::page
|
||||
R29["/articles/:id"]:::route --> ArticleDetailPage:::page
|
||||
R30["/zapas/:id"]:::route --> MatchDetailPage:::page
|
||||
R31["/aktivita/:id"]:::route --> ActivityDetailPage:::page
|
||||
|
||||
%% Setup & Auth
|
||||
R32["/setup"]:::route --> SetupPage:::page
|
||||
R33["/setup/styl"]:::route --> StylePreviewPage:::page
|
||||
R34["/login"]:::route --> AuthPage:::page
|
||||
R35["/register"]:::route --> RegisterPage:::page
|
||||
R36["/forgot-password"]:::route --> ForgotPasswordPage:::page
|
||||
R37["/reset-password"]:::route --> ResetPasswordPage:::page
|
||||
R38["/newsletter/unsubscribe/:email"]:::route --> NewsletterUnsubscribePage:::page
|
||||
R39["/newsletter/preferences"]:::route --> NewsletterPreferencesPage:::page
|
||||
R40["/403"]:::route --> ForbiddenPage:::page
|
||||
|
||||
%% Not found
|
||||
R99["*"]:::route --> NotFoundRoute:::route --> NotFoundPage:::page
|
||||
end
|
||||
|
||||
subgraph AdminRoutes[Admin Routes - guarded by ProtectedRoute]
|
||||
A0["/admin"]:::route --> AdminDashboardPage:::page
|
||||
A1["/admin/docs"]:::route --> AdminDocsPage:::page
|
||||
A2["/admin/o-klubu"]:::route --> AboutAdminPage:::page
|
||||
A3["/admin/videa"]:::route --> AdminVideosPage:::page
|
||||
A4["/admin/galerie"]:::route --> GalleryAdminPage:::page
|
||||
A5["/admin/obleceni"]:::route --> AdminMerchPage:::page
|
||||
A6["/admin/sponzori"]:::route --> SponsorsAdminPage:::page
|
||||
A7["/admin/zapasy"]:::route --> MatchesAdminPage:::page
|
||||
A8["/admin/hraci"]:::route --> PlayersAdminPage:::page
|
||||
A9["/admin/tymy"]:::route --> TeamsAdminPage:::page
|
||||
A10["/admin/uzivatele"]:::route --> UsersAdminPage:::page
|
||||
A11["/admin/bannery"]:::route --> BannersAdminPage:::page
|
||||
A12["/admin/zpravy"]:::route --> MessagesAdminPage:::page
|
||||
A13["/admin/nastaveni"]:::route --> SettingsAdminPage:::page
|
||||
A14["/admin/newsletter"]:::route --> NewsletterAdminPage:::page
|
||||
A15["/admin/ankety"]:::route --> PollsAdminPage:::page
|
||||
A16["/admin/aliasy-soutezi"]:::route --> CompetitionAliasesAdminPage:::page
|
||||
A17["/admin/prefetch"]:::route --> PrefetchAdminPage:::page
|
||||
A18["/admin/users/send-reset"]:::route --> AdminResetPasswordPage:::page
|
||||
A19["/admin/scoreboard"]:::route --> ScoreboardAdminPage:::page
|
||||
A20["/admin/scoreboard/remote"]:::route --> MobileScoreboardControlPage:::page
|
||||
A21["/admin/analytika"]:::route --> AnalyticsAdminPage:::page
|
||||
A22["/admin/errors"]:::route --> ErrorsAdminPage:::page
|
||||
A23["/admin/soubory"]:::route --> FilesAdminPage:::page
|
||||
A24["/admin/kontakty"]:::route --> ContactsAdminPage:::page
|
||||
A25["/admin/navigace"]:::route --> NavigationAdminPage:::page
|
||||
A26["/admin/komentare"]:::route --> CommentsAdminPage:::page
|
||||
A27["/admin/shortlinks"]:::route --> ShortlinksAdminPage:::page
|
||||
A28["/admin/engagement"]:::route --> EngagementAdminPage:::page
|
||||
A29["/admin/sweepstakes"]:::route --> SweepstakesAdminPage:::page
|
||||
A30["/admin/sweepstakes/:id/visual"]:::route --> SweepstakeVisualPage:::page
|
||||
end
|
||||
@@ -1,72 +0,0 @@
|
||||
# Frontpage Data Map
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Home[Homepage sections]
|
||||
News[News (Articles)]
|
||||
Matches[Upcoming & recent matches]
|
||||
TableSec[Standings / Tables]
|
||||
Activities[Activities (Events)]
|
||||
GallerySec[Gallery]
|
||||
VideosSec[Videos]
|
||||
PlayersSec[Players]
|
||||
SponsorsSec[Sponsors]
|
||||
MerchSec[Merch]
|
||||
PollsSec[Polls]
|
||||
MapSec[Club Map]
|
||||
NewsletterSec[Newsletter box]
|
||||
BannersSec[Banners]
|
||||
|
||||
Home --> News
|
||||
Home --> Matches
|
||||
Home --> TableSec
|
||||
Home --> Activities
|
||||
Home --> GallerySec
|
||||
Home --> VideosSec
|
||||
Home --> PlayersSec
|
||||
Home --> SponsorsSec
|
||||
Home --> MerchSec
|
||||
Home --> PollsSec
|
||||
Home --> MapSec
|
||||
Home --> NewsletterSec
|
||||
Home --> BannersSec
|
||||
|
||||
ARTICLES["Articles"]
|
||||
FACR_API["FACR API"]
|
||||
MATCH_OVERRIDES["Match overrides"]
|
||||
COMPETITION_ALIASES["Competition aliases"]
|
||||
EVENTS["Events"]
|
||||
SETTINGS["Settings"]
|
||||
ZONERAMA["Zonerama"]
|
||||
YOUTUBE["YouTube"]
|
||||
TEAMS["Teams"]
|
||||
DB_PLAYERS["Players"]
|
||||
SPONSORS["Sponsors"]
|
||||
CLOTHING["Clothing"]
|
||||
POLLS["Polls"]
|
||||
POLL_OPTIONS["Poll options"]
|
||||
GOOGLE_MAPS["Google Maps"]
|
||||
NEWSLETTER_SUBSCRIPTIONS["Newsletter subscriptions"]
|
||||
BANNERS["Banners"]
|
||||
|
||||
News -->|DB| ARTICLES
|
||||
Matches -->|Source| FACR_API
|
||||
Matches -->|Overrides| MATCH_OVERRIDES
|
||||
TableSec -->|Source| FACR_API
|
||||
TableSec -->|Aliases| COMPETITION_ALIASES
|
||||
Activities -->|DB| EVENTS
|
||||
GallerySec -->|Profile URL| SETTINGS
|
||||
GallerySec --> ZONERAMA
|
||||
VideosSec -->|Config| SETTINGS
|
||||
VideosSec --> YOUTUBE
|
||||
PlayersSec --> TEAMS
|
||||
PlayersSec --> DB_PLAYERS
|
||||
SponsorsSec -->|DB| SPONSORS
|
||||
MerchSec -->|DB| CLOTHING
|
||||
PollsSec -->|DB| POLLS
|
||||
PollsSec --> POLL_OPTIONS
|
||||
MapSec -->|lat/lng + style| SETTINGS
|
||||
MapSec --> GOOGLE_MAPS
|
||||
NewsletterSec -->|subscribe| NEWSLETTER_SUBSCRIPTIONS
|
||||
BannersSec -->|DB| BANNERS
|
||||
```
|
||||
@@ -1,68 +0,0 @@
|
||||
graph TB
|
||||
Home[Homepage sections]
|
||||
News[News Articles]
|
||||
Matches[Upcoming and recent matches]
|
||||
TableSec[Standings Tables]
|
||||
Activities[Activities Events]
|
||||
GallerySec[Gallery]
|
||||
VideosSec[Videos]
|
||||
PlayersSec[Players]
|
||||
SponsorsSec[Sponsors]
|
||||
MerchSec[Merch]
|
||||
PollsSec[Polls]
|
||||
MapSec[Club Map]
|
||||
NewsletterSec[Newsletter]
|
||||
BannersSec[Banners]
|
||||
|
||||
Home --> News
|
||||
Home --> Matches
|
||||
Home --> TableSec
|
||||
Home --> Activities
|
||||
Home --> GallerySec
|
||||
Home --> VideosSec
|
||||
Home --> PlayersSec
|
||||
Home --> SponsorsSec
|
||||
Home --> MerchSec
|
||||
Home --> PollsSec
|
||||
Home --> MapSec
|
||||
Home --> NewsletterSec
|
||||
Home --> BannersSec
|
||||
|
||||
ARTICLES[Articles]
|
||||
FACR_API[FACR API]
|
||||
MATCH_OVERRIDES[Match overrides]
|
||||
COMPETITION_ALIASES[Competition aliases]
|
||||
EVENTS[Events]
|
||||
SETTINGS[Settings]
|
||||
ZONERAMA[Zonerama]
|
||||
YOUTUBE[YouTube]
|
||||
TEAMS[Teams]
|
||||
DB_PLAYERS[Players]
|
||||
SPONSORS[Sponsors]
|
||||
CLOTHING[Clothing]
|
||||
POLLS[Polls]
|
||||
POLL_OPTIONS[Poll options]
|
||||
GOOGLE_MAPS[Google Maps]
|
||||
NEWSLETTER_SUBSCRIPTIONS[Newsletter subscriptions]
|
||||
BANNERS[Banners]
|
||||
|
||||
News -->|DB| ARTICLES
|
||||
Matches -->|Source| FACR_API
|
||||
Matches -->|Overrides| MATCH_OVERRIDES
|
||||
TableSec -->|Source| FACR_API
|
||||
TableSec -->|Aliases| COMPETITION_ALIASES
|
||||
Activities -->|DB| EVENTS
|
||||
GallerySec -->|Profile URL| SETTINGS
|
||||
GallerySec --> ZONERAMA
|
||||
VideosSec -->|Config| SETTINGS
|
||||
VideosSec --> YOUTUBE
|
||||
PlayersSec --> TEAMS
|
||||
PlayersSec --> DB_PLAYERS
|
||||
SponsorsSec -->|DB| SPONSORS
|
||||
MerchSec -->|DB| CLOTHING
|
||||
PollsSec -->|DB| POLLS
|
||||
PollsSec --> POLL_OPTIONS
|
||||
MapSec -->|lat lng and style| SETTINGS
|
||||
MapSec --> GOOGLE_MAPS
|
||||
NewsletterSec -->|subscribe| NEWSLETTER_SUBSCRIPTIONS
|
||||
BannersSec -->|DB| BANNERS
|
||||
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,43 @@
|
||||
%%{init: {'theme': 'neutral'}}%%
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Admin as Admin (GalleryAdminPage)
|
||||
participant FE as Frontend Services (gallery)
|
||||
participant BE as Backend API (GalleryController)
|
||||
participant Z as Zonerama API
|
||||
participant DB as Postgres
|
||||
|
||||
Note over Admin,BE: Profile & refresh
|
||||
FE->>BE: GET /api/v1/admin/gallery/profile
|
||||
BE-->>FE: Zonerama profile (cached)
|
||||
|
||||
FE->>BE: POST /api/v1/admin/gallery/refresh
|
||||
BE->>Z: Fetch albums/photos
|
||||
Z-->>BE: Payload
|
||||
BE->>DB: Upsert albums/photos
|
||||
BE-->>FE: 200 OK
|
||||
|
||||
rect rgba(230,255,230,0.2)
|
||||
Note over Admin,BE: Fetch single album
|
||||
FE->>BE: POST /api/v1/admin/gallery/albums/fetch {id}
|
||||
BE->>Z: GET album by id
|
||||
Z-->>BE: Album data
|
||||
BE->>DB: Upsert album
|
||||
BE-->>FE: 200 OK
|
||||
end
|
||||
|
||||
rect rgba(230,230,255,0.2)
|
||||
Note over FE,BE: Public endpoints
|
||||
FE->>BE: GET /api/v1/gallery/albums
|
||||
BE->>DB: List albums
|
||||
BE-->>FE: Albums
|
||||
|
||||
FE->>BE: GET /api/v1/gallery/albums/:id
|
||||
BE->>DB: Album+photos
|
||||
BE-->>FE: Album detail
|
||||
|
||||
FE->>BE: GET /api/v1/gallery/proxy-image?url=...
|
||||
BE->>Z: Download image (proxy)
|
||||
Z-->>BE: Binary
|
||||
BE-->>FE: Image
|
||||
end
|
||||
@@ -0,0 +1,260 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Fotbal Club — All Diagrams</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0f1115;--panel:#0f131f;--text:#e8eaf0;--muted:#9aa3b2;--primary:#0b5cff;--border:#212736;--badge:#1b2440;
|
||||
}
|
||||
html,body{margin:0;height:100%;background:var(--bg);color:var(--text);font-family:Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial}
|
||||
header{position:sticky;top:0;z-index:10;background:#0d1017;border-bottom:1px solid var(--border);padding:12px 16px}
|
||||
header h1{margin:0;font-size:16px;font-weight:700}
|
||||
.filters{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:8px}
|
||||
.filters input[type="search"], .filters select{background:#0f1420;color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 10px}
|
||||
.filters .chip{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);border-radius:16px;padding:6px 10px;background:#0f1420;cursor:pointer}
|
||||
.filters .chip input{margin:0}
|
||||
.filters .sp{flex:1}
|
||||
.btn{appearance:none;border:1px solid var(--border);background:#11182a;color:var(--text);padding:8px 12px;border-radius:8px;cursor:pointer;font-weight:600;font-size:12px}
|
||||
.btn.primary{background:var(--primary);border-color:var(--primary);color:#fff}
|
||||
.btn.ghost{background:transparent}
|
||||
main{padding:16px}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(460px,1fr));gap:16px}
|
||||
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden;display:flex;flex-direction:column}
|
||||
.card header{display:flex;align-items:center;gap:8px;justify-content:space-between;background:#0f131f;border-bottom:1px solid var(--border);padding:10px 12px;position:static}
|
||||
.title{display:flex;flex-direction:column;gap:4px}
|
||||
.title h2{margin:0;font-size:14px}
|
||||
.meta{color:var(--muted);font-size:11px}
|
||||
.badge{background:var(--badge);border:1px solid var(--border);border-radius:999px;padding:2px 8px;font-size:10px;color:#bcd}
|
||||
.diagram-wrap{position:relative;overflow:auto;min-height:320px;background:#fff}
|
||||
.diagram{padding:16px;min-height:320px}
|
||||
.toolbar{display:flex;gap:8px;align-items:center;padding:8px 12px;border-top:1px solid var(--border);background:#0f131f;flex-wrap:wrap}
|
||||
.toolbar .sp{flex:1}
|
||||
.diagram svg{max-width:100%;height:auto;background:#fff}
|
||||
.diagram svg text{fill:#111827 !important}
|
||||
.diagram svg .edgePath path, .diagram svg .flowchart-link{stroke:#334155 !important}
|
||||
.diagram svg .node > * { transition: filter .2s ease }
|
||||
.diagram svg .node:hover { filter: drop-shadow(0 0 6px var(--primary)) }
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.9.1/dist/mermaid.min.js"></script>
|
||||
<script>
|
||||
mermaid.initialize({ startOnLoad:false, securityLevel:'loose', theme:'dark', flowchart:{ curve:'basis', useMaxWidth:true } });
|
||||
|
||||
async function renderMermaidFile(mmdPath, container){
|
||||
try{
|
||||
container.innerHTML = '<div style="padding:16px;color:#9aa3b2">Loading '+mmdPath+'…</div>';
|
||||
const res = await fetch(mmdPath, { cache: 'no-store' });
|
||||
if(!res.ok) throw new Error('Failed to load '+mmdPath+': '+res.status);
|
||||
const code = await res.text();
|
||||
const id = 'm-'+Math.random().toString(36).slice(2);
|
||||
const { svg } = await mermaid.render(id, code);
|
||||
container.innerHTML = svg;
|
||||
const svgEl = container.querySelector('svg');
|
||||
if(svgEl){
|
||||
svgEl.style.maxWidth = '100%';
|
||||
svgEl.style.width = '100%';
|
||||
svgEl.style.height = 'auto';
|
||||
svgEl.removeAttribute('width');
|
||||
svgEl.removeAttribute('height');
|
||||
}
|
||||
}catch(e){ container.innerHTML = '<div style="padding:16px;color:#ef4444">Error: '+(e && e.message ? e.message : e)+'</div>'; }
|
||||
}
|
||||
|
||||
function downloadSVGOf(container, filename){
|
||||
const svg = container.querySelector('svg');
|
||||
if(!svg) return;
|
||||
const serializer = new XMLSerializer();
|
||||
let source = serializer.serializeToString(svg);
|
||||
if(!source.match(/^<svg[^>]+xmlns=/)) source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
|
||||
source = '<?xml version="1.0" standalone="no"?>\n'+source;
|
||||
const blob = new Blob([source], { type:'image/svg+xml;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a'); a.href = url; a.download = filename; a.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 4000);
|
||||
}
|
||||
|
||||
function openSVGInNewTab(container){
|
||||
const svg = container.querySelector('svg');
|
||||
if(!svg) return;
|
||||
const serializer = new XMLSerializer();
|
||||
let source = serializer.serializeToString(svg);
|
||||
if(!source.match(/^<svg[^>]+xmlns=/)) source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
|
||||
source = '<?xml version="1.0" standalone="no"?>\n'+source;
|
||||
// Inject white background and readable styles for new tab view
|
||||
const firstGt = source.indexOf('>');
|
||||
if(firstGt > 0){
|
||||
const inject = '<rect width="100%" height="100%" fill="#ffffff"/><style>text{fill:#111827}.edgePath path,.flowchart-link{stroke:#334155}</style>';
|
||||
source = source.slice(0, firstGt+1) + inject + source.slice(firstGt+1);
|
||||
}
|
||||
const blob = new Blob([source], { type:'image/svg+xml;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, '_blank');
|
||||
setTimeout(() => URL.revokeObjectURL(url), 10000);
|
||||
}
|
||||
|
||||
const ALL_DIAGRAMS = [
|
||||
// System & DB
|
||||
{ id:'system-clean', label:'System Overview (Clean)', file:'system-overall-clean.mmd', cat:'System', tags:['overview','recommended','big'] },
|
||||
{ id:'system', label:'System Overview (Classic)', file:'system-overall.mmd', cat:'System', tags:['overview','big'], defaultWires:'faint' },
|
||||
{ id:'db-er', label:'Database ER', file:'db-er.mmd', cat:'System', tags:['db'] },
|
||||
{ id:'db-models', label:'Database Models', file:'db-models.mmd', cat:'System', tags:['db'] },
|
||||
// Backend
|
||||
{ id:'be-routes', label:'Backend Routes Overview', file:'backend-routes-overview.mmd', cat:'Backend', tags:['routes'] },
|
||||
{ id:'be-packages', label:'Backend Packages', file:'backend-packages.mmd', cat:'Backend', tags:['packages'] },
|
||||
{ id:'be-mw', label:'Backend Middleware Pipeline', file:'backend-middleware-pipeline.mmd', cat:'Backend', tags:['middleware'] },
|
||||
{ id:'be-jobs', label:'Backend Background Jobs', file:'backend-jobs.mmd', cat:'Backend', tags:['jobs'] },
|
||||
{ id:'auth', label:'Auth Flow', file:'auth-flow.mmd', cat:'Backend', tags:['auth','flow'] },
|
||||
{ id:'err-flow', label:'Error Tracking Flow', file:'error-tracking-flow.mmd', cat:'Backend', tags:['errors','flow'] },
|
||||
// Frontend
|
||||
{ id:'fe-everything', label:'Frontend — Everything (Big)', file:'frontend-everything.mmd', cat:'Frontend', tags:['overview','big'], defaultWires:'faint' },
|
||||
{ id:'fe-overall', label:'Frontend — Overall', file:'frontend-overall.mmd', cat:'Frontend', tags:['architecture'] },
|
||||
{ id:'fe-routes', label:'Frontend — Routes', file:'frontend-routes.mmd', cat:'Frontend', tags:['routes'] },
|
||||
{ id:'fe-home', label:'Frontend — Homepage', file:'frontend-homepage.mmd', cat:'Frontend', tags:['homepage'] },
|
||||
{ id:'fe-modules', label:'Frontend — Modules', file:'frontend-modules.mmd', cat:'Frontend', tags:['modules'] },
|
||||
{ id:'fe-arch', label:'Frontend — Provider Tree', file:'frontend-architecture.mmd', cat:'Frontend', tags:['providers'] },
|
||||
{ id:'fe-api', label:'Frontend — API Map', file:'frontend-api-map.mmd', cat:'Frontend', tags:['api'] },
|
||||
// Admin
|
||||
{ id:'admin-overall', label:'Admin — Overall', file:'admin-overall.mmd', cat:'Admin', tags:['admin','overview'], defaultWires:'faint' },
|
||||
{ id:'scoreboard', label:'Scoreboard Flow', file:'scoreboard-flow.mmd', cat:'Admin', tags:['scoreboard','flow'] },
|
||||
{ id:'newsletter', label:'Newsletter Flow', file:'newsletter-flow.mmd', cat:'Admin', tags:['newsletter','flow'] },
|
||||
{ id:'comments', label:'Comments Flow', file:'comments-flow.mmd', cat:'Admin', tags:['comments','flow'] },
|
||||
{ id:'gallery-zonerama', label:'Gallery (Zonerama) Flow', file:'gallery-zonerama-flow.mmd', cat:'Admin', tags:['gallery','flow'] },
|
||||
{ id:'shortlinks', label:'Shortlinks Flow', file:'shortlinks-flow.mmd', cat:'Admin', tags:['shortlinks','flow'] },
|
||||
{ id:'upload-flow', label:'Upload Flow', file:'upload-flow.mmd', cat:'Admin', tags:['upload','flow'] },
|
||||
];
|
||||
|
||||
function createCard(d){
|
||||
const sec = document.createElement('section');
|
||||
sec.className = 'card';
|
||||
sec.id = 'card-'+d.id;
|
||||
sec.dataset.cat = d.cat;
|
||||
sec.dataset.tags = (d.tags||[]).join(',');
|
||||
// No default wires styling; keep full visibility
|
||||
|
||||
const h = document.createElement('header');
|
||||
const title = document.createElement('div'); title.className = 'title';
|
||||
const h2 = document.createElement('h2'); h2.textContent = d.label; title.appendChild(h2);
|
||||
const meta = document.createElement('div'); meta.className='meta'; meta.textContent = d.file + ' • ' + d.cat; title.appendChild(meta);
|
||||
const right = document.createElement('div');
|
||||
const badge = document.createElement('span'); badge.className = 'badge'; badge.textContent = d.cat; right.appendChild(badge);
|
||||
h.appendChild(title); h.appendChild(right);
|
||||
|
||||
const wrap = document.createElement('div'); wrap.className='diagram-wrap';
|
||||
const diag = document.createElement('div'); diag.className='diagram'; diag.dataset.file = d.file; wrap.appendChild(diag);
|
||||
|
||||
const tb = document.createElement('div'); tb.className='toolbar';
|
||||
tb.innerHTML = `
|
||||
<label><input type="checkbox" class="fit" checked> Fit width</label>
|
||||
<a class="btn ghost src" href="${d.file}" target="_blank">Source</a>
|
||||
<span class="sp"></span>
|
||||
<button class="btn open">Open SVG in new tab</button>
|
||||
<button class="btn refresh">Refresh</button>
|
||||
<button class="btn primary download">Download SVG</button>`;
|
||||
|
||||
sec.appendChild(h); sec.appendChild(wrap); sec.appendChild(tb);
|
||||
return sec;
|
||||
}
|
||||
|
||||
function applyFitZoomFor(card){
|
||||
const container = card.querySelector('.diagram');
|
||||
const svg = container?.querySelector('svg');
|
||||
if(!svg) return;
|
||||
const fit = card.querySelector('.fit');
|
||||
if(fit && fit.checked){ svg.style.width='100%'; svg.style.height='auto'; } else { svg.style.width=''; svg.style.height=''; }
|
||||
svg.style.transformOrigin = '';
|
||||
svg.style.transform = '';
|
||||
}
|
||||
|
||||
function wireCardControls(card, file){
|
||||
const diag = card.querySelector('.diagram');
|
||||
card.dataset.file = file;
|
||||
const fit = card.querySelector('.fit');
|
||||
const openBtn = card.querySelector('.open');
|
||||
const refresh = card.querySelector('.refresh');
|
||||
const download = card.querySelector('.download');
|
||||
fit.addEventListener('change', () => applyFitZoomFor(card));
|
||||
openBtn.addEventListener('click', () => openSVGInNewTab(diag));
|
||||
refresh.addEventListener('click', async () => { diag.dataset.rendered=''; await renderMermaidFile(file, diag); diag.dataset.rendered='1'; applyFitZoomFor(card); });
|
||||
download.addEventListener('click', () => downloadSVGOf(diag, (file.replace('.mmd','')||'diagram')+'.svg'));
|
||||
}
|
||||
|
||||
let observer;
|
||||
function setupObserver(){
|
||||
if(observer) observer.disconnect();
|
||||
observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(async e => {
|
||||
if(e.isIntersecting){
|
||||
const card = e.target; const diag = card.querySelector('.diagram');
|
||||
if(diag && !diag.dataset.rendered){
|
||||
await renderMermaidFile(diag.dataset.file, diag); diag.dataset.rendered='1'; applyFitZoomFor(card);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, { root:null, rootMargin:'200px', threshold:0 });
|
||||
document.querySelectorAll('.card').forEach(c => observer.observe(c));
|
||||
}
|
||||
|
||||
function buildGrid(){
|
||||
const grid = document.getElementById('grid');
|
||||
grid.innerHTML='';
|
||||
for(const d of ALL_DIAGRAMS){ const card = createCard(d); grid.appendChild(card); wireCardControls(card, d.file); }
|
||||
setupObserver();
|
||||
}
|
||||
|
||||
function applyFilters(){
|
||||
const q = (document.getElementById('search').value || '').toLowerCase();
|
||||
const cats = Array.from(document.querySelectorAll('input[name="cat-filter"]:checked')).map(i=>i.value);
|
||||
document.querySelectorAll('.card').forEach(card => {
|
||||
const label = card.querySelector('h2').textContent.toLowerCase();
|
||||
const file = card.dataset.file || '';
|
||||
const cat = card.dataset.cat;
|
||||
const okCat = cats.length===0 || cats.includes(cat);
|
||||
const okText = !q || label.includes(q) || file.toLowerCase().includes(q) || (card.dataset.tags||'').toLowerCase().includes(q);
|
||||
card.style.display = (okCat && okText) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function globalAction(action){
|
||||
if(action==='refresh-visible'){
|
||||
document.querySelectorAll('.card').forEach(async card => {
|
||||
if(card.offsetParent!==null){
|
||||
const diag = card.querySelector('.diagram');
|
||||
if(diag){ diag.dataset.rendered=''; await renderMermaidFile(diag.dataset.file, diag); diag.dataset.rendered='1'; applyFitZoomFor(card); }
|
||||
}
|
||||
});
|
||||
}
|
||||
if(action==='expand-all'){ document.querySelectorAll('.diagram-wrap').forEach(w => w.style.maxHeight=''); }
|
||||
if(action==='collapse-all'){ document.querySelectorAll('.diagram-wrap').forEach(w => w.style.maxHeight='200px'); }
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
buildGrid();
|
||||
document.querySelectorAll('input[name="cat-filter"]').forEach(i=> i.addEventListener('change', applyFilters));
|
||||
document.getElementById('search').addEventListener('input', applyFilters);
|
||||
document.getElementById('refreshVisible').addEventListener('click', () => globalAction('refresh-visible'));
|
||||
document.getElementById('expandAll').addEventListener('click', () => globalAction('expand-all'));
|
||||
document.getElementById('collapseAll').addEventListener('click', () => globalAction('collapse-all'));
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Fotbal Club — Unified Diagrams</h1>
|
||||
<div class="filters">
|
||||
<input id="search" type="search" placeholder="Search diagrams, files, tags…" />
|
||||
<label class="chip"><input type="checkbox" name="cat-filter" value="System" /> System</label>
|
||||
<label class="chip"><input type="checkbox" name="cat-filter" value="Backend" /> Backend</label>
|
||||
<label class="chip"><input type="checkbox" name="cat-filter" value="Frontend" /> Frontend</label>
|
||||
<label class="chip"><input type="checkbox" name="cat-filter" value="Admin" /> Admin</label>
|
||||
<span class="sp"></span>
|
||||
<button class="btn" id="refreshVisible">Refresh visible</button>
|
||||
<button class="btn ghost" id="expandAll">Expand all</button>
|
||||
<button class="btn ghost" id="collapseAll">Collapse all</button>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div id="grid" class="grid"></div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,29 @@
|
||||
%%{init: {'theme': 'neutral'}}%%
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Admin as Admin (NewsletterAdminPage)
|
||||
participant FE as Frontend Services (admin/newsletter.ts)
|
||||
participant BE as Backend /api/v1/admin/newsletter
|
||||
participant Email as EmailService (SMTP)
|
||||
|
||||
Note over Admin,BE: Manual send/test/preview/status
|
||||
Admin->>FE: Click Send/Preview/Test
|
||||
FE->>BE: POST /newsletter/send | /preview | /test
|
||||
BE->>BE: Build content (newsletter_content)
|
||||
BE->>Email: Send via SMTP (per-recipient)
|
||||
Email-->>BE: Accepted/Failed
|
||||
BE-->>FE: Result + stats
|
||||
|
||||
Note over FE,BE: Status & stats
|
||||
FE->>BE: GET /newsletter/status
|
||||
BE-->>FE: Enabled/automation state
|
||||
FE->>BE: GET /newsletter/stats/recent
|
||||
BE-->>FE: List recent sends, events
|
||||
|
||||
rect rgba(225,255,225,0.2)
|
||||
Note over BE,Email: Automation (weekly, match alerts, blog notifications, results)
|
||||
BE->>BE: NewsletterScheduler tick
|
||||
BE->>BE: NewsletterAutomation decides digests/reminders
|
||||
BE->>Email: Send batches
|
||||
Email-->>BE: Delivery responses
|
||||
end
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"args": ["--no-sandbox", "--disable-setuid-sandbox"]
|
||||
"args": ["--no-sandbox", "--disable-setuid-sandbox"],
|
||||
"headless": true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"args": ["--no-sandbox", "--disable-setuid-sandbox"]
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
%%{init: {'theme': 'forest', 'flowchart': { 'curve': 'linear' }}}%%
|
||||
flowchart LR
|
||||
|
||||
classDef page fill:#e0f2fe,stroke:#0369a1,color:#0c4a6e;
|
||||
classDef admin fill:#dcfce7,stroke:#16a34a,color:#065f46;
|
||||
classDef api fill:#eef2ff,stroke:#6366f1,color:#312e81;
|
||||
classDef pub fill:#f1f5f9,stroke:#334155,color:#0f172a;
|
||||
classDef db fill:#fde68a,stroke:#ca8a04,color:#7c2d12;
|
||||
classDef ext fill:#faf5ff,stroke:#a855f7,color:#6b21a8;
|
||||
|
||||
subgraph FE_Pages["Frontend Pages"]
|
||||
direction TB
|
||||
p_admin["Admin Scoreboard Page"]:::page
|
||||
p_mobile["Mobile Scoreboard Control Page"]:::page
|
||||
p_overlay_sb["Overlay Scoreboard Page"]:::page
|
||||
p_overlay_sp["Overlay Sponsors Page"]:::page
|
||||
end
|
||||
|
||||
subgraph BE_API["Backend API /api/v1"]
|
||||
direction TB
|
||||
a_pub["Public"]:::api
|
||||
a_admin["Admin"]:::api
|
||||
|
||||
%% Public endpoints
|
||||
e_sb_get["GET /scoreboard"]:::pub
|
||||
e_sb_colors["GET /scoreboard/colors/derive"]:::pub
|
||||
e_sb_sponsors["GET /scoreboard/sponsors"]:::pub
|
||||
e_sb_qr_get["GET /scoreboard/qr"]:::pub
|
||||
|
||||
%% Admin endpoints
|
||||
e_admin_get["GET /admin/scoreboard"]:::admin
|
||||
e_admin_put["PUT /admin/scoreboard"]:::admin
|
||||
e_timer_start["POST /admin/scoreboard/timer/start"]:::admin
|
||||
e_timer_pause["POST /admin/scoreboard/timer/pause"]:::admin
|
||||
e_timer_reset["POST /admin/scoreboard/timer/reset"]:::admin
|
||||
e_swap_sides["POST /admin/scoreboard/swap-sides"]:::admin
|
||||
e_second_half["POST /admin/scoreboard/second-half"]:::admin
|
||||
e_save["POST /admin/scoreboard/save"]:::admin
|
||||
e_saves_list["GET /admin/scoreboard/saves"]:::admin
|
||||
e_load["POST /admin/scoreboard/load"]:::admin
|
||||
e_sp_list["GET /admin/scoreboard/sponsors"]:::admin
|
||||
e_sp_upload["POST /admin/scoreboard/sponsors/upload"]:::admin
|
||||
e_sp_prefill["POST /admin/scoreboard/sponsors/prefill"]:::admin
|
||||
e_sp_delete["DELETE /admin/scoreboard/sponsors"]:::admin
|
||||
e_qr_get["GET /admin/scoreboard/qr"]:::admin
|
||||
e_qr_upload["POST /admin/scoreboard/qr"]:::admin
|
||||
end
|
||||
|
||||
subgraph DATA["Data Storage"]
|
||||
direction TB
|
||||
d_state["ScoreboardState (DB)"]:::db
|
||||
d_sponsor_files["UploadedFile + FileUsage (Sponsors)"]:::db
|
||||
end
|
||||
|
||||
%% FE -> Public
|
||||
p_overlay_sb --> e_sb_get
|
||||
p_overlay_sp --> e_sb_sponsors
|
||||
p_overlay_sb -. derive colors .-> e_sb_colors
|
||||
p_overlay_sb -. QR overlay uses .-> e_sb_qr_get
|
||||
|
||||
%% FE -> Admin (both pages use same admin endpoints)
|
||||
p_admin --> e_admin_get
|
||||
p_admin --> e_admin_put
|
||||
p_admin --> e_timer_start
|
||||
p_admin --> e_timer_pause
|
||||
p_admin --> e_timer_reset
|
||||
p_admin --> e_swap_sides
|
||||
p_admin --> e_second_half
|
||||
p_admin --> e_save
|
||||
p_admin --> e_saves_list
|
||||
p_admin --> e_load
|
||||
p_admin --> e_sp_list
|
||||
p_admin --> e_sp_upload
|
||||
p_admin --> e_sp_prefill
|
||||
p_admin --> e_sp_delete
|
||||
p_admin --> e_qr_get
|
||||
p_admin --> e_qr_upload
|
||||
|
||||
p_mobile --> e_timer_start
|
||||
p_mobile --> e_timer_pause
|
||||
p_mobile --> e_timer_reset
|
||||
p_mobile --> e_swap_sides
|
||||
p_mobile --> e_second_half
|
||||
|
||||
%% API -> Data
|
||||
e_admin_put --> d_state
|
||||
e_timer_start --> d_state
|
||||
e_timer_pause --> d_state
|
||||
e_timer_reset --> d_state
|
||||
e_swap_sides --> d_state
|
||||
e_second_half --> d_state
|
||||
e_save --> d_state
|
||||
e_load --> d_state
|
||||
e_sp_upload --> d_sponsor_files
|
||||
e_sp_delete --> d_sponsor_files
|
||||
e_sp_prefill --> d_sponsor_files
|
||||
|
||||
%% Public reads from DB-backed state/caches
|
||||
e_sb_get -. read .- d_state
|
||||
e_sb_sponsors -. read .- d_state
|
||||
e_sb_qr_get -. read .- d_state
|
||||
@@ -0,0 +1,38 @@
|
||||
%%{init: {'theme': 'neutral'}}%%
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Visitor as Visitor
|
||||
participant Editor as Editor/Admin
|
||||
participant FE as Frontend
|
||||
participant BE as Backend API
|
||||
participant DB as Postgres
|
||||
|
||||
Note over Visitor,BE: Public create (same-site only)
|
||||
FE->>BE: POST /api/v1/shortlinks/public {url, note}
|
||||
BE->>BE: RateLimit(30/min) + origin checks
|
||||
BE->>DB: Insert ShortLink
|
||||
DB-->>BE: id, code
|
||||
BE-->>FE: 201 Created {code, short_url}
|
||||
|
||||
rect rgba(220,255,220,0.2)
|
||||
Note over Editor,BE: Editor/Admin management
|
||||
FE->>BE: POST /api/v1/shortlinks (editor)
|
||||
BE->>DB: Insert ShortLink
|
||||
BE-->>FE: 201 Created
|
||||
|
||||
FE->>BE: GET /api/v1/shortlinks (editor)
|
||||
BE->>DB: List
|
||||
BE-->>FE: 200 OK [shortlinks]
|
||||
|
||||
FE->>BE: GET /api/v1/admin/shortlinks/:id/stats (admin)
|
||||
BE->>DB: Aggregate LinkClick
|
||||
BE-->>FE: 200 OK {stats}
|
||||
end
|
||||
|
||||
rect rgba(230,230,255,0.2)
|
||||
Note over FE,BE: Public redirect
|
||||
FE->>BE: GET /s/:code (root)
|
||||
BE->>DB: Lookup ShortLink by code
|
||||
BE->>DB: Insert LinkClick
|
||||
BE-->>FE: 302 Redirect to target
|
||||
end
|
||||
@@ -1,56 +0,0 @@
|
||||
# System Architecture
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph Clients
|
||||
A["Public site React SPA"]
|
||||
B["Admin SPA"]
|
||||
C["Scoreboard Overlay"]
|
||||
end
|
||||
|
||||
subgraph Frontend
|
||||
FE["React 18 + Chakra UI; Router + Query"]
|
||||
end
|
||||
|
||||
subgraph Backend
|
||||
BE["Go Gin REST API api v1; GORM services"]
|
||||
JOBS["Background jobs; Prefetcher; Newsletter automation"]
|
||||
end
|
||||
|
||||
subgraph Data
|
||||
DB["PostgreSQL"]
|
||||
UP["uploads static dist"]
|
||||
end
|
||||
|
||||
subgraph Integrations_optional
|
||||
FACR["FACR API"]
|
||||
YT["YouTube API"]
|
||||
ZON["Zonerama"]
|
||||
SMTP["SMTP email"]
|
||||
MAPS["Google Maps"]
|
||||
UMAMI["Umami Analytics"]
|
||||
end
|
||||
|
||||
A --> FE
|
||||
B --> FE
|
||||
C --> FE
|
||||
|
||||
FE -->|REST JSON| BE
|
||||
FE -->|uploads static| UP
|
||||
|
||||
BE --> DB
|
||||
BE --> UP
|
||||
|
||||
%% External calls
|
||||
BE --> FACR
|
||||
BE --> YT
|
||||
BE --> ZON
|
||||
BE --> SMTP
|
||||
BE -. "telemetry" .-> UMAMI
|
||||
BE --> MAPS
|
||||
|
||||
%% Jobs
|
||||
JOBS --> BE
|
||||
JOBS --> DB
|
||||
JOBS --> SMTP
|
||||
```
|
||||
@@ -1,52 +0,0 @@
|
||||
graph LR
|
||||
subgraph Clients
|
||||
A["Public site React SPA"]
|
||||
B["Admin SPA"]
|
||||
C["Scoreboard Overlay"]
|
||||
end
|
||||
|
||||
subgraph Frontend
|
||||
FE["React 18 + Chakra UI; Router + Query"]
|
||||
end
|
||||
|
||||
subgraph Backend
|
||||
BE["Go Gin REST API api v1; GORM services"]
|
||||
JOBS["Background jobs; Prefetcher; Newsletter automation"]
|
||||
end
|
||||
|
||||
subgraph Data
|
||||
DB["PostgreSQL"]
|
||||
UP["uploads static dist"]
|
||||
end
|
||||
|
||||
subgraph Integrations_optional
|
||||
FACR["FACR API"]
|
||||
YT["YouTube API"]
|
||||
ZON["Zonerama"]
|
||||
SMTP["SMTP email"]
|
||||
MAPS["Google Maps"]
|
||||
UMAMI["Umami Analytics"]
|
||||
end
|
||||
|
||||
A --> FE
|
||||
B --> FE
|
||||
C --> FE
|
||||
|
||||
FE -->|REST JSON| BE
|
||||
FE -->|uploads static| UP
|
||||
|
||||
BE --> DB
|
||||
BE --> UP
|
||||
|
||||
%% External calls
|
||||
BE --> FACR
|
||||
BE --> YT
|
||||
BE --> ZON
|
||||
BE --> SMTP
|
||||
BE -. "telemetry" .-> UMAMI
|
||||
BE --> MAPS
|
||||
|
||||
%% Jobs
|
||||
JOBS --> BE
|
||||
JOBS --> DB
|
||||
JOBS --> SMTP
|
||||
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,25 @@
|
||||
%%{init: {"theme":"forest","flowchart":{"curve":"linear"},"themeCSS":".edgePath path { stroke-dasharray: 6 4; animation: dash 16s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } }" }}%%
|
||||
flowchart LR
|
||||
|
||||
classDef client fill:#f1f5f9,stroke:#334155,color:#0f172a;
|
||||
classDef fe fill:#fff7ed,stroke:#f59e0b,color:#7c2d12;
|
||||
classDef be fill:#ecfdf5,stroke:#16a34a,color:#065f46;
|
||||
classDef db fill:#e3f2fd,stroke:#1e88e5,color:#0c4a6e;
|
||||
classDef ext fill:#f5f3ff,stroke:#8b5cf6,color:#4c1d95;
|
||||
classDef stat fill:#e2e8f0,stroke:#475569,color:#111827;
|
||||
|
||||
U((User Browser)):::client
|
||||
FE[Frontend (React app)]:::fe
|
||||
API[Backend API (Go + Gin)\n/api/v1]:::be
|
||||
DB[(PostgreSQL DB)]:::db
|
||||
STATIC[Static & Uploads\n/assets, /uploads]:::stat
|
||||
EXT[External Services\n(SMTP, Error Receiver, Umami, FACR, Zonerama, YouTube)]:::ext
|
||||
|
||||
U --> FE
|
||||
FE ==>|HTTP| API
|
||||
API --> DB
|
||||
API <-->|read/write| STATIC
|
||||
API --> EXT
|
||||
|
||||
%% Optional: public static reads
|
||||
U -.->|static files| STATIC
|
||||
@@ -0,0 +1,378 @@
|
||||
%%{init: {"theme":"forest","flowchart":{"curve":"linear"},"themeCSS":"svg { font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; } .edgePath path { stroke-dasharray: 6 4; animation: dash 16s linear infinite; } .animated-edge path { stroke-dasharray: 6 4; animation: dash 16s linear infinite; } @keyframes dash { to { stroke-dashoffset: -1000; } } .ext > rect, .ext > polygon, .ext > path { stroke: #7e57c2; } .db > rect, .db > polygon, .db > path { fill: #e3f2fd; stroke: #1e88e5; } .svc > rect, .svc > polygon, .svc > path { fill: #e8f5e9; stroke: #43a047; } .fe > rect, .fe > polygon, .fe > path { fill: #fff8e1; stroke: #f9a825; } .ctrl > rect, .ctrl > polygon, .ctrl > path { fill: #f3e5f5; stroke: #8e24aa; } .mid > rect, .mid > polygon, .mid > path { fill: #e0f2f1; stroke: #00897b; } .model > rect, .model > polygon, .model > path { fill: #ede7f6; stroke: #5e35b1; } .route > rect, .route > polygon, .route > path { fill: #e8eaf6; stroke: #3f51b5; }" }}%%
|
||||
flowchart TB
|
||||
|
||||
%% ========================= Docker & Runtime =========================
|
||||
subgraph DOCKER["Docker Compose (Local Dev/Prod)"]
|
||||
direction TB
|
||||
docker_net([Bridge Network: fotbal-network]):::svc
|
||||
docker_vol1["Volume: postgres_data"]:::svc
|
||||
docker_vol2["Bind: ./uploads -> /app/uploads"]:::svc
|
||||
docker_vol3["Bind: ./cache -> /app/cache"]:::svc
|
||||
|
||||
subgraph docker_backend["backend (Go) container"]
|
||||
direction TB
|
||||
be_8080["Expose 8080:8080"]
|
||||
be_env[".env + overrides"]
|
||||
be_cmd["command: ./main"]
|
||||
end
|
||||
|
||||
subgraph docker_frontend["frontend (Nginx) container"]
|
||||
direction TB
|
||||
fe_3000["Expose 3000:80"]
|
||||
fe_env[".env.frontend"]
|
||||
end
|
||||
|
||||
subgraph docker_db["postgres:15-alpine"]
|
||||
direction TB
|
||||
db_5432["Expose 5432:5432"]
|
||||
db_env["POSTGRES_* env"]
|
||||
end
|
||||
|
||||
docker_net --- docker_backend
|
||||
docker_net --- docker_frontend
|
||||
docker_net --- docker_db
|
||||
docker_vol1 --- docker_db
|
||||
docker_vol2 --- docker_backend
|
||||
docker_vol3 --- docker_backend
|
||||
end
|
||||
|
||||
user_browser((User Browser)):::ext
|
||||
user_browser ==>|HTTP 80| docker_frontend:::animated-edge
|
||||
user_browser -.->|dev direct (HTTP 8080)| docker_backend
|
||||
|
||||
%% ========================= Backend (Go/Gin) =========================
|
||||
subgraph BACKEND["Backend Service (Golang + Gin) :8080"]
|
||||
direction TB
|
||||
cfg[Config (internal/config.Config)\n- APP_ENV/PORT/DEBUG\n- DATABASE_URL (GORM)\n- JWT_SECRET/EXP\n- ALLOWED_ORIGINS (CORS)\n- UPLOAD_DIR/MAX_UPLOAD_SIZE\n- SMTP_* (Email)\n- FRONTEND_BASE_URL\n- PUBLIC_API_BASE_URL\n- ERROR_INGEST_URL/TOKEN\n- FACR_SCRAPER_BASE_URL\n- UMAMI_*\n- CLAMAV_* (optional)]
|
||||
logger[Logger (pkg/logger)]
|
||||
db_init[[InitDB() + AutoMigrate()]]:::db
|
||||
email_svc[EmailService (pkg/email)]:::svc
|
||||
|
||||
subgraph middleware[Middleware]
|
||||
direction TB
|
||||
mw_reqid[RequestID]
|
||||
mw_logger[RequestLogger]
|
||||
mw_recovery[CustomRecoveryWithReporter]
|
||||
mw_errstatus[ErrorStatusReporter]
|
||||
mw_sanitize[SanitizeHeaders]
|
||||
mw_dbctx[DBContext (req ctx timeout)]
|
||||
mw_size[RequestSizeLimit (2MB)]
|
||||
mw_ct[ValidateContentType]
|
||||
mw_csrf[CSRFProtection (protected)]
|
||||
mw_rate[RateLimit (per-route)]
|
||||
mw_sec[SecurityHeaders + AssetCacheControl]
|
||||
end
|
||||
|
||||
prometheus["GET /metrics (promhttp)"]
|
||||
static1["Static: /uploads -> UPLOAD_DIR"]
|
||||
static2["Static: /cache -> ./cache"]
|
||||
static3["Static: /dist -> ./static"]
|
||||
static4["Static: /premium-assets -> ./pro"]
|
||||
|
||||
subgraph router["Router"]
|
||||
direction TB
|
||||
api_grp["/api/v1"]:::route
|
||||
root_grp["/root/"]:::route
|
||||
end
|
||||
|
||||
subgraph controllers[Controllers]
|
||||
direction TB
|
||||
c_auth[AuthController\n/login,/logout,/register,/me\n/password-reset]
|
||||
c_contact[ContactController\n/contact + newsletter + admin forwarding]
|
||||
c_pass[PasswordController]
|
||||
c_ai[AIController\n/ai/blog,/ai/about,/ai/css,/ai/instagram]
|
||||
c_score[ScoreboardController\n/public + admin timer/sponsors/qr]
|
||||
c_about[AboutController]
|
||||
c_gallery[GalleryController\n/Zonerama profile/albums/picks]
|
||||
c_files[FilesController\n/list/unused/duplicates/usage\n/scan/refresh-tracking/delete]
|
||||
c_notify[NotificationsController]
|
||||
c_email[EmailController\n/open.gif/click/unsubscribe/stats]
|
||||
c_prefetch[PrefetchController\n/status/trigger]
|
||||
c_seo[SEOController\n/seo (public) + robots.txt + sitemap]
|
||||
c_nav[NavigationController\n/navigation + social-links + admin CRUD]
|
||||
c_poll[PollController\n/public vote/results + admin]
|
||||
c_sw[SweepstakesController\n/public current/visual + admin CRUD/finalize]
|
||||
c_cloth[ClothingController\n/public + admin CRUD]
|
||||
c_pec[PageElementConfigController\n/public + admin CRUD/batch]
|
||||
c_article[ArticleController\n/create + match-link]
|
||||
c_base[BaseController\n/health, uploads, categories, teams, players, matches, standings, zonerama, settings, shortlinks(public)]
|
||||
c_myu[MyUIbrixController\n/validate,/preview,/optimize]
|
||||
c_editor[EditorPreviewController\n/preview state + variants]
|
||||
c_short[ShortLinkController\n/public create + admin + redirect /s/:code]
|
||||
c_comment[CommentController\n/public list + CRUD + reactions\nban/unban/report (admin)]
|
||||
c_eng[EngagementController\n/rewards/leaderboard/profile/actions]
|
||||
c_facr[FACRController\n/facr club search/info/table]
|
||||
c_yt[YouTubeController\n/youtube/videos]
|
||||
c_umami[UmamiController\n/config + admin initialize/stats]
|
||||
c_error[ErrorController\n/errors ingest + admin + external]
|
||||
end
|
||||
|
||||
subgraph services[Services & Jobs]
|
||||
direction TB
|
||||
s_errrep[ErrorReporter]
|
||||
s_prefetch[Prefetcher\nStartPrefetcher(target)]
|
||||
s_nlsched[NewsletterScheduler]
|
||||
s_nlauto[NewsletterAutomation\nweekly, reminders, results]
|
||||
s_sweep[SweepstakesScheduler]
|
||||
s_umami[UmamiService]
|
||||
s_facr[FACRService]
|
||||
s_cache[CacheService]
|
||||
s_logo[LogoCache]
|
||||
s_filetrk[FileTracker]
|
||||
s_imgopt[ImageOptimizer]
|
||||
s_bad[BadWords/Spam]
|
||||
s_setup[SetupService]
|
||||
end
|
||||
|
||||
subgraph models[Models (GORM)]
|
||||
direction LR
|
||||
m_settings[Settings]
|
||||
m_user[User]
|
||||
m_article[Article]
|
||||
m_scoreboard[ScoreboardState]
|
||||
m_compalias[CompetitionAlias]
|
||||
m_team[Team]
|
||||
m_player[Player]
|
||||
m_contact_cat[ContactCategory]
|
||||
m_contact[Contact]
|
||||
m_contact_msg[ContactMessage]
|
||||
m_news[NewsletterSubscription]
|
||||
m_sponsor[Sponsor]
|
||||
m_cloth[Clothing]
|
||||
m_poll[Poll + PollOption + PollVote]
|
||||
m_nav[NavigationItem + SocialLink]
|
||||
m_pageel[PageElementConfig]
|
||||
m_short[ShortLink + LinkClick]
|
||||
m_comment[Comment + Reaction + Ban + UnbanRequest + Report]
|
||||
m_profile[UserProfile]
|
||||
m_points[PointsTransaction]
|
||||
m_ach[Achievement + UserAchievement]
|
||||
m_reward[RewardItem + RewardRedemption]
|
||||
m_over[MatchOverride + TeamLogoOverride]
|
||||
m_sweep[Sweepstake + Prize + Entry + Winner]
|
||||
m_up[UploadedFile + FileUsage]
|
||||
m_error[ErrorEvent]
|
||||
end
|
||||
|
||||
%% wiring inside backend
|
||||
cfg --> db_init
|
||||
cfg --> email_svc
|
||||
router --> middleware
|
||||
api_grp --> controllers
|
||||
root_grp --> c_seo
|
||||
root_grp --> c_short
|
||||
api_grp --> c_auth
|
||||
api_grp --> c_contact
|
||||
api_grp --> c_pass
|
||||
api_grp --> c_ai
|
||||
api_grp --> c_score
|
||||
api_grp --> c_about
|
||||
api_grp --> c_gallery
|
||||
api_grp --> c_files
|
||||
api_grp --> c_notify
|
||||
api_grp --> c_email
|
||||
api_grp --> c_prefetch
|
||||
api_grp --> c_seo
|
||||
api_grp --> c_nav
|
||||
api_grp --> c_poll
|
||||
api_grp --> c_sw
|
||||
api_grp --> c_cloth
|
||||
api_grp --> c_pec
|
||||
api_grp --> c_article
|
||||
api_grp --> c_base
|
||||
api_grp --> c_myu
|
||||
api_grp --> c_editor
|
||||
api_grp --> c_short
|
||||
api_grp --> c_comment
|
||||
api_grp --> c_eng
|
||||
api_grp --> c_facr
|
||||
api_grp --> c_yt
|
||||
api_grp --> c_umami
|
||||
api_grp --> c_error
|
||||
|
||||
%% controllers -> models
|
||||
controllers -->|GORM| models
|
||||
db_init ==>|Postgres conn| DB["PostgreSQL :5432 / fotbal_club"]:::db
|
||||
models ==>|tables| DB
|
||||
|
||||
%% services wiring
|
||||
s_prefetch -.->|GET public endpoints| api_grp
|
||||
s_nlsched --> s_nlauto
|
||||
s_nlauto --> email_svc
|
||||
s_sweep --> email_svc
|
||||
s_filetrk --> m_up
|
||||
s_imgopt --> m_up
|
||||
s_errrep --> c_error
|
||||
s_umami --> c_umami
|
||||
s_facr --> c_facr
|
||||
email_svc -->|SMTP| smtp[(SMTP Provider)]:::ext
|
||||
|
||||
%% externals
|
||||
facr_ext["FACR Scraper :8081"]:::ext
|
||||
errors_ingest["Error Receiver: errors.tdvorak.dev/api/v1/errors or local :8083"]:::ext
|
||||
errors_admin["Error Review Admin UI/API: errors.tdvorak.dev"]:::ext
|
||||
umami_ext["Umami Analytics server"]:::ext
|
||||
|
||||
s_facr <---> facr_ext:::animated-edge
|
||||
s_errrep --> errors_ingest:::animated-edge
|
||||
c_error <---> errors_admin
|
||||
s_umami <---> umami_ext
|
||||
|
||||
%% static serving
|
||||
static1 --- user_browser
|
||||
static2 --- user_browser
|
||||
static3 --- user_browser
|
||||
static4 --- user_browser
|
||||
|
||||
%% metrics
|
||||
prometheus --- user_browser
|
||||
end
|
||||
|
||||
user_browser ==>|HTTP /api/v1| api_grp:::animated-edge
|
||||
user_browser ==>|HTTP /robots.txt, /sitemap.xml, /s/:code| root_grp
|
||||
|
||||
%% ========================= Frontend (React) =========================
|
||||
subgraph FRONTEND[Frontend (React + ChakraUI)]
|
||||
direction TB
|
||||
fe_router[React Router (src/App.tsx)]:::fe
|
||||
|
||||
subgraph fe_public[Public Pages]
|
||||
direction LR
|
||||
p_home[HomePage /]
|
||||
p_blog[BlogPage /blog]
|
||||
p_newslist[ArticlesListPage]
|
||||
p_article[ArticleDetailPage /news/:slug | /articles/:id]
|
||||
p_about[AboutPage /o-klubu]
|
||||
p_club[ClubPage /klub]
|
||||
p_calendar[CalendarPage /kalendar]
|
||||
p_actcal[ActivitiesCalendarPage /aktivity]
|
||||
p_tables[TablesPage /tabulky]
|
||||
p_matches[MatchesPage /zapasy]
|
||||
p_match[MatchDetailPage /zapas/:id]
|
||||
p_players[PlayersPage /hraci]
|
||||
p_player[PlayerDetailPage /hraci/:id]
|
||||
p_sponsors[SponsorsPage /sponzori]
|
||||
p_contact[ContactPage /kontakt]
|
||||
p_gallery[GalleryPage /galerie]
|
||||
p_album[AlbumDetailPage /galerie/album/:id]
|
||||
p_videos[VideosPage /videa]
|
||||
p_clothing[ClothingPage /obleceni]
|
||||
p_polls[PollsPage /ankety]
|
||||
p_search[SearchPage /hledat]
|
||||
p_short[ShortRedirectPage /s/:code]
|
||||
p_over_sb[OverlayScoreboardPage /overlay/scoreboard]
|
||||
p_over_sp[OverlaySponsorsPage /overlay/sponsors]
|
||||
p_cookies[CookiePolicyPage]
|
||||
p_terms[TermsPage]
|
||||
p_privacy[PrivacyPolicyPage]
|
||||
p_notfound[NotFoundPage *]
|
||||
end
|
||||
|
||||
subgraph fe_auth[Auth & Setup]
|
||||
direction LR
|
||||
p_login[AuthPage /login]
|
||||
p_register[RegisterPage /register]
|
||||
p_forgot[ForgotPasswordPage /forgot-password]
|
||||
p_reset[ResetPasswordPage /reset-password]
|
||||
p_setup[SetupPage /setup]
|
||||
p_style[StylePreviewPage /setup/styl]
|
||||
p_news_unsub[NewsletterUnsubscribePage]
|
||||
p_news_prefs[NewsletterPreferencesPage]
|
||||
end
|
||||
|
||||
subgraph fe_admin[Admin Pages]
|
||||
direction LR
|
||||
a_dashboard[AdminDashboardPage]
|
||||
a_docs[AdminDocsPage]
|
||||
a_about[AboutAdminPage]
|
||||
a_videos[AdminVideosPage]
|
||||
a_gallery[GalleryAdminPage]
|
||||
a_merch[AdminMerchPage]
|
||||
a_sponsors[SponsorsAdminPage]
|
||||
a_matches[MatchesAdminPage]
|
||||
a_players[PlayersAdminPage]
|
||||
a_teams[TeamsAdminPage]
|
||||
a_users[UsersAdminPage]
|
||||
a_banners[BannersAdminPage]
|
||||
a_messages[MessagesAdminPage]
|
||||
a_settings[SettingsAdminPage]
|
||||
a_newsletter[NewsletterAdminPage]
|
||||
a_polls[PollsAdminPage]
|
||||
a_comp[CompetitionAliasesAdminPage]
|
||||
a_prefetch[PrefetchAdminPage]
|
||||
a_scoreboard[ScoreboardAdminPage]
|
||||
a_score_remote[MobileScoreboardControlPage]
|
||||
a_analytics[AnalyticsAdminPage]
|
||||
a_shortlinks[ShortlinksAdminPage]
|
||||
a_files[FilesAdminPage]
|
||||
a_contacts[ContactsAdminPage]
|
||||
a_navigation[NavigationAdminPage]
|
||||
a_comments[CommentsAdminPage]
|
||||
a_engagement[EngagementAdminPage]
|
||||
a_sweep[SweepstakesAdminPage]
|
||||
a_sweep_visual[SweepstakeVisualPage]
|
||||
a_adminreset[AdminResetPasswordPage]
|
||||
end
|
||||
|
||||
%% FE -> BE API mappings (high level)
|
||||
fe_router -->|services/api.ts| api_grp:::animated-edge
|
||||
p_blog -->|GET /articles| api_grp
|
||||
p_article -->|GET /articles/slug/:slug, /articles/:id\nPOST /articles/:id/read| api_grp
|
||||
p_home -->|GET /articles/featured, /matches, /standings, /settings, /navigation| api_grp
|
||||
p_matches -->|GET /matches,/standings| api_grp
|
||||
p_match -->|GET /matches/:id| api_grp
|
||||
p_players -->|GET /players| api_grp
|
||||
p_player -->|GET /players/:id| api_grp
|
||||
p_gallery -->|GET /gallery/albums| api_grp
|
||||
p_album -->|GET /gallery/albums/:id| api_grp
|
||||
p_videos -->|GET /youtube/videos| api_grp
|
||||
p_clothing -->|GET /clothing| api_grp
|
||||
p_polls -->|GET /polls| api_grp
|
||||
p_contact -->|POST /contact| api_grp
|
||||
p_over_sb -->|GET /scoreboard (public)| api_grp
|
||||
p_over_sp -->|GET /scoreboard/sponsors| api_grp
|
||||
p_short -->|GET /s/:code (root)| root_grp
|
||||
|
||||
%% Admin flows
|
||||
a_articles[ArticlesAdminPage] -->|POST/PUT/DELETE /articles\n/link-match| api_grp
|
||||
a_matches -->|GET /admin/matches| api_grp
|
||||
a_comments -->|GET/PATCH /admin/comments| api_grp
|
||||
a_navigation -->|CRUD /admin/navigation| api_grp
|
||||
a_files -->|GET/DELETE /admin/files| api_grp
|
||||
a_scoreboard -->|GET/PUT /admin/scoreboard + timer| api_grp
|
||||
a_score_remote -->|POST timer controls| api_grp
|
||||
a_newsletter -->|send/test/preview/status| api_grp
|
||||
a_sweep -->|CRUD /admin/sweepstakes| api_grp
|
||||
a_sweep_visual -->|GET /admin/sweepstakes/:id/visual| api_grp
|
||||
a_analytics -->|/admin/umami| api_grp
|
||||
|
||||
%% FE error reporting & analytics
|
||||
fe_router -->|POST /errors (ErrorReporter)| api_grp:::animated-edge
|
||||
fe_router -->|GET /umami/config| api_grp
|
||||
|
||||
end
|
||||
|
||||
%% ========================= Ports & CORS =========================
|
||||
subgraph PORTS[Ports & CORS]
|
||||
direction LR
|
||||
port_be[Backend :8080]
|
||||
port_fe[Frontend :3000 -> :80]
|
||||
port_db[Postgres :5432]
|
||||
cors[CORS AllowedOrigins\n- http://localhost:3000\n- http://localhost:8080\n+ FrontendBaseURL origin\n+ "*" optional in dev]
|
||||
end
|
||||
port_be --- docker_backend
|
||||
port_fe --- docker_frontend
|
||||
port_db --- docker_db
|
||||
cors -. controls .- router
|
||||
|
||||
%% Legend
|
||||
subgraph LEGEND[Legend]
|
||||
direction LR
|
||||
L1[External Service]:::ext
|
||||
L2[Database/Table]:::db
|
||||
L3[Service/Daemon]:::svc
|
||||
L4[Controller]:::ctrl
|
||||
L5[Middleware]:::mid
|
||||
L6[Model]:::model
|
||||
L7[Route Group]:::route
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
%%{init: {'theme': 'neutral'}}%%
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant U as Editor/User
|
||||
participant FE as Frontend (React)
|
||||
participant BE as Backend API
|
||||
participant FS as File Storage (uploads dir)
|
||||
participant DB as Postgres
|
||||
|
||||
Note over U,FE: Drag & drop / select image
|
||||
U->>FE: Choose file
|
||||
FE->>BE: POST /api/v1/upload (multipart/form-data)
|
||||
BE->>BE: RateLimit(30/min), size/type validation
|
||||
BE->>FS: Save file to UPLOAD_DIR
|
||||
BE->>DB: Insert UploadedFile + FileUsage (if provided)
|
||||
BE-->>FE: 200 OK {url, id}
|
||||
|
||||
rect rgba(230,255,230,0.2)
|
||||
Note over U,BE: Quick edits / crop (editor role required)
|
||||
FE->>BE: POST /api/v1/image-processing/process | crop-upload | quick-edit
|
||||
BE->>BE: JWTAuth + CSRF + RoleAuth(editor)
|
||||
BE->>FS: Read/transform/write
|
||||
BE->>DB: Track usages/updates
|
||||
BE-->>FE: 200 OK {url}
|
||||
end
|
||||