Files
MyClub/DOCS/PREMIUM_ARCHITECTURE.md
T
Tomas Dvorak d5b4faea61 dev day #81
2025-11-03 19:54:39 +01:00

18 KiB

Premium Version - Technical Architecture

Overview

This document describes the architecture for implementing a premium/pro version toggle system that allows switching between:

  • Standard Mode: Current React + MyUIbrix system
  • Premium Mode: Professional Elementor-style templates

System Design

Architecture Diagram

┌─────────────────────────────────────────────────────────────┐
│                        User Request                          │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│                   Nginx / Reverse Proxy                      │
│  Routes: /premium/* → Static Assets                          │
│          /api/* → Backend                                    │
│          /* → Frontend React App                             │
└────────────────────────┬────────────────────────────────────┘
                         │
          ┌──────────────┴──────────────┐
          │                             │
          ▼                             ▼
┌──────────────────┐           ┌──────────────────┐
│  Backend (Go)    │           │  Frontend (React)│
│                  │           │                  │
│  Middleware:     │◄─────────►│  Check Settings: │
│  - Premium Mode  │   API     │  premium_mode_   │
│  - Feature Flags │           │  active          │
│                  │           │                  │
│  Routes:         │           │  Conditional     │
│  /premium/css/*  │           │  Render:         │
│  /premium/js/*   │           │  - Standard      │
│  /premium/img/*  │           │  - Premium       │
└──────────────────┘           └──────────────────┘
          │                             │
          ▼                             ▼
┌──────────────────┐           ┌──────────────────┐
│  PostgreSQL      │           │  Browser         │
│                  │           │                  │
│  settings table: │           │  Loads:          │
│  - premium_mode_ │           │  - Premium CSS   │
│    active        │           │  - Premium JS    │
│  - premium_      │           │  - Club Theme    │
│    features      │           │                  │
└──────────────────┘           └──────────────────┘

Component Architecture

Backend Components

1. Configuration Layer

File: internal/config/config.go

type Config struct {
    // Existing fields...
    
    // Premium Mode Configuration
    PremiumMode         bool   `env:"PREMIUM_MODE" envDefault:"false"`
    PremiumHomepage     bool   `env:"PREMIUM_HOMEPAGE" envDefault:"false"`
    PremiumBlog         bool   `env:"PREMIUM_BLOG" envDefault:"false"`
    Premium404          bool   `env:"PREMIUM_404" envDefault:"false"`
    DisableMyUIbrix     bool   `env:"PREMIUM_DISABLE_MYUIBRIX" envDefault:"false"`
    PremiumAssetsPath   string `env:"PREMIUM_ASSETS_PATH" envDefault:"./pro"`
}

func (c *Config) IsPremiumActive(pageType string) bool {
    if !c.PremiumMode {
        return false
    }
    
    switch pageType {
    case "homepage":
        return c.PremiumHomepage
    case "blog":
        return c.PremiumBlog
    case "404":
        return c.Premium404
    default:
        return false
    }
}

2. Middleware Layer

File: internal/middleware/premium_mode.go

func PremiumModeMiddleware(cfg *config.Config) gin.HandlerFunc {
    return func(c *gin.Context) {
        // Set premium context
        c.Set("premium_mode", cfg.PremiumMode)
        c.Set("disable_myuibrix", cfg.DisableMyUIbrix)
        
        // Add premium features to context
        premiumFeatures := map[string]bool{
            "homepage": cfg.PremiumHomepage,
            "blog": cfg.PremiumBlog,
            "404": cfg.Premium404,
        }
        c.Set("premium_features", premiumFeatures)
        
        // Log premium mode status
        if cfg.PremiumMode {
            log.Debug("Premium mode active for request: %s", c.Request.URL.Path)
        }
        
        c.Next()
    }
}

3. Settings Extension

File: internal/models/settings.go

type Settings struct {
    // Existing fields...
    
    PremiumModeActive   bool   `json:"premium_mode_active" gorm:"default:false"`
    PremiumFeatures     string `json:"premium_features" gorm:"type:text"` // JSON
    PremiumThemeVariant string `json:"premium_theme_variant" gorm:"default:'default'"`
}

type PremiumFeatures struct {
    Homepage bool `json:"homepage"`
    Blog     bool `json:"blog"`
    Error404 bool `json:"404"`
}

func (s *Settings) GetPremiumFeatures() (*PremiumFeatures, error) {
    if s.PremiumFeatures == "" {
        return &PremiumFeatures{}, nil
    }
    
    var features PremiumFeatures
    err := json.Unmarshal([]byte(s.PremiumFeatures), &features)
    return &features, err
}

4. Controller Extension

File: internal/controllers/base_controller.go

func (ctrl *BaseController) GetPublicSettings(c *gin.Context) {
    settings, err := ctrl.DB.GetSettings()
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    
    // Add premium mode info from environment
    cfg := config.GetConfig()
    settings.PremiumModeActive = cfg.PremiumMode
    
    if cfg.PremiumMode {
        features, _ := settings.GetPremiumFeatures()
        c.JSON(http.StatusOK, gin.H{
            "settings": settings,
            "premium": gin.H{
                "enabled": true,
                "features": features,
                "disable_myuibrix": cfg.DisableMyUIbrix,
            },
        })
    } else {
        c.JSON(http.StatusOK, gin.H{
            "settings": settings,
            "premium": gin.H{
                "enabled": false,
            },
        })
    }
}

Frontend Components

1. Premium Layout System

File: frontend/src/layouts/PremiumLayout.tsx

import React, { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import { loadPremiumAssets, cleanupPremiumAssets } from '../utils/premiumAssets';

interface PremiumLayoutProps {
  children: React.ReactNode;
  pageType: 'home' | 'blog' | '404';
  settings?: any;
}

export const PremiumLayout: React.FC<PremiumLayoutProps> = ({ 
  children, 
  pageType,
  settings 
}) => {
  const [assetsLoaded, setAssetsLoaded] = useState(false);

  useEffect(() => {
    // Load premium assets
    loadPremiumAssets(pageType)
      .then(() => setAssetsLoaded(true))
      .catch(err => console.error('Failed to load premium assets:', err));

    // Cleanup on unmount
    return () => {
      cleanupPremiumAssets();
    };
  }, [pageType]);

  if (!assetsLoaded) {
    return <div>Loading premium theme...</div>;
  }

  return (
    <>
      <Helmet>
        <body className={`premium-mode theme-atleticos lte-fw-loaded page-${pageType}`} />
      </Helmet>
      <div className="lte-content-wrapper lte-layout-transparent-full">
        {children}
      </div>
    </>
  );
};

2. Asset Loader Utility

File: frontend/src/utils/premiumAssets.ts

interface AssetConfig {
  css: string[];
  js: string[];
  fonts: string[];
}

const assetConfig: Record<string, AssetConfig> = {
  home: {
    css: [
      '/premium/css/bootstrap.css',
      '/premium/css/bizoni.css',
      '/premium/css/elementor-frontend.min.css',
      '/premium/css/zoom-slider.css',
      '/premium/css/swiper.css',
      '/premium/css/post-32647.css',
    ],
    js: [
      '/premium/js/jquery.min.js',
      '/premium/js/jquery-migrate.min.js',
      '/premium/js/modernizr-2.6.2.min.js',
      '/premium/js/swiper.min.js',
      '/premium/js/jquery.zoomslider.js',
      '/premium/js/parallax-js.js',
      '/premium/js/script.js',
      '/premium/js/webpack.runtime.min.js',
      '/premium/js/frontend-modules.min.js',
      '/premium/js/frontend.min.js',
    ],
    fonts: [
      'https://fonts.googleapis.com/css?family=Open+Sans:400,400i,600,700|Sofia+Sans+Extra+Condensed:800,300i',
      'https://fonts.googleapis.com/icon?family=Material+Icons',
    ],
  },
  blog: {
    css: [
      '/premium/css/bootstrap.css',
      '/premium/css/bizoni.css',
      '/premium/css/elementor-frontend.min.css',
      '/premium/css/post-29393.css',
    ],
    js: [
      '/premium/js/jquery.min.js',
      '/premium/js/scripts.js',
    ],
    fonts: [],
  },
  '404': {
    css: [
      '/premium/css/bootstrap.css',
      '/premium/css/bizoni.css',
    ],
    js: [
      '/premium/js/jquery.min.js',
    ],
    fonts: [],
  },
};

const loadedAssets: Set<string> = new Set();

export const loadCSS = (href: string): Promise<void> => {
  if (loadedAssets.has(href)) {
    return Promise.resolve();
  }

  return new Promise((resolve, reject) => {
    const link = document.createElement('link');
    link.rel = 'stylesheet';
    link.href = href;
    link.onload = () => {
      loadedAssets.add(href);
      resolve();
    };
    link.onerror = () => reject(new Error(`Failed to load CSS: ${href}`));
    document.head.appendChild(link);
  });
};

export const loadJS = (src: string): Promise<void> => {
  if (loadedAssets.has(src)) {
    return Promise.resolve();
  }

  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = src;
    script.async = false; // Maintain load order
    script.onload = () => {
      loadedAssets.add(src);
      resolve();
    };
    script.onerror = () => reject(new Error(`Failed to load JS: ${src}`));
    document.body.appendChild(script);
  });
};

export const loadPremiumAssets = async (pageType: string): Promise<void> => {
  const config = assetConfig[pageType];
  if (!config) {
    throw new Error(`Unknown page type: ${pageType}`);
  }

  try {
    // Load CSS first
    await Promise.all(config.css.map(loadCSS));
    
    // Load fonts
    await Promise.all(config.fonts.map(loadCSS));
    
    // Load JS in order
    for (const src of config.js) {
      await loadJS(src);
    }
  } catch (error) {
    console.error('Failed to load premium assets:', error);
    throw error;
  }
};

export const cleanupPremiumAssets = (): void => {
  // Remove all premium CSS
  document.querySelectorAll('link[href^="/premium/"]').forEach(el => el.remove());
  
  // Remove all premium JS
  document.querySelectorAll('script[src^="/premium/"]').forEach(el => el.remove());
  
  // Clear loaded assets cache
  loadedAssets.clear();
};

3. Theme Hook

File: frontend/src/hooks/usePremiumTheme.ts

import { useEffect } from 'react';
import { useSettings } from './useSettings';

export const usePremiumTheme = () => {
  const { settings } = useSettings();

  useEffect(() => {
    if (!settings) return;

    const root = document.documentElement;

    // Inject club colors into premium CSS variables
    root.style.setProperty('--lte-main-color', settings.primary_color || '#e63946');
    root.style.setProperty('--lte-secondary-color', settings.secondary_color || '#1d3557');
    root.style.setProperty('--lte-text-on-primary', settings.text_on_primary || '#ffffff');
    root.style.setProperty('--lte-text-on-secondary', settings.text_on_secondary || '#f1faee');
    root.style.setProperty('--lte-accent-color', settings.accent_color || '#a8dadc');

    // Typography
    root.style.setProperty('--lte-font-primary', "'Open Sans', sans-serif");
    root.style.setProperty('--lte-font-display', "'Sofia Sans Extra Condensed', sans-serif");

    // Cleanup
    return () => {
      root.style.removeProperty('--lte-main-color');
      root.style.removeProperty('--lte-secondary-color');
      // ... other cleanup
    };
  }, [settings]);

  return { settings };
};

4. Routing Logic

File: frontend/src/App.tsx

import { useSettings } from './hooks/useSettings';
import { PremiumHomePage } from './pages/PremiumHomePage';
import { PremiumBlogPage } from './pages/PremiumBlogPage';
import { PremiumNotFoundPage } from './pages/PremiumNotFoundPage';
import HomePage from './pages/HomePage';
import BlogPage from './pages/BlogPage';
import NotFoundPage from './pages/NotFoundPage';

const App: React.FC = () => {
  const { settings, isLoading } = useSettings();
  
  if (isLoading) {
    return <LoadingScreen />;
  }

  const isPremiumMode = settings?.premium_mode_active;

  return (
    <BrowserRouter>
      <Routes>
        {/* Conditional homepage */}
        <Route 
          path="/" 
          element={isPremiumMode ? <PremiumHomePage /> : <HomePage />} 
        />
        
        {/* Conditional blog */}
        <Route 
          path="/blog/:slug" 
          element={isPremiumMode ? <PremiumBlogPage /> : <BlogPage />} 
        />
        
        {/* Other routes stay standard */}
        <Route path="/hraci" element={<PlayersPage />} />
        <Route path="/kontakt" element={<ContactPage />} />
        
        {/* Conditional 404 */}
        <Route 
          path="*" 
          element={isPremiumMode ? <PremiumNotFoundPage /> : <NotFoundPage />} 
        />
      </Routes>
    </BrowserRouter>
  );
};

