Skip to content

Commit 527f4fe

Browse files
committed
feat: new apps
1 parent d89e188 commit 527f4fe

File tree

2 files changed

+500
-0
lines changed

2 files changed

+500
-0
lines changed
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
import React, { useState, useRef } from 'react';
2+
import { Link } from 'react-router-dom';
3+
import {
4+
ArrowLeftIcon,
5+
UploadSimpleIcon,
6+
ArrowRightIcon,
7+
ShuffleIcon,
8+
ArrowCounterClockwiseIcon,
9+
CardsIcon,
10+
} from '@phosphor-icons/react';
11+
import { useToast } from '../../hooks/useToast';
12+
import Seo from '../../components/Seo';
13+
import GenerativeArt from '../../components/GenerativeArt';
14+
import LuxeArt from '../../components/LuxeArt';
15+
import BreadcrumbTitle from '../../components/BreadcrumbTitle';
16+
import { motion } from 'framer-motion';
17+
18+
function CsvFlashcardsPage() {
19+
const [cards, setCards] = useState([]);
20+
const [currentIndex, setCurrentIndex] = useState(0);
21+
const [isFlipped, setIsFlipped] = useState(false);
22+
const [fileName, setFileName] = useState('');
23+
const fileInputRef = useRef(null);
24+
const { addToast } = useToast();
25+
26+
const handleFileUpload = (event) => {
27+
const file = event.target.files[0];
28+
if (!file) return;
29+
30+
if (file.type !== 'text/csv' && !file.name.endsWith('.csv')) {
31+
addToast({
32+
title: 'Invalid File',
33+
message: 'Please upload a valid CSV file.',
34+
type: 'error',
35+
});
36+
return;
37+
}
38+
39+
setFileName(file.name);
40+
const reader = new FileReader();
41+
42+
reader.onload = (e) => {
43+
const text = e.target.result;
44+
parseCSV(text);
45+
};
46+
47+
reader.readAsText(file);
48+
};
49+
50+
const parseCSV = (text) => {
51+
try {
52+
// Basic CSV parser
53+
const lines = text.split(/\r?\n/).filter((line) => line.trim() !== '');
54+
if (lines.length < 1) {
55+
throw new Error('File is empty');
56+
}
57+
58+
const parsedCards = lines.map((line, index) => {
59+
// Handle basic comma separation, respecting quotes could be added here if needed
60+
// For now, simple split, assuming no commas in content or handle simple cases
61+
const parts = line.split(',');
62+
63+
// If we have more than 2 parts, join the rest for the answer or question?
64+
// Let's assume Col 1 = Question, Col 2 = Answer.
65+
if (parts.length < 2) return null;
66+
67+
const question = parts[0].trim();
68+
const answer = parts.slice(1).join(',').trim(); // Re-join rest in case answer had commas
69+
70+
return { id: index, question, answer };
71+
}).filter(card => card !== null);
72+
73+
if (parsedCards.length === 0) {
74+
throw new Error('No valid cards found');
75+
}
76+
77+
setCards(parsedCards);
78+
setCurrentIndex(0);
79+
setIsFlipped(false);
80+
addToast({
81+
title: 'Success',
82+
message: `Loaded ${parsedCards.length} cards.`,
83+
type: 'success',
84+
});
85+
} catch (error) {
86+
console.error(error);
87+
addToast({
88+
title: 'Error',
89+
message: 'Failed to parse CSV. Ensure format is: Question,Answer',
90+
type: 'error',
91+
});
92+
setCards([]);
93+
}
94+
};
95+
96+
const handleNext = () => {
97+
setIsFlipped(false);
98+
setTimeout(() => {
99+
setCurrentIndex((prev) => (prev + 1) % cards.length);
100+
}, 300);
101+
};
102+
103+
const handlePrev = () => {
104+
setIsFlipped(false);
105+
setTimeout(() => {
106+
setCurrentIndex((prev) => (prev - 1 + cards.length) % cards.length);
107+
}, 300);
108+
};
109+
110+
const handleShuffle = () => {
111+
const shuffled = [...cards].sort(() => Math.random() - 0.5);
112+
setCards(shuffled);
113+
setCurrentIndex(0);
114+
setIsFlipped(false);
115+
addToast({
116+
title: 'Shuffled',
117+
message: 'Deck shuffled.',
118+
duration: 2000,
119+
});
120+
};
121+
122+
const handleReset = () => {
123+
setCards([]);
124+
setFileName('');
125+
setCurrentIndex(0);
126+
setIsFlipped(false);
127+
if (fileInputRef.current) {
128+
fileInputRef.current.value = '';
129+
}
130+
};
131+
132+
const handleFlip = () => {
133+
setIsFlipped(!isFlipped);
134+
};
135+
136+
return (
137+
<div className="min-h-screen bg-[#050505] text-white selection:bg-indigo-500/30 font-sans">
138+
<Seo
139+
title="CSV Flashcards | Fezcodex"
140+
description="Study tool. Load custom flashcards via CSV."
141+
keywords={['Fezcodex', 'Flashcards', 'CSV', 'Study', 'Memory']}
142+
/>
143+
<div className="mx-auto max-w-5xl px-6 py-24 md:px-12">
144+
<header className="mb-12">
145+
<Link
146+
to="/apps"
147+
className="mb-8 inline-flex items-center gap-2 text-xs font-mono text-gray-500 hover:text-white transition-colors uppercase tracking-widest"
148+
>
149+
<ArrowLeftIcon weight="bold" />
150+
<span>Applications</span>
151+
</Link>
152+
153+
<div className="flex flex-col md:flex-row md:items-end justify-between gap-12">
154+
<div className="space-y-4">
155+
<BreadcrumbTitle title="Flashcard Forge" slug="csv-deck" variant="brutalist" />
156+
<p className="text-xl text-gray-400 max-w-2xl font-light leading-relaxed">
157+
Memory retention protocol. Upload structured CSV data (Question,Answer) to initiate a recall session.
158+
</p>
159+
</div>
160+
</div>
161+
</header>
162+
163+
{cards.length === 0 ? (
164+
<div className="border border-dashed border-white/20 bg-white/[0.02] rounded-sm p-12 text-center hover:bg-white/[0.04] transition-colors group cursor-pointer" onClick={() => fileInputRef.current.click()}>
165+
<input
166+
type="file"
167+
accept=".csv"
168+
className="hidden"
169+
ref={fileInputRef}
170+
onChange={handleFileUpload}
171+
/>
172+
<div className="mb-6 flex justify-center">
173+
<div className="p-4 bg-indigo-500/10 rounded-full group-hover:bg-indigo-500/20 transition-colors">
174+
<UploadSimpleIcon size={48} className="text-indigo-400" />
175+
</div>
176+
</div>
177+
<h3 className="text-lg font-bold mb-2">Upload CSV Deck</h3>
178+
<p className="text-gray-500 text-sm max-w-sm mx-auto">
179+
Drag and drop or click to select a file. <br/>
180+
Format: <span className="font-mono text-indigo-300">Question,Answer</span> (No header required)
181+
</p>
182+
</div>
183+
) : (
184+
<div className="space-y-8">
185+
{/* Controls Header */}
186+
<div className="flex flex-wrap items-center justify-between gap-4 p-4 border border-white/10 bg-white/[0.02]">
187+
<div className="flex items-center gap-4 text-sm font-mono text-gray-400">
188+
<CardsIcon size={20} />
189+
<span>{fileName}</span>
190+
<span className="w-px h-4 bg-white/10"/>
191+
<span className="text-white">Card {currentIndex + 1} <span className="text-gray-600">/</span> {cards.length}</span>
192+
</div>
193+
<div className="flex gap-2">
194+
<button
195+
onClick={handleShuffle}
196+
className="p-2 hover:bg-white/10 rounded-sm text-gray-400 hover:text-white transition-colors"
197+
title="Shuffle Deck"
198+
>
199+
<ShuffleIcon size={20} />
200+
</button> <button
201+
onClick={handleReset}
202+
className="p-2 hover:bg-red-500/10 rounded-sm text-gray-400 hover:text-red-400 transition-colors"
203+
title="Unload Deck"
204+
>
205+
<ArrowCounterClockwiseIcon size={20} />
206+
</button>
207+
</div>
208+
</div>
209+
210+
{/* Card Area */}
211+
<div className="w-full aspect-[3/2] md:aspect-[2/1] relative cursor-pointer group" onClick={handleFlip} style={{ perspective: '1000px' }}>
212+
<motion.div
213+
className="w-full h-full relative preserve-3d transition-transform duration-500"
214+
animate={{ rotateY: isFlipped ? 180 : 0 }}
215+
transition={{ type: "spring", stiffness: 260, damping: 20 }}
216+
style={{ transformStyle: 'preserve-3d' }}
217+
>
218+
{/* Front */}
219+
<div
220+
className="absolute inset-0 bg-black border border-white/10 flex flex-col items-center justify-center p-8 md:p-16 text-center select-none shadow-2xl overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-white/20"
221+
style={{ backfaceVisibility: 'hidden', transform: 'rotateY(0deg) translateZ(1px)' }}
222+
> <div className="absolute inset-0 opacity-10 pointer-events-none">
223+
<GenerativeArt seed={`card-${currentIndex}-front`} className="w-full h-full" />
224+
</div>
225+
<div className="absolute top-4 left-4 text-[10px] font-mono uppercase tracking-widest text-indigo-400">Query</div>
226+
<h2 className="text-2xl md:text-4xl font-light leading-tight relative z-10">{cards[currentIndex]?.question}</h2>
227+
<div className="absolute bottom-6 text-xs text-gray-600 font-mono animate-pulse">Click to Reveal</div>
228+
</div>
229+
230+
{/* Back */}
231+
232+
<div
233+
234+
className="absolute inset-0 bg-white text-black flex flex-col items-center justify-center p-8 md:p-16 text-center select-none shadow-2xl overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-black/20"
235+
236+
style={{ transform: 'rotateY(180deg) translateZ(1px)', backfaceVisibility: 'hidden', backgroundColor: 'white' }}
237+
238+
>
239+
240+
<div className="absolute inset-0 opacity-60 pointer-events-none">
241+
242+
<LuxeArt seed={`card-${currentIndex}-back`} colorful={true} className="w-full h-full mix-blend-multiply" />
243+
244+
</div>
245+
246+
<div className="absolute top-4 left-4 text-[10px] font-mono uppercase tracking-widest text-indigo-600 font-bold">Response</div>
247+
248+
<h2 className="text-2xl md:text-4xl font-bold leading-tight relative z-10">{cards[currentIndex]?.answer}</h2>
249+
250+
</div>
251+
</motion.div>
252+
</div>
253+
254+
{/* Navigation */}
255+
<div className="grid grid-cols-2 gap-4">
256+
<button
257+
onClick={handlePrev}
258+
className="flex items-center justify-center gap-3 p-4 border border-white/10 hover:bg-white/5 transition-all active:scale-[0.98] group"
259+
>
260+
<ArrowLeftIcon className="group-hover:-translate-x-1 transition-transform" />
261+
<span className="font-mono uppercase tracking-widest text-sm">Previous</span>
262+
</button>
263+
<button
264+
onClick={handleNext}
265+
className="flex items-center justify-center gap-3 p-4 bg-white text-black hover:bg-indigo-400 transition-all active:scale-[0.98] group"
266+
>
267+
<span className="font-mono uppercase tracking-widest text-sm font-bold">Next</span>
268+
<ArrowRightIcon className="group-hover:translate-x-1 transition-transform" />
269+
</button>
270+
</div>
271+
272+
</div>
273+
)}
274+
</div>
275+
</div>
276+
);
277+
}
278+
279+
export default CsvFlashcardsPage;

0 commit comments

Comments
 (0)