241 lines
11 KiB
JavaScript
241 lines
11 KiB
JavaScript
import React from 'react'
|
|
|
|
export const HOMEPAGE_ANNOUNCEMENT_STORAGE_KEY = 'skinbase:hidden_homepage_announcements'
|
|
|
|
const PRESETS = {
|
|
nova_aurora: {
|
|
shell: 'border-cyan-300/15 bg-[radial-gradient(circle_at_top_left,rgba(34,211,238,0.26),transparent_30%),radial-gradient(circle_at_80%_20%,rgba(168,85,247,0.22),transparent_28%),linear-gradient(135deg,rgba(6,12,24,0.96),rgba(10,17,34,0.9))] text-white',
|
|
glow: 'from-cyan-400/25 via-fuchsia-400/10 to-transparent',
|
|
badge: 'border-cyan-200/20 bg-cyan-300/10 text-cyan-100',
|
|
primary: 'border-cyan-300/30 bg-cyan-300/15 text-cyan-50 hover:bg-cyan-300/22',
|
|
secondary: 'border-white/12 bg-white/[0.05] text-white/85 hover:bg-white/[0.1]',
|
|
prose: 'prose-invert prose-a:text-cyan-200 prose-strong:text-white',
|
|
},
|
|
deep_space: {
|
|
shell: 'border-indigo-300/15 bg-[radial-gradient(circle_at_top,rgba(59,130,246,0.18),transparent_34%),linear-gradient(145deg,rgba(5,10,20,0.98),rgba(11,18,36,0.94))] text-white',
|
|
glow: 'from-indigo-400/20 via-sky-400/12 to-transparent',
|
|
badge: 'border-indigo-200/20 bg-indigo-300/10 text-indigo-100',
|
|
primary: 'border-indigo-300/30 bg-indigo-300/15 text-indigo-50 hover:bg-indigo-300/22',
|
|
secondary: 'border-white/12 bg-white/[0.05] text-white/85 hover:bg-white/[0.1]',
|
|
prose: 'prose-invert prose-a:text-indigo-200 prose-strong:text-white',
|
|
},
|
|
sunrise: {
|
|
shell: 'border-amber-300/20 bg-[radial-gradient(circle_at_top_left,rgba(251,191,36,0.28),transparent_28%),linear-gradient(145deg,rgba(44,15,18,0.95),rgba(17,9,24,0.96))] text-white',
|
|
glow: 'from-amber-300/25 via-rose-300/10 to-transparent',
|
|
badge: 'border-amber-100/20 bg-amber-300/15 text-amber-50',
|
|
primary: 'border-amber-300/35 bg-amber-300/18 text-amber-50 hover:bg-amber-300/24',
|
|
secondary: 'border-white/12 bg-white/[0.05] text-white/85 hover:bg-white/[0.1]',
|
|
prose: 'prose-invert prose-a:text-amber-100 prose-strong:text-white',
|
|
},
|
|
ocean_glow: {
|
|
shell: 'border-sky-300/15 bg-[radial-gradient(circle_at_top_right,rgba(56,189,248,0.24),transparent_28%),linear-gradient(135deg,rgba(3,20,38,0.98),rgba(8,27,45,0.92))] text-white',
|
|
glow: 'from-sky-400/24 via-emerald-300/10 to-transparent',
|
|
badge: 'border-sky-200/20 bg-sky-300/10 text-sky-100',
|
|
primary: 'border-sky-300/30 bg-sky-300/15 text-sky-50 hover:bg-sky-300/22',
|
|
secondary: 'border-white/12 bg-white/[0.05] text-white/85 hover:bg-white/[0.1]',
|
|
prose: 'prose-invert prose-a:text-sky-200 prose-strong:text-white',
|
|
},
|
|
spring_vibes: {
|
|
shell: 'border-emerald-300/18 bg-[radial-gradient(circle_at_top_left,rgba(74,222,128,0.2),transparent_28%),linear-gradient(145deg,rgba(8,24,22,0.98),rgba(14,28,38,0.92))] text-white',
|
|
glow: 'from-emerald-300/24 via-lime-200/8 to-transparent',
|
|
badge: 'border-emerald-200/20 bg-emerald-300/10 text-emerald-100',
|
|
primary: 'border-emerald-300/30 bg-emerald-300/15 text-emerald-50 hover:bg-emerald-300/22',
|
|
secondary: 'border-white/12 bg-white/[0.05] text-white/85 hover:bg-white/[0.1]',
|
|
prose: 'prose-invert prose-a:text-emerald-200 prose-strong:text-white',
|
|
},
|
|
fantasy_realms: {
|
|
shell: 'border-fuchsia-300/18 bg-[radial-gradient(circle_at_15%_10%,rgba(232,121,249,0.2),transparent_26%),linear-gradient(145deg,rgba(23,8,35,0.97),rgba(12,16,36,0.93))] text-white',
|
|
glow: 'from-fuchsia-300/24 via-violet-300/10 to-transparent',
|
|
badge: 'border-fuchsia-200/20 bg-fuchsia-300/10 text-fuchsia-100',
|
|
primary: 'border-fuchsia-300/30 bg-fuchsia-300/15 text-fuchsia-50 hover:bg-fuchsia-300/22',
|
|
secondary: 'border-white/12 bg-white/[0.05] text-white/85 hover:bg-white/[0.1]',
|
|
prose: 'prose-invert prose-a:text-fuchsia-200 prose-strong:text-white',
|
|
},
|
|
minimal_light: {
|
|
shell: 'border-slate-300/35 bg-[linear-gradient(140deg,rgba(255,255,255,0.96),rgba(240,247,255,0.92))] text-slate-900',
|
|
glow: 'from-sky-200/50 via-transparent to-transparent',
|
|
badge: 'border-slate-300/60 bg-white/70 text-slate-700',
|
|
primary: 'border-slate-900/10 bg-slate-900 text-white hover:bg-slate-800',
|
|
secondary: 'border-slate-300/60 bg-white text-slate-700 hover:bg-slate-50',
|
|
prose: 'prose prose-slate prose-a:text-sky-700 prose-strong:text-slate-900',
|
|
},
|
|
dark_glass: {
|
|
shell: 'border-white/12 bg-[linear-gradient(145deg,rgba(12,16,24,0.82),rgba(6,10,18,0.76))] text-white backdrop-blur-xl',
|
|
glow: 'from-white/10 via-white/0 to-transparent',
|
|
badge: 'border-white/12 bg-white/[0.06] text-white/90',
|
|
primary: 'border-white/18 bg-white/[0.09] text-white hover:bg-white/[0.14]',
|
|
secondary: 'border-white/12 bg-black/20 text-white/80 hover:bg-white/[0.08]',
|
|
prose: 'prose-invert prose-a:text-slate-200 prose-strong:text-white',
|
|
},
|
|
}
|
|
|
|
function cx(...parts) {
|
|
return parts.filter(Boolean).join(' ')
|
|
}
|
|
|
|
function readHiddenAnnouncements() {
|
|
if (typeof window === 'undefined') return {}
|
|
|
|
try {
|
|
const raw = window.localStorage.getItem(HOMEPAGE_ANNOUNCEMENT_STORAGE_KEY)
|
|
const parsed = raw ? JSON.parse(raw) : {}
|
|
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
} catch {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
function writeHiddenAnnouncements(payload) {
|
|
if (typeof window === 'undefined') return
|
|
window.localStorage.setItem(HOMEPAGE_ANNOUNCEMENT_STORAGE_KEY, JSON.stringify(payload))
|
|
}
|
|
|
|
export default function HomepageAnnouncement({ announcement, mode = 'live' }) {
|
|
const [hidden, setHidden] = React.useState(false)
|
|
const isLiveMode = mode === 'live'
|
|
const isPreviewMode = mode === 'preview'
|
|
|
|
React.useEffect(() => {
|
|
if (!isLiveMode || !announcement?.id) {
|
|
setHidden(false)
|
|
return
|
|
}
|
|
|
|
const hiddenAnnouncements = readHiddenAnnouncements()
|
|
setHidden(Number(hiddenAnnouncements[String(announcement.id)] || 0) === Number(announcement.dismiss_version || 1))
|
|
}, [announcement, isLiveMode])
|
|
|
|
if (!announcement) {
|
|
return null
|
|
}
|
|
|
|
const preset = PRESETS[announcement.gradient_preset] || PRESETS.nova_aurora
|
|
const overlayOpacity = Math.max(0, Math.min(100, Number(announcement.overlay_opacity ?? 55)))
|
|
|
|
const dismiss = () => {
|
|
if (!isLiveMode || !announcement?.id) return
|
|
|
|
const next = {
|
|
...readHiddenAnnouncements(),
|
|
[String(announcement.id)]: Number(announcement.dismiss_version || 1),
|
|
}
|
|
|
|
writeHiddenAnnouncements(next)
|
|
setHidden(true)
|
|
}
|
|
|
|
const restore = () => {
|
|
if (!isLiveMode || !announcement?.id) return
|
|
|
|
const next = readHiddenAnnouncements()
|
|
delete next[String(announcement.id)]
|
|
writeHiddenAnnouncements(next)
|
|
setHidden(false)
|
|
}
|
|
|
|
if (hidden && isLiveMode) {
|
|
return (
|
|
<section className="px-4 pt-8 sm:px-6 lg:px-8">
|
|
<div className="mx-auto max-w-7xl">
|
|
<button
|
|
type="button"
|
|
onClick={restore}
|
|
className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.06] px-4 py-2 text-sm font-semibold text-white/90 transition hover:border-white/20 hover:bg-white/[0.1]"
|
|
>
|
|
<span aria-hidden="true">✨</span>
|
|
<span>Show Skinbase announcement</span>
|
|
</button>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<section className="px-4 pt-8 sm:px-6 lg:px-8">
|
|
<div className="mx-auto max-w-7xl">
|
|
<div className={cx('relative overflow-hidden rounded-[2rem] border shadow-[0_28px_90px_rgba(0,0,0,0.35)]', preset.shell)}>
|
|
{announcement.background_image_url ? (
|
|
<div className="absolute inset-0">
|
|
<img src={announcement.background_image_url} alt="" className="h-full w-full object-cover" />
|
|
<div className="absolute inset-0 bg-slate-950" style={{ opacity: overlayOpacity / 100 }} />
|
|
</div>
|
|
) : null}
|
|
|
|
<div className={cx('pointer-events-none absolute inset-0 bg-gradient-to-br', preset.glow)} />
|
|
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(180deg,rgba(255,255,255,0.06),transparent_22%,rgba(2,6,23,0.15)_100%)]" />
|
|
|
|
{announcement.is_dismissible && isLiveMode ? (
|
|
<button
|
|
type="button"
|
|
onClick={dismiss}
|
|
className="absolute right-5 top-5 z-10 inline-flex items-center gap-2 rounded-full border border-white/10 bg-black/30 px-3 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-white/70 transition hover:border-white/20 hover:bg-black/45 hover:text-white sm:right-6 sm:top-6 lg:right-8 lg:top-8"
|
|
aria-label="Dismiss homepage announcement"
|
|
>
|
|
<svg aria-hidden="true" viewBox="0 0 16 16" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
<path d="M3.5 3.5 12.5 12.5" />
|
|
<path d="M12.5 3.5 3.5 12.5" />
|
|
</svg>
|
|
Dismiss
|
|
</button>
|
|
) : null}
|
|
|
|
<div className={cx(
|
|
'relative px-6 py-7 sm:px-8 lg:px-10 lg:py-10',
|
|
isPreviewMode
|
|
? 'flex min-h-[42rem] flex-col gap-8'
|
|
: 'grid gap-8 lg:grid-cols-[minmax(0,1.25fr)_auto] lg:items-end'
|
|
)}>
|
|
<div className={cx(isPreviewMode ? 'w-full flex-1' : 'max-w-3xl')}>
|
|
{announcement.badge_text ? (
|
|
<div className={cx('inline-flex rounded-full border px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em]', preset.badge)}>
|
|
{announcement.badge_text}
|
|
</div>
|
|
) : null}
|
|
|
|
<h2 className="mt-4 text-3xl font-semibold tracking-[-0.05em] sm:text-4xl lg:text-[3.15rem]">
|
|
{announcement.title}
|
|
</h2>
|
|
|
|
{announcement.subtitle ? (
|
|
<p className={cx('mt-3 text-base leading-7 text-current/80 sm:text-lg', isPreviewMode ? 'w-full max-w-none' : 'max-w-2xl')}>
|
|
{announcement.subtitle}
|
|
</p>
|
|
) : null}
|
|
|
|
{announcement.content_html ? (
|
|
<div
|
|
className={cx(
|
|
'mt-5 text-sm leading-7 sm:text-base',
|
|
'[&_p]:my-0 [&_p+p]:mt-6 [&_ul]:my-5 [&_ol]:my-5 [&_li+li]:mt-1.5 [&_blockquote]:my-5 [&_h2]:mb-3 [&_h2]:mt-7 [&_h3]:mb-2 [&_h3]:mt-6',
|
|
preset.prose,
|
|
isPreviewMode ? 'w-full max-w-none' : 'max-w-2xl'
|
|
)}
|
|
dangerouslySetInnerHTML={{ __html: announcement.content_html }}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className={cx(
|
|
isPreviewMode
|
|
? 'mt-auto flex w-full flex-col gap-4 border-t border-white/10 pt-5'
|
|
: 'flex flex-col items-start gap-3 lg:items-end'
|
|
)}>
|
|
<div className={cx('flex flex-wrap gap-3', isPreviewMode ? 'w-full' : 'lg:justify-end')}>
|
|
{announcement.primary_link ? (
|
|
<a href={announcement.primary_link.url} className={cx('inline-flex items-center justify-center rounded-full border px-5 py-3 text-sm font-semibold transition', isPreviewMode ? 'min-w-[11rem]' : '', preset.primary)}>
|
|
{announcement.primary_link.label}
|
|
</a>
|
|
) : null}
|
|
{announcement.secondary_link ? (
|
|
<a href={announcement.secondary_link.url} className={cx('inline-flex items-center justify-center rounded-full border px-5 py-3 text-sm font-semibold transition', isPreviewMode ? 'min-w-[11rem]' : '', preset.secondary)}>
|
|
{announcement.secondary_link.label}
|
|
</a>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
} |