mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
268 lines
8.3 KiB
Go
268 lines
8.3 KiB
Go
package controllers
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// EditorPreviewController handles real-time editor preview operations
|
|
type EditorPreviewController struct {
|
|
DB *gorm.DB
|
|
}
|
|
|
|
// NewEditorPreviewController creates a new editor preview controller
|
|
func NewEditorPreviewController(db *gorm.DB) *EditorPreviewController {
|
|
return &EditorPreviewController{DB: db}
|
|
}
|
|
|
|
// PreviewState represents the current editor state
|
|
type PreviewState struct {
|
|
SessionID string `json:"session_id"`
|
|
PageType string `json:"page_type"`
|
|
Elements []ElementPreviewConfig `json:"elements"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
UserID uint `json:"user_id,omitempty"`
|
|
}
|
|
|
|
// ElementPreviewConfig represents a single element's preview configuration
|
|
type ElementPreviewConfig struct {
|
|
ElementName string `json:"element_name"`
|
|
Variant string `json:"variant"`
|
|
Visible bool `json:"visible"`
|
|
DisplayOrder int `json:"display_order"`
|
|
CustomStyles map[string]interface{} `json:"custom_styles,omitempty"`
|
|
}
|
|
|
|
// GetPreviewState retrieves the current preview state for a session
|
|
// GET /api/v1/editor/preview/:session_id
|
|
func (c *EditorPreviewController) GetPreviewState(ctx *gin.Context) {
|
|
sessionID := ctx.Param("session_id")
|
|
|
|
// In a real implementation, you would fetch from cache/session store
|
|
// For now, return a stub response
|
|
ctx.JSON(http.StatusOK, gin.H{
|
|
"session_id": sessionID,
|
|
"page_type": "homepage",
|
|
"elements": []ElementPreviewConfig{},
|
|
"updated_at": time.Now(),
|
|
})
|
|
}
|
|
|
|
// UpdatePreviewState updates the preview state for a session
|
|
// POST /api/v1/editor/preview/:session_id
|
|
func (c *EditorPreviewController) UpdatePreviewState(ctx *gin.Context) {
|
|
sessionID := ctx.Param("session_id")
|
|
|
|
var state PreviewState
|
|
if err := ctx.ShouldBindJSON(&state); err != nil {
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
|
return
|
|
}
|
|
|
|
state.SessionID = sessionID
|
|
state.UpdatedAt = time.Now()
|
|
|
|
// In a real implementation, you would:
|
|
// 1. Store in Redis/cache with TTL (e.g., 1 hour)
|
|
// 2. Associate with user session
|
|
// 3. Validate element configurations
|
|
|
|
ctx.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"state": state,
|
|
})
|
|
}
|
|
|
|
// ApplyPreviewChanges commits preview changes to the database
|
|
// POST /api/v1/editor/preview/:session_id/apply
|
|
func (c *EditorPreviewController) ApplyPreviewChanges(ctx *gin.Context) {
|
|
sessionID := ctx.Param("session_id")
|
|
|
|
var payload struct {
|
|
PageType string `json:"page_type" binding:"required"`
|
|
Elements []ElementPreviewConfig `json:"elements" binding:"required"`
|
|
}
|
|
|
|
if err := ctx.ShouldBindJSON(&payload); err != nil {
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
|
return
|
|
}
|
|
|
|
// Start transaction
|
|
tx := c.DB.Begin()
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
tx.Rollback()
|
|
}
|
|
}()
|
|
|
|
// Delete existing configurations for this page type
|
|
if err := tx.Exec("DELETE FROM page_element_configs WHERE page_type = ?", payload.PageType).Error; err != nil {
|
|
tx.Rollback()
|
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to clear existing configurations"})
|
|
return
|
|
}
|
|
|
|
// Insert new configurations
|
|
for _, elem := range payload.Elements {
|
|
config := map[string]interface{}{
|
|
"page_type": payload.PageType,
|
|
"element_name": elem.ElementName,
|
|
"variant": elem.Variant,
|
|
"visible": elem.Visible,
|
|
"display_order": elem.DisplayOrder,
|
|
"created_at": time.Now(),
|
|
"updated_at": time.Now(),
|
|
}
|
|
|
|
// Persist custom styles into settings JSON under settings.styles for compatibility
|
|
if len(elem.CustomStyles) > 0 {
|
|
config["settings"] = map[string]interface{}{
|
|
"styles": elem.CustomStyles,
|
|
}
|
|
}
|
|
|
|
if err := tx.Table("page_element_configs").Create(config).Error; err != nil {
|
|
tx.Rollback()
|
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save configuration"})
|
|
return
|
|
}
|
|
}
|
|
|
|
// Commit transaction
|
|
if err := tx.Commit().Error; err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to commit changes"})
|
|
return
|
|
}
|
|
|
|
// Clear preview state from cache
|
|
// In a real implementation: cache.Delete(sessionID)
|
|
|
|
ctx.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"session_id": sessionID,
|
|
"message": "Changes applied successfully",
|
|
"applied_at": time.Now(),
|
|
})
|
|
}
|
|
|
|
// DiscardPreviewChanges discards preview changes without saving
|
|
// DELETE /api/v1/editor/preview/:session_id
|
|
func (c *EditorPreviewController) DiscardPreviewChanges(ctx *gin.Context) {
|
|
sessionID := ctx.Param("session_id")
|
|
|
|
// In a real implementation: cache.Delete(sessionID)
|
|
|
|
ctx.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"session_id": sessionID,
|
|
"message": "Preview discarded",
|
|
})
|
|
}
|
|
|
|
// ValidatePreviewConfig validates element configurations before saving
|
|
// POST /api/v1/editor/preview/validate
|
|
func (c *EditorPreviewController) ValidatePreviewConfig(ctx *gin.Context) {
|
|
var configs []ElementPreviewConfig
|
|
if err := ctx.ShouldBindJSON(&configs); err != nil {
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
|
return
|
|
}
|
|
|
|
// Validation rules
|
|
errors := []string{}
|
|
elementNames := make(map[string]bool)
|
|
|
|
for i, config := range configs {
|
|
// Check for duplicate element names
|
|
if elementNames[config.ElementName] {
|
|
errors = append(errors, "Duplicate element name: "+config.ElementName)
|
|
}
|
|
elementNames[config.ElementName] = true
|
|
|
|
// Validate variant is not empty
|
|
if config.Variant == "" {
|
|
errors = append(errors, "Element "+config.ElementName+" has empty variant")
|
|
}
|
|
|
|
// Validate display order
|
|
if config.DisplayOrder < 0 {
|
|
errors = append(errors, "Element "+config.ElementName+" has invalid display order")
|
|
}
|
|
|
|
// Check for gaps in display order
|
|
if config.DisplayOrder != i {
|
|
errors = append(errors, "Display order has gaps or is not sequential")
|
|
}
|
|
}
|
|
|
|
if len(errors) > 0 {
|
|
ctx.JSON(http.StatusBadRequest, gin.H{
|
|
"valid": false,
|
|
"errors": errors,
|
|
})
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, gin.H{
|
|
"valid": true,
|
|
"message": "Configuration is valid",
|
|
})
|
|
}
|
|
|
|
// GetAvailableVariants returns available variants for an element
|
|
// GET /api/v1/editor/variants/:element_name
|
|
func (c *EditorPreviewController) GetAvailableVariants(ctx *gin.Context) {
|
|
elementName := ctx.Param("element_name")
|
|
|
|
// Define available variants for each element
|
|
// In a real implementation, this could come from database or config
|
|
variants := map[string][]map[string]interface{}{
|
|
"hero": {
|
|
{"value": "grid", "label": "Grid Layout", "description": "3-card grid hero section"},
|
|
{"value": "scroller", "label": "Horizontal Scroller", "description": "Scrollable cards"},
|
|
{"value": "swiper", "label": "Swiper", "description": "Swipeable carousel"},
|
|
{"value": "swiper_full", "label": "Full-width Swiper", "description": "Full viewport carousel"},
|
|
},
|
|
"matches": {
|
|
{"value": "default", "label": "Default", "description": "Standard match display"},
|
|
{"value": "compact", "label": "Compact", "description": "Compact match cards"},
|
|
},
|
|
"table": {
|
|
{"value": "default", "label": "Default", "description": "Standard table layout"},
|
|
{"value": "compact", "label": "Compact", "description": "Compact table view"},
|
|
},
|
|
"videos": {
|
|
{"value": "default", "label": "Default", "description": "Standard video grid"},
|
|
{"value": "carousel", "label": "Carousel", "description": "Video carousel"},
|
|
},
|
|
"gallery": {
|
|
{"value": "default", "label": "Default", "description": "Standard gallery grid"},
|
|
{"value": "masonry", "label": "Masonry", "description": "Masonry layout"},
|
|
},
|
|
"sponsors": {
|
|
{"value": "grid", "label": "Grid", "description": "Grid layout"},
|
|
{"value": "slider", "label": "Slider", "description": "Horizontal slider"},
|
|
},
|
|
"newsletter": {
|
|
{"value": "default", "label": "Default", "description": "Standard newsletter form"},
|
|
{"value": "inline", "label": "Inline", "description": "Inline compact form"},
|
|
},
|
|
}
|
|
|
|
if variantList, ok := variants[elementName]; ok {
|
|
ctx.JSON(http.StatusOK, gin.H{
|
|
"element_name": elementName,
|
|
"variants": variantList,
|
|
})
|
|
} else {
|
|
ctx.JSON(http.StatusOK, gin.H{
|
|
"element_name": elementName,
|
|
"variants": []map[string]interface{}{{"value": "default", "label": "Default"}},
|
|
})
|
|
}
|
|
}
|