From 4e992f587be0992bbf541327b2c84902e1273d95 Mon Sep 17 00:00:00 2001 From: phillip Date: Fri, 8 Aug 2025 19:55:36 +0200 Subject: [PATCH] =?UTF-8?q?src/App=20copy.jsx=20gel=C3=B6scht?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App copy.jsx | 400 ----------------------------------------------- 1 file changed, 400 deletions(-) delete mode 100644 src/App copy.jsx diff --git a/src/App copy.jsx b/src/App copy.jsx deleted file mode 100644 index e58aa69..0000000 --- a/src/App copy.jsx +++ /dev/null @@ -1,400 +0,0 @@ -/* -Apple-like CSV Card Viewer — v3 - -Updates in this version (per your request): -- Precompute image URLs once when CSV or template changes to avoid repeated heavy string work during render. -- Thumbnails use lazy-loading and low fetch priority; main image uses high priority — reduces network contention. -- Use "edition" as the preferred default column name (search for 'edition' before 'number'). -- Add Dark Mode with a toggle (persisted to localStorage). Uses CSS variables so it works whether or not Tailwind is configured for dark mode. -- Removed the Apple logo and the CSV preview panel (cleaner UI). - -Drop this into a React + Tailwind project (Vite/CRA). If you want a vanilla HTML/JS single-file version, I can produce that next. -*/ - -import React, { useState, useEffect, useRef } from "react"; - -// Minimal CSV parser (handles quoted values) -function parseCSV(text) { - const rows = []; - let row = []; - let cur = ""; - let i = 0; - let inQuotes = false; - - while (i < text.length) { - const ch = text[i]; - if (inQuotes) { - if (ch === '"') { - if (i + 1 < text.length && text[i + 1] === '"') { - cur += '"'; - i += 2; - continue; - } else { - inQuotes = false; - i++; - continue; - } - } else { - cur += ch; - i++; - continue; - } - } else { - if (ch === ',') { - row.push(cur); - cur = ''; - i++; - continue; - } - if (ch === '\r') { i++; continue; } - if (ch === '\n') { - row.push(cur); - rows.push(row); - row = []; - cur = ''; - i++; - continue; - } - if (ch === '"') { - inQuotes = true; - i++; - continue; - } - cur += ch; - i++; - } - } - if (cur !== '' || row.length > 0) { - row.push(cur); - rows.push(row); - } - const maxLen = rows.reduce((m, r) => Math.max(m, r.length), 0); - return rows.map(r => { while (r.length < maxLen) r.push(''); return r; }); -} - -function slugify(s) { - if (!s && s !== 0) return ''; - return String(s) - .toLowerCase() - .replace(/[^a-z0-9-\s]/g, '') - .trim() - .replace(/\s+/g, '-'); -} - -export default function App(){ - const [rows, setRows] = useState([]); - const [headers, setHeaders] = useState([]); - const [charCol, setCharCol] = useState(null); - const [edCol, setEdCol] = useState(null); - const [imgCol, setImgCol] = useState(null); - const [baseTemplate, setBaseTemplate] = useState('http://d2l56h9h5tj8ue.cloudfront.net/images/cards/{slug(character)}-{edition}.jpg'); - const [useSlug, setUseSlug] = useState(true); - const [currentIndex, setCurrentIndex] = useState(0); - const [filterText, setFilterText] = useState(''); - const [filteredIdx, setFilteredIdx] = useState([]); - const [error, setError] = useState(''); - const [imageCache, setImageCache] = useState([]); // precomputed image URLs per row - const fileInputRef = useRef(); - - // Dark mode - const [darkMode, setDarkMode] = useState(() => { - try { return localStorage.getItem('darkMode') === 'true'; } catch { return false; } - }); - useEffect(()=>{ try { localStorage.setItem('darkMode', darkMode ? 'true' : 'false'); } catch{} }, [darkMode]); - - // Build filtered index whenever rows or filter change - useEffect(() => { - const idx = []; - for (let i = 0; i < rows.length; i++) { - const r = rows[i]; - const joined = r.join(' ').toLowerCase(); - if (!filterText || joined.includes(filterText.toLowerCase())) idx.push(i); - } - setFilteredIdx(idx); - if (idx.length === 0) setCurrentIndex(0); - else if (!idx.includes(currentIndex)) setCurrentIndex(idx[0]); - }, [rows, filterText]); - - useEffect(() => { - function onKey(e){ - if (!rows.length) return; - if (e.key === 'ArrowRight') next(); - if (e.key === 'ArrowLeft') prev(); - } - window.addEventListener('keydown', onKey); - return () => window.removeEventListener('keydown', onKey); - }, [rows, filteredIdx, currentIndex]); - - function prev(){ - if (!filteredIdx.length) return; - const p = filteredIdx.indexOf(currentIndex); - const newIndex = filteredIdx[(p - 1 + filteredIdx.length) % filteredIdx.length]; - setCurrentIndex(newIndex); - } - function next(){ - if (!filteredIdx.length) return; - const p = filteredIdx.indexOf(currentIndex); - const newIndex = filteredIdx[(p + 1) % filteredIdx.length]; - setCurrentIndex(newIndex); - } - - // Precompute image URLs for every row to avoid doing string replacements repeatedly during render - useEffect(()=>{ - if (!rows || rows.length === 0 || headers.length === 0) { setImageCache([]); return; } - const cache = new Array(rows.length); - for (let r = 0; r < rows.length; r++){ - const row = rows[r]; - // If user selected an image column, use it - if (imgCol !== null && headers[imgCol]){ - const v = row[imgCol]; - if (v && v.trim()) { cache[r] = v.trim(); continue; } - } - // Build from template (one pass) - let url = baseTemplate; - const ctx = {}; - headers.forEach((h, i) => { ctx[h] = row[i]; }); - const charVal = (charCol !== null && headers[charCol]) ? row[charCol] : ''; - const edVal = (edCol !== null && headers[edCol]) ? row[edCol] : ''; - // slug replacements - url = url.replace(/\{slug\(([^}]+)\)\}/g, (_, inner) => { - const key = inner.trim(); - const v = key === 'character' ? charVal : (ctx[key] ?? ''); - return slugify(v); - }); - // basic placeholders - url = url.replace(/\{character\}/g, charVal); - url = url.replace(/\{edition\}/g, edVal); - // fallback replace any {field} - url = url.replace(/\{([^}]+)\}/g, (_, f) => { - return (ctx[f] ?? ''); - }); - if (useSlug) { - url = url.replace(/\{character\}/g, slugify(charVal)); - } - cache[r] = url; - } - setImageCache(cache); - }, [rows, headers, baseTemplate, useSlug, imgCol, charCol, edCol]); - - function onFile(e){ - setError(''); - const f = e.target.files ? e.target.files[0] : e; - if (!f) return; - const reader = new FileReader(); - reader.onload = ev => { - try{ - const txt = ev.target.result; - const parsed = parseCSV(txt); - if (parsed.length < 1) { setError('Empty CSV'); return; } - const hdr = parsed[0].map(h => h.trim() || 'col' ); - const data = parsed.slice(1).map(r => r.map(c => c.trim())); - setHeaders(hdr); - setRows(data); - setCurrentIndex(0); - // guess columns - const lower = hdr.map(h => h.toLowerCase()); - // character guess - let charGuess = lower.findIndex(h => h.includes('character') || h.includes('name')); - if (charGuess === -1) charGuess = lower.findIndex(h=>h.includes('hero')); - if (charGuess === -1) charGuess = 0; - // edition guess: prefer 'edition' before 'number' - let edGuess = lower.findIndex(h => h.includes('edition')); - if (edGuess === -1) edGuess = lower.findIndex(h => h.includes('number') || h.includes('no') || h.includes('num') || h.includes('issue')); - if (edGuess === -1) edGuess = 1; - let imageGuess = lower.findIndex(h => h.includes('image') || h.includes('img') || h.includes('url')); - if (imageGuess === -1) imageGuess = null; - setCharCol(charGuess >=0 ? charGuess : null); - setEdCol(edGuess >=0 ? edGuess : null); - setImgCol(imageGuess); - } catch(err){ - setError('Failed to parse CSV: '+String(err)); - } - }; - reader.readAsText(f); - } - - // Accept drag and drop - useEffect(()=>{ - function onDrop(e){ - e.preventDefault(); - if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]) onFile(e.dataTransfer.files[0]); - } - function prevent(e){ e.preventDefault(); } - window.addEventListener('drop', onDrop); - window.addEventListener('dragover', prevent); - return () => { window.removeEventListener('drop', onDrop); window.removeEventListener('dragover', prevent); }; - }, []); - - const currentRow = rows[currentIndex] || null; - const currentImage = imageCache[currentIndex] || ''; - - return ( -
-
- -
-
-

