mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
upload
This commit is contained in:
@@ -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" }
|
||||
@@ -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:"-"`
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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" }
|
||||
@@ -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
|
||||
}
|
||||
@@ -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" }
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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" }
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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" }
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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" }
|
||||
Reference in New Issue
Block a user