Security improvements, title fixes

This commit is contained in:
Pavlenex 2026-03-31 15:15:23 +02:00
parent 40d3e6bb1e
commit b076856110
10 changed files with 150 additions and 80 deletions

View File

@ -6,7 +6,7 @@
<title>BTCPay Day Prague 2026</title>
<meta name="description" content="Join BTCPay Server contributors and community in Prague on June 14, 2026. Keynotes, panels, and surprise guests." />
<!-- Open Graph -->
<!-- Open Graph — replace /og-image.png with a real 1200×630 image before launch -->
<meta property="og:title" content="BTCPay Day Prague 2026" />
<meta property="og:description" content="Once a year, BTCPay Server contributors and community gather to celebrate achievements, network, and discuss the project's future." />
<meta property="og:type" content="website" />
@ -19,7 +19,12 @@
<meta name="twitter:description" content="Join us in Prague on June 14, 2026 for BTCPay Day — keynotes, panels, and surprise guests." />
<meta name="twitter:image" content="/og-image.png" />
<!-- CSP -->
<!--
CSP note: meta CSP cannot enforce frame-ancestors (clickjacking protection).
Add the following response header at the edge/CDN for full protection:
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENY
-->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https://img.youtube.com https://pbs.twimg.com; frame-src https://www.youtube.com https://www.openstreetmap.org; connect-src 'self'" />
<script src="/theme-init.js"></script>
@ -29,7 +34,8 @@
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<!-- apple-touch-icon: replace with a 180×180 PNG before launch -->
<link rel="apple-touch-icon" sizes="180x180" href="/favicon-32x32.png" />
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />

View File

