Clinic — Patient Portal Dashboard
Patient portal home: a warm greeting header, a next-appointment hero card with a live countdown and check-in / join-video actions, a quick-actions grid, recent lab-result tiles with status badges, an active-prescriptions list with one-tap refills, and a care-team strip.
MCP
程式碼
:root {
--teal: #129c93;
--teal-d: #0c7a73;
--teal-700: #0a655f;
--teal-50: #e7f5f3;
--coral: #ff7a66;
--coral-soft: #ffe6df;
--ink: #16322f;
--ink-2: #3a534f;
--muted: #6b827e;
--bg: #f1f7f6;
--white: #ffffff;
--line: rgba(16, 50, 47, 0.1);
--line-2: rgba(16, 50, 47, 0.18);
--ok: #2f9e6f;
--warn: #d98a2b;
--pending: #6b7280;
--danger: #d4503e;
--font: "Inter", system-ui, -apple-system, sans-serif;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow-1: 0 1px 2px rgba(16, 50, 47, 0.05), 0 4px 14px rgba(16, 50, 47, 0.06);
--shadow-2: 0 16px 40px rgba(12, 122, 115, 0.16);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font);
background: var(--bg);
color: var(--ink);
-webkit-font-smoothing: antialiased;
line-height: 1.5;
}
/* ── App bar ── */
.appbar {
background: var(--white);
border-bottom: 1px solid var(--line);
padding: 14px 28px;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 20;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
color: var(--ink);
}
.brand-mark {
width: 34px;
height: 34px;
display: grid;
place-items: center;
border-radius: 10px;
background: linear-gradient(150deg, var(--teal), var(--teal-d));
color: #fff;
font-size: 1.1rem;
font-weight: 700;
}
.brand-name {
font-size: 1.1rem;
font-weight: 700;
letter-spacing: -0.01em;
}
.brand-name em {
font-style: normal;
color: var(--teal-d);
}
.appbar-actions {
display: flex;
align-items: center;
gap: 10px;
}
.icon-btn {
position: relative;
width: 40px;
height: 40px;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--white);
color: var(--ink-2);
font-size: 1.05rem;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.icon-btn:hover {
background: var(--teal-50);
border-color: var(--teal);
}
.icon-btn .dot {
position: absolute;
top: 8px;
right: 9px;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--coral);
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
background: var(--teal-d);
color: #fff;
font-weight: 700;
font-size: 0.82rem;
cursor: pointer;
}
/* ── Layout ── */
.portal {
max-width: 1020px;
margin: 0 auto;
padding: 28px 28px 56px;
display: flex;
flex-direction: column;
gap: 22px;
}
.greeting {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.eyebrow {
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--teal-d);
}
.greeting h1 {
font-size: 1.75rem;
font-weight: 800;
letter-spacing: -0.02em;
margin: 2px 0;
}
.sub {
color: var(--muted);
font-size: 0.95rem;
}
.health-pill {
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--white);
border: 1px solid var(--line);
border-radius: 999px;
padding: 8px 14px;
font-size: 0.84rem;
font-weight: 500;
color: var(--ink-2);
box-shadow: var(--shadow-1);
}
.hp-dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--ok);
box-shadow: 0 0 0 3px rgba(47, 158, 111, 0.18);
}
/* ── Next visit hero ── */
.next-visit {
background: linear-gradient(135deg, var(--teal-d), var(--teal-700));
color: #fff;
border-radius: var(--r-lg);
padding: 26px 28px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
flex-wrap: wrap;
box-shadow: var(--shadow-2);
position: relative;
overflow: hidden;
}
.next-visit::after {
content: "";
position: absolute;
right: -60px;
top: -60px;
width: 220px;
height: 220px;
background: radial-gradient(circle, rgba(255, 122, 102, 0.35), transparent 70%);
}
.next-visit.is-checked-in {
background: linear-gradient(135deg, #0f7a5a, #0b6149);
}
.nv-label {
font-size: 0.76rem;
text-transform: uppercase;
letter-spacing: 0.1em;
opacity: 0.82;
font-weight: 600;
}
.next-visit h2 {
font-size: 1.25rem;
font-weight: 700;
margin: 6px 0 4px;
}
.nv-when {
opacity: 0.88;
font-size: 0.92rem;
}
.nv-count {
margin-top: 12px;
display: inline-block;
background: rgba(255, 255, 255, 0.16);
border-radius: 999px;
padding: 5px 12px;
font-size: 0.82rem;
font-weight: 600;
}
.nv-actions {
display: flex;
flex-direction: column;
gap: 8px;
position: relative;
z-index: 1;
min-width: 160px;
}
/* ── Buttons ── */
.btn {
border: none;
border-radius: 11px;
padding: 11px 16px;
font: inherit;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: transform 0.12s, background 0.15s, opacity 0.15s;
}
.btn:active {
transform: translateY(1px);
}
.btn.primary {
background: var(--coral);
color: #fff;
}
.btn.primary:hover {
background: #ff8e7c;
}
.btn.primary:disabled {
background: rgba(255, 255, 255, 0.25);
cursor: default;
}
.btn.ghost {
background: rgba(255, 255, 255, 0.16);
color: #fff;
}
.btn.ghost:hover {
background: rgba(255, 255, 255, 0.26);
}
.btn.link {
background: transparent;
color: rgba(255, 255, 255, 0.85);
text-decoration: underline;
padding: 6px;
font-weight: 500;
}
.section-h {
font-size: 1.02rem;
font-weight: 700;
letter-spacing: -0.01em;
}
/* ── Quick actions ── */
.quick-grid {
margin-top: 12px;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.quick-card {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 18px 14px;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
font: inherit;
font-weight: 600;
font-size: 0.86rem;
color: var(--ink);
cursor: pointer;
text-align: center;
transition: transform 0.14s, box-shadow 0.14s, border-color 0.14s;
}
.quick-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-1);
border-color: var(--teal);
}
.qc-ic {
width: 44px;
height: 44px;
display: grid;
place-items: center;
border-radius: 12px;
background: var(--teal-50);
color: var(--teal-d);
font-size: 1.3rem;
}
/* ── Columns / panels ── */
.columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
}
.panel {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 18px 20px;
}
.panel-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.see-all {
font-size: 0.82rem;
color: var(--teal-d);
text-decoration: none;
font-weight: 600;
}
.see-all:hover {
text-decoration: underline;
}
.results,
.meds {
list-style: none;
display: flex;
flex-direction: column;
gap: 4px;
}
.result,
.med {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid var(--line);
}
.result:last-child,
.med:last-child {
border-bottom: none;
}
.r-name,
.m-name {
font-weight: 600;
font-size: 0.92rem;
}
.r-meta,
.m-meta {
font-size: 0.8rem;
color: var(--muted);
margin-top: 2px;
}
.badge {
font-size: 0.74rem;
font-weight: 700;
padding: 4px 10px;
border-radius: 999px;
white-space: nowrap;
}
.badge.ok {
background: rgba(47, 158, 111, 0.14);
color: var(--ok);
}
.badge.warn {
background: rgba(217, 138, 43, 0.16);
color: var(--warn);
}
.badge.pending {
background: rgba(107, 114, 128, 0.14);
color: var(--pending);
}
.refill-btn {
border: 1px solid var(--teal);
background: var(--white);
color: var(--teal-d);
border-radius: 9px;
padding: 7px 14px;
font: inherit;
font-weight: 600;
font-size: 0.82rem;
cursor: pointer;
transition: background 0.15s;
white-space: nowrap;
}
.refill-btn:hover {
background: var(--teal-50);
}
.refill-btn.is-done {
border-color: var(--ok);
background: rgba(47, 158, 111, 0.12);
color: var(--ok);
cursor: default;
}
/* ── Care team ── */
.care-team {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 18px 20px;
}
.team {
margin-top: 14px;
display: flex;
gap: 22px;
flex-wrap: wrap;
}
.member {
display: flex;
align-items: center;
gap: 11px;
}
.m-avatar {
width: 42px;
height: 42px;
border-radius: 50%;
display: grid;
place-items: center;
color: #fff;
font-weight: 700;
font-size: 0.82rem;
}
.a1 {
background: var(--teal-d);
}
.a2 {
background: #4f7cac;
}
.a3 {
background: var(--coral);
}
.m-info {
display: flex;
flex-direction: column;
}
.m-info strong {
font-size: 0.9rem;
}
.m-info small {
color: var(--muted);
font-size: 0.8rem;
}
/* ── Toast ── */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%);
background: var(--ink);
color: #fff;
padding: 13px 20px;
border-radius: 12px;
font-size: 0.9rem;
font-weight: 500;
box-shadow: var(--shadow-2);
z-index: 50;
max-width: 90vw;
}
@media (max-width: 760px) {
.quick-grid {
grid-template-columns: repeat(2, 1fr);
}
.columns {
grid-template-columns: 1fr;
}
.nv-actions {
width: 100%;
}
}// ── Toast ──────────────────────────────────────────────────────────────────
const toast = document.getElementById("toast");
function showToast(msg) {
toast.textContent = msg;
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 2600);
}
// ── Live countdown to the next appointment ───────────────────────────────────
// Illustrative: target is set ~22h ahead of an arbitrary fixed "now".
const countdownEl = document.getElementById("countdown");
let remaining = 22 * 3600 + 14 * 60; // seconds
function renderCountdown() {
if (remaining <= 0) {
countdownEl.textContent = "starting now";
return;
}
const h = Math.floor(remaining / 3600);
const m = Math.floor((remaining % 3600) / 60);
const s = remaining % 60;
countdownEl.textContent = `starts in ${h}h ${String(m).padStart(2, "0")}m ${String(s).padStart(2, "0")}s`;
remaining -= 1;
}
renderCountdown();
setInterval(renderCountdown, 1000);
// ── Next-visit actions ───────────────────────────────────────────────────────
const nextVisit = document.getElementById("nextVisit");
const checkinBtn = document.getElementById("checkinBtn");
nextVisit.addEventListener("click", (e) => {
const btn = e.target.closest("[data-action]");
if (!btn) return;
const action = btn.dataset.action;
if (action === "checkin") {
nextVisit.classList.add("is-checked-in");
checkinBtn.textContent = "Checked in ✓";
checkinBtn.disabled = true;
showToast("You're checked in — the clinic has been notified.");
} else if (action === "join") {
showToast("Opening secure video room…");
} else if (action === "reschedule") {
showToast("Reschedule: pick a new slot from the calendar.");
}
});
// ── Quick actions ────────────────────────────────────────────────────────────
const QUICK_MSG = {
book: "Booking flow: choose a specialty and time.",
refill: "Refill request: select a medication below.",
message: "New secure message to your care team.",
results: "Opening your full results history.",
};
document.querySelector(".quick-grid").addEventListener("click", (e) => {
const card = e.target.closest("[data-action]");
if (!card) return;
showToast(QUICK_MSG[card.dataset.action]);
});
// ── Prescription refills ─────────────────────────────────────────────────────
document.getElementById("meds").addEventListener("click", (e) => {
const btn = e.target.closest(".refill-btn");
if (!btn || btn.classList.contains("is-done")) return;
btn.classList.add("is-done");
btn.textContent = "Requested ✓";
showToast(`Refill requested for ${btn.dataset.med} — we'll text you when it's ready.`);
});<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Patient Portal · Northbridge Clinic</title>
</head>
<body>
<header class="appbar">
<a class="brand" href="#">
<span class="brand-mark" aria-hidden="true">✚</span>
<span class="brand-name">Northbridge <em>Health</em></span>
</a>
<div class="appbar-actions">
<button class="icon-btn" aria-label="Messages">✉<span class="dot"></span></button>
<button class="icon-btn" aria-label="Notifications">◔<span class="dot"></span></button>
<button class="avatar" aria-label="Account">AM</button>
</div>
</header>
<main class="portal">
<section class="greeting">
<div>
<p class="eyebrow" id="todayDate">Monday, 8 June</p>
<h1>Good morning, Amara</h1>
<p class="sub">Here's a snapshot of your care.</p>
</div>
<div class="health-pill">
<span class="hp-dot"></span> All caught up · no action needed
</div>
</section>
<section class="next-visit" id="nextVisit">
<div class="nv-left">
<p class="nv-label">Next appointment</p>
<h2>Dr. Lena Okafor · Cardiology</h2>
<p class="nv-when">Tue 9 June · 10:30 AM · Video consult</p>
<p class="nv-count" id="countdown">starts in —</p>
</div>
<div class="nv-actions">
<button class="btn primary" data-action="checkin" id="checkinBtn">Check in</button>
<button class="btn ghost" data-action="join">Join video</button>
<button class="btn link" data-action="reschedule">Reschedule</button>
</div>
</section>
<section class="quick">
<h3 class="section-h">Quick actions</h3>
<div class="quick-grid">
<button class="quick-card" data-action="book">
<span class="qc-ic">+</span><span>Book appointment</span>
</button>
<button class="quick-card" data-action="refill">
<span class="qc-ic">℞</span><span>Request a refill</span>
</button>
<button class="quick-card" data-action="message">
<span class="qc-ic">✉</span><span>Message care team</span>
</button>
<button class="quick-card" data-action="results">
<span class="qc-ic">⌗</span><span>View lab results</span>
</button>
</div>
</section>
<div class="columns">
<section class="panel">
<div class="panel-head">
<h3 class="section-h">Recent results</h3>
<a href="#" class="see-all">See all</a>
</div>
<ul class="results">
<li class="result">
<div>
<p class="r-name">Lipid panel</p>
<p class="r-meta">Collected 2 Jun</p>
</div>
<span class="badge ok">Normal</span>
</li>
<li class="result">
<div>
<p class="r-name">HbA1c</p>
<p class="r-meta">Collected 2 Jun</p>
</div>
<span class="badge warn">Review</span>
</li>
<li class="result">
<div>
<p class="r-name">Complete blood count</p>
<p class="r-meta">Ordered 7 Jun</p>
</div>
<span class="badge pending">Pending</span>
</li>
</ul>
</section>
<section class="panel">
<div class="panel-head">
<h3 class="section-h">Active prescriptions</h3>
<a href="#" class="see-all">Manage</a>
</div>
<ul class="meds" id="meds">
<li class="med">
<div>
<p class="m-name">Atorvastatin 20mg</p>
<p class="m-meta">1 tablet nightly · 4 refills left</p>
</div>
<button class="refill-btn" data-med="Atorvastatin">Refill</button>
</li>
<li class="med">
<div>
<p class="m-name">Metformin 500mg</p>
<p class="m-meta">1 tablet twice daily · 2 refills left</p>
</div>
<button class="refill-btn" data-med="Metformin">Refill</button>
</li>
<li class="med">
<div>
<p class="m-name">Lisinopril 10mg</p>
<p class="m-meta">1 tablet daily · 0 refills left</p>
</div>
<button class="refill-btn" data-med="Lisinopril">Refill</button>
</li>
</ul>
</section>
</div>
<section class="care-team">
<h3 class="section-h">Your care team</h3>
<div class="team">
<div class="member"><span class="m-avatar a1">LO</span><span class="m-info"><strong>Dr. Lena Okafor</strong><small>Cardiology</small></span></div>
<div class="member"><span class="m-avatar a2">RP</span><span class="m-info"><strong>Dr. Ravi Patel</strong><small>Primary care</small></span></div>
<div class="member"><span class="m-avatar a3">SN</span><span class="m-info"><strong>Sara Nguyen, RN</strong><small>Care coordinator</small></span></div>
</div>
</section>
</main>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Patient Portal Dashboard
The home a patient lands on after signing in. A calm, clinical-white layout in teal and soft coral keeps the next visit front and centre: a hero card shows the upcoming appointment with a live countdown, plus check-in and join-video actions that update in place. A quick-actions grid covers the common jobs — book, refill, message, view results. Below, recent lab results appear as tiles with normal / review / pending status badges, active prescriptions list with one-tap refill requests, and the care team is shown as an avatar strip. All interactions are vanilla JS.
Illustrative UI only — not intended for real medical use.