openreader/src/components/auth/ClaimDataModal.tsx
Richard R b2bbd8fbef feat(data-storage): migrate all user state from IndexedDB to server-backed storage
Remove all Dexie/IndexedDB code and dependencies, including document and preview caches, local config, onboarding, and migration logic. Replace with server-backed React Query hooks for documents, folders, preferences, onboarding, and progress. Add browser Cache Storage for blob caching of documents, previews, and audio. Update API routes, database schema, and tests to support folder management, onboarding state, and server-side persistence of all user data. Refactor UI and hooks to use server state exclusively, ensuring all user state is synced and portable across devices.

BREAKING CHANGE: All user data, preferences, onboarding, and document state are now stored and synced on the server; browser IndexedDB is no longer used. Existing local-only data will not be available after this update.
2026-06-14 18:13:39 -06:00

124 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import toast from 'react-hot-toast';
import { Button, ModalFrame, ModalTitle } from '@/components/ui';
export type ClaimableCounts = {
documents: number;
audiobooks: number;
preferences: number;
progress: number;
documentSettings: number;
folders: number;
onboarding: number;
};
function toClaimableCounts(value: unknown): ClaimableCounts {
const rec = (value && typeof value === 'object') ? (value as Record<string, unknown>) : {};
return {
documents: Number(rec.documents ?? 0),
audiobooks: Number(rec.audiobooks ?? 0),
preferences: Number(rec.preferences ?? 0),
progress: Number(rec.progress ?? 0),
documentSettings: Number(rec.documentSettings ?? 0),
folders: Number(rec.folders ?? 0),
onboarding: Number(rec.onboarding ?? 0),
};
}
type ClaimDataModalProps = {
isOpen: boolean;
claimableCounts: ClaimableCounts;
onDismiss: () => void;
onClaimed: () => void;
};
export default function ClaimDataModal({
isOpen,
claimableCounts,
onDismiss,
onClaimed,
}: ClaimDataModalProps) {
const router = useRouter();
const [isClaiming, setIsClaiming] = useState(false);
const handleClaim = async () => {
setIsClaiming(true);
try {
const res = await fetch('/api/user/claim', {
method: 'POST',
});
if (res.ok) {
const data = await res.json();
const claimed = toClaimableCounts(data?.claimed);
toast.success(
`Successfully claimed ${claimed.documents} documents, `
+ `${claimed.audiobooks} audiobooks, `
+ `${claimed.preferences} preference set(s), `
+ `${claimed.progress} reading progress record(s), and `
+ `${claimed.documentSettings} document setting(s)!`,
);
onClaimed();
router.refresh();
return;
}
const data = await res.json().catch(() => null) as { error?: string } | null;
toast.error(data?.error || 'Failed to claim data.');
} catch {
toast.error('Failed to claim data.');
} finally {
setIsClaiming(false);
}
};
return (
<ModalFrame open={isOpen} onClose={onDismiss} panelTestId="claim-modal" className="z-[80]">
<ModalTitle className="mb-4">Existing Data Found</ModalTitle>
<p className="text-sm text-soft mb-2">
We found existing anonymous data from before auth was enabled.
Claim it now to attach it to your account.
</p>
<div className="mb-4 rounded-lg border border-line bg-surface-sunken p-3">
<div className="text-xs font-semibold uppercase tracking-wide text-soft">Claimable data</div>
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm text-soft">
<li>{claimableCounts.documents} document(s)</li>
<li>{claimableCounts.audiobooks} audiobook(s)</li>
<li>{claimableCounts.preferences} preference set(s)</li>
<li>{claimableCounts.progress} reading progress record(s)</li>
<li>{claimableCounts.documentSettings} document setting(s)</li>
<li>{claimableCounts.folders} folder(s)</li>
<li>{claimableCounts.onboarding} onboarding state record(s)</li>
</ul>
</div>
<p className="text-xs text-faint mb-6 italic">
First user to claim this data will own it and revoke access for anyone else.
</p>
<div className="flex justify-end gap-3">
<Button
data-testid="claim-dismiss-button"
variant="outline"
size="sm"
onClick={onDismiss}
disabled={isClaiming}
>
Dismiss
</Button>
<Button
data-testid="claim-submit-button"
variant="primary"
size="sm"
onClick={handleClaim}
disabled={isClaiming}
>
{isClaiming ? 'Claiming...' : 'Claim Data'}
</Button>
</div>
</ModalFrame>
);
}