implemented step 19 by Gemini
- Preview and Export
This commit is contained in:
@ -143,6 +143,14 @@ try {
|
|||||||
sendResponse(true, $result);
|
sendResponse(true, $result);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'exportWebsite':
|
||||||
|
$projectId = $data['project_id'] ?? null;
|
||||||
|
if (!$projectId) {
|
||||||
|
sendResponse(false, ['code' => 'MISSING_PROJECT_ID', 'message' => 'Project ID is required.'], 400);
|
||||||
|
}
|
||||||
|
sendResponse(true, $projectActions->exportWebsite($userId, $projectId));
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
sendResponse(false, ['code' => 'UNKNOWN_ACTION', 'message' => "Action '$action' is not defined."], 404);
|
sendResponse(false, ['code' => 'UNKNOWN_ACTION', 'message' => "Action '$action' is not defined."], 404);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -509,6 +509,102 @@ textarea:focus, input[type="text"]:focus, input[type="email"]:focus, input[type=
|
|||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Preview Step */
|
||||||
|
.step-header-with-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-toggle {
|
||||||
|
display: flex;
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
padding: 0.25rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-toggle {
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-toggle.active {
|
||||||
|
background-color: white;
|
||||||
|
color: var(--primary-color);
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-window {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background-color: #e2e8f0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-window iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 600px;
|
||||||
|
border: none;
|
||||||
|
background-color: white;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-window.mobile {
|
||||||
|
max-width: 375px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success / Export Screen */
|
||||||
|
.success-screen {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-actions {
|
||||||
|
margin: 2.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-large {
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions-box {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions-box h3 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions-box ol {
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions-box li {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|||||||
@ -317,19 +317,45 @@
|
|||||||
|
|
||||||
<!-- Step 7: Preview -->
|
<!-- Step 7: Preview -->
|
||||||
<div class="step" data-step="7">
|
<div class="step" data-step="7">
|
||||||
<h2>Náhľad webu</h2>
|
<div class="step-header-with-actions">
|
||||||
<p class="step-description">Pozrite si, ako vyzerá váš nový web.</p>
|
<div>
|
||||||
<div class="form-placeholder">
|
<h2>Náhľad vášho webu</h2>
|
||||||
<p>Obsah pre krok 7...</p>
|
<p class="step-description">Pozrite si, ako vyzerá váš nový web. Môžete prepínať medzi zobrazením pre počítač a mobil.</p>
|
||||||
|
</div>
|
||||||
|
<div class="device-toggle">
|
||||||
|
<button class="btn-toggle active" data-view="desktop">💻 PC</button>
|
||||||
|
<button class="btn-toggle" data-view="mobile">📱 Mobil</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="preview-container" class="preview-window">
|
||||||
|
<iframe id="preview-iframe" src="about:blank"></iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 8: Export -->
|
<!-- Step 8: Export -->
|
||||||
<div class="step" data-step="8">
|
<div class="step" data-step="8">
|
||||||
<h2>Hotovo!</h2>
|
<div class="success-screen">
|
||||||
<p class="step-description">Váš web je pripravený na stiahnutie.</p>
|
<div class="success-icon">🎉</div>
|
||||||
<div class="form-placeholder">
|
<h2>Váš web je pripravený!</h2>
|
||||||
<p>Obsah pre krok 8...</p>
|
<p>Gratulujeme! Váš nový web bol úspešne vygenerovaný a je pripravený na stiahnutie.</p>
|
||||||
|
|
||||||
|
<div class="export-actions">
|
||||||
|
<button id="btn-download-zip" class="btn-primary btn-large">Stiahnuť web (ZIP)</button>
|
||||||
|
<div style="margin-top: 1rem;">
|
||||||
|
<button id="btn-view-live" class="btn-secondary btn-small">👁️ Pozrieť LIVE v novom okne</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="instructions-box">
|
||||||
|
<h3>Čo robiť po stiahnutí?</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Rozbaľte stiahnutý .zip archív vo vašom počítači.</li>
|
||||||
|
<li>Nahrajte všetky súbory a priečinky na váš webhosting cez FTP.</li>
|
||||||
|
<li>Váš web bude okamžite funkčný na vašej doméne.</li>
|
||||||
|
<li>Ak ste zvolili <strong>Lokálny mód</strong> formulára, správy nájdete na adrese <code>/form-viewer.php</code>.</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -273,6 +273,24 @@ const App = {
|
|||||||
document.getElementById('config-smtp').classList.toggle('hidden', e.target.value !== 'smtp');
|
document.getElementById('config-smtp').classList.toggle('hidden', e.target.value !== 'smtp');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Step 7 events
|
||||||
|
document.querySelectorAll('.btn-toggle').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
document.querySelectorAll('.btn-toggle').forEach(b => b.classList.remove('active'));
|
||||||
|
e.target.classList.add('active');
|
||||||
|
const view = e.target.getAttribute('data-view');
|
||||||
|
document.getElementById('preview-container').className = 'preview-window ' + view;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 8 events
|
||||||
|
document.getElementById('btn-download-zip').addEventListener('click', () => this.downloadWebsite());
|
||||||
|
document.getElementById('btn-view-live').addEventListener('click', () => {
|
||||||
|
if (this.state.project) {
|
||||||
|
window.open(`/exports/${this.state.project.project_id}/index.html`, '_blank');
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
renderCategories() {
|
renderCategories() {
|
||||||
@ -534,9 +552,48 @@ const App = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.state.currentStep = n;
|
this.state.currentStep = n;
|
||||||
|
|
||||||
|
// Specific step logic
|
||||||
|
if (n === 7) {
|
||||||
|
this.loadPreview();
|
||||||
|
}
|
||||||
|
|
||||||
this.updateUI();
|
this.updateUI();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
loadPreview() {
|
||||||
|
const iframe = document.getElementById('preview-iframe');
|
||||||
|
const timestamp = new Date().getTime();
|
||||||
|
iframe.src = `/exports/${this.state.project.project_id}/index.html?t=${timestamp}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
async downloadWebsite() {
|
||||||
|
const btn = document.getElementById('btn-download-zip');
|
||||||
|
const originalText = btn.textContent;
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Pripravujem archív...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.apiCall('exportWebsite');
|
||||||
|
if (result.success) {
|
||||||
|
// Trigger download
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = '/' + result.data.download_url;
|
||||||
|
link.download = result.data.filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Export failed:', error);
|
||||||
|
alert('Chyba pri generovaní ZIP archívu.');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = originalText;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async nextStep() {
|
async nextStep() {
|
||||||
if (this.state.currentStep === 1) {
|
if (this.state.currentStep === 1) {
|
||||||
if (!this.state.selection.category || !this.state.selection.subcategory) {
|
if (!this.state.selection.category || !this.state.selection.subcategory) {
|
||||||
|
|||||||
@ -257,4 +257,55 @@ class ProjectActions
|
|||||||
'mime_type' => $mimeType
|
'mime_type' => $mimeType
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a ZIP archive of the generated website.
|
||||||
|
*/
|
||||||
|
public function exportWebsite(string $userId, string $projectId): array
|
||||||
|
{
|
||||||
|
// 1. Verify ownership
|
||||||
|
$this->getProjectStatus($userId, $projectId);
|
||||||
|
|
||||||
|
$projectExportDir = __DIR__ . "/../../exports/{$projectId}";
|
||||||
|
if (!is_dir($projectExportDir)) {
|
||||||
|
throw new Exception("Generated website not found. Please generate it first.", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$zipFileName = "web-" . $projectId . ".zip";
|
||||||
|
$zipPath = $projectExportDir . "/" . $zipFileName;
|
||||||
|
|
||||||
|
$zip = new \ZipArchive();
|
||||||
|
if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
|
||||||
|
throw new Exception("Could not create ZIP archive.", 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively add files
|
||||||
|
$files = new \RecursiveIteratorIterator(
|
||||||
|
new \RecursiveDirectoryIterator($projectExportDir, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||||
|
\RecursiveIteratorIterator::LEAVES_ONLY
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($files as $name => $file) {
|
||||||
|
// Skip the ZIP file itself and temporary wizard files if any
|
||||||
|
if (!$file->isDir()) {
|
||||||
|
$filePath = $file->getRealPath();
|
||||||
|
$relativePath = substr($filePath, strlen($projectExportDir) + 1);
|
||||||
|
|
||||||
|
// Don't include the zip we are currently creating
|
||||||
|
if ($relativePath === $zipFileName) continue;
|
||||||
|
|
||||||
|
// Don't include .htaccess if it's in the root (optional, but data/.htaccess should stay)
|
||||||
|
// Actually, we want to include all site files.
|
||||||
|
|
||||||
|
$zip->addFile($filePath, $relativePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'download_url' => "exports/{$projectId}/" . $zipFileName,
|
||||||
|
'filename' => $zipFileName
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user