From e711c527574412de2f9d451c7985c4f8667d269a Mon Sep 17 00:00:00 2001
From: Thomas Hallock
Date: Mon, 20 Oct 2025 17:37:40 -0500
Subject: [PATCH] feat(homepage): add interactive draggable flashcards with
physics
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add a fun, interactive flashcard display to the homepage's flashcard
generator section. Users can drag and throw 8-15 randomly generated
flashcards around with realistic physics-based momentum.
Features:
- Drag and drop flashcards with mouse/touch
- Throw cards with velocity-based physics
- 8-15 randomly generated flashcards (100-999 range)
- Real AbacusReact components for each card
- Client-side rendering to avoid hydration errors
Technical implementation:
- Uses @use-gesture/react for drag gesture handling
- Uses @react-spring/web for smooth physics animations
- Cards generated client-side with useEffect to prevent SSR mismatch
- Each card maintains its own spring-based position and rotation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude
---
apps/web/package.json | 1 +
apps/web/src/app/page.tsx | 83 ++------
.../src/components/InteractiveFlashcards.tsx | 191 ++++++++++++++++++
pnpm-lock.yaml | 3 +
4 files changed, 208 insertions(+), 70 deletions(-)
create mode 100644 apps/web/src/components/InteractiveFlashcards.tsx
diff --git a/apps/web/package.json b/apps/web/package.json
index 15b225e5..a2b2347c 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -53,6 +53,7 @@
"@tanstack/react-form": "^0.19.0",
"@tanstack/react-query": "^5.90.2",
"@types/jsdom": "^21.1.7",
+ "@use-gesture/react": "^10.3.1",
"bcryptjs": "^2.4.3",
"better-sqlite3": "^12.4.1",
"drizzle-orm": "^0.44.6",
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index d28c707e..6d35c07b 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -9,6 +9,7 @@ import { PageWithNav } from '@/components/PageWithNav'
import { TutorialPlayer } from '@/components/tutorial/TutorialPlayer'
import { getTutorialForEditor } from '@/utils/tutorialConverter'
import { getAvailableGames } from '@/lib/arcade/game-registry'
+import { InteractiveFlashcards } from '@/components/InteractiveFlashcards'
import { css } from '../../styled-system/css'
import { container, grid, hstack, stack } from '../../styled-system/patterns'
import { token } from '../../styled-system/tokens'
@@ -566,6 +567,18 @@ export default function HomePage() {
+ {/* Interactive Flashcards Display */}
+
+
+
+
+ {/* Features and CTA */}
- {/* Flashcards Display */}
-
- {/* Spread out flashcards */}
- {[
- { number: 123, rotation: -8, zIndex: 1, offset: '-60px' },
- { number: 456, rotation: -3, zIndex: 2, offset: '-30px' },
- { number: 789, rotation: 2, zIndex: 3, offset: '0px' },
- { number: 321, rotation: 5, zIndex: 2, offset: '30px' },
- { number: 654, rotation: 9, zIndex: 1, offset: '60px' },
- ].map((card, i) => (
-
- ))}
-
-
{/* Features */}
{[
diff --git a/apps/web/src/components/InteractiveFlashcards.tsx b/apps/web/src/components/InteractiveFlashcards.tsx
new file mode 100644
index 00000000..7b51d9a0
--- /dev/null
+++ b/apps/web/src/components/InteractiveFlashcards.tsx
@@ -0,0 +1,191 @@
+'use client'
+
+import { AbacusReact } from '@soroban/abacus-react'
+import { useDrag } from '@use-gesture/react'
+import { useEffect, useState } from 'react'
+import { animated, config, useSpring } from '@react-spring/web'
+import { css } from '../../styled-system/css'
+
+interface Flashcard {
+ id: number
+ number: number
+ initialX: number
+ initialY: number
+ initialRotation: number
+ zIndex: number
+}
+
+const CONTAINER_WIDTH = 800
+const CONTAINER_HEIGHT = 500
+
+/**
+ * InteractiveFlashcards - A fun, physics-based flashcard display
+ * Users can drag and throw flashcards around with realistic momentum
+ */
+export function InteractiveFlashcards() {
+ // Generate 8-15 random flashcards (client-side only to avoid hydration errors)
+ const [cards, setCards] = useState
([])
+
+ useEffect(() => {
+ const count = Math.floor(Math.random() * 8) + 8 // 8-15 cards
+ const generated: Flashcard[] = []
+
+ for (let i = 0; i < count; i++) {
+ generated.push({
+ id: i,
+ number: Math.floor(Math.random() * 900) + 100, // 100-999
+ initialX: Math.random() * (CONTAINER_WIDTH - 200) + 100,
+ initialY: Math.random() * (CONTAINER_HEIGHT - 200) + 100,
+ initialRotation: Math.random() * 40 - 20, // -20 to 20 degrees
+ zIndex: i,
+ })
+ }
+
+ setCards(generated)
+ }, [])
+
+ return (
+
+ {/* Hint text */}
+
+ Drag and throw the flashcards!
+
+
+ {cards.map((card) => (
+
+ ))}
+
+ )
+}
+
+interface DraggableCardProps {
+ card: Flashcard
+}
+
+function DraggableCard({ card }: DraggableCardProps) {
+ const [{ x, y, rotation, scale }, api] = useSpring(() => ({
+ x: card.initialX,
+ y: card.initialY,
+ rotation: card.initialRotation,
+ scale: 1,
+ config: config.wobbly,
+ }))
+
+ const [zIndex, setZIndex] = useState(card.zIndex)
+
+ const bind = useDrag(
+ ({ down, movement: [mx, my], velocity: [vx, vy], direction: [dx, dy] }) => {
+ // Bring card to front when dragging
+ if (down) {
+ setZIndex(1000)
+ }
+
+ api.start({
+ x: down ? card.initialX + mx : card.initialX + mx + vx * 100 * dx,
+ y: down ? card.initialY + my : card.initialY + my + vy * 100 * dy,
+ scale: down ? 1.1 : 1,
+ rotation: down ? card.initialRotation + mx / 20 : card.initialRotation + vx * 10,
+ immediate: down,
+ config: down ? config.stiff : config.wobbly,
+ })
+
+ // Update initial position after release for next drag
+ if (!down) {
+ card.initialX = card.initialX + mx + vx * 100 * dx
+ card.initialY = card.initialY + my + vy * 100 * dy
+ card.initialRotation = card.initialRotation + vx * 10
+ }
+ },
+ {
+ // Prevent scrolling when dragging
+ preventDefault: true,
+ filterTaps: true,
+ }
+ )
+
+ return (
+
+
+ {/* Abacus visualization */}
+
+
+ {/* Number display */}
+
+ {card.number}
+
+
+
+ )
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d5e660ec..72317c75 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -134,6 +134,9 @@ importers:
'@types/jsdom':
specifier: ^21.1.7
version: 21.1.7
+ '@use-gesture/react':
+ specifier: ^10.3.1
+ version: 10.3.1(react@18.3.1)
bcryptjs:
specifier: ^2.4.3
version: 2.4.3