Commit 67c6136f authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: branding page — manual save button, images stored in Supabase Storage

1. REMOVED auto-save entirely
   - No more saving on every keystroke
   - Changes tracked as 'dirty' state

2. NEW: Sticky bottom save bar
   - Shows 'No unsaved changes' (grey) / '️ Unsaved changes' (yellow)
   - '💾 Save All' button only active when changes exist
   - Success: '✓ Saved!' feedback
   - beforeunload warning if leaving with unsaved changes

3. Image uploads now go to Supabase Storage
   - Uploaded to: profile-images/branding/{slot}.{ext}
   - Public URL stored in platform_assets table
   - Survives deploys (not local filesystem)
   - Also saved locally for immediate preview in admin
   - Asset URL returned by /api/branding.php GET endpoint

4. Player app reads asset URLs from Supabase
   - theme.js fetches /api/branding.php → includes assets from platform_assets
   - Uploaded SVGs/PNGs render at exact specified size
   - object-fit:contain + image-rendering for no pixelation

Flow:
- Admin uploads logo.svg → Supabase Storage → URL in platform_assets
- Admin changes gold color → marks dirty → clicks Save → Supabase platform_theme
- Player loads app → /api/branding.php → gets colors + asset URLs → applies
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 1401d21f
...@@ -68,13 +68,45 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['asset'])) { ...@@ -68,13 +68,45 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['asset'])) {
$file = $_FILES['asset']; $file = $_FILES['asset'];
if ($file['error'] === 0 && $slot) { if ($file['error'] === 0 && $slot) {
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
foreach (glob($BRAND_DIR . $slot . '.*') as $old) { unlink($old); } $fileName = 'branding/' . $slot . '.' . $ext;
$dest = $BRAND_DIR . $slot . '.' . $ext; $fileContent = file_get_contents($file['tmp_name']);
move_uploaded_file($file['tmp_name'], $dest);
$theme['assets'][$slot] = '/public/assets/brand/' . $slot . '.' . $ext; // Upload to Supabase Storage (persistent across deploys)
$theme['asset_sizes'][$slot] = ['w' => $expectedW, 'h' => $expectedH]; $storageUrl = SUPABASE_URL . '/storage/v1/object/profile-images/' . $fileName;
$ch = curl_init($storageUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, $fileContent);
$mimeTypes = ['svg' => 'image/svg+xml', 'png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'webp' => 'image/webp', 'gif' => 'image/gif'];
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . SUPABASE_SERVICE_KEY,
'Content-Type: ' . ($mimeTypes[$ext] ?? 'application/octet-stream'),
'x-upsert: true'
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$uploadResult = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode >= 200 && $httpCode < 300) {
// Public URL for the uploaded asset
$publicUrl = SUPABASE_URL . '/storage/v1/object/public/profile-images/' . $fileName;
$theme['assets'][$slot] = $publicUrl;
// Save asset URL to Supabase platform_assets table
$existing = $sdb->get('platform_assets', ['id' => 'eq.' . $slot, 'select' => 'id', 'limit' => 1]);
if (!empty($existing) && !isset($existing['error'])) {
$sdb->update('platform_assets', ['asset_url' => $publicUrl, 'dimensions' => $expectedW . 'x' . $expectedH], ['id' => 'eq.' . $slot]);
} else {
$sdb->insert('platform_assets', ['id' => $slot, 'category' => 'custom', 'label' => $slot, 'asset_url' => $publicUrl, 'dimensions' => $expectedW . 'x' . $expectedH]);
}
// Also save locally for immediate preview
@mkdir($BRAND_DIR, 0777, true);
file_put_contents($BRAND_DIR . $slot . '.' . $ext, $fileContent);
file_put_contents($THEME_FILE, json_encode($theme, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); file_put_contents($THEME_FILE, json_encode($theme, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
} }
}
} }
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
...@@ -482,47 +514,78 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['asset'])) { ...@@ -482,47 +514,78 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['asset'])) {
</div> </div>
<div id="save-indicator" style="position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#34D399;color:#000;padding:8px 20px;border-radius:99px;font-size:13px;font-weight:700;opacity:0;transition:opacity 0.3s;z-index:999;pointer-events:none;">✓ Saved</div> <!-- Sticky save bar -->
<div id="save-bar" style="position:fixed;bottom:0;left:0;right:0;background:#0f0f1e;border-top:1px solid rgba(255,255,255,0.1);padding:12px 20px;display:flex;align-items:center;justify-content:space-between;z-index:999;">
<span id="save-status" style="font-size:12px;color:#64748b;">No unsaved changes</span>
<button id="save-btn" onclick="saveAll()" style="background:linear-gradient(135deg,#E4AC38,#FFCC66);color:#1a1a1a;border:none;padding:10px 28px;border-radius:8px;font-weight:700;font-size:14px;cursor:pointer;opacity:0.5;pointer-events:none;transition:opacity 0.2s;">💾 Save All</button>
</div>
<script> <script>
// Auto-save on any input change (debounced 500ms) let hasChanges = false;
let saveTimeout = null; const saveBtn = document.getElementById('save-btn');
const indicator = document.getElementById('save-indicator'); const saveStatus = document.getElementById('save-status');
function markDirty() {
if (!hasChanges) {
hasChanges = true;
saveBtn.style.opacity = '1';
saveBtn.style.pointerEvents = 'auto';
saveStatus.textContent = '⚠️ Unsaved changes';
saveStatus.style.color = '#FBBF24';
}
}
function saveAll() {
saveBtn.textContent = 'Saving...';
saveBtn.style.opacity = '0.6';
function autoSave() {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => {
const form = document.querySelector('form[method="POST"]:not([enctype])'); const form = document.querySelector('form[method="POST"]:not([enctype])');
if (!form) return; if (!form) return;
const formData = new FormData(form); const formData = new FormData(form);
formData.append('save_theme', '1'); formData.append('save_theme', '1');
fetch(window.location.href, { method: 'POST', body: formData }) fetch(window.location.href, { method: 'POST', body: formData })
.then(r => { if (r.ok) showSaved(); }) .then(r => {
.catch(() => {}); if (r.ok) {
}, 500); hasChanges = false;
} saveBtn.textContent = '✓ Saved!';
saveBtn.style.opacity = '0.5';
function showSaved() { saveBtn.style.pointerEvents = 'none';
indicator.style.opacity = '1'; saveStatus.textContent = '✓ All changes saved';
setTimeout(() => { indicator.style.opacity = '0'; }, 1500); saveStatus.style.color = '#34D399';
setTimeout(() => {
saveBtn.textContent = '💾 Save All';
saveStatus.textContent = 'No unsaved changes';
saveStatus.style.color = '#64748b';
}, 2000);
}
})
.catch(() => {
saveBtn.textContent = '❌ Error — retry';
saveBtn.style.opacity = '1';
});
} }
// Attach to all theme inputs // Mark dirty on any input change (NO auto-save)
document.querySelectorAll('input[name^="theme["]').forEach(input => { document.querySelectorAll('input[name^="theme["]').forEach(input => {
input.addEventListener('input', autoSave); input.addEventListener('input', markDirty);
input.addEventListener('change', autoSave); input.addEventListener('change', markDirty);
}); });
// Sync color picker ↔ text input // Sync color picker ↔ text input (no save, just sync visuals)
document.querySelectorAll('.color-row').forEach(row => { document.querySelectorAll('.color-row').forEach(row => {
const colorInput = row.querySelector('input[type="color"]'); const colorInput = row.querySelector('input[type="color"]');
const textInput = row.querySelector('input[type="text"]'); const textInput = row.querySelector('input[type="text"]');
if (colorInput && textInput) { if (colorInput && textInput) {
colorInput.addEventListener('input', () => { textInput.value = colorInput.value; autoSave(); }); colorInput.addEventListener('input', () => { textInput.value = colorInput.value; markDirty(); });
textInput.addEventListener('input', () => { if (/^#[0-9A-Fa-f]{6}$/.test(textInput.value)) { colorInput.value = textInput.value; autoSave(); } }); textInput.addEventListener('input', () => { if (/^#[0-9A-Fa-f]{6}$/.test(textInput.value)) { colorInput.value = textInput.value; markDirty(); } });
} }
}); });
// Warn before leaving with unsaved changes
window.addEventListener('beforeunload', (e) => {
if (hasChanges) { e.preventDefault(); e.returnValue = ''; }
});
</script> </script>
</body> </body>
</html> </html>
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