From daa6ec8b1e8320153cab27c8a2ac77e5035b0a38 Mon Sep 17 00:00:00 2001 From: Cametendo Date: Wed, 13 May 2026 16:06:27 +0200 Subject: [PATCH 1/4] Merge feature/marketplace into main --- .gitignore | 4 +- app/Config/Routes.php | 3 + app/Controllers/ThemeStore.php | 127 +++ .../Seeds/MarketplaceThemesSeeder.php | 298 ++++++- app/Views/theme_store.php | 767 ++++++++++++++++++ env.example | 12 +- public/themes/arctic-frost.css | 16 + public/themes/forest-grove.css | 16 + public/themes/midnight-void.css | 16 + public/themes/obsidian-rose.css | 16 + public/themes/ocean-breeze.css | 16 + public/themes/sunset-ember.css | 16 + public/todo-preview | 1 + writable/.htaccess | 0 writable/cache/index.html | 0 writable/debugbar/index.html | 0 writable/index.html | 0 writable/logs/index.html | 0 writable/session/index.html | 0 writable/uploads/index.html | 0 20 files changed, 1283 insertions(+), 25 deletions(-) create mode 100644 app/Controllers/ThemeStore.php create mode 100644 app/Views/theme_store.php create mode 100644 public/themes/arctic-frost.css create mode 100644 public/themes/forest-grove.css create mode 100644 public/themes/midnight-void.css create mode 100644 public/themes/obsidian-rose.css create mode 100644 public/themes/ocean-breeze.css create mode 100644 public/themes/sunset-ember.css create mode 120000 public/todo-preview mode change 100644 => 100755 writable/.htaccess mode change 100644 => 100755 writable/cache/index.html mode change 100644 => 100755 writable/debugbar/index.html mode change 100644 => 100755 writable/index.html mode change 100644 => 100755 writable/logs/index.html mode change 100644 => 100755 writable/session/index.html mode change 100644 => 100755 writable/uploads/index.html diff --git a/.gitignore b/.gitignore index 035d487..4dee025 100644 --- a/.gitignore +++ b/.gitignore @@ -125,4 +125,6 @@ _modules/* /results/ /phpunit*.xml .env -env \ No newline at end of file +env +.claude/ +.claude/* \ No newline at end of file diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 6a9c6e0..d0143f2 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -6,6 +6,9 @@ use CodeIgniter\Router\RouteCollection; * @var RouteCollection $routes */ $routes->get('/', 'Home::index'); +$routes->get('/themes', 'ThemeStore::index'); +$routes->post('/themes/upload', 'ThemeStore::upload'); +$routes->get('/themes/preview/(:segment)', 'ThemeStore::preview/$1'); // ============================================================================ // API Routes - Version 1.0 diff --git a/app/Controllers/ThemeStore.php b/app/Controllers/ThemeStore.php new file mode 100644 index 0000000..e30ad17 --- /dev/null +++ b/app/Controllers/ThemeStore.php @@ -0,0 +1,127 @@ +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('

Theme not found.

'); + } + + $distIndex = '/home/came/Nextcloud/arch-work/Projects/Todo-App/dist/index.html'; + + if (! file_exists($distIndex)) { + return $this->response->setBody( + '' + . '

Todo app dist not found.

' + ); + } + + $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 = ""; + + $todoHtml = str_replace('', $styleTag . "\n", $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)); + } +} diff --git a/app/Database/Seeds/MarketplaceThemesSeeder.php b/app/Database/Seeds/MarketplaceThemesSeeder.php index 9d26bc3..5933fdb 100644 --- a/app/Database/Seeds/MarketplaceThemesSeeder.php +++ b/app/Database/Seeds/MarketplaceThemesSeeder.php @@ -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', + 'id' => '550e8400-e29b-41d4-a716-446655440010', + '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', - 'price' => 0, + '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', + 'id' => '550e8400-e29b-41d4-a716-446655440011', + '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', - 'price' => 0, + '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'), ], diff --git a/app/Views/theme_store.php b/app/Views/theme_store.php new file mode 100644 index 0000000..b953bbd --- /dev/null +++ b/app/Views/theme_store.php @@ -0,0 +1,767 @@ + + + + + + Theme Store + + + + +
+

