Files
IdP/apps/frontend/app/downloads/page.tsx
2026-07-07 11:06:14 +03:00

206 lines
9.3 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';
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 latestRelease = useMemo(
() => releases.find((item) => item.platform === 'ANDROID') ?? null,
[releases]
);
const androidHistory = useMemo(
() => releases.filter((item) => item.platform === 'ANDROID'),
[releases]
);
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. Каждый релиз подписан 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>
) : latestRelease ? (
<>
<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{latestRelease.version}</span>
<span className="rounded-full bg-[#eef4ff] px-2.5 py-1 text-xs font-medium text-[#3390ec]">
build {latestRelease.versionCode}
</span>
</div>
<p className="mt-2 text-sm text-[#667085]">
{formatBytes(latestRelease.fileSize)} · {formatDate(latestRelease.createdAt)}
</p>
</div>
{latestRelease.releaseNotes ? (
<p className="rounded-2xl bg-[#f8f9fb] px-4 py-3 text-sm leading-relaxed text-[#1f2430]">
{latestRelease.releaseNotes}
</p>
) : null}
<div className="flex flex-col gap-3 sm:flex-row">
<Button className="flex-1 rounded-xl" asChild>
<a href="/downloads/android">
<Download className="mr-2 h-4 w-4" />
Скачать последнюю
</a>
</Button>
<Button variant="outline" className="flex-1 rounded-xl" asChild>
<a href={buildAppReleaseDownloadUrl('ANDROID', latestRelease.id)}>
Прямая ссылка
<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-3">
{loading ? (
<p className="text-sm text-[#667085]">Загружаем список версий...</p>
) : androidHistory.length ? (
androidHistory.map((release, index) => (
<div
key={release.id}
className="flex flex-wrap items-center justify-between gap-3 rounded-2xl bg-[#f8f9fb] px-4 py-3"
>
<div>
<div className="flex items-center gap-2">
<span className="font-medium text-[#1f2430]">v{release.version}</span>
<span className="text-xs text-[#667085]">build {release.versionCode}</span>
{index === 0 ? (
<span className="rounded-full bg-[#e8f8ee] px-2 py-0.5 text-[11px] font-medium text-[#1a7f37]">
Актуальная
</span>
) : null}
</div>
<p className="mt-1 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>
))
) : (
<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>{' '}
и сравнивать SHA-256 перед установкой. Постоянная ссылка на последнюю версию:
<span className="mt-2 block font-medium text-[#1f2430]">/downloads/android</span>
</p>
</div>
</div>
</section>
</div>
</main>
);
}