# Inštrukcie pre Claude — Vytvor admin rozhranie pre HTML stránku

## Čo treba urobiť

Dostaneš HTML stránku. Tvojou úlohou je:

1. Extrahovať všetok editovateľný obsah do `content.json`
2. Upraviť `index.html` aby načítaval obsah dynamicky z `content.json`
3. Vytvoriť `admin.html` — plné CMS rozhranie pre editáciu obsahu
4. Zapojiť Formspree formulár
5. Pridať WebP upload obrázkov cez admin

---

## Vstupy od používateľa

| Vstup | Popis |
|-------|-------|
| `index.html` | HTML stránka ktorú treba upraviť |
| `username/repo` | GitHub repozitár (private) |
| `github_pat_11XXX...` | Fine-grained token, Contents R/W |
| `https://formspree.io/f/XXXXXXXX` | Formspree endpoint |
| `info@firma.sk` | Email pre formulár |
| `https://api.cloudflare.com/.../deploy_hooks/XXXX` | Cloudflare Deploy Hook URL |

---

## KROK 1 — Vytvor content.json

Extrahuj z HTML všetok editovateľný obsah. Štruktúra:

```json
{
  "meta": {
    "title": "...",
    "description": "...",
    "og_title": "...",
    "og_description": "...",
    "canonical_url": "https://...",
    "logo_src": "/images/logo.webp",
    "logo_alt": "..."
  },
  "contact": {
    "phone": "+421...",
    "phone_display": "...",
    "email": "...",
    "address_street": "...",
    "address_city": "...",
    "address_zip": "...",
    "opening_hours": "...",
    "facebook_url": "...",
    "maps_url": "...",
    "google_reviews_url": "...",
    "footer_copy": "© ..."
  },
  "hero": {
    "badge": "...",
    "heading_line1": "...",
    "heading_line2": "...",
    "heading_line3": "...",
    "subtext": "...",
    "cta_primary_text": "...",
    "cta_secondary_text": "...",
    "trust_items": ["...", "...", "..."],
    "image_src": "",
    "image_alt": ""
  },
  "services": {
    "label": "...",
    "heading": "...",
    "subtext": "...",
    "items": [
      { "icon": "🧾", "title": "...", "text": "...", "tags": ["..."] }
    ]
  },
  "products": {
    "label": "...",
    "heading": "...",
    "subtext": "...",
    "cta_url": "https://...",
    "cta_text": "...",
    "items": [
      { "brand": "...", "title": "...", "text": "...", "badge": "", "image_src": "", "image_alt": "" }
    ]
  },
  "why_us": {
    "label": "...",
    "heading": "...",
    "subtext": "...",
    "rating": "4.7",
    "reviews_count": "26",
    "items": [
      { "num": "01", "title": "...", "text": "..." }
    ]
  },
  "process": {
    "label": "...",
    "heading": "...",
    "subtext": "...",
    "steps": [
      { "num": "1", "title": "...", "text": "..." }
    ]
  },
  "reviews": {
    "label": "...",
    "heading": "...",
    "rating": "4.7",
    "count": "26",
    "items": [
      { "stars": 5, "text": "...", "name": "...", "avatar": "AB", "date": "..." }
    ]
  },
  "faq": {
    "label": "...",
    "heading": "...",
    "items": [
      { "q": "...", "a": "..." }
    ]
  },
  "contact_section": {
    "label": "...",
    "heading": "...",
    "subtext": "...",
    "form_name_label": "Meno a priezvisko",
    "form_email_label": "E-mail",
    "form_phone_label": "Telefón",
    "form_message_label": "Správa",
    "form_message_placeholder": "...",
    "form_submit_text": "Odoslať správu",
    "form_success_text": "✅ Správa odoslaná! Ozveme sa čo najskôr."
  }
}
```

**Pravidlo:** Sekcie ktoré stránka nemá vynechaj. Prispôsob štruktúru obsahu stránky.

---

## KROK 2 — Uprav index.html

### Pridaj sanitizačné helpery

