Files
IdP/apps/frontend/app/downloads/page.tsx
2026-07-07 17:36:01 +03:00

241 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useEffect, useMemo, useState } from 'react';
import Link from 'next/link';
import { ArrowRight, Download, ShieldCheck, Smartphone, Sparkles } from 'lucide-react';
import { BrandLogo } from '@/components/id/brand-logo';
import { usePublicSettings } from '@/components/id/public-settings-provider';
import { Button } from '@/components/ui/button';
import {
AppRelease,
buildAppReleaseDownloadUrl,
fetchPublicAppReleases
} from '@/lib/api';
import { formatReleaseVariantLabel, groupReleasesByVersion } from '@/lib/app-release-variants';
function formatBytes(value: string) {
const size = Number(value);
if (!Number.isFinite(size) || size <= 0) return '—';
const units = ['Б', 'КБ', 'МБ', 'ГБ'];
let amount = size;
let unit = 0;
while (amount >= 1024 && unit < units.length - 1) {
amount /= 1024;
unit += 1;
}
return `${amount.toFixed(amount >= 10 || unit === 0 ? 0 : 1)} ${units[unit]}`;
}
function formatDate(value: string) {
return new Intl.DateTimeFormat('ru-RU', {
day: '2-digit',
month: 'long',
year: 'numeric'
}).format(new Date(value));
}
export default function DownloadsPage() {
const { projectName } = usePublicSettings();
const [releases, setReleases] = useState<AppRelease[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
void (async () => {
try {
const response = await fetchPublicAppReleases('ANDROID');
setReleases(response.releases ?? []);
} finally {
setLoading(false);
}
})();
}, []);
const groupedReleases = useMemo(
() => groupReleasesByVersion(releases.filter((item) => item.platform === 'ANDROID')),
[releases]
);
const latestGroup = groupedReleases[0] ?? null;
const latestVariants = latestGroup?.variants ?? [];
const preferredLatest =
latestVariants.find((item) => item.variant === 'universal') ?? latestVariants[0] ?? null;
return (
<main className="min-h-screen bg-[linear-gradient(180deg,#f7f9fc_0%,#ffffff_42%,#f4f7fb_100%)]">
<div className="mx-auto max-w-4xl px-4 py-10 sm:px-6 lg:py-14">
<div className="mb-10 flex flex-wrap items-center justify-between gap-4">
<BrandLogo />
<Button variant="outline" className="rounded-full" asChild>
<Link href="/auth/login">Войти в аккаунт</Link>
</Button>
</div>
<section className="relative overflow-hidden rounded-[32px] border border-[#e8edf5] bg-white px-6 py-10 shadow-[0_24px_80px_rgba(31,36,48,0.08)] sm:px-10 sm:py-12">
<div className="absolute -right-16 -top-16 h-56 w-56 rounded-full bg-[#eef4ff] blur-3xl" />
<div className="absolute -bottom-20 left-10 h-48 w-48 rounded-full bg-[#e8f8ee] blur-3xl" />
<div className="relative max-w-3xl">
<div className="mb-4 inline-flex items-center gap-2 rounded-full bg-[#eef4ff] px-3 py-1 text-sm font-medium text-[#3390ec]">
<Sparkles className="h-4 w-4" />
Официальная сборка {projectName}
</div>
<h1 className="text-4xl font-semibold tracking-tight text-[#1f2430] sm:text-5xl">
Скачайте приложение
</h1>
<p className="mt-4 max-w-2xl text-base leading-relaxed text-[#667085] sm:text-lg">
Актуальная версия для Android. Доступны разные сборки: universal, arm64-v8a и другие.
Каждый файл подписан SHA-256.
</p>
</div>
</section>
<section className="mt-8">
<div className="overflow-hidden rounded-[28px] border border-[#eceef4] bg-white shadow-[0_18px_50px_rgba(31,36,48,0.06)]">
<div className="bg-gradient-to-br from-[#34c759] to-[#0f9d58] px-6 py-8 text-white">
<div className="flex items-center gap-3">
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-white/15 backdrop-blur">
<Smartphone className="h-7 w-7" />
</div>
<div>
<h2 className="text-2xl font-semibold">Android</h2>
<p className="mt-1 text-sm text-white/85">Выберите подходящую сборку APK</p>
</div>
</div>
</div>
<div className="space-y-5 px-6 py-6">
{loading ? (
<p className="text-sm text-[#667085]">Проверяем доступные версии...</p>
) : latestGroup && preferredLatest ? (
<>
<div>
<p className="text-sm text-[#667085]">Последняя версия</p>
<div className="mt-2 flex flex-wrap items-center gap-2">
<span className="text-2xl font-semibold text-[#1f2430]">v{latestGroup.version}</span>
<span className="rounded-full bg-[#eef4ff] px-2.5 py-1 text-xs font-medium text-[#3390ec]">
build {latestGroup.versionCode}
</span>
{latestVariants.length > 1 ? (
<span className="rounded-full bg-[#f4f5f8] px-2.5 py-1 text-xs font-medium text-[#667085]">
{latestVariants.length} сборки
</span>
) : null}
</div>
<p className="mt-2 text-sm text-[#667085]">
{formatDate(preferredLatest.createdAt)}
</p>
</div>
{preferredLatest.releaseNotes ? (
<p className="rounded-2xl bg-[#f8f9fb] px-4 py-3 text-sm leading-relaxed text-[#1f2430]">
{preferredLatest.releaseNotes}
</p>
) : null}
<div className="grid gap-3 sm:grid-cols-2">
{latestVariants.map((release) => (
<div
key={release.id}
className="flex flex-col justify-between rounded-2xl border border-[#eceef4] bg-[#f8f9fb] p-4"
>
<div>
<p className="font-medium text-[#1f2430]">{formatReleaseVariantLabel(release.variant)}</p>
<p className="mt-1 text-xs text-[#667085]">
{formatBytes(release.fileSize)} · {release.fileName}
</p>
</div>
<Button className="mt-4 rounded-xl" asChild>
<a href={buildAppReleaseDownloadUrl('ANDROID', release.id)}>
<Download className="mr-2 h-4 w-4" />
Скачать
</a>
</Button>
</div>
))}
</div>
<div className="flex flex-col gap-3 sm:flex-row">
<Button className="flex-1 rounded-xl" asChild>
<a href={buildAppReleaseDownloadUrl('ANDROID', preferredLatest.id)}>
<Download className="mr-2 h-4 w-4" />
Скачать рекомендуемую
</a>
</Button>
<Button variant="outline" className="flex-1 rounded-xl" asChild>
<a href={buildAppReleaseDownloadUrl('ANDROID', undefined, 'universal')}>
Universal по ссылке
<ArrowRight className="ml-2 h-4 w-4" />
</a>
</Button>
</div>
</>
) : (
<p className="text-sm text-[#667085]">Сборка для Android пока не опубликована.</p>
)}
</div>
</div>
</section>
<section className="mt-10 rounded-[24px] border border-[#eceef4] bg-white p-6 shadow-sm">
<h3 className="text-xl font-semibold">История версий</h3>
<div className="mt-4 space-y-4">
{loading ? (
<p className="text-sm text-[#667085]">Загружаем список версий...</p>
) : groupedReleases.length ? (
groupedReleases.map((group, groupIndex) => (
<div key={group.key} className="rounded-2xl bg-[#f8f9fb] px-4 py-4">
<div className="mb-3 flex flex-wrap items-center gap-2">
<span className="font-medium text-[#1f2430]">v{group.version}</span>
<span className="text-xs text-[#667085]">build {group.versionCode}</span>
{groupIndex === 0 ? (
<span className="rounded-full bg-[#e8f8ee] px-2 py-0.5 text-[11px] font-medium text-[#1a7f37]">
Актуальная
</span>
) : null}
</div>
<div className="space-y-2">
{group.variants.map((release) => (
<div key={release.id} className="flex flex-wrap items-center justify-between gap-3 rounded-xl bg-white px-3 py-2">
<div>
<p className="text-sm font-medium text-[#1f2430]">
{formatReleaseVariantLabel(release.variant)}
</p>
<p className="text-xs text-[#667085]">
{formatDate(release.createdAt)} · {formatBytes(release.fileSize)}
</p>
</div>
<Button variant="outline" size="sm" className="rounded-xl" asChild>
<a href={buildAppReleaseDownloadUrl('ANDROID', release.id)}>Скачать</a>
</Button>
</div>
))}
</div>
</div>
))
) : (
<p className="text-sm text-[#667085]">Версии пока не опубликованы.</p>
)}
</div>
</section>
<section className="mt-10 rounded-[24px] border border-[#eceef4] bg-white p-6 shadow-sm">
<div className="flex items-start gap-3">
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-[#eef4ff] text-[#3390ec]">
<ShieldCheck className="h-5 w-5" />
</div>
<div>
<h3 className="text-lg font-semibold">Проверка обновлений в приложении</h3>
<p className="mt-2 text-sm leading-relaxed text-[#667085]">
Мобильный клиент может запрашивать{' '}
<code className="rounded bg-[#f4f5f8] px-1.5 py-0.5">GET /idp-api/releases/check?platform=ANDROID&amp;versionCode=...</code>{' '}
и получать список всех сборок последней версии в поле <code className="rounded bg-[#f4f5f8] px-1.5 py-0.5">variants</code>.
Для конкретной архитектуры используйте{' '}
<code className="rounded bg-[#f4f5f8] px-1.5 py-0.5">/downloads/android?variant=arm64-v8a</code>.
</p>
</div>
</div>
</section>
</div>
</main>
);
}