- Extended Tailwind config with 3xl (1920px) and 4xl (2560px) breakpoints - Added responsive max-widths (8xl, 9xl, 10xl) for large screens - Updated Layout component with scaling max-width and padding - Made Header container responsive for large displays - Added responsive Sidebar width (64→72→80 for lg→3xl→4xl) - Implemented responsive typography in globals.css - Updated Dashboard grids to utilize more columns on large screens - Added comprehensive unit tests for responsive classes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
294 lines
9.7 KiB
TypeScript
294 lines
9.7 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import api from '@/lib/api';
|
|
import { useAuthStore } from '@/stores/authStore';
|
|
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
|
import { ErrorMessage } from '@/components/ui/ErrorMessage';
|
|
import type { Beneficiary } from '@/types';
|
|
|
|
/**
|
|
* Dashboard Page
|
|
*
|
|
* Main dashboard showing overview of beneficiaries and sensor status.
|
|
* Displays:
|
|
* - Summary statistics (total beneficiaries, active sensors, etc.)
|
|
* - List of all beneficiaries with their status
|
|
* - Quick actions (add beneficiary, view equipment)
|
|
*/
|
|
export default function DashboardPage() {
|
|
const router = useRouter();
|
|
const { isAuthenticated, user } = useAuthStore();
|
|
const [beneficiaries, setBeneficiaries] = useState<Beneficiary[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Fetch beneficiaries on mount
|
|
useEffect(() => {
|
|
if (!isAuthenticated) {
|
|
router.push('/login');
|
|
return;
|
|
}
|
|
|
|
const fetchBeneficiaries = async () => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const response = await api.getAllBeneficiaries();
|
|
|
|
if (response.ok && response.data) {
|
|
setBeneficiaries(response.data);
|
|
} else {
|
|
setError(response.error?.message || 'Failed to load beneficiaries');
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'An unexpected error occurred');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchBeneficiaries();
|
|
}, [isAuthenticated, router]);
|
|
|
|
// Calculate summary statistics
|
|
const totalBeneficiaries = beneficiaries.length;
|
|
const withEquipment = beneficiaries.filter(b => b.hasDevices).length;
|
|
const activeSubscriptions = beneficiaries.filter(b => b.subscription?.status === 'active').length;
|
|
|
|
const handleAddBeneficiary = () => {
|
|
router.push('/add-loved-one');
|
|
};
|
|
|
|
const handleViewBeneficiary = (id: number) => {
|
|
router.push(`/beneficiaries/${id}`);
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex h-64 items-center justify-center">
|
|
<LoadingSpinner size="lg" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<ErrorMessage
|
|
message={error}
|
|
onRetry={() => window.location.reload()}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="mb-6 4xl:mb-8">
|
|
<h1 className="text-2xl font-bold text-slate-900 lg:text-3xl 3xl:text-4xl 4xl:text-5xl">
|
|
Welcome{user?.firstName ? `, ${user.firstName}` : ''}
|
|
</h1>
|
|
<p className="mt-2 text-slate-600 lg:text-lg 4xl:text-xl">
|
|
Monitor your loved ones and manage their health sensors
|
|
</p>
|
|
</div>
|
|
|
|
{/* Summary Cards */}
|
|
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-3 3xl:grid-cols-4 4xl:gap-8">
|
|
<SummaryCard
|
|
title="Total Beneficiaries"
|
|
value={totalBeneficiaries}
|
|
icon="👥"
|
|
color="blue"
|
|
/>
|
|
<SummaryCard
|
|
title="With Equipment"
|
|
value={withEquipment}
|
|
icon="📡"
|
|
color="green"
|
|
/>
|
|
<SummaryCard
|
|
title="Active Subscriptions"
|
|
value={activeSubscriptions}
|
|
icon="✓"
|
|
color="purple"
|
|
/>
|
|
</div>
|
|
|
|
{/* Beneficiaries List */}
|
|
<div>
|
|
<div className="mb-4 flex items-center justify-between 4xl:mb-6">
|
|
<h2 className="text-xl font-semibold text-slate-900 3xl:text-2xl 4xl:text-3xl">Your Loved Ones</h2>
|
|
<button
|
|
onClick={handleAddBeneficiary}
|
|
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
|
>
|
|
+ Add Beneficiary
|
|
</button>
|
|
</div>
|
|
|
|
{beneficiaries.length === 0 ? (
|
|
<div className="rounded-lg border border-slate-200 bg-white p-12 text-center">
|
|
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-slate-100">
|
|
<span className="text-3xl">👤</span>
|
|
</div>
|
|
<h3 className="mb-2 text-lg font-semibold text-slate-900">No beneficiaries yet</h3>
|
|
<p className="mb-6 text-slate-600">
|
|
Add your first loved one to start monitoring their health and safety.
|
|
</p>
|
|
<button
|
|
onClick={handleAddBeneficiary}
|
|
className="rounded-lg bg-blue-600 px-6 py-3 font-medium text-white transition-colors hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
|
>
|
|
Add Your First Beneficiary
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 3xl:grid-cols-4 4xl:grid-cols-5 4xl:gap-6">
|
|
{beneficiaries.map((beneficiary) => (
|
|
<BeneficiaryCard
|
|
key={beneficiary.id}
|
|
beneficiary={beneficiary}
|
|
onClick={() => handleViewBeneficiary(beneficiary.id)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Summary Card Component
|
|
*/
|
|
interface SummaryCardProps {
|
|
title: string;
|
|
value: number;
|
|
icon: string;
|
|
color: 'blue' | 'green' | 'purple';
|
|
}
|
|
|
|
function SummaryCard({ title, value, icon, color }: SummaryCardProps) {
|
|
const colorClasses = {
|
|
blue: 'bg-blue-50 text-blue-600',
|
|
green: 'bg-green-50 text-green-600',
|
|
purple: 'bg-purple-50 text-purple-600',
|
|
};
|
|
|
|
return (
|
|
<div className="rounded-lg border border-slate-200 bg-white p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-slate-600">{title}</p>
|
|
<p className="mt-2 text-3xl font-bold text-slate-900">{value}</p>
|
|
</div>
|
|
<div className={`flex h-12 w-12 items-center justify-center rounded-lg ${colorClasses[color]}`}>
|
|
<span className="text-2xl">{icon}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Beneficiary Card Component
|
|
*/
|
|
interface BeneficiaryCardProps {
|
|
beneficiary: Beneficiary;
|
|
onClick: () => void;
|
|
}
|
|
|
|
function BeneficiaryCard({ beneficiary, onClick }: BeneficiaryCardProps) {
|
|
const getEquipmentStatusBadge = () => {
|
|
if (!beneficiary.equipmentStatus) {
|
|
return { text: 'No Equipment', color: 'bg-slate-100 text-slate-700' };
|
|
}
|
|
|
|
const statusMap: Record<string, { text: string; color: string }> = {
|
|
none: { text: 'No Equipment', color: 'bg-slate-100 text-slate-700' },
|
|
ordered: { text: 'Ordered', color: 'bg-blue-100 text-blue-700' },
|
|
shipped: { text: 'Shipped', color: 'bg-yellow-100 text-yellow-700' },
|
|
delivered: { text: 'Delivered', color: 'bg-green-100 text-green-700' },
|
|
active: { text: 'Active', color: 'bg-green-100 text-green-700' },
|
|
demo: { text: 'Demo', color: 'bg-purple-100 text-purple-700' },
|
|
};
|
|
|
|
return statusMap[beneficiary.equipmentStatus] || statusMap.none;
|
|
};
|
|
|
|
const getSubscriptionBadge = () => {
|
|
if (!beneficiary.subscription) {
|
|
return null;
|
|
}
|
|
|
|
const statusMap: Record<string, { text: string; color: string }> = {
|
|
active: { text: 'Active', color: 'bg-green-100 text-green-700' },
|
|
trial: { text: 'Trial', color: 'bg-blue-100 text-blue-700' },
|
|
expired: { text: 'Expired', color: 'bg-red-100 text-red-700' },
|
|
cancelled: { text: 'Cancelled', color: 'bg-slate-100 text-slate-700' },
|
|
};
|
|
|
|
const badge = statusMap[beneficiary.subscription.status];
|
|
if (!badge) return null;
|
|
|
|
return (
|
|
<span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${badge.color}`}>
|
|
{badge.text}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
const equipmentBadge = getEquipmentStatusBadge();
|
|
const subscriptionBadge = getSubscriptionBadge();
|
|
|
|
return (
|
|
<button
|
|
onClick={onClick}
|
|
className="group w-full rounded-lg border border-slate-200 bg-white p-6 text-left transition-all hover:border-blue-300 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-center space-x-3">
|
|
{beneficiary.avatar ? (
|
|
<img
|
|
src={beneficiary.avatar}
|
|
alt={beneficiary.displayName}
|
|
className="h-12 w-12 rounded-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-slate-100">
|
|
<span className="text-xl">👤</span>
|
|
</div>
|
|
)}
|
|
<div>
|
|
<h3 className="font-semibold text-slate-900 group-hover:text-blue-600">
|
|
{beneficiary.displayName}
|
|
</h3>
|
|
{beneficiary.email && (
|
|
<p className="text-sm text-slate-600">{beneficiary.email}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 flex flex-wrap gap-2">
|
|
<span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${equipmentBadge.color}`}>
|
|
{equipmentBadge.text}
|
|
</span>
|
|
{subscriptionBadge}
|
|
{beneficiary.role && (
|
|
<span className="inline-flex items-center rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-medium text-slate-700">
|
|
{beneficiary.role === 'custodian' ? 'Custodian' : beneficiary.role === 'guardian' ? 'Guardian' : 'Caretaker'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{beneficiary.address && (
|
|
<p className="mt-3 text-sm text-slate-600">📍 {beneficiary.address}</p>
|
|
)}
|
|
</button>
|
|
);
|
|
}
|