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

631 lines
18 KiB
Markdown

# 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`
```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`
```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`
```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`
```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`
```typescript
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`
```typescript
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`
```typescript
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`
```typescript
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
```typescript
// 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
# 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
```typescript
<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
```bash
# Set environment variable
PREMIUM_MODE=false
# Restart backend
docker-compose restart backend
# Clear cache
redis-cli FLUSHALL
```
### Database Rollback
```sql
-- 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
```go
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