Commit 03a75d37 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: branding persists in Supabase — survives deploys

PROBLEM: Theme was stored in local theme.json inside Docker container.
Every deploy rebuilt container → all branding settings LOST.

FIX: Dual storage — writes to both local file AND Supabase.
Player app reads from Supabase API (persistent).

Changes:
1. New API: /api/branding.php
   - GET: returns full theme from platform_theme + platform_assets tables
   - POST save_theme: upserts values into platform_theme
   - POST save_asset: upserts asset URLs into platform_assets
   - Public endpoint (cached 60s)

2. theme.js updated:
   - Fetches from /api/branding.php instead of local theme.json
   - Same apply logic (CSS vars, animations, button shapes)

3. admin/branding.php updated:
   - Reads from Supabase on page load (not just local file)
   - Saves to BOTH local file AND Supabase on every edit
   - Auto-save JS still works (posts to same page → writes to Supabase)

Flow:
- Admin edits color → auto-save → PHP writes to Supabase + local
- Player opens app → theme.js fetches /api/branding.php → Supabase query
- Deploy happens → local file reset → Supabase still has all values 
- Admin page reloads → reads from Supabase → shows saved values Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 29021fee
......@@ -23,11 +23,42 @@ $BRAND_DIR = __DIR__ . '/../public/assets/brand/';
if (!is_dir($BRAND_DIR)) mkdir($BRAND_DIR, 0777, true);
$THEME_FILE = __DIR__ . '/../public/assets/brand/theme.json';
$theme = file_exists($THEME_FILE) ? json_decode(file_get_contents($THEME_FILE), true) : [];
// Load theme from Supabase first (persistent), fallback to local file
require_once __DIR__ . '/../includes/supabase.php';
$sdb = supabaseService();
$themeRows = $sdb->get('platform_theme', ['select' => 'id,value']);
$theme = [];
if (is_array($themeRows) && !isset($themeRows['error'])) {
foreach ($themeRows as $row) {
$theme[$row['id']] = $row['value'];
}
}
// Merge local file on top (for assets that are still local)
if (file_exists($THEME_FILE)) {
$localTheme = json_decode(file_get_contents($THEME_FILE), true) ?: [];
$theme = array_merge($theme, $localTheme);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_theme'])) {
$theme = array_merge($theme, $_POST['theme'] ?? []);
file_put_contents($THEME_FILE, json_encode($theme, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
// Also save to Supabase (persistent across deploys)
require_once __DIR__ . '/../includes/supabase.php';
$sdb = supabaseService();
foreach ($_POST['theme'] ?? [] as $key => $value) {
if (empty($key) || $value === null) continue;
$existing = $sdb->get('platform_theme', ['id' => 'eq.' . $key, 'select' => 'id', 'limit' => 1]);
if (!empty($existing) && !isset($existing['error'])) {
$sdb->update('platform_theme', ['value' => $value], ['id' => 'eq.' . $key]);
} else {
$cat = 'color';
if (strpos($key, 'anim_') === 0 || strpos($key, 'btn_') === 0) $cat = 'animation';
if (strpos($key, 'ludo_') === 0 || strpos($key, 'board_') === 0) $cat = 'game';
if (strpos($key, 'juice_') === 0) $cat = 'juice';
$sdb->insert('platform_theme', ['id' => $key, 'category' => $cat, 'label' => $key, 'value' => $value, 'default_value' => $value, 'value_type' => (strpos($value, '#') === 0) ? 'hex' : 'string']);
}
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['asset'])) {
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Cache-Control: public, max-age=60');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
require_once __DIR__ . '/../includes/supabase.php';
require_once __DIR__ . '/../includes/auth.php';
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
// Public endpoint — returns full theme + assets for player app
$sdb = supabaseService();
// Get all theme values
$themeRows = $sdb->get('platform_theme', ['select' => 'id,value,category']);
$theme = [];
if (is_array($themeRows) && !isset($themeRows['error'])) {
foreach ($themeRows as $row) {
$theme[$row['id']] = $row['value'];
}
}
// Get all assets
$assetRows = $sdb->get('platform_assets', ['select' => 'id,asset_url,dimensions']);
$assets = [];
if (is_array($assetRows) && !isset($assetRows['error'])) {
foreach ($assetRows as $row) {
if ($row['asset_url']) {
$assets[$row['id']] = $row['asset_url'];
}
}
}
$theme['assets'] = $assets;
echo json_encode($theme, JSON_UNESCAPED_UNICODE);
exit;
}
if ($method === 'POST') {
// Admin endpoint — saves theme values to Supabase
$input = getInput();
$action = $input['action'] ?? '';
$sdb = supabaseService();
if ($action === 'save_theme') {
$values = $input['values'] ?? [];
foreach ($values as $key => $value) {
if (empty($key) || $value === null) continue;
// Upsert: try update first, if no rows affected, insert
$existing = $sdb->get('platform_theme', ['id' => 'eq.' . $key, 'select' => 'id', 'limit' => 1]);
if (!empty($existing) && !isset($existing['error'])) {
$sdb->update('platform_theme', ['value' => $value], ['id' => 'eq.' . $key]);
} else {
$sdb->insert('platform_theme', [
'id' => $key,
'category' => categorizeKey($key),
'label' => $key,
'value' => $value,
'default_value' => $value,
'value_type' => (strpos($value, '#') === 0) ? 'hex' : 'string'
]);
}
}
jsonResponse(['success' => true]);
}
if ($action === 'save_asset') {
$slot = $input['slot'] ?? '';
$url = $input['url'] ?? '';
if (!$slot) jsonError('slot required');
$existing = $sdb->get('platform_assets', ['id' => 'eq.' . $slot, 'select' => 'id', 'limit' => 1]);
if (!empty($existing) && !isset($existing['error'])) {
$sdb->update('platform_assets', ['asset_url' => $url], ['id' => 'eq.' . $slot]);
} else {
$sdb->insert('platform_assets', [
'id' => $slot,
'category' => 'custom',
'label' => $slot,
'asset_url' => $url
]);
}
jsonResponse(['success' => true]);
}
jsonError('Invalid action');
}
function categorizeKey(string $key): string {
if (strpos($key, 'bg_') === 0) return 'color';
if (strpos($key, 'ludo_') === 0) return 'game';
if (strpos($key, 'board_') === 0) return 'game';
if (strpos($key, 'chess_') === 0) return 'game';
if (strpos($key, 'anim_') === 0) return 'animation';
if (strpos($key, 'btn_') === 0) return 'button';
if (strpos($key, 'juice_') === 0) return 'juice';
return 'color';
}
const THEME_URL = '/public/assets/brand/theme.json';
// Theme loads from Supabase via API (survives deploys)
const THEME_API = '/api/branding.php';
let themeData = null;
export async function load() {
try {
const res = await fetch(THEME_URL + '?t=' + Date.now());
const res = await fetch(THEME_API);
if (!res.ok) return;
themeData = await res.json();
applyColors();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment