From 6e9819180f590e746d34cb1691a0bf53e5f070e4 Mon Sep 17 00:00:00 2001 From: Daliso Ngoma Date: Thu, 9 Apr 2026 09:55:26 +0200 Subject: [PATCH 1/3] Remove privacy template disclaimer --- app/privacy/page.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/privacy/page.tsx b/app/privacy/page.tsx index 1dbcfc4..acba9d3 100644 --- a/app/privacy/page.tsx +++ b/app/privacy/page.tsx @@ -80,11 +80,6 @@ export default function PrivacyPage() { ))} - -

- This page is a starter template and should be reviewed by a - qualified legal professional before production use. -

); From 54a0046ea02984a0789a0b3de3cfefb2a146a0c0 Mon Sep 17 00:00:00 2001 From: Daliso Ngoma Date: Thu, 9 Apr 2026 10:11:35 +0200 Subject: [PATCH 2/3] Document Cloudflare deployment --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index d6d2b19..71f7cd5 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,23 @@ Then open http://localhost:3000. - `npm run build` — production build - `npm run start` — run the production build +## Deployment + +- This site is deployed on **Cloudflare Pages**, not Vercel. +- `next.config.mjs` uses `output: "export"` so `npm run build` produces the + static `out/` directory used for deployment. +- Production deploys happen from `.github/workflows/ci.yml` on pushes to + `main`, using `wrangler pages deploy out --project-name=justsomethingdotapp --branch=main`. +- For a manual preview deploy from a branch, build first and then run: + +```bash +npm run build +wrangler pages deploy out --project-name=justsomethingdotapp --branch= +``` + +- Do not set this repo up on Vercel unless the hosting platform is being + intentionally migrated. + ## Project structure ``` From 2572f107a2ed957bf7125b3cbe27ab2c71c3ce72 Mon Sep 17 00:00:00 2001 From: Daliso Ngoma Date: Thu, 9 Apr 2026 16:52:16 +0200 Subject: [PATCH 3/3] Launch Just Glucose on the App Store, refresh card statuses, add scroll reveals - Just Glucose now shows as Available with the official App Store badge - Just BP and Just Weight are marked as Building (TestFlight link removed) - Reorder cards so Just Glucose appears first - Homepage "View apps" buttons smooth-scroll to #apps section - Add Reveal component for fade/slide-in animations across the home page, respecting prefers-reduced-motion Co-Authored-By: Claude Opus 4.6 (1M context) --- app/globals.css | 26 +++++++++++++++++ app/layout.tsx | 2 +- app/page.tsx | 54 +++++++++++++++++++---------------- components/AppCard.tsx | 19 ++++++++++++- components/Reveal.tsx | 56 +++++++++++++++++++++++++++++++++++++ components/Section.tsx | 5 ++-- lib/apps.ts | 27 +++++++++--------- public/badges/app-store.svg | 46 ++++++++++++++++++++++++++++++ tests/AppCard.test.tsx | 20 ++++++------- tests/apps.test.ts | 19 ++++++------- 10 files changed, 212 insertions(+), 62 deletions(-) create mode 100644 components/Reveal.tsx create mode 100644 public/badges/app-store.svg diff --git a/app/globals.css b/app/globals.css index ec5b2cd..8ff4dcb 100644 --- a/app/globals.css +++ b/app/globals.css @@ -19,3 +19,29 @@ html, body { radial-gradient(circle at 80% 0%, rgba(225,72,90,0.06), transparent 40%), radial-gradient(circle at 50% 100%, rgba(47,163,107,0.06), transparent 45%); } + +/* Reveal-on-scroll / on-mount animation */ +.reveal { + opacity: 0; + transform: translateY(16px); + transition: + opacity 700ms cubic-bezier(0.22, 1, 0.36, 1), + transform 700ms cubic-bezier(0.22, 1, 0.36, 1); + will-change: opacity, transform; +} + +.reveal.is-visible { + opacity: 1; + transform: none; +} + +@media (prefers-reduced-motion: reduce) { + .reveal { + opacity: 1; + transform: none; + transition: none; + } + html { + scroll-behavior: auto !important; + } +} diff --git a/app/layout.tsx b/app/layout.tsx index f01d944..1d9c9ac 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -27,7 +27,7 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - +
-

+ justsomething.app -

-

+ + Simple health tracking, without the clutter. -

-

