diff --git a/apps/frontend/app/admin/releases/page.tsx b/apps/frontend/app/admin/releases/page.tsx index f62058f..9e55914 100644 --- a/apps/frontend/app/admin/releases/page.tsx +++ b/apps/frontend/app/admin/releases/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { Download, Loader2, Monitor, Smartphone, Trash2, Upload } from 'lucide-react'; +import { Download, Loader2, Smartphone, Trash2, Upload } from 'lucide-react'; import { useAuth } from '@/components/id/auth-provider'; import { AdminShell } from '@/components/id/admin-shell'; import { useToast } from '@/components/id/toast-provider'; @@ -9,14 +9,15 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { AppRelease, - AppReleasePlatform, deleteAdminAppRelease, fetchAdminAppReleases, getApiErrorMessage, updateAdminAppRelease, uploadAdminAppRelease } from '@/lib/api'; -import { cn } from '@/lib/utils'; + +const ANDROID_PLATFORM = 'ANDROID' as const; +const APK_ACCEPT = '.apk,application/vnd.android.package-archive'; function formatBytes(value: string) { const size = Number(value); @@ -41,18 +42,12 @@ function formatDate(value: string) { }).format(new Date(value)); } -const platformMeta: Record = { - ANDROID: { label: 'Android', icon: Smartphone, accept: '.apk,application/vnd.android.package-archive' }, - WINDOWS: { label: 'Windows', icon: Monitor, accept: '.exe,application/vnd.microsoft.portable-executable' } -}; - export default function AdminReleasesPage() { const { token, user } = useAuth(); const { showToast } = useToast(); const [releases, setReleases] = useState([]); const [loading, setLoading] = useState(true); const [uploading, setUploading] = useState(false); - const [platform, setPlatform] = useState('ANDROID'); const [version, setVersion] = useState(''); const [versionCode, setVersionCode] = useState(''); const [releaseNotes, setReleaseNotes] = useState(''); @@ -64,7 +59,7 @@ export default function AdminReleasesPage() { if (!token || !canManage) return; setLoading(true); try { - const response = await fetchAdminAppReleases(token); + const response = await fetchAdminAppReleases(token, ANDROID_PLATFORM); setReleases(response.releases ?? []); } catch (error) { showToast(getApiErrorMessage(error, 'Не удалось загрузить релизы') ?? 'Ошибка'); @@ -77,11 +72,10 @@ export default function AdminReleasesPage() { void loadReleases(); }, [loadReleases]); - const grouped = useMemo(() => { - const android = releases.filter((item) => item.platform === 'ANDROID'); - const windows = releases.filter((item) => item.platform === 'WINDOWS'); - return { android, windows }; - }, [releases]); + const androidReleases = useMemo( + () => releases.filter((item) => item.platform === ANDROID_PLATFORM), + [releases] + ); async function handleUpload(event: React.FormEvent) { event.preventDefault(); @@ -98,7 +92,7 @@ export default function AdminReleasesPage() { setUploading(true); try { const created = await uploadAdminAppRelease(token, { - platform, + platform: ANDROID_PLATFORM, version: version.trim(), versionCode: parsedVersionCode, releaseNotes: releaseNotes.trim() || undefined, @@ -130,7 +124,7 @@ export default function AdminReleasesPage() { async function removeRelease(release: AppRelease) { if (!token) return; - if (!window.confirm(`Удалить версию ${release.version} (${platformMeta[release.platform].label})?`)) return; + if (!window.confirm(`Удалить версию ${release.version} (Android)?`)) return; try { await deleteAdminAppRelease(token, release.id); setReleases((current) => current.filter((item) => item.id !== release.id)); @@ -151,14 +145,13 @@ export default function AdminReleasesPage() { return (
{items.map((release, index) => { - const Icon = platformMeta[release.platform].icon; const isLatest = index === 0 && release.isPublished; return (
- +
@@ -188,7 +181,7 @@ export default function AdminReleasesPage() {
void handleUpload(event)}> -
- {(['ANDROID', 'WINDOWS'] as AppReleasePlatform[]).map((item) => { - const meta = platformMeta[item]; - const Icon = meta.icon; - return ( - - ); - })} -
- @@ -303,16 +269,10 @@ export default function AdminReleasesPage() {
-
-
-

Android

- {renderReleaseList(grouped.android, 'Релизы Android пока не загружены')} -
-
-

Windows

- {renderReleaseList(grouped.windows, 'Релизы Windows пока не загружены')} -
-
+
+

Версии Android

+ {renderReleaseList(androidReleases, 'Релизы Android пока не загружены')} +
); diff --git a/apps/frontend/app/downloads/page.tsx b/apps/frontend/app/downloads/page.tsx index ecaef59..df82ecd 100644 --- a/apps/frontend/app/downloads/page.tsx +++ b/apps/frontend/app/downloads/page.tsx @@ -2,13 +2,12 @@ import { useEffect, useMemo, useState } from 'react'; import Link from 'next/link'; -import { ArrowRight, Download, Monitor, ShieldCheck, Smartphone, Sparkles } from 'lucide-react'; +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, - AppReleasePlatform, buildAppReleaseDownloadUrl, fetchPublicAppReleases } from '@/lib/api'; @@ -34,32 +33,6 @@ function formatDate(value: string) { }).format(new Date(value)); } -const platformCards: Array<{ - platform: AppReleasePlatform; - slug: 'android' | 'windows'; - title: string; - subtitle: string; - icon: typeof Smartphone; - gradient: string; -}> = [ - { - platform: 'ANDROID', - slug: 'android', - title: 'Android', - subtitle: 'Скачайте APK для телефона или планшета', - icon: Smartphone, - gradient: 'from-[#34c759] to-[#0f9d58]' - }, - { - platform: 'WINDOWS', - slug: 'windows', - title: 'Windows', - subtitle: 'Установщик для компьютера', - icon: Monitor, - gradient: 'from-[#3390ec] to-[#1d4ed8]' - } -]; - export default function DownloadsPage() { const { projectName } = usePublicSettings(); const [releases, setReleases] = useState([]); @@ -68,7 +41,7 @@ export default function DownloadsPage() { useEffect(() => { void (async () => { try { - const response = await fetchPublicAppReleases(); + const response = await fetchPublicAppReleases('ANDROID'); setReleases(response.releases ?? []); } finally { setLoading(false); @@ -76,24 +49,19 @@ export default function DownloadsPage() { })(); }, []); - const latestByPlatform = useMemo(() => { - const map = new Map(); - for (const release of releases) { - if (!map.has(release.platform)) { - map.set(release.platform, release); - } - } - return map; - }, [releases]); + const latestRelease = useMemo( + () => releases.find((item) => item.platform === 'ANDROID') ?? null, + [releases] + ); - const historyByPlatform = useMemo(() => ({ - ANDROID: releases.filter((item) => item.platform === 'ANDROID'), - WINDOWS: releases.filter((item) => item.platform === 'WINDOWS') - }), [releases]); + const androidHistory = useMemo( + () => releases.filter((item) => item.platform === 'ANDROID'), + [releases] + ); return (
-
+
- -
- - ) : ( -

Сборка для этой платформы пока не опубликована.

- )} +
+

Android

+

Скачайте APK для телефона или планшета

- ); - })} +
+ +
+ {loading ? ( +

Проверяем доступные версии...

+ ) : latestRelease ? ( + <> +
+

Последняя версия

+
+ v{latestRelease.version} + + build {latestRelease.versionCode} + +
+

+ {formatBytes(latestRelease.fileSize)} · {formatDate(latestRelease.createdAt)} +

+
+ + {latestRelease.releaseNotes ? ( +

+ {latestRelease.releaseNotes} +

+ ) : null} + + + + ) : ( +

Сборка для Android пока не опубликована.

+ )} +
+
-
- {platformCards.map((card) => { - const history = historyByPlatform[card.platform]; - return ( -
-

История версий · {card.title}

-
- {loading ? ( -

Загружаем список версий...

- ) : history.length ? ( - history.map((release, index) => ( -
-
-
- v{release.version} - build {release.versionCode} - {index === 0 ? ( - - Актуальная - - ) : null} -
-

- {formatDate(release.createdAt)} · {formatBytes(release.fileSize)} -

-
- -
- )) - ) : ( -

Версии пока не опубликованы.

- )} +
+

История версий

+
+ {loading ? ( +

Загружаем список версий...

+ ) : androidHistory.length ? ( + androidHistory.map((release, index) => ( +
+
+
+ v{release.version} + build {release.versionCode} + {index === 0 ? ( + + Актуальная + + ) : null} +
+

+ {formatDate(release.createdAt)} · {formatBytes(release.fileSize)} +

+
+
-
- ); - })} + )) + ) : ( +

Версии пока не опубликованы.

+ )} +
@@ -241,9 +193,8 @@ export default function DownloadsPage() {

Мобильный клиент может запрашивать{' '} GET /idp-api/releases/check?platform=ANDROID&versionCode=...{' '} - и сравнивать SHA-256 перед установкой. Постоянные ссылки на последнюю версию: + и сравнивать SHA-256 перед установкой. Постоянная ссылка на последнюю версию: /downloads/android - /downloads/windows

diff --git a/apps/frontend/app/downloads/windows/[releaseId]/route.ts b/apps/frontend/app/downloads/windows/[releaseId]/route.ts index f43940f..53400b9 100644 --- a/apps/frontend/app/downloads/windows/[releaseId]/route.ts +++ b/apps/frontend/app/downloads/windows/[releaseId]/route.ts @@ -1,9 +1,8 @@ -import type { NextRequest } from 'next/server'; -import { proxyReleaseDownload } from '@/lib/proxy-release-download'; +import { NextRequest, NextResponse } from 'next/server'; export const runtime = 'nodejs'; -export async function GET(request: NextRequest, context: { params: Promise<{ releaseId: string }> }) { - const { releaseId } = await context.params; - return proxyReleaseDownload(request, 'windows', releaseId); +export async function GET(request: NextRequest) { + const target = new URL('/downloads', request.nextUrl.origin); + return NextResponse.redirect(target, 302); } diff --git a/apps/frontend/app/downloads/windows/route.ts b/apps/frontend/app/downloads/windows/route.ts index 4ed5c82..53400b9 100644 --- a/apps/frontend/app/downloads/windows/route.ts +++ b/apps/frontend/app/downloads/windows/route.ts @@ -1,8 +1,8 @@ -import type { NextRequest } from 'next/server'; -import { proxyReleaseDownload } from '@/lib/proxy-release-download'; +import { NextRequest, NextResponse } from 'next/server'; export const runtime = 'nodejs'; export async function GET(request: NextRequest) { - return proxyReleaseDownload(request, 'windows'); + const target = new URL('/downloads', request.nextUrl.origin); + return NextResponse.redirect(target, 302); }