implemented step 10 by Gemini

- added 4. step of wizard for style and images
This commit is contained in:
2026-06-14 08:07:23 +02:00
parent 991ff9de00
commit c11f7e4d75
5 changed files with 440 additions and 10 deletions

View File

@ -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;

View File

@ -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;

View File

@ -132,10 +132,83 @@
<!-- Step 4: Vizuálny štýl -->
<div class="step" data-step="4">
<h2>Vizuálny štýl</h2>
<p class="step-description">Ako by mal váš nový web vyzerať?</p>
<div class="form-placeholder">
<p>Obsah pre krok 4...</p>
<h2>Vizuálny štýl a assety</h2>
<p class="step-description">Ako by mal váš web vyzerať? Vyberte si štýl a nahrajte vlastné obrázky.</p>
<div class="form-section">
<h3>Dizajnový smer</h3>
<div id="style-grid" class="category-grid">
<div class="category-card" data-style="minimal">
<div class="category-icon"></div>
<div class="category-name">Minimalistický</div>
<div class="form-hint" style="font-size: 0.7rem;">Čistý, veľa bieleho priestoru</div>
</div>
<div class="category-card" data-style="modern">
<div class="category-icon">🎨</div>
<div class="category-name">Moderný</div>
<div class="form-hint" style="font-size: 0.7rem;">Svieži, výrazné prvky</div>
</div>
<div class="category-card" data-style="premium">
<div class="category-icon">💎</div>
<div class="category-name">Premium</div>
<div class="form-hint" style="font-size: 0.7rem;">Elegantný, tmavé témy</div>
</div>
</div>
</div>
<div class="form-section">
<h3>Farebná paleta</h3>
<div id="palette-list" class="palette-grid">
<div class="palette-card" data-palette="blue-gray">
<div class="palette-colors">
<span style="background: #2563eb;"></span>
<span style="background: #64748b;"></span>
<span style="background: #f8fafc;"></span>
</div>
<span>Profesionálna modrá</span>
</div>
<div class="palette-card" data-palette="nature-green">
<div class="palette-colors">
<span style="background: #059669;"></span>
<span style="background: #fbbf24;"></span>
<span style="background: #fdfdfd;"></span>
</div>
<span>Prírodná zelená</span>
</div>
<div class="palette-card" data-palette="elegant-gold">
<div class="palette-colors">
<span style="background: #111827;"></span>
<span style="background: #d4af37;"></span>
<span style="background: #ffffff;"></span>
</div>
<span>Elegantná zlatá</span>
</div>
</div>
</div>
<div class="form-section">
<h3>Vaše assety</h3>
<div class="upload-container">
<label>Logo firmy</label>
<div class="upload-box" id="logo-upload-box">
<input type="file" id="logo-input" accept="image/*" class="hidden">
<div class="upload-placeholder">
<span>Kliknite pre nahranie loga</span>
</div>
<div id="logo-preview" class="image-preview hidden"></div>
</div>
</div>
<div class="upload-container" style="margin-top: 2rem;">
<label>Fotografie do galérie (max 5)</label>
<div class="gallery-upload-grid">
<div id="gallery-previews" class="gallery-previews"></div>
<div class="upload-box mini" id="gallery-add-box">
<input type="file" id="gallery-input" accept="image/*" multiple class="hidden">
<span>+</span>
</div>
</div>
</div>
</div>
</div>

View File

@ -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 = `<img src="/${this.state.selection.logo}" alt="Logo">
<button class="btn-remove-asset" id="btn-remove-logo">×</button>`;
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 = `<img src="/${path}" alt="Gallery ${index+1}">
<button class="btn-remove-asset" data-index="${index}">×</button>`;
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;

View File

@ -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
];
}
}