• 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 } = useAuth();

    // 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 [versionStatus, setVersionStatus] = useState<AppVersionStatus | null>(
    null,
    );
    const [syncStats, setSyncStats] = useState<SyncStats>({
    lastSyncTime: null,
    entriesSynced: 0,
    failedSyncs: 0,
    totalSyncs: 0,
    });

    useEffect(() => {
    let mounted = true;
    getAppVersionStatus().then((status) => {
    if (mounted) setVersionStatus(status);
    });
    return () => {
    mounted = false;
    };
    }, []);

    // Define interface for match result objects
    interface MatchResult {
    status?: string;
    selectedMatch?: {
    id: number;
    title: string;
    [key: string]: unknown;
    } | null;
    needsReview?: boolean;
    [key: string]: unknown;
    }

    // Load import stats from storage on component mount
    useEffect(() => {
    const importStats = getImportStats();
    if (importStats) {
    // Update stats from stored data
    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,
    }));
    }

    // Get match status data
    try {
    const matchResultsStr = localStorage.getItem("match_results");

    if (matchResultsStr) {
    const matchResults = JSON.parse(matchResultsStr);
    const totalCount = matchResults ? Object.keys(matchResults).length : 0;

    if (totalCount > 0) {
    // Count pending and skipped matches by iterating through match results
    let pendingCount = 0;
    let skippedCount = 0;

    Object.values(matchResults).forEach((result) => {
    // Type cast the unknown result to our MatchResult interface
    const matchResult = result as MatchResult;

    // Check if entry is explicitly marked as skipped
    if (matchResult.status === "skipped") {
    skippedCount++;
    }
    // Check if the entry genuinely needs review
    else if (
    matchResult.status === "pending" ||
    (matchResult.needsReview === true &&
    !matchResult.selectedMatch) ||
    (matchResult.status !== "skipped" && !matchResult.selectedMatch)
    ) {
    pendingCount++;
    }
    });

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

    try {
    const stats = JSON.parse(
    storage.getItem(STORAGE_KEYS.SYNC_STATS) || "{}",
    );
    setSyncStats({
    lastSyncTime: stats.lastSyncTime || null,
    entriesSynced: stats.entriesSynced || 0,
    failedSyncs: stats.failedSyncs || 0,
    totalSyncs: stats.totalSyncs || 0,
    });
    } catch {
    setSyncStats({
    lastSyncTime: null,
    entriesSynced: 0,
    failedSyncs: 0,
    totalSyncs: 0,
    });
    }
    }, []);

    // Format date for display
    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 {
    return "Invalid date";
    }
    };

    return (
    <motion.div
    className="container mx-auto px-4 py-8 md:px-6"
    initial={{ opacity: 0 }}
    animate={{ opacity: 1 }}
    transition={{ duration: 0.3 }}
    >
    <motion.div
    className="mb-10"
    variants={itemVariants}
    initial="hidden"
    animate="show"
    >
    <div className="flex flex-col space-y-3">
    <h1 className="text-4xl font-bold tracking-tight md:text-5xl">
    <span className="bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
    Dashboard
    </span>
    </h1>
    <p className="text-muted-foreground max-w-2xl">
    Welcome to the Kenmei to AniList sync tool. Import and synchronize
    your manga collection with ease.
    </p>
    <Separator className="mt-4" />
    </div>
    </motion.div>

    {/* Feature Carousel */}
    <motion.div
    className="mb-10"
    variants={itemVariants}
    initial="hidden"
    animate="show"
    >
    <Carousel
    opts={{
    align: "start",
    loop: true,
    }}
    plugins={[
    Autoplay({
    delay: 5000,
    stopOnInteraction: false,
    stopOnMouseEnter: true,
    }),
    ]}
    className="w-full"
    >
    <CarouselContent className="-ml-4">
    {featureCards.map((feature, index) => (
    <CarouselItem
    key={index}
    className="pl-4 md:basis-1/2 lg:basis-1/3"
    >
    <div className="p-1">
    <Card className="bg-muted/10 overflow-hidden border-none shadow-md">
    <div className={`h-2 bg-gradient-to-r ${feature.color}`} />
    <CardContent className="p-6">
    <div className="mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-gradient-to-br from-blue-100 to-indigo-100 text-blue-700 dark:from-blue-900/30 dark:to-indigo-900/30 dark:text-blue-300">
    {feature.icon}
    </div>
    <h3 className="mb-2 text-xl font-semibold">
    {feature.title}
    </h3>
    <p className="text-muted-foreground">
    {feature.description}
    </p>
    </CardContent>
    </Card>
    </div>
    </CarouselItem>
    ))}
    </CarouselContent>
    <div className="mt-4 flex justify-end gap-2">
    <CarouselPrevious className="static translate-y-0" />
    <CarouselNext className="static translate-y-0" />
    </div>
    </Carousel>
    </motion.div>

    <motion.div
    className="mb-10 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"
    variants={containerVariants}
    initial="hidden"
    animate="show"
    >
    <motion.div variants={itemVariants}>
    <Card className="overflow-hidden border-none bg-gradient-to-br from-blue-50 to-indigo-50 shadow-md transition-all hover:translate-y-[-3px] hover:shadow-lg dark:from-blue-950/40 dark:to-indigo-950/40">
    <CardHeader className="pb-2">
    <CardTitle className="flex items-center text-xl text-blue-700 dark:text-blue-400">
    <div className="mr-2 flex h-8 w-8 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-800/50">
    <Library className="h-5 w-5" />
    </div>
    Imported Items
    </CardTitle>
    </CardHeader>
    <CardContent>
    <div className="relative">
    <p className="mb-2 text-4xl font-bold text-blue-600 dark:text-blue-400">
    {stats.total}
    </p>
    <div className="text-muted-foreground flex items-center rounded-md bg-blue-50/50 px-2 py-1 text-sm dark:bg-blue-900/20">
    <Clock className="mr-1 h-3.5 w-3.5 text-blue-500" />
    {stats.total > 0
    ? `Last imported: ${formatDate(stats.lastSync)}`
    : "No items imported yet"}
    </div>
    <BarChart2 className="absolute right-0 bottom-0 h-16 w-16 text-blue-500 opacity-20 dark:text-blue-900/50" />
    </div>
    </CardContent>
    </Card>
    </motion.div>

    <motion.div variants={itemVariants}>
    <Card className="overflow-hidden border-none bg-gradient-to-br from-green-50 to-emerald-50 shadow-md transition-all hover:translate-y-[-3px] hover:shadow-lg dark:from-green-950/40 dark:to-emerald-950/40">
    <CardHeader className="pb-2">
    <CardTitle className="flex items-center text-xl text-green-700 dark:text-green-400">
    <div className="mr-2 flex h-8 w-8 items-center justify-center rounded-full bg-green-100 dark:bg-green-800/50">
    <ClipboardCheck className="h-5 w-5" />
    </div>
    Match Status
    </CardTitle>
    </CardHeader>
    <CardContent>
    <div className="relative">
    <p className="mb-2 text-4xl font-bold text-green-600 dark:text-green-400">
    {matchStatus.status === "none"
    ? "-"
    : matchStatus.status === "pending"
    ? "Ready"
    : "Complete"}
    </p>
    <div className="text-muted-foreground flex flex-row flex-wrap gap-2 text-sm">
    {matchStatus.status === "pending" ? (
    <Badge
    variant="outline"
    className="bg-amber-100/50 text-amber-700 shadow-sm dark:bg-amber-900/20 dark:text-amber-400"
    >
    <span className="flex items-center gap-1">
    <div className="h-1.5 w-1.5 rounded-full bg-amber-500"></div>
    {matchStatus.pendingMatches} manga need review
    </span>
    </Badge>
    ) : matchStatus.status === "complete" ? (
    <Badge
    variant="outline"
    className="bg-green-100/50 text-green-700 shadow-sm dark:bg-green-900/20 dark:text-green-400"
    >
    <span className="flex items-center gap-1">
    <div className="h-1.5 w-1.5 rounded-full bg-green-500"></div>
    {matchStatus.totalMatches - matchStatus.skippedMatches}{" "}
    manga matched
    </span>
    </Badge>
    ) : (
    <Badge
    variant="outline"
    className="bg-gray-100/50 text-gray-700 shadow-sm dark:bg-gray-900/20 dark:text-gray-400"
    >
    <span className="flex items-center gap-1">
    <div className="h-1.5 w-1.5 rounded-full bg-gray-500"></div>
    Import data first
    </span>
    </Badge>
    )}

    {matchStatus.skippedMatches > 0 && (
    <Badge
    variant="outline"
    className="bg-blue-100/50 text-blue-700 shadow-sm dark:bg-blue-900/20 dark:text-blue-400"
    >
    <span className="flex items-center gap-1">
    <div className="h-1.5 w-1.5 rounded-full bg-blue-500"></div>
    {matchStatus.skippedMatches} manga skipped
    </span>
    </Badge>
    )}
    </div>
    <RefreshCw
    className={`absolute right-0 bottom-0 h-16 w-16 opacity-20 ${
    matchStatus.status === "pending"
    ? "text-amber-500 dark:text-amber-900/50"
    : matchStatus.status === "complete"
    ? "text-green-500 dark:text-green-900/50"
    : "text-gray-500 dark:text-gray-900/50"
    }`}
    />
    </div>
    </CardContent>
    </Card>
    </motion.div>

    <motion.div variants={itemVariants}>
    <Card className="overflow-hidden border-none bg-gradient-to-br from-purple-50 to-fuchsia-50 shadow-md transition-all hover:translate-y-[-3px] hover:shadow-lg dark:from-purple-950/40 dark:to-fuchsia-950/40">
    <CardHeader className="pb-2">
    <CardTitle className="flex items-center text-xl text-purple-700 dark:text-purple-400">
    <BarChart2 className="mr-2 h-5 w-5" />
    Sync Statistics
    </CardTitle>
    </CardHeader>
    <CardContent>
    <div className="space-y-2">
    <div className="flex items-center gap-2">
    <Clock className="h-4 w-4 text-blue-500" />
    <span className="text-muted-foreground">Last Sync:</span>
    <span className="font-medium">
    {syncStats.lastSyncTime
    ? formatDate(syncStats.lastSyncTime)
    : "Never"}
    </span>
    </div>
    <div className="flex items-center gap-2">
    <CheckCheck className="h-4 w-4 text-green-500" />
    <span className="text-muted-foreground">Entries Synced:</span>
    <span className="font-medium">{syncStats.entriesSynced}</span>
    </div>
    <div className="flex items-center gap-2">
    <AlertCircle className="h-4 w-4 text-red-500" />
    <span className="text-muted-foreground">Failed Syncs:</span>
    <span className="font-medium">{syncStats.failedSyncs}</span>
    </div>
    <div className="flex items-center gap-2">
    <Sparkles className="h-4 w-4 text-blue-500" />
    <span className="text-muted-foreground">Total Syncs:</span>
    <span className="font-medium">{syncStats.totalSyncs}</span>
    </div>
    </div>
    </CardContent>
    </Card>
    </motion.div>
    </motion.div>

    <motion.div
    className="mb-10 grid grid-cols-1 gap-8"
    variants={containerVariants}
    initial="hidden"
    animate="show"
    >
    <motion.div variants={itemVariants}>
    <Card>
    <CardHeader>
    <CardTitle className="flex items-center text-xl">
    <Sparkles className="mr-2 h-5 w-5 text-blue-500" />
    Quick Actions
    </CardTitle>
    <CardDescription>
    Get started with these common tasks
    </CardDescription>
    </CardHeader>
    <CardContent>
    <div className="grid grid-cols-1 gap-4">
    <Button
    asChild
    variant="outline"
    className="h-auto w-full min-w-0 justify-start gap-3 border-blue-200 bg-blue-50 px-4 py-3 text-left transition-all hover:border-blue-300 hover:bg-blue-100 dark:border-blue-900 dark:bg-blue-950/30 dark:hover:border-blue-800 dark:hover:bg-blue-900/40"
    >
    <Link to="/import" className="flex w-full items-start">
    <div className="flex h-10 w-10 min-w-[2.5rem] flex-shrink-0 items-center justify-center rounded-full bg-blue-200 dark:bg-blue-800">
    <Download className="h-5 w-5 text-blue-700 dark:text-blue-300" />
    </div>
    <div className="mx-3 min-w-0 flex-1 space-y-1 overflow-hidden">
    <p className="text-sm leading-tight font-medium whitespace-normal">
    Import Data
    </p>
    <p
    className="word-break-normal overflow-hidden text-xs leading-tight text-wrap opacity-70"
    style={{ maxWidth: "100%", display: "block" }}
    >
    Upload your Kenmei CSV
    </p>
    </div>
    <ChevronRight className="text-muted-foreground mt-1.5 h-4 w-4 flex-shrink-0" />
    </Link>
    </Button>

    {stats.total > 0 ? (
    <Button
    asChild
    variant="outline"
    className="h-auto w-full min-w-0 justify-start gap-3 border-green-200 bg-green-50 px-4 py-3 text-left transition-all hover:border-green-300 hover:bg-green-100 dark:border-green-900 dark:bg-green-950/30 dark:hover:border-green-800 dark:hover:bg-green-900/40"
    >
    <Link to="/review" className="flex w-full items-start">
    <div className="flex h-10 w-10 min-w-[2.5rem] flex-shrink-0 items-center justify-center rounded-full bg-green-200 dark:bg-green-800">
    <ClipboardCheck className="h-5 w-5 text-green-700 dark:text-green-300" />
    </div>
    <div className="mx-3 min-w-0 flex-1 space-y-1 overflow-hidden">
    <p className="text-sm leading-tight font-medium whitespace-normal">
    Review Matches
    </p>
    <p
    className="word-break-normal overflow-hidden text-xs leading-tight text-wrap opacity-70"
    style={{ maxWidth: "100%", display: "block" }}
    >
    Check your manga matches
    </p>
    </div>
    <ChevronRight className="text-muted-foreground mt-1.5 h-4 w-4 flex-shrink-0" />
    </Link>
    </Button>
    ) : (
    <Button
    asChild
    variant="outline"
    className={`h-auto w-full min-w-0 justify-start gap-3 px-4 py-3 text-left transition-all ${
    authState.isAuthenticated
    ? "border-green-200 bg-green-50 hover:border-green-300 hover:bg-green-100 dark:border-green-900 dark:bg-green-950/30 dark:hover:border-green-800 dark:hover:bg-green-900/40"
    : "border-blue-200 bg-blue-50 hover:border-blue-300 hover:bg-blue-100 dark:border-blue-900 dark:bg-blue-950/30 dark:hover:border-blue-800 dark:hover:bg-blue-900/40"
    }`}
    >
    <Link to="/settings" className="flex w-full items-start">
    <div
    className={`flex h-10 w-10 min-w-[2.5rem] flex-shrink-0 items-center justify-center rounded-full ${
    authState.isAuthenticated
    ? "bg-green-200 dark:bg-green-800"
    : "bg-blue-200 dark:bg-blue-800"
    }`}
    >
    {authState.isAuthenticated ? (
    <UserCheck className="h-5 w-5 text-green-700 dark:text-green-300" />
    ) : (
    <AlertCircle className="h-5 w-5 text-blue-700 dark:text-blue-300" />
    )}
    </div>
    <div className="mx-3 min-w-0 flex-1 space-y-1 overflow-hidden">
    <p className="text-sm leading-tight font-medium whitespace-normal">
    {authState.isAuthenticated
    ? "AniList Connected"
    : "Connect to AniList"}
    </p>
    <p
    className="word-break-normal overflow-hidden text-xs leading-tight text-wrap opacity-70"
    style={{ maxWidth: "100%", display: "block" }}
    >
    {authState.isAuthenticated
    ? authState.username || "Authenticated User"
    : "Setup authentication"}
    </p>
    </div>
    {authState.isAuthenticated ? (
    <LogOut className="text-muted-foreground mt-1.5 h-4 w-4 flex-shrink-0" />
    ) : (
    <ChevronRight className="text-muted-foreground mt-1.5 h-4 w-4 flex-shrink-0" />
    )}
    </Link>
    </Button>
    )}

    <Button
    asChild
    variant="outline"
    className="h-auto w-full min-w-0 justify-start gap-3 border-purple-200 bg-purple-50 px-4 py-3 text-left transition-all hover:border-purple-300 hover:bg-purple-100 dark:border-purple-900 dark:bg-purple-950/30 dark:hover:border-purple-800 dark:hover:bg-purple-900/40"
    >
    <Link to="/sync" className="flex w-full items-start">
    <div className="flex h-10 w-10 min-w-[2.5rem] flex-shrink-0 items-center justify-center rounded-full bg-purple-200 dark:bg-purple-800">
    <RefreshCw className="h-5 w-5 text-purple-700 dark:text-purple-300" />
    </div>
    <div className="mx-3 min-w-0 flex-1 space-y-1 overflow-hidden">
    <p className="text-sm leading-tight font-medium whitespace-normal">
    Synchronize
    </p>
    <p
    className="word-break-normal overflow-hidden text-xs leading-tight text-wrap opacity-70"
    style={{ maxWidth: "100%", display: "block" }}
    >
    Sync to AniList
    </p>
    </div>
    <ChevronRight className="text-muted-foreground mt-1.5 h-4 w-4 flex-shrink-0" />
    </Link>
    </Button>

    <Button
    asChild
    variant="outline"
    className="h-auto w-full min-w-0 justify-start gap-3 border-amber-200 bg-amber-50 px-4 py-3 text-left transition-all hover:border-amber-300 hover:bg-amber-100 dark:border-amber-900 dark:bg-amber-950/30 dark:hover:border-amber-800 dark:hover:bg-amber-900/40"
    >
    <Link to="/settings" className="flex w-full items-start">
    <div className="flex h-10 w-10 min-w-[2.5rem] flex-shrink-0 items-center justify-center rounded-full bg-amber-200 dark:bg-amber-800">
    <Settings className="h-5 w-5 text-amber-700 dark:text-amber-300" />
    </div>
    <div className="mx-3 min-w-0 flex-1 space-y-1 overflow-hidden">
    <p className="text-sm leading-tight font-medium whitespace-normal">
    Settings
    </p>
    <p
    className="word-break-normal overflow-hidden text-xs leading-tight text-wrap opacity-70"
    style={{ maxWidth: "100%", display: "block" }}
    >
    Configure application settings
    </p>
    </div>
    <ChevronRight className="text-muted-foreground mt-1.5 h-4 w-4 flex-shrink-0" />
    </Link>
    </Button>
    </div>
    </CardContent>
    </Card>
    </motion.div>
    </motion.div>

    <motion.div variants={itemVariants} initial="hidden" animate="show">
    <Card className="border-none bg-gradient-to-r from-gray-50 to-gray-100 shadow-sm transition-all hover:shadow-md dark:from-gray-950/80 dark:to-gray-900/80">
    <CardContent className="flex flex-col items-center justify-center space-y-2 py-6">
    <p className="text-muted-foreground text-sm">
    Kenmei to AniList Sync ToolVersion {getAppVersion()}
    </p>
    <div className="flex items-center gap-2">
    {versionStatus === null ? (
    <Badge
    variant="outline"
    className="bg-gray-100 text-xs font-normal text-gray-600 dark:bg-gray-800/30 dark:text-gray-300"
    >
    Checking...
    </Badge>
    ) : versionStatus.status === "stable" ? (
    <Badge
    variant="outline"
    className="bg-green-50 text-xs font-normal text-green-700 dark:bg-green-900/30 dark:text-green-300"
    >
    Stable Release
    </Badge>
    ) : versionStatus.status === "beta" ? (
    <Badge
    variant="outline"
    className="bg-yellow-50 text-xs font-normal text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300"
    >
    Beta Release
    </Badge>
    ) : (
    <Badge
    variant="outline"
    className="bg-blue-50 text-xs font-normal text-blue-700 dark:bg-blue-900/30 dark:text-blue-300"
    >
    Development Build
    </Badge>
    )}
    <a
    href="https://github.com/RLAlpha49/KenmeiToAnilist"
    target="_blank"
    rel="noopener noreferrer"
    className="text-muted-foreground hover:text-foreground inline-flex items-center text-xs font-medium transition-colors"
    >
    <ExternalLink className="mr-1 h-3 w-3" />
    GitHub
    </a>
    </div>
    <p className="text-muted-foreground/60 text-xs">
    Made with ❤️ for manga readers
    </p>
    </CardContent>
    </Card>
    </motion.div>
    </motion.div>
    );
    }