From 5010da4ddbb451f6275cfa6b8db94051108d91e5 Mon Sep 17 00:00:00 2001 From: xziino Date: Fri, 22 May 2026 11:20:17 +0200 Subject: [PATCH 1/3] Fix: auth_start_href() durch direkte Links auf auth/start.php ersetzen Co-Authored-By: Claude Sonnet 4.6 --- auth/start.php | 2 +- templates/login.php | 2 +- templates/report-form.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/auth/start.php b/auth/start.php index a3f96da..0dec0e6 100644 --- a/auth/start.php +++ b/auth/start.php @@ -8,7 +8,7 @@ $state = bin2hex(random_bytes(16)); $_SESSION['pkce_verifier'] = $verifier; $_SESSION['oauth_state'] = $state; -$_SESSION['oauth_return'] = safe_return_path($_GET['return'] ?? ($_SERVER['HTTP_REFERER'] ?? null)); +$_SESSION['oauth_return'] = null; $params = http_build_query([ 'response_type' => 'code', diff --git a/templates/login.php b/templates/login.php index 336f18c..ef804a3 100644 --- a/templates/login.php +++ b/templates/login.php @@ -21,7 +21,7 @@

- diff --git a/templates/report-form.php b/templates/report-form.php index 9f0c275..09da893 100644 --- a/templates/report-form.php +++ b/templates/report-form.php @@ -23,7 +23,7 @@ - Reconnect + Reconnect From be9d0500367939dd48d8ee2aa44681615043d33e Mon Sep 17 00:00:00 2001 From: xziino Date: Fri, 22 May 2026 12:25:44 +0200 Subject: [PATCH 2/3] Planner: Ordner-Struktur, Rechtsklick-Remove, Duplikat-Schutz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ordner CRUD (erstellen, umbenennen, lΓΆschen) in der Plan-Sidebar - PlΓ€ne per πŸ“-Button in Ordner verschieben (Picker-Dropdown) - Ordner auf-/zuklappbar mit Chevron-Animation - Rechtsklick auf Ability-Badge entfernt die Zuweisung - uniquePlanName(): Duplikat-Namen beim manuellen Erstellen blockieren - Import-Modal: Name wird direkt mit eindeutigem Namen vorbefΓΌllt Co-Authored-By: Claude Sonnet 4.6 --- css/planner.css | 93 +++++++++++++ js/planner.js | 277 ++++++++++++++++++++++++++++++++++---- templates/tab-planner.php | 9 ++ 3 files changed, 355 insertions(+), 24 deletions(-) diff --git a/css/planner.css b/css/planner.css index 3a12bba..c06fbb9 100644 --- a/css/planner.css +++ b/css/planner.css @@ -416,3 +416,96 @@ .ability-chip.badge-assign-shield.ability-chip--active { background: rgba(74,158,255,.18); border-color: rgba(74,158,255,.6); color: var(--blue); } .ability-chip--other-job { opacity: 0.45; } + +/* ── Folder Sidebar ──────────────────────────────────────────────────────────── */ +.folder-section { margin-bottom: 2px; } + +.folder-row { + display: flex; + align-items: center; + gap: 6px; + padding: 7px 10px; + border-radius: var(--r); + cursor: pointer; + transition: background 0.12s; + user-select: none; +} +.folder-row:hover { background: var(--bg2); } + +.folder-chevron { + font-size: 9px; + color: var(--t3); + transition: transform 0.15s; + display: inline-block; + width: 10px; + flex-shrink: 0; +} +.folder-chevron.open { transform: rotate(90deg); } + +.folder-name-text { + font-size: 13px; + color: var(--t1); + font-weight: 500; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.folder-name-input { + font-size: 13px !important; + padding: 2px 6px !important; + flex: 1; + width: auto !important; + min-width: 80px; +} + +.folder-count { + font-size: 11px; + color: var(--t3); + background: var(--bg3); + border-radius: 10px; + padding: 1px 6px; + flex-shrink: 0; +} + +.folder-actions { + display: flex; + gap: 4px; + flex-shrink: 0; + opacity: 0; + transition: opacity 0.12s; +} +.folder-row:hover .folder-actions { opacity: 1; } + +.folder-plans { padding-left: 10px; } +.folder-plans.collapsed { display: none; } + +.folder-empty { + font-size: 12px; + color: var(--t3); + padding: 5px 10px; + font-style: italic; +} + +/* ── Folder Picker Dropdown ──────────────────────────────────────────────────── */ +.folder-picker { + background: var(--bgcard); + border: 1px solid var(--borderem); + border-radius: var(--r); + box-shadow: 0 4px 16px rgba(0,0,0,0.5); + min-width: 160px; + overflow: hidden; +} + +.folder-picker-option { + padding: 7px 14px; + font-size: 13px; + color: var(--t2); + cursor: pointer; + transition: background 0.1s; + white-space: nowrap; +} +.folder-picker-option:hover { background: var(--bg2); color: var(--t1); } +.folder-picker-option.active { color: var(--gold); } diff --git a/js/planner.js b/js/planner.js index 0664b0b..4ac4237 100644 --- a/js/planner.js +++ b/js/planner.js @@ -1,14 +1,33 @@ // ── Storage ─────────────────────────────────────────────────────────────────── const PLANNER_KEY = 'ff14-planner-plans'; +const FOLDERS_KEY = 'ff14-planner-folders'; function loadPlans() { try { return JSON.parse(localStorage.getItem(PLANNER_KEY) || '[]'); } catch { return []; } } +function savePlans(plans) { localStorage.setItem(PLANNER_KEY, JSON.stringify(plans)); } -function savePlans(plans) { - localStorage.setItem(PLANNER_KEY, JSON.stringify(plans)); +function loadFolders() { + try { return JSON.parse(localStorage.getItem(FOLDERS_KEY) || '[]'); } + catch { return []; } +} +function saveFolders(f) { localStorage.setItem(FOLDERS_KEY, JSON.stringify(f)); } + +function createFolder(name) { + const folder = { id: crypto.randomUUID(), name: name.trim() || 'Neuer Ordner', createdAt: Date.now(), updatedAt: Date.now() }; + const all = loadFolders(); all.push(folder); saveFolders(all); return folder; +} +function updateFolder(id, changes) { + const all = loadFolders(), idx = all.findIndex(f => f.id === id); + if (idx === -1) return; + all[idx] = { ...all[idx], ...changes, updatedAt: Date.now() }; + saveFolders(all); +} +function deleteFolder(id) { + saveFolders(loadFolders().filter(f => f.id !== id)); + savePlans(loadPlans().map(p => p.folderId === id ? { ...p, folderId: null, updatedAt: Date.now() } : p)); } // ── CRUD ────────────────────────────────────────────────────────────────────── @@ -20,6 +39,7 @@ function createPlan(name) { createdAt: Date.now(), updatedAt: Date.now(), source: null, + folderId: null, jobComposition: Array(8).fill(''), mechanics: [] }; @@ -46,6 +66,14 @@ function deletePlan(id) { savePlans(loadPlans().filter(p => p.id !== id)); } +function uniquePlanName(baseName) { + const names = new Set(loadPlans().map(p => p.name)); + if (!names.has(baseName)) return baseName; + let i = 2; + while (names.has(`${baseName} ${i}`)) i++; + return `${baseName} ${i}`; +} + function copyPlan(id) { const all = loadPlans(); const orig = all.find(p => p.id === id); @@ -64,7 +92,8 @@ function copyPlan(id) { // ── State ───────────────────────────────────────────────────────────────────── -let activePlanId = null; +let activePlanId = null; +let collapsedFolders = new Set(); // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -91,30 +120,68 @@ function fmtNumber(n) { // ── Rendering: Plan List ────────────────────────────────────────────────────── -function renderPlanList() { - const el = document.getElementById('plan-list'); - if (!el) return; - - const plans = loadPlans(); - - if (plans.length === 0) { - el.innerHTML = '
Noch keine PlΓ€ne vorhanden
'; - return; - } - - el.innerHTML = plans.map(p => ` +function planItemHtml(p) { + return `
${escHtml(p.name)}
${p.mechanics.length} Mechaniken · ${fmtDate(p.updatedAt)}
+
-
- `).join(''); + `; +} +function renderPlanList() { + const el = document.getElementById('plan-list'); + if (!el) return; + + const plans = loadPlans(); + const folders = loadFolders(); + + if (plans.length === 0 && folders.length === 0) { + el.innerHTML = '
Noch keine PlΓ€ne vorhanden
'; + return; + } + + const byFolder = new Map(); + const ungrouped = []; + for (const p of plans) { + const fid = p.folderId ?? null; + if (fid && folders.some(f => f.id === fid)) { + if (!byFolder.has(fid)) byFolder.set(fid, []); + byFolder.get(fid).push(p); + } else { + ungrouped.push(p); + } + } + + let html = folders.map(folder => { + const folderPlans = byFolder.get(folder.id) ?? []; + const collapsed = collapsedFolders.has(folder.id); + return ` +
+
+ β–Ά + ${escHtml(folder.name)} + ${folderPlans.length} +
+ + +
+
+
+ ${folderPlans.map(p => planItemHtml(p)).join('') || '
Leer
'} +
+
`; + }).join('') + ungrouped.map(p => planItemHtml(p)).join(''); + + el.innerHTML = html || '
Noch keine PlΓ€ne vorhanden
'; + + // Plan item: open el.querySelectorAll('.plan-item').forEach(row => { row.addEventListener('click', e => { if (e.target.closest('.plan-item-actions')) return; @@ -122,6 +189,15 @@ function renderPlanList() { }); }); + // Plan item: move to folder + el.querySelectorAll('.plan-btn-move').forEach(btn => { + btn.addEventListener('click', e => { + e.stopPropagation(); + showFolderPicker(btn.dataset.id, btn); + }); + }); + + // Plan item: copy el.querySelectorAll('.plan-btn-copy').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); @@ -130,6 +206,7 @@ function renderPlanList() { }); }); + // Plan item: delete el.querySelectorAll('.plan-btn-delete').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); @@ -137,10 +214,63 @@ function renderPlanList() { if (!plan) return; if (!confirm(`Plan "${plan.name}" lΓΆschen?`)) return; deletePlan(btn.dataset.id); - if (activePlanId === btn.dataset.id) { - activePlanId = null; - renderPlanDetail(null); - } + if (activePlanId === btn.dataset.id) { activePlanId = null; renderPlanDetail(null); } + renderPlanList(); + }); + }); + + // Folder row: toggle collapse + el.querySelectorAll('.folder-row').forEach(row => { + row.addEventListener('click', e => { + if (e.target.closest('.folder-actions')) return; + const fid = row.dataset.folderId; + if (collapsedFolders.has(fid)) collapsedFolders.delete(fid); + else collapsedFolders.add(fid); + renderPlanList(); + }); + }); + + // Folder: rename + el.querySelectorAll('.folder-rename-btn').forEach(btn => { + btn.addEventListener('click', e => { + e.stopPropagation(); + const fid = btn.dataset.id; + const folder = loadFolders().find(f => f.id === fid); + if (!folder) return; + const span = el.querySelector(`.folder-row[data-folder-id="${fid}"] .folder-name-text`); + if (!span) return; + const input = document.createElement('input'); + input.className = 'folder-name-input'; + input.value = folder.name; + span.replaceWith(input); + input.focus(); input.select(); + let saved = false; + const save = () => { + if (saved) return; saved = true; + const name = input.value.trim(); + if (name) updateFolder(fid, { name }); + renderPlanList(); + }; + input.addEventListener('blur', save); + input.addEventListener('keydown', ev => { + if (ev.key === 'Enter') { ev.preventDefault(); save(); } + if (ev.key === 'Escape') { saved = true; renderPlanList(); } + }); + }); + }); + + // Folder: delete + el.querySelectorAll('.folder-delete-btn').forEach(btn => { + btn.addEventListener('click', e => { + e.stopPropagation(); + const folder = loadFolders().find(f => f.id === btn.dataset.id); + if (!folder) return; + const count = (byFolder.get(btn.dataset.id) ?? []).length; + const msg = count > 0 + ? `Ordner "${folder.name}" lΓΆschen? Die ${count} enthaltenen PlΓ€ne werden nicht gelΓΆscht.` + : `Ordner "${folder.name}" lΓΆschen?`; + if (!confirm(msg)) return; + deleteFolder(btn.dataset.id); renderPlanList(); }); }); @@ -339,6 +469,14 @@ function initMechanicClicks(planId) { if (!card) return; showAbilityModal(planId, card.dataset.mechanicId); }); + list.addEventListener('contextmenu', e => { + const badge = e.target.closest('.badge-assign'); + if (!badge) return; + e.preventDefault(); + e.stopPropagation(); + const removeBtn = badge.querySelector('.badge-remove'); + if (removeBtn) removeAssignment(planId, removeBtn.dataset.mechanicId, removeBtn.dataset.ability); + }); } // ── Rename ──────────────────────────────────────────────────────────────────── @@ -401,15 +539,105 @@ function initNewPlanForm() { cancel.addEventListener('click', () => { form.style.display = 'none'; }); + const clearErr = () => { + input.style.borderColor = ''; + document.getElementById('planner-new-name-err')?.remove(); + }; + const doCreate = () => { const name = input.value.trim(); if (!name) { input.focus(); return; } + if (loadPlans().some(p => p.name === name)) { + input.style.borderColor = 'var(--red)'; + let err = document.getElementById('planner-new-name-err'); + if (!err) { + err = document.createElement('div'); + err.id = 'planner-new-name-err'; + err.style.cssText = 'font-size:11px;color:var(--red);margin-top:3px'; + input.insertAdjacentElement('afterend', err); + } + err.textContent = 'Name bereits vergeben'; + input.focus(); + return; + } + clearErr(); const plan = createPlan(name); form.style.display = 'none'; renderPlanList(); openPlan(plan.id); }; + input.addEventListener('input', clearErr); + save.addEventListener('click', doCreate); + input.addEventListener('keydown', e => { + if (e.key === 'Enter') doCreate(); + if (e.key === 'Escape') { clearErr(); form.style.display = 'none'; } + }); +} + +// ── Folder Picker ───────────────────────────────────────────────────────────── + +let folderPickerCleanup = null; + +function closeFolderPicker() { + document.getElementById('folder-picker')?.remove(); + if (folderPickerCleanup) { folderPickerCleanup(); folderPickerCleanup = null; } +} + +function showFolderPicker(planId, anchorEl) { + closeFolderPicker(); + const plan = getPlan(planId); + const folders = loadFolders(); + if (!plan) return; + if (folders.length === 0) return; + + const rect = anchorEl.getBoundingClientRect(); + const picker = document.createElement('div'); + picker.id = 'folder-picker'; + picker.className = 'folder-picker'; + picker.style.cssText = `position:fixed;top:${rect.bottom + 4}px;left:${rect.left}px;z-index:9999`; + + picker.innerHTML = [{ id: null, name: 'Kein Ordner' }, ...folders].map(f => ` +
+ ${f.id === null ? 'β€” ' : 'πŸ“ '}${escHtml(f.name)} +
`).join(''); + + document.body.appendChild(picker); + + picker.querySelectorAll('.folder-picker-option').forEach(opt => { + opt.addEventListener('click', e => { + e.stopPropagation(); + updatePlan(planId, { folderId: opt.dataset.fid || null }); + closeFolderPicker(); + renderPlanList(); + }); + }); + + const onOutside = e => { if (!picker.contains(e.target)) closeFolderPicker(); }; + setTimeout(() => document.addEventListener('click', onOutside), 0); + folderPickerCleanup = () => document.removeEventListener('click', onOutside); +} + +// ── New Folder Form ─────────────────────────────────────────────────────────── + +function initNewFolderForm() { + const btn = document.getElementById('planner-new-folder-btn'); + const form = document.getElementById('planner-new-folder-form'); + const input = document.getElementById('planner-new-folder-name'); + const save = document.getElementById('planner-new-folder-save'); + const cancel = document.getElementById('planner-new-folder-cancel'); + if (!btn) return; + + btn.addEventListener('click', () => { form.style.display = ''; input.value = ''; input.focus(); }); + cancel.addEventListener('click', () => { form.style.display = 'none'; }); + + const doCreate = () => { + const name = input.value.trim(); + if (!name) { input.focus(); return; } + createFolder(name); + form.style.display = 'none'; + renderPlanList(); + }; save.addEventListener('click', doCreate); input.addEventListener('keydown', e => { if (e.key === 'Enter') doCreate(); @@ -680,7 +908,7 @@ function doImport(data, withMitigations, whereMode, mergeId, newName) { const mechanics = aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitigations); if (whereMode === 'new') { - const plan = createPlan(newName || fightName || 'Importierter Plan'); + const plan = createPlan(uniquePlanName(newName || fightName || 'Importierter Plan')); return updatePlan(plan.id, { mechanics, source: { reportCode, fightName }, @@ -821,9 +1049,9 @@ let pendingImportData = null; function showImportModal(data) { pendingImportData = data; - // Pre-fill name + // Pre-fill name β€” auto-unique so the user sees immediately what name will be used const nameInput = document.getElementById('import-plan-name'); - if (nameInput) { nameInput.value = data.fightName || ''; } + if (nameInput) { nameInput.value = uniquePlanName(data.fightName || 'Importierter Plan'); } // Populate merge dropdown const planSelect = document.getElementById('import-plan-select'); @@ -928,6 +1156,7 @@ window.plannerTab = { document.addEventListener('DOMContentLoaded', () => { initNewPlanForm(); + initNewFolderForm(); initImportModal(); initAbilityModal(); renderPlanList(); diff --git a/templates/tab-planner.php b/templates/tab-planner.php index 98109ca..c657927 100644 --- a/templates/tab-planner.php +++ b/templates/tab-planner.php @@ -4,9 +4,18 @@
PlΓ€ne
+
+ +