This commit is contained in:
Tomas Dvorak
2025-10-23 22:26:50 +02:00
parent 63700eedb2
commit 70ea0c3c91
75 changed files with 3337 additions and 1160 deletions
+221 -21
View File
@@ -40,6 +40,31 @@ type BaseController struct {
DB *gorm.DB
}
func normalizePhone(raw, country string) string {
s := strings.TrimSpace(raw)
if s == "" {
return ""
}
re := regexp.MustCompile(`[\s\-\.\(\)]`)
s = re.ReplaceAllString(s, "")
if strings.HasPrefix(s, "00") {
s = "+" + s[2:]
}
if strings.HasPrefix(s, "+") {
return s
}
if matched, _ := regexp.MatchString(`^420\d{9}$`, s); matched {
return "+" + s
}
if matched, _ := regexp.MatchString(`^\d{9}$`, s); matched {
c := strings.ToLower(country)
if strings.Contains(c, "česk") || strings.Contains(c, "czech") {
return "+420" + s
}
}
return s
}
// GetMatchesHistory returns cached past matches with overrides applied (public)
// Optional query: q= filters by home/away/venue/competition
func (bc *BaseController) GetMatchesHistory(c *gin.Context) {
@@ -600,6 +625,24 @@ func (bc *BaseController) CreateCategory(c *gin.Context) {
Name: name,
Description: strings.TrimSpace(body.Description),
}
// Ensure category slug is set and unique
s := makeSlug(cat.Name)
if s == "" {
s = "category"
}
orig := s
for i := 0; i < 50; i++ {
var cnt int64
if err := bc.DB.Model(&models.Category{}).Where("slug = ?", s).Count(&cnt).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Chyba při kontrole jedinečnosti URL"})
return
}
if cnt == 0 {
break
}
s = fmt.Sprintf("%s-%d", orig, i+1)
}
cat.Slug = s
if err := bc.DB.Create(&cat).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Nelze vytvořit kategorii"})
@@ -1715,10 +1758,31 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) {
ClubName string `json:"club_name"`
ClubLogoURL string `json:"club_logo_url"`
ClubURL string `json:"club_url"`
// Social profiles (optional)
FacebookURL string `json:"facebook_url"`
InstagramURL string `json:"instagram_url"`
YoutubeURL string `json:"youtube_url"`
// Gallery (optional)
GalleryURL string `json:"gallery_url"`
GalleryLabel string `json:"gallery_label"`
// Location/Contact (optional)
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"`
MapStyle string `json:"map_style"`
// Frontpage style (optional)
FrontpageStyle string `json:"frontpage_style"`
// Theme (optional, can set later)
PrimaryColor string `json:"primary_color"`
SecondaryColor string `json:"secondary_color"`
AccentColor string `json:"accent_color"`
@@ -1726,7 +1790,9 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) {
TextColor string `json:"text_color"`
FontHeading string `json:"font_heading"`
FontBody string `json:"font_body"`
SMTP *struct {
// SMTP optional
SMTP *struct {
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
@@ -1789,24 +1855,36 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) {
s.YoutubeURL = v
}
// Always allow updating SMTP via setup when admin already exists (idempotent)
// Gallery
if body.GalleryURL != "" {
s.GalleryURL = strings.TrimSpace(body.GalleryURL)
}
if body.GalleryLabel != "" {
s.GalleryLabel = strings.TrimSpace(body.GalleryLabel)
}
// Frontpage style
if body.FrontpageStyle != "" {
s.FrontpageStyle = body.FrontpageStyle
}
// SMTP overrides from initial setup
if body.SMTP != nil {
if host := strings.TrimSpace(body.SMTP.Host); host != "" {
s.SMTPHost = host
if v := strings.TrimSpace(body.SMTP.Host); v != "" {
s.SMTPHost = v
}
if body.SMTP.Port > 0 {
s.SMTPPort = body.SMTP.Port
}
if u := strings.TrimSpace(body.SMTP.Username); u != "" {
s.SMTPUser = u
if v := strings.TrimSpace(body.SMTP.Username); v != "" {
s.SMTPUser = v
s.SMTPAuth = true
}
if p := body.SMTP.Password; p != "" {
s.SMTPPassword = p
if v := body.SMTP.Password; v != "" {
s.SMTPPassword = v
}
if from := strings.TrimSpace(body.SMTP.From); from != "" {
s.SMTPFrom = from
if v := strings.TrimSpace(body.SMTP.From); v != "" {
s.SMTPFrom = v
}
// Default FromName if empty
if s.SMTPFromName == "" {
s.SMTPFromName = "Fotbal Club"
}
@@ -1834,7 +1912,6 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) {
return
}
}
// Trigger background prefetch and YouTube cache refresh when settings are updated post-setup
scheme := "http"
if c.Request.TLS != nil {
@@ -2035,7 +2112,7 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) {
s.ContactCountry = v
}
if v := strings.TrimSpace(body.ContactPhone); v != "" {
s.ContactPhone = v
s.ContactPhone = normalizePhone(v, body.ContactCountry)
}
if v := strings.TrimSpace(body.ContactEmail); v != "" {
s.ContactEmail = v
@@ -2530,7 +2607,25 @@ func (bc *BaseController) CreateArticle(c *gin.Context) {
var cat models.Category
if err := bc.DB.Where("name = ?", name).First(&cat).Error; err != nil {
if err == gorm.ErrRecordNotFound {
// Create category with unique slug
cat = models.Category{Name: name}
s := makeSlug(cat.Name)
if s == "" {
s = "category"
}
orig := s
for i := 0; i < 50; i++ {
var cnt int64
if err := bc.DB.Model(&models.Category{}).Where("slug = ?", s).Count(&cnt).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Chyba při kontrole jedinečnosti URL (kategorie)"})
return
}
if cnt == 0 {
break
}
s = fmt.Sprintf("%s-%d", orig, i+1)
}
cat.Slug = s
if err := bc.DB.Create(&cat).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Nelze vytvořit kategorii"})
return
@@ -2685,6 +2780,8 @@ func (bc *BaseController) UpdateArticle(c *gin.Context) {
YouTubeVideoTitle *string `json:"youtube_video_title"`
YouTubeVideoURL *string `json:"youtube_video_url"`
YouTubeVideoThumbnail *string `json:"youtube_video_thumbnail"`
// Attachments array from frontend, stored as JSON string in model
Attachments []map[string]any `json:"attachments"`
}
var body reqBody
if err := c.ShouldBindJSON(&body); err != nil {
@@ -2729,7 +2826,25 @@ func (bc *BaseController) UpdateArticle(c *gin.Context) {
var cat models.Category
if err := bc.DB.Where("name = ?", name).First(&cat).Error; err != nil {
if err == gorm.ErrRecordNotFound {
// Create category with unique slug
cat = models.Category{Name: name}
s := makeSlug(cat.Name)
if s == "" {
s = "category"
}
orig := s
for i := 0; i < 50; i++ {
var cnt int64
if err := bc.DB.Model(&models.Category{}).Where("slug = ?", s).Count(&cnt).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Chyba při kontrole jedinečnosti URL (kategorie)"})
return
}
if cnt == 0 {
break
}
s = fmt.Sprintf("%s-%d", orig, i+1)
}
cat.Slug = s
if err := bc.DB.Create(&cat).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Nelze vytvořit kategorii"})
return
@@ -2758,8 +2873,25 @@ func (bc *BaseController) UpdateArticle(c *gin.Context) {
art.Featured = *body.Featured
}
if body.Slug != nil {
art.Slug = strings.TrimSpace(*body.Slug)
}
s := strings.TrimSpace(*body.Slug)
if s == "" {
s = makeSlug(art.Title)
}
// Ensure slug is unique across other articles
orig := s
for i := 0; i < 50; i++ {
var cnt int64
if err := bc.DB.Model(&models.Article{}).Where("slug = ? AND id != ?", s, art.ID).Count(&cnt).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Chyba při kontrole jedinečnosti URL"})
return
}
if cnt == 0 {
break
}
s = fmt.Sprintf("%s-%d", orig, i+1)
}
art.Slug = s
}
if body.SeoTitle != nil {
art.SEOTitle = strings.TrimSpace(*body.SeoTitle)
}
@@ -2791,6 +2923,12 @@ func (bc *BaseController) UpdateArticle(c *gin.Context) {
if body.YouTubeVideoThumbnail != nil {
art.YouTubeVideoThumbnail = strings.TrimSpace(*body.YouTubeVideoThumbnail)
}
// Attachments
if len(body.Attachments) > 0 {
if b, err := json.Marshal(body.Attachments); err == nil {
art.Attachments = string(b)
}
}
// Auto-fill SEO if still empty after updates
if strings.TrimSpace(art.SEOTitle) == "" && strings.TrimSpace(art.Title) != "" {
@@ -2801,7 +2939,7 @@ func (bc *BaseController) UpdateArticle(c *gin.Context) {
}
if err := bc.DB.Save(&art).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Nelze uložit změny"})
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Nelze uložit změny", "error": err.Error()})
return
}
if art.ImageURL == "" {
@@ -3108,13 +3246,46 @@ func (bc *BaseController) CreatePlayer(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"chyba": err.Error()})
return
}
// Auto-split full name if last name missing and first contains spaces; drop middle names
first := strings.TrimSpace(body.FirstName)
last := strings.TrimSpace(body.LastName)
if last == "" && strings.Contains(first, " ") {
parts := strings.Fields(first)
if len(parts) >= 2 {
first = parts[0]
last = parts[len(parts)-1]
}
}
if first == "" || last == "" {
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Jméno a příjmení jsou povinné"})
return
}
// Validate numeric limits
if body.JerseyNumber != nil {
if *body.JerseyNumber < 0 || *body.JerseyNumber > 99 {
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Číslo dresu musí být v rozmezí 099"})
return
}
}
if body.Height != nil {
if *body.Height < 50 || *body.Height > 250 {
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Výška musí být v rozmezí 50250 cm"})
return
}
}
if body.Weight != nil {
if *body.Weight < 30 || *body.Weight > 200 {
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Váha musí být v rozmezí 30200 kg"})
return
}
}
p := models.Player{
FirstName: strings.TrimSpace(body.FirstName),
LastName: strings.TrimSpace(body.LastName),
FirstName: first,
LastName: last,
Position: strings.TrimSpace(body.Position),
Nationality: strings.TrimSpace(body.Nationality),
Email: strings.TrimSpace(body.Email),
Phone: strings.TrimSpace(body.Phone),
Phone: normalizePhone(body.Phone, ""),
ImageURL: strings.TrimSpace(body.ImageURL),
}
if body.TeamID != nil {
@@ -3204,6 +3375,18 @@ func (bc *BaseController) UpdatePlayer(c *gin.Context) {
if body.LastName != nil {
p.LastName = strings.TrimSpace(*body.LastName)
}
// Auto-split if last name empty and first contains spaces; ensure both present
if strings.TrimSpace(p.LastName) == "" && strings.Contains(strings.TrimSpace(p.FirstName), " ") {
parts := strings.Fields(strings.TrimSpace(p.FirstName))
if len(parts) >= 2 {
p.FirstName = parts[0]
p.LastName = parts[len(parts)-1]
}
}
if strings.TrimSpace(p.FirstName) == "" || strings.TrimSpace(p.LastName) == "" {
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Jméno a příjmení jsou povinné"})
return
}
if body.Position != nil {
p.Position = strings.TrimSpace(*body.Position)
}
@@ -3220,15 +3403,27 @@ func (bc *BaseController) UpdatePlayer(c *gin.Context) {
p.TeamID = *body.TeamID
}
if body.JerseyNumber != nil {
if *body.JerseyNumber < 0 || *body.JerseyNumber > 99 {
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Číslo dresu musí být v rozmezí 099"})
return
}
p.JerseyNumber = *body.JerseyNumber
}
if body.Nationality != nil {
p.Nationality = strings.TrimSpace(*body.Nationality)
}
if body.Height != nil {
if *body.Height < 50 || *body.Height > 250 {
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Výška musí být v rozmezí 50250 cm"})
return
}
p.Height = *body.Height
}
if body.Weight != nil {
if *body.Weight < 30 || *body.Weight > 200 {
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Váha musí být v rozmezí 30200 kg"})
return
}
p.Weight = *body.Weight
}
if body.IsActive != nil {
@@ -3238,7 +3433,7 @@ func (bc *BaseController) UpdatePlayer(c *gin.Context) {
p.Email = strings.TrimSpace(*body.Email)
}
if body.Phone != nil {
p.Phone = strings.TrimSpace(*body.Phone)
p.Phone = normalizePhone(*body.Phone, "")
}
if body.ImageURL != nil {
p.ImageURL = strings.TrimSpace(*body.ImageURL)
@@ -4067,7 +4262,7 @@ func (bc *BaseController) UpdateSettings(c *gin.Context) {
}
var body reqBody
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"chyba": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Neplatná data", "detail": err.Error()})
return
}
@@ -4336,7 +4531,12 @@ func (bc *BaseController) UpdateSettings(c *gin.Context) {
s.ContactCountry = strings.TrimSpace(*body.ContactCountry)
}
if body.ContactPhone != nil {
s.ContactPhone = strings.TrimSpace(*body.ContactPhone)
v := strings.TrimSpace(*body.ContactPhone)
country := s.ContactCountry
if body.ContactCountry != nil {
country = strings.TrimSpace(*body.ContactCountry)
}
s.ContactPhone = normalizePhone(v, country)
}
if body.ContactEmail != nil {
s.ContactEmail = strings.TrimSpace(*body.ContactEmail)