Theme Store

+

Beautiful, ready-to-use themes for your application

+
+ free themes + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+ +
+
+ +
+ + +
+ +
+
+
+ + v +
+
by
+

+
+ + + +
+
+ + ⇓ Download + + +
+
+
+ +
+
+ + + + + + + + + + + + + diff --git a/env.example b/env.example index f359ec2..b12e39c 100644 --- a/env.example +++ b/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 diff --git a/public/themes/arctic-frost.css b/public/themes/arctic-frost.css new file mode 100644 index 0000000..8ebf313 --- /dev/null +++ b/public/themes/arctic-frost.css @@ -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; } diff --git a/public/themes/forest-grove.css b/public/themes/forest-grove.css new file mode 100644 index 0000000..a953d12 --- /dev/null +++ b/public/themes/forest-grove.css @@ -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; } diff --git a/public/themes/midnight-void.css b/public/themes/midnight-void.css new file mode 100644 index 0000000..c334f3f --- /dev/null +++ b/public/themes/midnight-void.css @@ -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; } diff --git a/public/themes/obsidian-rose.css b/public/themes/obsidian-rose.css new file mode 100644 index 0000000..7bf09da --- /dev/null +++ b/public/themes/obsidian-rose.css @@ -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; } diff --git a/public/themes/ocean-breeze.css b/public/themes/ocean-breeze.css new file mode 100644 index 0000000..c79a9da --- /dev/null +++ b/public/themes/ocean-breeze.css @@ -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; } diff --git a/public/themes/sunset-ember.css b/public/themes/sunset-ember.css new file mode 100644 index 0000000..3a9791e --- /dev/null +++ b/public/themes/sunset-ember.css @@ -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; } diff --git a/public/todo-preview b/public/todo-preview new file mode 120000 index 0000000..ad46dac --- /dev/null +++ b/public/todo-preview @@ -0,0 +1 @@ +/home/came/Nextcloud/arch-work/Projects/Todo-App/dist \ No newline at end of file diff --git a/writable/.htaccess b/writable/.htaccess old mode 100644 new mode 100755 diff --git a/writable/cache/index.html b/writable/cache/index.html old mode 100644 new mode 100755 diff --git a/writable/debugbar/index.html b/writable/debugbar/index.html old mode 100644 new mode 100755 diff --git a/writable/index.html b/writable/index.html old mode 100644 new mode 100755 diff --git a/writable/logs/index.html b/writable/logs/index.html old mode 100644 new mode 100755 diff --git a/writable/session/index.html b/writable/session/index.html old mode 100644 new mode 100755 diff --git a/writable/uploads/index.html b/writable/uploads/index.html old mode 100644 new mode 100755 From bb09f3d0249f410ff0a4994994f05f81211c89f2 Mon Sep 17 00:00:00 2001 From: Cametendo Date: Wed, 6 May 2026 14:17:25 +0200 Subject: [PATCH 2/4] Add marketplace --- app/Config/Routes.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index d0143f2..e0043d4 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -89,3 +89,9 @@ $routes->group('api/v1', ['namespace' => 'App\Controllers\Api\V1', 'filter' => [ $routes->put('user/themes/(:segment)', 'UserThemeController::update/$1'); $routes->delete('user/themes/(:segment)', 'UserThemeController::delete/$1'); }); +<<<<<<< Updated upstream +======= +$routes->get('/themes', 'ThemeStore::index'); +$routes->post('/themes/upload', 'ThemeStore::upload'); +$routes->get('/themes/preview/(:segment)', 'ThemeStore::preview/$1'); +>>>>>>> Stashed changes From f27498dc2602b008f6a42e5a80b3ca9c2f1aebb9 Mon Sep 17 00:00:00 2001 From: Cametendo Date: Wed, 6 May 2026 14:24:53 +0200 Subject: [PATCH 3/4] Fix merge conflict --- app/Config/Routes.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index e0043d4..4bd96a7 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -89,9 +89,6 @@ $routes->group('api/v1', ['namespace' => 'App\Controllers\Api\V1', 'filter' => [ $routes->put('user/themes/(:segment)', 'UserThemeController::update/$1'); $routes->delete('user/themes/(:segment)', 'UserThemeController::delete/$1'); }); -<<<<<<< Updated upstream -======= $routes->get('/themes', 'ThemeStore::index'); $routes->post('/themes/upload', 'ThemeStore::upload'); $routes->get('/themes/preview/(:segment)', 'ThemeStore::preview/$1'); ->>>>>>> Stashed changes From caf81ea4e21fd73a39cf06aa6d08180384d80ea4 Mon Sep 17 00:00:00 2001 From: Cametendo Date: Wed, 13 May 2026 15:29:47 +0200 Subject: [PATCH 4/4] Working marketplace --- app/Config/App.php | 2 +- app/Config/Cors.php | 2 +- app/Config/Routes.php | 14 ++ app/Controllers/ThemeStore.php | 151 +++++++++++++++++- app/Views/theme_store.php | 140 +++++++++++++--- public/.htaccess | 1 - public/themes/2341342134-1441f7.css | 38 +++++ public/themes/extract-test-theme-5fae6e.css | 37 +++++ public/themes/manual-game-update-2-e1a77a.css | 38 +++++ public/themes/manual-game-update-7cc79d.css | 38 +++++ public/themes/red-extract-theme-a3aabe.css | 37 +++++ public/themes/test-theme-103fb1.css | 37 +++++ public/themes/test-theme-6fcabb.css | 37 +++++ .../themestore-theme-by-came-0da6fd.css | 37 +++++ 14 files changed, 577 insertions(+), 32 deletions(-) create mode 100644 public/themes/2341342134-1441f7.css create mode 100644 public/themes/extract-test-theme-5fae6e.css create mode 100644 public/themes/manual-game-update-2-e1a77a.css create mode 100644 public/themes/manual-game-update-7cc79d.css create mode 100644 public/themes/red-extract-theme-a3aabe.css create mode 100644 public/themes/test-theme-103fb1.css create mode 100644 public/themes/test-theme-6fcabb.css create mode 100644 public/themes/themestore-theme-by-came-0da6fd.css diff --git a/app/Config/App.php b/app/Config/App.php index b761da7..5c9560f 100644 --- a/app/Config/App.php +++ b/app/Config/App.php @@ -16,7 +16,7 @@ class App extends BaseConfig * * E.g., http://example.com/ */ - public string $baseURL = 'http://localhost:8080/'; + public string $baseURL = 'http://localhost/Todo-App-Backend/public/'; /** * Allowed Hostnames in the Site URL other than the hostname in the baseURL. diff --git a/app/Config/Cors.php b/app/Config/Cors.php index 333fbc9..ceae347 100644 --- a/app/Config/Cors.php +++ b/app/Config/Cors.php @@ -57,7 +57,7 @@ class Cors extends BaseConfig * * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials */ - 'supportsCredentials' => false, + 'supportsCredentials' => true, /** * Set headers to allow. diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 4bd96a7..c01ff7b 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -90,5 +90,19 @@ $routes->group('api/v1', ['namespace' => 'App\Controllers\Api\V1', 'filter' => [ $routes->delete('user/themes/(:segment)', 'UserThemeController::delete/$1'); }); $routes->get('/themes', 'ThemeStore::index'); +$routes->options('/themes', static function () { + header('Access-Control-Allow-Origin: http://localhost:5173'); + header('Access-Control-Allow-Methods: GET, OPTIONS'); + header('Access-Control-Allow-Headers: Content-Type, Accept, Fetch'); + header('Access-Control-Allow-Credentials: true'); + return response()->setStatusCode(204); +}); $routes->post('/themes/upload', 'ThemeStore::upload'); +$routes->options('/themes/upload', static function () { + header('Access-Control-Allow-Origin: http://localhost:5173'); + header('Access-Control-Allow-Methods: POST, OPTIONS'); + header('Access-Control-Allow-Headers: Content-Type, Accept, Fetch'); + header('Access-Control-Allow-Credentials: true'); + return response()->setStatusCode(204); +}); $routes->get('/themes/preview/(:segment)', 'ThemeStore::preview/$1'); diff --git a/app/Controllers/ThemeStore.php b/app/Controllers/ThemeStore.php index e30ad17..d3a26db 100644 --- a/app/Controllers/ThemeStore.php +++ b/app/Controllers/ThemeStore.php @@ -3,11 +3,12 @@ namespace App\Controllers; use App\Models\MarketplaceThemeModel; +use App\Models\UserThemeModel; use CodeIgniter\HTTP\Response; class ThemeStore extends BaseController { - public function index(): string + public function index() { $model = new MarketplaceThemeModel(); $themes = $model->where('is_published', 1)->findAll(); @@ -17,6 +18,15 @@ class ThemeStore extends BaseController $theme['colors'] = $meta['colors'] ?? []; $theme['tags'] = $meta['tags'] ?? []; $theme['vars'] = $meta['vars'] ?? []; + + // Provide a preview array compatible with the frontend + $theme['preview'] = !empty($theme['colors']) ? array_values($theme['colors']) : ['#ffffff', '#f0f0f0', '#007acc']; + } + + if ($this->request->isAJAX() || $this->request->hasHeader('Fetch') || str_contains($this->request->getHeaderLine('Accept'), 'application/json')) { + header('Access-Control-Allow-Origin: http://localhost:5173'); + header('Access-Control-Allow-Credentials: true'); + return $this->response->setJSON($themes); } return view('theme_store', [ @@ -28,20 +38,32 @@ class ThemeStore extends BaseController public function upload(): Response { + header('Access-Control-Allow-Origin: http://localhost:5173'); + header('Access-Control-Allow-Credentials: true'); + $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 ($this->request->isAJAX() || $this->request->hasHeader('Fetch')) { + return $this->response->setStatusCode(400)->setJSON(['error' => 'Display name is required.']); + } + 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 ($this->request->isAJAX() || $this->request->hasHeader('Fetch')) { + return $this->response->setStatusCode(400)->setJSON(['error' => 'Please upload a valid CSS file.']); + } + 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.'); + if ($this->request->isAJAX() || $this->request->hasHeader('Fetch')) { + return $this->response->setStatusCode(400)->setJSON(['error' => 'Only .css files are allowed.']); + } + return redirect()->to('themes')->with('error', 'Only .css files are allowed.'); } $slug = strtolower(preg_replace('/[^a-z0-9]+/i', '-', $displayName)); @@ -50,6 +72,23 @@ class ThemeStore extends BaseController $file->move(FCPATH . 'themes', $filename, true); + // Extract CSS variables and colors from the uploaded file + $cssContent = file_get_contents(FCPATH . 'themes/' . $filename); + preg_match_all('/(--[a-zA-Z0-9-]+)\s*:\s*([^;]+);/', $cssContent, $matches); + + $vars = []; + if (!empty($matches[1])) { + foreach ($matches[1] as $index => $key) { + $vars[$key] = trim($matches[2][$index]); + } + } + + // Try to generate 3-color preview based on standard variables + $colors = []; + if (isset($vars['--bg'])) $colors['bg'] = $vars['--bg']; + if (isset($vars['--surface'])) $colors['surface'] = $vars['--surface']; + if (isset($vars['--accent'])) $colors['accent'] = $vars['--accent']; + $model = new MarketplaceThemeModel(); $model->insert([ 'id' => $this->uuid4(), @@ -62,12 +101,22 @@ class ThemeStore extends BaseController 'download_url' => '/themes/' . $filename, 'price' => 0, 'is_published' => true, - 'metadata' => json_encode(['tags' => ['custom', 'community'], 'colors' => []]), + 'metadata' => json_encode([ + 'tags' => ['custom', 'community'], + 'colors' => $colors, + 'vars' => $vars + ]), '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!'); + if ($this->request->isAJAX() || $this->request->hasHeader('Fetch')) { + return $this->response->setJSON([ + 'success' => true, + 'message' => '"' . esc($displayName) . '" uploaded successfully!' + ]); + } + return redirect()->to('themes')->with('success', '"' . esc($displayName) . '" uploaded successfully!'); } public function preview(string $id): Response @@ -117,6 +166,96 @@ class ThemeStore extends BaseController ->setBody($todoHtml); } + public function install(string $id): Response + { + $model = new MarketplaceThemeModel(); + $theme = $model->find($id); + + if (! $theme) { + return $this->response->setStatusCode(404)->setJSON(['error' => 'Theme not found in the marketplace.']); + } + + // Using session user_id or a default placeholder since standard auth might be configured separately + $userId = session()->get('user_id') ?? 'default-user-id'; + + $userThemeModel = new UserThemeModel(); + + if (! $userThemeModel->isInstalled($userId, $id)) { + $userThemeModel->installTheme($userId, $id); + } + + return $this->response->setJSON([ + 'success' => true, + 'message' => '"' . esc($theme['display_name']) . '" has been installed to your account.' + ]); + } + + public function activate(string $id): Response + { + $userId = session()->get('user_id') ?? 'default-user-id'; + + $userThemeModel = new UserThemeModel(); + + if (! $userThemeModel->isInstalled($userId, $id)) { + return $this->response->setStatusCode(400)->setJSON(['error' => 'Theme must be installed before it can be activated.']); + } + + $userThemeModel->setActiveTheme($userId, $id); + + return $this->response->setJSON(['success' => true, 'message' => 'Theme activated successfully.']); + } + + public function uninstall(string $id): Response + { + $userId = session()->get('user_id') ?? 'default-user-id'; + $userThemeModel = new UserThemeModel(); + + $userThemeModel->uninstallTheme($userId, $id); + + return $this->response->setJSON(['success' => true, 'message' => 'Theme successfully uninstalled.']); + } + + public function myThemes(): Response + { + $userId = session()->get('user_id') ?? 'default-user-id'; + $userThemeModel = new UserThemeModel(); + + return $this->response->setJSON(['success' => true, 'data' => $userThemeModel->getUserThemes($userId)]); + } + + public function serveCss(string $filename): Response + { + // Ensure it's just a file name (prevent directory traversal) + $filename = basename($filename); + $cssPath = FCPATH . 'themes/' . $filename; + + // If the file actually exists on disk (e.g. newly uploaded themes) + if (file_exists($cssPath)) { + $css = file_get_contents($cssPath); + return $this->response->setContentType('text/css')->setBody($css); + } + + // Generate dynamically for seeded themes that don't have a physical file + $model = new MarketplaceThemeModel(); + $name = preg_replace('/\.css$/i', '', $filename); + $theme = $model->where('name', $name)->first(); + + if (! $theme) { + throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); + } + + $meta = json_decode($theme['metadata'] ?? '{}', true); + $vars = $meta['vars'] ?? []; + + $css = "/* Theme: {$theme['display_name']} */\n:root {\n"; + foreach ($vars as $prop => $value) { + $css .= " {$prop}: {$value};\n"; + } + $css .= "}\n"; + + return $this->response->setContentType('text/css')->setBody($css); + } + private function uuid4(): string { $data = random_bytes(16); diff --git a/app/Views/theme_store.php b/app/Views/theme_store.php index b953bbd..faf4eea 100644 --- a/app/Views/theme_store.php +++ b/app/Views/theme_store.php @@ -164,7 +164,7 @@ } .btn:hover { opacity: 0.88; transform: translateY(-1px); } .btn-download { - background: linear-gradient(135deg, #7c3aed, #6d28d9); + background: linear-gradient(135deg, #22c55e, #16a34a); color: #fff; text-decoration: none; display: flex; align-items: center; justify-content: center; gap: 6px; } @@ -292,7 +292,7 @@ .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); + background: linear-gradient(135deg, #22c55e, #16a34a); color: #fff; font-weight: 700; text-decoration: none; display: flex; align-items: center; justify-content: center; gap: 8px; border: none; cursor: pointer; @@ -510,21 +510,31 @@
- - ⇓ Download - +
@@ -570,9 +580,9 @@ @@ -608,7 +618,7 @@
-
+
@@ -643,12 +653,93 @@