@ -1,7 +1,6 @@
import { useScrollReveal } from '@/hooks/useScrollReveal'
import { cn } from '@/lib/utils'
const RIGA_VIDEO_ID = 'JWZPN-SAO3U'
import { EVENT } from '@/data/event'
export default function About() {
const { ref, isVisible } = useScrollReveal()
@ -39,10 +38,11 @@ export default function About() {
<div className="glass rounded-2xl overflow-hidden">
<div className="aspect-video w-full">
<iframe
src={`https://www.youtube.com/embed/${RIGA_VIDEO_ID}?start=6423`}
src={`https://www.youtube.com/embed/${EVENT.youtubeRigaId}?start=${EVENT.rigaTimestamp}`}
title="BTCPay Day Riga 2024"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
loading="lazy"
className="w-full h-full"
/>
</div>

View File

@ -1,30 +1,45 @@
const TICKET_URL = '#register'
const PRAGUE_VIDEO_ID = 'rIjNPuYxVMo'
import { EVENT } from '@/data/event'
// Check once at module load — user must reload to pick up system-level changes,
// which is acceptable. Avoids a hook + subscriber just for a background video.
const prefersReducedMotion =
typeof window !== 'undefined' &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches
export default function Hero() {
return (
<section id="home" className="relative overflow-hidden pt-16 min-h-screen flex flex-col justify-center">
{/* Video background */}
<div className="absolute inset-0 z-0 pointer-events-none">
<div style={{
position: 'absolute',
width: 'max(100%, 177.78vh)',
height: 'max(100%, 56.25vw)',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}>
<iframe
src={`https://www.youtube.com/embed/${PRAGUE_VIDEO_ID}?autoplay=1&mute=1&controls=0&loop=1&playlist=${PRAGUE_VIDEO_ID}&modestbranding=1&playsinline=1`}
allow="autoplay; encrypted-media"
className="w-full h-full"
style={{ pointerEvents: 'none' }}
/>
{/* Video background — skipped entirely when OS reduces motion */}
{!prefersReducedMotion && (
<div className="absolute inset-0 z-0 pointer-events-none">
<div style={{
position: 'absolute',
width: 'max(100%, 177.78vh)',
height: 'max(100%, 56.25vw)',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}>
<iframe
src={`https://www.youtube.com/embed/${EVENT.youtubeHeroId}?autoplay=1&mute=1&controls=0&loop=1&playlist=${EVENT.youtubeHeroId}&modestbranding=1&playsinline=1`}
title="BTCPay Day Prague background"
allow="autoplay; encrypted-media"
className="w-full h-full"
style={{ pointerEvents: 'none' }}
/>
</div>
{/* Dark overlay — heavy enough for text legibility at all video frames */}
<div className="absolute inset-0" style={{ background: 'linear-gradient(to bottom, rgba(0,15,6,0.68), rgba(0,10,4,0.78))' }} />
</div>
{/* Dark overlay — heavy enough for text legibility at all video frames */}
<div className="absolute inset-0" style={{ background: 'linear-gradient(to bottom, rgba(0,15,6,0.68), rgba(0,10,4,0.78))' }} />
</div>
)}
{/* Static gradient fallback shown when motion is reduced */}
{prefersReducedMotion && (
<div className="absolute inset-0 z-0" style={{
background: 'radial-gradient(ellipse 80% 60% at 50% 30%, hsl(110 48% 14% / 0.7) 0%, hsl(216 28% 7%) 70%)',
}} />
)}
{/* Content */}
<div className="relative z-10 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 w-full">
@ -32,7 +47,7 @@ export default function Hero() {
<div className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full border border-white/50 bg-black/40 backdrop-blur-md mb-8">
<span className="text-sm font-bold tracking-wide text-white">
June 14, 2026 · 12:00 PM · Hotel Duo · Prague
{EVENT.date} · {EVENT.time} · {EVENT.venue} · Prague
</span>
</div>
@ -47,7 +62,7 @@ export default function Hero() {
<div className="flex items-center gap-4">
<a
href={TICKET_URL}
href={EVENT.ticketUrl}
className="inline-flex items-center justify-center px-8 py-3 rounded-xl text-sm font-semibold bg-primary text-primary-foreground hover:opacity-90 active:scale-[0.98] transition-all duration-150"
>
Register

View File

@ -1,15 +1,7 @@
import { MapPin, Clock, Calendar, ExternalLink } from 'lucide-react'
import { useScrollReveal } from '@/hooks/useScrollReveal'
import { cn } from '@/lib/utils'
const VENUE = {
name: 'Hotel Duo',
address: 'Teplická 492, 190 00 Praha 9',
city: 'Prague, Czech Republic',
date: 'June 14, 2026',
time: '12:00 PM',
mapsUrl: 'https://maps.google.com/?q=Hotel+Duo+Praha+Teplická+492',
}
import { EVENT } from '@/data/event'
// OpenStreetMap embed for Hotel Duo Praha (approx. coords: 50.1039, 14.4697)
const OSM_EMBED = 'https://www.openstreetmap.org/export/embed.html?bbox=14.449%2C50.094%2C14.490%2C50.114&layer=mapnik&marker=50.1039%2C14.4697'
@ -32,9 +24,9 @@ export default function Location() {
{/* Venue details card */}
<div className="glass rounded-2xl p-8 flex flex-col gap-6">
<div>
<h3 className="font-display font-bold text-2xl text-foreground">{VENUE.name}</h3>
<p className="text-muted-foreground mt-1">{VENUE.address}</p>
<p className="text-muted-foreground text-sm">{VENUE.city}</p>
<h3 className="font-display font-bold text-2xl text-foreground">{EVENT.venue}</h3>
<p className="text-muted-foreground mt-1">{EVENT.address}</p>
<p className="text-muted-foreground text-sm">{EVENT.city}</p>
</div>
<div className="flex flex-col gap-3">
@ -42,24 +34,24 @@ export default function Location() {
<span className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10">
<Calendar size={14} className="text-primary" />
</span>
{VENUE.date}
{EVENT.date}
</div>
<div className="flex items-center gap-3 text-sm text-foreground">
<span className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10">
<Clock size={14} className="text-primary" />
</span>
Doors open at {VENUE.time}
Doors open at {EVENT.time}
</div>
<div className="flex items-center gap-3 text-sm text-foreground">
<span className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10">
<MapPin size={14} className="text-primary" />
</span>
{VENUE.city}
{EVENT.city}
</div>
</div>
<a
href={VENUE.mapsUrl}
href={EVENT.mapsUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 self-start px-5 py-2.5 rounded-xl border border-border text-sm font-medium text-foreground hover:bg-muted transition-colors duration-150"

View File

@ -1,22 +1,41 @@
import { useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { Menu, X, Send } from 'lucide-react'
import ThemeToggle from '@/components/ThemeToggle'
import BTCPayMark from '@/components/BTCPayMark'
// TODO: Replace with your real ticket registration URL
const TICKET_URL = '#register'
const TELEGRAM_URL = 'https://t.me/+h9RyKmiXBdhhM2I0'
import { EVENT } from '@/data/event'
const NAV_LINKS = [
{ label: 'Speakers', href: '#speakers' },
{ label: 'Agenda', href: '#agenda' },
{ label: 'Location', href: '#location' },
{ label: 'Speakers', href: '#speakers' },
{ label: 'Agenda', href: '#agenda' },
{ label: 'Location', href: '#location' },
{ label: 'Supporters', href: '#supporters' },
]
export default function Navbar() {
const [mobileOpen, setMobileOpen] = useState(false)
const firstLinkRef = useRef<HTMLAnchorElement>(null)
// Scroll lock
useEffect(() => {
document.body.style.overflow = mobileOpen ? 'hidden' : ''
return () => { document.body.style.overflow = '' }
}, [mobileOpen])
// Escape key closes menu
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape' && mobileOpen) setMobileOpen(false)
}
document.addEventListener('keydown', onKey)
return () => document.removeEventListener('keydown', onKey)
}, [mobileOpen])
// Move focus into overlay when it opens
useEffect(() => {
if (mobileOpen) firstLinkRef.current?.focus()
}, [mobileOpen])
const close = () => setMobileOpen(false)
return (
<>
@ -30,7 +49,7 @@ export default function Navbar() {
</span>
</a>
<nav className="hidden md:flex items-center gap-1">
<nav className="hidden md:flex items-center gap-1" aria-label="Primary navigation">
{NAV_LINKS.map((link) => (
<a
key={link.href}
@ -44,7 +63,7 @@ export default function Navbar() {
<div className="flex items-center gap-1.5 ml-auto">
<a
href={TELEGRAM_URL}
href={EVENT.telegramUrl}
target="_blank"
rel="noopener noreferrer"
aria-label="Join BTCPay Day on Telegram"
@ -54,7 +73,7 @@ export default function Navbar() {
</a>
<ThemeToggle />
<a
href={TICKET_URL}
href={EVENT.ticketUrl}
className="hidden sm:flex items-center gap-1.5 px-4 py-1.5 rounded-lg text-sm font-semibold bg-primary text-primary-foreground hover:opacity-90 transition-opacity duration-150 ml-1"
>
Register Now
@ -63,6 +82,8 @@ export default function Navbar() {
type="button"
className="flex md:hidden items-center justify-center w-8 h-8 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-all duration-150 ml-1"
aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
aria-expanded={mobileOpen}
aria-controls="mobile-menu"
onClick={() => setMobileOpen((v) => !v)}
>
{mobileOpen ? <X size={18} /> : <Menu size={18} />}
@ -71,14 +92,22 @@ export default function Navbar() {
</div>
</header>
{/* Mobile menu — implemented as a dialog for correct modal semantics */}
{mobileOpen && (
<div className="fixed inset-0 z-40 bg-background/95 backdrop-blur-2xl flex flex-col pt-20 px-8 pb-10 md:hidden">
<nav className="flex flex-col gap-2 flex-1">
{NAV_LINKS.map((link) => (
<div
id="mobile-menu"
role="dialog"
aria-modal="true"
aria-label="Navigation menu"
className="fixed inset-0 z-40 bg-background/95 backdrop-blur-2xl flex flex-col pt-20 px-8 pb-10 md:hidden"
>
<nav className="flex flex-col gap-2 flex-1" aria-label="Mobile navigation">
{NAV_LINKS.map((link, i) => (
<a
key={link.href}
ref={i === 0 ? firstLinkRef : undefined}
href={link.href}
onClick={() => setMobileOpen(false)}
onClick={close}
className="py-4 text-3xl font-display font-bold text-foreground/80 hover:text-foreground border-b border-border/40 transition-colors duration-150"
>
{link.label}
@ -86,14 +115,14 @@ export default function Navbar() {
))}
</nav>
<a
href={TICKET_URL}
onClick={() => setMobileOpen(false)}
href={EVENT.ticketUrl}
onClick={close}
className="mt-8 flex items-center justify-center py-4 rounded-2xl text-lg font-semibold bg-primary text-primary-foreground hover:opacity-90 transition-opacity"
>
Register Now
</a>
<a
href={TELEGRAM_URL}
href={EVENT.telegramUrl}
target="_blank"
rel="noopener noreferrer"
className="mt-4 flex items-center justify-center gap-2 py-3 rounded-2xl text-sm text-muted-foreground border border-border/60 hover:bg-muted transition-colors"

View File

@ -1,7 +1,5 @@
import { useEffect, useState } from 'react'
// TODO: Replace with your real ticket registration URL
const TICKET_URL = '#register'
import { EVENT } from '@/data/event'
export default function StickyBar() {
const [visible, setVisible] = useState(false)
@ -26,11 +24,11 @@ export default function StickyBar() {
<div className="max-w-7xl mx-auto px-4 sm:px-6 h-14 flex items-center justify-between gap-4">
<span className="text-sm font-medium text-foreground hidden sm:block">
BTCPay Day ·{' '}
<span className="text-muted-foreground">Prague · June 14, 2026 · 12:00 PM</span>
<span className="text-muted-foreground">Prague · {EVENT.date} · {EVENT.time}</span>
</span>
<span className="text-sm font-medium text-foreground sm:hidden">BTCPay Day Prague 2026</span>
<a
href={TICKET_URL}
href={EVENT.ticketUrl}
className="shrink-0 flex items-center justify-center px-5 py-1.5 rounded-lg text-sm font-semibold bg-primary text-primary-foreground hover:opacity-90 transition-opacity duration-150"
>
Register Now

13
src/data/event.ts Normal file
View File

@ -0,0 +1,13 @@
export const EVENT = {
date: 'June 14, 2026',
time: '12:00 PM',
venue: 'Hotel Duo',
address: 'Teplická 492, 190 00 Praha 9',
city: 'Prague, Czech Republic',
mapsUrl: 'https://maps.google.com/?q=Hotel+Duo+Praha+Teplická+492',
ticketUrl: '#register',
telegramUrl: 'https://t.me/+h9RyKmiXBdhhM2I0',
youtubeHeroId: 'rIjNPuYxVMo',
youtubeRigaId: 'JWZPN-SAO3U',
rigaTimestamp: 6423,
} as const

View File

@ -7,14 +7,14 @@ export interface Speaker {
}
export const speakers: Speaker[] = [
{ name: 'DSTRUKT', title: 'Designer & Developer', photo: '/images/speakers/dstrukt__.jpg', xHandle: 'dstrukt__' },
{ name: 'Kukks', title: 'Core Contributor', photo: '/images/speakers/MrKukks.jpg', xHandle: 'MrKukks' },
{ name: 'DSTRUKT', title: 'Product Designer', photo: '/images/speakers/dstrukt__.jpg', xHandle: 'dstrukt__' },
{ name: 'Kukks', title: 'Maintainer', photo: '/images/speakers/MrKukks.jpg', xHandle: 'MrKukks' },
{ name: 'ndeet', title: 'Ecommerce Integrations Developer', photo: '/images/speakers/ndeet.jpg', xHandle: 'ndeet' },
{ name: 'Nicolas Dorier', title: 'Founder & Lead Developer', photo: '/images/speakers/NicolasDorier.jpg', xHandle: 'NicolasDorier' },
{ name: 'Pavlenex', title: 'Project Manager', photo: '/images/speakers/pavlenex.jpg', xHandle: 'pavlenex' },
{ name: 'thgO.O', title: 'Full-Stack Developer', photo: '/images/speakers/thgO_O.jpg', xHandle: 'thgO_O' },
{ name: 'Tobses', title: 'Full-Time Contributor', photo: '/images/speakers/TChileta.jpg', xHandle: 'TChileta' },
{ name: 'Uncle Rockstar Developer', title: 'Core Contributor', photo: '/images/speakers/r0ckstardev.jpg', xHandle: 'r0ckstardev' },
{ name: 'webworthy', title: 'Contributor', photo: '/images/speakers/WebWorthy.jpg', xHandle: 'WebWorthy' },
{ name: 'TBD', title: 'Speaker', photo: undefined, xHandle: undefined },
{ name: 'Pavlenex', title: 'Product Manager', photo: '/images/speakers/pavlenex.jpg', xHandle: 'pavlenex' },
{ name: 'thgO.O', title: 'Core Contributor', photo: '/images/speakers/thgO_O.jpg', xHandle: 'thgO_O' },
{ name: 'Tobses', title: 'Core Contributor', photo: '/images/speakers/TChileta.jpg', xHandle: 'TChileta' },
{ name: 'Uncle Rockstar Developer', title: 'Maintainer', photo: '/images/speakers/r0ckstardev.jpg', xHandle: 'r0ckstardev' },
{ name: 'webworthy', title: 'Video & Marketing', photo: '/images/speakers/WebWorthy.jpg', xHandle: 'WebWorthy' },
// Add more speakers: { name, title, photo: '/images/speakers/handle.jpg', xHandle }
]

View File

@ -9,8 +9,10 @@ const THEME_COLORS: Record<Theme, string> = {
function getInitialTheme(): Theme {
if (typeof window === 'undefined') return 'dark'
const stored = localStorage.getItem('theme')
if (stored === 'light' || stored === 'dark') return stored
try {
const stored = localStorage.getItem('theme')
if (stored === 'light' || stored === 'dark') return stored
} catch { /* storage blocked — fall through to system preference */ }
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
@ -18,7 +20,9 @@ function applyTheme(theme: Theme) {
const root = document.documentElement
root.classList.toggle('dark', theme === 'dark')
root.style.colorScheme = theme
localStorage.setItem('theme', theme)
try {
localStorage.setItem('theme', theme)
} catch { /* storage blocked — theme works for this session only */ }
const meta = document.querySelector('meta[name="theme-color"]')
if (meta) meta.setAttribute('content', THEME_COLORS[theme])
}

View File

@ -145,6 +145,19 @@
to { opacity: 1; transform: translateY(0); }
}
/* Respect OS-level reduced-motion preference */
@media (prefers-reduced-motion: reduce) {
.reveal {
opacity: 1;
transform: none;
transition: none;
}
.card-enter {
animation: none;
opacity: 1;
}
}
/* Sticky bar slide-in from top */
@keyframes slideDown {
from { opacity: 0; transform: translateY(-100%); }