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
8 changes: 5 additions & 3 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,19 +96,21 @@ Also in `Settings → General → Pull Requests`:

### Release data freshness

`/download` is **prerendered**, not fetched in the browser. `components/DownloadPanel.tsx` is a server component that resolves GitHub releases at render time via `lib/releases.ts`, then hands the data to `DownloadPanelClient.tsx` for the interactive bits. Visitors never call GitHub, so the page costs one upstream request per day instead of one per visitor — comfortably under GitHub's 60/hour unauthenticated limit.
`/installation` is **prerendered**, not fetched in the browser. `components/InstallFlow.tsx` is a server component that resolves GitHub releases at render time via `lib/releases.ts`, then hands the data to `InstallFlowClient.tsx` for the interactive bits. Visitors never call GitHub, so the page costs one upstream request per day instead of one per visitor — comfortably under GitHub's 60/hour unauthenticated limit.

(`/download` is the page's former URL and permanently redirects to `/installation`; the redirect is declared in `next.config.ts`.)

Refresh is driven by the cron in `vercel.json`:

```
0 0 * * * → /api/revalidate-releases
```

At midnight UTC it purges the `releases` cache tag and the prerendered `/download` page; the next visitor triggers one fresh fetch. **A new release therefore takes up to a day to appear on the site.** That delay is intentional — a grace period to pull a release that turns out to be problematic before the website advertises it.
At midnight UTC it purges the `releases` cache tag and the prerendered `/installation` page; the next visitor triggers one fresh fetch. **A new release therefore takes up to a day to appear on the site.** That delay is intentional — a grace period to pull a release that turns out to be problematic before the website advertises it.

Two safety nets:

- `export const revalidate` on `app/download/page.tsx` (24h) refreshes the page even if the cron stops firing. It duplicates `RELEASES_REVALIDATE_SECONDS` in `lib/releases.ts` because Next requires a literal there — change both together.
- `export const revalidate` on `app/installation/page.tsx` (24h) refreshes the page even if the cron stops firing. It duplicates `RELEASES_REVALIDATE_SECONDS` in `lib/releases.ts` because Next requires a literal there — change both together.
- If GitHub is down when a refresh runs, the last good render keeps being served; visitors see nothing wrong.

To publish a release immediately, either redeploy or call the endpoint by hand:
Expand Down
4 changes: 2 additions & 2 deletions app/api/revalidate-releases/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { RELEASES_CACHE_TAG } from "../../../lib/releases";
/**
* Cron target, wired to midnight UTC daily in `vercel.json`.
*
* Drops both the cached GitHub response and the prerendered download page, so
* Drops both the cached GitHub response and the prerendered installation page, so
* the first visitor after midnight triggers one fresh fetch. New releases are
* therefore picked up with up to a day's delay — a deliberate grace period in
* case a release turns out to be problematic.
Expand All @@ -28,7 +28,7 @@ export async function GET(request: Request) {
}

revalidateTag(RELEASES_CACHE_TAG, "max");
revalidatePath("/download");
revalidatePath("/installation");

return NextResponse.json({
revalidated: true,
Expand Down
2 changes: 1 addition & 1 deletion app/features/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ export default function FeaturesPage() {
Research Foundations
</GridButton>
<GridButton
href="/download"
href="/installation"
className="text-lg px-8 py-4 rounded-lg text-cyan-400 border-cyan-400"
>
Download VoxKit
Expand Down
85 changes: 85 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,91 @@ body:has([data-full-viewport]) {
.fall-in { animation: none !important; transform: none !important; }
} */

/*
* Opting out of the browser's scroll anchoring for the installation flow.
*
* Collapsing a step removes several hundred pixels from above the viewport, and
* Chrome and Firefox respond by silently adjusting scrollTop to keep whatever
* they picked as the anchor node still. The flow is already scrolling to a step
* it chose deliberately, so that correction is a second hand on the wheel: the
* two arrive at different answers mid-transition and the result reads as the
* scrollbar twitching. One mechanism has to own scroll position, and it is the
* one that knows which step the visitor asked for.
*/
[data-install-flow] {
overflow-anchor: none;
}

/*
* The chevron on an installation-step connector, gliding down its rail to mark
* where the flow continues. Loops indefinitely because it is the page's only
* "do this next" signal once the step numbers came out of the panel corners.
*/
@keyframes stepChevronGlide {
0%,
100% {
transform: translateY(-3px);
opacity: 0.5;
}
50% {
transform: translateY(3px);
opacity: 1;
}
}
.step-chevron-glide {
animation: stepChevronGlide 1.6s ease-in-out infinite;
}

/*
* A halo expanding out of the live connector button. The rail around it is
* deliberately quiet, so this is what says "press this" from across the page.
* cyan-400 (#22d3ee) to match the ring the button already carries.
*/
@keyframes stepAdvancePulse {
0%,
100% {
box-shadow: 0 0 0 0 rgba(34, 211, 238, 0.4);
}
50% {
box-shadow: 0 0 0 10px rgba(34, 211, 238, 0);
}
}
.step-advance-pulse {
animation: stepAdvancePulse 2.4s ease-out infinite;
}

/* Indefinite loops are exactly what this preference is for. The button keeps
its ring, fill, and label, so it is no less obviously a control without the
motion; only the attention-grabbing halo and glide stop.
The panels open and close on transitions rather than keyframes, and those are
spread across utility classes on four panels and three connectors -- one
scoped rule collapses them all rather than hunting each down. */
@media (prefers-reduced-motion: reduce) {
.step-chevron-glide {
animation: none;
opacity: 1;
}
.step-advance-pulse {
animation: none;
}
/* Drop the transitions that move things -- the height ease, the padding, the
heading resize -- and keep the ones that only recolour or fade. A
cross-fade carries no motion, so it costs the preference nothing, and it
leaves the open and closed states still visibly connected rather than
replacing one with the other between frames. */
[data-step-body] {
transition-property: opacity !important;
}
[data-step-card] {
transition-property: background-color, border-color, box-shadow !important;
}
[data-step-card] h2,
[data-step-card] svg,
[data-step-mark] > * {
transition-property: color, opacity !important;
}
}

@keyframes gradient-shift {
0%,
100% {
Expand Down
29 changes: 13 additions & 16 deletions app/download/page.tsx → app/installation/page.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,38 @@
import type { Metadata } from "next";
import { Info } from "lucide-react";
import { Footer, Navbar } from "../../layout";
import DownloadPanel from "../../components/DownloadPanel";
import InstallFlow from "../../components/InstallFlow";

export const metadata: Metadata = {
title: "Download VoxKit",
title: "Installation Guide",
description:
"Download the latest VoxKit release for macOS, Windows, or Linux. Built for speech pathology researchers, no command line required.",
"Download and setup the latest version of VoxKit for macOS, Windows, or Linux. Built for speech pathology researchers, no command line required.",
};

// Fallback only: the nightly cron at /api/revalidate-releases is what normally
// refreshes this page. Keep in sync with RELEASES_REVALIDATE_SECONDS (24h) --
// Next requires a literal here, so it cannot be imported.
export const revalidate = 86400;

export default function DownloadPage() {
export default function InstallationPage() {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white">
<Navbar view="Download" />
<Navbar view="Installation" />
<div className="pt-32 pb-20 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h1 className="text-5xl font-bold mb-4 bg-gradient-to-r from-blue-400 via-blue-300 to-cyan-400 bg-clip-text text-transparent">
Download VoxKit
Installation Guide
</h1>
<p className="text-xl text-slate-300 max-w-2xl mx-auto">
Get the latest version of VoxKit for your operating system
</p>
</div>
<div className="bg-slate-800/40 border border-slate-600 rounded-xl p-4 max-w-4xl mx-auto mb-4 flex items-start gap-3">
<Info className="w-5 h-5 text-cyan-400 flex-shrink-0 mt-0.5" />
<p className="text-slate-300 text-sm">
Some researchers with university-managed devices may need to contact
their university&apos;s IT to install the app.
Download and setup the latest version of the app
</p>
</div>

<DownloadPanel />
{/* The one column the three steps share. Width lives here so the
panels stay aligned with each other rather than each choosing;
the connectors between them supply the vertical rhythm. */}
<div className="max-w-4xl mx-auto">
<InstallFlow />
</div>
</div>
<Footer />
</div>
Expand Down
12 changes: 3 additions & 9 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export default function VoxKitLanding() {
{/* CTA Buttons */}

<GridButton
href="/help/getting-started"
href="/installation"
className="text-lg px-8 py-4 rounded-lg text-cyan-400 border-cyan-400"
>
Get Started Today
Expand Down Expand Up @@ -199,16 +199,10 @@ export default function VoxKitLanding() {
Expected Workflow
</GridButton>
<GridButton
href="/help/getting-started"
href="/installation"
className="text-lg px-8 py-4 rounded-lg bg-gradient-to-r from-blue-500 to-blue-600 text-white border-white"
>
<Users className="w-5 h-5" /> Getting Started
</GridButton>
<GridButton
href="/download"
className="text-lg px-8 py-4 rounded-lg bg-gradient-to-r from-blue-500 to-blue-600 text-white border-white"
>
<Download className="w-5 h-5" /> Download Now
<Download className="w-5 h-5" /> Install VoxKit
</GridButton>
</div>
</div>
Expand Down
Loading
Loading