initial commit

This commit is contained in:
2026-06-24 17:02:04 -04:00
commit d521c66903
50 changed files with 2519 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
"use client";
import { useState } from "react";
import {
CalendarDays,
Camera,
ContactRound,
Home,
MessageCircle,
Shield,
} from "lucide-react";
import { PhoneMockup } from "~/components/phone-mockup";
import { cn } from "~/lib/utils";
const features = [
{
id: "home",
label: "Profile",
icon: Home,
title: "One profile for the person you care for",
description:
"Visits, medications, reminders, and questions live together under the person they belong to.",
image: "/screenshots/home.png",
alt: "Medscribe home dashboard with record visit, search, medications, and ask shortcuts",
},
{
id: "context",
label: "Context",
icon: ContactRound,
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.",
image: "/screenshots/emergency-record.png",
alt: "Medscribe emergency record with critical care context",
},
{
id: "visits",
label: "Visits",
icon: CalendarDays,
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.",
image: "/screenshots/visit-timeline.png",
alt: "Medscribe visit timeline with appointment cards",
},
{
id: "meds",
label: "Meds",
icon: Camera,
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.",
image: "/screenshots/medications.png",
alt: "Medscribe medications screen with active medications",
},
{
id: "ask",
label: "Ask",
icon: MessageCircle,
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.",
image: "/screenshots/chat.png",
alt: "Medscribe Ask chat answering a question about blood pressure medications",
},
{
id: "privacy",
label: "Privacy",
icon: Shield,
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.",
image: "/screenshots/settings.png",
alt: "Medscribe settings showing on-device AI models and privacy notice",
},
] as const;
export function FeatureShowcase() {
const [activeId, setActiveId] = useState<(typeof features)[number]["id"]>("home");
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="flex flex-wrap gap-2">
{features.map((feature) => {
const Icon = feature.icon;
const isActive = feature.id === activeId;
return (
<button
key={feature.id}
type="button"
onClick={() => setActiveId(feature.id)}
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-border-soft bg-surface text-muted-foreground hover:border-primary/20 hover:text-foreground",
)}
>
<Icon className="size-4" />
{feature.label}
</button>
);
})}
</div>
<div className="space-y-1.5">
<h3 className="font-serif text-2xl font-semibold tracking-tight text-foreground">
{active.title}
</h3>
<p className="max-w-xl text-sm leading-relaxed text-muted-foreground sm:text-base">
{active.description}
</p>
</div>
</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>
</div>
);
}
+21
View File
@@ -0,0 +1,21 @@
import { cn } from "~/lib/utils";
interface MedscribeLogoProps {
className?: string;
}
export function MedscribeLogo({ className }: MedscribeLogoProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 162"
role="img"
aria-label="Medscribe"
className={cn("fill-current", className)}
>
<g transform="translate(0 162) scale(0.1 -0.1)">
<path d="M1005 1598 c-18 -16 -66 -122 -163 -358 l-138 -335 -328 -5 -328 -5 -24 -28 c-31 -36 -31 -78 0 -114 l24 -28 370 0 c370 0 370 0 394 22 14 14 62 115 118 253 68 165 97 226 102 213 7 -17 238 -1137 238 -1153 0 -19 55 -60 81 -60 56 0 73 29 215 375 75 182 141 336 146 343 7 8 45 12 122 12 132 0 256 19 349 53 37 13 67 21 67 17 0 -16 -71 -247 -125 -406 -34 -99 -56 -178 -52 -188 12 -32 158 -76 175 -52 4 6 19 38 32 71 162 413 559 899 721 883 53 -5 74 -44 73 -136 -1 -105 -21 -185 -121 -484 -48 -142 -83 -263 -80 -273 8 -20 57 -41 118 -50 l46 -7 60 124 c80 164 300 494 397 596 81 86 160 103 207 44 32 -41 22 -152 -27 -307 -36 -109 -39 -130 -39 -230 0 -102 2 -114 27 -156 33 -56 63 -77 134 -92 136 -28 299 54 420 214 l39 50 16 -55 c23 -75 59 -134 114 -185 57 -53 121 -81 208 -90 366 -39 585 238 512 650 -8 46 -42 169 -75 273 -33 104 -65 219 -71 254 -11 61 -7 170 7 234 5 21 1 30 -18 42 -33 22 -81 5 -107 -38 -23 -37 -51 -136 -51 -181 0 -42 -93 -207 -189 -335 -105 -142 -160 -198 -198 -206 -50 -10 -119 -83 -137 -145 -9 -29 -16 -63 -16 -77 0 -56 -161 -247 -235 -278 -51 -21 -121 -18 -145 6 -46 46 -40 127 29 354 60 198 71 285 47 368 -28 97 -95 137 -211 125 -133 -14 -266 -123 -406 -335 -49 -74 -89 -132 -89 -129 0 3 5 18 11 34 24 63 59 224 68 308 17 166 -17 282 -93 316 -23 10 -57 19 -77 19 -165 1 -401 -196 -627 -524 -34 -49 -62 -86 -62 -82 0 3 16 68 36 144 60 233 51 292 -48 292 -59 0 -117 -27 -149 -70 -62 -80 -86 -103 -131 -126 -81 -41 -169 -56 -365 -63 -178 -6 -183 -7 -207 -31 -14 -14 -64 -123 -117 -254 -68 -165 -95 -223 -101 -210 -4 11 -59 275 -123 588 -64 313 -120 577 -126 587 -23 43 -84 51 -124 17z m3892 -999 c5 -142 4 -164 -16 -229 -17 -55 -33 -84 -68 -121 -65 -70 -118 -93 -198 -86 -144 11 -245 126 -245 276 0 71 17 116 56 150 44 39 79 40 132 1 l42 -30 17 27 c10 16 18 46 18 71 0 36 -5 48 -31 70 -17 15 -39 32 -49 37 -16 10 -12 19 46 91 36 43 96 126 134 183 l69 103 43 -193 c39 -170 45 -214 50 -350z" />
</g>
</svg>
);
}
+59
View File
@@ -0,0 +1,59 @@
"use client";
import Image from "next/image";
import { IPhoneMockup } from "react-device-mockup";
import { cn } from "~/lib/utils";
interface PhoneMockupProps {
src: string;
alt: string;
className?: string;
priority?: boolean;
/** Rendered mockup width in px. @default 280 */
width?: number;
/** Use "simulator" only for captures that already include device chrome. */
captureMode?: "simulator" | "raw";
}
export function PhoneMockup({
src,
alt,
className,
priority,
width = 280,
captureMode = "raw",
}: PhoneMockupProps) {
const isSimulatorCapture = captureMode === "simulator";
return (
<div className={cn("mx-auto w-fit drop-shadow-[0_18px_45px_rgba(15,23,42,0.14)]", className)}>
<IPhoneMockup
screenWidth={width}
screenType="island"
frameColor="#111827"
statusbarColor="#f8fafc"
hideStatusBar
hideNavBar
transparentNavBar
>
<div className="relative h-full w-full overflow-hidden bg-surface">
<Image
src={src}
alt={alt}
width={1206}
height={2622}
priority={priority}
unoptimized
className={cn(
"h-full w-full object-cover",
isSimulatorCapture
? "scale-[1.22] object-[50%_58%]"
: "object-top",
)}
/>
</div>
</IPhoneMockup>
</div>
);
}
+14
View File
@@ -0,0 +1,14 @@
"use client";
import { Printer } from "lucide-react";
import { Button } from "~/components/ui/button";
export function PrintButton() {
return (
<Button type="button" onClick={() => window.print()} className="print-hidden">
<Printer className="size-4" />
Print PDF
</Button>
);
}
+17
View File
@@ -0,0 +1,17 @@
export function ScreenBackground() {
return (
<div className="pointer-events-none fixed inset-0 -z-10 overflow-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)
`,
backgroundSize: "28px 28px",
}}
/>
<div className="absolute inset-x-0 top-0 h-[260px] bg-gradient-to-b from-primary-light/24 to-transparent" />
</div>
);
}
+7
View File
@@ -0,0 +1,7 @@
export function SectionLabel({ children }: { children: React.ReactNode }) {
return (
<p className="text-xs font-semibold tracking-widest text-muted-foreground uppercase">
{children}
</p>
);
}
+25
View File
@@ -0,0 +1,25 @@
export function SiteFooter() {
return (
<footer className="print-hidden border-t border-border-soft bg-surface/70">
<div className="mx-auto flex max-w-6xl flex-col gap-4 px-4 py-10 text-sm text-muted-foreground sm:px-6 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="font-serif text-base font-semibold text-foreground">Medscribe</p>
<p className="mt-1 max-w-md leading-relaxed">
Be known when care gets complicated.
</p>
</div>
<div>
<p>
Built by{" "}
<a
href="https://soconnor.dev"
className="text-foreground underline-offset-4 hover:underline"
>
Sean O&apos;Connor
</a>
</p>
</div>
</div>
</footer>
);
}
+48
View File
@@ -0,0 +1,48 @@
import Link from "next/link";
import { FileText } from "lucide-react";
import { MedscribeLogo } from "~/components/medscribe-logo";
import { Button } from "~/components/ui/button";
const navLinks = [
{ href: "/#features", label: "Features" },
{ href: "/#privacy", label: "Privacy" },
{ href: "/#impact", label: "Impact" },
{ href: "/executive-summary", label: "Summary" },
];
export function SiteHeader() {
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"
>
<MedscribeLogo className="h-7 w-[88px]" />
</Link>
<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>
</div>
</header>
);
}
+30
View File
@@ -0,0 +1,30 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "~/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
{
variants: {
variant: {
default: "bg-primary-light text-primary",
outline: "border border-border-soft bg-surface text-muted-foreground",
success: "bg-success-light text-success",
},
},
defaultVariants: {
variant: "default",
},
},
);
function Badge({
className,
variant,
...props
}: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
return <span className={cn(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants };
+51
View File
@@ -0,0 +1,51 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "~/lib/utils";
const buttonVariants = cva(
"inline-flex shrink-0 items-center justify-center gap-2 rounded-[14px] text-sm font-semibold whitespace-nowrap transition-all outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-white shadow-sm hover:bg-primary/90",
outline:
"border border-border-soft bg-surface text-foreground hover:bg-surface-soft",
secondary:
"bg-primary-light text-primary hover:bg-primary-light/80",
ghost: "text-muted-foreground hover:bg-surface-soft hover:text-foreground",
},
size: {
default: "h-11 px-5",
sm: "h-9 px-4 text-xs",
lg: "h-12 px-6 text-base",
icon: "size-11",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };
+40
View File
@@ -0,0 +1,40 @@
import * as React from "react";
import { cn } from "~/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"rounded-lg border border-border-soft bg-surface text-foreground shadow-[0_1px_4px_rgba(0,0,0,0.05)]",
className,
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return <div className={cn("flex flex-col gap-1.5 p-5", className)} {...props} />;
}
function CardTitle({ className, ...props }: React.ComponentProps<"h3">) {
return (
<h3
className={cn("font-serif text-lg font-semibold leading-tight", className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p className={cn("text-sm leading-relaxed text-muted-foreground", className)} {...props} />
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return <div className={cn("px-5 pb-5", className)} {...props} />;
}
export { Card, CardHeader, CardTitle, CardDescription, CardContent };