207 lines
7.4 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
import { useAuthStore } from "@/features/auth/stores/auth.store";
import { useDashboardSummary, useDashboardTasks } from "@/features/dashboard/hooks";
import { TaskList, QuickStats, ActivityFeed } from "@/features/dashboard/components";
import { ErrorState } from "@/components/atoms/error-state";
import { PageLayout } from "@/components/templates";
import { cn } from "@/shared/utils";
import { InlineToast } from "@/components/atoms/inline-toast";
import { useInternetEligibility } from "@/features/services/hooks";
export function DashboardView() {
const { user, isAuthenticated, loading: authLoading, clearLoading } = useAuthStore();
const hideToastTimeout = useRef<number | null>(null);
const [eligibilityToast, setEligibilityToast] = useState<{
visible: boolean;
text: string;
tone: "info" | "success" | "warning" | "error";
}>({ visible: false, text: "", tone: "info" });
// Clear auth loading state when dashboard loads (after successful login)
useEffect(() => {
clearLoading();
}, [clearLoading]);
const { data: summary, isLoading: summaryLoading, error } = useDashboardSummary();
const { tasks, isLoading: tasksLoading, taskCount } = useDashboardTasks();
const { data: eligibility } = useInternetEligibility({ enabled: isAuthenticated });
// Combined loading state - AppShell handles unauthenticated redirect
const isLoading = authLoading || summaryLoading;
useEffect(() => {
if (!isAuthenticated || !user?.id) return;
const eligibilityData = eligibility as
| { status?: string; eligibility?: string }
| null
| undefined;
const status = eligibilityData?.status;
if (!status) return; // query not ready yet
const key = `cp:internet-eligibility:last:${user.id}`;
const last = localStorage.getItem(key);
if (status === "pending") {
localStorage.setItem(key, "PENDING");
return;
}
const eligibilityValue = eligibilityData?.eligibility;
if (status === "eligible" && typeof eligibilityValue === "string") {
const current = eligibilityValue.trim();
if (last === "PENDING") {
setEligibilityToast({
visible: true,
text: "We've finished reviewing your address — you can now choose personalized internet plans.",
tone: "success",
});
if (hideToastTimeout.current) window.clearTimeout(hideToastTimeout.current);
hideToastTimeout.current = window.setTimeout(() => {
setEligibilityToast(t => ({ ...t, visible: false }));
hideToastTimeout.current = null;
}, 3500);
}
localStorage.setItem(key, current);
}
}, [eligibility, isAuthenticated, user?.id]);
useEffect(() => {
return () => {
if (hideToastTimeout.current) window.clearTimeout(hideToastTimeout.current);
};
}, []);
if (isLoading) {
return (
<PageLayout title="Dashboard" description="Overview of your account" loading>
<div className="space-y-8">
{/* Greeting skeleton */}
<div className="space-y-3">
<div className="h-4 cp-skeleton-shimmer rounded w-24" />
<div className="h-10 cp-skeleton-shimmer rounded w-56" />
<div className="h-4 cp-skeleton-shimmer rounded w-40" />
</div>
{/* Tasks skeleton */}
<div className="space-y-4">
<div className="h-24 cp-skeleton-shimmer rounded-2xl" />
<div className="h-24 cp-skeleton-shimmer rounded-2xl" />
</div>
{/* Bottom section skeleton */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="h-44 cp-skeleton-shimmer rounded-2xl" />
<div className="h-44 cp-skeleton-shimmer rounded-2xl" />
</div>
</div>
</PageLayout>
);
}
// Handle error state
if (error) {
const errorMessage = (() => {
if (typeof error === "string") return error;
if (error instanceof Error) return error.message;
if (typeof error === "object" && "message" in error) {
return String((error as { message?: unknown }).message);
}
return "An unexpected error occurred";
})();
return (
<PageLayout title="Dashboard" description="Overview of your account">
<ErrorState title="Error loading dashboard" message={errorMessage} variant="page" />
</PageLayout>
);
}
// Get user's display name
const displayName = user?.firstname || user?.email?.split("@")[0] || "there";
// Determine urgency level for task badge
const hasUrgentTask = tasks.some(t => t.tone === "critical");
return (
<PageLayout title="Dashboard" description="Overview of your account">
<InlineToast
visible={eligibilityToast.visible}
text={eligibilityToast.text}
tone={eligibilityToast.tone}
/>
{/* Greeting Section */}
<div className="mb-8">
<p
className="text-sm font-medium text-muted-foreground animate-in fade-in slide-in-from-bottom-2 duration-500"
style={{ animationDelay: "0ms" }}
>
Welcome back
</p>
<h2
className="text-3xl sm:text-4xl font-bold text-foreground mt-1 font-display animate-in fade-in slide-in-from-bottom-4 duration-500"
style={{ animationDelay: "50ms" }}
>
{displayName}
</h2>
{/* Task status badge */}
{taskCount > 0 ? (
<div
className="flex items-center gap-2 mt-3 animate-in fade-in slide-in-from-bottom-4 duration-500"
style={{ animationDelay: "100ms" }}
>
<span
className={cn(
"inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-medium",
hasUrgentTask ? "bg-danger/10 text-danger" : "bg-warning/10 text-warning"
)}
>
{hasUrgentTask && <ExclamationTriangleIcon className="h-4 w-4" />}
{taskCount === 1 ? "1 task needs attention" : `${taskCount} tasks need attention`}
</span>
</div>
) : (
<p
className="text-sm text-muted-foreground mt-2 animate-in fade-in slide-in-from-bottom-4 duration-500"
style={{ animationDelay: "100ms" }}
>
Everything is up to date
</p>
)}
</div>
{/* Tasks Section - Main focus area */}
<section
className="mb-10 animate-in fade-in slide-in-from-bottom-6 duration-600"
style={{ animationDelay: "150ms" }}
aria-labelledby="tasks-heading"
>
<h3 id="tasks-heading" className="sr-only">
Your Tasks
</h3>
<TaskList tasks={tasks} isLoading={tasksLoading} maxTasks={4} />
</section>
{/* Bottom Section: Quick Stats + Recent Activity */}
<section
className="grid grid-cols-1 lg:grid-cols-2 gap-6 cp-stagger-children"
aria-label="Account overview"
>
<QuickStats
activeSubscriptions={summary?.stats?.activeSubscriptions ?? 0}
openCases={summary?.stats?.openCases ?? 0}
recentOrders={summary?.stats?.recentOrders}
isLoading={summaryLoading}
/>
<ActivityFeed
activities={summary?.recentActivity || []}
maxItems={5}
isLoading={summaryLoading}
/>
</section>
</PageLayout>
);
}