Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
.dsc-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}

.dsc-card {
background: #4fa0c8 !important;
border: 1px solid var(--dsc-accent-color);
border-radius: 12px;
padding: 1.25rem 1.25rem 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
position: relative;
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
cursor: default;
}

.dsc-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
border-radius: 12px 12px 0 0;
background: var(--dsc-accent-color);
opacity: 0.85;
}

.dsc-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px #4fa0c8;
}
.dsc-card-header {
display: flex;
align-items: center;
gap: 0.6rem;
}

.dsc-icon-wrap {
width: 34px;
height: 34px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
background: var(--dsc-accent-bg);
color: var(--dsc-accent-color);
flex-shrink: 0;
}

.dsc-label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #ffffff;
}

.dsc-value {
font-size: 2rem;
font-weight: 700;
color: #ffffff;
line-height: 1;
margin-top: 0.25rem;
letter-spacing: -0.02em;
}

.dsc-suffix {
font-size: 1rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.75);
margin-left: 2px;
}
.dsc-description {
font-size: 0.72rem;
color: rgba(255, 255, 255, 0.7);
margin: 0;
}

.dsc-card.accent {
--dsc-accent-color: white;
--dsc-accent-bg: #4fa0c8;
}

.dsc-card.dsc-skeleton {
pointer-events: none;
}

.dsc-skeleton-icon {
width: 34px;
height: 34px;
border-radius: 8px;
background: #4fa0c8 !important;
animation: dsc-pulse 1.4s ease-in-out infinite;
}

.dsc-skeleton-line {
height: 10px;
border-radius: 4px;
background: #4fa0c8 !important;
animation: dsc-pulse 1.4s ease-in-out infinite;
}

.dsc-skeleton-line.short { width: 50%; }
.dsc-skeleton-line.long { width: 35%; height: 28px; margin-top: 0.5rem; }

@keyframes dsc-pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 0.9; }
}

.dsc-error {
padding: 1rem 1.25rem;
background: rgba(251, 113, 133, 0.1);
border: 1px solid rgba(251, 113, 133, 0.25);
border-radius: 10px;
color: #fb7185;
font-size: 0.85rem;
margin-bottom: 1.5rem;
}

@media (max-width: 640px) {
.dsc-grid {
grid-template-columns: repeat(2, 1fr);
}

.dsc-value {
font-size: 1.6rem;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { motion } from "framer-motion";
import {
Users,
UserCheck,
Stethoscope,
ClipboardList,
CheckCircle2,
Clock,
TrendingUp,
} from "lucide-react";
import "./DashboardSummaryCards.css"

const CARD_CONFIG = [
{
key: "totalPatients",
label: "Total Patients",
icon: Stethoscope,
accent: "accent-teal",
description: "All registered patients",
},
{
key: "totalActivePatients",
label: "Active Patients",
icon: UserCheck,
accent: "accent-green",
description: "Currently active",
},
{
key: "totalStaff",
label: "Care Staff",
icon: Users,
accent: "accent-blue",
description: "Organisation staff",
},
{
key: "totalTasks",
label: "Total Tasks",
icon: ClipboardList,
accent: "accent-purple",
description: "All assigned tasks",
},
{
key: "completedTasks",
label: "Completed Tasks",
icon: CheckCircle2,
accent: "accent-emerald",
description: "Successfully completed",
},
{
key: "pendingTasks",
label: "Pending Tasks",
icon: Clock,
accent: "accent-amber",
description: "Awaiting completion",
},
{
key: "taskCompletionRate",
label: "Completion Rate",
icon: TrendingUp,
accent: "accent-rose",
description: "Tasks completed (%)",
suffix: "%",
},
];

export default function DashboardSummaryCards({ summary, loading, error }) {
if (loading) {
return (
<div className="dsc-grid">
{CARD_CONFIG.map((card) => (
<div key={card.key} className="dsc-card dsc-skeleton">
<div className="dsc-skeleton-icon" />
<div className="dsc-skeleton-line short" />
<div className="dsc-skeleton-line long" />
</div>
))}
</div>
);
}

if (error) {
return (
<div className="dsc-error">
<p>⚠ Could not load summary: {error}</p>
</div>
);
}

if (!summary) return null;

return (
<div className="dsc-grid">
{CARD_CONFIG.map((card, i) => {
const Icon = card.icon;
const value = summary[card.key] ?? "—";

return (
<motion.div
key={card.key}
className={`dsc-card ${card.accent}`}
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.35, delay: i * 0.06 }}
>
<div className="dsc-card-header">
<div className="dsc-icon-wrap">
<Icon size={18} />
</div>
<span className="dsc-label">{card.label}</span>
</div>
<div className="dsc-value">
{value}
{card.suffix && value !== "—" && (
<span className="dsc-suffix">{card.suffix}</span>
)}
</div>
<p className="dsc-description">{card.description}</p>
</motion.div>
);
})}
</div>
);
}
52 changes: 46 additions & 6 deletions guardian-admin-dashboard/src/pages/DashboardHome.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,48 @@
import { useEffect, useState } from "react";
import { motion } from "framer-motion";
import StatCard from "../components/dashboard/StatCard";
import { DASHBOARD_STATS } from "../utils/constants";
import { ArrowRight, Building2, Users, ShieldAlert, FileBarChart2 } from "lucide-react";
import { Link } from "react-router-dom";
import axios from "axios";
import DashboardSummaryCards from "../components/dashboard/DashboardSummaryCards";

export default function DashboardHome() {
const [summary, setSummary] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");

useEffect(() => {
const fetchSummary = async () => {
try {
const token = localStorage.getItem("guardian_admin_token");

const response = await axios.get(
"https://guardian-backend-git-fix-cors-patelrudra2306-5873s-projects.vercel.app/api/v1/admin/dashboard-summary",
{
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
Accept: "application/json",
},
withCredentials: false,
}
);
setSummary(response.data);
} catch (err) {
setError(
err?.response?.data?.message ||
err?.message ||
"Failed to load dashboard summary."
);
} finally {
setLoading(false);
}
};

fetchSummary();
}, []);

return (
<div className="dashboard-home">
<motion.section
Expand Down Expand Up @@ -45,11 +83,17 @@ export default function DashboardHome() {
</div>
</motion.section>

<section className="stats-grid">
<DashboardSummaryCards
summary={summary}
loading={loading}
error={error}
/>

{/* <section className="stats-grid">
{DASHBOARD_STATS.map((item) => (
<StatCard key={item.title} {...item} />
))}
</section>
</section> */}

<section className="dashboard-panels">
<motion.article
Expand All @@ -74,23 +118,19 @@ export default function DashboardHome() {
transition={{ duration: 0.45, delay: 0.12 }}
>
<h3>Upcoming modules</h3>

<div className="mini-module-list">
<div className="mini-module-item">
<ShieldAlert size={18} />
<span>Alerts & Monitoring</span>
</div>

<div className="mini-module-item">
<Users size={18} />
<span>Staff Administration</span>
</div>

<div className="mini-module-item">
<Building2 size={18} />
<span>Organisation Workflows</span>
</div>

<div className="mini-module-item">
<FileBarChart2 size={18} />
<span>Reports & Analytics</span>
Expand Down
3 changes: 2 additions & 1 deletion guardian-admin-dashboard/src/utils/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ export function setAuthToken(token) {
}

export function getAuthToken() {
return localStorage.getItem(STORAGE_KEYS.token);
const token = localStorage.getItem(STORAGE_KEYS.token);
return token;
}

export function removeAuthToken() {
Expand Down
Loading