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]]({currentImage})
{ 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}
}
-
-
-
-
- );
-}