diff --git a/frontend/src/features/download/api.ts b/frontend/src/features/download/api.ts index 93370e8..50f4719 100644 --- a/frontend/src/features/download/api.ts +++ b/frontend/src/features/download/api.ts @@ -4,14 +4,18 @@ const DEFAULT_REPO = 'loki5512344/LoVisual-' export const GITHUB_REPO: string = import.meta.env.VITE_GITHUB_REPO || DEFAULT_REPO /** - * One-click download of the newest build. Served directly from our own origin (not a - * github.com redirect) so the button keeps working even where GitHub itself is blocked. - * The file at `/downloads/lovisual.jar` on the server is NOT rebuilt automatically — - * whoever cuts a mod release has to `./gradlew build` and re-upload it there by hand - * (this is a separate Gradle project from the site, deliberately not wired into + * One-click download of the newest build. Served by our own gateway + * (`GET /downloads/lovisual.jar`, reached as `/api/...` through the site origin's + * nginx), so the button keeps working even where GitHub itself is blocked. + * The file on the server is NOT rebuilt automatically — whoever cuts a mod + * release has to `./gradlew build` and re-upload it there by hand (the mod is a + * separate Gradle project from the site, deliberately not wired into * `backend/deploy/deploy.sh`). */ -export const DOWNLOAD_URL = '/downloads/lovisual.jar' +export const DOWNLOAD_URL = '/api/downloads/lovisual.jar' + +/** Releases page — the secondary, GitHub-hosted path (shown behind a ribbon for now). */ +export const GITHUB_RELEASES_URL = `https://github.com/${GITHUB_REPO}/releases` /** A published GitHub release, narrowed to the fields the download page renders. */ export type Release = { diff --git a/frontend/src/features/download/i18n/en.ts b/frontend/src/features/download/i18n/en.ts index ff38d81..b6789d3 100644 --- a/frontend/src/features/download/i18n/en.ts +++ b/frontend/src/features/download/i18n/en.ts @@ -4,10 +4,12 @@ import type { downloadRu } from './ru' export const downloadEn = { nav: 'Download', button: 'Download for free', - hint: 'One .jar straight from GitHub', + hint: 'One .jar straight from our server', versionHint: 'v{version} · {size}', page: { title: 'Download LoVisual', + githubButton: 'Code on GitHub', + githubSoon: 'temporarily unavailable', lead: 'A free visuals mod for Minecraft 26.2 on Fabric. One file: no installers, no ads.', reqTitle: 'What you need', reqMc: 'Minecraft', diff --git a/frontend/src/features/download/i18n/ru.ts b/frontend/src/features/download/i18n/ru.ts index f985746..47e5702 100644 --- a/frontend/src/features/download/i18n/ru.ts +++ b/frontend/src/features/download/i18n/ru.ts @@ -1,10 +1,12 @@ export const downloadRu = { nav: 'Скачать', button: 'Скачать бесплатно', - hint: 'Один .jar прямо с GitHub', + hint: 'Один .jar прямо с нашего сервера', versionHint: 'v{version} · {size}', page: { title: 'Скачать LoVisual', + githubButton: 'Код на GitHub', + githubSoon: 'временно не работает', lead: 'Бесплатный визуальный мод для Minecraft 26.2 на Fabric. Один файл: без установщиков и без рекламы.', reqTitle: 'Что понадобится', reqMc: 'Minecraft', diff --git a/frontend/src/features/download/tests/download.test.tsx b/frontend/src/features/download/tests/download.test.tsx index 92579b0..c9d1ca5 100644 --- a/frontend/src/features/download/tests/download.test.tsx +++ b/frontend/src/features/download/tests/download.test.tsx @@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { afterEach, expect, test, vi } from 'vitest' import { DownloadButton } from '../DownloadButton' import { ReleaseNotes } from '../ReleaseNotes' +import DownloadPage from '../../../pages/download/DownloadPage' function withQuery(ui: React.ReactElement) { const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }) @@ -20,13 +21,28 @@ const release = (body: string) => ({ afterEach(() => vi.restoreAllMocks()) -test('DownloadButton links straight to the latest lovisual.jar', () => { +test('DownloadButton links straight to the latest lovisual.jar on our own backend', () => { const { container } = withQuery() const link = container.querySelector('a') - expect(link?.getAttribute('href')).toBe('/downloads/lovisual.jar') + expect(link?.getAttribute('href')).toBe('/api/downloads/lovisual.jar') expect(link?.hasAttribute('download')).toBe(true) }) +test('the page offers both paths: our jar and a GitHub button locked behind a ribbon', async () => { + vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('offline')) + withQuery() + + const direct = await screen.findByRole('link', { name: 'Скачать бесплатно' }) + expect(direct).toHaveAttribute('href', '/api/downloads/lovisual.jar') + expect(direct).toHaveAttribute('download') + + // The GitHub path is visible but not a link, and says why. + const gh = screen.getByText('Код на GitHub') + expect(gh.closest('a')).toBeNull() + expect(gh).toHaveAttribute('aria-disabled', 'true') + expect(screen.getByText('временно не работает')).toBeInTheDocument() +}) + test('ReleaseNotes renders Markdown but never raw HTML from a release body', async () => { vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, diff --git a/frontend/src/features/landing/i18n/en.ts b/frontend/src/features/landing/i18n/en.ts index fac36c4..0d2f905 100644 --- a/frontend/src/features/landing/i18n/en.ts +++ b/frontend/src/features/landing/i18n/en.ts @@ -8,7 +8,7 @@ export const landingEn = { headlineAccent: "you'll fall in love with", lead: 'A free Minecraft 26.2 mod: wings, particles, your own HUD, weather and fifty-odd other visual modules. Not a cheat, just pretty.', download: 'Download for free', - downloadHint: 'One .jar from GitHub: drop it in mods and play', + downloadHint: 'One .jar from our server: drop it in mods and play', register: 'Create account', facts: { modules: 'modules', @@ -89,7 +89,7 @@ export const landingEn = { q5: 'How do I move my settings to another PC?', a5: 'Save your config to a cloud slot, then load it on the other PC with the slot’s code.', downloadTitle: 'Grab it and jump in', - downloadLead: 'One .jar from GitHub. Drop it in your mods folder, launch the game with Fabric, open the menu.', + downloadLead: 'One .jar from our server. Drop it in your mods folder, launch the game with Fabric, open the menu.', downloadCta: 'Download for free', downloadNote: 'Latest release for Minecraft 26.2 on Fabric', releaseLink: 'All versions on GitHub', diff --git a/frontend/src/features/landing/i18n/ru.ts b/frontend/src/features/landing/i18n/ru.ts index 117082a..bf3d1a0 100644 --- a/frontend/src/features/landing/i18n/ru.ts +++ b/frontend/src/features/landing/i18n/ru.ts @@ -5,7 +5,7 @@ export const landingRu = { headlineAccent: 'в которые влюбляешься', lead: 'Бесплатный мод на Minecraft 26.2: крылья, партиклы, свой HUD, погода и ещё полсотни визуальных модулей. Не чит, просто красиво.', download: 'Скачать бесплатно', - downloadHint: 'Один .jar с GitHub: кидаешь в mods и играешь', + downloadHint: 'Один .jar с нашего сервера: кидаешь в mods и играешь', register: 'Создать аккаунт', facts: { modules: 'модулей', @@ -86,7 +86,7 @@ export const landingRu = { q5: 'Как перенести настройки на другой комп?', a5: 'Сохрани конфиг в облачный слот, а на другом компе загрузи его по коду слота.', downloadTitle: 'Качай и заходи в игру', - downloadLead: 'Один .jar с GitHub. Кидаешь его в папку mods, запускаешь игру через Fabric, открываешь меню.', + downloadLead: 'Один .jar с нашего сервера. Кидаешь его в папку mods, запускаешь игру через Fabric, открываешь меню.', downloadCta: 'Скачать бесплатно', downloadNote: 'Последний релиз для Minecraft 26.2 на Fabric', releaseLink: 'Все версии на GitHub', diff --git a/frontend/src/features/landing/sections/explore/FaqDownload.tsx b/frontend/src/features/landing/sections/explore/FaqDownload.tsx index 9426f6b..48f9952 100644 --- a/frontend/src/features/landing/sections/explore/FaqDownload.tsx +++ b/frontend/src/features/landing/sections/explore/FaqDownload.tsx @@ -1,5 +1,6 @@ import { useTranslation } from 'react-i18next' -import { DOWNLOAD_URL, GITHUB_REPO } from '@features/download/api' +import { Link } from 'react-router' +import { GITHUB_RELEASES_URL } from '@features/download/api' import '@features/landing/styles/landing.css' import '@features/landing/styles/sections.css' @@ -40,12 +41,12 @@ export function FaqDownload() {

{t('faq.downloadTitle')}

{t('faq.downloadLead')}

- + {t('faq.downloadCta')} - + - + {t('hero.download')} - + {t('hero.register')} diff --git a/frontend/src/features/landing/tests/landing.test.tsx b/frontend/src/features/landing/tests/landing.test.tsx index 215c0cb..7faa645 100644 --- a/frontend/src/features/landing/tests/landing.test.tsx +++ b/frontend/src/features/landing/tests/landing.test.tsx @@ -59,3 +59,20 @@ test('cloud section speaks in player terms in both languages', async () => { unmount() } }) + +test('hero download CTA goes to the page with both download buttons', async () => { + vi.stubGlobal('matchMedia', (q: string) => ({ matches: q.includes('reduce'), addEventListener() {}, removeEventListener() {} })) + mockFetch({ 'POST /auth/refresh': () => json({}, 401) }) + const router = createMemoryRouter([{ path: '/', element: }]) + // Explicit ru: an earlier test in this file re-inits the default instance in English. + render( + + + + + , + ) + const cta = await screen.findByRole('link', { name: 'Скачать бесплатно' }) + expect(cta).toHaveAttribute('href', '/download') + expect(cta).not.toHaveAttribute('download') +}) diff --git a/frontend/src/pages/download/DownloadPage.tsx b/frontend/src/pages/download/DownloadPage.tsx index f2acea7..0fd4a72 100644 --- a/frontend/src/pages/download/DownloadPage.tsx +++ b/frontend/src/pages/download/DownloadPage.tsx @@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { DownloadButton } from '@features/download/DownloadButton' import { ReleaseNotes } from '@features/download/ReleaseNotes' -import { GITHUB_REPO, fetchReleases, releaseSize } from '@features/download/api' +import { GITHUB_RELEASES_URL, fetchReleases, releaseSize } from '@features/download/api' import { PageTitle } from '@shared/layout/PageTitle' // Requirement versions mirror mod/gradle.properties (the mod's build source of truth). @@ -12,6 +12,25 @@ const REQUIREMENTS = [ { key: 'reqApi', value: '0.152.2+26.2' }, ] as const +/** + * The second download path, kept visible but locked: GitHub releases are + * temporarily off (see TODO.md «Скачивание»), so the button is not a link and + * carries a ribbon saying so. + */ +function GithubButtonOnHold() { + const { t } = useTranslation('download') + return ( + + + {t('page.githubButton')} + + + {t('page.githubSoon')} + + + ) +} + export default function DownloadPage() { const { t } = useTranslation('download') const { data } = useQuery({ queryKey: ['releases'], queryFn: fetchReleases, staleTime: 10 * 60 * 1000 }) @@ -22,12 +41,14 @@ export default function DownloadPage() {
{t('page.title')} - +
+ + +
@@ -61,7 +82,7 @@ export default function DownloadPage() {

{t('notes.title')}

{t('footer.sectionProject')} - {project.map((i) => ( - - {i.label} - - ))} + {project.map((i) => + i.to ? ( + {i.label} + ) : ( + + {i.label} + + ), + )}