Publish partner deck and open closed beta site

This commit is contained in:
2026-08-24 15:43:39 -04:00
parent f6aadc5107
commit e9b5564127
11 changed files with 112 additions and 28 deletions
+3 -2
View File
@@ -13,8 +13,9 @@
# SERVERVAR="foo" # SERVERVAR="foo"
# NEXT_PUBLIC_CLIENTVAR="bar" # NEXT_PUBLIC_CLIENTVAR="bar"
# Password required to view the site (login page at /login). # Optional password gate for private previews. The gate is off unless both
# Leave empty/unset to disable the gate. # values are set, so a stale password alone cannot hide the public site.
SITE_PASSWORD_ENABLED="false"
SITE_PASSWORD="" SITE_PASSWORD=""
# Umami analytics (optional). Omit NEXT_PUBLIC_UMAMI_WEBSITE_ID to disable tracking. # Umami analytics (optional). Omit NEXT_PUBLIC_UMAMI_WEBSITE_ID to disable tracking.
+18 -5
View File
@@ -25,6 +25,7 @@ Public routes include:
- `/how-it-works` — review loop, medication maintenance, Questions, and handoff - `/how-it-works` — review loop, medication maintenance, Questions, and handoff
- `/privacy` — plain-language privacy and patient-control commitments - `/privacy` — plain-language privacy and patient-control commitments
- `/access` — health-data-free access request - `/access` — health-data-free access request
- `/medscribe-corporate-partner-overview.pdf` — public eight-slide partner deck
Named partner print material remains isolated and excluded from search: Named partner print material remains isolated and excluded from search:
@@ -44,6 +45,19 @@ Set in `.env` (see `.env.example`):
Leave the website ID unset to disable tracking (default for local dev). Leave the website ID unset to disable tracking (default for local dev).
### Optional preview password
The public site is open by default. To enable the private-preview gate, set
both values in the deployment environment:
```bash
SITE_PASSWORD_ENABLED="true"
SITE_PASSWORD="choose-a-strong-password"
```
Unset `SITE_PASSWORD_ENABLED` or set it to `false` to keep the site public. A
stored password alone does not enable the gate.
## Build ## Build
```bash ```bash
@@ -66,11 +80,10 @@ Export **in-app content only** from the simulator or device — no simulator chr
1. Run the app on an **iPhone 17 Pro** simulator or device. 1. Run the app on an **iPhone 17 Pro** simulator or device.
2. Capture each screen (⌘S in Simulator saves to Desktop). 2. Capture each screen (⌘S in Simulator saves to Desktop).
3. Fit each 1206 × 2622 capture into the bezel's 1201 × 2617 display opening, 3. Place each 1206 × 2622 capture at native size in the 1350 × 2760 Silver
clip it with a 198 px corner radius, and composite it into the 1350 × 2760 bezel at offset `+72+69`, clip it to the calibrated display-opening mask,
Silver bezel at offset `+74+71`. Export a transparent, lossless WebP and and export a transparent, lossless WebP into the shared versioned asset
update the shared versioned asset directory. directory referenced by the routes.
referenced by the routes.
Product images are composited at the bezel source size of 1350 × 2760 with Product images are composited at the bezel source size of 1350 × 2760 with
transparent backgrounds and rendered responsively through transparent backgrounds and rendered responsively through
Binary file not shown.
+4 -3
View File
@@ -19,7 +19,7 @@ import { ACCESS_INVITE_MAILTO } from "~/lib/access";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Request access", title: "Request access",
description: description:
"Request access to Medscribe without including personal health information.", "Request access to Medscribe's closed beta without including personal health information.",
}; };
const fitCards = [ const fitCards = [
@@ -86,8 +86,9 @@ export default function AccessPage() {
Request Medscribe access Request Medscribe access
</h2> </h2>
<p className="text-muted-foreground mt-4 text-sm leading-relaxed"> <p className="text-muted-foreground mt-4 text-sm leading-relaxed">
Tell us your platform and whether you are interested as a patient, The beta is currently closed to a small group. Tell us your
caregiver, or both. platform and whether you are interested as a patient, caregiver,
or both, and we&apos;ll follow up as access becomes available.
</p> </p>
<Button asChild size="lg" className="mt-7 w-full sm:w-auto"> <Button asChild size="lg" className="mt-7 w-full sm:w-auto">
<a href={ACCESS_INVITE_MAILTO}> <a href={ACCESS_INVITE_MAILTO}>
+50
View File
@@ -3,6 +3,8 @@ import {
ArrowRight, ArrowRight,
Check, Check,
ContactRound, ContactRound,
Download,
FileText,
HeartHandshake, HeartHandshake,
LockKeyhole, LockKeyhole,
ScanLine, ScanLine,
@@ -511,6 +513,54 @@ export default function HomePage() {
</div> </div>
</section> </section>
<section
id="partners"
className="pitch-lazy overflow-hidden px-4 py-20 sm:px-6 lg:py-28"
>
<div className="section-wash surface-panel mx-auto grid max-w-6xl gap-10 rounded-[2.75rem] px-6 py-12 sm:px-10 lg:grid-cols-[1fr_auto] lg:items-center lg:px-14 lg:py-16">
<div className="pitch-copy-motion max-w-3xl">
<div className="bg-primary-light text-primary flex size-12 items-center justify-center rounded-2xl">
<FileText className="size-6" aria-hidden="true" />
</div>
<p className="text-primary mt-7 text-xs font-bold tracking-[0.2em] uppercase">
For partners
</p>
<h2 className="text-foreground mt-4 font-display text-4xl leading-tight font-semibold tracking-[-0.035em] sm:text-5xl">
See the partnership story.
</h2>
<p className="text-muted-foreground mt-5 max-w-2xl text-lg leading-relaxed">
An eight-slide overview of the problem Medscribe addresses, the
product in practice, and the opportunity to bring it to more
patients and caregivers.
</p>
<p className="text-foreground mt-6 text-sm font-semibold tracking-[-0.01em]">
<span className="block">Hadlock Holdings LLC</span>
<span className="text-muted-foreground mt-1 block">
Sean O&apos;Connor · Bill Vitriol
</span>
</p>
</div>
<div className="flex flex-col gap-3 sm:flex-row lg:flex-col">
<Button asChild size="lg">
<a
href="/medscribe-corporate-partner-overview.pdf"
target="_blank"
rel="noreferrer"
>
View pitch deck
<ArrowRight className="size-4" aria-hidden="true" />
</a>
</Button>
<Button asChild size="lg" variant="outline">
<a href="/medscribe-corporate-partner-overview.pdf" download>
Download PDF
<Download className="size-4" aria-hidden="true" />
</a>
</Button>
</div>
</div>
</section>
<section className="pitch-lazy flex min-h-[82svh] items-center px-4 py-24 sm:px-6 lg:py-32"> <section className="pitch-lazy flex min-h-[82svh] items-center px-4 py-24 sm:px-6 lg:py-32">
<div className="pitch-copy-motion section-wash surface-panel mx-auto w-full max-w-6xl overflow-hidden rounded-[3rem] px-6 py-16 text-center sm:px-10 lg:px-20 lg:py-24"> <div className="pitch-copy-motion section-wash surface-panel mx-auto w-full max-w-6xl overflow-hidden rounded-[3rem] px-6 py-16 text-center sm:px-10 lg:px-20 lg:py-24">
<p className="text-primary text-xs font-bold tracking-[0.2em] uppercase"> <p className="text-primary text-xs font-bold tracking-[0.2em] uppercase">
+4 -1
View File
@@ -8,7 +8,10 @@ import {
const COOKIE_MAX_AGE = 60 * 60 * 24 * 30; const COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
export async function POST(req: Request) { export async function POST(req: Request) {
const password = process.env.SITE_PASSWORD; const password =
process.env.SITE_PASSWORD_ENABLED === "true"
? process.env.SITE_PASSWORD
: undefined;
if (!password) { if (!password) {
return NextResponse.json({ ok: true }); return NextResponse.json({ ok: true });
+15 -10
View File
@@ -4,6 +4,10 @@ import { MedscribeLogo } from "~/components/medscribe-logo";
const footerLinks = [ const footerLinks = [
{ href: "/how-it-works", label: "How it works" }, { href: "/how-it-works", label: "How it works" },
{
href: "/medscribe-corporate-partner-overview.pdf",
label: "Partner deck",
},
{ href: "/privacy", label: "Privacy" }, { href: "/privacy", label: "Privacy" },
{ href: "/access", label: "Request access" }, { href: "/access", label: "Request access" },
] as const; ] as const;
@@ -24,6 +28,9 @@ export function SiteFooter() {
A private, patient-controlled way to keep visits, medications, and A private, patient-controlled way to keep visits, medications, and
important care context together. important care context together.
</p> </p>
<p className="text-muted-foreground mt-3 text-xs leading-relaxed">
Medscribe is a product of Hadlock Holdings LLC.
</p>
<p className="text-muted-foreground mt-3 text-xs leading-relaxed"> <p className="text-muted-foreground mt-3 text-xs leading-relaxed">
Medscribe does not diagnose, prescribe, or replace emergency Medscribe does not diagnose, prescribe, or replace emergency
services or a clinician&apos;s verification. services or a clinician&apos;s verification.
@@ -48,20 +55,18 @@ export function SiteFooter() {
<div> <div>
<p className="text-foreground font-semibold">Contact</p> <p className="text-foreground font-semibold">Contact</p>
<p className="text-muted-foreground mt-3 leading-relaxed"> <p className="text-foreground mt-3 leading-relaxed">
Built by{" "} Sean O&apos;Connor
<a <span className="text-muted-foreground mx-2" aria-hidden="true">
href="https://soconnor.dev" ·
className="text-foreground focus-visible:ring-primary/40 rounded-sm underline-offset-4 outline-none hover:underline focus-visible:ring-2" </span>
> Bill Vitriol
Sean O&apos;Connor
</a>
</p> </p>
<a <a
href="mailto:sean@soconnor.dev?subject=Medscribe%20website%20question" href="mailto:access@medscribe.dev?subject=Medscribe%20website%20question"
className="text-foreground focus-visible:ring-primary/40 mt-2 inline-block rounded-sm underline-offset-4 outline-none hover:underline focus-visible:ring-2" className="text-foreground focus-visible:ring-primary/40 mt-2 inline-block rounded-sm underline-offset-4 outline-none hover:underline focus-visible:ring-2"
> >
sean@soconnor.dev access@medscribe.dev
</a> </a>
<p className="text-muted-foreground/80 mt-5 text-xs"> <p className="text-muted-foreground/80 mt-5 text-xs">
&copy; {new Date().getFullYear()} Hadlock Holdings LLC &copy; {new Date().getFullYear()} Hadlock Holdings LLC
+1 -1
View File
@@ -1,5 +1,5 @@
export const productRelease = { export const productRelease = {
stage: "Private by design", stage: "Closed beta",
platforms: "iOS and Android", platforms: "iOS and Android",
} as const; } as const;
+2
View File
@@ -9,6 +9,7 @@ export const env = createEnv({
server: { server: {
NODE_ENV: z.enum(["development", "test", "production"]), NODE_ENV: z.enum(["development", "test", "production"]),
// Optional site-wide password gate via `/login`, enforced in `src/proxy.ts`. // Optional site-wide password gate via `/login`, enforced in `src/proxy.ts`.
SITE_PASSWORD_ENABLED: z.enum(["true", "false"]).optional(),
SITE_PASSWORD: z.string().optional(), SITE_PASSWORD: z.string().optional(),
}, },
@@ -30,6 +31,7 @@ export const env = createEnv({
*/ */
runtimeEnv: { runtimeEnv: {
NODE_ENV: process.env.NODE_ENV, NODE_ENV: process.env.NODE_ENV,
SITE_PASSWORD_ENABLED: process.env.SITE_PASSWORD_ENABLED,
SITE_PASSWORD: process.env.SITE_PASSWORD, SITE_PASSWORD: process.env.SITE_PASSWORD,
NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID, NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
NEXT_PUBLIC_UMAMI_SCRIPT_URL: process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL, NEXT_PUBLIC_UMAMI_SCRIPT_URL: process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL,
+1 -1
View File
@@ -1,2 +1,2 @@
export const ACCESS_INVITE_MAILTO = export const ACCESS_INVITE_MAILTO =
"mailto:sean@soconnor.dev?subject=Medscribe%20access&body=Hi%20Sean%2C%0D%0A%0D%0AI%27m%20interested%20in%20Medscribe.%0D%0A%0D%0APlatform%20(iOS%20or%20Android)%3A%0D%0AI%27m%20interested%20as%20a%20(patient%2C%20caregiver%2C%20or%20both)%3A%0D%0A%0D%0APlease%20do%20not%20include%20diagnoses%2C%20medication%20names%2C%20or%20other%20health%20details%20in%20this%20email.%0D%0A%0D%0AThanks%21"; "mailto:access@medscribe.dev?subject=Medscribe%20access&body=Hi%20Medscribe%20team%2C%0D%0A%0D%0AI%27m%20interested%20in%20Medscribe.%0D%0A%0D%0APlatform%20(iOS%20or%20Android)%3A%0D%0AI%27m%20interested%20as%20a%20(patient%2C%20caregiver%2C%20or%20both)%3A%0D%0A%0D%0APlease%20do%20not%20include%20diagnoses%2C%20medication%20names%2C%20or%20other%20health%20details%20in%20this%20email.%0D%0A%0D%0AThanks%21";
+14 -5
View File
@@ -10,17 +10,26 @@ import {
/** /**
* Site-wide password gate via a login page and signed session cookie. * Site-wide password gate via a login page and signed session cookie.
* *
* Set `SITE_PASSWORD` in `.env` to require a password before the site loads. * Set `SITE_PASSWORD_ENABLED=true` and `SITE_PASSWORD` to require a password
* Leave `SITE_PASSWORD` unset to disable the gate. * before the site loads. The gate is off by default.
*/ */
export async function proxy(req: NextRequest) { export async function proxy(req: NextRequest) {
const password = process.env.SITE_PASSWORD; const password =
process.env.SITE_PASSWORD_ENABLED === "true"
? process.env.SITE_PASSWORD
: undefined;
const { pathname } = req.nextUrl;
if (!password) return NextResponse.next(); if (!password) {
if (pathname === "/login") {
return NextResponse.redirect(new URL("/", req.url));
}
return NextResponse.next();
}
const token = req.cookies.get(AUTH_COOKIE_NAME)?.value; const token = req.cookies.get(AUTH_COOKIE_NAME)?.value;
const isAuthed = await verifyAccessToken(password, token); const isAuthed = await verifyAccessToken(password, token);
const { pathname } = req.nextUrl;
if (pathname === "/login" || pathname === "/api/auth") { if (pathname === "/login" || pathname === "/api/auth") {
if (isAuthed && pathname === "/login") { if (isAuthed && pathname === "/login") {