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:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user