Data Flow

1. Standard Mode (PREMIUM_MODE=false)

Request → Backend → Settings API → Frontend
                                  → Render: HomePage
                                  → MyUIbrix: Enabled

2. Premium Mode (PREMIUM_MODE=true)

Request → Backend → Settings API → Frontend
                                  → Check: premium_mode_active=true
                                  → Load: Premium Assets
                                  → Inject: Club Colors
                                  → Render: PremiumHomePage
                                  → MyUIbrix: Disabled

3. Asset Loading Sequence

1. React App Loads
2. Check Settings API
3. If Premium Mode:
   a. Load Core CSS (bootstrap, bizoni)
   b. Load Component CSS (zoom-slider, swiper)
   c. Load Fonts (Google Fonts)
   d. Load Core JS (jQuery, modernizr)
   e. Load Libraries (swiper, parallax)
   f. Load Elementor JS (webpack, frontend)
   g. Initialize Premium Components
   h. Inject Club Theme
4. Render Premium Page

Performance Optimization

Code Splitting Strategy

// Lazy load premium components
const PremiumHomePage = React.lazy(() => import('./pages/PremiumHomePage'));
const PremiumBlogPage = React.lazy(() => import('./pages/PremiumBlogPage'));

// Use Suspense for loading states
<Suspense fallback={<LoadingScreen />}>
  <PremiumHomePage />