+ + A growing family of focused apps. One thing each. Quick to log, calm to use, and designed to stay out of your way. -

-
+ + View apps @@ -32,7 +33,7 @@ export default function HomePage() { > Get support -
+
@@ -44,8 +45,10 @@ export default function HomePage() { intro="Every Just app does a single job well — no dashboards to configure, no feeds to scroll." >
- {apps.map((app) => ( - + {apps.map((app, i) => ( + + + ))}
@@ -54,7 +57,7 @@ export default function HomePage() {
-
+
Philosophy
@@ -66,21 +69,23 @@ export default function HomePage() { app focuses on a single measurement so logging feels effortless and the numbers stay readable.

-
+
    {[ ["Single purpose", "One app, one job — no feature creep."], ["Low friction", "Log an entry in a tap or two."], ["Clarity first", "Calm screens, readable numbers."], ["Useful, not loud", "Helpful without getting in the way."], - ].map(([t, d]) => ( -
  • ( +
    {t}
    {d}
    -
  • + ))}
@@ -94,9 +99,10 @@ export default function HomePage() { intro="We release new Just apps when they feel truly useful. Here are a few ideas we're exploring." >
- {futureApps.map((f) => ( -
( +
{f.name}
@@ -104,14 +110,14 @@ export default function HomePage() {
Coming soon
-
+ ))}
{/* Trust / privacy */}
-
+
Built with care
@@ -126,12 +132,12 @@ export default function HomePage() { .

-
+
{/* Final CTA */}
-
+

Explore the Just family.

@@ -141,7 +147,7 @@ export default function HomePage() {

View apps @@ -153,7 +159,7 @@ export default function HomePage() { Get support
-
+
); diff --git a/components/AppCard.tsx b/components/AppCard.tsx index 83f3d2e..d67cd1f 100644 --- a/components/AppCard.tsx +++ b/components/AppCard.tsx @@ -5,6 +5,7 @@ const statusLabel = { available: "Available", "in-progress": "In Progress", "coming-soon": "Coming Soon", + building: "Building", } as const; export default function AppCard({ app }: { app: JustApp }) { @@ -33,7 +34,23 @@ export default function AppCard({ app }: { app: JustApp }) { {app.tagline} - {app.testflightUrl && ( + {app.appStoreUrl ? ( +
+ Download on the App Store + + ) : app.testflightUrl && ( (null); + const [visible, setVisible] = useState(false); + + useEffect(() => { + const node = ref.current; + if (!node) return; + + // If already in view or above the viewport on mount, reveal immediately. + const rect = node.getBoundingClientRect(); + if (rect.top < window.innerHeight) { + setVisible(true); + return; + } + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setVisible(true); + observer.disconnect(); + } + }); + }, + { threshold: 0.15, rootMargin: "0px 0px -40px 0px" }, + ); + observer.observe(node); + return () => observer.disconnect(); + }, []); + + return createElement( + as, + { + ref, + className: `reveal ${visible ? "is-visible" : ""} ${className}`.trim(), + style: delay ? { transitionDelay: `${delay}ms` } : undefined, + }, + children, + ); +} diff --git a/components/Section.tsx b/components/Section.tsx index 47ce0e7..8580d7f 100644 --- a/components/Section.tsx +++ b/components/Section.tsx @@ -1,4 +1,5 @@ import type { ReactNode } from "react"; +import Reveal from "./Reveal"; export function Section({ id, @@ -18,7 +19,7 @@ export function Section({ return (
{(eyebrow || title || intro) && ( -
+ {eyebrow && (
{eyebrow} @@ -32,7 +33,7 @@ export function Section({ {intro && (

{intro}

)} -
+ )} {children}
diff --git a/lib/apps.ts b/lib/apps.ts index cccb488..b5bf940 100644 --- a/lib/apps.ts +++ b/lib/apps.ts @@ -1,4 +1,4 @@ -export type AppStatus = "available" | "in-progress" | "coming-soon"; +export type AppStatus = "available" | "in-progress" | "coming-soon" | "building"; export type JustApp = { slug: string; @@ -8,19 +8,29 @@ export type JustApp = { accent: "bp" | "weight" | "glucose" | "neutral"; status: AppStatus; testflightUrl?: string; + appStoreUrl?: string; icon: string; }; // Current apps in the Just family. Add new entries here to expand the site. export const apps: JustApp[] = [ + { + slug: "just-glucose", + name: "Just Glucose", + tagline: "Glucose, clearly tracked.", + description: "A simple glucose tracking app.", + accent: "glucose", + status: "available", + appStoreUrl: "https://apps.apple.com/us/app/just-glucose/id6761859666", + icon: "/icons/just-glucose.png", + }, { slug: "just-bp", name: "Just BP", tagline: "Blood pressure, simply logged.", description: "A simple blood pressure tracking app.", accent: "bp", - status: "available", - testflightUrl: "https://testflight.apple.com/join/1NqxSEA2", + status: "building", icon: "/icons/just-bp.png", }, { @@ -29,18 +39,9 @@ export const apps: JustApp[] = [ tagline: "Weight, without the wait.", description: "A simple weight logging app.", accent: "weight", - status: "coming-soon", + status: "building", icon: "/icons/just-weight.png", }, - { - slug: "just-glucose", - name: "Just Glucose", - tagline: "Glucose, clearly tracked.", - description: "A simple glucose tracking app.", - accent: "glucose", - status: "coming-soon", - icon: "/icons/just-glucose.png", - }, ]; export const futureApps: { name: string; hint: string }[] = [ diff --git a/public/badges/app-store.svg b/public/badges/app-store.svg new file mode 100644 index 0000000..072b425 --- /dev/null +++ b/public/badges/app-store.svg @@ -0,0 +1,46 @@ + + Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/AppCard.test.tsx b/tests/AppCard.test.tsx index a95d1d8..f18cd59 100644 --- a/tests/AppCard.test.tsx +++ b/tests/AppCard.test.tsx @@ -4,23 +4,23 @@ import AppCard from "@/components/AppCard"; import { apps } from "@/lib/apps"; describe("", () => { - it("renders the TestFlight CTA when a URL is provided", () => { + it("shows a Building badge for in-development apps", () => { const bp = apps.find((a) => a.slug === "just-bp")!; render(); - const link = screen.getByRole("link", { name: /testflight/i }); - expect(link).toHaveAttribute("href", bp.testflightUrl); - expect(link).toHaveAttribute("target", "_blank"); - }); - - it("shows a Coming Soon badge for unreleased apps", () => { - const weight = apps.find((a) => a.slug === "just-weight")!; - render(); - expect(screen.getByText(/coming soon/i)).toBeInTheDocument(); + expect(screen.getByText(/building/i)).toBeInTheDocument(); expect( screen.queryByRole("link", { name: /testflight/i }), ).not.toBeInTheDocument(); }); + it("renders the App Store CTA when an appStoreUrl is provided", () => { + const glucose = apps.find((a) => a.slug === "just-glucose")!; + render(); + const link = screen.getByRole("link", { name: /app store/i }); + expect(link).toHaveAttribute("href", glucose.appStoreUrl); + expect(link).toHaveAttribute("target", "_blank"); + }); + it("renders the app name and description", () => { const glucose = apps.find((a) => a.slug === "just-glucose")!; render(); diff --git a/tests/apps.test.ts b/tests/apps.test.ts index 277dbf5..eb995db 100644 --- a/tests/apps.test.ts +++ b/tests/apps.test.ts @@ -14,19 +14,16 @@ describe("app family data", () => { expect(new Set(slugs).size).toBe(slugs.length); }); - it("Just BP is available with a TestFlight link", () => { - const bp = apps.find((a) => a.slug === "just-bp"); - expect(bp?.status).toBe("available"); - expect(bp?.testflightUrl).toMatch(/^https:\/\/testflight\.apple\.com\//); + it("Just BP and Just Weight are building", () => { + expect(apps.find((a) => a.slug === "just-bp")?.status).toBe("building"); + expect(apps.find((a) => a.slug === "just-bp")?.testflightUrl).toBeUndefined(); + expect(apps.find((a) => a.slug === "just-weight")?.status).toBe("building"); }); - it("Weight and Glucose are coming soon", () => { - expect(apps.find((a) => a.slug === "just-weight")?.status).toBe( - "coming-soon", - ); - expect(apps.find((a) => a.slug === "just-glucose")?.status).toBe( - "coming-soon", - ); + it("Just Glucose is available with an App Store link", () => { + const glucose = apps.find((a) => a.slug === "just-glucose"); + expect(glucose?.status).toBe("available"); + expect(glucose?.appStoreUrl).toMatch(/^https:\/\/apps\.apple\.com\//); }); it("exposes at least one future app placeholder", () => {