This commit is contained in:
Tomáš Dvořák
2025-10-16 13:32:05 +02:00
commit 12cba639b9
663 changed files with 168914 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
package models
import "gorm.io/gorm"
// AboutPage stores the "O klubu" (About Club) page content
type AboutPage struct {
gorm.Model
Title string `json:"title"` // Page title
Subtitle string `json:"subtitle"` // Optional subtitle
Style string `json:"style" gorm:"default:'default'"` // Style: default, modern, timeline, custom
Content string `json:"content" gorm:"type:text"` // HTML content
HeroImage string `json:"hero_image"` // Hero/banner image URL
Sections string `json:"sections" gorm:"type:text"` // JSON array of sections for structured styles
Published bool `json:"published" gorm:"default:false"` // Whether page is published
SEOTitle string `json:"seo_title"` // SEO meta title
SEODesc string `json:"seo_description" gorm:"type:text"` // SEO meta description
}
func (AboutPage) TableName() string { return "about_pages" }
+15
View File
@@ -0,0 +1,15 @@
package models
import (
"time"
"gorm.io/gorm"
)
// BaseModel contains common fields for all models
type BaseModel struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
+15
View File
@@ -0,0 +1,15 @@
package models
import "gorm.io/gorm"
// Category represents a category for articles
type Category struct {
gorm.Model
Name string `gorm:"uniqueIndex;not null" json:"name"`
Description string `json:"description"`
Slug string `gorm:"uniqueIndex" json:"slug"`
}
func (Category) TableName() string {
return "categories"
}
+21
View File
@@ -0,0 +1,21 @@
package models
import "gorm.io/gorm"
// Clothing represents a merchandising item
type Clothing struct {
gorm.Model
Title string `gorm:"not null" json:"title"`
Description string `gorm:"type:text" json:"description"`
Price float64 `json:"price"`
Currency string `gorm:"default:'Kč'" json:"currency"`
ImageURL string `json:"image_url"`
URL string `json:"url"`
IsActive bool `gorm:"default:true" json:"is_active"`
DisplayOrder int `gorm:"default:0" json:"display_order"`
}
// TableName specifies the table name for the Clothing model
func (Clothing) TableName() string {
return "clothing"
}
+10
View File
@@ -0,0 +1,10 @@
package models
// ClubSearchResult represents a club search result from FACR API
type ClubSearchResult struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
LogoURL string `json:"logo_url"`
Category string `json:"category"`
}
+16
View File
@@ -0,0 +1,16 @@
package models
import "gorm.io/gorm"
// CompetitionAlias allows renaming competitions site-wide by FACR code
// Example: code "A1A" -> alias "Krajský přebor"
// The API continues to return original codes; frontend/admin can map to aliases.
type CompetitionAlias struct {
gorm.Model
Code string `gorm:"uniqueIndex;not null" json:"code"` // FACR competition code, e.g. A1A
Alias string `gorm:"not null" json:"alias"` // Display name used in UI
OriginalName string `json:"original_name"` // Optional: last seen original name
DisplayOrder int `json:"display_order"` // Custom sort order (lower = higher priority)
}
func (CompetitionAlias) TableName() string { return "competition_aliases" }
+115
View File
@@ -0,0 +1,115 @@
package models
import (
"time"
"gorm.io/datatypes"
"gorm.io/gorm"
)
// ContactMessage represents a message sent through the contact form
type ContactMessage struct {
BaseModel
Name string `json:"name" gorm:"not null"`
Email string `json:"email" gorm:"not null"`
Subject string `json:"subject" gorm:"not null"`
Message string `json:"message" gorm:"type:text;not null"`
Source string `json:"source" gorm:"size:50;default:contact"` // e.g., "contact", "sponsor"
IPAddress string `json:"ip_address" gorm:"size:45"`
UserAgent string `json:"user_agent" gorm:"type:text"`
IsRead bool `json:"is_read" gorm:"default:false"`
ReadAt time.Time `json:"read_at,omitempty"`
}
// TableName specifies the table name for the ContactMessage model
func (ContactMessage) TableName() string {
return "contact_messages"
}
// NewsletterSubscription represents a user subscription to the newsletter
type NewsletterSubscription struct {
BaseModel
Email string `json:"email" gorm:"uniqueIndex;not null"`
IsActive bool `json:"is_active" gorm:"default:true"`
// Preferences stores subscriber choices (e.g. matches, scores, events, blog) as JSON
// Use datatypes.JSONMap so GORM/driver can marshal/unmarshal JSONB correctly
Preferences datatypes.JSONMap `json:"preferences" gorm:"type:jsonb;default:'{}'::jsonb"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName specifies the table name for the NewsletterSubscription model
func (NewsletterSubscription) TableName() string {
return "newsletter_subscriptions"
}
// CreateContactMessage creates a new contact message in the database
func CreateContactMessage(db *gorm.DB, message *ContactMessage) error {
return db.Create(message).Error
}
// GetContactMessages retrieves a paginated list of contact messages
func GetContactMessages(db *gorm.DB, page, limit int) ([]ContactMessage, int64, error) {
var messages []ContactMessage
var count int64
offset := (page - 1) * limit
// Get total count
if err := db.Model(&ContactMessage{}).Count(&count).Error; err != nil {
return nil, 0, err
}
// Get paginated results
err := db.Offset(offset).
Limit(limit).
Order("created_at DESC").
Find(&messages).Error
return messages, count, err
}
// MarkMessageAsRead marks a contact message as read
func MarkMessageAsRead(db *gorm.DB, id uint) error {
return db.Model(&ContactMessage{}).
Where("id = ?", id).
Updates(map[string]interface{}{
"is_read": true,
"read_at": time.Now(),
}).Error
}
// SubscribeToNewsletter subscribes an email to the newsletter
func SubscribeToNewsletter(db *gorm.DB, email string) error {
subscription := NewsletterSubscription{
Email: email,
IsActive: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return db.Where(NewsletterSubscription{Email: email}).
Attrs(subscription).
FirstOrCreate(&subscription).
Update("is_active", true).
Error
}
// UnsubscribeFromNewsletter unsubscribes an email from the newsletter
func UnsubscribeFromNewsletter(db *gorm.DB, email string) error {
return db.Model(&NewsletterSubscription{}).
Where("email = ?", email).
Update("is_active", false).
Error
}
// GetActiveSubscribers returns a list of all active newsletter subscribers
func GetActiveSubscribers(db *gorm.DB) ([]string, error) {
var subscribers []string
err := db.Model(&NewsletterSubscription{}).
Where("is_active = ?", true).
Pluck("email", &subscribers).
Error
return subscribers, err
}
+67
View File
@@ -0,0 +1,67 @@
package models
import (
"time"
"gorm.io/datatypes"
)
// EmailLog stores one row per attempted delivery to a recipient
type EmailLog struct {
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Subject string `json:"subject"`
RecipientEmail string `json:"recipient_email" gorm:"index"`
Type string `json:"type"` // newsletter|welcome|welcome_back|other
Status string `json:"status"` // sent|failed
ProviderMessageID string `json:"provider_message_id"`
SendError string `json:"send_error"`
Token string `json:"token" gorm:"index"`
}
// EmailEvent records interactions: open, click, spam, unsubscribe, bounce, complaint
type EmailEvent struct {
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `json:"created_at"`
EmailLogID uint `json:"email_log_id" gorm:"index"`
EventType string `json:"event_type"`
Meta datatypes.JSONMap `json:"meta"`
}
// NewsletterSentLog tracks sent newsletters to prevent duplicates
type NewsletterSentLog struct {
ID uint `gorm:"primaryKey" json:"id"`
NewsletterType string `json:"newsletter_type" gorm:"index"` // weekly|match_reminder|match_result|blog_release
Subject string `json:"subject"`
ContentIDs string `json:"content_ids" gorm:"type:text"` // JSON array of IDs
RecipientsCount int `json:"recipients_count"`
SentAt time.Time `json:"sent_at" gorm:"index"`
CreatedAt time.Time `json:"created_at"`
}
func (NewsletterSentLog) TableName() string { return "newsletter_sent_log" }
// MatchNotification tracks match alerts sent to avoid duplicates
type MatchNotification struct {
ID uint `gorm:"primaryKey" json:"id"`
MatchID string `json:"match_id" gorm:"index"` // External FACR match ID
NotificationType string `json:"notification_type"` // reminder_48h|reminder_day|result
SentAt time.Time `json:"sent_at" gorm:"index"`
RecipientsCount int `json:"recipients_count"`
CreatedAt time.Time `json:"created_at"`
}
func (MatchNotification) TableName() string { return "match_notifications" }
// BlogNotification tracks blog release notifications
type BlogNotification struct {
ID uint `gorm:"primaryKey" json:"id"`
ArticleID uint `json:"article_id" gorm:"uniqueIndex"`
SentAt time.Time `json:"sent_at" gorm:"index"`
RecipientsCount int `json:"recipients_count"`
CreatedAt time.Time `json:"created_at"`
}
func (BlogNotification) TableName() string { return "blog_notifications" }
+43
View File
@@ -0,0 +1,43 @@
package models
import (
"time"
)
type EventType string
const (
EventTypeMatch EventType = "match"
EventTypeTraining EventType = "training"
EventTypeMeeting EventType = "meeting"
EventTypeOther EventType = "other"
)
type Event struct {
BaseModel
Title string `json:"title" gorm:"not null"`
Description string `json:"description"`
StartTime time.Time `json:"start_time" gorm:"not null"`
EndTime *time.Time `json:"end_time"`
Location string `json:"location"`
Type EventType `json:"type" gorm:"type:varchar(20);not null;default:'other'"`
CategoryName string `json:"category_name" gorm:"type:varchar(255)"`
IsPublic bool `json:"is_public" gorm:"default:true"`
CreatedByID uint `json:"created_by_id"`
CreatedBy User `json:"created_by" gorm:"foreignKey:CreatedByID"`
ImageURL string `json:"image_url"`
FileURL string `json:"file_url"`
Attachments []EventAttachment `json:"attachments" gorm:"constraint:OnDelete:CASCADE"`
YoutubeURL string `json:"youtube_url" gorm:"type:varchar(500)"`
Latitude *float64 `json:"latitude"`
Longitude *float64 `json:"longitude"`
}
type EventAttachment struct {
BaseModel
EventID uint `json:"event_id" gorm:"index;not null"`
Name string `json:"name"`
URL string `json:"url"`
MimeType string `json:"mime_type"`
Size int64 `json:"size"`
}
+35
View File
@@ -0,0 +1,35 @@
package models
import (
"time"
"gorm.io/gorm"
)
// MatchOverride stores admin overrides for externally-fetched matches (FACR)
// It uses ExternalMatchID (e.g., FACR match_id) as stable key and allows overriding
// names, venue, datetime and logos.
type MatchOverride struct {
gorm.Model
ExternalMatchID string `gorm:"uniqueIndex;not null" json:"external_match_id"`
HomeNameOverride *string `json:"home_name_override"`
AwayNameOverride *string `json:"away_name_override"`
VenueOverride *string `json:"venue_override"`
DateTimeOverride *time.Time `json:"date_time_override"`
HomeLogoURL *string `json:"home_logo_url"`
AwayLogoURL *string `json:"away_logo_url"`
Notes string `gorm:"type:text" json:"notes"`
}
func (MatchOverride) TableName() string { return "match_overrides" }
// TeamLogoOverride persists custom team logos to override remote ones
// Keyed by ExternalTeamID (e.g., FACR team id)
type TeamLogoOverride struct {
gorm.Model
ExternalTeamID string `gorm:"uniqueIndex;not null" json:"external_team_id"`
TeamName string `json:"team_name"`
LogoURL string `json:"logo_url"`
}
func (TeamLogoOverride) TableName() string { return "team_logo_overrides" }
+367
View File
@@ -0,0 +1,367 @@
package models
import (
"encoding/json"
"time"
"gorm.io/gorm"
)
// User represents a user in the system
type User struct {
gorm.Model
Email string `gorm:"uniqueIndex;not null" json:"email"`
Password string `gorm:"not null" json:"-"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Role string `gorm:"default:editor" json:"role"` // admin, editor
IsActive bool `gorm:"default:true"`
LastLogin *time.Time `json:"last_login,omitempty"`
}
// Article represents a blog article
type Article struct {
gorm.Model
Title string `gorm:"not null" json:"title"`
Content string `gorm:"type:text;not null" json:"content"`
AuthorID *uint `gorm:"index" json:"author_id,omitempty"`
Author *User `gorm:"foreignKey:AuthorID" json:"author,omitempty"`
CategoryID *uint `gorm:"index" json:"category_id,omitempty"`
Category *Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
ImageURL string `json:"image_url"`
Published bool `gorm:"default:false" json:"published"`
PublishedAt *time.Time `json:"published_at,omitempty"`
Slug string `gorm:"uniqueIndex" json:"slug"`
Excerpt string `gorm:"type:text" json:"excerpt"`
Featured bool `gorm:"default:false;index" json:"featured"`
// Fields for SEO and social previews
SEOTitle string `json:"seo_title"`
SEODescription string `gorm:"type:text" json:"seo_description"`
// OG image for social sharing (optional)
OGImageURL string `json:"og_image_url"`
// Optional: link to external content or embedded media
ExternalLink string `json:"external_link"`
ViewCount int `gorm:"default:0;index" json:"view_count"`
ReadTime int `gorm:"default:0" json:"read_time"` // estimated reading time in minutes
UniqueViews int `gorm:"default:0" json:"unique_views"` // Unique visitors (tracked by IP/session)
// Store the category name directly to simplify queries (denormalized)
CategoryName string `json:"category_name"`
Attachments string `gorm:"type:text" json:"attachments"` // JSON array: ["url1", "url2", ...]
// Gallery association (optional)
GalleryAlbumID string `json:"gallery_album_id"`
GalleryAlbumURL string `json:"gallery_album_url"`
// Stored as JSON string or comma-separated list; frontend normalizes
GalleryPhotoIDs string `gorm:"type:text" json:"gallery_photo_ids"`
// YouTube video association (optional)
YouTubeVideoID string `json:"youtube_video_id"`
YouTubeVideoTitle string `gorm:"type:text" json:"youtube_video_title"`
YouTubeVideoURL string `json:"youtube_video_url"`
YouTubeVideoThumbnail string `json:"youtube_video_thumbnail"`
}
// ArticleTeamLink represents a link from an article to a team identified by an external FACR ID
type ArticleTeamLink struct {
gorm.Model
ArticleID uint `gorm:"not null;index" json:"article_id"`
Article Article `gorm:"foreignKey:ArticleID" json:"-"`
ExternalTeamID string `gorm:"not null;index" json:"external_team_id"`
TeamName string `json:"team_name"`
}
func (ArticleTeamLink) TableName() string { return "article_team_links" }
// ArticleMatchLink represents a link from an article to a match identified by an external FACR match ID
type ArticleMatchLink struct {
gorm.Model
ArticleID uint `gorm:"not null;index" json:"article_id"`
Article Article `gorm:"foreignKey:ArticleID" json:"-"`
ExternalMatchID string `gorm:"not null;index" json:"external_match_id"`
Title string `json:"title"`
}
func (ArticleMatchLink) TableName() string { return "article_match_links" }
// Team represents a football team
type Team struct {
gorm.Model
Name string `gorm:"not null"`
ShortName string
Description string
LogoURL string `json:"logo_url"`
IsActive bool `gorm:"default:true"`
}
// Player represents a football player
type Player struct {
gorm.Model
FirstName string `gorm:"not null" json:"first_name"`
LastName string `gorm:"not null" json:"last_name"`
DateOfBirth time.Time `json:"date_of_birth"`
Position string `json:"position"`
JerseyNumber int `json:"jersey_number"`
TeamID uint `json:"team_id"`
Team Team `gorm:"foreignKey:TeamID" json:"team"`
Nationality string `json:"nationality"`
Height int `json:"height"` // in cm
Weight int `json:"weight"` // in kg
ImageURL string `json:"image_url"`
IsActive bool `gorm:"default:true" json:"is_active"`
Email string `json:"email"`
Phone string `json:"phone"`
}
// Sponsor represents a sponsor
type Sponsor struct {
gorm.Model
Name string `gorm:"not null" json:"name"`
LogoURL string `json:"logo_url"`
WebsiteURL string `json:"website_url"`
Description string `json:"description"`
IsActive bool `gorm:"default:true" json:"is_active"`
Tier string `gorm:"default:'standard'" json:"tier"` // general (hlavní), standard
DisplayOrder int `gorm:"default:0" json:"display_order"` // For custom ordering
// Banner-specific metadata (optional)
Placement string `json:"placement"` // e.g., homepage_top, homepage_sidebar
Width int `json:"width"`
Height int `json:"height"`
}
type Settings struct {
gorm.Model
// Frontpage layout and style variants (e.g., "classic", "grid"; "light", "dark")
FrontpageLayout string `json:"frontpage_layout"`
FrontpageStyle string `json:"frontpage_style"`
// Sponsors module display preferences
SponsorsLayout string `json:"sponsors_layout"` // grid | slider | scroller | pyramid
SponsorsTheme string `json:"sponsors_theme"` // light | dark
// FACR club selection
ClubID string `json:"club_id"` // UUID from fotbal.cz
ClubType string `json:"club_type"` // football|futsal
ClubName string `json:"club_name"`
ClubLogoURL string `json:"club_logo_url"`
ClubURL string `json:"club_url"`
// Theme customization: colors & fonts
PrimaryColor string `json:"primary_color"` // e.g. #0033aa
SecondaryColor string `json:"secondary_color"`
AccentColor string `json:"accent_color"`
BackgroundColor string `json:"background_color"`
TextColor string `json:"text_color"`
FontHeading string `json:"font_heading"` // e.g. "Poppins, sans-serif"
FontBody string `json:"font_body"`
// Custom assets: raw overrides stored as TEXT
CustomCSS string `gorm:"type:text" json:"custom_css"`
CustomJS string `gorm:"type:text" json:"custom_js"`
CustomHTMLHome string `gorm:"type:text" json:"custom_html_home"`
CustomHTMLBlogList string `gorm:"type:text" json:"custom_html_blog_list"`
CustomHTMLBlogPost string `gorm:"type:text" json:"custom_html_blog_post"`
// Custom pages & navigation
AboutHTML string `gorm:"type:text" json:"about_html"`
ShowAboutInNav bool `gorm:"default:true" json:"show_about_in_nav"`
CustomNavJSON string `gorm:"type:text" json:"-"`
CustomNav []CustomNavLink `gorm:"-" json:"custom_nav,omitempty"`
// SMTP configuration (optional, overrides environment when present)
SMTPHost string `json:"smtp_host"`
SMTPPort int `json:"smtp_port"`
SMTPUser string `json:"smtp_user"`
SMTPPassword string `json:"smtp_password"`
SMTPFrom string `json:"smtp_from"`
SMTPFromName string `json:"smtp_from_name"`
SMTPEncryption string `json:"smtp_encryption"` // tls|ssl|none
SMTPAuth bool `json:"smtp_auth"`
SMTPSkipVerify bool `json:"smtp_skip_verify"`
// SEO defaults (site-wide)
SiteTitle string `json:"site_title"`
SiteDescription string `json:"site_description"`
MetaKeywords string `json:"meta_keywords"` // comma-separated
DefaultOGImageURL string `json:"default_og_image_url"`
TwitterHandle string `json:"twitter_handle"` // e.g. @club
CanonicalBaseURL string `json:"canonical_base_url"` // e.g. https://www.club.cz
AdditionalMeta string `gorm:"type:text" json:"additional_meta"` // raw extra meta
EnableIndexing bool `json:"enable_indexing"` // robots allow/disallow
// Social profiles
FacebookURL string `json:"facebook_url"`
InstagramURL string `json:"instagram_url"`
YoutubeURL string `json:"youtube_url"`
// Generic gallery link (preferred over legacy specific providers)
GalleryURL string `json:"gallery_url"`
GalleryLabel string `json:"gallery_label"`
// Videos module configuration
VideosModuleEnabled bool `json:"videos_module_enabled"`
VideosStyle string `json:"videos_style"` // slider | grid3 | grid
VideosSource string `json:"videos_source"` // auto | manual
VideosLimit int `json:"videos_limit"` // number of items on homepage
// Manual videos storage (JSON strings)
VideosJSON string `gorm:"type:text" json:"-"`
VideosItemsJSON string `gorm:"type:text" json:"-"`
// Merch module configuration
MerchModuleEnabled bool `json:"merch_module_enabled"`
MerchStyle string `json:"merch_style"` // grid | slider (future)
MerchSource string `json:"merch_source"` // manual | auto (future)
MerchLimit int `json:"merch_limit"`
// Manual merch storage
MerchItemsJSON string `gorm:"type:text" json:"-"`
// Newsletter automation toggle (persisted)
NewsletterEnabled bool `json:"newsletter_enabled"`
// Newsletter defaults
DefaultDigestType string `json:"default_digest_type"` // blogs|events|matches|scores|weekly
DefaultDigestCompetitions string `json:"default_digest_competitions"` // comma-separated codes
// Newsletter scheduling (admin-configurable)
// Enable/disable specific automated digests
EnableWeekly bool `json:"enable_weekly"`
EnableMatchReminders bool `json:"enable_match_reminders"`
EnableResults bool `json:"enable_results"`
// Weekly schedule
NewsletterWeeklyDay string `json:"newsletter_weekly_day"` // sun|mon|tue|wed|thu|fri|sat
NewsletterWeeklyHour int `json:"newsletter_weekly_hour"` // 0-23 local time
// Match reminder lead time (hours before kickoff)
NewsletterReminderLeadHours int `json:"newsletter_reminder_lead_hours"` // default 48
// Quiet hours for results (local time, inclusive start, exclusive end)
NewsletterQuietStart int `json:"newsletter_quiet_start"` // 0-23
NewsletterQuietEnd int `json:"newsletter_quiet_end"` // 0-23
// Contact/Location information for map
ContactAddress string `json:"contact_address"`
ContactCity string `json:"contact_city"`
ContactZip string `json:"contact_zip"`
ContactCountry string `json:"contact_country"`
ContactPhone string `json:"contact_phone"`
ContactEmail string `json:"contact_email"`
LocationLatitude float64 `json:"location_latitude"`
LocationLongitude float64 `json:"location_longitude"`
MapZoomLevel int `gorm:"default:15" json:"map_zoom_level"`
MapStyle string `json:"map_style"` // OpenStreetMap style URL or preset: default, dark, satellite
ShowMapOnHomepage bool `json:"show_map_on_homepage"`
// Homepage matches display configuration
FinishedMatchDisplayDays int `gorm:"default:2" json:"finished_match_display_days"` // Number of days to show finished matches with scores on homepage
}
// TableName specifies table name for Settings model
func (Settings) TableName() string { return "settings" }
// CustomNavLink represents an item in the main navigation managed via settings
type CustomNavLink struct {
Label string `json:"label"`
URL string `json:"url"`
External bool `json:"external"`
}
// LoadCustomNav hydrates the in-memory CustomNav slice from the persisted JSON string.
func (s *Settings) LoadCustomNav() {
if s.CustomNavJSON == "" {
s.CustomNav = nil
return
}
var items []CustomNavLink
if err := json.Unmarshal([]byte(s.CustomNavJSON), &items); err != nil {
s.CustomNav = nil
return
}
s.CustomNav = items
}
// SetCustomNav stores the provided navigation links and updates the serialized JSON column.
func (s *Settings) SetCustomNav(items []CustomNavLink) error {
s.CustomNav = items
if len(items) == 0 {
s.CustomNavJSON = ""
return nil
}
b, err := json.Marshal(items)
if err != nil {
return err
}
s.CustomNavJSON = string(b)
return nil
}
// Club represents the main club/team configuration
type Club struct {
gorm.Model
Name string `gorm:"not null" json:"name"`
PrimaryColor string `gorm:"default:'#1a365d'" json:"primary_color"`
SportType string `gorm:"not null" json:"sport_type"`
LogoPath string `json:"logo_path"`
}
// TableName specifies the table name for the User model
func (User) TableName() string {
return "users"
}
// TableName specifies the table name for the Article model
func (Article) TableName() string {
return "articles"
}
// TableName specifies the table name for the Team model
func (Team) TableName() string {
return "teams"
}
// TableName specifies the table name for the Player model
func (Player) TableName() string {
return "players"
}
// TableName specifies the table name for the Sponsor model
func (Sponsor) TableName() string {
return "sponsors"
}
// TableName specifies the table name for the Club model
func (Club) TableName() string {
return "clubs"
}
// ContactCategory represents a category for organizing contacts (e.g., "Management", "Coaches", "Office")
type ContactCategory struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
Name string `gorm:"not null;uniqueIndex" json:"name"`
Description string `json:"description"`
DisplayOrder int `gorm:"default:0" json:"display_order"`
IsActive bool `gorm:"default:true" json:"is_active"`
}
// TableName specifies the table name for the ContactCategory model
func (ContactCategory) TableName() string {
return "contact_categories"
}
// Contact represents a contact person (e.g., coach, manager, office staff)
type Contact struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
CategoryID *uint `gorm:"index" json:"category_id,omitempty"`
Category *ContactCategory `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
Name string `gorm:"not null" json:"name"`
Position string `json:"position"` // e.g., "Head Coach", "President"
Email string `json:"email"`
Phone string `json:"phone"`
ImageURL string `json:"image_url"`
Description string `gorm:"type:text" json:"description"`
DisplayOrder int `gorm:"default:0" json:"display_order"`
IsActive bool `gorm:"default:true" json:"is_active"`
}
// TableName specifies the table name for the Contact model
func (Contact) TableName() string {
return "contacts"
}
+146
View File
@@ -0,0 +1,146 @@
package models
import (
"gorm.io/gorm"
)
// NavigationItemType defines the type of navigation item
type NavigationItemType string
const (
NavTypeInternal NavigationItemType = "internal" // Direct URL
NavTypeExternal NavigationItemType = "external" // External link
NavTypeDropdown NavigationItemType = "dropdown" // Has children
NavTypePage NavigationItemType = "page" // Links to predefined page
)
// NavigationItem represents a single navigation menu item
type NavigationItem struct {
gorm.Model
Label string `gorm:"not null" json:"label"`
URL string `json:"url,omitempty"`
Icon string `json:"icon,omitempty"`
Type NavigationItemType `gorm:"not null;default:'internal'" json:"type"`
PageType string `json:"page_type,omitempty"` // e.g., 'blog', 'about', 'calendar'
PageID *uint `json:"page_id,omitempty"` // optional reference to specific content
Visible bool `gorm:"not null;default:true" json:"visible"`
DisplayOrder int `gorm:"not null;default:0" json:"display_order"`
ParentID *uint `json:"parent_id,omitempty"`
Parent *NavigationItem `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
Children []NavigationItem `gorm:"foreignKey:ParentID" json:"children,omitempty"`
Target string `gorm:"default:'_self'" json:"target"` // _self or _blank
CSSClass string `json:"css_class,omitempty"`
RequiresAuth bool `gorm:"default:false" json:"requires_auth"`
RequiresAdmin bool `gorm:"default:false" json:"requires_admin"`
}
// TableName specifies the table name for the NavigationItem model
func (NavigationItem) TableName() string {
return "navigation_items"
}
// GetURL returns the computed URL based on type and page_type
func (n *NavigationItem) GetURL() string {
if n.URL != "" {
return n.URL
}
// Map page types to URLs for frontend
if n.Type == NavTypePage && n.PageType != "" {
pageURLMap := map[string]string{
"home": "/",
"about": "/o-klubu",
"calendar": "/kalendar",
"matches": "/zapasy",
"activities": "/aktivity",
"players": "/hraci",
"tables": "/tabulky",
"blog": "/blog",
"videos": "/videa",
"gallery": "/galerie",
"sponsors": "/sponzori",
"contact": "/kontakt",
"search": "/hledat",
}
if url, ok := pageURLMap[n.PageType]; ok {
return url
}
}
// Map admin page types to URLs
if (n.Type == NavTypeInternal || n.Type == NavTypePage) && n.PageType != "" && n.RequiresAdmin {
adminURLMap := map[string]string{
"dashboard": "/admin",
"analytics": "/admin/analytika",
"teams": "/admin/tymy",
"matches": "/admin/zapasy",
"activities": "/admin/aktivity",
"players": "/admin/hraci",
"articles": "/admin/clanky",
"categories": "/admin/kategorie",
"about": "/admin/o-klubu",
"videos": "/admin/videa",
"gallery": "/admin/galerie",
"scoreboard": "/admin/scoreboard",
"scoreboard_remote": "/admin/scoreboard/remote",
"clothing": "/admin/obleceni",
"sponsors": "/admin/sponzori",
"banners": "/admin/bannery",
"messages": "/admin/zpravy",
"contacts": "/admin/kontakty",
"newsletter": "/admin/newsletter",
"polls": "/admin/ankety",
"navigation": "/admin/navigace",
"competition_aliases": "/admin/aliasy-soutezi",
"prefetch": "/admin/prefetch",
"users": "/admin/uzivatele",
"settings": "/admin/nastaveni",
"files": "/admin/soubory",
"docs": "/admin/docs",
}
if url, ok := adminURLMap[n.PageType]; ok {
return url
}
}
return "#"
}
// SocialLink represents a social media link
type SocialLink struct {
gorm.Model
Platform string `gorm:"not null" json:"platform"` // facebook, instagram, youtube, etc.
URL string `gorm:"not null" json:"url"`
DisplayOrder int `gorm:"not null;default:0" json:"display_order"`
Visible bool `gorm:"not null;default:true" json:"visible"`
Icon string `json:"icon,omitempty"` // optional custom icon
}
// TableName specifies the table name for the SocialLink model
func (SocialLink) TableName() string {
return "social_links"
}
// GetIconName returns the React Icons name for the platform
func (s *SocialLink) GetIconName() string {
if s.Icon != "" {
return s.Icon
}
iconMap := map[string]string{
"facebook": "FaFacebook",
"instagram": "FaInstagram",
"youtube": "FaYoutube",
"twitter": "FaTwitter",
"tiktok": "FaTiktok",
"linkedin": "FaLinkedin",
"discord": "FaDiscord",
"twitch": "FaTwitch",
}
if icon, ok := iconMap[s.Platform]; ok {
return icon
}
return "FaLink"
}
+51
View File
@@ -0,0 +1,51 @@
package models
import (
"database/sql/driver"
"encoding/json"
"errors"
"gorm.io/gorm"
)
// PageElementConfig represents the visual variant configuration for a page element
type PageElementConfig struct {
gorm.Model
PageType string `gorm:"not null;index" json:"page_type"` // e.g., "homepage", "about", "blog"
ElementName string `gorm:"not null;index" json:"element_name"` // e.g., "header", "hero", "news", "matches"
Variant string `gorm:"not null" json:"variant"` // e.g., "unified", "edge", "minimal", "modern"
Visible bool `gorm:"default:true" json:"visible"` // Whether element is shown on page
DisplayOrder int `gorm:"default:0" json:"display_order"` // Order of element on page
Settings ElementSettings `gorm:"type:jsonb" json:"settings,omitempty"` // Additional variant-specific settings
}
// ElementSettings is a map for storing additional settings
type ElementSettings map[string]interface{}
// TableName specifies the table name for the PageElementConfig model
func (PageElementConfig) TableName() string {
return "page_element_configs"
}
// Scan implements the sql.Scanner interface for ElementSettings
func (es *ElementSettings) Scan(value interface{}) error {
if value == nil {
*es = make(ElementSettings)
return nil
}
bytes, ok := value.([]byte)
if !ok {
return errors.New("type assertion to []byte failed")
}
return json.Unmarshal(bytes, es)
}
// Value implements the driver.Valuer interface for ElementSettings
func (es ElementSettings) Value() (driver.Value, error) {
if es == nil {
return nil, nil
}
return json.Marshal(es)
}
+24
View File
@@ -0,0 +1,24 @@
package models
import (
"time"
"gorm.io/gorm"
)
// PasswordReset stores password reset tokens, verification codes, and their status
type PasswordReset struct {
gorm.Model
UserID uint `gorm:"index;not null" json:"user_id"`
User User `gorm:"foreignKey:UserID" json:"-"`
Token string `gorm:"uniqueIndex;size:128;not null" json:"token"`
ExpiresAt time.Time `gorm:"index;not null" json:"expires_at"`
UsedAt *time.Time `json:"used_at"`
IP string `json:"ip"`
UserAgent string `json:"user_agent"`
VerificationCode string `gorm:"size:6" json:"-"`
VerificationCodeExpires *time.Time `json:"verification_code_expires_at"`
VerificationAttempts int `gorm:"default:0" json:"verification_attempts"`
}
func (PasswordReset) TableName() string { return "password_resets" }
+121
View File
@@ -0,0 +1,121 @@
package models
import (
"time"
)
// Poll represents a voting poll
type Poll struct {
ID uint `gorm:"primarykey" json:"id"`
Title string `gorm:"size:255;not null" json:"title"`
Description string `gorm:"type:text" json:"description"`
Type string `gorm:"size:50;not null;default:'single'" json:"type"` // single, multiple, rating
Status string `gorm:"size:20;not null;default:'draft'" json:"status"` // draft, active, closed, archived
StartDate *time.Time `json:"start_date"`
EndDate *time.Time `json:"end_date"`
AllowMultiple bool `gorm:"default:false" json:"allow_multiple"` // Allow voting for multiple options
MaxChoices int `gorm:"default:1" json:"max_choices"` // Max number of choices if allow_multiple
ShowResults string `gorm:"size:20;default:'after_vote'" json:"show_results"` // always, after_vote, after_end, never
RequireAuth bool `gorm:"default:false" json:"require_auth"` // Require login to vote
AllowGuestVote bool `gorm:"default:true" json:"allow_guest_vote"` // Allow anonymous voting
Featured bool `gorm:"default:false" json:"featured"` // Show on homepage
CategoryID *uint `json:"category_id"` // Optional category
Category *Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
RelatedMatchID *uint `json:"related_match_id"` // Link to specific match (for MOTM voting)
RelatedArticleID *uint `json:"related_article_id"` // Link to blog article
RelatedArticle *Article `gorm:"foreignKey:RelatedArticleID" json:"related_article,omitempty"`
RelatedEventID *uint `json:"related_event_id"` // Link to event/activity
RelatedEvent *Event `gorm:"foreignKey:RelatedEventID" json:"related_event,omitempty"`
RelatedVideoURL string `gorm:"size:500" json:"related_video_url"` // YouTube video URL/ID
ImageURL string `gorm:"size:500" json:"image_url"`
TotalVotes int `gorm:"default:0" json:"total_votes"`
Options []PollOption `gorm:"foreignKey:PollID;constraint:OnDelete:CASCADE" json:"options"`
Votes []PollVote `gorm:"foreignKey:PollID;constraint:OnDelete:CASCADE" json:"votes,omitempty"`
CreatedBy uint `json:"created_by"`
Creator *User `gorm:"foreignKey:CreatedBy" json:"creator,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"`
}
// PollOption represents a single option in a poll
type PollOption struct {
ID uint `gorm:"primarykey" json:"id"`
PollID uint `gorm:"not null;index" json:"poll_id"`
Poll *Poll `gorm:"foreignKey:PollID" json:"poll,omitempty"`
Text string `gorm:"size:255;not null" json:"text"`
Description string `gorm:"type:text" json:"description"`
ImageURL string `gorm:"size:500" json:"image_url"`
DisplayOrder int `gorm:"default:0" json:"display_order"`
VoteCount int `gorm:"default:0" json:"vote_count"`
PlayerID *uint `json:"player_id"` // Optional link to player (for MOTM)
Player *Player `gorm:"foreignKey:PlayerID" json:"player,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// PollVote represents a user's vote
type PollVote struct {
ID uint `gorm:"primarykey" json:"id"`
PollID uint `gorm:"not null;index" json:"poll_id"`
Poll *Poll `gorm:"foreignKey:PollID" json:"poll,omitempty"`
OptionID uint `gorm:"not null;index" json:"option_id"`
Option *PollOption `gorm:"foreignKey:OptionID" json:"option,omitempty"`
UserID *uint `gorm:"index" json:"user_id"` // Null for guest votes
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
IPHash string `gorm:"size:64;index" json:"ip_hash"` // Hashed IP for duplicate prevention
UserAgent string `gorm:"size:500" json:"user_agent"`
SessionToken string `gorm:"size:100;index" json:"session_token"` // For guest vote tracking
CreatedAt time.Time `json:"created_at"`
}
// TableName overrides the table name
func (Poll) TableName() string {
return "polls"
}
func (PollOption) TableName() string {
return "poll_options"
}
func (PollVote) TableName() string {
return "poll_votes"
}
// IsActive checks if poll is currently accepting votes
func (p *Poll) IsActive() bool {
if p.Status != "active" {
return false
}
now := time.Now()
if p.StartDate != nil && now.Before(*p.StartDate) {
return false
}
if p.EndDate != nil && now.After(*p.EndDate) {
return false
}
return true
}
// CanShowResults checks if results should be displayed
func (p *Poll) CanShowResults(hasVoted bool) bool {
switch p.ShowResults {
case "always":
return true
case "after_vote":
return hasVoted
case "after_end":
if p.EndDate != nil {
return time.Now().After(*p.EndDate)
}
return p.Status == "closed" || p.Status == "archived"
case "never":
return false
default:
return false
}
}
+38
View File
@@ -0,0 +1,38 @@
package models
import "gorm.io/gorm"
// ScoreboardState is a singleton table to persist scoreboard settings
// Only one row is used (ID=1)
type ScoreboardState struct {
gorm.Model
HomeName string `json:"home_name"`
AwayName string `json:"away_name"`
HomeLogoURL string `json:"home_logo_url"`
AwayLogoURL string `json:"away_logo_url"`
HomeShort string `json:"home_short"`
AwayShort string `json:"away_short"`
PrimaryColor string `json:"primary_color"`
SecondaryColor string `json:"secondary_color"`
HomeScore int `json:"home_score"`
AwayScore int `json:"away_score"`
HalfLength int `json:"half_length"`
Theme string `json:"theme"`
ExternalMatchID string `json:"external_match_id"`
Active bool `json:"active"`
// Timer fields
Timer string `json:"timer"`
Running bool `json:"running"`
TimerStartUnix int64 `json:"timer_start_unix" gorm:"column:timer_start_unix"`
ElapsedSeconds int `json:"elapsed_seconds" gorm:"column:elapsed_seconds"`
// Extended fields (ported from MyClub ScoreBoard)
// Visual sides flipped (UI-only flag, does not swap data)
SidesFlipped bool `json:"sides_flipped"`
// Current half: 1 or 2
Half int `json:"half"`
// QR overlay schedule settings
QRShowEveryMinutes int `json:"qr_show_every_minutes"`
QRShowDurationSeconds int `json:"qr_show_duration_seconds"`
}
func (ScoreboardState) TableName() string { return "scoreboard_states" }
+61
View File
@@ -0,0 +1,61 @@
package models
import (
"encoding/json"
"time"
"gorm.io/gorm"
)
type SetupStatus string
const (
SetupStatusPending SetupStatus = "pending"
SetupStatusCompleted SetupStatus = "completed"
SetupStatusSkipped SetupStatus = "skipped"
)
type SetupInfo struct {
gorm.Model
Status SetupStatus `gorm:"not null;default:'pending'" json:"status"`
SkippedAt *time.Time `json:"skipped_at,omitempty"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
SMTPConfigured bool `gorm:"default:false" json:"smtp_configured"`
ClubImported bool `gorm:"default:false" json:"club_imported"`
}
type ClubInfo struct {
gorm.Model
SetupInfoID uint `gorm:"not null" json:"-"`
FACRClubID string `gorm:"uniqueIndex" json:"facr_club_id"`
Name string `gorm:"not null" json:"name"`
ShortName string `json:"short_name"`
LogoURL string `json:"logo_url"`
PrimaryColor string `json:"primary_color"`
SecondaryColor string `json:"secondary_color"`
TextColor string `json:"text_color"`
}
// TableName specifies the table name for the SetupInfo model
func (SetupInfo) TableName() string {
return "setup_info"
}
// TableName specifies the table name for the ClubInfo model
func (ClubInfo) TableName() string {
return "club_info"
}
// ToJSON returns the JSON representation of the club info
func (c *ClubInfo) ToJSON() (string, error) {
data, err := json.Marshal(c)
if err != nil {
return "", err
}
return string(data), nil
}
// FromJSON populates the club info from JSON data
func (c *ClubInfo) FromJSON(jsonData string) error {
return json.Unmarshal([]byte(jsonData), c)
}
+38
View File
@@ -0,0 +1,38 @@
package models
import (
"gorm.io/gorm"
)
// UploadedFile represents a file uploaded to the server
type UploadedFile struct {
gorm.Model
Filename string `gorm:"not null" json:"filename"`
FilePath string `gorm:"uniqueIndex;not null" json:"file_path"`
FileURL string `gorm:"not null" json:"file_url"`
FileSize int64 `gorm:"default:0" json:"file_size"`
MimeType string `json:"mime_type"`
UploadedByID *uint `gorm:"index" json:"uploaded_by_id,omitempty"`
UploadedBy *User `gorm:"foreignKey:UploadedByID" json:"uploaded_by,omitempty"`
Usages []FileUsage `gorm:"foreignKey:FileID" json:"usages,omitempty"`
}
// FileUsage tracks where a file is being used
type FileUsage struct {
gorm.Model
FileID uint `gorm:"not null;index" json:"file_id"`
File *UploadedFile `gorm:"foreignKey:FileID" json:"file,omitempty"`
EntityType string `gorm:"not null;index" json:"entity_type"` // article, player, sponsor, event, contact, settings
EntityID uint `gorm:"not null;index" json:"entity_id"`
FieldName string `json:"field_name"` // image_url, logo_url, attachments, etc.
}
// TableName specifies the table name for UploadedFile
func (UploadedFile) TableName() string {
return "uploaded_files"
}
// TableName specifies the table name for FileUsage
func (FileUsage) TableName() string {
return "file_usages"
}
+30
View File
@@ -0,0 +1,30 @@
package models
import (
"encoding/json"
)
// VisitorEvent stores client-side analytics events
// TableName: visitor_events
// Common events: "page_view", "click", "interaction"
// Fields kept generic to avoid rigid schema changes
// Metadata stores arbitrary event payload (e.g., element details)
type VisitorEvent struct {
BaseModel
EventType string `gorm:"index;size:50" json:"event_type"`
Page string `gorm:"index;size:512" json:"page"` // Short page path
PagePath string `gorm:"index;size:512" json:"page_path"` // Full page path (alias for Page)
PageName string `gorm:"size:512" json:"page_name"` // Human-readable page name
Element string `gorm:"size:256" json:"element"` // Element identifier (for clicks/interactions)
Referrer string `gorm:"size:512" json:"referrer"`
UserAgent string `gorm:"size:512" json:"user_agent"`
IPAddress string `gorm:"size:64" json:"ip_address"`
SessionID string `gorm:"size:128;index" json:"session_id"`
UserID *uint `gorm:"index" json:"user_id,omitempty"`
Data json.RawMessage `gorm:"type:jsonb" json:"data"` // Generic event data
Metadata json.RawMessage `gorm:"type:jsonb" json:"metadata"` // Legacy metadata field
}
func (VisitorEvent) TableName() string { return "visitor_events" }