79 lines
3.1 KiB
TypeScript
79 lines
3.1 KiB
TypeScript
import { useTranslation } from 'react-i18next'
|
||
import { Button } from '@/ui'
|
||
import { ArrowClockwise, BookOpen, X } from '@phosphor-icons/react'
|
||
import { useState } from 'react'
|
||
import { useAppVersion } from './useAppVersion'
|
||
|
||
const DOCS_URL = '/docs/' // ordinis-docs deployment, mirrors prod /docs path
|
||
|
||
/**
|
||
* Banner — показывается top-of-page когда client bundle отстаёт от server.
|
||
*
|
||
* <p>Pattern такой же как Altum VS / Geoportal: persistent strip, contextual
|
||
* CTAs, dismissable (но re-appears после next refetch если still mismatched).
|
||
*
|
||
* <p>Two CTAs:
|
||
* <ul>
|
||
* <li><b>Обновить</b> — {@code window.location.reload()}. Browser fetches new
|
||
* bundle (cache-busting через Vite hashed filenames).</li>
|
||
* <li><b>Что нового →</b> — opens user руководство (changelog / release notes).</li>
|
||
* </ul>
|
||
*
|
||
* <p>Dismiss button (×) — temporarily скрывает banner до следующего
|
||
* {@code useServerVersion} refetch (5 min). Не persists в localStorage —
|
||
* критичный update должен progressively становиться более insistent.
|
||
*/
|
||
export function UpdateBanner() {
|
||
const { t } = useTranslation()
|
||
const { updateAvailable, serverVersion, serverCommit, serverTag } = useAppVersion()
|
||
const [dismissed, setDismissed] = useState(false)
|
||
|
||
if (!updateAvailable || dismissed) return null
|
||
|
||
// serverTag === 'none' для branch builds (Spring Boot build-info default
|
||
// sentinel non-null). Не показываем "none" как версию — fallback к
|
||
// serverVersion (e.g. "0.1.0-SNAPSHOT") и потом к 7-char commit hash.
|
||
const tag = serverTag && serverTag !== 'none' ? serverTag : null
|
||
const versionLabel = tag || serverVersion || serverCommit?.substring(0, 7) || ''
|
||
|
||
return (
|
||
<div
|
||
role="status"
|
||
aria-live="polite"
|
||
className="bg-accent text-on-accent"
|
||
>
|
||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-2 flex items-center gap-3 flex-wrap text-sm">
|
||
<ArrowClockwise size={18} weight="bold" className="shrink-0" aria-hidden />
|
||
<span className="flex-1 min-w-0">
|
||
{t('updateBanner.message', { version: versionLabel })}
|
||
</span>
|
||
<div className="flex items-center gap-2 ml-auto">
|
||
<Button
|
||
size="sm"
|
||
variant="secondary"
|
||
onClick={() => window.open(DOCS_URL, '_blank', 'noopener,noreferrer')}
|
||
>
|
||
<BookOpen size={16} weight="regular" className="mr-1" aria-hidden />
|
||
{t('updateBanner.docs')}
|
||
</Button>
|
||
<Button
|
||
size="sm"
|
||
variant="primary"
|
||
onClick={() => window.location.reload()}
|
||
>
|
||
{t('updateBanner.reload')}
|
||
</Button>
|
||
<button
|
||
type="button"
|
||
aria-label={t('updateBanner.dismiss') as string}
|
||
onClick={() => setDismissed(true)}
|
||
className="ml-1 p-1 rounded hover:bg-white/10"
|
||
>
|
||
<X size={16} weight="bold" aria-hidden />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|