</Suspense>

Asset Optimization

  1. CSS Minification: All premium CSS files minified
  2. JS Bundle Splitting: Separate bundles for homepage, blog, 404
  3. Image Lazy Loading: Premium images load on scroll
  4. Font Subsetting: Load only used glyphs
  5. Resource Hints: Preload critical assets

Caching Strategy

# Nginx configuration
location /premium/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

Security Considerations

1. Asset Isolation

  • Premium assets served from separate directory
  • No cross-contamination with standard mode

2. Feature Toggles

  • Environment-based (server-side control)
  • Cannot be manipulated by client

3. Content Security Policy

<Helmet>
  <meta http-equiv="Content-Security-Policy" content="
    default-src 'self';
    script-src 'self' https://unpkg.com https://fonts.googleapis.com 'unsafe-inline';
    style-src 'self' https://fonts.googleapis.com 'unsafe-inline';
    font-src 'self' https://fonts.gstatic.com;
    img-src 'self' data: https:;
  " />
</Helmet>

Rollback Strategy

Quick Rollback

# Set environment variable
PREMIUM_MODE=false

# Restart backend
docker-compose restart backend

# Clear cache
redis-cli FLUSHALL

Database Rollback

-- Revert settings
UPDATE settings SET premium_mode_active = FALSE;

-- Run down migration
migrate -path database/migrations -database "postgres://..." down 1

Monitoring & Logging

Key Metrics

  • Premium mode activation rate
  • Asset load times
  • Error rates (CSS/JS loading failures)
  • User engagement (premium vs standard)
  • Performance metrics (Lighthouse scores)

Logging

log.Info("Premium mode activated", map[string]interface{}{
    "user_id": userID,
    "page_type": pageType,
    "assets_loaded": len(loadedAssets),
    "load_time_ms": loadTime,
})

Migration Path

Phase 1: Add Premium Support (No Breaking Changes)

  • Add environment variables
  • Extend Settings model
  • Create premium components
  • Keep standard mode as default

Phase 2: Test Premium Mode (Opt-In)

  • Enable for specific users/teams
  • Collect feedback
  • Fix bugs
  • Optimize performance

Phase 3: Production Rollout

  • Enable premium mode globally
  • Monitor metrics
  • Gradual migration
  • Keep rollback option

Phase 4: Sunset Standard Mode (Optional)

  • After 3-6 months of stable premium operation
  • Remove MyUIbrix dependencies
  • Simplify codebase