trial
This commit is contained in:
parent
c9bc8023ed
commit
87cabc1980
1 changed files with 85 additions and 217 deletions
|
|
@ -1,239 +1,107 @@
|
||||||
'use client'
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
import React, { useEffect, useRef } from 'react'
|
import { PayloadRedirects } from '@/components/PayloadRedirects'
|
||||||
import { cn } from '@/utilities/ui'
|
import configPromise from '@payload-config'
|
||||||
import { useThemeMode } from '@/hooks/useThemeMode'
|
import { getPayload, type RequiredDataFromCollectionSlug } from 'payload'
|
||||||
import { CMSLink } from '@/components/Link'
|
import { draftMode } from 'next/headers'
|
||||||
import RichText from '@/components/RichText'
|
import React, { cache } from 'react'
|
||||||
import type { Page } from '@/payload-types'
|
import { homeStatic } from '@/endpoints/seed/home-static'
|
||||||
|
|
||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
import { RenderBlocks } from '@/blocks/RenderBlocks'
|
||||||
|
import { RenderHero } from '@/heros/RenderHero'
|
||||||
|
import { generateMeta } from '@/utilities/generateMeta'
|
||||||
|
import PageClient from './page.client'
|
||||||
|
import { LivePreviewListener } from '@/components/LivePreviewListener'
|
||||||
|
import ScrollToTop from '@/components/ScrollToTop'
|
||||||
|
|
||||||
interface Star {
|
import HeroBackground from '@/components/HeroBackground'
|
||||||
x: number; y: number; r: number
|
import HeroPage from '@/components/HeroPage'
|
||||||
v: number; baseAlpha: number
|
|
||||||
phase: number; twinkleSpeed: number
|
export const dynamic = 'force-dynamic'
|
||||||
vx: number; vy: number
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Wave {
|
type Args = {
|
||||||
amp: number; freq: number; speed: number
|
params: Promise<{
|
||||||
phase: number; yBase: number; width: number; v: number
|
slug?: string
|
||||||
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
const LANDING_PAGE_SLUGS = ['home']
|
||||||
|
|
||||||
const STAR_TONES = [255, 210, 160, 110, 65]
|
export default async function Page({ params: paramsPromise }: Args) {
|
||||||
const WAVE_TONES = [55, 90, 125, 165, 205]
|
const { isEnabled: draft } = await draftMode()
|
||||||
const WAVE_CONFIGS = [
|
const { slug = 'home' } = await paramsPromise
|
||||||
{ amp: 26, freq: 0.011, speed: 0.35, phase: 0.0, yBase: 0.38, width: 1.6, toneIdx: 0 },
|
const decodedSlug = decodeURIComponent(slug)
|
||||||
{ amp: 20, freq: 0.015, speed: 0.50, phase: 1.3, yBase: 0.52, width: 1.8, toneIdx: 1 },
|
const url = '/' + decodedSlug
|
||||||
{ amp: 30, freq: 0.008, speed: 0.28, phase: 2.6, yBase: 0.63, width: 2.2, toneIdx: 2 },
|
|
||||||
{ amp: 16, freq: 0.019, speed: 0.65, phase: 0.8, yBase: 0.74, width: 2.0, toneIdx: 3 },
|
|
||||||
{ amp: 22, freq: 0.013, speed: 0.42, phase: 3.2, yBase: 0.85, width: 2.6, toneIdx: 4 },
|
|
||||||
]
|
|
||||||
|
|
||||||
// ─── Canvas helpers ───────────────────────────────────────────────────────────
|
let page: RequiredDataFromCollectionSlug<'pages'> | null
|
||||||
|
|
||||||
function makeStars(W: number, H: number): Star[] {
|
page = await queryPageBySlug({ slug: decodedSlug })
|
||||||
return Array.from({ length: 180 }, () => {
|
|
||||||
const i = Math.floor(Math.random() * STAR_TONES.length)
|
|
||||||
return {
|
|
||||||
x: Math.random() * W, y: Math.random() * H,
|
|
||||||
r: 0.4 + Math.random() * 1.6,
|
|
||||||
v: STAR_TONES[i], baseAlpha: 0.5 + i * 0.1,
|
|
||||||
phase: Math.random() * Math.PI * 2,
|
|
||||||
twinkleSpeed: 0.8 + Math.random() * 2.0,
|
|
||||||
vx: (Math.random() - 0.5) * 0.3,
|
|
||||||
vy: (Math.random() - 0.5) * 0.15,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeWaves(): Wave[] {
|
if (!page && decodedSlug === 'home') {
|
||||||
return WAVE_CONFIGS.map(c => ({ ...c, v: WAVE_TONES[c.toneIdx] }))
|
page = homeStatic
|
||||||
}
|
|
||||||
|
|
||||||
function drawStars(ctx: CanvasRenderingContext2D, stars: Star[], W: number, H: number, ga: number) {
|
|
||||||
const now = Date.now() / 1000
|
|
||||||
for (const s of stars) {
|
|
||||||
s.x += s.vx; s.y += s.vy
|
|
||||||
if (s.x < 0) s.x = W; if (s.x > W) s.x = 0
|
|
||||||
if (s.y < 0) s.y = H; if (s.y > H) s.y = 0
|
|
||||||
const tw = 0.35 + 0.65 * ((Math.sin(now * s.twinkleSpeed + s.phase) + 1) / 2)
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2)
|
|
||||||
ctx.fillStyle = `rgba(${s.v},${s.v},${s.v},${s.baseAlpha * tw * ga})`
|
|
||||||
ctx.fill()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function wavePts(w: Wave, W: number, H: number, now: number) {
|
if (!page) {
|
||||||
const pts: { x: number; y: number }[] = []
|
return <PayloadRedirects url={url} />
|
||||||
for (let x = 0; x <= W; x += 8)
|
|
||||||
pts.push({ x, y: H * w.yBase + Math.sin(x * w.freq + w.phase + now * w.speed) * w.amp })
|
|
||||||
return pts
|
|
||||||
}
|
|
||||||
|
|
||||||
function wavePath(ctx: CanvasRenderingContext2D, pts: { x: number; y: number }[]) {
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.moveTo(pts[0].x, pts[0].y)
|
|
||||||
for (let i = 0; i < pts.length - 1; i++) {
|
|
||||||
const mx = (pts[i].x + pts[i + 1].x) / 2
|
|
||||||
const my = (pts[i].y + pts[i + 1].y) / 2
|
|
||||||
ctx.quadraticCurveTo(pts[i].x, pts[i].y, mx, my)
|
|
||||||
}
|
}
|
||||||
ctx.lineTo(pts[pts.length - 1].x, pts[pts.length - 1].y)
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawWaves(ctx: CanvasRenderingContext2D, waves: Wave[], W: number, H: number, ga: number, bgR: number) {
|
const { hero, layout } = page
|
||||||
const now = Date.now() / 1000
|
|
||||||
const bg = Math.round(bgR)
|
if (LANDING_PAGE_SLUGS.includes(decodedSlug)) {
|
||||||
for (const w of waves) {
|
return (
|
||||||
const v = w.v
|
<HeroPage richText={hero.richText} links={hero.links}>
|
||||||
const yBase = H * w.yBase
|
<RenderBlocks blocks={layout} />
|
||||||
const pts = wavePts(w, W, H, now)
|
<ScrollToTop />
|
||||||
const grad = ctx.createLinearGradient(0, yBase - w.amp, 0, yBase + w.amp * 3)
|
</HeroPage>
|
||||||
grad.addColorStop(0, `rgba(${v},${v},${v},${0.030 * ga})`)
|
)
|
||||||
grad.addColorStop(0.4, `rgba(${v},${v},${v},${0.012 * ga})`)
|
|
||||||
grad.addColorStop(1, `rgba(${bg},${bg},${bg},0)`)
|
|
||||||
wavePath(ctx, pts)
|
|
||||||
ctx.lineTo(W, H); ctx.lineTo(0, H); ctx.closePath()
|
|
||||||
ctx.fillStyle = grad
|
|
||||||
ctx.fill()
|
|
||||||
wavePath(ctx, pts)
|
|
||||||
ctx.strokeStyle = `rgba(${v},${v},${v},${0.85 * ga})`
|
|
||||||
ctx.lineWidth = w.width
|
|
||||||
ctx.lineJoin = 'round'
|
|
||||||
ctx.lineCap = 'round'
|
|
||||||
ctx.stroke()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Background canvas ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function Background({ isDark }: { isDark: boolean }) {
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
||||||
const darkRef = useRef(isDark)
|
|
||||||
useEffect(() => { darkRef.current = isDark }, [isDark])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const canvas = canvasRef.current
|
|
||||||
if (!canvas) return
|
|
||||||
const ctx = canvas.getContext('2d')
|
|
||||||
if (!ctx) return
|
|
||||||
let W = 0, H = 0
|
|
||||||
let stars: Star[] = [], waves: Wave[] = []
|
|
||||||
let bgR = 0, bgG = 0, bgB = 0, crossfade = 0
|
|
||||||
let rafId: number
|
|
||||||
|
|
||||||
function resize() {
|
|
||||||
W = canvas!.width = canvas!.offsetWidth
|
|
||||||
H = canvas!.height = canvas!.offsetHeight
|
|
||||||
stars = makeStars(W, H)
|
|
||||||
waves = makeWaves()
|
|
||||||
}
|
|
||||||
|
|
||||||
function loop() {
|
|
||||||
const dark = darkRef.current
|
|
||||||
const tBg = dark ? 0 : 255
|
|
||||||
const tCross = dark ? 0 : 1
|
|
||||||
bgR += (tBg - bgR) * 0.04
|
|
||||||
bgG += (tBg - bgG) * 0.04
|
|
||||||
bgB += (tBg - bgB) * 0.04
|
|
||||||
crossfade += (tCross - crossfade) * 0.03
|
|
||||||
ctx!.fillStyle = `rgb(${Math.round(bgR)},${Math.round(bgG)},${Math.round(bgB)})`
|
|
||||||
ctx!.fillRect(0, 0, W, H)
|
|
||||||
const sa = 1 - crossfade, wa = crossfade
|
|
||||||
if (sa > 0.01) drawStars(ctx!, stars, W, H, sa)
|
|
||||||
if (wa > 0.01) drawWaves(ctx!, waves, W, H, wa, bgR)
|
|
||||||
rafId = requestAnimationFrame(loop)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ro = new ResizeObserver(resize)
|
|
||||||
ro.observe(canvas)
|
|
||||||
resize()
|
|
||||||
rafId = requestAnimationFrame(loop)
|
|
||||||
return () => { cancelAnimationFrame(rafId); ro.disconnect() }
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return <canvas ref={canvasRef} style={{ display: 'block', width: '100%', height: '100%' }} />
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Hero section ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
type HeroProps = Pick<Page['hero'], 'richText' | 'links'>
|
|
||||||
|
|
||||||
function HeroSection({ richText, links }: HeroProps) {
|
|
||||||
return (
|
|
||||||
<section className="relative mx-auto w-full max-w-5xl">
|
|
||||||
{/* Outer border lines */}
|
|
||||||
<div aria-hidden="true" className="absolute inset-0 mx-auto hidden min-h-screen w-full max-w-5xl lg:block pointer-events-none">
|
|
||||||
<div className="mask-y-from-80% mask-y-to-100% absolute inset-y-0 left-0 z-10 h-full w-px bg-foreground/10" />
|
|
||||||
<div className="mask-y-from-80% mask-y-to-100% absolute inset-y-0 right-0 z-10 h-full w-px bg-foreground/10" />
|
|
||||||
</div>
|
|
||||||
{/* Inner border lines */}
|
|
||||||
<div aria-hidden="true" className="absolute inset-0 -z-[1] size-full overflow-hidden pointer-events-none">
|
|
||||||
<div className="absolute inset-y-0 left-4 w-px bg-linear-to-b from-transparent via-foreground/10 to-foreground/10 md:left-8" />
|
|
||||||
<div className="absolute inset-y-0 right-4 w-px bg-linear-to-b from-transparent via-foreground/10 to-foreground/10 md:right-8" />
|
|
||||||
<div className="absolute inset-y-0 left-8 w-px bg-linear-to-b from-transparent via-foreground/5 to-foreground/5 md:left-12" />
|
|
||||||
<div className="absolute inset-y-0 right-8 w-px bg-linear-to-b from-transparent via-foreground/5 to-foreground/5 md:right-12" />
|
|
||||||
</div>
|
|
||||||
<div className="relative flex flex-col items-center justify-center gap-5 pt-32 pb-28 px-6 text-center">
|
|
||||||
{richText && (
|
|
||||||
<RichText
|
|
||||||
className="fade-in slide-in-from-bottom-10 animate-in fill-mode-backwards delay-100 duration-500 ease-out max-w-[36.5rem] text-center text-foreground"
|
|
||||||
data={richText}
|
|
||||||
enableGutter={false}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{Array.isArray(links) && links.length > 0 && (
|
|
||||||
<div className="fade-in slide-in-from-bottom-10 animate-in fill-mode-backwards delay-300 duration-500 ease-out flex flex-wrap items-center justify-center gap-3 pt-2">
|
|
||||||
{links.map(({ link }, i) => (
|
|
||||||
<CMSLink
|
|
||||||
key={i}
|
|
||||||
{...link}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-2 rounded-full px-5 py-2.5 text-sm font-medium transition-all',
|
|
||||||
i === 0
|
|
||||||
? 'bg-foreground text-background hover:bg-foreground/90'
|
|
||||||
: 'border border-foreground/25 text-foreground hover:bg-foreground/8'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Main export ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
interface HeroPageProps {
|
|
||||||
richText?: Page['hero']['richText']
|
|
||||||
links?: Page['hero']['links']
|
|
||||||
children?: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HeroPage({ richText, links, children }: HeroPageProps) {
|
|
||||||
const { isDark } = useThemeMode()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative', minHeight: '100vh' }}>
|
<div style={{ position: 'relative', minHeight: '100vh' }}>
|
||||||
<div style={{ position: 'fixed', inset: 0, zIndex: -1 }}>
|
<div style={{ position: 'fixed', inset: 0, zIndex: 0 }}>
|
||||||
<Background isDark={isDark} />
|
<HeroBackground showToggle={false} />
|
||||||
</div>
|
|
||||||
<div style={{ position: 'relative', zIndex: 1 }}>
|
|
||||||
<main>
|
|
||||||
<HeroSection richText={richText} links={links} />
|
|
||||||
{children && (
|
|
||||||
<div id="content">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
|
<article className="pt-16" style={{ position: 'relative', zIndex: 1 }}>
|
||||||
|
<PageClient />
|
||||||
|
<PayloadRedirects disableNotFound url={url} />
|
||||||
|
{draft && <LivePreviewListener />}
|
||||||
|
<RenderHero {...hero} />
|
||||||
|
<RenderBlocks blocks={layout} />
|
||||||
|
</article>
|
||||||
|
<ScrollToTop />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params: paramsPromise }: Args): Promise<Metadata> {
|
||||||
|
const { slug = 'home' } = await paramsPromise
|
||||||
|
const decodedSlug = decodeURIComponent(slug)
|
||||||
|
const page = await queryPageBySlug({ slug: decodedSlug })
|
||||||
|
|
||||||
|
return generateMeta({ doc: page })
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryPageBySlug = cache(async ({ slug }: { slug: string }) => {
|
||||||
|
const { isEnabled: draft } = await draftMode()
|
||||||
|
|
||||||
|
const payload = await getPayload({ config: configPromise })
|
||||||
|
|
||||||
|
const result = await payload.find({
|
||||||
|
collection: 'pages',
|
||||||
|
draft,
|
||||||
|
limit: 1,
|
||||||
|
pagination: false,
|
||||||
|
overrideAccess: draft,
|
||||||
|
where: {
|
||||||
|
slug: {
|
||||||
|
equals: slug,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return result.docs?.[0] || null
|
||||||
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue