• Home page component for the Kenmei to AniList sync tool.

    Displays dashboard statistics, feature carousel, quick actions, and sync status for the user.

    Returns Element

    export function HomePage() {
    // Get auth state to check authentication status
    const { authState } = useAuthState();
    const { startOnboarding: startOnboardingFlow, isActive: isOnboardingActive } =
    useOnboarding();

    // State for dashboard data
    const [stats, setStats] = useState<StatsState>({
    total: 0,
    reading: 0,
    completed: 0,
    onHold: 0,
    dropped: 0,
    planToRead: 0,
    lastSync: null,
    syncStatus: "none",
    });
    const [matchStatus, setMatchStatus] = useState<{
    pendingMatches: number;
    skippedMatches: number;
    totalMatches: number;
    status: "none" | "pending" | "complete";
    }>({
    pendingMatches: 0,
    skippedMatches: 0,
    totalMatches: 0,
    status: "none",
    });

    // Version status state
    const [syncStats, setSyncStats] = useState<SyncStats>({
    lastSyncTime: null,
    entriesSynced: 0,
    failedSyncs: 0,
    totalSyncs: 0,
    });

    // Loading and error state
    const [loadError, setLoadError] = useState<string | null>(null);

    // Load import stats & related data on mount
    useEffect(() => {
    const loadImportStats = () => {
    try {
    const importStats = getImportStats();
    if (!importStats) return;

    const statusCounts = importStats.statusCounts || {};
    setStats((prev) => ({
    ...prev,
    total: importStats.total || 0,
    reading: statusCounts.reading || 0,
    completed: statusCounts.completed || 0,
    dropped: statusCounts.dropped || 0,
    onHold: statusCounts.on_hold || 0,
    planToRead: statusCounts.plan_to_read || 0,
    lastSync: importStats.timestamp || null,
    }));
    } catch (error) {
    console.error("[HomePage] ❌ Error loading import stats:", error);
    setLoadError("Failed to load import statistics");
    }
    };

    const parseMatchResults = () => {
    try {
    console.debug("[HomePage] 🔍 Loading match results...");
    const matchResults = getSavedMatchResults();
    if (!matchResults || matchResults.length === 0) {
    console.debug("[HomePage] 🔍 No match results found");
    setMatchStatus(getEmptyMatchStatus());
    return;
    }

    const entries = matchResults;
    const totalCount = entries.length;

    if (totalCount === 0) {
    console.debug("[HomePage] 🔍 Match results empty");
    setMatchStatus(getEmptyMatchStatus());
    return;
    }

    const { pendingCount, skippedCount } = calculateMatchCounts(entries);

    console.info(
    `[HomePage] ✅ Loaded match status: ${totalCount} total, ${pendingCount} pending, ${skippedCount} skipped`,
    );

    setMatchStatus({
    pendingMatches: pendingCount,
    skippedMatches: skippedCount,
    totalMatches: totalCount,
    status: pendingCount === 0 ? "complete" : "pending",
    });
    } catch (error) {
    console.error("[HomePage] ❌ Error retrieving match status:", error);
    }
    };

    const loadSyncStats = () => {
    try {
    console.debug("[HomePage] 🔍 Loading sync stats...");
    const raw = storage.getItem(STORAGE_KEYS.SYNC_STATS) || "{}";
    const parsed = JSON.parse(raw);
    setSyncStats({
    lastSyncTime: parsed.lastSyncTime || null,
    entriesSynced: parsed.entriesSynced || 0,
    failedSyncs: parsed.failedSyncs || 0,
    totalSyncs: parsed.totalSyncs || 0,
    });
    console.info(
    `[HomePage] ✅ Loaded sync stats: ${parsed.totalSyncs || 0} total syncs, ${parsed.entriesSynced || 0} entries synced`,
    );
    } catch (error) {
    console.error("[HomePage] ❌ Error loading sync stats:", error);
    setSyncStats({
    lastSyncTime: null,
    entriesSynced: 0,
    failedSyncs: 0,
    totalSyncs: 0,
    });
    setLoadError("Failed to load sync statistics");
    }
    };

    console.debug("[HomePage] 🔍 Initializing HomePage data...");
    setLoadError(null);
    loadImportStats();
    parseMatchResults();
    loadSyncStats();
    }, []);

    const handleRestartOnboarding = () => {
    startOnboardingFlow();
    };

    /**
    * Formats a number with locale-specific thousand separators.
    * @param value - The number to format.
    * @returns Formatted number string with locale separators.
    * @source
    */
    const formatNumber = (value: number) => value.toLocaleString();

    /**
    * Formats a date string into a human-readable local date and time.
    * @param dateString - ISO date string or null.
    * @returns Formatted date and time string or "Never" if null.
    * @source
    */
    const formatDate = (dateString: string | null) => {
    if (!dateString) return "Never";

    try {
    const date = new Date(dateString);
    return (
    date.toLocaleDateString() +
    " " +
    date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
    );
    } catch (error) {
    console.warn("[HomePage] ⚠️ Invalid date format:", dateString, error);
    return "Invalid date";
    }
    };

    const heroAction = useMemo(
    () =>
    getHeroAction(
    authState.isAuthenticated,
    stats.total,
    matchStatus.pendingMatches,
    syncStats.totalSyncs,
    ),
    [
    authState.isAuthenticated,
    stats.total,
    matchStatus.pendingMatches,
    syncStats.totalSyncs,
    ],
    );

    const heroHighlights = useMemo(
    () => [
    {
    label: "Imported Entries",
    value: stats.total,
    icon: Library,
    accent: "text-blue-500 dark:text-blue-400",
    },
    {
    label: "Pending Review",
    value: matchStatus.pendingMatches,
    icon: ClipboardCheck,
    accent: "text-amber-500 dark:text-amber-400",
    },
    {
    label: "Entries Synced",
    value: syncStats.entriesSynced,
    icon: CheckCheck,
    accent: "text-emerald-500 dark:text-emerald-400",
    },
    ],
    [stats.total, matchStatus.pendingMatches, syncStats.entriesSynced],
    );

    const statusBreakdown = useMemo(
    () => [
    {
    label: "Reading",
    value: stats.reading,
    gradient: "from-sky-500 to-blue-500",
    },
    {
    label: "Completed",
    value: stats.completed,
    gradient: "from-green-500 to-emerald-500",
    },
    {
    label: "On Hold",
    value: stats.onHold,
    gradient: "from-amber-500 to-orange-500",
    },
    {
    label: "Plan to Read",
    value: stats.planToRead,
    gradient: "from-purple-500 to-indigo-500",
    },
    {
    label: "Dropped",
    value: stats.dropped,
    gradient: "from-rose-500 to-red-500",
    },
    ],
    [
    stats.reading,
    stats.completed,
    stats.onHold,
    stats.planToRead,
    stats.dropped,
    ],
    );

    let matchStatusText: string;
    if (matchStatus.status === "none") {
    matchStatusText = "Waiting";
    } else if (matchStatus.status === "pending") {
    matchStatusText = "Needs Review";
    } else {
    matchStatusText = "Complete";
    }

    const syncSuccessRate =
    syncStats.entriesSynced + syncStats.failedSyncs > 0
    ? Math.round(
    (syncStats.entriesSynced /
    Math.max(syncStats.entriesSynced + syncStats.failedSyncs, 1)) *
    100,
    )
    : null;

    const reviewFootnote = getReviewFootnote(
    matchStatus.pendingMatches,
    matchStatus.status,
    );

    const reviewAction = getReviewAction(
    stats.total,
    authState.isAuthenticated,
    authState.username,
    reviewFootnote,
    );

    const quickActionConfigs = [
    {
    key: "import",
    label: "Import data",
    description: "Upload your Kenmei CSV export",
    to: "/import",
    icon: Download,
    tone: "from-blue-500/20 via-indigo-500/20 to-transparent",
    iconClass: "text-blue-600 dark:text-blue-300",
    badgeClass: "bg-blue-500/20 text-blue-700 dark:text-blue-300",
    footnote:
    stats.total > 0
    ? `${formatNumber(stats.total)} imported`
    : "New import",
    },
    reviewAction,
    {
    key: "sync",
    label: "Synchronize",
    description: "Push your curated list to AniList",
    to: "/sync",
    icon: RefreshCw,
    tone: "from-fuchsia-500/20 via-purple-500/20 to-transparent",
    iconClass: "text-purple-600 dark:text-purple-300",
    badgeClass: "bg-fuchsia-500/20 text-purple-700 dark:text-purple-300",
    footnote:
    syncStats.entriesSynced > 0
    ? `${formatNumber(syncStats.entriesSynced)} synced`
    : "Ready when reviewed",
    },
    {
    key: "settings",
    label: "Settings",
    description: "Tune sync priorities and privacy rules",
    to: "/settings",
    icon: Settings,
    tone: "from-amber-500/20 via-orange-500/20 to-transparent",
    iconClass: "text-amber-600 dark:text-amber-300",
    badgeClass: "bg-amber-500/20 text-amber-700 dark:text-amber-300",
    footnote: "Customize automation",
    },
    ];

    return (
    <>
    <AnimatePresence mode="wait"></AnimatePresence>
    <div className="relative min-h-screen overflow-hidden">
    {/* Error notification banner */}
    {loadError && (
    <div className="fixed left-0 right-0 top-0 z-50 border-b border-red-200 bg-red-50 p-4 dark:border-red-800 dark:bg-red-950/50">
    <div className="container mx-auto flex items-center justify-between">
    <div className="flex items-center gap-2">
    <AlertCircle className="h-5 w-5 text-red-600 dark:text-red-400" />
    <span className="text-sm font-medium text-red-800 dark:text-red-300">
    {loadError}
    </span>
    </div>
    <button
    onClick={() => setLoadError(null)}
    className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
    aria-label="Dismiss error"
    >

    </button>
    </div>
    </div>
    )}
    <div className="pointer-events-none absolute inset-0">
    <div className="bg-linear-to-br absolute -left-1/4 top-[-10%] h-[460px] w-[460px] rounded-full from-sky-400/25 via-blue-500/15 to-transparent blur-[180px]" />
    <div className="bg-linear-to-br absolute right-[-15%] top-[35%] h-[380px] w-[380px] rounded-full from-emerald-400/20 via-teal-400/15 to-transparent blur-[160px]" />
    <div className="bg-linear-to-br absolute bottom-[-25%] left-1/2 h-[360px] w-[520px] -translate-x-1/2 rounded-full from-rose-400/20 via-orange-300/15 to-transparent blur-[200px]" />
    </div>

    <motion.main
    className="container relative mx-auto px-4 py-12 md:px-6"
    initial={{ opacity: 0 }}
    animate={{ opacity: 1 }}
    transition={{ duration: 0.3 }}
    >
    <motion.section
    className="mb-12"
    variants={itemVariants}
    initial="hidden"
    animate="show"
    >
    <div>
    <div className="bg-linear-to-br relative overflow-hidden rounded-3xl border border-white/30 from-white/80 via-white/60 to-white/30 p-8 shadow-2xl backdrop-blur-xl dark:border-white/10 dark:from-slate-950/70 dark:via-slate-950/60 dark:to-slate-950/40">
    <div className="pointer-events-none absolute -left-28 -top-40 h-72 w-72 rounded-full bg-blue-500/20 blur-3xl" />
    <div className="pointer-events-none absolute bottom-[-140px] right-[-60px] h-64 w-64 rounded-full bg-purple-500/25 blur-3xl" />
    <div className="relative z-10 flex flex-col gap-8">
    <div className="space-y-4">
    <Badge
    variant="outline"
    className="text-foreground/70 w-fit rounded-full border-white/50 bg-white/80 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] shadow-sm backdrop-blur-sm dark:border-white/10 dark:bg-white/10"
    >
    KenmeiAniList
    </Badge>
    <div className="space-y-4">
    <h1 className="text-foreground text-4xl font-semibold tracking-tight md:text-5xl lg:text-6xl">
    Move your library from KenmeiAniList with ease
    </h1>
    <p className="text-muted-foreground text-lg md:max-w-2xl">
    Follow a guided journey to import, review, and sync your
    manga collection.
    </p>
    </div>
    </div>
    <div className="flex flex-col gap-3 sm:flex-row sm:items-center">
    <Button
    asChild
    size="lg"
    className={`bg-linear-to-r group h-auto rounded-full ${heroAction.tone} px-6 py-3 text-base font-semibold text-white shadow-lg transition hover:shadow-xl focus-visible:ring-2 focus-visible:ring-white/70 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-slate-950`}
    >
    <Link
    to={heroAction.href}
    className="flex items-center gap-2"
    >
    <span>{heroAction.label}</span>
    <ArrowUpRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
    </Link>
    </Button>
    {!isOnboardingActive && (
    <Button
    onClick={handleRestartOnboarding}
    variant="ghost"
    size="lg"
    className="h-auto rounded-full px-6 py-3 text-base font-semibold"
    >
    <HelpCircle className="mr-2 h-4 w-4" />
    Start guided tour
    </Button>
    )}
    </div>
    <div className="grid gap-4 sm:grid-cols-3">
    {heroHighlights.map((metric) => {
    const Icon = metric.icon;
    return (
    <div
    key={metric.label}
    className="group rounded-2xl border border-white/40 bg-white/75 p-4 shadow-sm backdrop-blur-sm transition hover:-translate-y-1 hover:shadow-xl dark:border-white/10 dark:bg-slate-950/70"
    >
    <section
    aria-label={`${metric.label}: ${formatNumber(metric.value)}`}
    >
    <div className="flex h-10 w-10 items-center justify-center rounded-full bg-white/70 shadow-sm dark:bg-slate-900/75">
    <Icon
    className={`h-5 w-5 ${metric.accent}`}
    aria-hidden="true"
    />
    </div>
    <p className="mt-3 text-2xl font-semibold">
    {formatNumber(metric.value)}
    </p>
    <p className="text-muted-foreground text-sm">
    {metric.label}
    </p>
    </section>
    </div>
    );
    })}
    </div>
    </div>
    </div>
    </div>
    </motion.section>

    <motion.section
    className="mb-12 space-y-4"
    variants={itemVariants}
    initial="hidden"
    animate="show"
    >
    <div className="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
    <div>
    <h2 className="text-2xl font-semibold">Action board</h2>
    <p className="text-muted-foreground text-sm">
    Quick commands to move your migration forward.
    </p>
    </div>
    <Badge
    variant="outline"
    className="text-foreground/70 w-fit rounded-full border-white/30 bg-white/60 px-3 py-1 text-[0.65rem] font-semibold uppercase tracking-[0.2em] dark:border-white/10 dark:bg-slate-950/60"
    >
    Guided flow
    </Badge>
    </div>
    <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
    {quickActionConfigs.map((action) => {
    const ActionIcon = action.icon;
    return (
    <Link
    key={action.key}
    to={action.to}
    className={`bg-linear-to-br group flex h-full flex-col justify-between rounded-2xl border border-white/25 p-5 shadow-lg backdrop-blur transition hover:-translate-y-1 hover:border-white/40 hover:shadow-xl focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 dark:border-white/10 dark:focus-visible:ring-offset-slate-950 ${action.tone}`}
    title={`${action.label}: ${action.description}`}
    >
    <div className="flex items-start justify-between gap-3">
    <div className="flex items-center gap-3">
    <div className="flex h-12 w-12 items-center justify-center rounded-xl bg-white/75 shadow-sm dark:bg-slate-900/70">
    <ActionIcon
    className={`h-5 w-5 ${action.iconClass}`}
    aria-hidden="true"
    />
    </div>
    <div>
    <p className="text-foreground text-sm font-semibold">
    {action.label}
    </p>
    <p className="text-muted-foreground text-xs">
    {action.description}
    </p>
    </div>
    </div>
    <ChevronRight
    className="text-muted-foreground h-4 w-4 transition group-hover:translate-x-1"
    aria-hidden="true"
    />
    </div>
    {action.footnote && (
    <Badge
    variant="outline"
    className={`mt-6 w-fit rounded-full border-transparent px-2.5 py-0.5 text-[0.65rem] font-semibold uppercase tracking-[0.2em] ${action.badgeClass}`}
    >
    {action.footnote}
    </Badge>
    )}
    </Link>
    );
    })}
    </div>
    </motion.section>

    <motion.section
    className="mb-12 space-y-4"
    variants={itemVariants}
    initial="hidden"
    animate="show"
    >
    <div className="space-y-1">
    <h2 className="text-2xl font-semibold">Migration insights</h2>
    <p className="text-muted-foreground text-sm">
    Live metrics help you validate progress and spot blockers.
    </p>
    </div>
    <div className="grid gap-6 lg:grid-cols-3">
    <Card className="relative overflow-hidden border border-white/30 bg-white/80 p-6 shadow-xl backdrop-blur dark:border-white/10 dark:bg-slate-950/70">
    <div className="pointer-events-none absolute -right-16 -top-10 h-40 w-40 rounded-full bg-blue-500/20 blur-3xl" />
    <CardHeader className="space-y-2 p-0 pb-4">
    <CardTitle className="flex items-center gap-2 text-lg font-semibold">
    <div className="flex h-10 w-10 items-center justify-center rounded-full bg-white/60 text-blue-600 shadow-sm dark:bg-slate-900/60">
    <Library className="h-5 w-5" />
    </div>
    Library snapshot
    </CardTitle>
    <CardDescription>
    {stats.total > 0
    ? "Your Kenmei export is ready to sync."
    : "Import to unlock personalized recommendations."}
    </CardDescription>
    </CardHeader>
    <CardContent className="space-y-4 p-0">
    <p className="text-4xl font-semibold">
    {formatNumber(stats.total)}
    </p>
    <div className="text-muted-foreground flex items-center gap-2 text-sm">
    <Clock className="h-4 w-4 text-blue-500" />
    <span>
    {stats.total > 0
    ? `Last import ${formatDate(stats.lastSync)}`
    : "Awaiting first import"}
    </span>
    </div>
    </CardContent>
    </Card>

    <Card className="relative overflow-hidden border border-white/30 bg-white/80 p-6 shadow-xl backdrop-blur dark:border-white/10 dark:bg-slate-950/70">
    <div className="pointer-events-none absolute left-[-30px] top-10 h-36 w-36 rounded-full bg-emerald-400/20 blur-3xl" />
    <CardHeader className="space-y-2 p-0 pb-4">
    <CardTitle className="flex items-center gap-2 text-lg font-semibold">
    <div className="flex h-10 w-10 items-center justify-center rounded-full bg-white/60 text-emerald-600 shadow-sm dark:bg-slate-900/60">
    <ClipboardCheck className="h-5 w-5" />
    </div>
    Review status
    </CardTitle>
    <CardDescription>
    Resolve matches to unlock a confident sync.
    </CardDescription>
    </CardHeader>
    <CardContent className="space-y-4 p-0">
    <div className="flex items-center gap-3">
    <p className="text-3xl font-semibold">{matchStatusText}</p>
    {renderMatchStatusBadge(matchStatus)}
    </div>
    <div className="text-muted-foreground flex flex-wrap gap-2 text-xs">
    {matchStatus.totalMatches > 0 && (
    <span className="inline-flex items-center gap-1 rounded-full bg-white/70 px-3 py-1 shadow-sm dark:bg-slate-900/70">
    <BarChart2 className="h-3.5 w-3.5 text-emerald-500" />
    {formatNumber(matchStatus.totalMatches)} total
    </span>
    )}
    {matchStatus.skippedMatches > 0 && (
    <span className="inline-flex items-center gap-1 rounded-full bg-white/70 px-3 py-1 shadow-sm dark:bg-slate-900/70">
    <AlertCircle className="h-3.5 w-3.5 text-blue-500" />
    {formatNumber(matchStatus.skippedMatches)} skipped
    </span>
    )}
    </div>
    </CardContent>
    </Card>

    <Card className="relative overflow-hidden border border-white/30 bg-white/80 p-6 shadow-xl backdrop-blur dark:border-white/10 dark:bg-slate-950/70">
    <div className="pointer-events-none absolute bottom-[-60px] right-[-50px] h-40 w-40 rounded-full bg-purple-500/25 blur-3xl" />
    <CardHeader className="space-y-2 p-0 pb-4">
    <CardTitle className="flex items-center gap-2 text-lg font-semibold">
    <div className="flex h-10 w-10 items-center justify-center rounded-full bg-white/60 text-purple-600 shadow-sm dark:bg-slate-900/60">
    <Sparkles className="h-5 w-5" />
    </div>
    Sync health
    </CardTitle>
    <CardDescription>
    Validate reliability before running your next sync.
    </CardDescription>
    </CardHeader>
    <CardContent className="text-muted-foreground space-y-3 p-0 text-sm">
    <div className="flex items-center justify-between">
    <span className="text-foreground font-medium">
    Last sync
    </span>
    <span>
    {syncStats.lastSyncTime
    ? formatDate(syncStats.lastSyncTime)
    : "Not yet"}
    </span>
    </div>
    <div className="flex items-center justify-between">
    <span className="text-foreground font-medium">
    Entries synced
    </span>
    <span className="flex items-center gap-2">
    {formatNumber(syncStats.entriesSynced)}
    {syncStats.entriesSynced >= 100 && (
    <span className="text-sm" title="Milestone unlocked">
    🎉
    </span>
    )}
    </span>
    </div>
    <div className="flex items-center justify-between">
    <span className="text-foreground font-medium">
    Failed syncs
    </span>
    <span className="flex items-center gap-2">
    {formatNumber(syncStats.failedSyncs)}
    {syncStats.failedSyncs > 0 && (
    <span
    className="text-xs font-semibold text-amber-600 dark:text-amber-400"
    title="Review sync logs to investigate failures"
    >
    ⚠️
    </span>
    )}
    </span>
    </div>
    <div className="flex items-center justify-between">
    <span className="text-foreground font-medium">
    Total sync runs
    </span>
    <span>{formatNumber(syncStats.totalSyncs)}</span>
    </div>
    {syncSuccessRate !== null && (
    <div
    className={`flex items-center justify-between rounded-xl px-3 py-2 text-xs font-semibold uppercase tracking-wide shadow-sm ${
    syncSuccessRate >= 95
    ? "bg-emerald-500/25 text-emerald-900 dark:text-emerald-100"
    : "bg-white/60 text-purple-700 dark:bg-slate-900/60 dark:text-purple-200"
    }`}
    >
    <span>Reliability</span>
    <span className="flex items-center gap-1">
    {syncSuccessRate}% success
    {syncSuccessRate >= 95 && <span>⭐</span>}
    </span>
    </div>
    )}
    </CardContent>
    </Card>

    <Card className="border border-white/30 bg-white/80 p-6 shadow-xl backdrop-blur lg:col-span-3 dark:border-white/10 dark:bg-slate-950/70">
    <CardHeader className="space-y-2 p-0 pb-4">
    <CardTitle className="text-lg font-semibold">
    Status distribution
    </CardTitle>
    <CardDescription>
    Understand how your library breaks down by reading state.
    </CardDescription>
    </CardHeader>
    <CardContent className="space-y-4 p-0">
    {statusBreakdown.map((status) => {
    const percent =
    stats.total > 0
    ? Math.round((status.value / stats.total) * 100)
    : 0;
    return (
    <div key={status.label} className="space-y-2">
    <div className="flex items-center justify-between text-sm">
    <span className="text-foreground font-medium">
    {status.label}
    </span>
    <span className="text-muted-foreground">
    {formatNumber(status.value)}{" "}
    {status.value === 1 ? "entry" : "entries"} •{" "}
    {percent}%
    </span>
    </div>
    <div className="bg-muted relative h-2.5 overflow-hidden rounded-full">
    <div
    className={`bg-linear-to-r absolute inset-y-0 left-0 rounded-full ${status.gradient}`}
    style={{ width: `${percent}%` }}
    />
    </div>
    </div>
    );
    })}
    {stats.total === 0 && (
    <p className="text-muted-foreground text-sm">
    Import your Kenmei CSV to unlock detailed visualizations.
    </p>
    )}
    </CardContent>
    </Card>
    </div>
    </motion.section>
    </motion.main>
    </div>
    </>
    );
    }