diff --git a/PROGRESS.md b/PROGRESS.md index 013f287..21539f9 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -37,11 +37,15 @@ - Ensured voices list still loads while the form is disabled during save - Added unit tests for the upsert+voice flow (`mnemo_cards_admin`) - **Admin Packs: Color Palette Picker**: Added palette-based color selection for packs - - Replaced free-text color input with swatches + native color picker + - Replaced free-text color input with native color picker (palette) + - Removed hardcoded default swatch colors (keep only palette) - Added unit tests for the new color picker component (`mnemo_cards_admin`) - **Admin Tests: Pack Linking**: Added ability to link tests to packs in admin - Test editor now supports selecting packs and syncing links on Save - Added unit test coverage for the new pack-linking component (`mnemo_cards_admin`) +- **Admin Tests: Color Picker + Pack Color Sync**: Added palette-based test color selection + - Test editor now uses the same palette picker as packs + - When selecting/changing a pack in test editor, test color auto-updates to the pack color - **Service Reliability**: Enhanced service management and monitoring - Port conflict detection and resolution @@ -79,6 +83,7 @@ - Card UI: moved voice play icon into a `Stack` overlay so it doesn't take an extra row on the card (with widget test coverage) - CardViewer UI: navigation arrows are now constrained under the card on wide screens; favorites (heart) button is positioned closer to the card - Added widget test coverage for wide-screen navigation button positioning + - Pack details: "Перемешать" button no longer has an active (pressed) state (widget test updated) - Added app version display on authentication page - **UI Components Refactoring**: Extracted authentication components - Created `SignInWithGoogleButton` component for Google authentication @@ -98,6 +103,7 @@ - Added SafeArea wrapping and themed question cards on `game_page.dart` - Added widget tests for themed question cards, exit control, and interactive-only notice - Timer/progress now sourced from live session state on `game_page.dart` + - Fixed broken game navigation: `TestPage` now pushes `/game/:id` so Exit/Finish/Back work (incl. deep-link fallback) + widget tests - **Game Tests Package**: Started shared package `games/packages/game_tests` - Added configurable `GameTestSettings` (sounds/haptics/delay) - Integrated into `mnemo_cards_web_v2` via `TestsModule`/`TestsStateManager` @@ -132,6 +138,11 @@ - Admin voice uploads are persisted to `data/voice/` and DB stores only a file name (no base64) - Public voice list heals legacy base64-in-DB by migrating to file on first read - `/api/v2/voice/` serves bytes (or redirects for remote URLs) + - **Admin Tests: Stop Returning base64 Images**: `/api/v2/admin/tests/` now returns image **links**, never base64 + - Self-heals legacy base64/data-url values on read by persisting them into `data/cards/` and storing only a `cardId` in DB + - Normalizes admin round-trip values (`/api/.../cards//image` → ``) so editing tests doesn't pollute DB with API URLs + - When linking a test to a pack, automatically links all referenced image cards to that pack so `/api/v2/packs//cards//image` works + - Added focused backend unit test: `mnemo_cards_backend/test/api/v2/admin_tests_api_v2_image_urls_test.dart` - **User Telegram Field**: Added telegram field to user model - Added `telegram` field to `UserModel` and `UserDto` alongside `email` - Added `telegram` column to `users` table in database (migration v2→v3) diff --git a/TODO.md b/TODO.md index 00217f9..a5ed344 100644 --- a/TODO.md +++ b/TODO.md @@ -53,6 +53,7 @@ - Common libraries: Target 90% coverage - ✅ Added unit test for version display on auth page - ✅ Web: CardViewer navigation controls are constrained under the card on wide screens (widget test added) + - ✅ Web: Pack details "Перемешать" button has no active (pressed) state (widget test updated) - ✅ **User Telegram Field Tests**: Added comprehensive unit tests for telegram field - Created `test/models/user_model_telegram_test.dart` with 6 test cases - Created `test/user_dto_telegram_test.dart` with 7 test cases @@ -60,6 +61,7 @@ - ✅ Refactored authentication components - need to add unit tests for SignInWithGoogleButton and SignInWithTelegram - ✅ Added comprehensive unit tests for ThemeToggleWidget (7 test cases covering all functionality) - ✅ Game/Test flow cleanup: removed traditional test entry, themed game page, added widget coverage for question card surfaces, exit control, and interactive-only notice; timer/progress pulled from live session state + - ✅ Web: fixed game navigation actions (Exit/Finish/Back) by using push to `/game/:id` and adding deep-link fallback + widget tests - ✅ Game Tests package: created `games/packages/game_tests`, added `GameTestSettings`, integrated into `mnemo_cards_web_v2` settings and state manager - ✅ **Test Page Code Quality Fix**: Fixed critical code duplication in `test_page.dart` - Removed 700+ lines of duplicate code @@ -110,6 +112,7 @@ - Response time optimization - [x] **Card Images Storage**: Stop storing base64 in DB; always store path/file name and serve via image endpoints (фикс 500 на картинках) - [x] **Card Voices Storage**: Stop storing base64 in DB; store voice file name and serve via `/api/v2/voice/` (voices list returns direct `url`) + - [x] **Tests Images (Admin API)**: Stop returning base64 in `/api/v2/admin/tests/`; always return image links and heal legacy base64 on read - [x] **Generated Tests Cleanup**: Fix orphan generated tests and add TTL purge (cron/DB cleanup) ### Features @@ -123,10 +126,14 @@ - Added voice uploader to card editor (upload on Save) - Added unit tests for upsert+voice flow - [x] **Admin Packs: Color Palette Picker**: Add palette-based pack color selection - - Replace free-text hex input with swatches + native picker + - Replace free-text hex input with native picker (palette) + - Remove hardcoded default swatch colors (keep only palette) - Add unit tests for the picker component - [x] **Admin Tests: Link to Packs**: Add pack selection in test editor and sync test↔pack links on Save - Add unit test coverage for pack-linking UI +- [x] **Admin Tests: Color Picker + Pack Color Sync**: Use palette picker for test color and sync from selected pack + - Replace free-text test color input with `ColorPaletteInput` + - Auto-update test color when selecting/changing a pack in test editor - [x] **Matrix Test (image selection)**: Add matrix question type end-to-end (backend + web + admin) - Auto-generate matrix images from pack card pool (generator framework) - Web: multi-stage single question (shake on wrong, flip+reveal translation on correct, then disappear) diff --git a/mnemo_cards_admin/src/components/TestPacksManager.tsx b/mnemo_cards_admin/src/components/TestPacksManager.tsx index ee3572b..2db995d 100644 --- a/mnemo_cards_admin/src/components/TestPacksManager.tsx +++ b/mnemo_cards_admin/src/components/TestPacksManager.tsx @@ -19,12 +19,14 @@ import { Search, Check } from 'lucide-react' interface TestPacksManagerProps { currentPackIds: string[] onPacksChange: (addIds: string[], removeIds: string[]) => void + onSelectedPackColorChange?: (color: string) => void disabled?: boolean } export function TestPacksManager({ currentPackIds, onPacksChange, + onSelectedPackColorChange, disabled = false, }: TestPacksManagerProps) { // Reset internal state when switching tests / pack list. @@ -35,6 +37,7 @@ export function TestPacksManager({ key={stateKey} currentPackIds={currentPackIds} onPacksChange={onPacksChange} + onSelectedPackColorChange={onSelectedPackColorChange} disabled={disabled} /> ) @@ -43,6 +46,7 @@ export function TestPacksManager({ function TestPacksManagerInner({ currentPackIds, onPacksChange, + onSelectedPackColorChange, disabled = false, }: TestPacksManagerProps) { const [search, setSearch] = useState('') @@ -72,6 +76,11 @@ function TestPacksManagerInner({ enabled: !disabled, }) + const allPacks: CardPackPreviewDto[] = packsData?.items || [] + const packsById = useMemo(() => { + return new Map(allPacks.map((pack) => [String(pack.id), pack])) + }, [allPacks]) + useEffect(() => { const toAdd = selectedList.filter( (id) => !currentPackIds.includes(id) && !removedList.includes(id), @@ -93,28 +102,35 @@ function TestPacksManagerInner({ selectedPacks.size, ]) - const handleTogglePack = (packId: string) => { - if (disabled || !packId) return + const handleTogglePack = (pack: CardPackPreviewDto) => { + if (disabled || !pack?.id) return - const packIdStr = String(packId) + const packIdStr = String(pack.id) const isCurrentlySelected = selectedPacks.has(packIdStr) && !removedPacks.has(packIdStr) const isInCurrentTest = currentPackIds.includes(packIdStr) if (isCurrentlySelected) { - const newSelected = new Set(selectedPacks) - newSelected.delete(packIdStr) - setSelectedPacks(newSelected) + const nextSelected = new Set(selectedPacks) + nextSelected.delete(packIdStr) + setSelectedPacks(nextSelected) if (isInCurrentTest) { setRemovedPacks((prev) => new Set([...prev, packIdStr])) } + + if (onSelectedPackColorChange) { + const fallbackId = Array.from(nextSelected)[0] + const fallback = fallbackId ? packsById.get(fallbackId) : undefined + if (fallback?.color) onSelectedPackColorChange(fallback.color) + } + return } - const newSelected = new Set(selectedPacks) - newSelected.add(packIdStr) - setSelectedPacks(newSelected) + const nextSelected = new Set(selectedPacks) + nextSelected.add(packIdStr) + setSelectedPacks(nextSelected) if (removedPacks.has(packIdStr)) { setRemovedPacks((prev) => { @@ -123,6 +139,10 @@ function TestPacksManagerInner({ return next }) } + + if (onSelectedPackColorChange) { + onSelectedPackColorChange(pack.color ?? '') + } } const isPackSelected = (packId: string | number) => { @@ -134,7 +154,6 @@ function TestPacksManagerInner({ return currentPackIds.includes(String(packId)) } - const allPacks: CardPackPreviewDto[] = packsData?.items || [] const filteredPacks = search ? allPacks.filter( (pack) => @@ -197,7 +216,7 @@ function TestPacksManagerInner({ handleTogglePack(packIdStr)} + onClick={() => handleTogglePack(pack)} > {selected ? ( diff --git a/mnemo_cards_admin/src/components/ui/color-palette-input.test.tsx b/mnemo_cards_admin/src/components/ui/color-palette-input.test.tsx index cf53923..ff90949 100644 --- a/mnemo_cards_admin/src/components/ui/color-palette-input.test.tsx +++ b/mnemo_cards_admin/src/components/ui/color-palette-input.test.tsx @@ -19,19 +19,19 @@ describe('ColorPaletteInput', () => { expect(screen.getByLabelText('Color')).toHaveValue('#FF0000') }) - it('calls onChange when a swatch is clicked', () => { + it('calls onChange when text input changes', () => { const onChange = vi.fn() render( - , + <> + + + , ) - fireEvent.click(screen.getByRole('button', { name: 'Set color #ABCDEF' })) + fireEvent.change(screen.getByLabelText('Color'), { + target: { value: '#ABCDEF' }, + }) expect(onChange).toHaveBeenCalledWith('#ABCDEF') }) diff --git a/mnemo_cards_admin/src/components/ui/color-palette-input.tsx b/mnemo_cards_admin/src/components/ui/color-palette-input.tsx index 2ac4087..16ae61c 100644 --- a/mnemo_cards_admin/src/components/ui/color-palette-input.tsx +++ b/mnemo_cards_admin/src/components/ui/color-palette-input.tsx @@ -2,22 +2,6 @@ import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' -const DEFAULT_COLOR_PALETTE = [ - '#EF4444', // red-500 - '#F97316', // orange-500 - '#F59E0B', // amber-500 - '#EAB308', // yellow-500 - '#22C55E', // green-500 - '#10B981', // emerald-500 - '#14B8A6', // teal-500 - '#06B6D4', // cyan-500 - '#3B82F6', // blue-500 - '#6366F1', // indigo-500 - '#A855F7', // purple-500 - '#EC4899', // pink-500 - '#6B7280', // gray-500 -] as const - const isHexColor = (value: string): boolean => { const trimmed = value.trim() return /^#(?:[0-9a-fA-F]{3}){1,2}$/.test(trimmed) @@ -28,7 +12,6 @@ export type ColorPaletteInputProps = { value: string onChange: (value: string) => void disabled?: boolean - palette?: readonly string[] placeholder?: string } @@ -37,7 +20,6 @@ export const ColorPaletteInput = ({ value, onChange, disabled = false, - palette = DEFAULT_COLOR_PALETTE, placeholder = '#FF0000', }: ColorPaletteInputProps) => { const trimmed = value.trim() @@ -84,27 +66,6 @@ export const ColorPaletteInput = ({
- {palette.map((color) => { - const isSelected = trimmed.toLowerCase() === color.toLowerCase() - - return ( -