SOURCE.IO / src /components /FlashcardsDeck.tsx
Adeen
Deploy: OCR support for PDF/Images/DOCX
ae14296
import { useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { ChevronLeft, ChevronRight, Shuffle, RotateCcw } from "lucide-react";
import type { FlashcardRow } from "@/store/workspace";
function shuffleArr<T>(arr: T[]): T[] {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
export default function FlashcardsDeck({ cards }: { cards: FlashcardRow[] }) {
const [order, setOrder] = useState<number[]>(() => cards.map((_, i) => i));
const [idx, setIdx] = useState(0);
const [flipped, setFlipped] = useState(false);
const ordered = useMemo(() => order.map((i) => cards[i]).filter(Boolean), [order, cards]);
const total = ordered.length;
const card = ordered[idx];
const go = (delta: number) => {
setFlipped(false);
setIdx((i) => Math.max(0, Math.min(total - 1, i + delta)));
};
if (!card) return null;
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<Badge variant="outline" className="text-xs">
{idx + 1} / {total}
</Badge>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => { setOrder(shuffleArr(order)); setIdx(0); setFlipped(false); }}
>
<Shuffle className="h-3.5 w-3.5 mr-1.5" /> Shuffle
</Button>
<Button
variant="outline"
size="sm"
onClick={() => { setOrder(cards.map((_, i) => i)); setIdx(0); setFlipped(false); }}
>
<RotateCcw className="h-3.5 w-3.5 mr-1.5" /> Reset
</Button>
</div>
</div>
{/* Flip card */}
<div
className="relative w-full h-72 sm:h-80 cursor-pointer select-none"
style={{ perspective: "1200px" }}
onClick={() => setFlipped((f) => !f)}
role="button"
aria-label="Flip card"
>
<div
className="absolute inset-0 transition-transform duration-500"
style={{
transformStyle: "preserve-3d",
transform: flipped ? "rotateY(180deg)" : "rotateY(0deg)",
}}
>
<div
className="absolute inset-0 rounded-xl border border-border bg-card p-6 sm:p-8 flex flex-col items-center justify-center text-center shadow-sm"
style={{ backfaceVisibility: "hidden" }}
>
<Badge variant="secondary" className="mb-4 text-[10px] uppercase tracking-wide">Question</Badge>
<p className="text-lg sm:text-xl font-medium leading-relaxed">{card.front}</p>
<p className="absolute bottom-3 text-xs text-muted-foreground">Tap to reveal</p>
</div>
<div
className="absolute inset-0 rounded-xl border border-primary/30 bg-card p-6 sm:p-8 flex flex-col items-center justify-center text-center shadow-sm"
style={{ backfaceVisibility: "hidden", transform: "rotateY(180deg)" }}
>
<Badge className="mb-4 text-[10px] uppercase tracking-wide bg-primary/20 text-primary border-primary/30">Answer</Badge>
<p className="text-base sm:text-lg leading-relaxed">{card.back}</p>
</div>
</div>
</div>
<div className="flex items-center justify-between">
<Button variant="outline" onClick={() => go(-1)} disabled={idx === 0}>
<ChevronLeft className="h-4 w-4 mr-1" /> Prev
</Button>
<div className="flex-1 mx-4">
<div className="h-1 rounded-full bg-muted overflow-hidden">
<div
className="h-full bg-primary transition-all"
style={{ width: `${((idx + 1) / total) * 100}%` }}
/>
</div>
</div>
<Button variant="outline" onClick={() => go(1)} disabled={idx === total - 1}>
Next <ChevronRight className="h-4 w-4 ml-1" />
</Button>
</div>
</div>
);
}