mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
631 lines
18 KiB
Markdown
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
|