export function ImportPage() {
const navigate = useNavigate();
const [importData, setImportData] = useState<KenmeiData | null>(null);
const [error, setError] = useState<AppError | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [importSuccess, setImportSuccess] = useState(false);
const [previousMatchCount, setPreviousMatchCount] = useState(0);
const [progress, setProgress] = useState(0);
const statusCountsSnapshot = useMemo(
() => (importData ? getStatusCounts(importData.manga) : null),
[importData],
);
const handleFileLoaded = (data: KenmeiData) => {
setImportData(data);
setError(null);
setImportSuccess(false);
toast.success("File loaded", {
description:
`${data.manga.length} entries queued for review.` +
(previousMatchCount > 0
? " Prior matches will be reapplied automatically."
: " We'll keep your Kenmei metadata intact."),
});
};
const handleError = (error: AppError, toastId?: string) => {
setError(error);
setImportData(null);
setImportSuccess(false);
const descriptions: Partial<Record<ErrorType, string>> = {
[ErrorType.VALIDATION]:
"The file format looks off. Please double-check the export steps and try again.",
[ErrorType.STORAGE]:
"We couldn't persist the import locally. Make sure storage permissions are available and retry.",
[ErrorType.NETWORK]:
"A network hiccup interrupted the import. Check your connection and try again.",
};
const description =
descriptions[error.type] ||
"Please try again in a moment. If the issue persists, export a fresh file from Kenmei.";
toast.error(error.message, {
id: toastId,
description,
});
};
const handleImport = async () => {
if (!importData) {
return;
}
setIsLoading(true);
const loadingToastId = toast.loading("Processing your library", {
description: "Merging entries and preserving previous matches...",
}) as string;
// Start progress animation
setProgress(10);
const progressInterval = setInterval(() => {
setProgress((prev) => {
if (prev >= 90) {
clearInterval(progressInterval);
return 90;
}
return prev + 10;
});
}, 200);
try {
// Normalize the imported manga with proper ID assignment
const normalizedManga = normalizeMangaItems(importData.manga);
// Get previously imported manga to compare against
const previousManga = getPreviousMangaData();
// Merge manga data and get results
const { mergedManga, results } = mergeMangaData(
previousManga,
normalizedManga,
);
console.log(
`Import results: Previous: ${previousManga.length}, New file: ${normalizedManga.length}, Final merged: ${mergedManga.length}`,
);
console.log(
`Changes: ${results.newMangaCount} new manga, ${results.updatedMangaCount} updated manga`,
);
// Ensure all manga have proper IDs and required fields
const validMergedManga = validateMangaData(mergedManga);
saveKenmeiData({ manga: validMergedManga });
// Update existing match results with new data if any exist
updateMatchResults(validMergedManga);
// Clear pending manga storage after import to force recalculation
clearPendingMangaStorage();
// Show success state briefly before redirecting
setImportSuccess(true);
setProgress(100);
toast.success("Import ready for review", {
id: loadingToastId,
duration: 2800,
description: "We'll take you to the match review in just a sec.",
});
// Redirect to the review page after a short delay
setTimeout(() => {
navigate({ to: "/review" });
}, 1500);
} catch (err) {
// Handle any errors that might occur during storage
console.error("Storage error:", err);
handleError(
createError(
ErrorType.STORAGE,
"Failed to save import data. Please try again.",
),
loadingToastId,
);
} finally {
clearInterval(progressInterval);
setIsLoading(false);
}
};
const dismissError = () => {
setError(null);
};
const resetForm = () => {
setImportData(null);
setError(null);
setImportSuccess(false);
toast("Import reset", {
description:
"Upload a fresh Kenmei export whenever you're ready to try again.",
});
};
useEffect(() => {
// Check if we have previous match results
const savedResults = getSavedMatchResults();
if (savedResults && Array.isArray(savedResults)) {
const reviewedCount = savedResults.filter(
(m) =>
m.status === "matched" ||
m.status === "manual" ||
m.status === "skipped",
).length;
setPreviousMatchCount(reviewedCount);
}
}, []);
return (
<motion.div
className="container mx-auto space-y-10 px-4 py-8 md:px-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.4 }}
>
<motion.header
className="max-w-3xl space-y-3"
initial={{ opacity: 0, y: -12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<h1 className="text-3xl font-semibold tracking-tight sm:text-4xl">
Import your Kenmei library
</h1>
<p className="text-muted-foreground text-base">
Upload your Kenmei export, review each match, and sync the results to
AniList. We preserve previous match decisions so you can pick up right
where you left off.
</p>
</motion.header>
{error && (
<motion.div
className="mb-6"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3 }}
>
<ErrorMessage
message={error.message}
type={error.type}
dismiss={dismissError}
retry={importData ? handleImport : undefined}
/>
</motion.div>
)}
{(() => {
if (importSuccess && importData) {
return (
<ImportSuccessContent importData={importData} progress={progress} />
);
}
if (!importData) {
return (
<FileUploadContent
onFileLoaded={handleFileLoaded}
onError={handleError}
/>
);
}
const statusCounts =
statusCountsSnapshot ?? getStatusCounts(importData.manga);
return (
<FileReadyContent
importData={importData}
statusCounts={statusCounts}
previousMatchCount={previousMatchCount}
isLoading={isLoading}
onImport={handleImport}
onReset={resetForm}
/>
);
})()}
</motion.div>
);
}
Import page component for the Kenmei to AniList sync tool.
Handles file upload, import process, error handling, and displays import summary and data table for the user.