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"
# NEXT_PUBLIC_CLIENTVAR="bar"
# Password required to view the site (login page at /login).
# Leave empty/unset to disable the gate.
# Optional password gate for private previews. The gate is off unless both
# values are set, so a stale password alone cannot hide the public site.
SITE_PASSWORD_ENABLED="false"
SITE_PASSWORD=""
# 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
- `/privacy` — plain-language privacy and patient-control commitments
- `/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:
@@ -44,6 +45,19 @@ Set in `.env` (see `.env.example`):
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
```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.
2. Capture each screen (⌘S in Simulator saves to Desktop).
3. Fit each 1206 × 2622 capture into the bezel's 1201 × 2617 display opening,
clip it with a 198 px corner radius, and composite it into the 1350 × 2760
Silver bezel at offset `+74+71`. Export a transparent, lossless WebP and
update the shared versioned asset directory.
referenced by the routes.
3. Place each 1206 × 2622 capture at native size in the 1350 × 2760 Silver
bezel at offset `+72+69`, clip it to the calibrated display-opening mask,
and export a transparent, lossless WebP into the shared versioned asset
directory referenced by the routes.
Product images are composited at the bezel source size of 1350 × 2760 with
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 = {
title: "Request access",
description:
"Request access to Medscribe without including personal health information.",
"Request access to Medscribe's closed beta without including personal health information.",
};
const fitCards = [
@@ -86,8 +86,9 @@ export default function AccessPage() {
Request Medscribe access
</h2>
<p className="text-muted-foreground mt-4 text-sm leading-relaxed">
Tell us your platform and whether you are interested as a patient,
caregiver, or both.
The beta is currently closed to a small group. Tell us your
platform and whether you are interested as a patient, caregiver,
or both, and we&apos;ll follow up as access becomes available.
</p>
<Button asChild size="lg" className="mt-7 w-full sm:w-auto">
<a href={ACCESS_INVITE_MAILTO}>
+50
View File
@@ -3,6 +3,8 @@ import {
ArrowRight,
Check,
ContactRound,
Download,
FileText,
HeartHandshake,
LockKeyhole,
ScanLine,
@@ -511,6 +513,54 @@ export default function HomePage() {
</div>
</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">
<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">
+4 -1
View File
@@ -8,7 +8,10 @@ import {
const COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
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) {
return NextResponse.json({ ok: true });
+14 -9
View File
@@ -4,6 +4,10 @@ import { MedscribeLogo } from "~/components/medscribe-logo";
const footerLinks = [
{ href: "/how-it-works", label: "How it works" },
{
href: "/medscribe-corporate-partner-overview.pdf",
label: "Partner deck",
},
{ href: "/privacy", label: "Privacy" },
{ href: "/access", label: "Request access" },
] as const;
@@ -24,6 +28,9 @@ export function SiteFooter() {
A private, patient-controlled way to keep visits, medications, and
important care context together.
</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">
Medscribe does not diagnose, prescribe, or replace emergency
services or a clinician&apos;s verification.
@@ -48,20 +55,18 @@ export function SiteFooter() {
<div>
<p className="text-foreground font-semibold">Contact</p>
<p className="text-muted-foreground mt-3 leading-relaxed">
Built by{" "}
<a
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"
>
<p className="text-foreground mt-3 leading-relaxed">
Sean O&apos;Connor
</a>
<span className="text-muted-foreground mx-2" aria-hidden="true">
·
</span>
Bill Vitriol
</p>
<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"
>
sean@soconnor.dev
access@medscribe.dev
</a>
<p className="text-muted-foreground/80 mt-5 text-xs">
&copy; {new Date().getFullYear()} Hadlock Holdings LLC
+1 -1
View File
@@ -1,5 +1,5 @@
export const productRelease = {
stage: "Private by design",
stage: "Closed beta",
platforms: "iOS and Android",
} as const;
+2
View File
@@ -9,6 +9,7 @@ export const env = createEnv({
server: {
NODE_ENV: z.enum(["development", "test", "production"]),
// 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(),
},
@@ -30,6 +31,7 @@ export const env = createEnv({
*/
runtimeEnv: {
NODE_ENV: process.env.NODE_ENV,
SITE_PASSWORD_ENABLED: process.env.SITE_PASSWORD_ENABLED,
SITE_PASSWORD: process.env.SITE_PASSWORD,
NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
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 =
"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.
*
* Set `SITE_PASSWORD` in `.env` to require a password before the site loads.
* Leave `SITE_PASSWORD` unset to disable the gate.
* Set `SITE_PASSWORD_ENABLED=true` and `SITE_PASSWORD` to require a password
* before the site loads. The gate is off by default.
*/
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 isAuthed = await verifyAccessToken(password, token);
const { pathname } = req.nextUrl;
if (pathname === "/login" || pathname === "/api/auth") {
if (isAuthed && pathname === "/login") {