CSV Card Viewer

-

Upload a CSV and browse cards with images.

-
- -
- - - -
-
- -
- {/* Left: Controls & list */} -
-
-

Settings & Mapping

-

Map CSV columns and set the image template.

- -
-
- - -
-
- - -
-
- - -
- -
- - setBaseTemplate(e.target.value)} className="w-full mt-1 rounded-md p-2 border panel" /> -

Use placeholders: {'{character}'}, {'{edition}'}, or {'{slug(character)}'}.

-
- -
- setUseSlug(e.target.checked)} /> - -
- -
- - setFilterText(e.target.value)} className="w-full mt-1 rounded-md p-2 border panel" /> -
- -
-
- -
-

Thumbnail gallery

-

Click a thumbnail to jump to that card. Keyboard: ← →

-
- {filteredIdx.length === 0 &&
No items (upload CSV first)
} - {filteredIdx.map(i => ( - - ))} -
-
-
- - {/* Main: Large image + details */} -
-
- {currentRow ? ( -
-
-
-
-
-
- {/* main image: high priority */} - {currentRow[charCol] { e.currentTarget.src = ''; e.currentTarget.alt = 'Image not available'; }} className="w-full h-full object-cover" /> -
-
-
-
-
{ headers[charCol] ? currentRow[charCol] : currentRow[0] }
-
{ headers[edCol] ? `${currentRow[edCol]}` : '' }
-
-
- - -
-
-
-
- -
-
-

Details

-
- {headers.map((h, idx) => ( -
-
{h}
-
{ currentRow[idx] }
-
- ))} -
-
-
-
-
- ) : ( -
-

No card selected.

-

Upload a CSV file to begin. Drag & drop CSV anywhere on the window too.

-
- )} -
-
- -
- - {error &&
{error}
} - - -
-
- ); -}