Compare commits

..

No commits in common. "c3f18b38f57b55a110493317f09b7d5d26be9240" and "c67b08737ed0c393f8f598c7cbf4ae17a2e52632" have entirely different histories.

6 changed files with 28 additions and 359 deletions

View File

@ -8,7 +8,7 @@ $state = bin2hex(random_bytes(16));
$_SESSION['pkce_verifier'] = $verifier;
$_SESSION['oauth_state'] = $state;
$_SESSION['oauth_return'] = null;
$_SESSION['oauth_return'] = safe_return_path($_GET['return'] ?? ($_SERVER['HTTP_REFERER'] ?? null));
$params = http_build_query([
'response_type' => 'code',

View File

@ -416,96 +416,3 @@
.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); }

View File

@ -2,33 +2,14 @@
const PLANNER_KEY = 'ff14-planner-plans';
const PLANNER_ACTIVE_KEY = 'ff14-planner-active-plan';
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 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));
function savePlans(plans) {
localStorage.setItem(PLANNER_KEY, JSON.stringify(plans));
}
// ── CRUD ──────────────────────────────────────────────────────────────────────
@ -41,7 +22,6 @@ function createPlan(name) {
updatedAt: Date.now(),
source: null,
mitigationNames: {},
folderId: null,
jobComposition: Array(8).fill(''),
mechanics: []
};
@ -68,14 +48,6 @@ 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);
@ -83,7 +55,7 @@ function copyPlan(id) {
const copy = {
...JSON.parse(JSON.stringify(orig)),
id: crypto.randomUUID(),
name: uniquePlanName(orig.name + ' (Kopie)'),
name: orig.name + ' (Kopie)',
createdAt: Date.now(),
updatedAt: Date.now()
};
@ -94,8 +66,7 @@ function copyPlan(id) {
// ── State ─────────────────────────────────────────────────────────────────────
let activePlanId = null;
let collapsedFolders = new Set();
let activePlanId = null;
// ── Helpers ───────────────────────────────────────────────────────────────────
@ -140,68 +111,30 @@ function sameMechanic(existing, incoming, source) {
// ── Rendering: Plan List ──────────────────────────────────────────────────────
function planItemHtml(p) {
return `
function renderPlanList() {
const el = document.getElementById('plan-list');
if (!el) return;
const plans = loadPlans();
if (plans.length === 0) {
el.innerHTML = '<div class="plan-list-empty">Noch keine Pläne vorhanden</div>';
return;
}
el.innerHTML = plans.map(p => `
<div class="plan-item${p.id === activePlanId ? ' active' : ''}" data-id="${escHtml(p.id)}">
<div class="plan-item-info">
<div class="plan-item-name">${escHtml(p.name)}</div>
<div class="plan-item-meta">${p.mechanics.length} Mechaniken &middot; ${fmtDate(p.updatedAt)}</div>
</div>
<div class="plan-item-actions">
<button class="plan-btn plan-btn-move" data-id="${escHtml(p.id)}" title="In Ordner verschieben">📁</button>
<button class="plan-btn plan-btn-copy" data-id="${escHtml(p.id)}" title="Kopieren"></button>
<button class="plan-btn plan-btn-danger plan-btn-delete" data-id="${escHtml(p.id)}" title="Löschen"></button>
</div>
</div>`;
}
</div>
`).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 = '<div class="plan-list-empty">Noch keine Pläne vorhanden</div>';
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 `
<div class="folder-section" data-folder-id="${escHtml(folder.id)}">
<div class="folder-row" data-folder-id="${escHtml(folder.id)}">
<span class="folder-chevron${collapsed ? '' : ' open'}"></span>
<span class="folder-name-text">${escHtml(folder.name)}</span>
<span class="folder-count">${folderPlans.length}</span>
<div class="folder-actions">
<button class="plan-btn folder-rename-btn" data-id="${escHtml(folder.id)}" title="Umbenennen"></button>
<button class="plan-btn plan-btn-danger folder-delete-btn" data-id="${escHtml(folder.id)}" title="Ordner löschen"></button>
</div>
</div>
<div class="folder-plans${collapsed ? ' collapsed' : ''}">
${folderPlans.map(p => planItemHtml(p)).join('') || '<div class="folder-empty">Leer</div>'}
</div>
</div>`;
}).join('') + ungrouped.map(p => planItemHtml(p)).join('');
el.innerHTML = html || '<div class="plan-list-empty">Noch keine Pläne vorhanden</div>';
// Plan item: open
el.querySelectorAll('.plan-item').forEach(row => {
row.addEventListener('click', e => {
if (e.target.closest('.plan-item-actions')) return;
@ -209,15 +142,6 @@ 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();
@ -226,7 +150,6 @@ function renderPlanList() {
});
});
// Plan item: delete
el.querySelectorAll('.plan-btn-delete').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
@ -234,63 +157,10 @@ 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); }
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);
if (activePlanId === btn.dataset.id) {
activePlanId = null;
renderPlanDetail(null);
}
renderPlanList();
});
});
@ -490,14 +360,6 @@ 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 ────────────────────────────────────────────────────────────────────
@ -562,105 +424,15 @@ 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 => `
<div class="folder-picker-option${(plan.folderId ?? null) === f.id ? ' active' : ''}" data-fid="${escHtml(f.id ?? '')}">
${f.id === null ? '— ' : '📁 '}${escHtml(f.name)}
</div>`).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();
@ -946,7 +718,7 @@ function doImport(data, withMitigations, whereMode, mergeId, newName) {
const source = { reportCode, fightId, fightName, fightStart, fightEnd, language: plannerLanguage() };
if (whereMode === 'new') {
const plan = createPlan(uniquePlanName(newName || fightName || 'Importierter Plan'));
const plan = createPlan(newName || fightName || 'Importierter Plan');
return updatePlan(plan.id, {
mechanics,
source,
@ -1170,9 +942,9 @@ let pendingImportData = null;
function showImportModal(data) {
pendingImportData = data;
// Pre-fill name — auto-unique so the user sees immediately what name will be used
// Pre-fill name
const nameInput = document.getElementById('import-plan-name');
if (nameInput) { nameInput.value = uniquePlanName(data.fightName || 'Importierter Plan'); }
if (nameInput) { nameInput.value = data.fightName || ''; }
// Populate merge dropdown
const planSelect = document.getElementById('import-plan-select');
@ -1277,7 +1049,6 @@ window.plannerTab = {
document.addEventListener('DOMContentLoaded', () => {
initNewPlanForm();
initNewFolderForm();
initImportModal();
initAbilityModal();
activePlanId = localStorage.getItem(PLANNER_ACTIVE_KEY);

View File

@ -21,7 +21,7 @@
<?php endif; ?>
</p>
<a href="auth/start.php" class="btn btn-gold btn-login">
<a href="<?= htmlspecialchars(auth_start_href(), ENT_QUOTES) ?>" class="btn btn-gold btn-login">
<?= $tokenExpired ? 'Reconnect to FFLogs' : 'Connect to FFLogs' ?>
</a>

View File

@ -23,7 +23,7 @@
</select>
</div>
<button class="btn btn-gold" type="submit" style="align-self:flex-end">Fetch</button>
<a class="btn" href="auth/start.php" style="align-self:flex-end;text-decoration:none">Reconnect</a>
<a class="btn" href="<?= htmlspecialchars(auth_start_href(), ENT_QUOTES) ?>" style="align-self:flex-end;text-decoration:none">Reconnect</a>
</div>
</form>
</div>

View File

@ -4,18 +4,9 @@
<div class="plan-sidebar">
<div class="plan-sidebar-header">
<div class="card-title">Pläne</div>
<button id="planner-new-folder-btn" class="btn btn-sm" title="Neuer Ordner">+ Ordner</button>
<button id="planner-new-btn" class="btn btn-sm btn-gold">+ Neu</button>
</div>
<div id="planner-new-folder-form" class="plan-new-form" style="display:none">
<input type="text" id="planner-new-folder-name" placeholder="Ordner-Name…">
<div class="plan-new-actions">
<button id="planner-new-folder-save" class="btn btn-sm btn-gold">Erstellen</button>
<button id="planner-new-folder-cancel" class="btn btn-sm">Abbrechen</button>
</div>
</div>
<div id="planner-new-form" class="plan-new-form" style="display:none">
<input type="text" id="planner-new-name" placeholder="Plan-Name…">
<div class="plan-new-actions">