diff --git a/client/src/components/FourZero/NoFearBeacon.tsx b/client/src/components/FourZero/NoFearBeacon.tsx index d34b609d8..3e07342a0 100644 --- a/client/src/components/FourZero/NoFearBeacon.tsx +++ b/client/src/components/FourZero/NoFearBeacon.tsx @@ -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() { {copy.beaconSub} + {/* 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. */} + + {/* White retirement badge: viewable until Aug 23. */} + {chrome.until} {open && ( diff --git a/client/src/components/FourZero/fourzero.css b/client/src/components/FourZero/fourzero.css index 100f300de..cd150ebc4 100644 --- a/client/src/components/FourZero/fourzero.css +++ b/client/src/components/FourZero/fourzero.css @@ -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; } diff --git a/client/src/components/FourZero/noFearLines.ts b/client/src/components/FourZero/noFearLines.ts index 61dbaf0cd..6092e21c4 100644 --- a/client/src/components/FourZero/noFearLines.ts +++ b/client/src/components/FourZero/noFearLines.ts @@ -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 = { + 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 +}