```javascript
function s(str) {
  if (str === null || str === undefined) return '';
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
}

function safeUrl(url) {
  if (!url) return '#';
  const u = String(url).trim();
  if (/^(https?:|tel:|mailto:|\/[^/])/.test(u)) return u;
  if (u === '/') return u;
  return '#';
}
```

### Pravidlá pri applyContent()

- `textContent` pre jednoduché texty
- `innerHTML` len pre HTML obsah — každá premenná cez `s()`
- `href` vždy cez `safeUrl()`
- `src` obrázkov vždy cez `safeUrl()`
- Fallback ak obrázok chýba: `img.onerror = () => img.closest('.obal').style.display = 'none'`

### Načítanie content.json

```javascript
async function loadContent() {
  try {
    const res = await fetch('content.json?v=' + Date.now());
    if (!res.ok) throw new Error('HTTP ' + res.status);
    const d = await res.json();
    applyContent(d);
  } catch(e) {
    console.error('Failed to load content.json:', e);
  }
}
// Na konci scriptu:
loadContent();
```

### Formspree formulár

```html
<form id="cf" action="https://formspree.io/f/XXXXXXXX" method="POST" onsubmit="hf(event)">
  <input type="text" name="_gotcha" style="display:none">
  <input type="text" name="meno" required>
  <input type="email" name="email" required>
  <input type="tel" name="telefon">
  <textarea name="sprava" required></textarea>
  <button type="submit">Odoslať</button>
</form>
<div id="fs" style="display:none"></div>
```

```javascript
async function hf(e) {
  e.preventDefault();
  const form = e.target;
  const btn = form.querySelector('button[type="submit"]');
  const successEl = document.getElementById('fs');
  btn.textContent = 'Odosielam...';
  btn.disabled = true;
  try {
    const res = await fetch(form.action, {
      method: 'POST',
      body: new FormData(form),
      headers: { Accept: 'application/json' }
    });
    if (res.ok) {
      form.reset();
      form.style.display = 'none';
      successEl.style.display = 'block';
    } else {
      throw new Error('Chyba');
    }
  } catch(err) {
    btn.textContent = '❌ Chyba – skúste znova';
    btn.disabled = false;
  }
}
```

---

## KROK 3 — Vytvor admin.html

### Bezpečnostné hlavičky

```html
<meta http-equiv="Content-Security-Policy"
  content="default-src 'self' https://fonts.googleapis.com https://fonts.gstatic.com https://api.github.com;
           script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
           img-src * data:; connect-src https://api.github.com;">
<meta name="referrer" content="no-referrer">
<meta name="robots" content="noindex, nofollow">
```

### Konfigurácia a auto-init

```javascript
const GH_TOKEN = 'github_pat_11XXX...'; // Fine-grained token
const GH_REPO  = 'username/repo';
const GH_PATH  = 'content.json';
let CONTENT = null;
let GH_SHA  = '';
let hasUnsavedChanges = false;

window.addEventListener('DOMContentLoaded', () => {
  fetchContentFromGitHub().then(() => {
    document.getElementById('repo-label').textContent = GH_REPO;
    renderAllPanels();
    showToast('Obsah načítaný ✓', 'success');
  }).catch(err => showToast('❌ ' + err.message, 'error'));
});

window.addEventListener('beforeunload', e => {
  if (hasUnsavedChanges) { e.preventDefault(); e.returnValue = ''; }
});
```

### Načítanie z GitHubu — UTF-8 safe

