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) => ( -
-
-
- -
-
- {card.number} -
-
-
- ))} -
- {/* 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