package models import ( "encoding/json" "strings" "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:fan" json:"role"` // admin, editor, fan IsActive bool `gorm:"default:true"` LastLogin *time.Time `json:"last_login,omitempty"` } // LoadVideosOverrides hydrates the in-memory VideosOverrides slice from the persisted JSON column. func (s *Settings) LoadVideosOverrides() { if s.VideosOverridesJSON == "" { s.VideosOverrides = nil return } var items []VideoTitleOverride if err := json.Unmarshal([]byte(s.VideosOverridesJSON), &items); err != nil { s.VideosOverrides = nil return } s.VideosOverrides = items } // SetVideosOverrides stores provided overrides and updates serialized JSON column. func (s *Settings) SetVideosOverrides(items []VideoTitleOverride) error { s.VideosOverrides = items if len(items) == 0 { s.VideosOverridesJSON = "" return nil } b, err := json.Marshal(items) if err != nil { return err } s.VideosOverridesJSON = string(b) return nil } func (s *Settings) LoadVideosTitleOverrides() { if s.VideosOverrides == nil && s.VideosOverridesJSON != "" { s.LoadVideosOverrides() } m := map[string]string{} if len(s.VideosOverrides) > 0 { for _, it := range s.VideosOverrides { vid := strings.TrimSpace(it.VideoID) t := strings.TrimSpace(it.Title) if vid != "" && t != "" { m[vid] = t } } } s.VideosTitleOverrides = m } // 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"` // Match link (loaded separately, not stored in this table) // Removed omitempty to always include in JSON (even if null) MatchLink *ArticleMatchLink `gorm:"-" json:"match_link"` // Computed helpers (not persisted) CategorySlug string `gorm:"-" json:"category_slug,omitempty"` CompetitionAlias string `gorm:"-" json:"competition_alias,omitempty"` NormalizedCategory string `gorm:"-" json:"normalized_category,omitempty"` URL string `gorm:"-" json:"url,omitempty"` } // 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"` Gender string `json:"gender"` 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"` } // VideoTitleOverride represents a per-video title override (for auto YouTube source) type VideoTitleOverride struct { VideoID string `json:"video_id"` Title string `json:"title"` } // CustomNavLink represents a simple custom navigation link stored in settings.custom_nav type CustomNavLink struct { Label string `json:"label"` URL string `json:"url"` External bool `json:"external"` } 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 // Deployment base URLs (optional runtime hints) // FrontendBaseURL: e.g. https://club.example.com FrontendBaseURL string `json:"frontend_base_url"` // APIBaseURL: full API root, e.g. https://api.example.com/api/v1 or https://backend.example.com/api/v1 APIBaseURL string `json:"api_base_url"` // 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 // Transient decoded forms for admin/public JSON responses (not persisted) Videos []string `gorm:"-" json:"videos,omitempty"` VideosItems []struct { URL string `json:"url"` Title *string `json:"title,omitempty"` Length *string `json:"length,omitempty"` UploadedAt *string `json:"uploaded_at,omitempty"` ThumbnailURL *string `json:"thumbnail_url,omitempty"` } `gorm:"-" json:"videos_items,omitempty"` // Manual videos storage (JSON strings) VideosJSON string `gorm:"type:text" json:"-"` VideosItemsJSON string `gorm:"type:text" json:"-"` // Title overrides for auto-fetched videos (stored as JSON array of {video_id,title}) VideosOverridesJSON string `gorm:"type:text" json:"-"` VideosOverrides []VideoTitleOverride `gorm:"-" json:"videos_overrides,omitempty"` // Derived helper for API responses (map form used by frontend/admin): video_id -> title VideosTitleOverrides map[string]string `gorm:"-" json:"videos_title_overrides,omitempty"` // 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"` // Contact form auto-forwarding ContactForwardEnabled bool `json:"contact_forward_enabled"` ContactForwardList string `gorm:"type:text" json:"contact_forward_list"` // comma/space/semicolon-separated emails LocationLatitude float64 `json:"location_latitude"` LocationLongitude float64 `json:"location_longitude"` MapZoomLevel int `gorm:"default:15" json:"map_zoom_level"` MapStyle string `json:"map_style"` ShowMapOnHomepage bool `json:"show_map_on_homepage"` // Homepage matches display configuration FinishedMatchDisplayDays int `gorm:"default:2" json:"finished_match_display_days"` StorageQuotaMB int `json:"storage_quota_mb"` StorageWarnThreshold int `json:"storage_warn_threshold"` StorageCriticalThreshold int `json:"storage_critical_threshold"` // External error-review integration ErrorReviewIngestURL string `json:"error_review_ingest_url"` ErrorReviewIngestToken string `json:"error_review_ingest_token"` ErrorReviewAdminURL string `json:"error_review_admin_url"` ErrorReviewAdminToken string `json:"error_review_admin_token"` ErrorReviewUIURL string `json:"error_review_ui_url"` // E-shop payment configuration RevolutEnabled bool `json:"revolut_enabled"` } // TableName specifies table name for Settings model func (Settings) TableName() string { return "settings" } // 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" }