Compare commits
10 Commits
4f62bb7aa7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 60127ec0a7 | |||
| 460f357d48 | |||
| cea97332b4 | |||
| 8ed5413116 | |||
| c0f13495ce | |||
| 4135b621c4 | |||
| 269cc5f5d5 | |||
| 029d7a232a | |||
| d350daa1d8 | |||
| 7efd6b24a5 |
58
AGENTS.md
Normal file
58
AGENTS.md
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# WebWizard - Agent's Project Guide
|
||||||
|
|
||||||
|
Welcome! This file provides a high-level overview of the **WebWizard** project to help you understand the architecture, conventions, and workflows.
|
||||||
|
|
||||||
|
## 📝 Project Overview
|
||||||
|
WebWizard is an **AI-driven website concierge** (MVP) that allows users to create static websites through a guided wizard. It collects business information, uses AI (DAIAPI) to generate marketing copy and SEO metadata, and then renders a complete, standalone static website for export.
|
||||||
|
|
||||||
|
## 🛠️ Technical Stack
|
||||||
|
- **Backend:** PHP 8.2+ (Pure PHP, no external frameworks like Laravel or Symfony).
|
||||||
|
- **Frontend:** Vanilla JavaScript (ES6+), CSS Grid/Flexbox (No UI libraries like Bootstrap).
|
||||||
|
- **Data Persistence:** File-based JSON storage in the `data/` directory. No database.
|
||||||
|
- **Autoloading:** Composer (PSR-4, Namespace: `App\`).
|
||||||
|
- **Indentation:** **STRICTLY TABS** (Global preference).
|
||||||
|
|
||||||
|
## 🏗️ Architecture & Workflow
|
||||||
|
The application follows a linear data-gathering process:
|
||||||
|
|
||||||
|
1. **Wizard (Frontend)**: 8 steps in `public/index.html` managed by `public/js/wizard.js`.
|
||||||
|
2. **API Layer**: All communication goes through `public/ajax.php` (POST with JSON payload).
|
||||||
|
3. **Actions**: Logic residing in `src/Actions/` (e.g., `ProjectActions`, `TaskActions`).
|
||||||
|
4. **AI Queuing**: When wizard finishes, a task is created in `data/llm/`.
|
||||||
|
5. **Worker**: `scripts/worker.php` processes the queue, calls `DAIClient`, and stores generated content.
|
||||||
|
6. **Renderer**: `src/Services/Renderer.php` combines user data + AI content + PHP templates (`src/Templates/`) into a static site in `exports/<project_id>/`.
|
||||||
|
7. **Export**: The final site is zipped for download.
|
||||||
|
|
||||||
|
## 📂 Directory Structure
|
||||||
|
- `public/`: Web root. Contains `index.html`, `ajax.php`, `css/`, and `js/`.
|
||||||
|
- `src/`: PHP source code (`App\` namespace).
|
||||||
|
- `Actions/`: API controllers/handlers.
|
||||||
|
- `Services/`: Business logic (FileStorage, Renderer, LLMpool, DAIClient).
|
||||||
|
- `Templates/`: PHP šablóny for the generated websites.
|
||||||
|
- `Utils/`: Helpers and Configuration loader.
|
||||||
|
- `data/`: Protected data storage (Users, Projects, Consent, AI tasks). Protected by `.htaccess`.
|
||||||
|
- `exports/`: Destination for generated websites. Subfolders are project-specific.
|
||||||
|
- `scripts/`: CLI scripts (e.g., the `worker.php`).
|
||||||
|
- `tests/`: Debug and connectivity test scripts.
|
||||||
|
|
||||||
|
## ⚙️ Configuration
|
||||||
|
- `.env`: Local environment settings (e.g., `DAIAPI_URL`).
|
||||||
|
- `Config.php`: Utility to load and access these settings.
|
||||||
|
|
||||||
|
## 🔒 Security Principles
|
||||||
|
- **Protected Data**: Direct web access to `data/` and `exports/` subfolders (except public assets) is blocked.
|
||||||
|
- **Sanitization**: All output in templates uses the `e()` helper (htmlspecialchars).
|
||||||
|
- **Exports**: Generated sites use `config.php` (not JSON) to hide SMTP credentials.
|
||||||
|
- **Passwords**: Local viewer passwords are hashed using `password_hash()`.
|
||||||
|
|
||||||
|
## 🤖 AI Integration (DAIAPI)
|
||||||
|
- Connects to a local/VPN API at `http://10.2.8.1:9001/run` (DEV) or `http://192.168.122.10:9001/run` (PROD).
|
||||||
|
- Prompt logic is in `src/Prompts/ContentPrompt.php`. It forces JSON output and forbids HTML tags.
|
||||||
|
|
||||||
|
## 🚀 Common Commands
|
||||||
|
- **Run Worker (Continuous)**: `php scripts/worker.php --loop`
|
||||||
|
- **Test AI Connectivity**: `php tests/debug_dai.php`
|
||||||
|
- **Regenerate Autoloader**: `composer dump-autoload`
|
||||||
|
|
||||||
|
---
|
||||||
|
*Always respect the "No Frameworks" and "Tabs only" rules. Keep the code surgical and sémantic.*
|
||||||
78
README.md
78
README.md
@ -1,3 +1,77 @@
|
|||||||
# WebWizard
|
# WebWizard - AI Website Concierge (MVP)
|
||||||
|
|
||||||
Simple wizard for create website with LLM.
|
WebWizard je inteligentný asistent na tvorbu statických webových stránok pre malé podniky a živnostníkov. Pomocou intuitívneho wizardu a umelej inteligencie (DAIAPI) premení vaše základné informácie na profesionálny, responzívny web pripravený na nasadenie.
|
||||||
|
|
||||||
|
## 🚀 Hlavné funkcie
|
||||||
|
- **Inteligentný Wizard**: 8 krokov od definície biznisu až po finálny export.
|
||||||
|
- **AI Content Generation**: Automatické písanie marketingových textov a SEO metadát cez DAIAPI.
|
||||||
|
- **Vizuálna personalizácia**: Výber z preddefinovaných farebných paliet a dizajnových štýlov.
|
||||||
|
- **Správa Assetov**: Asynchrónne nahrávanie loga a fotogalérie.
|
||||||
|
- **Kontaktný formulár**: Podpora SMTP odosielania alebo lokálneho ukladania správ s vlastným prehliadačom.
|
||||||
|
- **Statický Export**: Kompletný balíček webu v ZIP formáte pripravený na FTP upload.
|
||||||
|
|
||||||
|
## 🛠️ Technický Stack
|
||||||
|
- **Backend**: PHP 8.2+ (bez frameworkov, čistý kód).
|
||||||
|
- **Frontend**: Vanilla JavaScript (ES6+), CSS Grid/Flexbox.
|
||||||
|
- **Dáta**: Súborový systém (JSON), žiadna databáza.
|
||||||
|
- **AI**: Integrácia s lokálnym DAIAPI.
|
||||||
|
|
||||||
|
## 📋 Požiadavky
|
||||||
|
- Webový server (Apache/Nginx) s podporou PHP 8.2.
|
||||||
|
- PHP rozšírenia: `curl`, `zip`, `fileinfo`.
|
||||||
|
- Povolený zápis (write permissions) do priečinkov `data/` a `exports/`.
|
||||||
|
|
||||||
|
## ⚙️ Inštalácia
|
||||||
|
1. Naklonujte repozitár na váš server.
|
||||||
|
2. Spustite `composer install` pre vygenerovanie autoloadera.
|
||||||
|
3. Skopírujte `.env.example` do `.env` a nastavte URL pre vaše DAIAPI:
|
||||||
|
```ini
|
||||||
|
DAIAPI_URL=http://10.2.8.1:9001/run
|
||||||
|
```
|
||||||
|
4. Nastavte Document Root vášho webservera do priečinka `public/`.
|
||||||
|
|
||||||
|
### Príklad NGINX konfigurácie
|
||||||
|
Pre správne fungovanie (najmä prístup k náhľadom v `/exports`) odporúčame nasledovnú konfiguráciu:
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name webwizard.test;
|
||||||
|
|
||||||
|
root /cesta/k/projektu/public;
|
||||||
|
|
||||||
|
# Sprístupnenie vygenerovaných webov pre náhľad
|
||||||
|
location /exports {
|
||||||
|
alias /cesta/k/projektu/exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
index index.php index.html;
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
include php.conf; # alebo vaša konfigurácia pre PHP-FPM
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🤖 Spustenie Workera
|
||||||
|
Pre spracovanie AI úloh na pozadí je potrebné spustiť worker skript:
|
||||||
|
|
||||||
|
**Manuálne (DEV):**
|
||||||
|
```powershell
|
||||||
|
php scripts/worker.php --loop
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cez Cron (PROD):**
|
||||||
|
Nastavte spúšťanie každú minútu:
|
||||||
|
```cron
|
||||||
|
* * * * * /usr/bin/php /cesta/k/projektu/scripts/worker.php >> /cesta/k/projektu/data/worker.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 Bezpečnosť
|
||||||
|
- Všetky vstupné dáta sú sanitizované.
|
||||||
|
- Prístup do `data/` je blokovaný cez `.htaccess`.
|
||||||
|
- Citlivé konfigurácie exportovaných webov sú uložené v `.php` súboroch.
|
||||||
|
- Heslá pre prezeranie správ sú hašované pomocou `password_hash()`.
|
||||||
|
|
||||||
|
## 📄 Licencia
|
||||||
|
MIT
|
||||||
|
|||||||
@ -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() {
|
||||||
@ -317,6 +335,8 @@ const App = {
|
|||||||
} else {
|
} else {
|
||||||
document.getElementById('custom-category-group').classList.add('hidden');
|
document.getElementById('custom-category-group').classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateUI();
|
||||||
},
|
},
|
||||||
|
|
||||||
renderSubcategories(categoryId) {
|
renderSubcategories(categoryId) {
|
||||||
@ -335,6 +355,7 @@ const App = {
|
|||||||
chip.addEventListener('click', () => {
|
chip.addEventListener('click', () => {
|
||||||
this.state.selection.subcategory = sub.id;
|
this.state.selection.subcategory = sub.id;
|
||||||
this.renderSubcategories(categoryId);
|
this.renderSubcategories(categoryId);
|
||||||
|
this.updateUI();
|
||||||
});
|
});
|
||||||
|
|
||||||
container.appendChild(chip);
|
container.appendChild(chip);
|
||||||
@ -534,9 +555,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) {
|
||||||
@ -727,7 +787,7 @@ const App = {
|
|||||||
try {
|
try {
|
||||||
const result = await this.apiCall('generateWebsite');
|
const result = await this.apiCall('generateWebsite');
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log('Generation started:', result.data.task_id);
|
|
||||||
this.startPolling();
|
this.startPolling();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -760,7 +820,7 @@ const App = {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Polling error:', error);
|
console.error('Polling error:', error);
|
||||||
}
|
}
|
||||||
}, 10000);
|
}, 5000);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateGenerationStatus(status) {
|
updateGenerationStatus(status) {
|
||||||
|
|||||||
@ -19,6 +19,10 @@ if (php_sapi_name() !== 'cli') {
|
|||||||
die("This script can only be run from the command line." . PHP_EOL);
|
die("This script can only be run from the command line." . PHP_EOL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse arguments
|
||||||
|
$isLoop = in_array('--loop', $argv);
|
||||||
|
$loopInterval = 5; // seconds
|
||||||
|
|
||||||
$lockFile = sys_get_temp_dir() . '/webwizard_worker.lock';
|
$lockFile = sys_get_temp_dir() . '/webwizard_worker.lock';
|
||||||
$fp = fopen($lockFile, 'w');
|
$fp = fopen($lockFile, 'w');
|
||||||
|
|
||||||
@ -26,17 +30,39 @@ if (!flock($fp, LOCK_EX | LOCK_NB)) {
|
|||||||
die("[" . date('Y-m-d H:i:s') . "] Worker is already running. Skipping." . PHP_EOL);
|
die("[" . date('Y-m-d H:i:s') . "] Worker is already running. Skipping." . PHP_EOL);
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "[" . date('Y-m-d H:i:s') . "] Starting LLMpool worker..." . PHP_EOL;
|
if ($isLoop) {
|
||||||
|
echo "[" . date('Y-m-d H:i:s') . "] Worker started in loop mode (interval: {$loopInterval}s)" . PHP_EOL;
|
||||||
try {
|
} else {
|
||||||
$worker = new LLMpool();
|
echo "[" . date('Y-m-d H:i:s') . "] Starting LLMpool worker (single run)..." . PHP_EOL;
|
||||||
$worker->processQueue();
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
echo "[" . date('Y-m-d H:i:s') . "] FATAL ERROR: " . $e->getMessage() . PHP_EOL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "[" . date('Y-m-d H:i:s') . "] Finished LLMpool worker." . PHP_EOL;
|
$worker = new LLMpool();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main processing logic encapsulated to avoid duplication
|
||||||
|
*/
|
||||||
|
$processCycle = function() use ($worker) {
|
||||||
|
echo "[" . date('Y-m-d H:i:s') . "] Cycle started: Processing queue..." . PHP_EOL;
|
||||||
|
try {
|
||||||
|
$worker->processQueue();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
echo "[" . date('Y-m-d H:i:s') . "] FATAL ERROR: " . $e->getMessage() . PHP_EOL;
|
||||||
|
}
|
||||||
|
echo "[" . date('Y-m-d H:i:s') . "] Cycle finished." . PHP_EOL;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execution based on mode
|
||||||
|
if ($isLoop) {
|
||||||
|
while (true) {
|
||||||
|
$processCycle();
|
||||||
|
sleep($loopInterval);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$processCycle();
|
||||||
|
echo "[" . date('Y-m-d H:i:s') . "] Finished LLMpool worker." . PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// These are only reached in non-loop mode (or if loop is broken by signal)
|
||||||
flock($fp, LOCK_UN);
|
flock($fp, LOCK_UN);
|
||||||
fclose($fp);
|
fclose($fp);
|
||||||
unlink($lockFile);
|
unlink($lockFile);
|
||||||
|
|||||||
@ -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
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,12 +14,14 @@ class LLMpool
|
|||||||
private FileStorage $storage;
|
private FileStorage $storage;
|
||||||
private DAIClient $daiClient;
|
private DAIClient $daiClient;
|
||||||
private ContentPrompt $promptGenerator;
|
private ContentPrompt $promptGenerator;
|
||||||
|
private Renderer $renderer;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->storage = new FileStorage();
|
$this->storage = new FileStorage();
|
||||||
$this->daiClient = new DAIClient();
|
$this->daiClient = new DAIClient();
|
||||||
$this->promptGenerator = new ContentPrompt();
|
$this->promptGenerator = new ContentPrompt();
|
||||||
|
$this->renderer = new Renderer();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -91,12 +93,21 @@ class LLMpool
|
|||||||
}
|
}
|
||||||
|
|
||||||
$projectData['content']['generated'] = $content;
|
$projectData['content']['generated'] = $content;
|
||||||
|
$this->storage->put("projects/{$projectId}.json", $projectData);
|
||||||
|
|
||||||
|
// 5. Render static site
|
||||||
|
if (!$this->renderer->render($projectId)) {
|
||||||
|
throw new Exception("Rendering failed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Update Project Status
|
||||||
|
$projectData = $this->storage->get("projects/{$projectId}.json");
|
||||||
$projectData['status'] = 'ready'; // Transition to ready
|
$projectData['status'] = 'ready'; // Transition to ready
|
||||||
$projectData['current_step'] = 7; // Automatically ready for preview
|
$projectData['current_step'] = 7; // Automatically ready for preview
|
||||||
$projectData['updated_at'] = date('c');
|
$projectData['updated_at'] = date('c');
|
||||||
$this->storage->put("projects/{$projectId}.json", $projectData);
|
$this->storage->put("projects/{$projectId}.json", $projectData);
|
||||||
|
|
||||||
// 6. Finish Task (Delete or Archive)
|
// 7. Finish Task (Delete or Archive)
|
||||||
$this->storage->delete($taskPath);
|
$this->storage->delete($taskPath);
|
||||||
|
|
||||||
error_log("LLMpool: Task $taskFilename processed successfully for project $projectId.");
|
error_log("LLMpool: Task $taskFilename processed successfully for project $projectId.");
|
||||||
|
|||||||
188
src/Services/Renderer.php
Normal file
188
src/Services/Renderer.php
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../Utils/Helpers.php';
|
||||||
|
|
||||||
|
class Renderer
|
||||||
|
{
|
||||||
|
private FileStorage $storage;
|
||||||
|
private string $exportsPath;
|
||||||
|
private string $templatesPath;
|
||||||
|
|
||||||
|
private array $palettes = [
|
||||||
|
'blue-gray' => [
|
||||||
|
'primary' => '#2563eb',
|
||||||
|
'secondary' => '#64748b',
|
||||||
|
'bg' => '#f8fafc',
|
||||||
|
'text' => '#1e293b'
|
||||||
|
],
|
||||||
|
'nature-green' => [
|
||||||
|
'primary' => '#059669',
|
||||||
|
'secondary' => '#fbbf24',
|
||||||
|
'bg' => '#fdfdfd',
|
||||||
|
'text' => '#064e3b'
|
||||||
|
],
|
||||||
|
'elegant-gold' => [
|
||||||
|
'primary' => '#d4af37',
|
||||||
|
'secondary' => '#111827',
|
||||||
|
'bg' => '#ffffff',
|
||||||
|
'text' => '#111827'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
private array $styles = [
|
||||||
|
'minimal' => [
|
||||||
|
'radius' => '0px',
|
||||||
|
'shadow' => 'none',
|
||||||
|
'padding' => '4rem 1rem'
|
||||||
|
],
|
||||||
|
'modern' => [
|
||||||
|
'radius' => '0.5rem',
|
||||||
|
'shadow' => '0 4px 6px -1px rgb(0 0 0 / 0.1)',
|
||||||
|
'padding' => '6rem 1.5rem'
|
||||||
|
],
|
||||||
|
'premium' => [
|
||||||
|
'radius' => '1.5rem',
|
||||||
|
'shadow' => '0 10px 15px -3px rgb(0 0 0 / 0.1)',
|
||||||
|
'padding' => '8rem 2rem'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->storage = new FileStorage();
|
||||||
|
$this->exportsPath = realpath(__DIR__ . '/../../exports');
|
||||||
|
$this->templatesPath = realpath(__DIR__ . '/../Templates');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a complete static website for a project.
|
||||||
|
*/
|
||||||
|
public function render(string $projectId): bool
|
||||||
|
{
|
||||||
|
$projectData = $this->storage->get("projects/{$projectId}.json");
|
||||||
|
if (!$projectData || empty($projectData['content']['generated'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$projectExportDir = $this->exportsPath . DIRECTORY_SEPARATOR . $projectId;
|
||||||
|
$assetsDir = $projectExportDir . DIRECTORY_SEPARATOR . 'assets';
|
||||||
|
$cssDir = $assetsDir . DIRECTORY_SEPARATOR . 'css';
|
||||||
|
$jsDir = $assetsDir . DIRECTORY_SEPARATOR . 'js';
|
||||||
|
|
||||||
|
// 1. Create directory structure
|
||||||
|
if (!is_dir($cssDir)) mkdir($cssDir, 0777, true);
|
||||||
|
if (!is_dir($jsDir)) mkdir($jsDir, 0777, true);
|
||||||
|
|
||||||
|
// 2. Copy static assets
|
||||||
|
copy($this->templatesPath . '/css/site.css', $cssDir . '/site.css');
|
||||||
|
copy($this->templatesPath . '/js/site.js', $jsDir . '/site.js');
|
||||||
|
|
||||||
|
// 3. Handle Contact Form Logic
|
||||||
|
$formConfig = $projectData['wizard_data']['modules']['contact_form'] ?? [];
|
||||||
|
if (!empty($formConfig['enabled'])) {
|
||||||
|
// Copy handler
|
||||||
|
copy($this->templatesPath . '/emailer.php', $projectExportDir . '/ajax.php');
|
||||||
|
|
||||||
|
// Create config for handler (PHP file is not web-accessible)
|
||||||
|
$siteConfig = [
|
||||||
|
'site_name' => $projectData['wizard_data']['identity']['business_name'],
|
||||||
|
'form_mode' => $formConfig['mode'] ?? 'local',
|
||||||
|
'smtp' => $formConfig['smtp'] ?? null,
|
||||||
|
'local_password_hash' => $formConfig['local_viewer']['password_hash'] ?? null
|
||||||
|
];
|
||||||
|
|
||||||
|
$configContent = "<?php\nreturn " . var_export($siteConfig, true) . ";\n";
|
||||||
|
file_put_contents($projectExportDir . '/config.php', $configContent);
|
||||||
|
|
||||||
|
if (($siteConfig['form_mode'] ?? 'local') === 'local') {
|
||||||
|
// Copy viewer
|
||||||
|
copy($this->templatesPath . '/form-viewer.php', $projectExportDir . '/form-viewer.php');
|
||||||
|
// Create messages dir
|
||||||
|
$msgDir = $projectExportDir . '/messages';
|
||||||
|
if (!is_dir($msgDir)) mkdir($msgDir, 0777, true);
|
||||||
|
file_put_contents($msgDir . '/.htaccess', "Deny from all");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Prepare relative paths for assets
|
||||||
|
if (!empty($projectData['wizard_data']['assets']['logo'])) {
|
||||||
|
$projectData['wizard_data']['assets']['logo'] = $this->makePathRelative($projectData['wizard_data']['assets']['logo'], $projectId);
|
||||||
|
}
|
||||||
|
if (!empty($projectData['wizard_data']['assets']['gallery'])) {
|
||||||
|
foreach ($projectData['wizard_data']['assets']['gallery'] as $key => $path) {
|
||||||
|
$projectData['wizard_data']['assets']['gallery'][$key] = $this->makePathRelative($path, $projectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Render Content Sections
|
||||||
|
$sectionsHtml = '';
|
||||||
|
$selectedSections = $projectData['wizard_data']['modules']['sections'] ?? [];
|
||||||
|
|
||||||
|
foreach ($selectedSections as $sectionId) {
|
||||||
|
try {
|
||||||
|
$sectionsHtml .= $this->renderTemplate("sections/{$sectionId}", [
|
||||||
|
'project' => $projectData
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Renderer: Failed to render section $sectionId: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Prepare Visual Variables
|
||||||
|
$visuals = $projectData['wizard_data']['visuals'] ?? [];
|
||||||
|
$palette = $this->palettes[$visuals['palette'] ?? 'blue-gray'] ?? $this->palettes['blue-gray'];
|
||||||
|
$style = $this->styles[$visuals['style'] ?? 'modern'] ?? $this->styles['modern'];
|
||||||
|
|
||||||
|
// 5. Render HTML base template
|
||||||
|
$html = $this->renderTemplate('base', [
|
||||||
|
'project' => $projectData,
|
||||||
|
'content' => $sectionsHtml,
|
||||||
|
'css_vars' => [
|
||||||
|
'primary' => $palette['primary'],
|
||||||
|
'secondary' => $palette['secondary'],
|
||||||
|
'bg' => $palette['bg'],
|
||||||
|
'text' => $palette['text'],
|
||||||
|
'radius' => $style['radius'],
|
||||||
|
'shadow' => $style['shadow'],
|
||||||
|
'padding' => $style['padding']
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 6. Save to exports
|
||||||
|
return file_put_contents($projectExportDir . DIRECTORY_SEPARATOR . 'index.html', $html) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes an absolute or project-root relative path relative to the project's export directory.
|
||||||
|
*/
|
||||||
|
private function makePathRelative(string $path, string $projectId): string
|
||||||
|
{
|
||||||
|
$prefix = "exports/{$projectId}/";
|
||||||
|
if (strpos($path, $prefix) === 0) {
|
||||||
|
return substr($path, strlen($prefix));
|
||||||
|
}
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal helper to render a PHP template with variables.
|
||||||
|
*/
|
||||||
|
private function renderTemplate(string $templateName, array $vars = []): string
|
||||||
|
{
|
||||||
|
$templateFile = $this->templatesPath . DIRECTORY_SEPARATOR . $templateName . '.php';
|
||||||
|
if (!file_exists($templateFile)) {
|
||||||
|
throw new Exception("Template not found: $templateName");
|
||||||
|
}
|
||||||
|
|
||||||
|
extract($vars);
|
||||||
|
ob_start();
|
||||||
|
include $templateFile;
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/Templates/base.php
Normal file
87
src/Templates/base.php
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @var array $project
|
||||||
|
* @var string $content
|
||||||
|
*/
|
||||||
|
$seo = $project['content']['generated']['seo'] ?? [];
|
||||||
|
$identity = $project['wizard_data']['identity'] ?? [];
|
||||||
|
$contact = $project['wizard_data']['contact'] ?? [];
|
||||||
|
$visuals = $project['wizard_data']['visuals'] ?? [];
|
||||||
|
$assets = $project['wizard_data']['assets'] ?? [];
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="sk">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<title><?= e($seo['title'] ?? $identity['business_name'] ?? 'Môj Web') ?></title>
|
||||||
|
<meta name="description" content="<?= e($seo['description'] ?? '') ?>">
|
||||||
|
|
||||||
|
<!-- Open Graph -->
|
||||||
|
<meta property="og:title" content="<?= e($seo['title'] ?? '') ?>">
|
||||||
|
<meta property="og:description" content="<?= e($seo['description'] ?? '') ?>">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
|
||||||
|
<!-- Assets -->
|
||||||
|
<link rel="stylesheet" href="assets/css/site.css">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
<?php if (!empty($css_vars)): ?>
|
||||||
|
--primary-color: <?= $css_vars['primary'] ?>;
|
||||||
|
--secondary-color: <?= $css_vars['secondary'] ?>;
|
||||||
|
--bg-color: <?= $css_vars['bg'] ?>;
|
||||||
|
--text-color: <?= $css_vars['text'] ?>;
|
||||||
|
--radius: <?= $css_vars['radius'] ?>;
|
||||||
|
--shadow: <?= $css_vars['shadow'] ?>;
|
||||||
|
--section-padding: <?= $css_vars['padding'] ?>;
|
||||||
|
<?php endif; ?>
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<nav>
|
||||||
|
<div class="container">
|
||||||
|
<div class="logo">
|
||||||
|
<?php if (!empty($assets['logo'])): ?>
|
||||||
|
<img src="<?= e($assets['logo']) ?>" alt="<?= e($identity['business_name']) ?>">
|
||||||
|
<?php else: ?>
|
||||||
|
<strong><?= e($identity['business_name']) ?></strong>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<ul class="nav-links">
|
||||||
|
<!-- Navigation will be dynamic in later steps -->
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="content">
|
||||||
|
<?= $content ?>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="container">
|
||||||
|
<div class="footer-grid">
|
||||||
|
<div class="footer-info">
|
||||||
|
<h4><?= e($identity['business_name']) ?></h4>
|
||||||
|
<p><?= e($identity['tagline']) ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="footer-contact">
|
||||||
|
<p><?= e($contact['email']) ?></p>
|
||||||
|
<p><?= e($contact['phone']) ?></p>
|
||||||
|
<p><?= e($contact['address']) ?>, <?= e($contact['city']) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer-bottom">
|
||||||
|
<p>© <?= date('Y') ?> <?= e($identity['business_name']) ?>. Všetky práva vyhradené.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="assets/js/site.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
239
src/Templates/css/site.css
Normal file
239
src/Templates/css/site.css
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
/* Reset & Base Styles */
|
||||||
|
:root {
|
||||||
|
--primary-color: #2563eb;
|
||||||
|
--secondary-color: #64748b;
|
||||||
|
--text-color: #1f2937;
|
||||||
|
--bg-color: #ffffff;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||||
|
--section-padding: 5rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
line-height: 1.6;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3 {
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
padding: var(--section-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Common Components */
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: var(--secondary-color);
|
||||||
|
color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header & Nav */
|
||||||
|
header {
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav .container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo img {
|
||||||
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
footer {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
color: white;
|
||||||
|
padding: 4rem 0;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer h4, footer p {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section specific */
|
||||||
|
.section-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero */
|
||||||
|
.hero {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
text-align: center;
|
||||||
|
padding: calc(var(--section-padding) * 1.5);
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero .subtitle {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--secondary-color);
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
max-width: 800px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Services */
|
||||||
|
.services-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card {
|
||||||
|
padding: 2.5rem;
|
||||||
|
border: 1px solid rgba(0,0,0,0.05);
|
||||||
|
background-color: white;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card:hover {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gallery */
|
||||||
|
.gallery-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FAQ */
|
||||||
|
.faq-list {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contact */
|
||||||
|
.contact-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form {
|
||||||
|
background-color: white;
|
||||||
|
padding: 2.5rem;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form .form-group {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form input, .contact-form textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form input:focus, .contact-form textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.contact-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.hero h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
.hero-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/Templates/emailer.php
Normal file
82
src/Templates/emailer.php
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Universal Form Handler for WebWizard exported sites.
|
||||||
|
* Handles both SMTP mailing and local file storage.
|
||||||
|
*/
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$config = [];
|
||||||
|
if (file_exists('config.php')) {
|
||||||
|
$config = include 'config.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendResponse(bool $success, string $message) {
|
||||||
|
echo json_encode(['success' => $success, 'message' => $message]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
sendResponse(false, 'Method not allowed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Honeypot spam protection
|
||||||
|
if (!empty($_POST['website'])) {
|
||||||
|
sendResponse(true, 'Spam detected.'); // Fake success for bots
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = trim($_POST['name'] ?? '');
|
||||||
|
$email = trim($_POST['email'] ?? '');
|
||||||
|
$message = trim($_POST['message'] ?? '');
|
||||||
|
|
||||||
|
if (!$name || !$email || !$message) {
|
||||||
|
sendResponse(false, 'Prosím vyplňte všetky povinné polia.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
sendResponse(false, 'Neplatná emailová adresa.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$mode = $config['form_mode'] ?? 'local';
|
||||||
|
|
||||||
|
if ($mode === 'local') {
|
||||||
|
// Save to messages directory
|
||||||
|
$msgDir = 'messages';
|
||||||
|
if (!is_dir($msgDir)) {
|
||||||
|
mkdir($msgDir, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$msgId = date('Ymd-His') . '-' . bin2hex(random_bytes(4));
|
||||||
|
$msgData = [
|
||||||
|
'id' => $msgId,
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
'sender_name' => $name,
|
||||||
|
'sender_email' => $email,
|
||||||
|
'message' => $message
|
||||||
|
];
|
||||||
|
|
||||||
|
if (file_put_contents($msgDir . '/' . $msgId . '.json', json_encode($msgData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE))) {
|
||||||
|
sendResponse(true, 'Vaša správa bola úspešne uložená.');
|
||||||
|
} else {
|
||||||
|
sendResponse(false, 'Nepodarilo sa uložiť správu.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// SMTP / Mail mode
|
||||||
|
$to = $config['smtp']['recipient'] ?? '';
|
||||||
|
if (!$to) {
|
||||||
|
sendResponse(false, 'Cieľový email nie je nakonfigurovaný.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$subject = "Nová správa z webu: " . ($config['site_name'] ?? 'WebWizard');
|
||||||
|
$body = "Meno: $name\nEmail: $email\n\nSpráva:\n$message";
|
||||||
|
$headers = "From: webwizard@{$_SERVER['HTTP_HOST']}\r\n" .
|
||||||
|
"Reply-To: $email\r\n" .
|
||||||
|
"X-Mailer: PHP/" . phpversion();
|
||||||
|
|
||||||
|
// Using standard mail() as fallback for MVP if no complex SMTP is provided
|
||||||
|
if (mail($to, $subject, $body, $headers)) {
|
||||||
|
sendResponse(true, 'Vaša správa bola úspešne odoslaná.');
|
||||||
|
} else {
|
||||||
|
sendResponse(false, 'Nepodarilo sa odoslať email.');
|
||||||
|
}
|
||||||
|
}
|
||||||
95
src/Templates/form-viewer.php
Normal file
95
src/Templates/form-viewer.php
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Local Message Viewer for WebWizard exported sites.
|
||||||
|
*/
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$config = [];
|
||||||
|
if (file_exists('config.php')) {
|
||||||
|
$config = include 'config.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
$passwordHash = $config['local_password_hash'] ?? null;
|
||||||
|
|
||||||
|
// Handle Logout
|
||||||
|
if (isset($_GET['logout'])) {
|
||||||
|
session_destroy();
|
||||||
|
header('Location: form-viewer.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Login
|
||||||
|
$error = '';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['password'])) {
|
||||||
|
if ($passwordHash && password_verify($_POST['password'], $passwordHash)) {
|
||||||
|
$_SESSION['ww_authorized'] = true;
|
||||||
|
} else {
|
||||||
|
$error = 'Nesprávne heslo.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$isAuthorized = $_SESSION['ww_authorized'] ?? false;
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="sk">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Prehliadač správ - WebWizard</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; background: #f4f4f4; padding: 2rem; color: #333; }
|
||||||
|
.container { max-width: 800px; margin: 0 auto; background: #fff; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||||
|
h1 { border-bottom: 2px solid #eee; padding-bottom: 1rem; }
|
||||||
|
.msg-item { border: 1px solid #eee; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; }
|
||||||
|
.msg-header { display: flex; justify-content: space-between; font-size: 0.85rem; color: #666; margin-bottom: 0.5rem; }
|
||||||
|
.msg-sender { font-weight: bold; color: #2563eb; }
|
||||||
|
.login-form { text-align: center; padding: 2rem 0; }
|
||||||
|
input[type="password"] { padding: 0.75rem; width: 250px; border: 1px solid #ddd; border-radius: 4px; }
|
||||||
|
button { padding: 0.75rem 1.5rem; background: #2563eb; color: #fff; border: none; border-radius: 4px; cursor: pointer; }
|
||||||
|
.error { color: #dc2626; margin-bottom: 1rem; }
|
||||||
|
.no-messages { text-align: center; color: #666; padding: 2rem; }
|
||||||
|
.logout-link { float: right; font-size: 0.9rem; color: #666; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<?php if (!$isAuthorized): ?>
|
||||||
|
<div class="login-form">
|
||||||
|
<h1>Prihlásenie</h1>
|
||||||
|
<?php if ($error): ?><div class="error"><?= $error ?></div><?php endif; ?>
|
||||||
|
<form method="POST">
|
||||||
|
<input type="password" name="password" placeholder="Heslo k správam" required autofocus>
|
||||||
|
<button type="submit">Vstúpiť</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<a href="?logout=1" class="logout-link">Odhlásiť sa</a>
|
||||||
|
<h1>Prijaté správy</h1>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$files = glob('messages/*.json');
|
||||||
|
rsort($files); // Newest first
|
||||||
|
|
||||||
|
if (empty($files)): ?>
|
||||||
|
<p class="no-messages">Zatiaľ ste neprijali žiadne správy.</p>
|
||||||
|
<?php else:
|
||||||
|
foreach ($files as $file):
|
||||||
|
$data = json_decode(file_get_contents($file), true);
|
||||||
|
if ($data): ?>
|
||||||
|
<div class="msg-item">
|
||||||
|
<div class="msg-header">
|
||||||
|
<span><?= htmlspecialchars($data['timestamp']) ?></span>
|
||||||
|
<span>ID: <?= htmlspecialchars($data['id']) ?></span>
|
||||||
|
</div>
|
||||||
|
<div>Od: <span class="msg-sender"><?= htmlspecialchars($data['sender_name']) ?></span> (<<?= htmlspecialchars($data['sender_email']) ?>>)</div>
|
||||||
|
<div style="margin-top: 1rem; white-space: pre-wrap;"><?= htmlspecialchars($data['message']) ?></div>
|
||||||
|
</div>
|
||||||
|
<?php endif;
|
||||||
|
endforeach;
|
||||||
|
endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
40
src/Templates/js/site.js
Normal file
40
src/Templates/js/site.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* WebWizard Site Scripts
|
||||||
|
*/
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const contactForm = document.getElementById('site-contact-form');
|
||||||
|
|
||||||
|
if (contactForm) {
|
||||||
|
contactForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(contactForm);
|
||||||
|
const submitBtn = contactForm.querySelector('button[type="submit"]');
|
||||||
|
const originalBtnText = submitBtn.textContent;
|
||||||
|
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.textContent = 'Odosielam...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(contactForm.action, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
alert(result.message);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
contactForm.reset();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Form submission error:', error);
|
||||||
|
alert('Vyskytla sa chyba pri odosielaní formulára. Skúste to prosím neskôr.');
|
||||||
|
} finally {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.textContent = originalBtnText;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
16
src/Templates/sections/about.php
Normal file
16
src/Templates/sections/about.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @var array $project
|
||||||
|
*/
|
||||||
|
$about = $project['content']['generated']['about'] ?? [];
|
||||||
|
?>
|
||||||
|
<section id="about" class="about">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2><?= e($about['title'] ?? 'O nás') ?></h2>
|
||||||
|
</div>
|
||||||
|
<div class="about-content">
|
||||||
|
<?= nl2br(e($about['text'] ?? $project['wizard_data']['identity']['description'])) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
54
src/Templates/sections/contact.php
Normal file
54
src/Templates/sections/contact.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @var array $project
|
||||||
|
*/
|
||||||
|
$contact = $project['wizard_data']['contact'] ?? [];
|
||||||
|
$formEnabled = $project['wizard_data']['modules']['contact_form']['enabled'] ?? false;
|
||||||
|
?>
|
||||||
|
<section id="contact" class="contact">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>Kontakt</h2>
|
||||||
|
</div>
|
||||||
|
<div class="contact-grid">
|
||||||
|
<div class="contact-info">
|
||||||
|
<h3>Kde nás nájdete</h3>
|
||||||
|
<p><strong>Adresa:</strong> <?= e($contact['address']) ?>, <?= e($contact['city']) ?></p>
|
||||||
|
<p><strong>Email:</strong> <a href="mailto:<?= e($contact['email']) ?>"><?= e($contact['email']) ?></a></p>
|
||||||
|
<p><strong>Telefón:</strong> <a href="tel:<?= e($contact['phone']) ?>"><?= e($contact['phone']) ?></a></p>
|
||||||
|
|
||||||
|
<?php if (!empty($contact['socials']['facebook']) || !empty($contact['socials']['instagram'])): ?>
|
||||||
|
<div class="socials">
|
||||||
|
<?php if ($contact['socials']['facebook']): ?>
|
||||||
|
<a href="<?= e($contact['socials']['facebook']) ?>" target="_blank" class="social-link">Facebook</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($contact['socials']['instagram']): ?>
|
||||||
|
<a href="<?= e($contact['socials']['instagram']) ?>" target="_blank" class="social-link">Instagram</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($formEnabled): ?>
|
||||||
|
<div class="contact-form">
|
||||||
|
<h3>Napíšte nám</h3>
|
||||||
|
<form action="ajax.php" method="POST" id="site-contact-form">
|
||||||
|
<!-- Honeypot -->
|
||||||
|
<input type="text" name="website" style="display:none !important" tabindex="-1" autocomplete="off">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" name="name" placeholder="Vaše meno" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="email" name="email" placeholder="Váš email" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<textarea name="message" placeholder="Vaša správa" rows="4" required></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Odoslať správu</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
21
src/Templates/sections/faq.php
Normal file
21
src/Templates/sections/faq.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @var array $project
|
||||||
|
*/
|
||||||
|
$faq = $project['content']['generated']['faq'] ?? [];
|
||||||
|
?>
|
||||||
|
<section id="faq" class="faq">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>Časté otázky (FAQ)</h2>
|
||||||
|
</div>
|
||||||
|
<div class="faq-list">
|
||||||
|
<?php foreach ($faq as $item): ?>
|
||||||
|
<div class="faq-item">
|
||||||
|
<h3><?= e($item['question']) ?></h3>
|
||||||
|
<p><?= e($item['answer']) ?></p>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
20
src/Templates/sections/gallery.php
Normal file
20
src/Templates/sections/gallery.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @var array $project
|
||||||
|
*/
|
||||||
|
$gallery = $project['wizard_data']['assets']['gallery'] ?? [];
|
||||||
|
?>
|
||||||
|
<section id="gallery" class="gallery">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>Galéria</h2>
|
||||||
|
</div>
|
||||||
|
<div class="gallery-grid">
|
||||||
|
<?php foreach ($gallery as $imagePath): ?>
|
||||||
|
<div class="gallery-item">
|
||||||
|
<img src="<?= e($imagePath) ?>" alt="Galéria - <?= e($project['wizard_data']['identity']['business_name']) ?>" loading="lazy">
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
18
src/Templates/sections/hero.php
Normal file
18
src/Templates/sections/hero.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @var array $project
|
||||||
|
*/
|
||||||
|
$hero = $project['content']['generated']['hero'] ?? [];
|
||||||
|
?>
|
||||||
|
<section id="hero" class="hero">
|
||||||
|
<div class="container">
|
||||||
|
<div class="hero-content">
|
||||||
|
<h1><?= e($hero['title'] ?? $project['wizard_data']['identity']['business_name']) ?></h1>
|
||||||
|
<p class="subtitle"><?= e($hero['subtitle'] ?? $project['wizard_data']['identity']['tagline']) ?></p>
|
||||||
|
<div class="hero-actions">
|
||||||
|
<a href="#contact" class="btn btn-primary">Kontaktujte nás</a>
|
||||||
|
<a href="#services" class="btn btn-secondary">Naše služby</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
13
src/Templates/sections/pricing.php
Normal file
13
src/Templates/sections/pricing.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @var array $project
|
||||||
|
*/
|
||||||
|
?>
|
||||||
|
<section id="pricing" class="pricing">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>Cenník</h2>
|
||||||
|
</div>
|
||||||
|
<p>Informácie o cenách našich služieb nájdete v sekcii Služby alebo nás kontaktujte pre individuálnu ponuku.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
28
src/Templates/sections/services.php
Normal file
28
src/Templates/sections/services.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @var array $project
|
||||||
|
*/
|
||||||
|
$services = $project['content']['generated']['services'] ?? [];
|
||||||
|
$pricingNote = $project['wizard_data']['services']['pricing_note'] ?? '';
|
||||||
|
?>
|
||||||
|
<section id="services" class="services">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>Naše služby</h2>
|
||||||
|
</div>
|
||||||
|
<div class="services-grid">
|
||||||
|
<?php foreach ($services as $service): ?>
|
||||||
|
<article class="service-card">
|
||||||
|
<h3><?= e($service['title']) ?></h3>
|
||||||
|
<p><?= e($service['text']) ?></p>
|
||||||
|
<?php if (!empty($service['price_info'])): ?>
|
||||||
|
<div class="price"><?= e($service['price_info']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</article>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php if ($pricingNote): ?>
|
||||||
|
<p class="pricing-note"><em><?= e($pricingNote) ?></em></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
15
src/Utils/Helpers.php
Normal file
15
src/Utils/Helpers.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global helper functions for templates.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!function_exists('e')) {
|
||||||
|
/**
|
||||||
|
* Securely escapes a value for HTML output.
|
||||||
|
*/
|
||||||
|
function e($value): string
|
||||||
|
{
|
||||||
|
return htmlspecialchars((string)($value ?? ''), ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,72 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require 'vendor/autoload.php';
|
|
||||||
|
|
||||||
// Load configuration
|
|
||||||
\App\Utils\Config::load(__DIR__ . '/../.env');
|
|
||||||
|
|
||||||
use App\Services\LLMpool;
|
|
||||||
use App\Services\FileStorage;
|
|
||||||
use App\Actions\ProjectActions;
|
|
||||||
|
|
||||||
echo "Testing LLMpool Worker (Step 14)..." . PHP_EOL . PHP_EOL;
|
|
||||||
|
|
||||||
$storage = new FileStorage();
|
|
||||||
$projectActions = new ProjectActions();
|
|
||||||
|
|
||||||
// 1. Create a dummy project
|
|
||||||
$userId = 'u_test_worker';
|
|
||||||
$userData = [
|
|
||||||
'user_id' => $userId,
|
|
||||||
'created_at' => date('c'),
|
|
||||||
'projects' => []
|
|
||||||
];
|
|
||||||
$storage->put("users/{$userId}.json", $userData);
|
|
||||||
|
|
||||||
$project = $projectActions->createProject($userId);
|
|
||||||
$projectId = $project['project_id'];
|
|
||||||
|
|
||||||
// Mock some wizard data
|
|
||||||
$project['wizard_data'] = [
|
|
||||||
'business_category' => ['group' => 'gastro', 'subcategory' => 'cafe'],
|
|
||||||
'identity' => ['business_name' => 'Kaviareň u Robota', 'tagline' => 'Káva s dušou'],
|
|
||||||
'services' => ['items' => [['name' => 'Espresso', 'description' => 'Silná káva', 'price_from' => '2']]],
|
|
||||||
'smart_answers' => ['terrace' => true]
|
|
||||||
];
|
|
||||||
$storage->put("projects/{$projectId}.json", $project);
|
|
||||||
|
|
||||||
// 2. Create a dummy task
|
|
||||||
$taskId = 't_test_worker';
|
|
||||||
$taskData = [
|
|
||||||
'task_id' => $taskId,
|
|
||||||
'project_id' => $projectId,
|
|
||||||
'status' => 'queued',
|
|
||||||
'attempt_count' => 0,
|
|
||||||
'max_attempts' => 3,
|
|
||||||
'created_at' => date('c'),
|
|
||||||
'wizard_data' => $project['wizard_data']
|
|
||||||
];
|
|
||||||
$storage->put("llm/{$taskId}.json", $taskData);
|
|
||||||
|
|
||||||
echo "[INFO] Task created: $taskId for project $projectId" . PHP_EOL;
|
|
||||||
|
|
||||||
// 3. Run Worker
|
|
||||||
$worker = new LLMpool();
|
|
||||||
echo "[PROCESS] Running worker for task $taskId.json..." . PHP_EOL;
|
|
||||||
|
|
||||||
// Since we call real AI, it might take time
|
|
||||||
$success = $worker->processTask($taskId . '.json');
|
|
||||||
|
|
||||||
if ($success) {
|
|
||||||
echo "[SUCCESS] Worker finished successfully." . PHP_EOL;
|
|
||||||
$updatedProject = $storage->get("projects/{$projectId}.json");
|
|
||||||
echo "[INFO] Project status: " . $updatedProject['status'] . PHP_EOL;
|
|
||||||
echo "[INFO] Generated content keys: " . implode(', ', array_keys($updatedProject['content']['generated'])) . PHP_EOL;
|
|
||||||
} else {
|
|
||||||
echo "[FAIL] Worker failed to process task." . PHP_EOL;
|
|
||||||
$task = $storage->get("llm/{$taskId}.json");
|
|
||||||
if ($task) {
|
|
||||||
echo "[DEBUG] Task status: " . $task['status'] . PHP_EOL;
|
|
||||||
echo "[DEBUG] Last error: " . ($task['last_error'] ?? 'N/A') . PHP_EOL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user