initial commit
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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'Connor
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user