mirror of
https://github.com/JGH0/Todo-App-Backend.git
synced 2026-06-03 13:28:47 +02:00
Add marketplace
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -126,3 +126,5 @@ _modules/*
|
||||
/phpunit*.xml
|
||||
.env
|
||||
env
|
||||
.claude/
|
||||
.claude/*
|
||||
@@ -6,3 +6,9 @@ use CodeIgniter\Router\RouteCollection;
|
||||
* @var RouteCollection $routes
|
||||
*/
|
||||
$routes->get('/', 'Home::index');
|
||||
<<<<<<< Updated upstream
|
||||
=======
|
||||
$routes->get('/themes', 'ThemeStore::index');
|
||||
$routes->post('/themes/upload', 'ThemeStore::upload');
|
||||
$routes->get('/themes/preview/(:segment)', 'ThemeStore::preview/$1');
|
||||
>>>>>>> Stashed changes
|
||||
|
||||
127
app/Controllers/ThemeStore.php
Normal file
127
app/Controllers/ThemeStore.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\MarketplaceThemeModel;
|
||||
use CodeIgniter\HTTP\Response;
|
||||
|
||||
class ThemeStore extends BaseController
|
||||
{
|
||||
public function index(): string
|
||||
{
|
||||
$model = new MarketplaceThemeModel();
|
||||
$themes = $model->where('is_published', 1)->findAll();
|
||||
|
||||
foreach ($themes as &$theme) {
|
||||
$meta = json_decode($theme['metadata'] ?? '{}', true);
|
||||
$theme['colors'] = $meta['colors'] ?? [];
|
||||
$theme['tags'] = $meta['tags'] ?? [];
|
||||
$theme['vars'] = $meta['vars'] ?? [];
|
||||
}
|
||||
|
||||
return view('theme_store', [
|
||||
'themes' => $themes,
|
||||
'flash_success' => session()->getFlashdata('success'),
|
||||
'flash_error' => session()->getFlashdata('error'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function upload(): Response
|
||||
{
|
||||
$file = $this->request->getFile('theme_css');
|
||||
$displayName = trim($this->request->getPost('display_name') ?? '');
|
||||
$description = trim($this->request->getPost('description') ?? '');
|
||||
|
||||
if ($displayName === '') {
|
||||
return redirect()->to('/themes')->with('error', 'Display name is required.');
|
||||
}
|
||||
|
||||
if (! $file || ! $file->isValid() || $file->hasMoved()) {
|
||||
return redirect()->to('/themes')->with('error', 'Please upload a valid CSS file.');
|
||||
}
|
||||
|
||||
if (strtolower($file->getExtension()) !== 'css') {
|
||||
return redirect()->to('/themes')->with('error', 'Only .css files are allowed.');
|
||||
}
|
||||
|
||||
$slug = strtolower(preg_replace('/[^a-z0-9]+/i', '-', $displayName));
|
||||
$slug = trim($slug, '-');
|
||||
$filename = $slug . '-' . substr(bin2hex(random_bytes(3)), 0, 6) . '.css';
|
||||
|
||||
$file->move(FCPATH . 'themes', $filename, true);
|
||||
|
||||
$model = new MarketplaceThemeModel();
|
||||
$model->insert([
|
||||
'id' => $this->uuid4(),
|
||||
'name' => $slug,
|
||||
'display_name' => $displayName,
|
||||
'description' => $description ?: 'Custom community theme.',
|
||||
'author' => 'Community',
|
||||
'version' => '1.0.0',
|
||||
'thumbnail_url' => null,
|
||||
'download_url' => '/themes/' . $filename,
|
||||
'price' => 0,
|
||||
'is_published' => true,
|
||||
'metadata' => json_encode(['tags' => ['custom', 'community'], 'colors' => []]),
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
return redirect()->to('/themes')->with('success', '"' . esc($displayName) . '" uploaded successfully!');
|
||||
}
|
||||
|
||||
public function preview(string $id): Response
|
||||
{
|
||||
$model = new MarketplaceThemeModel();
|
||||
$theme = $model->find($id);
|
||||
|
||||
if (! $theme) {
|
||||
return $this->response->setStatusCode(404)->setBody('<p style="font-family:sans-serif;padding:2rem">Theme not found.</p>');
|
||||
}
|
||||
|
||||
$distIndex = '/home/came/Nextcloud/arch-work/Projects/Todo-App/dist/index.html';
|
||||
|
||||
if (! file_exists($distIndex)) {
|
||||
return $this->response->setBody(
|
||||
'<html><body style="font-family:sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#0f0f17;color:#94a3b8">'
|
||||
. '<div style="text-align:center"><p>Todo app dist not found.</p></div></body></html>'
|
||||
);
|
||||
}
|
||||
|
||||
$todoHtml = file_get_contents($distIndex);
|
||||
|
||||
// Rewrite asset paths from /assets/ to the public symlink so Apache serves them
|
||||
$assetBase = rtrim(base_url('todo-preview'), '/');
|
||||
$todoHtml = str_replace('="/assets/', '="' . $assetBase . '/assets/', $todoHtml);
|
||||
|
||||
// Build CSS variable overrides from the stored vars map
|
||||
$meta = json_decode($theme['metadata'] ?? '{}', true);
|
||||
$vars = $meta['vars'] ?? [];
|
||||
|
||||
$cssVars = ":root {\n";
|
||||
foreach ($vars as $prop => $value) {
|
||||
$cssVars .= " {$prop}: {$value};\n";
|
||||
}
|
||||
$cssVars .= "}\n";
|
||||
|
||||
// Also inject any raw CSS from the downloaded file (for custom/uploaded themes)
|
||||
$cssPath = FCPATH . ltrim($theme['download_url'], '/');
|
||||
$rawCss = file_exists($cssPath) ? file_get_contents($cssPath) : '';
|
||||
|
||||
$styleTag = "<style>\n/* Theme Store: {$theme['display_name']} */\n{$cssVars}\n{$rawCss}\n</style>";
|
||||
|
||||
$todoHtml = str_replace('</head>', $styleTag . "\n</head>", $todoHtml);
|
||||
|
||||
return $this->response
|
||||
->setHeader('Content-Type', 'text/html; charset=utf-8')
|
||||
->setBody($todoHtml);
|
||||
}
|
||||
|
||||
private function uuid4(): string
|
||||
{
|
||||
$data = random_bytes(16);
|
||||
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
|
||||
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
|
||||
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
||||
}
|
||||
}
|
||||
@@ -8,34 +8,296 @@ class MarketplaceThemesSeeder extends Seeder
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
$this->db->query('SET FOREIGN_KEY_CHECKS=0');
|
||||
$this->db->table('marketplace_themes')->truncate();
|
||||
$this->db->query('SET FOREIGN_KEY_CHECKS=1');
|
||||
|
||||
$data = [
|
||||
[
|
||||
'id' => '550e8400-e29b-41d4-a716-446655440010',
|
||||
'name' => 'default-light',
|
||||
'display_name' => 'Default Light',
|
||||
'description' => 'Clean and simple light theme',
|
||||
'author' => 'System',
|
||||
'version' => '1.0.0',
|
||||
'name' => 'ocean-breeze',
|
||||
'display_name' => 'Ocean Breeze',
|
||||
'description' => 'A refreshing light theme inspired by the open sea. Soft teals and ocean blues create a calm, productive workspace that\'s easy on the eyes during long work sessions.',
|
||||
'author' => 'ThemeForge',
|
||||
'version' => '1.2.0',
|
||||
'thumbnail_url' => null,
|
||||
'download_url' => '/themes/default-light.zip',
|
||||
'download_url' => '/themes/ocean-breeze.css',
|
||||
'price' => 0,
|
||||
'is_published' => true,
|
||||
'metadata' => json_encode(['tags' => ['light', 'clean']]),
|
||||
'metadata' => json_encode([
|
||||
'tags' => ['light', 'blue', 'calm', 'minimal'],
|
||||
'colors' => [
|
||||
'Primary' => '#0077B6',
|
||||
'Secondary' => '#00B4D8',
|
||||
'Background' => '#E0F4FF',
|
||||
'Surface' => '#FFFFFF',
|
||||
'Text' => '#1A2B3C',
|
||||
'Accent' => '#48CAE4',
|
||||
],
|
||||
'vars' => [
|
||||
'--bg' => '#E0F4FF',
|
||||
'--surface' => '#FFFFFF',
|
||||
'--surface-strong' => '#FFFFFF',
|
||||
'--surface-muted' => '#F0F9FF',
|
||||
'--border' => '#BAE0F2',
|
||||
'--line' => '#90C8E0',
|
||||
'--text' => '#1A2B3C',
|
||||
'--text-muted' => '#4A6B7A',
|
||||
'--text-strong' => '#0D1B26',
|
||||
'--accent' => '#0077B6',
|
||||
'--accent-text' => '#FFFFFF',
|
||||
'--accent-soft' => '#CCE9F5',
|
||||
'--sidebar-bg' => '#FFFFFF',
|
||||
'--sidebar-border' => '#BAE0F2',
|
||||
'--sidebar-text' => '#1A2B3C',
|
||||
'--sidebar-text-muted' => '#4A6B7A',
|
||||
'--input-bg' => '#FFFFFF',
|
||||
'--input-border' => '#BAE0F2',
|
||||
'--modal-bg' => '#FFFFFF',
|
||||
'--chip' => '#C8E8F0',
|
||||
'--success' => '#D4F0E4',
|
||||
],
|
||||
]),
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
],
|
||||
[
|
||||
'id' => '550e8400-e29b-41d4-a716-446655440011',
|
||||
'name' => 'default-dark',
|
||||
'display_name' => 'Default Dark',
|
||||
'description' => 'Dark theme for night owls',
|
||||
'author' => 'System',
|
||||
'version' => '1.0.0',
|
||||
'name' => 'midnight-void',
|
||||
'display_name' => 'Midnight Void',
|
||||
'description' => 'Deep space dark theme for night owls and late-night coders. Rich dark purples and blues with vibrant neon accents give this theme a premium, modern feel.',
|
||||
'author' => 'ThemeForge',
|
||||
'version' => '2.0.1',
|
||||
'thumbnail_url' => null,
|
||||
'download_url' => '/themes/default-dark.zip',
|
||||
'download_url' => '/themes/midnight-void.css',
|
||||
'price' => 0,
|
||||
'is_published' => true,
|
||||
'metadata' => json_encode(['tags' => ['dark', 'night']]),
|
||||
'metadata' => json_encode([
|
||||
'tags' => ['dark', 'purple', 'neon', 'night'],
|
||||
'colors' => [
|
||||
'Primary' => '#7C3AED',
|
||||
'Secondary' => '#A78BFA',
|
||||
'Background' => '#0D0D1A',
|
||||
'Surface' => '#1A1A2E',
|
||||
'Text' => '#E2E8F0',
|
||||
'Accent' => '#F472B6',
|
||||
],
|
||||
'vars' => [
|
||||
'--bg' => '#0D0D1A',
|
||||
'--surface' => '#1A1A2E',
|
||||
'--surface-strong' => '#222234',
|
||||
'--surface-muted' => '#121220',
|
||||
'--border' => '#2A2A44',
|
||||
'--line' => '#333350',
|
||||
'--text' => '#E2E8F0',
|
||||
'--text-muted' => '#94A3B8',
|
||||
'--text-strong' => '#F1F5F9',
|
||||
'--accent' => '#7C3AED',
|
||||
'--accent-text' => '#FFFFFF',
|
||||
'--accent-soft' => '#2D1A5E',
|
||||
'--sidebar-bg' => '#16162A',
|
||||
'--sidebar-border' => '#2A2A44',
|
||||
'--sidebar-text' => '#E2E8F0',
|
||||
'--sidebar-text-muted' => '#94A3B8',
|
||||
'--input-bg' => '#0D0D1A',
|
||||
'--input-border' => '#2A2A44',
|
||||
'--modal-bg' => '#1A1A2E',
|
||||
'--chip' => '#2A2A44',
|
||||
'--success' => '#0D2A1A',
|
||||
],
|
||||
]),
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
],
|
||||
[
|
||||
'id' => '550e8400-e29b-41d4-a716-446655440012',
|
||||
'name' => 'forest-grove',
|
||||
'display_name' => 'Forest Grove',
|
||||
'description' => 'Earthy greens and warm neutrals bring the tranquility of a woodland retreat to your workspace. A grounding, nature-inspired theme designed for focused productivity.',
|
||||
'author' => 'NaturePalette',
|
||||
'version' => '1.0.5',
|
||||
'thumbnail_url' => null,
|
||||
'download_url' => '/themes/forest-grove.css',
|
||||
'price' => 0,
|
||||
'is_published' => true,
|
||||
'metadata' => json_encode([
|
||||
'tags' => ['light', 'green', 'earthy', 'nature'],
|
||||
'colors' => [
|
||||
'Primary' => '#2D6A4F',
|
||||
'Secondary' => '#52B788',
|
||||
'Background' => '#F0F7EE',
|
||||
'Surface' => '#FFFFFF',
|
||||
'Text' => '#1B2E22',
|
||||
'Accent' => '#B7E4C7',
|
||||
],
|
||||
'vars' => [
|
||||
'--bg' => '#F0F7EE',
|
||||
'--surface' => '#FFFFFF',
|
||||
'--surface-strong' => '#FFFFFF',
|
||||
'--surface-muted' => '#F5FAF4',
|
||||
'--border' => '#C0DACB',
|
||||
'--line' => '#A0C4B0',
|
||||
'--text' => '#1B2E22',
|
||||
'--text-muted' => '#527A62',
|
||||
'--text-strong' => '#0D1F14',
|
||||
'--accent' => '#2D6A4F',
|
||||
'--accent-text' => '#FFFFFF',
|
||||
'--accent-soft' => '#C0E8D4',
|
||||
'--sidebar-bg' => '#FFFFFF',
|
||||
'--sidebar-border' => '#C0DACB',
|
||||
'--sidebar-text' => '#1B2E22',
|
||||
'--sidebar-text-muted' => '#527A62',
|
||||
'--input-bg' => '#FFFFFF',
|
||||
'--input-border' => '#C0DACB',
|
||||
'--modal-bg' => '#FFFFFF',
|
||||
'--chip' => '#B8E0C8',
|
||||
'--success' => '#CCF0DC',
|
||||
],
|
||||
]),
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
],
|
||||
[
|
||||
'id' => '550e8400-e29b-41d4-a716-446655440013',
|
||||
'name' => 'sunset-ember',
|
||||
'display_name' => 'Sunset Ember',
|
||||
'description' => 'Warm oranges, deep reds, and golden highlights capture the magic of a perfect sunset. This vibrant theme adds energy and warmth to every interaction.',
|
||||
'author' => 'ChromaCraft',
|
||||
'version' => '1.1.2',
|
||||
'thumbnail_url' => null,
|
||||
'download_url' => '/themes/sunset-ember.css',
|
||||
'price' => 0,
|
||||
'is_published' => true,
|
||||
'metadata' => json_encode([
|
||||
'tags' => ['warm', 'orange', 'vibrant', 'sunset'],
|
||||
'colors' => [
|
||||
'Primary' => '#D62828',
|
||||
'Secondary' => '#F77F00',
|
||||
'Background' => '#FFF5E4',
|
||||
'Surface' => '#FFFFFF',
|
||||
'Text' => '#2D1B00',
|
||||
'Accent' => '#FCBF49',
|
||||
],
|
||||
'vars' => [
|
||||
'--bg' => '#FFF5E4',
|
||||
'--surface' => '#FFFFFF',
|
||||
'--surface-strong' => '#FFFFFF',
|
||||
'--surface-muted' => '#FFF8F0',
|
||||
'--border' => '#F0D0A8',
|
||||
'--line' => '#E0B880',
|
||||
'--text' => '#2D1B00',
|
||||
'--text-muted' => '#8A6040',
|
||||
'--text-strong' => '#1A0A00',
|
||||
'--accent' => '#D62828',
|
||||
'--accent-text' => '#FFFFFF',
|
||||
'--accent-soft' => '#FFE0CC',
|
||||
'--sidebar-bg' => '#FFFFFF',
|
||||
'--sidebar-border' => '#F0D0A8',
|
||||
'--sidebar-text' => '#2D1B00',
|
||||
'--sidebar-text-muted' => '#8A6040',
|
||||
'--input-bg' => '#FFFFFF',
|
||||
'--input-border' => '#F0D0A8',
|
||||
'--modal-bg' => '#FFFFFF',
|
||||
'--chip' => '#F8D8B0',
|
||||
'--success' => '#DDFADC',
|
||||
],
|
||||
]),
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
],
|
||||
[
|
||||
'id' => '550e8400-e29b-41d4-a716-446655440014',
|
||||
'name' => 'arctic-frost',
|
||||
'display_name' => 'Arctic Frost',
|
||||
'description' => 'Ultra-clean whites and icy blues inspired by frozen tundras. A minimalist theme that maximises clarity and focus with crisp contrast and breathable spacing.',
|
||||
'author' => 'MinimalStudio',
|
||||
'version' => '3.0.0',
|
||||
'thumbnail_url' => null,
|
||||
'download_url' => '/themes/arctic-frost.css',
|
||||
'price' => 0,
|
||||
'is_published' => true,
|
||||
'metadata' => json_encode([
|
||||
'tags' => ['light', 'minimal', 'clean', 'ice'],
|
||||
'colors' => [
|
||||
'Primary' => '#2176AE',
|
||||
'Secondary' => '#57C4E5',
|
||||
'Background' => '#F8FBFF',
|
||||
'Surface' => '#FFFFFF',
|
||||
'Text' => '#1C2B3A',
|
||||
'Accent' => '#A8DADC',
|
||||
],
|
||||
'vars' => [
|
||||
'--bg' => '#F8FBFF',
|
||||
'--surface' => '#FFFFFF',
|
||||
'--surface-strong' => '#FFFFFF',
|
||||
'--surface-muted' => '#F0F5FC',
|
||||
'--border' => '#C0D4E8',
|
||||
'--line' => '#A0BCDA',
|
||||
'--text' => '#1C2B3A',
|
||||
'--text-muted' => '#4E6478',
|
||||
'--text-strong' => '#0D1B2A',
|
||||
'--accent' => '#2176AE',
|
||||
'--accent-text' => '#FFFFFF',
|
||||
'--accent-soft' => '#CCE0F0',
|
||||
'--sidebar-bg' => '#FFFFFF',
|
||||
'--sidebar-border' => '#C0D4E8',
|
||||
'--sidebar-text' => '#1C2B3A',
|
||||
'--sidebar-text-muted' => '#4E6478',
|
||||
'--input-bg' => '#FFFFFF',
|
||||
'--input-border' => '#C0D4E8',
|
||||
'--modal-bg' => '#FFFFFF',
|
||||
'--chip' => '#B8D4E8',
|
||||
'--success' => '#D4F0E4',
|
||||
],
|
||||
]),
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
],
|
||||
[
|
||||
'id' => '550e8400-e29b-41d4-a716-446655440015',
|
||||
'name' => 'obsidian-rose',
|
||||
'display_name' => 'Obsidian Rose',
|
||||
'description' => 'A sophisticated dark theme blending deep charcoal blacks with rose gold accents. Elegant and bold, this theme is built for those who want style without sacrificing readability.',
|
||||
'author' => 'ChromaCraft',
|
||||
'version' => '1.3.0',
|
||||
'thumbnail_url' => null,
|
||||
'download_url' => '/themes/obsidian-rose.css',
|
||||
'price' => 0,
|
||||
'is_published' => true,
|
||||
'metadata' => json_encode([
|
||||
'tags' => ['dark', 'elegant', 'rose', 'premium'],
|
||||
'colors' => [
|
||||
'Primary' => '#C9184A',
|
||||
'Secondary' => '#FF4D6D',
|
||||
'Background' => '#0A0A0F',
|
||||
'Surface' => '#1C1C28',
|
||||
'Text' => '#F1E3E4',
|
||||
'Accent' => '#B5838D',
|
||||
],
|
||||
'vars' => [
|
||||
'--bg' => '#0A0A0F',
|
||||
'--surface' => '#1C1C28',
|
||||
'--surface-strong' => '#242430',
|
||||
'--surface-muted' => '#14141E',
|
||||
'--border' => '#2A2A38',
|
||||
'--line' => '#383848',
|
||||
'--text' => '#F1E3E4',
|
||||
'--text-muted' => '#B5939A',
|
||||
'--text-strong' => '#FAF0F1',
|
||||
'--accent' => '#C9184A',
|
||||
'--accent-text' => '#FFFFFF',
|
||||
'--accent-soft' => '#3D0A1A',
|
||||
'--sidebar-bg' => '#161620',
|
||||
'--sidebar-border' => '#2A2A38',
|
||||
'--sidebar-text' => '#F1E3E4',
|
||||
'--sidebar-text-muted' => '#B5939A',
|
||||
'--input-bg' => '#0A0A0F',
|
||||
'--input-border' => '#2A2A38',
|
||||
'--modal-bg' => '#1C1C28',
|
||||
'--chip' => '#2A2030',
|
||||
'--success' => '#0A2016',
|
||||
],
|
||||
]),
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
],
|
||||
|
||||
767
app/Views/theme_store.php
Normal file
767
app/Views/theme_store.php
Normal file
@@ -0,0 +1,767 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Theme Store</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #0f0f17;
|
||||
color: #e2e8f0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
header {
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
padding: 60px 24px 50px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse at 50% 0%, rgba(124,58,237,0.15) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
header h1 {
|
||||
font-size: clamp(2rem, 5vw, 3.2rem);
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.03em;
|
||||
background: linear-gradient(135deg, #a78bfa, #60a5fa, #f472b6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
position: relative;
|
||||
}
|
||||
header p {
|
||||
margin-top: 12px;
|
||||
color: #94a3b8;
|
||||
font-size: 1.1rem;
|
||||
position: relative;
|
||||
}
|
||||
.header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
margin-top: 20px;
|
||||
position: relative;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.header-badge {
|
||||
background: rgba(124,58,237,0.2);
|
||||
border: 1px solid rgba(124,58,237,0.4);
|
||||
color: #a78bfa;
|
||||
padding: 4px 14px;
|
||||
border-radius: 100px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.btn-upload-header {
|
||||
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 8px 20px;
|
||||
border-radius: 100px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: opacity 0.15s, transform 0.15s;
|
||||
}
|
||||
.btn-upload-header:hover { opacity: 0.88; transform: translateY(-1px); }
|
||||
|
||||
/* ── Flash messages ── */
|
||||
.flash {
|
||||
max-width: 680px;
|
||||
margin: 24px auto 0;
|
||||
padding: 12px 18px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.flash-success { background: rgba(52,211,153,0.12); border: 1px solid rgba(52,211,153,0.3); color: #34d399; }
|
||||
.flash-error { background: rgba(248,113,113,0.12); border: 1px solid rgba(248,113,113,0.3); color: #f87171; }
|
||||
|
||||
/* ── Grid ── */
|
||||
.grid-wrapper {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px 80px;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
/* ── Card ── */
|
||||
.card {
|
||||
background: #1a1a2e;
|
||||
border: 1px solid rgba(255,255,255,0.07);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 48px rgba(0,0,0,0.5);
|
||||
border-color: rgba(167,139,250,0.3);
|
||||
}
|
||||
.card-preview {
|
||||
height: 90px;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card-preview .swatch { flex: 1; transition: flex 0.3s ease; }
|
||||
.card:hover .card-preview .swatch { flex: 1.4; }
|
||||
|
||||
.card-body { padding: 22px; }
|
||||
.card-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.card-name { font-size: 1.15rem; font-weight: 700; color: #f1f5f9; }
|
||||
.card-version {
|
||||
font-size: 0.75rem; color: #64748b;
|
||||
background: rgba(255,255,255,0.05);
|
||||
padding: 2px 8px; border-radius: 100px;
|
||||
}
|
||||
.card-author { font-size: 0.8rem; color: #94a3b8; margin-bottom: 10px; }
|
||||
.card-desc {
|
||||
font-size: 0.88rem; color: #94a3b8; line-height: 1.55;
|
||||
margin-bottom: 14px;
|
||||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
||||
}
|
||||
.card-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 18px; }
|
||||
.tag {
|
||||
background: rgba(167,139,250,0.1);
|
||||
border: 1px solid rgba(167,139,250,0.25);
|
||||
color: #a78bfa;
|
||||
font-size: 0.72rem; padding: 2px 9px; border-radius: 100px; font-weight: 500;
|
||||
}
|
||||
.card-actions { display: flex; gap: 10px; }
|
||||
.btn {
|
||||
flex: 1; padding: 10px 0; border-radius: 8px; border: none;
|
||||
cursor: pointer; font-size: 0.85rem; font-weight: 600;
|
||||
transition: opacity 0.15s, transform 0.15s;
|
||||
}
|
||||
.btn:hover { opacity: 0.88; transform: translateY(-1px); }
|
||||
.btn-download {
|
||||
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||
color: #fff; text-decoration: none;
|
||||
display: flex; align-items: center; justify-content: center; gap: 6px;
|
||||
}
|
||||
.btn-details {
|
||||
background: rgba(255,255,255,0.06);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.btn-details:hover { background: rgba(255,255,255,0.1); }
|
||||
|
||||
/* ── Backdrop shared ── */
|
||||
.modal-backdrop {
|
||||
display: none;
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.8);
|
||||
backdrop-filter: blur(6px);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
.modal-backdrop.open { display: flex; }
|
||||
|
||||
/* ── Details Modal ── */
|
||||
.details-modal {
|
||||
background: #1a1a2e;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 20px;
|
||||
width: 95vw;
|
||||
max-width: 1100px;
|
||||
height: 88vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: popIn 0.22s cubic-bezier(0.34,1.56,0.64,1);
|
||||
overflow: hidden;
|
||||
}
|
||||
@keyframes popIn {
|
||||
from { opacity: 0; transform: scale(0.92) translateY(20px); }
|
||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||
}
|
||||
|
||||
/* colour stripe at top */
|
||||
.modal-preview-stripe {
|
||||
height: 80px;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.modal-preview-stripe .swatch { flex: 1; }
|
||||
|
||||
/* tab bar */
|
||||
.modal-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||
background: rgba(255,255,255,0.02);
|
||||
flex-shrink: 0;
|
||||
padding: 0 24px;
|
||||
}
|
||||
.modal-tab {
|
||||
padding: 12px 20px;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
background: none;
|
||||
border-top: none; border-left: none; border-right: none;
|
||||
display: flex; align-items: center; gap: 7px;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
.modal-tab:hover { color: #94a3b8; }
|
||||
.modal-tab.active { color: #a78bfa; border-bottom-color: #7c3aed; }
|
||||
|
||||
/* tab panels */
|
||||
.modal-body {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.tab-panel {
|
||||
display: none;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.tab-panel.active { display: block; }
|
||||
|
||||
/* details panel */
|
||||
.details-panel { padding: 28px; }
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.modal-title { font-size: 1.4rem; font-weight: 800; color: #f1f5f9; }
|
||||
.modal-close {
|
||||
background: rgba(255,255,255,0.06);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
color: #94a3b8;
|
||||
border-radius: 8px; width: 34px; height: 34px;
|
||||
cursor: pointer; font-size: 1.1rem;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: background 0.15s; flex-shrink: 0;
|
||||
}
|
||||
.modal-close:hover { background: rgba(255,255,255,0.12); color: #fff; }
|
||||
.modal-meta { font-size: 0.82rem; color: #64748b; margin-bottom: 16px; }
|
||||
.modal-desc { font-size: 0.92rem; color: #94a3b8; line-height: 1.65; margin-bottom: 22px; }
|
||||
.section-label {
|
||||
font-size: 0.75rem; font-weight: 700;
|
||||
text-transform: uppercase; letter-spacing: 0.08em;
|
||||
color: #64748b; margin-bottom: 10px;
|
||||
}
|
||||
.colors-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
.color-chip { border-radius: 10px; overflow: hidden; border: 1px solid rgba(255,255,255,0.07); }
|
||||
.color-chip-swatch { height: 48px; }
|
||||
.color-chip-info { padding: 6px 8px; background: rgba(255,255,255,0.03); }
|
||||
.color-chip-name { font-size: 0.7rem; color: #94a3b8; display: block; }
|
||||
.color-chip-hex { font-size: 0.75rem; font-weight: 600; color: #e2e8f0; font-family: 'SF Mono', monospace; }
|
||||
.modal-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 24px; }
|
||||
.modal-actions { display: flex; gap: 10px; }
|
||||
.btn-download-lg {
|
||||
flex: 1; padding: 13px; font-size: 0.95rem; border-radius: 10px;
|
||||
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||
color: #fff; font-weight: 700; text-decoration: none;
|
||||
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
border: none; cursor: pointer;
|
||||
transition: opacity 0.15s, transform 0.15s;
|
||||
}
|
||||
.btn-download-lg:hover { opacity: 0.88; transform: translateY(-1px); }
|
||||
|
||||
/* preview panel */
|
||||
.preview-panel {
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
}
|
||||
.preview-panel.active { display: flex; }
|
||||
.preview-toolbar {
|
||||
padding: 10px 16px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.preview-dot { width: 10px; height: 10px; border-radius: 50%; }
|
||||
.preview-url {
|
||||
flex: 1;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
border-radius: 6px;
|
||||
padding: 4px 12px;
|
||||
font-size: 0.78rem;
|
||||
color: #64748b;
|
||||
font-family: monospace;
|
||||
}
|
||||
.preview-iframe-wrap {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.preview-iframe-wrap iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
.preview-loading {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #0f0f17;
|
||||
color: #94a3b8;
|
||||
gap: 14px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.spinner {
|
||||
width: 32px; height: 32px;
|
||||
border: 3px solid rgba(167,139,250,0.2);
|
||||
border-top-color: #7c3aed;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* ── Upload Modal ── */
|
||||
.upload-modal {
|
||||
background: #1a1a2e;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 20px;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
animation: popIn 0.22s cubic-bezier(0.34,1.56,0.64,1);
|
||||
overflow: hidden;
|
||||
}
|
||||
.upload-header {
|
||||
padding: 24px 28px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.upload-title { font-size: 1.25rem; font-weight: 700; color: #f1f5f9; }
|
||||
.upload-body { padding: 0 28px 28px; }
|
||||
.field { margin-bottom: 18px; }
|
||||
.field label {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 6px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.field input[type="text"],
|
||||
.field textarea {
|
||||
width: 100%;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
color: #e2e8f0;
|
||||
font-size: 0.9rem;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.field input[type="text"]:focus,
|
||||
.field textarea:focus {
|
||||
border-color: rgba(124,58,237,0.5);
|
||||
}
|
||||
.field textarea { resize: vertical; min-height: 80px; }
|
||||
.file-drop {
|
||||
border: 2px dashed rgba(124,58,237,0.3);
|
||||
border-radius: 10px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
position: relative;
|
||||
color: #64748b;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
.file-drop:hover,
|
||||
.file-drop.drag-over {
|
||||
border-color: rgba(124,58,237,0.7);
|
||||
background: rgba(124,58,237,0.05);
|
||||
}
|
||||
.file-drop input[type="file"] {
|
||||
position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%;
|
||||
}
|
||||
.file-drop-icon { font-size: 1.8rem; margin-bottom: 8px; display: block; }
|
||||
.file-drop strong { color: #a78bfa; }
|
||||
.file-name-preview {
|
||||
margin-top: 8px;
|
||||
font-size: 0.8rem;
|
||||
color: #34d399;
|
||||
display: none;
|
||||
}
|
||||
.btn-submit {
|
||||
width: 100%;
|
||||
padding: 13px;
|
||||
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s, transform 0.15s;
|
||||
}
|
||||
.btn-submit:hover { opacity: 0.88; transform: translateY(-1px); }
|
||||
|
||||
/* ── Footer ── */
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 32px 24px;
|
||||
color: #334155;
|
||||
font-size: 0.82rem;
|
||||
border-top: 1px solid rgba(255,255,255,0.04);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar { width: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(167,139,250,0.3); border-radius: 3px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<h1>Theme Store</h1>
|
||||
<p>Beautiful, ready-to-use themes for your application</p>
|
||||
<div class="header-row">
|
||||
<span class="header-badge"><?= count($themes) ?> free themes</span>
|
||||
<button class="btn-upload-header" onclick="openUploadModal()">
|
||||
<span>+</span> Upload Theme
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<?php if ($flash_success): ?>
|
||||
<div style="max-width:1200px;margin:0 auto;padding:0 24px">
|
||||
<div class="flash flash-success">✓ <?= esc($flash_success) ?></div>
|
||||
</div>
|
||||
<?php elseif ($flash_error): ?>
|
||||
<div style="max-width:1200px;margin:0 auto;padding:0 24px">
|
||||
<div class="flash flash-error">⚠ <?= esc($flash_error) ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="grid-wrapper">
|
||||
<div class="grid">
|
||||
<?php foreach ($themes as $theme): ?>
|
||||
<div class="card">
|
||||
<div class="card-preview">
|
||||
<?php foreach (array_values($theme['colors']) as $hex): ?>
|
||||
<div class="swatch" style="background:<?= esc($hex) ?>"></div>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($theme['colors'])): ?>
|
||||
<div class="swatch" style="background:linear-gradient(135deg,#7c3aed,#f472b6)"></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-meta">
|
||||
<span class="card-name"><?= esc($theme['display_name']) ?></span>
|
||||
<span class="card-version">v<?= esc($theme['version']) ?></span>
|
||||
</div>
|
||||
<div class="card-author">by <?= esc($theme['author']) ?></div>
|
||||
<p class="card-desc"><?= esc($theme['description']) ?></p>
|
||||
<div class="card-tags">
|
||||
<?php foreach ($theme['tags'] as $tag): ?>
|
||||
<span class="tag"><?= esc($tag) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a class="btn btn-download" href="<?= esc($theme['download_url']) ?>" download>
|
||||
⇓ Download
|
||||
</a>
|
||||
<button class="btn btn-details"
|
||||
onclick='openDetailsModal(<?= json_encode([
|
||||
"id" => $theme["id"],
|
||||
"display_name" => $theme["display_name"],
|
||||
"description" => $theme["description"],
|
||||
"author" => $theme["author"],
|
||||
"version" => $theme["version"],
|
||||
"download_url" => $theme["download_url"],
|
||||
"colors" => $theme["colors"],
|
||||
"tags" => $theme["tags"],
|
||||
"vars" => $theme["vars"],
|
||||
]) ?>)'>
|
||||
Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Details Modal ── -->
|
||||
<div class="modal-backdrop" id="details-backdrop" onclick="closeDetailsOnBackdrop(event)">
|
||||
<div class="details-modal" id="details-modal">
|
||||
|
||||
<div class="modal-preview-stripe" id="dm-stripe"></div>
|
||||
|
||||
<div class="modal-tabs">
|
||||
<button class="modal-tab active" id="tab-details" onclick="switchTab('details')">
|
||||
✎ Details
|
||||
</button>
|
||||
<button class="modal-tab" id="tab-preview" onclick="switchTab('preview')">
|
||||
▶ Live Preview
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<!-- Details panel -->
|
||||
<div class="tab-panel active" id="panel-details">
|
||||
<div class="details-panel">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" id="dm-title"></span>
|
||||
<button class="modal-close" onclick="closeDetailsModal()">✕</button>
|
||||
</div>
|
||||
<div class="modal-meta" id="dm-meta"></div>
|
||||
<p class="modal-desc" id="dm-desc"></p>
|
||||
|
||||
<div id="dm-colors-section">
|
||||
<div class="section-label">Colour Palette</div>
|
||||
<div class="colors-grid" id="dm-colors"></div>
|
||||
</div>
|
||||
|
||||
<div class="section-label" style="margin-bottom:10px">Tags</div>
|
||||
<div class="modal-tags" id="dm-tags"></div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<a class="btn-download-lg" id="dm-download" href="#" download>
|
||||
⇓ Download Theme
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview panel -->
|
||||
<div class="tab-panel" id="panel-preview" style="height:100%">
|
||||
<div class="preview-panel" id="preview-panel-inner">
|
||||
<div class="preview-toolbar">
|
||||
<div class="preview-dot" style="background:#ff5f57"></div>
|
||||
<div class="preview-dot" style="background:#febc2e"></div>
|
||||
<div class="preview-dot" style="background:#28c840"></div>
|
||||
<div class="preview-url" id="preview-url">localhost:5173 — with theme applied</div>
|
||||
</div>
|
||||
<div class="preview-iframe-wrap">
|
||||
<div class="preview-loading" id="preview-loading">
|
||||
<div class="spinner"></div>
|
||||
<span>Loading preview…</span>
|
||||
</div>
|
||||
<iframe id="preview-iframe" title="Theme Preview" onload="iframeLoaded()"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- .modal-body -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Upload Modal ── -->
|
||||
<div class="modal-backdrop" id="upload-backdrop" onclick="closeUploadOnBackdrop(event)">
|
||||
<div class="upload-modal">
|
||||
<div class="upload-header">
|
||||
<span class="upload-title">Upload Custom Theme</span>
|
||||
<button class="modal-close" onclick="closeUploadModal()">✕</button>
|
||||
</div>
|
||||
<div class="upload-body">
|
||||
<form method="post" action="/themes/upload" enctype="multipart/form-data">
|
||||
|
||||
<div class="field">
|
||||
<label>Display Name *</label>
|
||||
<input type="text" name="display_name" placeholder="e.g. Neon Sunset" required>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Description</label>
|
||||
<textarea name="description" placeholder="Describe your theme's mood and colours…"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>CSS File *</label>
|
||||
<div class="file-drop" id="file-drop">
|
||||
<input type="file" name="theme_css" accept=".css" required onchange="previewFileName(this)">
|
||||
<span class="file-drop-icon">💾</span>
|
||||
<div>Drop your <strong>.css</strong> file here<br>or <strong>click to browse</strong></div>
|
||||
<div class="file-name-preview" id="file-name-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-submit">↑ Upload Theme</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
© <?= date('Y') ?> Theme Store — All themes are free to use
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
let currentThemeId = null;
|
||||
let currentThemeVars = {};
|
||||
let previewLoaded = false;
|
||||
|
||||
/* ── Details modal ── */
|
||||
function openDetailsModal(theme) {
|
||||
currentThemeId = theme.id;
|
||||
currentThemeVars = theme.vars || {};
|
||||
previewLoaded = false;
|
||||
|
||||
const colors = theme.colors || {};
|
||||
const tags = theme.tags || [];
|
||||
|
||||
document.getElementById('dm-title').textContent = theme.display_name;
|
||||
document.getElementById('dm-meta').textContent = 'by ' + theme.author + ' · v' + theme.version;
|
||||
document.getElementById('dm-desc').textContent = theme.description;
|
||||
document.getElementById('dm-download').href = theme.download_url;
|
||||
document.getElementById('preview-url').textContent = 'localhost — ' + theme.display_name + ' applied';
|
||||
|
||||
// stripe
|
||||
const stripe = document.getElementById('dm-stripe');
|
||||
const colorValues = Object.values(colors);
|
||||
stripe.innerHTML = colorValues.length
|
||||
? colorValues.map(c => `<div class="swatch" style="background:${c}"></div>`).join('')
|
||||
: '<div class="swatch" style="background:linear-gradient(135deg,#7c3aed,#f472b6)"></div>';
|
||||
|
||||
// colour chips
|
||||
const colorsSection = document.getElementById('dm-colors-section');
|
||||
const grid = document.getElementById('dm-colors');
|
||||
if (Object.keys(colors).length) {
|
||||
colorsSection.style.display = '';
|
||||
grid.innerHTML = Object.entries(colors).map(([name, hex]) => `
|
||||
<div class="color-chip">
|
||||
<div class="color-chip-swatch" style="background:${hex}"></div>
|
||||
<div class="color-chip-info">
|
||||
<span class="color-chip-name">${name}</span>
|
||||
<span class="color-chip-hex">${hex}</span>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
} else {
|
||||
colorsSection.style.display = 'none';
|
||||
}
|
||||
|
||||
// tags
|
||||
document.getElementById('dm-tags').innerHTML =
|
||||
tags.length ? tags.map(t => `<span class="tag">${t}</span>`).join('') : '<span style="color:#475569;font-size:0.82rem">No tags</span>';
|
||||
|
||||
// reset to details tab
|
||||
switchTab('details');
|
||||
|
||||
document.getElementById('details-backdrop').classList.add('open');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeDetailsModal() {
|
||||
document.getElementById('details-backdrop').classList.remove('open');
|
||||
document.getElementById('preview-iframe').src = '';
|
||||
document.getElementById('preview-loading').style.display = 'flex';
|
||||
currentThemeVars = {};
|
||||
previewLoaded = false;
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
function closeDetailsOnBackdrop(e) {
|
||||
if (e.target === document.getElementById('details-backdrop')) closeDetailsModal();
|
||||
}
|
||||
|
||||
/* ── Tabs ── */
|
||||
function switchTab(name) {
|
||||
document.querySelectorAll('.modal-tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
||||
|
||||
document.getElementById('tab-' + name).classList.add('active');
|
||||
document.getElementById('panel-' + name).classList.add('active');
|
||||
|
||||
if (name === 'preview') {
|
||||
document.getElementById('preview-panel-inner').classList.add('active');
|
||||
if (!previewLoaded && currentThemeId) {
|
||||
document.getElementById('preview-loading').style.display = 'flex';
|
||||
const encoded = btoa(JSON.stringify(currentThemeVars));
|
||||
document.getElementById('preview-iframe').src = 'http://localhost:5173/?__theme=' + encoded;
|
||||
}
|
||||
} else {
|
||||
document.getElementById('preview-panel-inner').classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
function iframeLoaded() {
|
||||
previewLoaded = true;
|
||||
document.getElementById('preview-loading').style.display = 'none';
|
||||
}
|
||||
|
||||
/* ── Upload modal ── */
|
||||
function openUploadModal() {
|
||||
document.getElementById('upload-backdrop').classList.add('open');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
function closeUploadModal() {
|
||||
document.getElementById('upload-backdrop').classList.remove('open');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
function closeUploadOnBackdrop(e) {
|
||||
if (e.target === document.getElementById('upload-backdrop')) closeUploadModal();
|
||||
}
|
||||
|
||||
function previewFileName(input) {
|
||||
const el = document.getElementById('file-name-preview');
|
||||
if (input.files && input.files[0]) {
|
||||
el.textContent = '✓ ' + input.files[0].name;
|
||||
el.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Drag-over styling
|
||||
const drop = document.getElementById('file-drop');
|
||||
drop.addEventListener('dragover', () => drop.classList.add('drag-over'));
|
||||
drop.addEventListener('dragleave', () => drop.classList.remove('drag-over'));
|
||||
drop.addEventListener('drop', () => drop.classList.remove('drag-over'));
|
||||
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') { closeDetailsModal(); closeUploadModal(); } });
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
12
env.example
12
env.example
@@ -30,13 +30,13 @@
|
||||
# DATABASE
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
# database.default.hostname = localhost
|
||||
# database.default.database = ci4
|
||||
# database.default.username = root
|
||||
# database.default.password = root
|
||||
# database.default.DBDriver = MySQLi
|
||||
database.default.hostname = localhost
|
||||
database.default.database = ci4
|
||||
database.default.username = root
|
||||
database.default.password = root
|
||||
database.default.DBDriver = MySQLi
|
||||
# database.default.DBPrefix =
|
||||
# database.default.port = 3306
|
||||
database.default.port = 3306
|
||||
|
||||
# If you use MySQLi as tests, first update the values of Config\Database::$tests.
|
||||
# database.tests.hostname = localhost
|
||||
|
||||
16
public/themes/arctic-frost.css
Normal file
16
public/themes/arctic-frost.css
Normal file
@@ -0,0 +1,16 @@
|
||||
/* Arctic Frost Theme — MinimalStudio v3.0.0 */
|
||||
:root {
|
||||
--color-primary: #2176AE;
|
||||
--color-secondary: #57C4E5;
|
||||
--color-background: #F8FBFF;
|
||||
--color-surface: #FFFFFF;
|
||||
--color-text: #1C2B3A;
|
||||
--color-accent: #A8DADC;
|
||||
}
|
||||
|
||||
body { background-color: var(--color-background); color: var(--color-text); font-family: system-ui, sans-serif; }
|
||||
a, .link { color: var(--color-primary); }
|
||||
.btn-primary { background: var(--color-primary); color: #fff; border: none; border-radius: 6px; padding: 8px 18px; cursor: pointer; }
|
||||
.btn-primary:hover { background: var(--color-secondary); }
|
||||
.card { background: var(--color-surface); border-radius: 10px; box-shadow: 0 1px 6px rgba(33,118,174,0.08); padding: 20px; border: 1px solid #DDE8F0; }
|
||||
.tag { background: var(--color-accent); color: var(--color-text); border-radius: 4px; padding: 2px 8px; font-size: 0.75rem; }
|
||||
16
public/themes/forest-grove.css
Normal file
16
public/themes/forest-grove.css
Normal file
@@ -0,0 +1,16 @@
|
||||
/* Forest Grove Theme — NaturePalette v1.0.5 */
|
||||
:root {
|
||||
--color-primary: #2D6A4F;
|
||||
--color-secondary: #52B788;
|
||||
--color-background: #F0F7EE;
|
||||
--color-surface: #FFFFFF;
|
||||
--color-text: #1B2E22;
|
||||
--color-accent: #B7E4C7;
|
||||
}
|
||||
|
||||
body { background-color: var(--color-background); color: var(--color-text); font-family: system-ui, sans-serif; }
|
||||
a, .link { color: var(--color-primary); }
|
||||
.btn-primary { background: var(--color-primary); color: #fff; border: none; border-radius: 6px; padding: 8px 18px; cursor: pointer; }
|
||||
.btn-primary:hover { background: var(--color-secondary); }
|
||||
.card { background: var(--color-surface); border-radius: 10px; box-shadow: 0 2px 8px rgba(45,106,79,0.12); padding: 20px; }
|
||||
.tag { background: var(--color-accent); color: var(--color-text); border-radius: 4px; padding: 2px 8px; font-size: 0.75rem; }
|
||||
16
public/themes/midnight-void.css
Normal file
16
public/themes/midnight-void.css
Normal file
@@ -0,0 +1,16 @@
|
||||
/* Midnight Void Theme — ThemeForge v2.0.1 */
|
||||
:root {
|
||||
--color-primary: #7C3AED;
|
||||
--color-secondary: #A78BFA;
|
||||
--color-background: #0D0D1A;
|
||||
--color-surface: #1A1A2E;
|
||||
--color-text: #E2E8F0;
|
||||
--color-accent: #F472B6;
|
||||
}
|
||||
|
||||
body { background-color: var(--color-background); color: var(--color-text); font-family: system-ui, sans-serif; }
|
||||
a, .link { color: var(--color-secondary); }
|
||||
.btn-primary { background: var(--color-primary); color: #fff; border: none; border-radius: 6px; padding: 8px 18px; cursor: pointer; }
|
||||
.btn-primary:hover { background: var(--color-accent); }
|
||||
.card { background: var(--color-surface); border-radius: 10px; box-shadow: 0 2px 16px rgba(124,58,237,0.25); padding: 20px; }
|
||||
.tag { background: var(--color-accent); color: #0D0D1A; border-radius: 4px; padding: 2px 8px; font-size: 0.75rem; }
|
||||
16
public/themes/obsidian-rose.css
Normal file
16
public/themes/obsidian-rose.css
Normal file
@@ -0,0 +1,16 @@
|
||||
/* Obsidian Rose Theme — ChromaCraft v1.3.0 */
|
||||
:root {
|
||||
--color-primary: #C9184A;
|
||||
--color-secondary: #FF4D6D;
|
||||
--color-background: #0A0A0F;
|
||||
--color-surface: #1C1C28;
|
||||
--color-text: #F1E3E4;
|
||||
--color-accent: #B5838D;
|
||||
}
|
||||
|
||||
body { background-color: var(--color-background); color: var(--color-text); font-family: system-ui, sans-serif; }
|
||||
a, .link { color: var(--color-secondary); }
|
||||
.btn-primary { background: var(--color-primary); color: #fff; border: none; border-radius: 6px; padding: 8px 18px; cursor: pointer; }
|
||||
.btn-primary:hover { background: var(--color-secondary); }
|
||||
.card { background: var(--color-surface); border-radius: 10px; box-shadow: 0 2px 16px rgba(201,24,74,0.2); padding: 20px; }
|
||||
.tag { background: var(--color-accent); color: #0A0A0F; border-radius: 4px; padding: 2px 8px; font-size: 0.75rem; }
|
||||
16
public/themes/ocean-breeze.css
Normal file
16
public/themes/ocean-breeze.css
Normal file
@@ -0,0 +1,16 @@
|
||||
/* Ocean Breeze Theme — ThemeForge v1.2.0 */
|
||||
:root {
|
||||
--color-primary: #0077B6;
|
||||
--color-secondary: #00B4D8;
|
||||
--color-background: #E0F4FF;
|
||||
--color-surface: #FFFFFF;
|
||||
--color-text: #1A2B3C;
|
||||
--color-accent: #48CAE4;
|
||||
}
|
||||
|
||||
body { background-color: var(--color-background); color: var(--color-text); font-family: system-ui, sans-serif; }
|
||||
a, .link { color: var(--color-primary); }
|
||||
.btn-primary { background: var(--color-primary); color: #fff; border: none; border-radius: 6px; padding: 8px 18px; cursor: pointer; }
|
||||
.btn-primary:hover { background: var(--color-secondary); }
|
||||
.card { background: var(--color-surface); border-radius: 10px; box-shadow: 0 2px 8px rgba(0,119,182,0.1); padding: 20px; }
|
||||
.tag { background: var(--color-accent); color: var(--color-text); border-radius: 4px; padding: 2px 8px; font-size: 0.75rem; }
|
||||
16
public/themes/sunset-ember.css
Normal file
16
public/themes/sunset-ember.css
Normal file
@@ -0,0 +1,16 @@
|
||||
/* Sunset Ember Theme — ChromaCraft v1.1.2 */
|
||||
:root {
|
||||
--color-primary: #D62828;
|
||||
--color-secondary: #F77F00;
|
||||
--color-background: #FFF5E4;
|
||||
--color-surface: #FFFFFF;
|
||||
--color-text: #2D1B00;
|
||||
--color-accent: #FCBF49;
|
||||
}
|
||||
|
||||
body { background-color: var(--color-background); color: var(--color-text); font-family: system-ui, sans-serif; }
|
||||
a, .link { color: var(--color-primary); }
|
||||
.btn-primary { background: var(--color-secondary); color: #fff; border: none; border-radius: 6px; padding: 8px 18px; cursor: pointer; }
|
||||
.btn-primary:hover { background: var(--color-primary); }
|
||||
.card { background: var(--color-surface); border-radius: 10px; box-shadow: 0 2px 8px rgba(214,40,40,0.1); padding: 20px; }
|
||||
.tag { background: var(--color-accent); color: var(--color-text); border-radius: 4px; padding: 2px 8px; font-size: 0.75rem; }
|
||||
1
public/todo-preview
Symbolic link
1
public/todo-preview
Symbolic link
@@ -0,0 +1 @@
|
||||
/home/came/Nextcloud/arch-work/Projects/Todo-App/dist
|
||||
0
writable/.htaccess
Normal file → Executable file
0
writable/.htaccess
Normal file → Executable file
0
writable/cache/index.html
vendored
Normal file → Executable file
0
writable/cache/index.html
vendored
Normal file → Executable file
0
writable/debugbar/index.html
Normal file → Executable file
0
writable/debugbar/index.html
Normal file → Executable file
0
writable/index.html
Normal file → Executable file
0
writable/index.html
Normal file → Executable file
0
writable/logs/index.html
Normal file → Executable file
0
writable/logs/index.html
Normal file → Executable file
0
writable/session/index.html
Normal file → Executable file
0
writable/session/index.html
Normal file → Executable file
0
writable/uploads/index.html
Normal file → Executable file
0
writable/uploads/index.html
Normal file → Executable file
Reference in New Issue
Block a user