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 2b4edf6..1a6a9d4 100644 --- a/app/Config/Cors.php +++ b/app/Config/Cors.php @@ -34,7 +34,7 @@ class Cors extends BaseConfig * - ['http://localhost:8080'] * - ['https://www.example.com'] */ - 'allowedOrigins' => [], + 'allowedOrigins' => ['http://localhost:5173', 'http://127.0.0.1:5173'], /** * Origin regex patterns for the `Access-Control-Allow-Origin` header. @@ -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. @@ -68,7 +68,7 @@ class Cors extends BaseConfig * * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers */ - 'allowedHeaders' => [], + 'allowedHeaders' => ['*'], /** * Set headers to expose. @@ -93,7 +93,7 @@ class Cors extends BaseConfig * * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods */ - 'allowedMethods' => [], + 'allowedMethods' => ['*'], /** * Set how many seconds the results of a preflight request can be cached. diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 32a3118..46fe407 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -7,5 +7,24 @@ use CodeIgniter\Router\RouteCollection; */ $routes->get('/', 'Home::index'); $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'); +$routes->post('/themes/install/(:segment)', 'ThemeStore::install/$1'); +$routes->post('/themes/activate/(:segment)', 'ThemeStore::activate/$1'); +$routes->delete('/themes/uninstall/(:segment)', 'ThemeStore::uninstall/$1'); +$routes->get('/themes/my-themes', 'ThemeStore::myThemes'); +$routes->get('/themes/(:segment)', 'ThemeStore::serveCss/$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 @@