From c11f7e4d75450598085f979d0d97783239c7f8fd Mon Sep 17 00:00:00 2001 From: igor Date: Sun, 14 Jun 2026 08:07:23 +0200 Subject: [PATCH] implemented step 10 by Gemini - added 4. step of wizard for style and images --- public/ajax.php | 31 +++++-- public/css/wizard.css | 130 ++++++++++++++++++++++++++++ public/index.html | 81 +++++++++++++++++- public/js/wizard.js | 151 ++++++++++++++++++++++++++++++++- src/Actions/ProjectActions.php | 57 +++++++++++++ 5 files changed, 440 insertions(+), 10 deletions(-) diff --git a/public/ajax.php b/public/ajax.php index 49f28c4..49ae8cc 100644 --- a/public/ajax.php +++ b/public/ajax.php @@ -30,11 +30,23 @@ try { sendResponse(false, ['code' => 'METHOD_NOT_ALLOWED', 'message' => 'Only POST requests are allowed.'], 405); } - // Read JSON input + // Read JSON input or FormData $input = file_get_contents('php://input'); $data = json_decode($input, true); - if (json_last_error() !== JSON_ERROR_NONE) { + // If multipart/form-data, action and project_id are in $_POST + if (!$data && !empty($_POST)) { + $data = [ + 'action' => $_POST['action'] ?? null, + 'project_id' => $_POST['project_id'] ?? null, + 'payload' => $_POST['payload'] ?? [] + ]; + if (is_string($data['payload'])) { + $data['payload'] = json_decode($data['payload'], true); + } + } + + if (!$data && $_SERVER['REQUEST_METHOD'] === 'POST') { sendResponse(false, ['code' => 'INVALID_JSON', 'message' => 'Invalid JSON input.'], 400); } @@ -44,10 +56,10 @@ try { sendResponse(false, ['code' => 'MISSING_ACTION', 'message' => 'Action is required.'], 400); } - // Check X-User-ID header (except for initSession if we want to allow it) - $userId = $_SERVER['HTTP_X_USER_ID'] ?? null; + // Check X-User-ID header (except for initSession) + $userId = $_SERVER['HTTP_X_USER_ID'] ?? $_POST['X-User-ID'] ?? null; if (!$userId && $action !== 'initSession') { - sendResponse(false, ['code' => 'UNAUTHORIZED', 'message' => 'X-User-ID header is missing.'], 401); + sendResponse(false, ['code' => 'UNAUTHORIZED', 'message' => 'X-User-ID is missing.'], 401); } // Router @@ -109,6 +121,15 @@ try { sendResponse($success, ['message' => 'Consent saved successfully.']); break; + case 'uploadAsset': + $projectId = $data['project_id'] ?? null; + if (!$projectId || empty($_FILES['file'])) { + sendResponse(false, ['code' => 'MISSING_DATA', 'message' => 'Project ID and file are required.'], 400); + } + $result = $projectActions->uploadAsset($userId, $projectId, $_FILES['file']); + sendResponse(true, $result); + break; + default: sendResponse(false, ['code' => 'UNKNOWN_ACTION', 'message' => "Action '$action' is not defined."], 404); break; diff --git a/public/css/wizard.css b/public/css/wizard.css index 0bbde85..5b53ad0 100644 --- a/public/css/wizard.css +++ b/public/css/wizard.css @@ -313,6 +313,136 @@ textarea:focus, input[type="text"]:focus, input[type="email"]:focus { margin-bottom: 1.5rem; } +/* Palette Grid */ +.palette-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.palette-card { + border: 2px solid var(--border-color); + border-radius: 0.5rem; + padding: 1rem; + cursor: pointer; + transition: all 0.2s; + display: flex; + flex-direction: column; + gap: 0.75rem; + background-color: white; +} + +.palette-card:hover { + border-color: var(--primary-color); +} + +.palette-card.selected { + border-color: var(--primary-color); + box-shadow: 0 0 0 1px var(--primary-color); +} + +.palette-colors { + display: flex; + height: 1.5rem; + border-radius: 0.25rem; + overflow: hidden; +} + +.palette-colors span { + flex: 1; +} + +/* Upload UI */ +.upload-box { + border: 2px dashed var(--border-color); + border-radius: 0.5rem; + padding: 2rem; + text-align: center; + cursor: pointer; + transition: all 0.2s; + background-color: #f8fafc; + position: relative; + min-height: 120px; + display: flex; + align-items: center; + justify-content: center; +} + +.upload-box:hover { + border-color: var(--primary-color); + background-color: #eff6ff; +} + +.upload-box.mini { + padding: 1rem; + min-height: 80px; + width: 80px; + font-size: 1.5rem; + color: var(--text-muted); +} + +.image-preview { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + padding: 0.5rem; +} + +.image-preview img { + width: 100%; + height: 100%; + object-fit: contain; + border-radius: 0.25rem; +} + +.gallery-upload-grid { + display: flex; + flex-wrap: wrap; + gap: 1rem; +} + +.gallery-previews { + display: flex; + flex-wrap: wrap; + gap: 1rem; +} + +.gallery-item { + width: 80px; + height: 80px; + border-radius: 0.5rem; + border: 1px solid var(--border-color); + overflow: hidden; + position: relative; +} + +.gallery-item img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.btn-remove-asset { + position: absolute; + top: -5px; + right: -5px; + background: #ef4444; + color: white; + width: 20px; + height: 20px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + cursor: pointer; + border: none; + padding: 0; +} + button:disabled { opacity: 0.5; cursor: not-allowed; diff --git a/public/index.html b/public/index.html index a021f8b..bf7a856 100644 --- a/public/index.html +++ b/public/index.html @@ -132,10 +132,83 @@
-

