Files
iptvnator/apps/website/src/components/DownloadCTA.astro
T
4grayandClaude Fable 5.1 baba0529ef feat(website): rebuild the hero, features, download and support sections (#1551)
* feat(website): rebuild the hero, features, download and support sections

Third landing redesign PR: the home page now speaks the app's own
language instead of a template's.

- Hero: left-aligned headline, a primary button that follows the
  visitor's OS (server-rendered fallback goes to /download/), an "All
  platforms" button and a text link for self-hosting. Project numbers
  (stars, downloads, languages, license) are set in the page's own
  typography from the GitHub API at build time (`lib/github-stats.ts`);
  anything the build cannot resolve is left out rather than faked. A
  small "Now playing" card with fictional demo content replaces the
  mascot, badges and social chips; the screenshot is lit by a blurred
  copy of itself, like the player's ambient mode.
- Features: a bento of interface fragments (EPG rows with progress,
  posters with a resume bar, a download queue with a REC dot, a remote
  D-pad) instead of numbered cards with icon watermarks and a marquee.
  The marquee CSS is gone.
- Download: one row per platform with the release's file names and a
  "Detected" badge for the visitor's OS (`lib/detect-platform.ts`),
  package-manager commands in the same panel.
- Support becomes a single row; the disclaimer moves into the footer,
  which also gets the mascot and a link list.
- Home sections drop the decorative eyebrows and the divider rules; the
  unused broadcast-wave, support-signal and watermark illustrations are
  removed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(website): cover the rebuilt home sections and the OS-aware download CTA

`website-home-sections.test.mjs` (in the website test target) reads the
built home page — generic hero CTA to /download/, project facts, no
badge images or dashed borders, every feature page linked from the
bento, one download row per platform with release file names and the
Detected badge hidden, the disclaimer and mascot in the footer — and
then drives it in Chromium under Windows, macOS, Linux and Android user
agents: the primary button and the Detected row follow the platform,
the phone keeps the generic markup, and the copy button writes the
command to the clipboard.

`detectPlatform()` now consults the user-agent string before the
client-hints platform and the deprecated `navigator.platform`, so the
source a visitor's tools actually change decides first; silent sources
fall through instead of vetoing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(website): keep iPads on the generic download path

iPadOS asks for desktop sites with a Macintosh user agent and a
`MacIntel` platform, so the OS guess sent iPad visitors to the macOS
installers. A "mac" verdict is now trusted only on a device without
touch points: no Mac has a touch screen, every iPad reports several.
The browser test adds an iPad-in-desktop-mode row to the generic-device
matrix next to the Android phone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 00:51:46 +02:00

143 lines
6.2 KiB
Plaintext

---
import type { ResolvedRelease } from '../lib/downloads';
import { formatReleaseDate, resolveDownloads } from '../lib/downloads';
import { DOWNLOAD_PLATFORMS, PLATFORM_ICON_PATHS, PLATFORM_LABELS, PLATFORM_PAGES } from '../lib/platforms';
interface Props {
release: ResolvedRelease;
}
const { release } = Astro.props;
const releaseDate = formatReleaseDate(release.publishedAt);
/**
* One row per platform, like the settings pages inside the app. The row lists
* the files the release actually carries and sends the visitor to the
* platform page, which has the direct links plus install and first-run notes.
*/
const rows = DOWNLOAD_PLATFORMS.map((platform) => {
const downloads = resolveDownloads(release, platform);
const shown = [...downloads.filter((d) => d.recommended), ...downloads.filter((d) => !d.recommended)].slice(0, 2);
return {
platform,
label: PLATFORM_LABELS[platform],
icon: PLATFORM_ICON_PATHS[platform],
href: PLATFORM_PAGES[platform],
files: shown.map((d) => ({ name: d.fileName, detail: d.detail })),
more: Math.max(downloads.length - shown.length, 0),
};
});
const packageManagers = [
{ label: 'Homebrew', command: 'brew install --cask iptvnator' },
{ label: 'Snap', command: 'sudo snap install iptvnator' },
{ label: 'AUR', command: 'yay -S iptvnator-bin' },
];
---
<section id="download" class="relative py-24 sm:py-28">
<div class="relative mx-auto max-w-6xl px-6">
<div class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<h2 class="font-display text-3xl font-bold tracking-tight text-surface-50 sm:text-4xl lg:text-5xl">
Get IPTVnator {release.version}
</h2>
<p class="text-base text-surface-400 sm:text-right">
{releaseDate && <span>Released {releaseDate} · </span>}
<a href={release.url} class="text-accent-400 transition-colors hover:text-accent-300" rel="noopener">Release notes</a>
</p>
</div>
<div class="mt-12 grid gap-5 lg:grid-cols-[minmax(0,1.25fr)_minmax(0,0.75fr)]" data-download-panel>
<!-- Platform rows -->
<div class="overflow-hidden rounded-xl border border-surface-800 bg-surface-900/40">
{
rows.map((row) => (
<div
class="grid gap-4 border-t border-surface-800 p-5 first:border-t-0 transition-colors duration-300 sm:grid-cols-[9.5rem_minmax(0,1fr)_auto] sm:items-center"
data-platform-row={row.platform}
>
<div class="flex items-center gap-2.5 font-display text-base font-semibold text-surface-50">
<svg class="h-[18px] w-[18px] shrink-0 text-surface-400" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d={row.icon} />
</svg>
{row.label}
<span class="hidden font-mono text-[10px] font-medium uppercase tracking-[0.1em] text-accent-400" data-detected-badge>
Detected
</span>
</div>
<ul class="min-w-0 font-mono text-xs leading-relaxed text-surface-400">
{row.files.map((file) => (
<li class="truncate" title={file.detail}>
{file.name}
</li>
))}
{row.more > 0 && <li class="text-surface-600">+ {row.more} more on the {row.label} page</li>}
</ul>
<a
href={row.href}
class="inline-flex items-center justify-center whitespace-nowrap rounded-lg border border-surface-700 px-4 py-2.5 text-sm font-semibold text-surface-100 transition-colors duration-200 hover:border-surface-500 hover:bg-surface-900"
data-platform-link
>
Download for {row.label}
</a>
</div>
))
}
</div>
<!-- Package managers -->
<div class="flex flex-col rounded-xl border border-surface-800 bg-surface-900/40">
{
packageManagers.map((pm) => (
<div class="grid grid-cols-[4.5rem_minmax(0,1fr)_auto] items-center gap-3 border-t border-surface-800 px-5 py-4 first:border-t-0">
<span class="font-mono text-[11px] uppercase tracking-[0.12em] text-surface-500">{pm.label}</span>
<code class="min-w-0 font-mono text-[13px] text-surface-100">{pm.command}</code>
<button
type="button"
class="copy-btn rounded-md border border-surface-700 px-2.5 py-1 font-mono text-[11px] text-surface-400 transition-colors hover:border-surface-500 hover:text-surface-100"
data-command={pm.command}
aria-label={`Copy ${pm.label} command`}
>
Copy
</button>
</div>
))
}
<p class="mt-auto border-t border-surface-800 px-5 py-4 text-sm leading-relaxed text-surface-400">
Prefer the browser? <a href="/iptvnator/download/docker/" class="text-accent-400 transition-colors hover:text-accent-300">Self-host the web app</a> and open it as a PWA on any device.
</p>
</div>
</div>
</div>
</section>
<script>
import { detectPlatform } from '../lib/detect-platform';
const platform = detectPlatform();
if (platform) {
const row = document.querySelector<HTMLElement>(`[data-platform-row="${platform}"]`);
row?.classList.add('bg-surface-850/60');
row?.querySelector('[data-detected-badge]')?.classList.remove('hidden');
const link = row?.querySelector('[data-platform-link]');
link?.classList.remove('border-surface-700', 'text-surface-100', 'hover:border-surface-500', 'hover:bg-surface-900');
link?.classList.add('border-transparent', 'bg-accent-500', 'text-surface-950', 'hover:bg-accent-400');
}
document.querySelectorAll<HTMLButtonElement>('.copy-btn').forEach((btn) => {
btn.addEventListener('click', async () => {
const command = btn.dataset.command;
if (!command) return;
try {
await navigator.clipboard.writeText(command);
btn.textContent = 'Copied';
window.setTimeout(() => {
btn.textContent = 'Copy';
}, 1500);
} catch {
// Clipboard access can be denied; the command stays selectable as text.
}
});
});
</script>