From e8c6c59d95f263236495a59cd8cc155d80cdf0e5 Mon Sep 17 00:00:00 2001 From: clacina Date: Fri, 1 May 2026 06:37:48 -0700 Subject: [PATCH 1/3] Speed control working --- _specs/mobile-responsive-layouts.md | 71 +++++++++++++++++++++++++++++ src/components/FlashcardSession.jsx | 62 +++++++++++++++++++++++-- 2 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 _specs/mobile-responsive-layouts.md diff --git a/_specs/mobile-responsive-layouts.md b/_specs/mobile-responsive-layouts.md new file mode 100644 index 0000000..2b9ad84 --- /dev/null +++ b/_specs/mobile-responsive-layouts.md @@ -0,0 +1,71 @@ +# Spec for mobile-responsive-layouts + +branch: claude/feature/mobile-responsive-layouts +figma_component (if used): N/A + +## Summary + +Restructure the flashcard session UI so that the video player is the primary focal element across all major mobile form factors and orientations. Each combination of device class and orientation gets its own dedicated React component with its own scoped CSS, making per-view tweaks straightforward for a developer without touching shared styles. + +## Functional Requirements + +- Detect the current viewport configuration at runtime and render the appropriate layout component: + - Phone Portrait + - Phone Landscape + - Tablet Portrait + - Tablet Landscape +- The video player must be the visually dominant element in every layout — it should occupy the largest share of available screen space without being clipped or obscured. +- Each layout component must be a standalone React component (e.g. `PhonePortraitLayout`, `PhoneLandscapeLayout`, `TabletPortraitLayout`, `TabletLandscapeLayout`). +- Each layout component owns its own CSS block (or section in `App.css`) clearly delimited with a comment header so it is easy to find and edit independently. +- All CSS values that a developer is likely to want to tweak (video size ratios, padding, font sizes, button sizes) must use named CSS custom properties defined at the top of each layout's CSS block. +- Navigation controls (previous, next, shuffle, etc.) and the term label must remain accessible and usable in every layout, but they may be repositioned or resized to keep the video dominant. +- The disclaimer / info footer must remain reachable in all layouts (can be collapsed, scrollable, or in a modal — whichever fits the layout best). +- Layout selection logic must be encapsulated in a single place (a custom hook or a small utility) so the detection rules can be updated without touching the layout components themselves. +- Breakpoint definitions (device-class thresholds and orientation detection) must be defined as named constants, not magic numbers. +- Desktop/large-screen behavior must not regress — the existing layout should remain the default for non-mobile viewports. + +## Possible Edge Cases + +- Device orientation changes mid-session (user rotates phone) — the layout must switch without losing session state (current card index, score, etc.). +- Tablets that report phone-sized viewport widths in certain browser modes. +- Browsers that do not support the `Screen Orientation API` — fall back to `window.innerWidth / window.innerHeight` comparison. +- Very small phones (320 px wide) where landscape video + controls compete for limited vertical space. +- Foldable devices that change form factor dynamically. +- Keyboard/split-screen multitasking on tablets that narrows the effective viewport. +- Videos with non-standard aspect ratios that would overflow or leave large empty areas. + +## Acceptance Criteria + +- [ ] Rotating a phone between portrait and landscape re-renders the correct layout component without navigating away or resetting the flashcard session. +- [ ] On a phone in portrait mode the video player occupies at least 55% of the visible viewport height. +- [ ] On a phone in landscape mode the video player occupies at least 65% of the visible viewport width. +- [ ] On a tablet in portrait mode the video player occupies at least 50% of the visible viewport height. +- [ ] On a tablet in landscape mode the video player occupies at least 55% of the visible viewport width. +- [ ] Each layout's CSS custom properties (tweakable values) are documented with a short inline comment describing what they control. +- [ ] The layout detection logic is covered by unit tests covering phone portrait, phone landscape, tablet portrait, tablet landscape, and desktop classifications. +- [ ] No existing desktop functionality regresses (verified by manual walkthrough and ESLint passing). +- [ ] `yarn lint` passes with no new errors. + +## Open Questions + +- Should the term label (the sign word being practiced) overlay the video or sit outside it? This affects the layout significantly in landscape mode. + - Overlay on phone, outside on tablets +- Should navigation buttons (prev/next) be gesture-swipe driven on mobile, or remain as tap targets? + - gesture-swipe +- What are the exact pixel breakpoints that define "phone" vs "tablet"? (Common choice: < 768 px = phone, ≥ 768 px = tablet.) + - use common +- Should each layout component render the full session UI (video + controls + header) or be a layout shell that accepts children for the controls? + - layout shell that accepts children for the controls +- Is there a design mockup / Figma reference that should guide the proportions, or should the developer define them? + - allow the developer to define it. + +## Testing Guidelines + +Create a test file(s) in the `./tests` folder for the new feature, and create meaningful tests for the following cases, without going too heavy: + +- Layout detection hook returns `phone-portrait` for a narrow, taller-than-wide viewport. +- Layout detection hook returns `phone-landscape` for a narrow, wider-than-tall viewport. +- Layout detection hook returns `tablet-portrait` for a wide, taller-than-wide viewport above the phone threshold. +- Layout detection hook returns `tablet-landscape` for a wide, wider-than-tall viewport above the phone threshold. +- Layout detection hook returns `desktop` for a viewport above the tablet threshold. +- Orientation change event triggers a re-evaluation of the layout classification. diff --git a/src/components/FlashcardSession.jsx b/src/components/FlashcardSession.jsx index 32ba3cf..e8769de 100644 --- a/src/components/FlashcardSession.jsx +++ b/src/components/FlashcardSession.jsx @@ -12,6 +12,9 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} const [termDrawerOpen, setTermDrawerOpen] = useState(false); const [isMobileHorizontal, setIsMobileHorizontal] = useState(false); const [autoPlay, setAutoPlay] = useState(false); + const [showPlayerControls, setShowPlayerControls] = useState(true); + const [playing, setPlaying] = useState(false); + const [playbackRate, setPlaybackRate] = useState(1); const selectRef = useRef(null); useEffect(() => { @@ -30,14 +33,17 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} }, []); const goNext = useCallback(() => { + setPlaying(false); setCurrentIndex(i => (i + 1) % localTerms.length); }, [localTerms.length]); const goPrev = useCallback(() => { + setPlaying(false); setCurrentIndex(i => (i - 1 + localTerms.length) % localTerms.length); }, [localTerms.length]); function handleShuffle() { + setPlaying(false); const indices = shuffle([...localTerms.keys()]); setLocalTerms(indices.map(i => localTerms[i])); setLocalColors(indices.map(i => localColors[i])); @@ -109,7 +115,25 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} className={`btn-nav${autoPlay ? ' btn-nav--active' : ''}`} onClick={() => setAutoPlay(p => !p)} >{autoPlay ? '⏸ Auto' : '▶ Auto'} - + + + + + + + + + +
@@ -119,12 +143,17 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} className="flashcard-video-iframe" title="ASL sign video" src={playbackUrl} + playing={playing} autoPlay={autoPlay} - controls={true} + controls={showPlayerControls} playsinline={true} muted={true} // width="100%" // height="100%" + playbackRate={playbackRate} + onPlay={() => setPlaying(true)} + onPause={() => setPlaying(false)} + onEnded={() => setPlaying(false)} onError={playbackError} config={{ file: { @@ -146,7 +175,7 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} ref={selectRef} size={20} className="term-select" - onChange={e => setCurrentIndex(Number(e.target.value))} + onChange={e => { setPlaying(false); setCurrentIndex(Number(e.target.value)); }} value={currentIndex} > {sortedTerms.map(({term, i, fix}) => ( @@ -179,12 +208,17 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} className="flashcard-video-iframe" title="ASL sign video" src={playbackUrl} + playing={playing} + playbackRate={playbackRate} playsinline={true} autoPlay={autoPlay} - controls={true} + controls={showPlayerControls} muted={true} width="100%" height="100%" + onPlay={() => setPlaying(true)} + onPause={() => setPlaying(false)} + onEnded={() => setPlaying(false)} onError={playbackError} config={{ file: { @@ -212,6 +246,24 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} onClick={() => setAutoPlay(p => !p)} >{autoPlay ? '🔁 Auto' : '⏸ Wait'} + + + + + + + + +
@@ -225,7 +277,7 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} ref={selectRef} size={20} className="term-select" - onChange={e => { setCurrentIndex(Number(e.target.value)); setTermDrawerOpen(false); }} + onChange={e => { setPlaying(false); setCurrentIndex(Number(e.target.value)); setTermDrawerOpen(false); }} value={currentIndex} > {sortedTerms.map(({term, i, fix}) => ( From 20a8d86d100d4b5d8607414088a14edc8c0ab125 Mon Sep 17 00:00:00 2001 From: clacina Date: Fri, 1 May 2026 07:34:37 -0700 Subject: [PATCH 2/3] Split nav into reusable component --- src/components/FlashcardNav.jsx | 62 ++++++++++ src/components/FlashcardSession.jsx | 149 +++++++++++----------- tests/FlashcardNav.test.jsx | 185 ++++++++++++++++++++++++++++ 3 files changed, 326 insertions(+), 70 deletions(-) create mode 100644 src/components/FlashcardNav.jsx create mode 100644 tests/FlashcardNav.test.jsx diff --git a/src/components/FlashcardNav.jsx b/src/components/FlashcardNav.jsx new file mode 100644 index 0000000..3e513cf --- /dev/null +++ b/src/components/FlashcardNav.jsx @@ -0,0 +1,62 @@ +import Tippy from "@tippyjs/react"; + +export function FlashcardNav({ + className, + onPrev, + onNext, + onShuffle, + onOpenTerms, + autoPlay, + onToggleAutoPlay, + autoPlayActiveLabel, + autoPlayInactiveLabel, + showPlayerControls, + onTogglePlayerControls, + playing, + onTogglePlaying, + playbackRate, + onTogglePlaybackRate, + repeat, + onToggleRepeat, +}) { + return ( +
+ + + + {onOpenTerms && ( + + )} + + + + + + + + + + + + + + + +
+ ); +} \ No newline at end of file diff --git a/src/components/FlashcardSession.jsx b/src/components/FlashcardSession.jsx index e8769de..d4fd2df 100644 --- a/src/components/FlashcardSession.jsx +++ b/src/components/FlashcardSession.jsx @@ -3,7 +3,7 @@ import {contrastColor} from "../utils/contrastColor"; import {shuffle} from "../utils/shuffle"; import ReactPlayer from 'react-player' import toast from "react-hot-toast"; -import Tippy from "@tippyjs/react"; +import {FlashcardNav} from "./FlashcardNav"; export function FlashcardSession({terms, cardColors, onBack, title, description}) { const [currentIndex, setCurrentIndex] = useState(0); @@ -15,6 +15,7 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} const [showPlayerControls, setShowPlayerControls] = useState(true); const [playing, setPlaying] = useState(false); const [playbackRate, setPlaybackRate] = useState(1); + const [repeat, setRepeat] = useState(false); const selectRef = useRef(null); useEffect(() => { @@ -92,6 +93,35 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} toast.error("Playback error"); } + const PLAYBACK_STATE_START = 1 + const PLAYBACK_STATE_END = 2 + const PLAYBACK_STATE_PAUSE = 3 + + function playingStateChanged(stateChange) { + switch(stateChange) { + case PLAYBACK_STATE_START: + console.log("Playback start"); + setPlaying(true); + break; + case PLAYBACK_STATE_END: + console.log("Playback end: ", repeat); + setPlaying(false); + break; + case PLAYBACK_STATE_PAUSE: + console.log("Playback paused"); + setPlaying(false); + break; + default: + console.error("Unknown Playback state change: ", stateChange); + } + } + + function onSelectTerm(e) { + setPlaying(false); + setCurrentIndex(Number(e.target.value)); + setTermDrawerOpen(false); + } + const bg = localColors[currentIndex]; const fg = contrastColor(bg); const playbackUrl = getPlaybackUrl(); @@ -106,35 +136,24 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} {localTerms[currentIndex].term}

{currentIndex + 1} / {localTerms.length}

-
- - - - - - - - - - - - - - - -
+ setAutoPlay(p => !p)} + autoPlayActiveLabel="⏸ Auto" + autoPlayInactiveLabel="▶ Auto" + showPlayerControls={showPlayerControls} + onTogglePlayerControls={() => setShowPlayerControls(p => !p)} + playing={playing} + onTogglePlaying={() => setPlaying(p => !p)} + playbackRate={playbackRate} + onTogglePlaybackRate={() => setPlaybackRate(r => r === 1 ? 0.5 : 1)} + repeat={repeat} + onToggleRepeat={() => setRepeat(r => !r)} + />
@@ -144,16 +163,17 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} title="ASL sign video" src={playbackUrl} playing={playing} + loop={repeat} autoPlay={autoPlay} controls={showPlayerControls} playsinline={true} muted={true} - // width="100%" - // height="100%" + width="100%" + height="100%" playbackRate={playbackRate} - onPlay={() => setPlaying(true)} - onPause={() => setPlaying(false)} - onEnded={() => setPlaying(false)} + onPlay={() => playingStateChanged(PLAYBACK_STATE_START)} + onPause={() => playingStateChanged(PLAYBACK_STATE_PAUSE)} + onEnded={() => playingStateChanged(PLAYBACK_STATE_END)} onError={playbackError} config={{ file: { @@ -175,7 +195,7 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} ref={selectRef} size={20} className="term-select" - onChange={e => { setPlaying(false); setCurrentIndex(Number(e.target.value)); }} + onChange={onSelectTerm} value={currentIndex} > {sortedTerms.map(({term, i, fix}) => ( @@ -216,9 +236,9 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} muted={true} width="100%" height="100%" - onPlay={() => setPlaying(true)} - onPause={() => setPlaying(false)} - onEnded={() => setPlaying(false)} + onPlay={() => playingStateChanged(PLAYBACK_STATE_START)} + onPause={() => playingStateChanged(PLAYBACK_STATE_PAUSE)} + onEnded={() => playingStateChanged(PLAYBACK_STATE_END)} onError={playbackError} config={{ file: { @@ -235,36 +255,25 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} )}

