import { apiGet, apiPut } from '../api.js'; import { canEdit, pageHeaderHTML, showToast } from '../app.js'; import { SOURCE_LANG, normalizeEntry, germanFor, escHtml as esc } from '../translations-helpers.js'; import { APP_UI_SCREENS, screenById, getVariants, variantById } from '../app-ui-screens.js'; const STORAGE_SCREEN = 'qdb_appui_screen'; const STORAGE_VARIANT = 'qdb_appui_variant'; let appStrings = []; /** @type {Record} key → german text */ let germanByKey = {}; let selectedScreenId = ''; /** @type {Record} screenId → variantId */ let selectedVariantByScreen = {}; let saveTimers = {}; let savesInFlight = 0; export async function appUiPage() { const app = document.getElementById('app'); app.innerHTML = ` ${pageHeaderHTML( 'App UI', '', { subtitle: 'Edit German wording for the counselor app. Early version — some design details in the preview may be off.', }, )}
`; try { selectedScreenId = localStorage.getItem(STORAGE_SCREEN) || APP_UI_SCREENS[0].id; if (!screenById(selectedScreenId)) selectedScreenId = APP_UI_SCREENS[0].id; try { selectedVariantByScreen = JSON.parse(localStorage.getItem(STORAGE_VARIANT) || '{}') || {}; } catch (_) { selectedVariantByScreen = {}; } renderShell(); await loadAppStrings(); } catch (e) { document.getElementById('appuiRoot').innerHTML = `

${esc(e.message)}

`; } } function currentVariantId(screen) { const variants = getVariants(screen); const stored = selectedVariantByScreen[screen.id]; if (stored && variants.some(v => v.id === stored)) return stored; return variants[0].id; } function setVariant(screenId, variantId) { selectedVariantByScreen[screenId] = variantId; localStorage.setItem(STORAGE_VARIANT, JSON.stringify(selectedVariantByScreen)); } function renderShell() { document.getElementById('appuiRoot').innerHTML = `
`; renderScreenNav(); } function renderScreenNav() { const nav = document.getElementById('appuiScreenNav'); nav.innerHTML = APP_UI_SCREENS.map(s => { const variants = getVariants(s); const multi = variants.length > 1 && variants[0].id !== 'default'; return ` `; }).join(''); nav.querySelectorAll('.appui-screen-btn').forEach(btn => { btn.addEventListener('click', () => { selectedScreenId = btn.dataset.screen; localStorage.setItem(STORAGE_SCREEN, selectedScreenId); renderScreenNav(); renderWorkspace(); }); }); } function renderVariantBar(screen) { const bar = document.getElementById('appuiVariantBar'); const variants = getVariants(screen); const multi = variants.length > 1 && variants[0].id !== 'default'; if (!multi) { bar.hidden = true; bar.innerHTML = ''; return; } const activeId = currentVariantId(screen); bar.hidden = false; bar.innerHTML = `
${variants.map(v => ` `).join('')}
`; bar.querySelectorAll('.appui-variant-chip').forEach(btn => { btn.addEventListener('click', () => { setVariant(screen.id, btn.dataset.variant); renderWorkspace(); }); }); } async function loadAppStrings() { const data = await apiGet('translations.php?all=1'); appStrings = (data.appStrings || []).map(normalizeEntry).filter(e => e.type === 'app_string'); germanByKey = {}; appStrings.forEach(e => { const key = e.key || e.entityId; if (key) germanByKey[key] = germanFor(e); }); renderWorkspace(); } function textFor(key) { if (germanByKey[key] != null && germanByKey[key] !== '') return germanByKey[key]; const entry = appStrings.find(e => (e.key || e.entityId) === key); if (entry) return germanFor(entry); return key; } function renderWorkspace() { const screen = screenById(selectedScreenId); const variant = variantById(screen, currentVariantId(screen)); const focusSet = new Set(variant.focusKeys || screen.keys); renderVariantBar(screen); const hint = document.getElementById('appuiKeysHint'); if (hint) { const nFocus = [...focusSet].filter(k => screen.keys.includes(k)).length; hint.textContent = variant.id !== 'default' ? `${variant.label} · ${nFocus} keys` : `${screen.keys.length} keys · German`; } const tablet = document.getElementById('appuiTabletScreen'); tablet.innerHTML = variant.mock(); fillFields(tablet); bindFields(tablet); renderKeysList(screen, focusSet); } function fillFields(root) { root.querySelectorAll('[data-appui-key]').forEach(el => { const key = el.dataset.appuiKey; el.value = textFor(key); el.title = key; el.disabled = !canEdit(); }); } function bindFields(root) { root.querySelectorAll('[data-appui-key]').forEach(el => { el.addEventListener('input', () => { const key = el.dataset.appuiKey; germanByKey[key] = el.value; syncKeyListInput(key, el.value); scheduleSave(key, el); }); el.addEventListener('change', () => { const key = el.dataset.appuiKey; germanByKey[key] = el.value; scheduleSave(key, el, true); }); }); } function renderKeysList(screen, focusSet) { const list = document.getElementById('appuiKeysList'); const editable = canEdit(); const focused = screen.keys.filter(k => focusSet.has(k)); const other = screen.keys.filter(k => !focusSet.has(k)); const row = (key, inFocus) => { const val = esc(textFor(key)); return `
${esc(key)}
`; }; let html = ''; if (focused.length && other.length) { html += `
This state
`; } html += focused.map(k => row(k, true)).join(''); if (other.length) { html += `
Other keys on this screen
`; html += other.map(k => row(k, false)).join(''); } list.innerHTML = html; list.querySelectorAll('[data-appui-key]').forEach(el => { el.addEventListener('input', () => { const key = el.dataset.appuiKey; germanByKey[key] = el.value; syncTabletField(key, el.value); scheduleSave(key, el); }); el.addEventListener('change', () => { const key = el.dataset.appuiKey; germanByKey[key] = el.value; scheduleSave(key, el, true); }); el.addEventListener('focus', () => { list.querySelectorAll('.appui-key-row').forEach(r => r.classList.remove('focused')); el.closest('.appui-key-row')?.classList.add('focused'); highlightTabletKey(el.dataset.appuiKey); }); }); } function syncKeyListInput(key, value) { const inp = document.querySelector(`#appuiKeysList [data-appui-key="${CSS.escape(key)}"]`); if (inp && inp.value !== value) inp.value = value; } function syncTabletField(key, value) { document.querySelectorAll(`#appuiTabletScreen [data-appui-key="${CSS.escape(key)}"]`).forEach(el => { if (el.value !== value) el.value = value; }); } function highlightTabletKey(key) { document.querySelectorAll('#appuiTabletScreen [data-appui-key]').forEach(el => { el.classList.toggle('appui-field-highlight', el.dataset.appuiKey === key); }); } function setSaveStatus(state) { const el = document.getElementById('appuiSaveStatus'); if (!el) return; if (state === 'saving') el.textContent = 'Saving…'; else if (state === 'saved') el.textContent = 'Saved'; else if (state === 'error') el.textContent = 'Save failed'; else el.textContent = ''; } function scheduleSave(key, el, immediate = false) { if (!canEdit()) return; clearTimeout(saveTimers[key]); const run = () => saveGerman(key, el); if (immediate) run(); else { saveTimers[key] = setTimeout(run, 450); setSaveStatus('saving'); } } async function saveGerman(key, el) { savesInFlight++; setSaveStatus('saving'); try { await apiPut('translations.php', { type: 'app_string', id: key, languageCode: SOURCE_LANG, text: el.value, }); germanByKey[key] = el.value; const entry = appStrings.find(e => (e.key || e.entityId) === key); if (entry) { entry.germanText = el.value; entry.translations = { ...(entry.translations || {}), [SOURCE_LANG]: el.value }; } el.classList.add('save-flash'); setTimeout(() => el.classList.remove('save-flash'), 400); } catch (e) { setSaveStatus('error'); showToast(e.message, 'error'); } finally { savesInFlight = Math.max(0, savesInFlight - 1); if (savesInFlight === 0) setSaveStatus('saved'); } }