Sergei 5b04765b0d Add responsive design support for 768px to 4K screens
- 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>
2026-02-01 11:34:33 -08:00

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>
);
}