```javascript
async function fetchContentFromGitHub() {
  const safePath = GH_PATH.replace(/\.\.\//g, '').replace(/^\//, '');
  const res = await fetch(`https://api.github.com/repos/${GH_REPO}/contents/${safePath}`, {
    headers: {
      Authorization: 'Bearer ' + GH_TOKEN,
      Accept: 'application/vnd.github.v3+json',
      'X-GitHub-Api-Version': '2022-11-28'
    }
  });
  if (res.status === 401) throw new Error('Neplatný token');
  if (res.status === 403) throw new Error('Nedostatočné oprávnenia');
  if (res.status === 404) throw new Error('Repozitár alebo súbor nenájdený');
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const data = await res.json();
  if (!data.content || !data.sha) throw new Error('Neočakávaný formát odpovede');
  GH_SHA = data.sha;
  // KRITICKÉ: atob() nie je UTF-8 safe — TextDecoder
  const binary = atob(data.content.replace(/\n/g, ''));
  const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
  CONTENT = JSON.parse(new TextDecoder('utf-8').decode(bytes));
}
```

### Uloženie na GitHub — UTF-8 safe + Deploy Hook

```javascript
async function saveContent() {
  const btn = document.getElementById('save-btn');
  btn.disabled = true;
  try {
    collectAllFormData();
    const jsonStr = JSON.stringify(CONTENT, null, 2);
    // KRITICKÉ: TextEncoder pre správne UTF-8
    const encBytes = new TextEncoder().encode(jsonStr);
    const encBinary = Array.from(encBytes).map(b => String.fromCharCode(b)).join('');
    const encoded = btoa(encBinary);
    const safePath = GH_PATH.replace(/\.\.\//g, '').replace(/^\//, '');
    const res = await fetch(`https://api.github.com/repos/${GH_REPO}/contents/${safePath}`, {
      method: 'PUT',
      headers: {
        Authorization: 'Bearer ' + GH_TOKEN,
        'Content-Type': 'application/json',
        'X-GitHub-Api-Version': '2022-11-28'
      },
      body: JSON.stringify({
        message: 'Admin: aktualizácia obsahu stránky',
        content: encoded,
        sha: GH_SHA
      })
    });
    if (res.status === 409) throw new Error('Konflikt SHA — obnovte stránku');
    if (res.status === 422) throw new Error('SHA nesúhlasí — obnovte stránku');
    if (!res.ok) { const err = await res.json(); throw new Error(err.message || `HTTP ${res.status}`); }
    const result = await res.json();
    GH_SHA = result.content.sha;
    hasUnsavedChanges = false;
    // Cloudflare Deploy Hook — spustí production deploy
    fetch('DEPLOY_HOOK_URL', { method: 'POST' }).catch(() => {});
    showToast('✅ Uložené! Spúšťam deploy...', 'success');
  } catch(e) {
    showToast('❌ ' + e.message, 'error');
  }
  btn.disabled = false;
}
```

### Refresh SHA po upload obrázku

```javascript
async function refreshSHA() {
  try {
    const safePath = GH_PATH.replace(/\.\.\//g, '').replace(/^\//, '');
    const res = await fetch(`https://api.github.com/repos/${GH_REPO}/contents/${safePath}`, {
      headers: { Authorization: 'Bearer ' + GH_TOKEN, 'X-GitHub-Api-Version': '2022-11-28' }
    });
    if (res.ok) { const data = await res.json(); GH_SHA = data.sha; }
  } catch(e) { console.warn('SHA refresh failed:', e); }
}
```

### WebP upload → GitHub

```javascript
function triggerUpload(inputId, previewId) {
  const fi = document.createElement('input');
  fi.type = 'file'; fi.accept = 'image/*';
  fi.onchange = e => convertToWebP(e.target.files[0], inputId, previewId);
  fi.click();
}

function convertToWebP(file, inputId, previewId) {
  const reader = new FileReader();
  reader.onload = evt => {
    const img = new Image();
    img.onload = () => {
      const MAX = 1600;
      let w = img.width, h = img.height;
      if (w > MAX) { h = Math.round(h * MAX / w); w = MAX; }
      const canvas = document.createElement('canvas');
      canvas.width = w; canvas.height = h;
      canvas.getContext('2d').drawImage(img, 0, 0, w, h);
      canvas.toBlob(blob => uploadImageToGitHub(blob, file.name, inputId, previewId), 'image/webp', 0.85);
    };
    img.src = evt.target.result;
  };
  reader.readAsDataURL(file);
}

async function uploadImageToGitHub(blob, originalName, inputId, previewId) {
  const statusEl = document.getElementById(inputId + '-status');
  if (statusEl) statusEl.textContent = '⏳ Nahrávam...';
  try {
    const baseName = originalName.replace(/\.[^.]+$/, '').replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
    const fileName = `images/${baseName}-${Date.now()}.webp`;
    const arrayBuffer = await blob.arrayBuffer();
    const bytes = new Uint8Array(arrayBuffer);
    const base64 = btoa(Array.from(bytes).map(b => String.fromCharCode(b)).join(''));
    const uploadRes = await fetch(`https://api.github.com/repos/${GH_REPO}/contents/${fileName}`, {
      method: 'PUT',
      headers: { Authorization: 'Bearer ' + GH_TOKEN, 'Content-Type': 'application/json', 'X-GitHub-Api-Version': '2022-11-28' },
      body: JSON.stringify({ message: `Upload: ${fileName}`, content: base64 })
    });
    if (!uploadRes.ok) { const err = await uploadRes.json(); throw new Error(err.message || `HTTP ${uploadRes.status}`); }
    // KRITICKÉ: obnoviť SHA — upload vytvoril nový commit
    await refreshSHA();
    // Relatívna URL — funguje na akejkoľvek deploy URL
    const url = `/${fileName}`;
    const input = document.getElementById(inputId);
    if (input) { input.value = url; input.dispatchEvent(new Event('input')); }
    const preview = document.getElementById(previewId);
    if (preview) { preview.src = url; preview.classList.add('show'); }
    if (statusEl) { statusEl.textContent = '✅ Nahraté — uložte zmeny'; statusEl.className = 'upload-status ok'; }
    hasUnsavedChanges = true;
  } catch(e) {
    if (statusEl) { statusEl.textContent = '❌ ' + e.message; statusEl.className = 'upload-status err'; }
  }
}
```

### Sanitizácia URL obrázkov pred uložením

```javascript
function sanitizeImageUrl(url) {
  if (!url) return '';
  const m = url.match(/https?:\/\/[^/]*pages\.dev(\/.*)/);
  if (m) return m[1];
  return url;
}
// Pri každom image_src v collectAllFormData:
CONTENT.hero.image_src = sanitizeImageUrl(val('hero-img'));
```

### Pomocné funkcie

```javascript
function esc(s='') {
  return String(s).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function val(id) { const el = document.getElementById(id); return el ? el.value : ''; }
function field(id, label, value, type='text') {
  return `<div class="fg"><label>${label}</label><input type="${type}" id="${id}" value="${esc(value)}"></div>`;
}
function textarea(id, label, value, rows=3) {
  return `<div class="fg"><label>${label}</label><textarea id="${id}" rows="${rows}">${esc(value)}</textarea></div>`;
}
function imgField(id, label, value, previewId) {
  return `<div class="fg"><label>${label}</label>
    <div class="img-upload-wrap">
      <input type="url" id="${id}" value="${esc(value)}">
      <button type="button" class="btn-upload" onclick="triggerUpload('${id}','${previewId}')">📁 Nahrať</button>
    </div>
    <div class="upload-status" id="${id}-status"></div>
  </div>`;
}
function showToast(msg, type='') {
  const t = document.getElementById('toast');
  t.textContent = msg; t.className = 'show ' + type;
  clearTimeout(t._timer);
  t._timer = setTimeout(() => t.className = '', 4000);
}
function showLoading(show) {
  document.getElementById('loading').classList.toggle('show', show);
}
document.addEventListener('input', e => {
  if (e.target.closest('#content-area')) hasUnsavedChanges = true;
});
```

---

## Kritické upozornenia

| # | Problém | Riešenie |
|---|---------|----------|
| 1 | `atob()` rozbíja diakritiku | Vždy `TextDecoder('utf-8')` pri načítaní |
| 2 | `btoa()` rozbíja diakritiku | Vždy `TextEncoder().encode()` pri ukladaní |
| 3 | `safeUrl()` blokuje `/images/...` | Regex musí obsahovať `\/[^/]` pre relatívne cesty |
| 4 | 409 Conflict po upload obrázku | Volaj `refreshSHA()` ihneď po každom upload commite |
| 5 | Obrázok 404 na stránke | Ukladaj `/images/nazov.webp` nie `https://hash.pages.dev/...` |
| 6 | `_redirects` s pravidlom `/ /index.html 200` | Spôsobuje infinite loop — súbor nechaj prázdny |
| 7 | Zmeny nejdú do production | Deploy Hook musí byť volaný po každom `saveContent()` |