{currentIndex + 1} / {localTerms.length}

-
- - - - - - - - - - - - - - - - -
+ setTermDrawerOpen(true)} + autoPlay={autoPlay} + onToggleAutoPlay={() => setAutoPlay(p => !p)} + autoPlayActiveLabel="🔁 Auto" + autoPlayInactiveLabel="⏸ Wait" + showPlayerControls={showPlayerControls} + onTogglePlayerControls={() => setShowPlayerControls(p => !p)} + playing={playing} + onTogglePlaying={() => setPlaying(p => !p)} + playbackRate={playbackRate} + loop={repeat} + onTogglePlaybackRate={() => setPlaybackRate(r => r === 1 ? 0.5 : 1)} + repeat={repeat} + onToggleRepeat={() => setRepeat(r => !r)} + />
setTermDrawerOpen(false)} /> @@ -277,7 +286,7 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} ref={selectRef} size={20} className="term-select" - onChange={e => { setPlaying(false); setCurrentIndex(Number(e.target.value)); setTermDrawerOpen(false); }} + onChange={onSelectTerm} value={currentIndex} > {sortedTerms.map(({term, i, fix}) => ( diff --git a/tests/FlashcardNav.test.jsx b/tests/FlashcardNav.test.jsx new file mode 100644 index 0000000..e0f3b3c --- /dev/null +++ b/tests/FlashcardNav.test.jsx @@ -0,0 +1,185 @@ +import {describe, it, expect, vi} from 'vitest'; +import {render, screen, fireEvent} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import {FlashcardNav} from '../src/components/FlashcardNav'; + +function baseProps(overrides = {}) { + return { + onPrev: vi.fn(), + onNext: vi.fn(), + onShuffle: vi.fn(), + autoPlay: false, + onToggleAutoPlay: vi.fn(), + autoPlayActiveLabel: '🔁 Auto', + autoPlayInactiveLabel: '⏸ Wait', + showPlayerControls: true, + onTogglePlayerControls: vi.fn(), + playing: false, + onTogglePlaying: vi.fn(), + playbackRate: 1, + onTogglePlaybackRate: vi.fn(), + repeat: false, + onToggleRepeat: vi.fn(), + ...overrides, + }; +} + +describe('FlashcardNav', () => { + it('renders Prev, Next and Shuffle buttons', () => { + render(); + expect(screen.getByText('← Prev')).toBeInTheDocument(); + expect(screen.getByText('Next →')).toBeInTheDocument(); + expect(screen.getByText('⇄ Shuffle')).toBeInTheDocument(); + }); + + it('calls onPrev when Prev is clicked', () => { + const onPrev = vi.fn(); + render(); + fireEvent.click(screen.getByText('← Prev')); + expect(onPrev).toHaveBeenCalledOnce(); + }); + + it('calls onNext when Next is clicked', () => { + const onNext = vi.fn(); + render(); + fireEvent.click(screen.getByText('Next →')); + expect(onNext).toHaveBeenCalledOnce(); + }); + + it('calls onShuffle when Shuffle is clicked', () => { + const onShuffle = vi.fn(); + render(); + fireEvent.click(screen.getByText('⇄ Shuffle')); + expect(onShuffle).toHaveBeenCalledOnce(); + }); + + it('does not render Terms button when onOpenTerms is not provided', () => { + render(); + expect(screen.queryByText('📋 Terms')).not.toBeInTheDocument(); + }); + + it('renders Terms button when onOpenTerms is provided', () => { + render(); + expect(screen.getByText('📋 Terms')).toBeInTheDocument(); + }); + + it('calls onOpenTerms when Terms is clicked', () => { + const onOpenTerms = vi.fn(); + render(); + fireEvent.click(screen.getByText('📋 Terms')); + expect(onOpenTerms).toHaveBeenCalledOnce(); + }); + + it('shows inactive auto-play label when autoPlay is false', () => { + render(); + expect(screen.getByText('⏸ Wait')).toBeInTheDocument(); + }); + + it('shows active auto-play label when autoPlay is true', () => { + render(); + expect(screen.getByText('🔁 Auto')).toBeInTheDocument(); + }); + + it('auto-play button has active class when autoPlay is true', () => { + render(); + expect(screen.getByText('🔁 Auto')).toHaveClass('btn-nav--active'); + }); + + it('calls onToggleAutoPlay when auto-play button is clicked', () => { + const onToggleAutoPlay = vi.fn(); + render(); + fireEvent.click(screen.getByText('⏸ Wait')); + expect(onToggleAutoPlay).toHaveBeenCalledOnce(); + }); + + it('controls button has active class when showPlayerControls is true', () => { + render(); + expect(screen.getByText('🎛️ Controls')).toHaveClass('btn-nav--active'); + }); + + it('controls button lacks active class when showPlayerControls is false', () => { + render(); + expect(screen.getByText('🎛️ Controls')).not.toHaveClass('btn-nav--active'); + }); + + it('calls onTogglePlayerControls when controls button is clicked', () => { + const onTogglePlayerControls = vi.fn(); + render(); + fireEvent.click(screen.getByText('🎛️ Controls')); + expect(onTogglePlayerControls).toHaveBeenCalledOnce(); + }); + + it('shows ▶ play icon when not playing', () => { + render(); + expect(screen.getByText('▶')).toBeInTheDocument(); + }); + + it('shows ⏸ pause icon when playing', () => { + render(); + expect(screen.getByText('⏸')).toBeInTheDocument(); + }); + + it('play button has active class when playing', () => { + render(); + expect(screen.getByText('⏸')).toHaveClass('btn-nav--active'); + }); + + it('calls onTogglePlaying when play/pause button is clicked', () => { + const onTogglePlaying = vi.fn(); + render(); + fireEvent.click(screen.getByText('▶')); + expect(onTogglePlaying).toHaveBeenCalledOnce(); + }); + + it('shows 1× speed label at normal rate', () => { + render(); + expect(screen.getByText(/🐢\s*1×/)).toBeInTheDocument(); + }); + + it('shows ½× speed label at half rate and applies active class', () => { + render(); + const btn = screen.getByText(/🐢\s*½×/); + expect(btn).toBeInTheDocument(); + expect(btn).toHaveClass('btn-nav--active'); + }); + + it('calls onTogglePlaybackRate when speed button is clicked', () => { + const onTogglePlaybackRate = vi.fn(); + render(); + fireEvent.click(screen.getByText(/🐢\s*1×/)); + expect(onTogglePlaybackRate).toHaveBeenCalledOnce(); + }); + + it('applies extra className to the nav container', () => { + const {container} = render(); + expect(container.firstChild).toHaveClass('flashcard-nav', 'fcs-landscape__nav'); + }); + + it('container has only flashcard-nav class when no extra className is given', () => { + const {container} = render(); + expect(container.firstChild).toHaveClass('flashcard-nav'); + expect(container.firstChild.className).toBe('flashcard-nav'); + }); + + it('renders Loop button', () => { + render(); + expect(screen.getByText('🔁 Loop')).toBeInTheDocument(); + }); + + it('Loop button lacks active class when repeat is false', () => { + render(); + expect(screen.getByText('🔁 Loop')).not.toHaveClass('btn-nav--active'); + }); + + it('Loop button has active class when repeat is true', () => { + render(); + expect(screen.getByText('🔁 Loop')).toHaveClass('btn-nav--active'); + }); + + it('calls onToggleRepeat when Loop button is clicked', () => { + const onToggleRepeat = vi.fn(); + render(); + fireEvent.click(screen.getByText('🔁 Loop')); + expect(onToggleRepeat).toHaveBeenCalledOnce(); + }); +}); \ No newline at end of file From 13b8277083f0c2bd558b4485c7dd14114c5a881b Mon Sep 17 00:00:00 2001 From: clacina Date: Fri, 1 May 2026 08:27:25 -0700 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=94=A8=20refactor:=20extract=20Flashc?= =?UTF-8?q?ardPlayer=20component=20and=20fix=20test=20suite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pulls the repeated video player markup into a dedicated FlashcardPlayer component so the landscape and portrait layouts share one implementation. Fixes the missing loop prop in the portrait layout. Stubs window.matchMedia in FlashcardSession tests so the orientation-detection effect no longer crashes jsdom. Cleans up YouTube embed URLs by removing the non-functional ;autoplay=1 query param. Co-Authored-By: Claude Sonnet 4.6 --- src/components/FlashcardPlayer.jsx | 50 ++++++++++ src/components/FlashcardSession.jsx | 91 +++++------------- src/data/faire.json | 16 ++-- src/data/questions.json | 20 ++-- src/data/terms.json | 2 +- src/data/verbs.json | 8 +- tests/FlashcardPlayer.test.jsx | 138 ++++++++++++++++++++++++++++ tests/FlashcardSession.test.jsx | 18 +++- 8 files changed, 253 insertions(+), 90 deletions(-) create mode 100644 src/components/FlashcardPlayer.jsx create mode 100644 tests/FlashcardPlayer.test.jsx diff --git a/src/components/FlashcardPlayer.jsx b/src/components/FlashcardPlayer.jsx new file mode 100644 index 0000000..1e42da7 --- /dev/null +++ b/src/components/FlashcardPlayer.jsx @@ -0,0 +1,50 @@ +import ReactPlayer from 'react-player'; + +export function FlashcardPlayer({ + url, + playing, + loop, + autoPlay, + controls, + playbackRate, + onPlay, + onPause, + onEnded, + onError, +}) { + return ( +
+ {url ? ( + + ) : ( +
+ No video available +
+ )} +
+ ); +} diff --git a/src/components/FlashcardSession.jsx b/src/components/FlashcardSession.jsx index d4fd2df..2ee99b9 100644 --- a/src/components/FlashcardSession.jsx +++ b/src/components/FlashcardSession.jsx @@ -1,9 +1,9 @@ import {useState, useEffect, useCallback, useMemo, useRef} from "react"; import {contrastColor} from "../utils/contrastColor"; import {shuffle} from "../utils/shuffle"; -import ReactPlayer from 'react-player' import toast from "react-hot-toast"; import {FlashcardNav} from "./FlashcardNav"; +import {FlashcardPlayer} from "./FlashcardPlayer"; export function FlashcardSession({terms, cardColors, onBack, title, description}) { const [currentIndex, setCurrentIndex] = useState(0); @@ -156,39 +156,18 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} />
-
- {playbackUrl ? ( - playingStateChanged(PLAYBACK_STATE_START)} - onPause={() => playingStateChanged(PLAYBACK_STATE_PAUSE)} - onEnded={() => playingStateChanged(PLAYBACK_STATE_END)} - onError={playbackError} - config={{ - file: { - attributes: { - playsinline: true - } - } - }} - /> - ) : ( -
- No video available -
- )} -
+ playingStateChanged(PLAYBACK_STATE_START)} + onPause={() => playingStateChanged(PLAYBACK_STATE_PAUSE)} + onEnded={() => playingStateChanged(PLAYBACK_STATE_END)} + onError={playbackError} + />