Vizuálny štýl

-

Ako by mal váš nový web vyzerať?

-
-

Obsah pre krok 4...

+

Vizuálny štýl a assety

+

Ako by mal váš web vyzerať? Vyberte si štýl a nahrajte vlastné obrázky.

+ +
+

Dizajnový smer

+
+
+
+
Minimalistický
+
Čistý, veľa bieleho priestoru
+
+
+
🎨
+
Moderný
+
Svieži, výrazné prvky
+
+
+
💎
+
Premium
+
Elegantný, tmavé témy
+
+
+
+ +
+

Farebná paleta

+
+
+
+ + + +
+ Profesionálna modrá +
+
+
+ + + +
+ Prírodná zelená +
+
+
+ + + +
+ Elegantná zlatá +
+
+
+ +
+

Vaše assety

+
+ +
+ +
+ Kliknite pre nahranie loga +
+ +
+
+ +
+ + +
diff --git a/public/js/wizard.js b/public/js/wizard.js index 5a7e1b1..321e80e 100644 --- a/public/js/wizard.js +++ b/public/js/wizard.js @@ -12,7 +12,11 @@ const App = { selection: { category: null, subcategory: null, - customDescription: '' + customDescription: '', + style: null, + palette: null, + logo: null, + gallery: [] } }, @@ -187,6 +191,27 @@ const App = { // Step 3 events document.getElementById('btn-add-service').addEventListener('click', () => this.addServiceItem()); + + // Step 4 events + document.querySelectorAll('#style-grid .category-card').forEach(card => { + card.addEventListener('click', () => { + this.state.selection.style = card.getAttribute('data-style'); + this.updateStyleSelection(); + }); + }); + + document.querySelectorAll('#palette-list .palette-card').forEach(card => { + card.addEventListener('click', () => { + this.state.selection.palette = card.getAttribute('data-palette'); + this.updatePaletteSelection(); + }); + }); + + document.getElementById('logo-upload-box').addEventListener('click', () => document.getElementById('logo-input').click()); + document.getElementById('logo-input').addEventListener('change', (e) => this.handleFileUpload(e, 'logo')); + + document.getElementById('gallery-add-box').addEventListener('click', () => document.getElementById('gallery-input').click()); + document.getElementById('gallery-input').addEventListener('change', (e) => this.handleFileUpload(e, 'gallery')); }, renderCategories() { @@ -341,6 +366,103 @@ const App = { }); }, + updateStyleSelection() { + document.querySelectorAll('#style-grid .category-card').forEach(card => { + card.classList.toggle('selected', card.getAttribute('data-style') === this.state.selection.style); + }); + this.updateUI(); + }, + + updatePaletteSelection() { + document.querySelectorAll('#palette-list .palette-card').forEach(card => { + card.classList.toggle('selected', card.getAttribute('data-palette') === this.state.selection.palette); + }); + this.updateUI(); + }, + + async handleFileUpload(event, target) { + const files = event.target.files; + if (!files.length) return; + + for (const file of files) { + const formData = new FormData(); + formData.append('action', 'uploadAsset'); + formData.append('project_id', this.state.project.project_id); + formData.append('file', file); + formData.append('X-User-ID', this.state.userId); + + try { + const response = await fetch('/ajax.php', { + method: 'POST', + body: formData + }); + const result = await response.json(); + if (result.success) { + if (target === 'logo') { + this.state.selection.logo = result.data.path; + this.renderLogoPreview(); + } else { + if (this.state.selection.gallery.length < 5) { + this.state.selection.gallery.push(result.data.path); + this.renderGalleryPreviews(); + } else { + alert('Maximálne 5 fotografií.'); + } + } + } else { + alert('Upload zlyhal: ' + result.error.message); + } + } catch (error) { + console.error('Upload error:', error); + alert('Chyba pri nahrávaní.'); + } + } + this.updateUI(); + }, + + renderLogoPreview() { + const preview = document.getElementById('logo-preview'); + preview.innerHTML = `Logo + `; + preview.classList.remove('hidden'); + document.querySelector('#logo-upload-box .upload-placeholder').classList.add('hidden'); + + document.getElementById('btn-remove-logo').addEventListener('click', (e) => { + e.stopPropagation(); + this.removeAsset('logo'); + }); + }, + + renderGalleryPreviews() { + const container = document.getElementById('gallery-previews'); + container.innerHTML = ''; + this.state.selection.gallery.forEach((path, index) => { + const item = document.createElement('div'); + item.className = 'gallery-item'; + item.innerHTML = `Gallery ${index+1} + `; + + item.querySelector('.btn-remove-asset').addEventListener('click', (e) => { + e.stopPropagation(); + this.removeAsset('gallery', index); + }); + + container.appendChild(item); + }); + }, + + removeAsset(target, index = null) { + if (target === 'logo') { + this.state.selection.logo = null; + document.getElementById('logo-preview').classList.add('hidden'); + document.querySelector('#logo-upload-box .upload-placeholder').classList.remove('hidden'); + } else { + this.state.selection.gallery.splice(index, 1); + this.renderGalleryPreviews(); + } + this.updateUI(); + }, + showStep(n) { const steps = document.querySelectorAll('.step'); steps.forEach(step => step.classList.remove('active')); @@ -463,6 +585,31 @@ const App = { alert('Nepodarilo sa uložiť dáta: ' + error.message); return; } + } else if (this.state.currentStep === 4) { + if (!this.state.selection.style || !this.state.selection.palette) { + alert('Prosím, vyberte vizuálny štýl a farebnú paletu.'); + return; + } + + try { + await this.apiCall('saveStep', { + step: 4, + data: { + visuals: { + style: this.state.selection.style, + palette: this.state.selection.palette + }, + assets: { + logo: this.state.selection.logo, + gallery: this.state.selection.gallery + } + } + }); + } catch (error) { + console.error('Save step 4 failed:', error); + alert('Nepodarilo sa uložiť dáta: ' + error.message); + return; + } } if (this.state.currentStep < this.state.totalSteps) { @@ -492,6 +639,8 @@ const App = { const phone = document.getElementById('contact-phone').value; const gdpr = document.getElementById('gdpr-consent').checked; nextDisabled = !name || !gdpr || (!email && !phone); + } else if (this.state.currentStep === 4) { + nextDisabled = !this.state.selection.style || !this.state.selection.palette; } btnNext.disabled = nextDisabled; diff --git a/src/Actions/ProjectActions.php b/src/Actions/ProjectActions.php index c7184b5..08a7ec3 100644 --- a/src/Actions/ProjectActions.php +++ b/src/Actions/ProjectActions.php @@ -163,6 +163,14 @@ class ProjectActions $projectData['wizard_data']['services'] = $data['services']; $projectData['wizard_data']['smart_answers'] = $data['smart_answers']; break; + + case 4: + if (!isset($data['visuals']) || !isset($data['assets'])) { + throw new Exception("Missing visuals or assets data.", 400); + } + $projectData['wizard_data']['visuals'] = $data['visuals']; + $projectData['wizard_data']['assets'] = $data['assets']; + break; // More steps will be added later } @@ -172,4 +180,53 @@ class ProjectActions return $this->storage->put($projectPath, $projectData); } + + /** + * Handles secure asset upload. + */ + public function uploadAsset(string $userId, string $projectId, array $file): array + { + // 1. Ownership check + $this->getProjectStatus($userId, $projectId); + + // 2. Validation + $maxSize = 2 * 1024 * 1024; // 2MB + if ($file['size'] > $maxSize) { + throw new Exception("File is too large (max 2MB).", 400); + } + + $allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/svg+xml']; + $finfo = new \finfo(FILEINFO_MIME_TYPE); + $mimeType = $finfo->file($file['tmp_name']); + + if (!in_array($mimeType, $allowedMimeTypes)) { + throw new Exception("Invalid file type. Allowed: JPG, PNG, WEBP, SVG.", 400); + } + + $extension = pathinfo($file['name'], PATHINFO_EXTENSION); + $allowedExtensions = ['jpg', 'jpeg', 'png', 'webp', 'svg']; + if (!in_array(strtolower($extension), $allowedExtensions)) { + throw new Exception("Invalid file extension.", 400); + } + + // 3. Prepare storage + $uploadDir = __DIR__ . "/../../exports/{$projectId}/assets/images"; + if (!is_dir($uploadDir)) { + mkdir($uploadDir, 0777, true); + } + + // 4. Secure filename + $filename = bin2hex(random_bytes(8)) . '.' . $extension; + $targetPath = $uploadDir . '/' . $filename; + + if (!move_uploaded_file($file['tmp_name'], $targetPath)) { + throw new Exception("Failed to move uploaded file.", 500); + } + + return [ + 'path' => "exports/{$projectId}/assets/images/{$filename}", + 'filename' => $filename, + 'mime_type' => $mimeType + ]; + } }