({ data, updatedDate }: { data: Question[]; updatedDate: string })
| 420 | } |
| 421 | |
| 422 | export default function QuestionsTable({ data, updatedDate }: { data: Question[]; updatedDate: string }) { |
| 423 | const isMobile = useIsMobile(); |
| 424 | const searchParams = useSearchParams(); |
| 425 | const { syncNow, syncVersion } = useAuth(); |
| 426 | |
| 427 | const [sorting, setSorting] = useState<SortingState>([]); |
| 428 | const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(() => |
| 429 | parseInitialFilters(searchParams) |
| 430 | ); |
| 431 | const [globalFilter, setGlobalFilter] = useState( |
| 432 | () => searchParams.get("q") ?? "" |
| 433 | ); |
| 434 | const [completed, setCompleted] = useState<Set<number>>(new Set()); |
| 435 | const [starred, setStarred] = useState<Set<number>>(new Set()); |
| 436 | const [shuffleOrder, setShuffleOrder] = useState<number[] | null>(null); |
| 437 | const [notes, setNotes] = useState<Record<number, string>>({}); |
| 438 | const [solvedDates, setSolvedDates] = useState<Record<number, string>>({}); |
| 439 | const [reminders, setReminders] = useState<Record<number, Reminder>>({}); |
| 440 | const [migrationToast, setMigrationToast] = useState<string | null>(null); |
| 441 | const [toastFading, setToastFading] = useState(false); |
| 442 | const [hydrated, setHydrated] = useState(false); |
| 443 | |
| 444 | useEffect(() => { |
| 445 | const migrated = migrateLegacyProgress(data); |
| 446 | setCompleted(migrated ?? loadCompleted()); |
| 447 | if (migrated) { |
| 448 | setMigrationToast(`Migrated ${migrated.size} completed question${migrated.size === 1 ? "" : "s"} from V1`); |
| 449 | } |
| 450 | setStarred(loadStarred()); |
| 451 | setShuffleOrder(loadShuffleOrder()); |
| 452 | setNotes(loadNotes()); |
| 453 | setSolvedDates(loadSolvedDates()); |
| 454 | setReminders(loadReminders()); |
| 455 | setHydrated(true); |
| 456 | }, [data]); |
| 457 | |
| 458 | // Reload from localStorage when remote sync arrives |
| 459 | useEffect(() => { |
| 460 | if (!hydrated || syncVersion === 0) return; |
| 461 | setCompleted(loadCompleted()); |
| 462 | setStarred(loadStarred()); |
| 463 | setNotes(loadNotes()); |
| 464 | setSolvedDates(loadSolvedDates()); |
| 465 | setReminders(loadReminders()); |
| 466 | }, [syncVersion, hydrated]); |
| 467 | |
| 468 | useEffect(() => { |
| 469 | if (!migrationToast) return; |
| 470 | const fadeTimer = setTimeout(() => setToastFading(true), 2500); |
| 471 | const removeTimer = setTimeout(() => { setMigrationToast(null); setToastFading(false); }, 3200); |
| 472 | return () => { clearTimeout(fadeTimer); clearTimeout(removeTimer); }; |
| 473 | }, [migrationToast]); |
| 474 | |
| 475 | useEffect(() => { |
| 476 | const params = new URLSearchParams(); |
| 477 | if (globalFilter) params.set("q", globalFilter); |
| 478 | const difficulty = columnFilters.find((f) => f.id === "difficulty")?.value as string[] | undefined; |
| 479 | if (difficulty?.length) params.set("difficulty", difficulty.join(",")); |
nothing calls this directly
no test coverage detected