Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<branch-name>
```

- Do not set this repo up on Vercel unless the hosting platform is being
intentionally migrated.

## Project structure

```
Expand Down
26 changes: 26 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
2 changes: 1 addition & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const metadata: Metadata = {

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en-GB">
<html lang="en-GB" className="scroll-smooth">
<body className="min-h-screen antialiased">
<a
href="#main"
Expand Down
54 changes: 30 additions & 24 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Link from "next/link";
import { Section } from "@/components/Section";
import AppCard from "@/components/AppCard";
import Reveal from "@/components/Reveal";
import { apps, futureApps } from "@/lib/apps";

export default function HomePage() {
Expand All @@ -9,19 +10,19 @@ export default function HomePage() {
{/* Hero */}
<section className="bg-grid">
<div className="mx-auto max-w-6xl px-6 pb-24 pt-20 sm:pt-28">
<p className="mb-5 text-xs font-semibold uppercase tracking-widest text-ink-muted">
<Reveal as="p" className="mb-5 text-xs font-semibold uppercase tracking-widest text-ink-muted">
justsomething.app
</p>
<h1 className="max-w-3xl text-4xl font-semibold tracking-tight sm:text-6xl">
</Reveal>
<Reveal as="h1" delay={80} className="max-w-3xl text-4xl font-semibold tracking-tight sm:text-6xl">
Simple health tracking, without the clutter.
</h1>
<p className="mt-6 max-w-2xl text-lg text-ink-muted sm:text-xl">
</Reveal>
<Reveal as="p" delay={160} className="mt-6 max-w-2xl text-lg text-ink-muted sm:text-xl">
A growing family of focused apps. One thing each. Quick to log,
calm to use, and designed to stay out of your way.
</p>
<div className="mt-10 flex flex-wrap gap-3">
</Reveal>
<Reveal delay={240} className="mt-10 flex flex-wrap gap-3">
<Link
href="/apps"
href="#apps"
className="inline-flex items-center justify-center rounded-full bg-ink px-6 py-3 text-sm font-medium text-white transition hover:bg-ink-soft focus:outline-none focus-visible:ring-2 focus-visible:ring-ink focus-visible:ring-offset-2"
>
View apps
Expand All @@ -32,7 +33,7 @@ export default function HomePage() {
>
Get support
</Link>
</div>
</Reveal>
</div>
</section>

Expand All @@ -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."
>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{apps.map((app) => (
<AppCard key={app.slug} app={app} />
{apps.map((app, i) => (
<Reveal key={app.slug} delay={i * 100}>
<AppCard app={app} />
</Reveal>
))}
</div>
</Section>
Expand All @@ -54,7 +57,7 @@ export default function HomePage() {
<section className="border-y border-black/5 bg-white">
<div className="mx-auto max-w-6xl px-6 py-20">
<div className="grid gap-12 lg:grid-cols-2">
<div>
<Reveal>
<div className="mb-3 text-xs font-semibold uppercase tracking-widest text-ink-muted">
Philosophy
</div>
Expand All @@ -66,21 +69,23 @@ export default function HomePage() {
app focuses on a single measurement so logging feels effortless
and the numbers stay readable.
</p>
</div>
</Reveal>
<ul className="grid gap-6 sm:grid-cols-2">
{[
["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]) => (
<li
].map(([t, d], i) => (
<Reveal
key={t}
as="li"
delay={i * 80}
className="rounded-2xl border border-black/5 bg-paper p-5"
>
<div className="text-sm font-semibold">{t}</div>
<div className="mt-1 text-sm text-ink-muted">{d}</div>
</li>
</Reveal>
))}
</ul>
</div>
Expand All @@ -94,24 +99,25 @@ export default function HomePage() {
intro="We release new Just apps when they feel truly useful. Here are a few ideas we're exploring."
>
<div className="grid gap-4 sm:grid-cols-2">
{futureApps.map((f) => (
<div
{futureApps.map((f, i) => (
<Reveal
key={f.name}
delay={i * 100}
className="rounded-2xl border border-dashed border-black/15 bg-white/60 p-5"
>
<div className="text-sm font-semibold text-ink">{f.name}</div>
<div className="mt-1 text-sm text-ink-muted">{f.hint}</div>
<div className="mt-4 text-xs uppercase tracking-wider text-ink-muted">
Coming soon
</div>
</div>
</Reveal>
))}
</div>
</Section>

{/* Trust / privacy */}
<section className="bg-white">
<div className="mx-auto max-w-4xl px-6 py-20 text-center">
<Reveal className="mx-auto max-w-4xl px-6 py-20 text-center">
<div className="mb-3 text-xs font-semibold uppercase tracking-widest text-ink-muted">
Built with care
</div>
Expand All @@ -126,12 +132,12 @@ export default function HomePage() {
</Link>
.
</p>
</div>
</Reveal>
</section>

{/* Final CTA */}
<section className="bg-grid">
<div className="mx-auto max-w-4xl px-6 py-24 text-center">
<Reveal className="mx-auto max-w-4xl px-6 py-24 text-center">
<h2 className="text-3xl font-semibold tracking-tight sm:text-4xl">
Explore the Just family.
</h2>
Expand All @@ -141,7 +147,7 @@ export default function HomePage() {
</p>
<div className="mt-8 flex flex-wrap justify-center gap-3">
<Link
href="/apps"
href="#apps"
className="rounded-full bg-ink px-6 py-3 text-sm font-medium text-white hover:bg-ink-soft"
>
View apps
Expand All @@ -153,7 +159,7 @@ export default function HomePage() {
Get support
</Link>
</div>
</div>
</Reveal>
</section>
</>
);
Expand Down
5 changes: 0 additions & 5 deletions app/privacy/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,6 @@ export default function PrivacyPage() {
</section>
))}
</div>

<p className="mt-6 text-xs text-ink-muted">
This page is a starter template and should be reviewed by a
qualified legal professional before production use.
</p>
</article>
</>
);
Expand Down
19 changes: 18 additions & 1 deletion components/AppCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) {
Expand Down Expand Up @@ -33,7 +34,23 @@ export default function AppCard({ app }: { app: JustApp }) {
{app.tagline}
</div>

{app.testflightUrl && (
{app.appStoreUrl ? (
<a
href={app.appStoreUrl}
target="_blank"
rel="noopener noreferrer"
aria-label={`Download ${app.name} on the App Store`}
className="mt-6 inline-flex self-start rounded-xl transition hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-ink focus-visible:ring-offset-2"
>
<Image
src="/badges/app-store.svg"
alt="Download on the App Store"
width={180}
height={60}
className="h-12 w-auto"
/>
</a>
) : app.testflightUrl && (
<a
href={app.testflightUrl}
target="_blank"
Expand Down
56 changes: 56 additions & 0 deletions components/Reveal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use client";

import { createElement, useEffect, useRef, useState, type ReactNode } from "react";

type RevealProps = {
children: ReactNode;
delay?: number;
as?: keyof JSX.IntrinsicElements;
className?: string;
};

export default function Reveal({
children,
delay = 0,
as = "div",
className = "",
}: RevealProps) {
const ref = useRef<HTMLElement | null>(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,
);
}
5 changes: 3 additions & 2 deletions components/Section.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ReactNode } from "react";
import Reveal from "./Reveal";

export function Section({
id,
Expand All @@ -18,7 +19,7 @@ export function Section({
return (
<section id={id} className={`mx-auto max-w-6xl px-6 py-20 ${className}`}>
{(eyebrow || title || intro) && (
<header className="mb-12 max-w-2xl">
<Reveal as="header" className="mb-12 max-w-2xl">
{eyebrow && (
<div className="mb-3 text-xs font-semibold uppercase tracking-widest text-ink-muted">
{eyebrow}
Expand All @@ -32,7 +33,7 @@ export function Section({
{intro && (
<p className="mt-4 text-lg text-ink-muted">{intro}</p>
)}
</header>
</Reveal>
)}
{children}
</section>
Expand Down
Loading
Loading