Add Umami analytics and broaden beta copy for Android.
Optional NEXT_PUBLIC_UMAMI_* env vars load tracking when set; marketing pages now mention iOS TestFlight and Google Play internal testing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,3 +16,7 @@
|
|||||||
# Password required to view the site (login page at /login).
|
# Password required to view the site (login page at /login).
|
||||||
# Leave empty/unset to disable the gate.
|
# Leave empty/unset to disable the gate.
|
||||||
SITE_PASSWORD=""
|
SITE_PASSWORD=""
|
||||||
|
|
||||||
|
# Umami analytics (optional). Omit NEXT_PUBLIC_UMAMI_WEBSITE_ID to disable tracking.
|
||||||
|
# NEXT_PUBLIC_UMAMI_WEBSITE_ID="your-website-uuid"
|
||||||
|
# NEXT_PUBLIC_UMAMI_SCRIPT_URL="https://cloud.umami.is/script.js"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Medscribe Web
|
# Medscribe Web
|
||||||
|
|
||||||
Marketing homepage for [Medscribe](../README.md) — a privacy-first, on-device medical companion for iOS.
|
Marketing homepage for [Medscribe](../README.md) — a privacy-first, on-device medical companion for iOS and Android.
|
||||||
|
|
||||||
Built with [create-t3-app](https://create.t3.gg/) (Next.js + TypeScript + Tailwind). Single-page site; no database or API routes.
|
Built with [create-t3-app](https://create.t3.gg/) (Next.js + TypeScript + Tailwind). Single-page site; no database or API routes.
|
||||||
|
|
||||||
@@ -15,6 +15,15 @@ bun run dev
|
|||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000).
|
Open [http://localhost:3000](http://localhost:3000).
|
||||||
|
|
||||||
|
### Analytics (optional)
|
||||||
|
|
||||||
|
Set in `.env` (see `.env.example`):
|
||||||
|
|
||||||
|
- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` — website UUID from your Umami dashboard
|
||||||
|
- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` — optional; defaults to `https://cloud.umami.is/script.js` (use your self-hosted URL if needed)
|
||||||
|
|
||||||
|
Leave the website ID unset to disable tracking (default for local dev).
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -26,10 +35,10 @@ Screenshots live in `public/screenshots/`. They are wrapped in an iPhone-style f
|
|||||||
|
|
||||||
### Capturing raw screenshots for the site
|
### Capturing raw screenshots for the site
|
||||||
|
|
||||||
Export **in-app content only** from the simulator or device — no simulator chrome or macOS capture toolbar. A normal in-app iOS status bar/tab bar is fine.
|
Export **in-app content only** from the simulator or device — no simulator chrome or macOS capture toolbar. A normal in-app status bar/tab bar is fine.
|
||||||
|
|
||||||
1. Run the app on **iPhone 17 Pro** (or 16 Pro) simulator or device.
|
1. Run the app on **iPhone 17 Pro** (or 16 Pro) simulator or device, or on an **Android** emulator/device.
|
||||||
2. Capture each screen (⌘S in Simulator saves to Desktop).
|
2. Capture each screen (⌘S in Simulator saves to Desktop; `adb exec-out screencap -p` on Android).
|
||||||
3. Drop PNGs into `public/screenshots/` and update paths in `src/app/page.tsx` / `src/components/feature-showcase.tsx`.
|
3. Drop PNGs into `public/screenshots/` and update paths in `src/app/page.tsx` / `src/components/feature-showcase.tsx`.
|
||||||
|
|
||||||
The frame dimensions assume tall iPhone screenshots and are controlled in `src/components/phone-mockup.tsx`. Use `captureMode="simulator"` only for old captures that already include device chrome.
|
The frame dimensions assume tall iPhone screenshots and are controlled in `src/components/phone-mockup.tsx`. Use `captureMode="simulator"` only for old captures that already include device chrome.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { MedscribeLogo } from "~/components/medscribe-logo";
|
|||||||
import { PhoneMockup } from "~/components/phone-mockup";
|
import { PhoneMockup } from "~/components/phone-mockup";
|
||||||
import { PrintButton } from "~/components/print-button";
|
import { PrintButton } from "~/components/print-button";
|
||||||
import { SectionLabel } from "~/components/section-label";
|
import { SectionLabel } from "~/components/section-label";
|
||||||
|
import { BETA_FOOTER_NOTE, BETA_SUMMARY } from "~/lib/beta";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -326,9 +327,7 @@ export default function ExecutiveSummaryPage() {
|
|||||||
{/* Beta */}
|
{/* Beta */}
|
||||||
<section className="flex flex-col gap-5 rounded-xl border border-border-soft bg-surface p-6 sm:flex-row sm:items-center sm:justify-between sm:p-8">
|
<section className="flex flex-col gap-5 rounded-xl border border-border-soft bg-surface p-6 sm:flex-row sm:items-center sm:justify-between sm:p-8">
|
||||||
<p className="max-w-xl text-sm leading-relaxed text-muted-foreground">
|
<p className="max-w-xl text-sm leading-relaxed text-muted-foreground">
|
||||||
A beta is available on iOS through TestFlight. Everything runs
|
{BETA_SUMMARY} If you'd like to try it, reach out.
|
||||||
privately on the device, so no health data leaves the phone. If
|
|
||||||
you'd like to try it, reach out.
|
|
||||||
</p>
|
</p>
|
||||||
<PrintButton size="lg" className="shrink-0" />
|
<PrintButton size="lg" className="shrink-0" />
|
||||||
</section>
|
</section>
|
||||||
@@ -459,9 +458,7 @@ export default function ExecutiveSummaryPage() {
|
|||||||
|
|
||||||
<footer className="mt-auto flex items-center justify-between border-t border-teal-900/10 pt-1.5 text-[10pt] text-slate-500">
|
<footer className="mt-auto flex items-center justify-between border-t border-teal-900/10 pt-1.5 text-[10pt] text-slate-500">
|
||||||
<p>Sean O'Connor</p>
|
<p>Sean O'Connor</p>
|
||||||
<p className="text-slate-400">
|
<p className="text-slate-400">{BETA_FOOTER_NOTE}</p>
|
||||||
A beta is available on iOS through TestFlight.
|
|
||||||
</p>
|
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ import {
|
|||||||
import { FeatureShowcase } from "~/components/feature-showcase";
|
import { FeatureShowcase } from "~/components/feature-showcase";
|
||||||
import { PhoneMockup } from "~/components/phone-mockup";
|
import { PhoneMockup } from "~/components/phone-mockup";
|
||||||
import { SectionLabel } from "~/components/section-label";
|
import { SectionLabel } from "~/components/section-label";
|
||||||
|
import {
|
||||||
|
BETA_CTA_BODY,
|
||||||
|
BETA_CTA_TITLE,
|
||||||
|
BETA_INVITE_MAILTO,
|
||||||
|
} from "~/lib/beta";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -119,8 +124,9 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="animate-fade-in-up-delay-2 flex flex-wrap gap-2">
|
<div className="animate-fade-in-up-delay-2 flex flex-wrap gap-2">
|
||||||
<Badge variant="success">On TestFlight</Badge>
|
<Badge variant="success">Beta available</Badge>
|
||||||
<Badge variant="outline">iOS</Badge>
|
<Badge variant="outline">iOS · TestFlight</Badge>
|
||||||
|
<Badge variant="outline">Android · Play internal</Badge>
|
||||||
<Badge variant="outline">Emergency-ready</Badge>
|
<Badge variant="outline">Emergency-ready</Badge>
|
||||||
<Badge variant="outline">No cloud AI</Badge>
|
<Badge variant="outline">No cloud AI</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -319,11 +325,9 @@ export default function HomePage() {
|
|||||||
<Card className="overflow-hidden border-primary-button/20 bg-primary-button text-white">
|
<Card className="overflow-hidden border-primary-button/20 bg-primary-button text-white">
|
||||||
<CardHeader className="gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<CardHeader className="gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<CardTitle className="text-white">Try Medscribe on iOS</CardTitle>
|
<CardTitle className="text-white">{BETA_CTA_TITLE}</CardTitle>
|
||||||
<CardDescription className="text-white/75">
|
<CardDescription className="text-white/75">
|
||||||
The app is live on TestFlight. Reach out for an invite if
|
{BETA_CTA_BODY}
|
||||||
you're managing medications, visits, or emergency context for
|
|
||||||
someone you love.
|
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -332,9 +336,7 @@ export default function HomePage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="border-white/30 bg-white text-primary-button hover:bg-white/90"
|
className="border-white/30 bg-white text-primary-button hover:bg-white/90"
|
||||||
>
|
>
|
||||||
<a href="mailto:sean@soconnor.dev?subject=Medscribe%20TestFlight%20invite%20request&body=Hi%20Sean%2C%0D%0A%0D%0AI%27d%20like%20a%20TestFlight%20invite%20to%20try%20Medscribe.%20I%27m%20managing%20medications%2C%20visits%2C%20or%20emergency%20context%20for%20someone%20I%20care%20for.%0D%0A%0D%0AThanks%21">
|
<a href={BETA_INVITE_MAILTO}>Request an invite</a>
|
||||||
Request an invite
|
|
||||||
</a>
|
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
+6
-1
@@ -3,6 +3,8 @@ import "~/styles/globals.css";
|
|||||||
import { type Metadata } from "next";
|
import { type Metadata } from "next";
|
||||||
import { Instrument_Sans, Playfair_Display } from "next/font/google";
|
import { Instrument_Sans, Playfair_Display } from "next/font/google";
|
||||||
|
|
||||||
|
import { UmamiAnalytics } from "~/components/umami-analytics";
|
||||||
|
|
||||||
const instrumentSans = Instrument_Sans({
|
const instrumentSans = Instrument_Sans({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
variable: "--font-instrument-sans",
|
variable: "--font-instrument-sans",
|
||||||
@@ -42,7 +44,10 @@ export default function RootLayout({
|
|||||||
}: Readonly<{ children: React.ReactNode }>) {
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" className={`${instrumentSans.variable} ${playfair.variable}`}>
|
<html lang="en" className={`${instrumentSans.variable} ${playfair.variable}`}>
|
||||||
<body className="min-h-screen">{children}</body>
|
<body className="min-h-screen">
|
||||||
|
{children}
|
||||||
|
<UmamiAnalytics />
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import Script from "next/script";
|
||||||
|
|
||||||
|
import { env } from "~/env";
|
||||||
|
|
||||||
|
const DEFAULT_UMAMI_SCRIPT_URL = "https://cloud.umami.is/script.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Privacy-friendly page analytics via Umami. Enabled when
|
||||||
|
* `NEXT_PUBLIC_UMAMI_WEBSITE_ID` is set in `.env`.
|
||||||
|
*/
|
||||||
|
export function UmamiAnalytics() {
|
||||||
|
const websiteId = env.NEXT_PUBLIC_UMAMI_WEBSITE_ID;
|
||||||
|
if (!websiteId) return null;
|
||||||
|
|
||||||
|
const src = env.NEXT_PUBLIC_UMAMI_SCRIPT_URL ?? DEFAULT_UMAMI_SCRIPT_URL;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Script
|
||||||
|
defer
|
||||||
|
src={src}
|
||||||
|
data-website-id={websiteId}
|
||||||
|
strategy="afterInteractive"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
+6
-2
@@ -18,7 +18,10 @@ export const env = createEnv({
|
|||||||
* `NEXT_PUBLIC_`.
|
* `NEXT_PUBLIC_`.
|
||||||
*/
|
*/
|
||||||
client: {
|
client: {
|
||||||
// NEXT_PUBLIC_CLIENTVAR: z.string(),
|
/** Umami website ID — omit to disable analytics. */
|
||||||
|
NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.string().optional(),
|
||||||
|
/** Umami script URL (defaults to Umami Cloud). */
|
||||||
|
NEXT_PUBLIC_UMAMI_SCRIPT_URL: z.string().url().optional(),
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,7 +31,8 @@ export const env = createEnv({
|
|||||||
runtimeEnv: {
|
runtimeEnv: {
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
SITE_PASSWORD: process.env.SITE_PASSWORD,
|
SITE_PASSWORD: process.env.SITE_PASSWORD,
|
||||||
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
|
NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
|
||||||
|
NEXT_PUBLIC_UMAMI_SCRIPT_URL: process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/** Shared beta / early-access copy for the marketing site. */
|
||||||
|
|
||||||
|
export const BETA_SUMMARY =
|
||||||
|
"A beta is available on iOS (TestFlight) and Android (Google Play internal testing). Everything runs privately on the device, so no health data leaves the phone.";
|
||||||
|
|
||||||
|
export const BETA_CTA_TITLE = "Try Medscribe";
|
||||||
|
|
||||||
|
export const BETA_CTA_BODY =
|
||||||
|
"The app is in beta on iOS and Android. Reach out for an invite if you're managing medications, visits, or emergency context for someone you love.";
|
||||||
|
|
||||||
|
export const BETA_INVITE_MAILTO =
|
||||||
|
"mailto:sean@soconnor.dev?subject=Medscribe%20beta%20invite%20request&body=Hi%20Sean%2C%0D%0A%0D%0AI%27d%20like%20a%20beta%20invite%20to%20try%20Medscribe%20(iOS%20or%20Android).%20I%27m%20managing%20medications%2C%20visits%2C%20or%20emergency%20context%20for%20someone%20I%20care%20for.%0D%0A%0D%0AThanks%21";
|
||||||
|
|
||||||
|
export const BETA_FOOTER_NOTE =
|
||||||
|
"Beta on iOS (TestFlight) and Android (Google Play internal testing).";
|
||||||
Reference in New Issue
Block a user