implemented step 10 by Gemini
- added 4. step of wizard for style and images
This commit is contained in:
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user