Add login-page site gate and refresh marketing site for complex-care positioning.

Replace HTTP Basic Auth with a branded /login flow and signed session cookie, using Next.js 16's proxy convention for route protection.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 02:21:58 -04:00
co-authored by Cursor
parent d521c66903
commit b0636ba5ce
18 changed files with 877 additions and 252 deletions
+54 -11
View File
@@ -4,6 +4,7 @@ import { useState } from "react";
import {
CalendarDays,
Camera,
Check,
ContactRound,
Home,
MessageCircle,
@@ -21,6 +22,11 @@ const features = [
title: "One profile for the person you care for",
description:
"Visits, medications, reminders, and questions live together under the person they belong to.",
points: [
"Switch between everyone you care for",
"One home for the whole picture",
"Nothing scattered across apps",
],
image: "/screenshots/home.png",
alt: "Medscribe home dashboard with record visit, search, medications, and ask shortcuts",
},
@@ -31,6 +37,11 @@ const features = [
title: "The context behind the handoff",
description:
"Conditions, medication changes, visit summaries, and caregiver notes stay close enough to become useful when time is short.",
points: [
"Baseline, allergies, and conditions up front",
"Time-critical medications flagged",
"A source trail behind each fact",
],
image: "/screenshots/emergency-record.png",
alt: "Medscribe emergency record with critical care context",
},
@@ -41,6 +52,11 @@ const features = [
title: "Visit recordings that refresh the record",
description:
"A visit can become searchable context, so a medication change or follow-up instruction does not depend on memory alone.",
points: [
"Consent-gated, on-device recording",
"Searchable transcript and summary",
"Follow-ups captured, not forgotten",
],
image: "/screenshots/visit-timeline.png",
alt: "Medscribe visit timeline with appointment cards",
},
@@ -51,6 +67,11 @@ const features = [
title: "Medication OCR, reminders, and history",
description:
"Prescription labels, reminders, refill alerts, and dose history help the family see what is supposed to happen and what actually happened.",
points: [
"Scan labels with the camera",
"Reminders and refill alerts",
"Dose history at a glance",
],
image: "/screenshots/medications.png",
alt: "Medscribe medications screen with active medications",
},
@@ -61,6 +82,11 @@ const features = [
title: "Medication questions grounded in the record",
description:
"Plain-language questions can draw from the person's record and bundled FDA facts, while keeping diagnosis and dosing with clinicians.",
points: [
"Plain-language answers",
"Grounded in the record and FDA facts",
"Defers dosing and diagnosis to clinicians",
],
image: "/screenshots/chat.png",
alt: "Medscribe Ask chat answering a question about blood pressure medications",
},
@@ -71,6 +97,11 @@ const features = [
title: "Private by architecture, not policy",
description:
"The sensitive work happens on device, so the family record is not another cloud inbox waiting to be mined.",
points: [
"On-device AI, no cloud processing",
"No inbox to breach or mine",
"You control what is ever shared",
],
image: "/screenshots/settings.png",
alt: "Medscribe settings showing on-device AI models and privacy notice",
},
@@ -81,8 +112,8 @@ export function FeatureShowcase() {
const active = features.find((f) => f.id === activeId) ?? features[0];
return (
<div className="grid items-start gap-5 lg:grid-cols-[minmax(0,1fr)_minmax(170px,220px)] lg:gap-10">
<div className="space-y-3">
<div className="grid items-center gap-8 lg:grid-cols-[minmax(0,1fr)_300px] lg:gap-12">
<div className="flex flex-col gap-5">
<div className="flex flex-wrap gap-2">
{features.map((feature) => {
const Icon = feature.icon;
@@ -96,7 +127,7 @@ export function FeatureShowcase() {
className={cn(
"inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm font-medium transition-all",
isActive
? "border-primary bg-primary text-white shadow-sm"
? "border-primary-button bg-primary-button text-white shadow-sm"
: "border-border-soft bg-surface text-muted-foreground hover:border-primary/20 hover:text-foreground",
)}
>
@@ -107,22 +138,34 @@ export function FeatureShowcase() {
})}
</div>
<div className="space-y-1.5">
<h3 className="font-serif text-2xl font-semibold tracking-tight text-foreground">
<div className="space-y-2">
<h3 className="font-serif text-2xl font-semibold tracking-tight text-foreground sm:text-3xl">
{active.title}
</h3>
<p className="max-w-xl text-sm leading-relaxed text-muted-foreground sm:text-base">
{active.description}
</p>
</div>
<ul className="grid gap-x-5 gap-y-2.5 sm:grid-cols-2">
{active.points.map((point) => (
<li
key={point}
className="flex items-start gap-2 text-sm leading-snug text-foreground"
>
<span className="mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full bg-primary-light">
<Check className="size-3 text-primary" />
</span>
<span>{point}</span>
</li>
))}
</ul>
</div>
<div className="flex w-full max-w-[220px] items-start justify-center rounded-lg bg-surface-soft/70 p-4 shadow-[0_12px_38px_rgba(15,23,42,0.08)] lg:-mt-16 lg:justify-self-end">
<PhoneMockup
src={active.image}
alt={active.alt}
width={146}
/>
<div className="justify-self-center lg:justify-self-end">
<div className="flex items-center justify-center rounded-2xl border border-border-soft bg-gradient-to-b from-surface-soft to-primary-light/30 p-6 shadow-[0_18px_45px_rgba(15,23,42,0.10)]">
<PhoneMockup src={active.image} alt={active.alt} width={188} />
</div>
</div>
</div>
);
+113
View File
@@ -0,0 +1,113 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Lock } from "lucide-react";
import { MedscribeLogo } from "~/components/medscribe-logo";
import { ScreenBackground } from "~/components/screen-background";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
type LoginFormProps = {
redirectTo: string;
};
export function LoginForm({ redirectTo }: LoginFormProps) {
const router = useRouter();
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
setError(null);
setIsSubmitting(true);
try {
const response = await fetch("/api/auth", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password }),
});
if (!response.ok) {
setError("Incorrect password. Try again.");
return;
}
router.replace(redirectTo);
router.refresh();
} catch {
setError("Something went wrong. Please try again.");
} finally {
setIsSubmitting(false);
}
}
return (
<div className="relative flex min-h-screen items-center justify-center px-4 py-16">
<ScreenBackground />
<Card className="w-full max-w-md border-border-soft/80 bg-surface/95 shadow-[0_20px_50px_rgba(15,23,42,0.12)] backdrop-blur-sm">
<CardHeader className="items-center space-y-4 text-center">
<MedscribeLogo className="h-8 w-[100px] text-foreground" />
<div className="space-y-2">
<CardTitle className="font-serif text-2xl">
Enter site password
</CardTitle>
<CardDescription>
This preview site is password protected.
</CardDescription>
</div>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<label htmlFor="password" className="sr-only">
Password
</label>
<div className="relative">
<Lock className="pointer-events-none absolute top-1/2 left-4 size-4 -translate-y-1/2 text-muted-foreground" />
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
autoFocus
required
value={password}
onChange={(event) => setPassword(event.target.value)}
placeholder="Password"
className="h-12 w-full rounded-[14px] border border-border-soft bg-canvas pr-4 pl-11 text-sm text-foreground outline-none transition-colors placeholder:text-muted-foreground focus:border-primary/40 focus:ring-2 focus:ring-primary/20"
/>
</div>
</div>
{error ? (
<p className="text-sm text-red-600 dark:text-red-400" role="alert">
{error}
</p>
) : null}
<Button
type="submit"
size="lg"
className="w-full"
disabled={isSubmitting || password.length === 0}
>
{isSubmitting ? "Checking..." : "Continue"}
</Button>
</form>
</CardContent>
</Card>
</div>
);
}
+14 -2
View File
@@ -1,12 +1,24 @@
"use client";
import type { ComponentProps } from "react";
import { Printer } from "lucide-react";
import { Button } from "~/components/ui/button";
import { cn } from "~/lib/utils";
export function PrintButton() {
type PrintButtonProps = {
className?: string;
size?: ComponentProps<typeof Button>["size"];
};
export function PrintButton({ className, size }: PrintButtonProps) {
return (
<Button type="button" onClick={() => window.print()} className="print-hidden">
<Button
type="button"
size={size}
onClick={() => window.print()}
className={cn("print-hidden", className)}
>
<Printer className="size-4" />
Print PDF
</Button>
+3 -3
View File
@@ -1,12 +1,12 @@
export function ScreenBackground() {
return (
<div className="pointer-events-none fixed inset-0 -z-10 overflow-hidden">
<div className="pointer-events-none fixed inset-0 -z-10 overflow-hidden print:hidden">
<div
className="absolute inset-0 bg-canvas"
style={{
backgroundImage: `
linear-gradient(to right, rgba(15,23,42,0.055) 1px, transparent 1px),
linear-gradient(to bottom, rgba(15,23,42,0.055) 1px, transparent 1px)
linear-gradient(to right, var(--grid-line) 1px, transparent 1px),
linear-gradient(to bottom, var(--grid-line) 1px, transparent 1px)
`,
backgroundSize: "28px 28px",
}}
+117 -32
View File
@@ -1,48 +1,133 @@
"use client";
import { useState } from "react";
import { FileText, Home, Menu, X } from "lucide-react";
import Link from "next/link";
import { FileText } from "lucide-react";
import { usePathname } from "next/navigation";
import { MedscribeLogo } from "~/components/medscribe-logo";
import { Button } from "~/components/ui/button";
import { cn } from "~/lib/utils";
const navLinks = [
{ href: "/#features", label: "Features" },
{ href: "/#privacy", label: "Privacy" },
{ href: "/#impact", label: "Impact" },
{ href: "/executive-summary", label: "Summary" },
{ href: "/", label: "About", icon: Home },
{ href: "/executive-summary", label: "Summary", icon: FileText },
];
export function SiteHeader() {
const pathname = usePathname();
const [isOpen, setIsOpen] = useState(false);
return (
<header className="print-hidden sticky top-3 z-50 px-3 sm:px-6">
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between gap-4 rounded-full border border-border-soft/80 bg-canvas/82 px-4 shadow-[0_10px_30px_rgba(15,23,42,0.08)] backdrop-blur-md sm:px-5">
<Link
href="/"
className="flex items-center text-foreground transition-opacity hover:opacity-80"
aria-label="Medscribe home"
<div className="relative mx-auto max-w-5xl">
<div className="flex h-14 items-center justify-between gap-4 rounded-full border border-border-soft/80 bg-canvas/82 px-4 shadow-[0_10px_30px_rgba(15,23,42,0.08)] backdrop-blur-md sm:px-5">
<Link
href="/"
className="flex items-center text-foreground transition-opacity hover:opacity-80"
aria-label="Medscribe home"
onClick={() => setIsOpen(false)}
>
<MedscribeLogo className="h-7 w-[88px]" />
</Link>
{/* Desktop nav */}
<nav className="hidden items-center gap-6 text-sm sm:flex">
{navLinks.map((link) => {
const isActive = link.href === pathname;
return (
<Link
key={link.href}
href={link.href}
aria-current={isActive ? "page" : undefined}
className={cn(
"relative py-1 transition-colors",
isActive
? "text-foreground"
: "text-muted-foreground hover:text-foreground",
)}
>
{link.label}
<span
className={cn(
"absolute inset-x-0 -bottom-0.5 h-0.5 origin-center rounded-full bg-primary transition-transform duration-200",
isActive ? "scale-x-100" : "scale-x-0",
)}
aria-hidden
/>
</Link>
);
})}
</nav>
{/* Mobile menu toggle */}
<button
type="button"
onClick={() => setIsOpen((v) => !v)}
className="relative size-6 text-muted-foreground transition-colors hover:text-foreground focus:outline-none sm:hidden"
aria-label={isOpen ? "Close menu" : "Open menu"}
aria-expanded={isOpen}
>
<Menu
className={cn(
"absolute inset-0 transition-opacity duration-200",
isOpen ? "opacity-0" : "opacity-100",
)}
/>
<X
className={cn(
"absolute inset-0 transition-opacity duration-200",
isOpen ? "opacity-100" : "opacity-0",
)}
/>
</button>
</div>
{/* Mobile dropdown */}
<div
className={cn(
"absolute inset-x-0 top-16 overflow-hidden rounded-2xl border border-border-soft/80 bg-canvas/95 shadow-[0_10px_30px_rgba(15,23,42,0.12)] backdrop-blur-md transition-all duration-200 sm:hidden",
isOpen
? "pointer-events-auto opacity-100"
: "pointer-events-none -translate-y-1 opacity-0",
)}
>
<MedscribeLogo className="h-7 w-[88px]" />
</Link>
<nav className="flex flex-col p-2">
{navLinks.map((link) => {
const isActive = link.href === pathname;
const Icon = link.icon;
<nav className="hidden items-center gap-5 text-sm text-muted-foreground md:flex">
{navLinks.map((link) => (
<a
key={link.href}
href={link.href}
className="transition-colors hover:text-foreground"
>
{link.label}
</a>
))}
</nav>
<Button asChild size="sm" className="h-8 rounded-full px-3">
<a href="/executive-summary">
<FileText className="size-4" />
<span className="hidden sm:inline">Executive summary</span>
<span className="sm:hidden">Summary</span>
</a>
</Button>
return (
<Link
key={link.href}
href={link.href}
aria-current={isActive ? "page" : undefined}
onClick={() => setIsOpen(false)}
className={cn(
"flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition-colors",
isActive
? "bg-primary-light/60 text-foreground"
: "text-muted-foreground hover:bg-surface-soft hover:text-foreground",
)}
>
<Icon className="size-4" />
{link.label}
</Link>
);
})}
</nav>
</div>
</div>
{/* Backdrop */}
<div
className={cn(
"fixed inset-0 -z-10 bg-canvas/30 backdrop-blur-sm transition-opacity duration-200 sm:hidden",
isOpen ? "opacity-100" : "pointer-events-none opacity-0",
)}
onClick={() => setIsOpen(false)}
aria-hidden
/>
</header>
);
}
+2 -1
View File
@@ -9,7 +9,8 @@ const buttonVariants = cva(
{
variants: {
variant: {
default: "bg-primary text-white shadow-sm hover:bg-primary/90",
default:
"bg-primary-button text-white shadow-sm hover:bg-primary-button/90",
outline:
"border border-border-soft bg-surface text-foreground hover:bg-surface-soft",
secondary: