mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
dev day #81
This commit is contained in:
@@ -0,0 +1,630 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user