Files
SkinbaseNova/resources/js/components/homepage/HomepageAnnouncement.jsx

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 Nova 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>
)
}