From 02bcf6909619301bb0628abecc1e7640da369fb4 Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 19 Nov 2025 00:20:22 +0100 Subject: [PATCH] enhance: integrated UI with visual loading feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Integrate file upload controls with status display for cleaner UX - Add dark grey internal styling vs. dark green external theme - Create enhanced SVG template with prominent month indicators - Improve file loading error detection and user guidance - Add visual confirmation system for external resource loading - Update generator to support enhanced template styling - Fix CSV/template loading context binding issues 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- engine.js | 356 +++++++++++++++++++++++++++++++++++-------- example/style.css | 65 +++++++- example/template.svg | 53 ++++++- generator.js | 54 +++++-- index.html | 279 +++++++++++++++++++++++++++++---- 5 files changed, 703 insertions(+), 104 deletions(-) diff --git a/engine.js b/engine.js index e8f1920..f409c74 100644 --- a/engine.js +++ b/engine.js @@ -4,20 +4,68 @@ window.timelineEngine = { csvOverride: false, cssOverride: false, + projectBasePath: '', + + updateFileStatus(fileType, filename, status = 'loaded') { + const statusElement = document.getElementById(`${fileType}File`); + + if (statusElement) { + if (filename && status === 'loaded') { + statusElement.textContent = filename; + statusElement.style.color = '#28a745'; + statusElement.title = `Loaded: ${filename}`; + } else if (filename && status === 'error') { + statusElement.textContent = `Error: ${filename}`; + statusElement.style.color = '#dc3545'; + statusElement.title = `Failed to load: ${filename}`; + } else { + statusElement.textContent = 'Not loaded'; + statusElement.style.color = '#6c757d'; + statusElement.title = 'No file loaded'; + } + } + + // Add visual feedback with brief highlight animation + if (statusElement && filename) { + statusElement.style.transform = 'scale(1.05)'; + setTimeout(() => { + statusElement.style.transform = 'scale(1)'; + }, 200); + } + }, + + resolveProjectPath(relativePath) { + if (!relativePath) return relativePath; + // If path is already absolute (starts with http/https) or has no base path, return as-is + if (relativePath.startsWith('http') || !this.projectBasePath) { + return relativePath; + } + const resolvedPath = this.projectBasePath + relativePath; + console.log("Resolved path:", relativePath, "->", resolvedPath); + return resolvedPath; + }, + async autoLoadDefaultProject() { + console.log("Starting autoLoadDefaultProject"); // Versuche zuerst Binect-Projekt, sonst Example - const candidates = ["binect/project.json", "example/project.json"]; - for (const path of candidates) { + const candidates = [ + { path: "binect/project.json", basePath: "binect/" }, + { path: "example/project.json", basePath: "example/" } + ]; + for (const candidate of candidates) { try { - const res = await fetch(path); + const res = await fetch(candidate.path); if (!res.ok) continue; const cfg = await res.json(); + this.projectBasePath = candidate.basePath; await this.loadProjectConfigObject(cfg); + console.log("Project loaded successfully from:", candidate.path); return; } catch (e) { continue; } } + console.log("No project could be auto-loaded"); }, async loadProjectConfigObject(cfg) { @@ -27,64 +75,175 @@ window.timelineEngine = { document.getElementById("projectSubtitle").innerText = cfg.description || "Projektkonfiguration geladen."; + // Update project status + this.updateFileStatus('project', name, 'loaded'); + + // Track loading errors for user feedback + const loadingErrors = []; + // Stylesheet if (cfg.stylesheet && !this.cssOverride) { - document.getElementById("dynamicCss").href = cfg.stylesheet; + const stylesheetPath = this.resolveProjectPath(cfg.stylesheet); + const linkElement = document.getElementById("dynamicCss"); + + // Set up load/error event handlers before setting href + const handleLoad = () => { + console.log("Stylesheet loaded successfully:", stylesheetPath); + this.updateFileStatus('css', cfg.stylesheet + ' ✨', 'loaded'); + linkElement.removeEventListener('load', handleLoad); + linkElement.removeEventListener('error', handleError); + }; + + const handleError = () => { + console.error("Stylesheet could not be loaded:", stylesheetPath); + this.updateFileStatus('css', cfg.stylesheet, 'error'); + loadingErrors.push(`Stylesheet: ${cfg.stylesheet}`); + linkElement.removeEventListener('load', handleLoad); + linkElement.removeEventListener('error', handleError); + }; + + linkElement.addEventListener('load', handleLoad); + linkElement.addEventListener('error', handleError); + linkElement.href = stylesheetPath; } // SVG template if (cfg.svgTemplate) { try { - this.template = await fetch(cfg.svgTemplate).then(r => r.text()); + const templatePath = this.resolveProjectPath(cfg.svgTemplate); + console.log("Attempting to fetch SVG template from:", templatePath); + const response = await fetch(templatePath); + console.log("SVG template fetch response:", response.status, response.statusText); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + this.template = await response.text(); + console.log("SVG template loaded, length:", this.template.length); + this.updateFileStatus('svg', cfg.svgTemplate, 'loaded'); } catch (e) { - console.warn("SVG template could not be loaded:", e); + console.error("SVG template could not be loaded:", e); + console.error("Failed SVG template path was:", this.resolveProjectPath(cfg.svgTemplate)); + this.updateFileStatus('svg', cfg.svgTemplate, 'error'); + loadingErrors.push(`SVG template: ${cfg.svgTemplate}`); } } // CSV data if (cfg.dataSource && !this.csvOverride) { try { - const csvText = await fetch(cfg.dataSource).then(r => r.text()); + const csvPath = this.resolveProjectPath(cfg.dataSource); + console.log("Attempting to fetch CSV from:", csvPath); + const response = await fetch(csvPath); + console.log("CSV fetch response:", response.status, response.statusText); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const csvText = await response.text(); + console.log("CSV text loaded, length:", csvText.length); + console.log("CSV preview:", csvText.substring(0, 200)); + + this.updateFileStatus('csv', cfg.dataSource, 'loaded'); this.processCsv(csvText); } catch (e) { - console.warn("CSV could not be loaded:", e); + console.error("CSV could not be loaded:", e); + console.error("Failed CSV path was:", this.resolveProjectPath(cfg.dataSource)); + this.updateFileStatus('csv', cfg.dataSource, 'error'); + loadingErrors.push(`CSV data: ${cfg.dataSource}`); } } + + // Show user feedback if there were loading errors + if (loadingErrors.length > 0) { + setTimeout(() => { + const viewer = document.getElementById("viewer"); + if (viewer && !viewer.innerHTML.includes(' +
⚠️
+

Projektdateien konnten nicht geladen werden

+

+ Fehlgeschlagene Dateien:
+ ${loadingErrors.map(err => `• ${err}`).join('
')} +

+

+ 💡 Tipp: Lade die Dateien manuell mit den Load-Buttons oben oder + verwende einen lokalen Server (make serve). +

+ + `; + } + }, 500); // Small delay to let other operations complete + } }, processCsv(text) { + console.log("processCsv called with text length:", text?.length); + if (!this.config || !this.config.fieldMapping) { console.error("No config or fieldMapping found."); return; } + + if (typeof Papa === 'undefined') { + console.error("Papa Parse library not available"); + document.getElementById("viewer").innerHTML = + "Fehler: CSV Parser nicht verfügbar."; + return; + } + const m = this.config.fieldMapping; + const self = this; // Capture 'this' context Papa.parse(text, { header: true, skipEmptyLines: true, complete: (res) => { + console.log("Papa.parse complete, found", res.data.length, "rows"); const rows = res.data; const items = rows.map((r) => { const dueField = (m.due || []).find(f => r[f]); - return { + const item = { id: m.id ? r[m.id] : (r["ID"] || ""), title: m.title ? r[m.title] : (r["Title"] || ""), lane: m.lane ? (r[m.lane] || "Ohne Epic") : (r["Lane"] || "Default"), - due: this.parseDate(dueField ? r[dueField] : null) + due: self.parseDate(dueField ? r[dueField] : null) }; + return item; }).filter(i => i.title && i.due); + console.log("Filtered to", items.length, "valid items"); + if (!items.length) { document.getElementById("viewer").innerHTML = "Keine gültigen Items gefunden."; return; } - const svg = window.timelineGenerator.generate(items, this.config, this.template); - document.getElementById("viewer").innerHTML = svg; - const dlBtn = document.getElementById("downloadSvg"); - dlBtn.disabled = false; - dlBtn.style.opacity = 1; + // Ensure generator is available + if (!window.timelineGenerator) { + console.error("Timeline generator not available"); + document.getElementById("viewer").innerHTML = + "Fehler: Timeline Generator nicht verfügbar."; + return; + } + + try { + console.log("Generating timeline with:", items, self.config, self.template ? "template loaded" : "no template"); + const svg = window.timelineGenerator.generate(items, self.config, self.template); + document.getElementById("viewer").innerHTML = svg; + const dlBtn = document.getElementById("downloadSvg"); + dlBtn.disabled = false; + dlBtn.style.opacity = 1; + console.log("Timeline generated successfully"); + } catch (error) { + console.error("Error generating timeline:", error); + document.getElementById("viewer").innerHTML = + "Fehler beim Generieren der Timeline."; + } } }); }, @@ -118,58 +277,131 @@ window.timelineEngine = { // --------- UI event handlers --------- -document.getElementById("projectInput").addEventListener("change", async (ev) => { - const file = ev.target.files[0]; - if (!file) return; - const text = await file.text(); - const cfg = JSON.parse(text); - await window.timelineEngine.loadProjectConfigObject(cfg); -}); +function setupEventHandlers() { + const projectInput = document.getElementById("projectInput"); + if (projectInput) { + projectInput.addEventListener("change", async (ev) => { + const file = ev.target.files[0]; + if (!file) return; + try { + const text = await file.text(); + const cfg = JSON.parse(text); -document.getElementById("csvInput").addEventListener("change", async (ev) => { - const file = ev.target.files[0]; - if (!file) return; - const text = await file.text(); - window.timelineEngine.csvOverride = true; - window.timelineEngine.processCsv(text); -}); + // For manually loaded projects, try to infer base path from filename + // If it's example/project.json or binect/project.json, set appropriate base path + const filename = file.name; + if (filename === 'project.json') { + // Try to detect if this is a known project by checking the config + if (cfg.name && cfg.name.includes('Example')) { + window.timelineEngine.projectBasePath = 'example/'; + console.log("Detected example project, setting base path to example/"); + } else { + window.timelineEngine.projectBasePath = ''; + console.log("Unknown project, clearing base path"); + } + } else { + window.timelineEngine.projectBasePath = ''; + } -document.getElementById("cssInput").addEventListener("change", async (ev) => { - const file = ev.target.files[0]; - if (!file) return; - const cssText = await file.text(); - window.timelineEngine.cssOverride = true; - const blob = new Blob([cssText], { type: "text/css" }); - document.getElementById("dynamicCss").href = URL.createObjectURL(blob); -}); + window.timelineEngine.updateFileStatus('project', file.name, 'loaded'); + await window.timelineEngine.loadProjectConfigObject(cfg); -document.getElementById("svgInput").addEventListener("change", async (ev) => { - const file = ev.target.files[0]; - if (!file) return; - window.timelineEngine.template = await file.text(); -}); + // Show message about relative paths if project has data sources + if (cfg.dataSource || cfg.stylesheet || cfg.svgTemplate) { + const viewer = document.getElementById("viewer"); + if (viewer && (viewer.innerHTML.includes("could not be loaded") || viewer.innerHTML.includes("Keine gültigen Items"))) { + viewer.innerHTML += "

💡 Hinweis: Stelle sicher, dass sich die referenzierten Dateien (CSV, CSS, SVG) im gleichen Verzeichnis wie die HTML-Datei befinden."; + } + } + } catch (error) { + console.error("Error loading project:", error); + window.timelineEngine.updateFileStatus('project', file.name, 'error'); + } + }); + } -document.getElementById("downloadSvg").addEventListener("click", () => { - const svg = document.querySelector("#viewer svg"); - if (!svg) return; + const csvInput = document.getElementById("csvInput"); + if (csvInput) { + csvInput.addEventListener("change", async (ev) => { + const file = ev.target.files[0]; + if (!file) return; + const text = await file.text(); + window.timelineEngine.csvOverride = true; + window.timelineEngine.updateFileStatus('csv', file.name, 'loaded'); + window.timelineEngine.processCsv(text); + }); + } - // Always external view for export: IDs ausblenden - svg.querySelectorAll(".item-id").forEach(el => el.style.display = "none"); + const cssInput = document.getElementById("cssInput"); + if (cssInput) { + cssInput.addEventListener("change", async (ev) => { + const file = ev.target.files[0]; + if (!file) return; + const cssText = await file.text(); + window.timelineEngine.cssOverride = true; + const blob = new Blob([cssText], { type: "text/css" }); + document.getElementById("dynamicCss").href = URL.createObjectURL(blob); + window.timelineEngine.updateFileStatus('css', file.name, 'loaded'); + }); + } - const now = new Date(); - const ts = String(now.getFullYear()).slice(2) - + String(now.getMonth() + 1).padStart(2, "0") - + String(now.getDate()).padStart(2, "0") - + "T" - + String(now.getHours()).padStart(2, "0") - + String(now.getMinutes()).padStart(2, "0"); + const svgInput = document.getElementById("svgInput"); + if (svgInput) { + svgInput.addEventListener("change", async (ev) => { + const file = ev.target.files[0]; + if (!file) return; + window.timelineEngine.template = await file.text(); + window.timelineEngine.updateFileStatus('svg', file.name, 'loaded'); + }); + } - const blob = new Blob([svg.outerHTML], { type: "image/svg+xml" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `${ts}-timeline.svg`; - a.click(); - URL.revokeObjectURL(url); -}); + const downloadSvg = document.getElementById("downloadSvg"); + if (downloadSvg) { + downloadSvg.addEventListener("click", () => { + const svg = document.querySelector("#viewer svg"); + if (!svg) return; + + // Always external view for export: IDs ausblenden + svg.querySelectorAll(".item-id").forEach(el => el.style.display = "none"); + + const now = new Date(); + const ts = String(now.getFullYear()).slice(2) + + String(now.getMonth() + 1).padStart(2, "0") + + String(now.getDate()).padStart(2, "0") + + "T" + + String(now.getHours()).padStart(2, "0") + + String(now.getMinutes()).padStart(2, "0"); + + const blob = new Blob([svg.outerHTML], { type: "image/svg+xml" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${ts}-timeline.svg`; + a.click(); + URL.revokeObjectURL(url); + }); + } + + const toggleView = document.getElementById("toggleView"); + if (toggleView) { + toggleView.addEventListener("click", () => { + const body = document.body; + const isInternal = body.classList.contains("internal-mode"); + + if (isInternal) { + body.classList.remove("internal-mode"); + body.classList.add("external-mode"); + toggleView.textContent = "Switch to Internal View"; + // Hide IDs in external mode + document.querySelectorAll(".item-id").forEach(el => el.style.display = "none"); + } else { + body.classList.remove("external-mode"); + body.classList.add("internal-mode"); + toggleView.textContent = "Switch to External View"; + // Show IDs in internal mode + document.querySelectorAll(".item-id").forEach(el => el.style.display = "inline"); + } + }); + } +} diff --git a/example/style.css b/example/style.css index c78f748..86bc14c 100644 --- a/example/style.css +++ b/example/style.css @@ -1 +1,64 @@ -body { background:#fafafa; } +/* Example Project Dark Green Theme */ +/* This CSS demonstrates successful external stylesheet loading */ + +body { + background: #1e3a2f !important; +} + +#projectName { + color: #2d8659 !important; + border-bottom: 2px solid #2d8659; + padding-bottom: 8px; +} + +#projectSubtitle { + color: #4a9b6b !important; +} + +/* File Manager Override */ +#fileManager { + background: #243329 !important; + border-color: #2d8659 !important; +} + +#fileManager h3 { + color: #4a9b6b !important; +} + +.file-item { + background: #2a3f32 !important; + border-color: #2d8659 !important; +} + +.file-item:hover { + border-color: #4a9b6b !important; + box-shadow: 0 2px 8px rgba(45, 134, 89, 0.2) !important; +} + +.file-label { + color: #4a9b6b !important; +} + +.upload-btn { + background: #2d8659 !important; +} + +.upload-btn:hover { + background: #1e5a3d !important; +} + +/* Buttons */ +button { + background: #2d8659 !important; +} + +button:hover:not(:disabled) { + background: #4a9b6b !important; +} + +/* Viewer */ +#viewer { + background: #2a3f32 !important; + border-color: #2d8659 !important; + color: #e8f5e8 !important; +} diff --git a/example/template.svg b/example/template.svg index 91cc643..b6a8951 100644 --- a/example/template.svg +++ b/example/template.svg @@ -1,5 +1,50 @@ - - - {{MONTHS}} - {{LANES}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 📊 External Template Active + + + Enhanced styling with prominent months ✨ + + + + + + {{MONTHS}} + + + + + {{LANES}} + + + + diff --git a/generator.js b/generator.js index aad2a93..6bae671 100644 --- a/generator.js +++ b/generator.js @@ -45,8 +45,22 @@ window.timelineGenerator = { months.forEach((m, i) => { const x = left + i * monthWidth; const label = m.toLocaleString("de-DE", { month: "short", year: "2-digit" }); - monthGraphics += ``; - monthGraphics += `${label}`; + + // Enhanced styling when using external template + if (template && template.includes('Enhanced')) { + // More prominent month indicators for external template + monthGraphics += ``; + monthGraphics += ``; + monthGraphics += `${label}`; + // Add month separator + if (i > 0) { + monthGraphics += ``; + } + } else { + // Default styling + monthGraphics += ``; + monthGraphics += `${label}`; + } }); // Helper to compute month index @@ -60,10 +74,18 @@ window.timelineGenerator = { laneNames.forEach((laneName, laneIdx) => { const laneY = top + laneIdx * (laneHeight + laneGap); - // Background band for lane - laneBlocks += ``; - // Lane label - laneBlocks += `${this.escapeXml(laneName)}`; + // Enhanced styling when using external template + if (template && template.includes('Enhanced')) { + // More subtle lane borders for enhanced template + laneBlocks += ``; + // Enhanced lane label + laneBlocks += `${this.escapeXml(laneName)}`; + } else { + // Default lane styling + laneBlocks += ``; + // Default lane label + laneBlocks += `${this.escapeXml(laneName)}`; + } const laneItems = laneMap.get(laneName).sort((a, b) => a.due - b.due); laneItems.forEach((it, idx) => { @@ -72,11 +94,21 @@ window.timelineGenerator = { const cx = left + clampedMi * monthWidth + monthWidth * 0.5; const cy = laneY + 10 + idx * 18; - laneBlocks += ``; - laneBlocks += ` - ${this.escapeXml(it.id || "")}: - ${this.escapeXml(it.title || "")} - `; + // Enhanced task item styling for external template + if (template && template.includes('Enhanced')) { + laneBlocks += ``; + laneBlocks += ` + ${this.escapeXml(it.id || "")}: + ${this.escapeXml(it.title || "")} + `; + } else { + // Default task item styling + laneBlocks += ``; + laneBlocks += ` + ${this.escapeXml(it.id || "")}: + ${this.escapeXml(it.title || "")} + `; + } }); }); diff --git a/index.html b/index.html index 1a81da5..273ab92 100644 --- a/index.html +++ b/index.html @@ -5,46 +5,273 @@ Timeline Generator - - + + + -

Timeline Generator

+

Timeline Generator

- Wähle ein Projekt oder lade CSV/CSS/SVG-Dateien, um eine Timeline zu erzeugen. + Lade Projektdateien um eine Timeline zu erstellen oder verwende den lokalen Server für automatisches Laden.

-
- - - - + +
+

Project Files

- - +
+
+
+ Project Configuration + +
+
+ Not loaded +
+
+ +
+
+ CSV Data + +
+
+ Not loaded +
+
+ +
+
+ Stylesheet + +
+
+ Not loaded +
+
+ +
+
+ SVG Template + +
+
+ Not loaded +
+
+
+ +
+ + +
- Noch keine Timeline generiert. +
+
📊
+

Keine Timeline verfügbar

+

+ Lade eine Projektkonfiguration oder CSV-Datei um zu beginnen. +

+