Publish partner deck and open closed beta site
This commit is contained in:
+3
-2
@@ -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.
|
||||||
|
|||||||
@@ -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.
@@ -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'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}>
|
||||||
|
|||||||
@@ -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'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">
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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's verification.
|
services or a clinician'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'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'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">
|
||||||
© {new Date().getFullYear()} Hadlock Holdings LLC
|
© {new Date().getFullYear()} Hadlock Holdings LLC
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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") {
|
||||||
|
|||||||
Reference in New Issue
Block a user