mirror of
https://github.com/JGH0/Todo-App-Backend.git
synced 2026-06-03 13:28:47 +02:00
Working marketplace
This commit is contained in:
@@ -16,7 +16,7 @@ class App extends BaseConfig
|
|||||||
*
|
*
|
||||||
* E.g., http://example.com/
|
* 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.
|
* Allowed Hostnames in the Site URL other than the hostname in the baseURL.
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class Cors extends BaseConfig
|
|||||||
* - ['http://localhost:8080']
|
* - ['http://localhost:8080']
|
||||||
* - ['https://www.example.com']
|
* - ['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.
|
* 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
|
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
|
||||||
*/
|
*/
|
||||||
'supportsCredentials' => false,
|
'supportsCredentials' => true,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set headers to allow.
|
* 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
|
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
|
||||||
*/
|
*/
|
||||||
'allowedHeaders' => [],
|
'allowedHeaders' => ['*'],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set headers to expose.
|
* 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
|
* @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.
|
* Set how many seconds the results of a preflight request can be cached.
|
||||||
|
|||||||
@@ -7,5 +7,24 @@ use CodeIgniter\Router\RouteCollection;
|
|||||||
*/
|
*/
|
||||||
$routes->get('/', 'Home::index');
|
$routes->get('/', 'Home::index');
|
||||||
$routes->get('/themes', 'ThemeStore::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->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->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');
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
use App\Models\MarketplaceThemeModel;
|
use App\Models\MarketplaceThemeModel;
|
||||||
|
use App\Models\UserThemeModel;
|
||||||
use CodeIgniter\HTTP\Response;
|
use CodeIgniter\HTTP\Response;
|
||||||
|
|
||||||
class ThemeStore extends BaseController
|
class ThemeStore extends BaseController
|
||||||
{
|
{
|
||||||
public function index(): string
|
public function index()
|
||||||
{
|
{
|
||||||
$model = new MarketplaceThemeModel();
|
$model = new MarketplaceThemeModel();
|
||||||
$themes = $model->where('is_published', 1)->findAll();
|
$themes = $model->where('is_published', 1)->findAll();
|
||||||
@@ -17,6 +18,15 @@ class ThemeStore extends BaseController
|
|||||||
$theme['colors'] = $meta['colors'] ?? [];
|
$theme['colors'] = $meta['colors'] ?? [];
|
||||||
$theme['tags'] = $meta['tags'] ?? [];
|
$theme['tags'] = $meta['tags'] ?? [];
|
||||||
$theme['vars'] = $meta['vars'] ?? [];
|
$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', [
|
return view('theme_store', [
|
||||||
@@ -28,20 +38,32 @@ class ThemeStore extends BaseController
|
|||||||
|
|
||||||
public function upload(): Response
|
public function upload(): Response
|
||||||
{
|
{
|
||||||
|
header('Access-Control-Allow-Origin: http://localhost:5173');
|
||||||
|
header('Access-Control-Allow-Credentials: true');
|
||||||
|
|
||||||
$file = $this->request->getFile('theme_css');
|
$file = $this->request->getFile('theme_css');
|
||||||
$displayName = trim($this->request->getPost('display_name') ?? '');
|
$displayName = trim($this->request->getPost('display_name') ?? '');
|
||||||
$description = trim($this->request->getPost('description') ?? '');
|
$description = trim($this->request->getPost('description') ?? '');
|
||||||
|
|
||||||
if ($displayName === '') {
|
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()) {
|
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') {
|
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));
|
$slug = strtolower(preg_replace('/[^a-z0-9]+/i', '-', $displayName));
|
||||||
@@ -50,6 +72,23 @@ class ThemeStore extends BaseController
|
|||||||
|
|
||||||
$file->move(FCPATH . 'themes', $filename, true);
|
$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 = new MarketplaceThemeModel();
|
||||||
$model->insert([
|
$model->insert([
|
||||||
'id' => $this->uuid4(),
|
'id' => $this->uuid4(),
|
||||||
@@ -62,12 +101,22 @@ class ThemeStore extends BaseController
|
|||||||
'download_url' => '/themes/' . $filename,
|
'download_url' => '/themes/' . $filename,
|
||||||
'price' => 0,
|
'price' => 0,
|
||||||
'is_published' => true,
|
'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'),
|
'created_at' => date('Y-m-d H:i:s'),
|
||||||
'updated_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
|
public function preview(string $id): Response
|
||||||
@@ -117,6 +166,96 @@ class ThemeStore extends BaseController
|
|||||||
->setBody($todoHtml);
|
->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
|
private function uuid4(): string
|
||||||
{
|
{
|
||||||
$data = random_bytes(16);
|
$data = random_bytes(16);
|
||||||
|
|||||||
@@ -164,7 +164,7 @@
|
|||||||
}
|
}
|
||||||
.btn:hover { opacity: 0.88; transform: translateY(-1px); }
|
.btn:hover { opacity: 0.88; transform: translateY(-1px); }
|
||||||
.btn-download {
|
.btn-download {
|
||||||
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
background: linear-gradient(135deg, #22c55e, #16a34a);
|
||||||
color: #fff; text-decoration: none;
|
color: #fff; text-decoration: none;
|
||||||
display: flex; align-items: center; justify-content: center; gap: 6px;
|
display: flex; align-items: center; justify-content: center; gap: 6px;
|
||||||
}
|
}
|
||||||
@@ -292,7 +292,7 @@
|
|||||||
.modal-actions { display: flex; gap: 10px; }
|
.modal-actions { display: flex; gap: 10px; }
|
||||||
.btn-download-lg {
|
.btn-download-lg {
|
||||||
flex: 1; padding: 13px; font-size: 0.95rem; border-radius: 10px;
|
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;
|
color: #fff; font-weight: 700; text-decoration: none;
|
||||||
display: flex; align-items: center; justify-content: center; gap: 8px;
|
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||||
border: none; cursor: pointer;
|
border: none; cursor: pointer;
|
||||||
@@ -510,21 +510,31 @@
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a class="btn btn-download" href="<?= esc($theme['download_url']) ?>" download>
|
<button class="btn btn-download" onclick="installTheme(<?= htmlspecialchars(json_encode([
|
||||||
⇓ Download
|
'id' => $theme['id'],
|
||||||
</a>
|
'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'],
|
||||||
|
]), ENT_QUOTES, 'UTF-8') ?>)">
|
||||||
|
⇓ Install Theme
|
||||||
|
</button>
|
||||||
<button class="btn btn-details"
|
<button class="btn btn-details"
|
||||||
onclick='openDetailsModal(<?= json_encode([
|
onclick="openDetailsModal(<?= htmlspecialchars(json_encode([
|
||||||
"id" => $theme["id"],
|
'id' => $theme['id'],
|
||||||
"display_name" => $theme["display_name"],
|
'display_name' => $theme['display_name'],
|
||||||
"description" => $theme["description"],
|
'description' => $theme['description'],
|
||||||
"author" => $theme["author"],
|
'author' => $theme['author'],
|
||||||
"version" => $theme["version"],
|
'version' => $theme['version'],
|
||||||
"download_url" => $theme["download_url"],
|
'download_url' => $theme['download_url'],
|
||||||
"colors" => $theme["colors"],
|
'colors' => $theme['colors'],
|
||||||
"tags" => $theme["tags"],
|
'tags' => $theme['tags'],
|
||||||
"vars" => $theme["vars"],
|
'vars' => $theme['vars'],
|
||||||
]) ?>)'>
|
]), ENT_QUOTES, 'UTF-8') ?>)">
|
||||||
Details
|
Details
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -570,9 +580,9 @@
|
|||||||
<div class="modal-tags" id="dm-tags"></div>
|
<div class="modal-tags" id="dm-tags"></div>
|
||||||
|
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<a class="btn-download-lg" id="dm-download" href="#" download>
|
<button class="btn-download-lg" id="dm-download" onclick="installCurrentTheme()">
|
||||||
⇓ Download Theme
|
⇓ Install Theme
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -608,7 +618,7 @@
|
|||||||
<button class="modal-close" onclick="closeUploadModal()">✕</button>
|
<button class="modal-close" onclick="closeUploadModal()">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="upload-body">
|
<div class="upload-body">
|
||||||
<form method="post" action="/themes/upload" enctype="multipart/form-data">
|
<form method="post" action="<?= site_url('themes/upload') ?>" enctype="multipart/form-data">
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Display Name *</label>
|
<label>Display Name *</label>
|
||||||
@@ -643,12 +653,93 @@
|
|||||||
<script>
|
<script>
|
||||||
let currentThemeId = null;
|
let currentThemeId = null;
|
||||||
let currentThemeVars = {};
|
let currentThemeVars = {};
|
||||||
|
let currentThemeData = null;
|
||||||
let previewLoaded = false;
|
let previewLoaded = false;
|
||||||
|
|
||||||
|
/* ── Theme Installation ── */
|
||||||
|
function installTheme(theme) {
|
||||||
|
// Convert theme data to match Todo-App format
|
||||||
|
const themeData = {
|
||||||
|
name: theme.display_name,
|
||||||
|
description: theme.description,
|
||||||
|
preview: Object.values(theme.colors || {}).length ? Object.values(theme.colors) : ['#ffffff', '#f0f0f0', '#007acc'],
|
||||||
|
vars: theme.vars || {},
|
||||||
|
source: 'theme-store'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Installing theme:', themeData);
|
||||||
|
|
||||||
|
// Send install message to parent Todo-App
|
||||||
|
if (window.parent && window.parent !== window) {
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'THEME_DOWNLOAD_REQUEST',
|
||||||
|
data: themeData
|
||||||
|
}, '*');
|
||||||
|
|
||||||
|
// Show success feedback
|
||||||
|
showInstallFeedback(theme.display_name);
|
||||||
|
} else {
|
||||||
|
// Fallback: redirect to the frontend with the theme data
|
||||||
|
const installUrl = "http://localhost:5173/#theme-install:" + encodeURIComponent(JSON.stringify(themeData));
|
||||||
|
window.location.href = installUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function installCurrentTheme() {
|
||||||
|
if (currentThemeData) {
|
||||||
|
installTheme(currentThemeData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showInstallFeedback(themeName) {
|
||||||
|
// Create a temporary success message
|
||||||
|
const feedback = document.createElement('div');
|
||||||
|
feedback.className = 'install-feedback';
|
||||||
|
feedback.innerHTML = `✅ "${themeName}" is being installed...`;
|
||||||
|
feedback.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: linear-gradient(135deg, #22c55e, #16a34a);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
z-index: 10000;
|
||||||
|
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.3);
|
||||||
|
animation: slideIn 0.3s ease-out;
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(feedback);
|
||||||
|
|
||||||
|
// Remove after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
feedback.style.animation = 'slideOut 0.3s ease-out';
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(feedback);
|
||||||
|
}, 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add animations
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes slideIn {
|
||||||
|
from { transform: translateX(100%); opacity: 0; }
|
||||||
|
to { transform: translateX(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
@keyframes slideOut {
|
||||||
|
from { transform: translateX(0); opacity: 1; }
|
||||||
|
to { transform: translateX(100%); opacity: 0; }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
/* ── Details modal ── */
|
/* ── Details modal ── */
|
||||||
function openDetailsModal(theme) {
|
function openDetailsModal(theme) {
|
||||||
currentThemeId = theme.id;
|
currentThemeId = theme.id;
|
||||||
currentThemeVars = theme.vars || {};
|
currentThemeVars = theme.vars || {};
|
||||||
|
currentThemeData = theme; // Store full theme data for installation
|
||||||
previewLoaded = false;
|
previewLoaded = false;
|
||||||
|
|
||||||
const colors = theme.colors || {};
|
const colors = theme.colors || {};
|
||||||
@@ -657,7 +748,6 @@
|
|||||||
document.getElementById('dm-title').textContent = theme.display_name;
|
document.getElementById('dm-title').textContent = theme.display_name;
|
||||||
document.getElementById('dm-meta').textContent = 'by ' + theme.author + ' · v' + theme.version;
|
document.getElementById('dm-meta').textContent = 'by ' + theme.author + ' · v' + theme.version;
|
||||||
document.getElementById('dm-desc').textContent = theme.description;
|
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';
|
document.getElementById('preview-url').textContent = 'localhost — ' + theme.display_name + ' applied';
|
||||||
|
|
||||||
// stripe
|
// stripe
|
||||||
@@ -720,8 +810,12 @@
|
|||||||
document.getElementById('preview-panel-inner').classList.add('active');
|
document.getElementById('preview-panel-inner').classList.add('active');
|
||||||
if (!previewLoaded && currentThemeId) {
|
if (!previewLoaded && currentThemeId) {
|
||||||
document.getElementById('preview-loading').style.display = 'flex';
|
document.getElementById('preview-loading').style.display = 'flex';
|
||||||
const encoded = btoa(JSON.stringify(currentThemeVars));
|
const themeData = {
|
||||||
document.getElementById('preview-iframe').src = 'http://localhost:5173/?__theme=' + encoded;
|
name: currentThemeData.display_name,
|
||||||
|
vars: currentThemeVars
|
||||||
|
};
|
||||||
|
const encoded = btoa(JSON.stringify(themeData));
|
||||||
|
document.getElementById('preview-iframe').src = 'http://localhost:5173/?__theme_preview=' + encoded + '&__preview_mode=true';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('preview-panel-inner').classList.remove('active');
|
document.getElementById('preview-panel-inner').classList.remove('active');
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ Options -Indexes
|
|||||||
# such as an image or css document, if this isn't true it sends the
|
# such as an image or css document, if this isn't true it sends the
|
||||||
# request to the front controller, index.php
|
# request to the front controller, index.php
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteRule ^([\s\S]*)$ index.php/$1 [L,NC,QSA]
|
RewriteRule ^([\s\S]*)$ index.php/$1 [L,NC,QSA]
|
||||||
|
|
||||||
# Ensure Authorization header is passed along
|
# Ensure Authorization header is passed along
|
||||||
|
|||||||
38
public/themes/2341342134-1441f7.css
Normal file
38
public/themes/2341342134-1441f7.css
Normal file
File diff suppressed because one or more lines are too long
37
public/themes/extract-test-theme-5fae6e.css
Normal file
37
public/themes/extract-test-theme-5fae6e.css
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* @todo-theme-meta
|
||||||
|
{
|
||||||
|
"name": "Extract Test Theme",
|
||||||
|
"id": "custom-1778676985034",
|
||||||
|
"group": "Custom",
|
||||||
|
"preview": [
|
||||||
|
"#f5f5f5",
|
||||||
|
"#ffffff",
|
||||||
|
"#274f69"
|
||||||
|
],
|
||||||
|
"hasWallpaper": false
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #f5f5f5;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface-strong: #ffffff;
|
||||||
|
--surface-muted: #fafafa;
|
||||||
|
--border: #d0d0d0;
|
||||||
|
--line: #b9b9b5;
|
||||||
|
--text: #1f1f1f;
|
||||||
|
--text-muted: #686866;
|
||||||
|
--text-strong: #111111;
|
||||||
|
--accent: #274f69;
|
||||||
|
--accent-text: #ffffff;
|
||||||
|
--accent-soft: #d6e4ec;
|
||||||
|
--sidebar-bg: #ffffff;
|
||||||
|
--sidebar-border: #d0d0d0;
|
||||||
|
--sidebar-text: #222222;
|
||||||
|
--sidebar-text-muted: #686866;
|
||||||
|
--input-bg: #ffffff;
|
||||||
|
--input-border: #cfcfcf;
|
||||||
|
--modal-bg: #ffffff;
|
||||||
|
--chip: #d8d8d8;
|
||||||
|
--success: #dff7e7;
|
||||||
|
}
|
||||||
38
public/themes/manual-game-update-2-e1a77a.css
Normal file
38
public/themes/manual-game-update-2-e1a77a.css
Normal file
File diff suppressed because one or more lines are too long
38
public/themes/manual-game-update-7cc79d.css
Normal file
38
public/themes/manual-game-update-7cc79d.css
Normal file
File diff suppressed because one or more lines are too long
37
public/themes/red-extract-theme-a3aabe.css
Normal file
37
public/themes/red-extract-theme-a3aabe.css
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* @todo-theme-meta
|
||||||
|
{
|
||||||
|
"name": "Red Extract Theme",
|
||||||
|
"id": "custom-1778677606575",
|
||||||
|
"group": "Custom",
|
||||||
|
"preview": [
|
||||||
|
"#f5f5f5",
|
||||||
|
"#ffffff",
|
||||||
|
"#274f69"
|
||||||
|
],
|
||||||
|
"hasWallpaper": false
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #f5f5f5;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface-strong: #ffffff;
|
||||||
|
--surface-muted: #fafafa;
|
||||||
|
--border: #d0d0d0;
|
||||||
|
--line: #b9b9b5;
|
||||||
|
--text: #1f1f1f;
|
||||||
|
--text-muted: #686866;
|
||||||
|
--text-strong: #111111;
|
||||||
|
--accent: #274f69;
|
||||||
|
--accent-text: #ffffff;
|
||||||
|
--accent-soft: #d6e4ec;
|
||||||
|
--sidebar-bg: #ffffff;
|
||||||
|
--sidebar-border: #d0d0d0;
|
||||||
|
--sidebar-text: #222222;
|
||||||
|
--sidebar-text-muted: #686866;
|
||||||
|
--input-bg: #ffffff;
|
||||||
|
--input-border: #cfcfcf;
|
||||||
|
--modal-bg: #ffffff;
|
||||||
|
--chip: #d8d8d8;
|
||||||
|
--success: #dff7e7;
|
||||||
|
}
|
||||||
37
public/themes/test-theme-103fb1.css
Normal file
37
public/themes/test-theme-103fb1.css
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* @todo-theme-meta
|
||||||
|
{
|
||||||
|
"name": "Test Theme",
|
||||||
|
"id": "custom-1778671955013",
|
||||||
|
"group": "Custom",
|
||||||
|
"preview": [
|
||||||
|
"#f5f5f5",
|
||||||
|
"#ffffff",
|
||||||
|
"#274f69"
|
||||||
|
],
|
||||||
|
"hasWallpaper": false
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #f5f5f5;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface-strong: #ffffff;
|
||||||
|
--surface-muted: #fafafa;
|
||||||
|
--border: #d0d0d0;
|
||||||
|
--line: #b9b9b5;
|
||||||
|
--text: #1f1f1f;
|
||||||
|
--text-muted: #686866;
|
||||||
|
--text-strong: #111111;
|
||||||
|
--accent: #274f69;
|
||||||
|
--accent-text: #ffffff;
|
||||||
|
--accent-soft: #d6e4ec;
|
||||||
|
--sidebar-bg: #ffffff;
|
||||||
|
--sidebar-border: #d0d0d0;
|
||||||
|
--sidebar-text: #222222;
|
||||||
|
--sidebar-text-muted: #686866;
|
||||||
|
--input-bg: #ffffff;
|
||||||
|
--input-border: #cfcfcf;
|
||||||
|
--modal-bg: #ffffff;
|
||||||
|
--chip: #d8d8d8;
|
||||||
|
--success: #dff7e7;
|
||||||
|
}
|
||||||
37
public/themes/test-theme-6fcabb.css
Normal file
37
public/themes/test-theme-6fcabb.css
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* @todo-theme-meta
|
||||||
|
{
|
||||||
|
"name": "Test Theme",
|
||||||
|
"id": "custom-1778671955013",
|
||||||
|
"group": "Custom",
|
||||||
|
"preview": [
|
||||||
|
"#f5f5f5",
|
||||||
|
"#ffffff",
|
||||||
|
"#274f69"
|
||||||
|
],
|
||||||
|
"hasWallpaper": false
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #f5f5f5;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface-strong: #ffffff;
|
||||||
|
--surface-muted: #fafafa;
|
||||||
|
--border: #d0d0d0;
|
||||||
|
--line: #b9b9b5;
|
||||||
|
--text: #1f1f1f;
|
||||||
|
--text-muted: #686866;
|
||||||
|
--text-strong: #111111;
|
||||||
|
--accent: #274f69;
|
||||||
|
--accent-text: #ffffff;
|
||||||
|
--accent-soft: #d6e4ec;
|
||||||
|
--sidebar-bg: #ffffff;
|
||||||
|
--sidebar-border: #d0d0d0;
|
||||||
|
--sidebar-text: #222222;
|
||||||
|
--sidebar-text-muted: #686866;
|
||||||
|
--input-bg: #ffffff;
|
||||||
|
--input-border: #cfcfcf;
|
||||||
|
--modal-bg: #ffffff;
|
||||||
|
--chip: #d8d8d8;
|
||||||
|
--success: #dff7e7;
|
||||||
|
}
|
||||||
37
public/themes/themestore-theme-by-came-0da6fd.css
Normal file
37
public/themes/themestore-theme-by-came-0da6fd.css
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* @todo-theme-meta
|
||||||
|
{
|
||||||
|
"name": "THemestore theme by Came",
|
||||||
|
"id": "custom-1778674717129",
|
||||||
|
"group": "Custom",
|
||||||
|
"preview": [
|
||||||
|
"#17acde",
|
||||||
|
"#222020",
|
||||||
|
"#00bbff"
|
||||||
|
],
|
||||||
|
"hasWallpaper": false
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #17acde;
|
||||||
|
--surface: #222020;
|
||||||
|
--surface-strong: #2bc582;
|
||||||
|
--surface-muted: #38363a;
|
||||||
|
--border: #000000;
|
||||||
|
--line: #ffffff;
|
||||||
|
--text: #d12e57;
|
||||||
|
--text-muted: #2f84c1;
|
||||||
|
--text-strong: #ff0000;
|
||||||
|
--accent: #00bbff;
|
||||||
|
--accent-text: #570000;
|
||||||
|
--accent-soft: #005f85;
|
||||||
|
--sidebar-bg: #004370;
|
||||||
|
--sidebar-border: #2a5070;
|
||||||
|
--sidebar-text: #d8f0ff;
|
||||||
|
--sidebar-text-muted: #7ab0d0;
|
||||||
|
--input-bg: #ffffff;
|
||||||
|
--input-border: #a0cce0;
|
||||||
|
--modal-bg: #ffffff;
|
||||||
|
--chip: #b0d8ec;
|
||||||
|
--success: #d0f0e0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user