mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 18:52:56 +00:00
18 KiB
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
- CSS Minification: All premium CSS files minified
- JS Bundle Splitting: Separate bundles for homepage, blog, 404
- Image Lazy Loading: Premium images load on scroll
- Font Subsetting: Load only used glyphs
- 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