feat(dashboard): retire the 4.0.0 moment after Aug 23 — viewable-until badge + a permanent dismiss X

This commit is contained in:
Maurice
2026-07-24 11:40:05 +02:00
committed by Maurice
parent 2cec06161d
commit 51f8afdb60
3 changed files with 125 additions and 8 deletions
@@ -1,13 +1,20 @@
import { lazy, Suspense, useEffect, useRef, useState } from 'react'
import { Play } from 'lucide-react'
import { Play, X } from 'lucide-react'
import { useTranslation } from '../../i18n'
import { noFearCopy } from './noFearLines'
import { noFearChrome, noFearCopy } from './noFearLines'
import './fourzero.css'
// TREK 4.0.0 release moment — remove together with the FourZero folder.
const NoFearShow = lazy(() => import('./NoFearShow'))
// The moment retires itself. Last day it shows: 2026-08-23 (local midnight after
// it ends the run). After that the beacon renders nothing even if the folder is
// still in the build — so self-hosters who don't update past this version still
// see it disappear on time. The end date holds regardless of updates.
const VISIBLE_UNTIL = new Date('2026-08-24T00:00:00').getTime()
const DISMISS_KEY = 'trek.fourzero.dismissed'
// Abstract night-world for the teaser: light points loosely shaped like the
// inhabited continents (relative coords), between which golden arcs travel.
const TEASER_POINTS: [number, number][] = [
@@ -123,7 +130,20 @@ function TeaserCanvas() {
export default function NoFearBeacon() {
const { language } = useTranslation()
const copy = noFearCopy(language)
const chrome = noFearChrome(language)
const [open, setOpen] = useState(false)
const [confirming, setConfirming] = useState(false)
const [dismissed, setDismissed] = useState(() => {
try { return localStorage.getItem(DISMISS_KEY) === '1' } catch { return false }
})
// Past the end date, or dismissed for good — nothing renders.
if (dismissed || Date.now() >= VISIBLE_UNTIL) return null
const dismiss = () => {
try { localStorage.setItem(DISMISS_KEY, '1') } catch { /* private mode — hide for this session at least */ }
setDismissed(true)
}
return (
<>
@@ -135,6 +155,21 @@ export default function NoFearBeacon() {
<span className="fz-beacon-sub" style={{ color: 'rgba(245, 240, 225, 0.75)' }}>{copy.beaconSub}</span>
<span className="fz-beacon-playbtn" aria-hidden><Play size={16} fill="currentColor" /></span>
</button>
{/* Top-right X: two-step so a stray click can't retire the moment for good.
Dismissing is permanent (localStorage) — only an update brings it back. */}
<button
type="button"
className={`fz-beacon-dismiss ${confirming ? 'fz-beacon-dismiss-confirm' : ''}`}
aria-label={chrome.dismiss}
title={chrome.dismiss}
onClick={() => (confirming ? dismiss() : setConfirming(true))}
onMouseLeave={() => setConfirming(false)}
onBlur={() => setConfirming(false)}
>
{confirming ? chrome.confirm : <X size={15} />}
</button>
{/* White retirement badge: viewable until Aug 23. */}
<span className="fz-beacon-until">{chrome.until}</span>
</div>
{open && (
<Suspense fallback={null}>
+46 -6
View File
@@ -396,19 +396,59 @@
.fz-beacon-dismiss {
position: absolute;
top: 8px;
inset-inline-start: 8px;
z-index: 2;
top: 10px;
inset-inline-end: 10px;
z-index: 3;
display: inline-flex;
align-items: center;
padding: 5px;
border: none;
border-radius: 999px;
background: transparent;
color: rgba(245, 240, 225, 0.4);
background: rgba(8, 10, 16, 0.4);
color: rgba(245, 240, 225, 0.55);
font-family: inherit;
font-size: 11px;
font-weight: 600;
cursor: pointer;
opacity: 0;
backdrop-filter: blur(4px);
transition: opacity 0.3s ease, color 0.2s, background 0.2s;
}
.fz-beacon-dismiss:hover { color: rgba(245, 240, 225, 0.85); }
/* Stays out of the way until the card is hovered or the button is focused. */
.fz-beacon:hover .fz-beacon-dismiss,
.fz-beacon-dismiss:focus-visible { opacity: 1; }
.fz-beacon-dismiss:hover { color: #fff; background: rgba(8, 10, 16, 0.65); }
/* Armed confirm: a clear, deliberate red pill — this click is permanent. */
.fz-beacon-dismiss-confirm {
opacity: 1;
padding: 5px 11px;
background: rgba(200, 60, 45, 0.9);
color: #fff;
}
.fz-beacon-dismiss-confirm:hover { background: rgba(214, 68, 52, 0.95); }
/* White retirement badge at the foot of the card: viewable until Aug 23. */
.fz-beacon-until {
position: absolute;
left: 50%;
bottom: 14px;
transform: translateX(-50%);
z-index: 3;
pointer-events: none;
padding: 4px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.92);
color: #1a1712;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.01em;
white-space: nowrap;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
}
/* While the show runs: no floating dashboard chrome above it, no page scroll. */
body.fz-show-open { overflow: hidden; }
@@ -789,3 +789,45 @@ export const ANTHEM_CASCADE: { lang: string; text: string }[] = [
]
export const INSPIRED_URL = 'https://www.youtube.com/watch?v=Gg-SCpXba64'
// The beacon retires itself after the release window. The white "viewable until"
// badge and the dismiss copy live in one compact table so the 23 language blocks
// above stay untouched — and it all still leaves with the folder.
interface NoFearChrome {
/** The white badge: this moment is viewable until Aug 23. */
until: string
/** aria-label / title of the top-right X. */
dismiss: string
/** Armed confirm label — a stray click must not retire it for good. */
confirm: string
}
const CHROME: Record<string, NoFearChrome> = {
de: { until: 'Noch bis 23.08. zu sehen', dismiss: 'Ausblenden', confirm: 'Dauerhaft ausblenden?' },
en: { until: 'Shown until Aug 23', dismiss: 'Dismiss', confirm: 'Hide for good?' },
es: { until: 'Visible hasta el 23 ago', dismiss: 'Ocultar', confirm: '¿Ocultar para siempre?' },
fr: { until: 'Visible jusqu’au 23 août', dismiss: 'Masquer', confirm: 'Masquer définitivement ?' },
it: { until: 'Visibile fino al 23 ago', dismiss: 'Nascondi', confirm: 'Nascondere per sempre?' },
nl: { until: 'Te zien tot 23 aug', dismiss: 'Verbergen', confirm: 'Voorgoed verbergen?' },
pl: { until: 'Widoczne do 23 sierpnia', dismiss: 'Ukryj', confirm: 'Ukryć na zawsze?' },
ru: { until: 'Показывается до 23 августа', dismiss: 'Скрыть', confirm: 'Скрыть навсегда?' },
uk: { until: 'Доступно до 23 серпня', dismiss: 'Сховати', confirm: 'Сховати назавжди?' },
cs: { until: 'K vidění do 23. srpna', dismiss: 'Skrýt', confirm: 'Skrýt natrvalo?' },
sv: { until: 'Visas till 23 aug', dismiss: 'Dölj', confirm: 'Dölj för gott?' },
tr: { until: '23 Ağustos’a kadar görünür', dismiss: 'Gizle', confirm: 'Kalıcı olarak gizlensin mi?' },
gr: { until: 'Έως 23 Αυγούστου', dismiss: 'Απόκρυψη', confirm: 'Μόνιμη απόκρυψη;' },
hu: { until: 'Augusztus 23-ig látható', dismiss: 'Elrejtés', confirm: 'Végleg elrejti?' },
br: { until: 'Visível até 23 de ago', dismiss: 'Ocultar', confirm: 'Ocultar para sempre?' },
ca: { until: 'Visible fins al 23 d’ag.', dismiss: 'Amaga', confirm: 'Amagar per sempre?' },
id: { until: 'Tampil hingga 23 Agu', dismiss: 'Sembunyikan', confirm: 'Sembunyikan selamanya?' },
vi: { until: 'Hiển thị đến 23 tháng 8', dismiss: 'Ẩn', confirm: 'Ẩn vĩnh viễn?' },
ja: { until: '8月23日まで公開', dismiss: '非表示', confirm: '完全に非表示にしますか?' },
ko: { until: '8월 23일까지 표시', dismiss: '숨기기', confirm: '영구히 숨기시겠어요?' },
zh: { until: '展示至 8月23日', dismiss: '隐藏', confirm: '永久隐藏?' },
ar: { until: 'يظهر حتى 23 أغسطس', dismiss: 'إخفاء', confirm: 'إخفاء نهائيًا؟' },
'zh-TW': { until: '展示至 8月23日', dismiss: '隱藏', confirm: '永久隱藏?' },
}
export function noFearChrome(language: string): NoFearChrome {
return CHROME[language] ?? CHROME.en
}