lw14 ticketing #34444
* mamba init * shader-updates * feat: add better bloom * cleanup * Fix GLTF model loading to respect original material properties Signed-off-by: Piotr Goszczyński <pg@tonik.com> * Fix GLTF model loading to respect original material properties Signed-off-by: Piotr Goszczyński <pg@tonik.com> * stash changes * Update font to Arial in mamba ticket component Signed-off-by: Piotr Goszczyński <pg@tonik.com> * Fix wireframe size in debug mode Signed-off-by: Piotr Goszczyński <pg@tonik.com> * Fix wireframe size in debug mode by attaching to child mesh instead of model Signed-off-by: Piotr Goszczyński <pg@tonik.com> * fix scaling * fix * feat: load png with color material * fix: spline material * add toggling of effects * stash * feat: ticket model * load better model * strcture * fix rotation speed * rotation * fix rotation * new model * prepare final designs * add color palette * feat: add fonts and rendering * render dynamic texture * load all textures * fix: color sace * feat: secret model * improve perf * postprocessing * fix: output pass * improve shaders * increase glitches * cleanup * update colors * remove unused files * add header * feat: ui * fix: raycaster * feat: add button toggling * Improve ui * fix corners positoins * add tunnel effect * fix: dates * add dynamic rocket position rendering * add different logs * feat: add migrations and connect to real data * chore: state cleanup to use reducer Migrate logic to centralized reducer for ease of debugging * feat: add hud layout * add grain effect to hud * limit glitching, move page to main url * add vignette effect * fix: desktop layout * remove vertical space * feat: save secret state on ticket upgrade * feat: make platinum version take priority over secret * add mobile header * remove unnecessary allocations * fix: resize hud * add page scaling for mobile layouts * add ticket scaling based on resolution * adjust mobile styles * fix: resize texture distortion * add gauges mobile breaking points * feat: mobile claim layout * mobile styles * Show ticket referer * rename files * update copy links * add partymode part 1 * feat: og images, lw14 * add presence tracking * add live updates of guages * Update meetups label * update label * username ticket lw14 * add proper subscription on first render * remove green edge * feat: remove species & planet from ticket scene * change urls * feat/mamba-assets: update assets * feat: fix shader grain * add color to shader * remove vertical space * fix: og image generation * fix referal and removing of access token * make urls nicer * feat/mamba-assets: dynamic line break + dynamic background * small adjustments * add trimming to ogimages * minor: missing font file * fix og image line wrapping * remove dot from meetups * improve ticket quality * upss, forgot to commit one file * feat add dynamic og image linking * update static site url * fix low mobile resolution * add banners * simplify banner loading * add missing fonts * fix name breaking to prioritize spaces * update quality based on devicePixelRatio * fix button positioning * revert: feature flags disabling * update start date * limit shader effects and disable axis, potential fix to chrome freeze * feat: init mamba 01 * create narrow ticket scene * narrow scene for ticket and fix follow mouse * fix dates * alternative version * updated ticket page * remove migrations * remove files * prettier * prettier * Fix TS and prettier issues * Fix TS issues * Test * Remove console log * fix updating ticket on share * chore: lw cleanup * fix ticket layout * fix chrome crashes * lower crt intensity * tunnel improvements * fix tunnel effect * chore: z-index and prettier fix * chore: padding * fix double tunnel * chore: font on banners --------- Signed-off-by: Piotr Goszczyński <pg@tonik.com> Co-authored-by: dztonik <dz@tonik.com> Co-authored-by: Jonathan Summers-Muir <MildTomato@users.noreply.github.com> Co-authored-by: Joshen Lim <joshenlimek@gmail.com> Co-authored-by: Francesco Sansalvadore <f.sansalvadore@gmail.com>
@@ -11,6 +11,7 @@ import { QueryClientProvider } from './data/queryClient.client'
|
||||
import { PageTelemetry } from './telemetry/telemetry.client'
|
||||
import { ScrollRestoration } from './ui/helpers.scroll.client'
|
||||
import { ThemeSandbox } from './ui/theme.client'
|
||||
import { PromoToast } from 'ui-patterns'
|
||||
|
||||
/**
|
||||
* Global providers that wrap the entire app
|
||||
@@ -27,6 +28,7 @@ function GlobalProviders({ children }: PropsWithChildren) {
|
||||
<CommandProvider>
|
||||
<div className="flex flex-col">
|
||||
<SiteLayout>
|
||||
<PromoToast />
|
||||
{children}
|
||||
<DocsCommandMenu />
|
||||
</SiteLayout>
|
||||
|
||||
BIN
apps/docs/public/img/launchweek/14/promo-banner-bg.png
Normal file
|
After Width: | Height: | Size: 382 KiB |
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { ImageResponse } from '@vercel/og'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { themes } from '~/components/LaunchWeek/13/Ticket/ticketThemes'
|
||||
import { themes } from '~/components/LaunchWeek/14/utils/ticketThemes'
|
||||
|
||||
export const runtime = 'edge' // 'nodejs' is the default
|
||||
export const dynamic = 'force-dynamic' // defaults to auto
|
||||
@@ -15,11 +15,11 @@ const corsHeaders = {
|
||||
|
||||
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL
|
||||
|
||||
const STORAGE_URL = `${SUPABASE_URL}/storage/v1/object/public/images/launch-week/lw13`
|
||||
const STORAGE_URL = `${SUPABASE_URL}/storage/v1/object/public/images/launch-week/lw14`
|
||||
|
||||
// Load custom font
|
||||
const FONT_URL = `${STORAGE_URL}/assets/font/CircularStd-Book.otf`
|
||||
const MONO_FONT_URL = `${STORAGE_URL}/assets/font/SourceCodePro-Regular.ttf`
|
||||
const FONT_URL = `${STORAGE_URL}/assets/font/Nippo-Regular.otf`
|
||||
const MONO_FONT_URL = `${STORAGE_URL}/assets/font/DepartureMono-Regular.otf`
|
||||
const font = fetch(new URL(FONT_URL, import.meta.url)).then((res) => res.arrayBuffer())
|
||||
const mono_font = fetch(new URL(MONO_FONT_URL, import.meta.url)).then((res) => res.arrayBuffer())
|
||||
|
||||
@@ -45,14 +45,14 @@ export async function GET(req: Request, res: Response) {
|
||||
await supabaseAdminClient
|
||||
.from(LW_TABLE)
|
||||
.update({ shared_on_twitter: 'now' })
|
||||
.eq('launch_week', 'lw13')
|
||||
.eq('launch_week', 'lw14')
|
||||
.eq('username', username)
|
||||
.is('shared_on_twitter', null)
|
||||
} else if (userAgent?.toLocaleLowerCase().includes('linkedin')) {
|
||||
await supabaseAdminClient
|
||||
.from(LW_TABLE)
|
||||
.update({ shared_on_linkedin: 'now' })
|
||||
.eq('launch_week', 'lw13')
|
||||
.eq('launch_week', 'lw14')
|
||||
.eq('username', username)
|
||||
.is('shared_on_linkedin', null)
|
||||
}
|
||||
@@ -61,61 +61,100 @@ export async function GET(req: Request, res: Response) {
|
||||
const { data: user, error } = await supabaseAdminClient
|
||||
.from(LW_MATERIALIZED_VIEW)
|
||||
.select(
|
||||
'id, name, metadata, shared_on_twitter, shared_on_linkedin, platinum, secret, role, company, location'
|
||||
'id, name, metadata, shared_on_twitter, shared_on_linkedin, platinum, secret, role, company, location, ticket_number'
|
||||
)
|
||||
.eq('launch_week', 'lw13')
|
||||
.eq('launch_week', 'lw14')
|
||||
.eq('username', username)
|
||||
.maybeSingle()
|
||||
|
||||
if (error) console.log('fetch error', error.message)
|
||||
if (error) console.log('Failed to fetch user. Inner error:', error.message)
|
||||
if (!user) throw new Error(error?.message ?? 'user not found')
|
||||
|
||||
const {
|
||||
name,
|
||||
secret,
|
||||
platinum: isPlatinum,
|
||||
metadata,
|
||||
shared_on_twitter: sharedOnTwitter,
|
||||
shared_on_linkedin: sharedOnLinkedIn,
|
||||
ticket_number,
|
||||
} = user
|
||||
|
||||
const isDark = metadata.theme !== 'light'
|
||||
|
||||
const platinum = isPlatinum ?? (!!sharedOnTwitter && !!sharedOnLinkedIn) ?? false
|
||||
if (assumePlatinum && !platinum)
|
||||
return await fetch(`${STORAGE_URL}/assets/platinum_no_meme.jpg`)
|
||||
if (assumePlatinum && !platinum) return await fetch(`${STORAGE_URL}/images/og-14-platinum.png`)
|
||||
|
||||
const seatCode = (466561 + (ticket_number || 0)).toString(36).toUpperCase()
|
||||
|
||||
// Generate image and upload to storage.
|
||||
const ticketType = secret ? 'secret' : platinum ? 'platinum' : 'regular'
|
||||
|
||||
const STYLING_CONFIG = (isDark?: boolean) => ({
|
||||
TICKET_FOREGROUND: themes(isDark)[ticketType].TICKET_FOREGROUND,
|
||||
const STYLING_CONFIG = () => ({
|
||||
TICKET_FOREGROUND: themes()[ticketType].TICKET_FOREGROUND,
|
||||
})
|
||||
|
||||
const TICKET_THEME = {
|
||||
regular: {
|
||||
color: 'rgb(239, 239, 239)',
|
||||
background: 'transparent',
|
||||
},
|
||||
secret: {
|
||||
color: 'rgba(44, 244, 148)',
|
||||
background: 'rgba(44, 244, 148, 0.2)',
|
||||
},
|
||||
platinum: {
|
||||
color: 'rgba(255, 199, 58)',
|
||||
background: 'rgba(255, 199, 58, 0.2)',
|
||||
},
|
||||
}
|
||||
|
||||
const fontData = await font
|
||||
const monoFontData = await mono_font
|
||||
|
||||
const OG_WIDTH = 1200
|
||||
const OG_HEIGHT = 628
|
||||
const USERNAME_LEFT = 400
|
||||
const USERNAME_BOTTOM = 100
|
||||
const USERNAME_WIDTH = 400
|
||||
const DISPLAY_NAME = name || username
|
||||
const USERNAME_BOTTOM = 435
|
||||
|
||||
const BACKGROUND = (isDark?: boolean) => ({
|
||||
const BACKGROUND = () => ({
|
||||
regular: {
|
||||
LOGO: `${STORAGE_URL}/assets/supabase/supabase-logo-icon.png?v4`,
|
||||
BACKGROUND_IMG: `${STORAGE_URL}/assets/ticket-og-bg-regular-${isDark ? 'dark' : 'light'}.png?v4`,
|
||||
BACKGROUND_IMG: `${STORAGE_URL}/assets/og-14-regular.png`,
|
||||
},
|
||||
platinum: {
|
||||
LOGO: `${STORAGE_URL}/assets/supabase/supabase-logo-icon.png?v4`,
|
||||
BACKGROUND_IMG: `${STORAGE_URL}/assets/ticket-og-bg-platinum.png?v4`,
|
||||
BACKGROUND_IMG: `${STORAGE_URL}/assets/og-14-platinum.png`,
|
||||
},
|
||||
secret: {
|
||||
LOGO: `${STORAGE_URL}/assets/supabase/supabase-logo-icon.png?v4`,
|
||||
BACKGROUND_IMG: `${STORAGE_URL}/assets/ticket-og-bg-secret.png?v4`,
|
||||
BACKGROUND_IMG: `${STORAGE_URL}/assets/og-14-secret.png`,
|
||||
},
|
||||
})
|
||||
|
||||
const usernameToLines = (username: string): string[] => {
|
||||
const lineLenght = 12
|
||||
|
||||
const line1 = username.slice(0, lineLenght).trim().replace(/ /g, '\u00A0')
|
||||
const line2 = username
|
||||
.slice(lineLenght, lineLenght * 2)
|
||||
.trim()
|
||||
.replace(/ /g, '\u00A0')
|
||||
let line3 = username
|
||||
.slice(lineLenght * 2)
|
||||
.trim()
|
||||
.replace(/ /g, '\u00A0')
|
||||
|
||||
// NOTE: If third line is too long, trim to 8 characters and add '...'
|
||||
if (line3.length > lineLenght) {
|
||||
line3 = line3.slice(0, 8) + '...'
|
||||
}
|
||||
|
||||
// NOTE: Only include non-empty lines
|
||||
return [line1, line2, line3].filter((line) => line.length > 0)
|
||||
}
|
||||
|
||||
const computeBackgroundWidth = (letters: number) => {
|
||||
return 100 + (letters * 40 + (letters - 1) * 12)
|
||||
}
|
||||
const lines = usernameToLines(name ?? username)
|
||||
|
||||
const generatedTicketImage = new ImageResponse(
|
||||
(
|
||||
<>
|
||||
@@ -124,16 +163,15 @@ export async function GET(req: Request, res: Response) {
|
||||
width: '1200px',
|
||||
height: '628px',
|
||||
position: 'relative',
|
||||
fontFamily: '"Circular"',
|
||||
fontFamily: '"Nippo-Regular"',
|
||||
overflow: 'hidden',
|
||||
color: STYLING_CONFIG(isDark).TICKET_FOREGROUND,
|
||||
color: STYLING_CONFIG().TICKET_FOREGROUND,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '60px',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{/* Background */}
|
||||
{/* Background */}
|
||||
<img
|
||||
width="1204"
|
||||
height="634"
|
||||
@@ -145,42 +183,56 @@ export async function GET(req: Request, res: Response) {
|
||||
right: '-2px',
|
||||
zIndex: '0',
|
||||
backgroundSize: 'cover',
|
||||
backgroundColor: STYLING_CONFIG().TICKET_FOREGROUND,
|
||||
}}
|
||||
src={BACKGROUND(isDark)[ticketType].BACKGROUND_IMG}
|
||||
src={BACKGROUND()[ticketType].BACKGROUND_IMG}
|
||||
/>
|
||||
|
||||
{/* Name & username */}
|
||||
{/* Seat number */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
transform: 'rotate(-90deg)',
|
||||
fontFamily: '"Nippo-Regular"',
|
||||
fontSize: '82px',
|
||||
position: 'absolute',
|
||||
bottom: USERNAME_BOTTOM,
|
||||
left: USERNAME_LEFT,
|
||||
width: USERNAME_WIDTH,
|
||||
height: 'auto',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'clip',
|
||||
textAlign: 'left',
|
||||
letterSpacing: '-0.5px',
|
||||
marginBottom: '10px',
|
||||
color: TICKET_THEME[ticketType].color,
|
||||
top: 70,
|
||||
right: 135,
|
||||
}}
|
||||
>
|
||||
<p
|
||||
{seatCode}
|
||||
</div>
|
||||
|
||||
{/* Render each username line */}
|
||||
{lines.map((line, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
margin: '0',
|
||||
padding: '0',
|
||||
fontSize: '54',
|
||||
lineHeight: '105%',
|
||||
display: 'flex',
|
||||
marginBottom: '10px',
|
||||
position: 'absolute',
|
||||
bottom: USERNAME_BOTTOM - index * 80,
|
||||
paddingLeft: '93px',
|
||||
paddingRight: 0,
|
||||
left: 27,
|
||||
height: '61px',
|
||||
width: `${computeBackgroundWidth(line.length)}px`,
|
||||
backgroundColor: TICKET_THEME[ticketType].background,
|
||||
}}
|
||||
>
|
||||
{DISPLAY_NAME}
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: '"DepartureMono-Regular"',
|
||||
margin: '0',
|
||||
color: TICKET_THEME[ticketType].color,
|
||||
padding: '0',
|
||||
fontSize: '82px',
|
||||
lineHeight: '56px',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
{line}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
@@ -189,12 +241,12 @@ export async function GET(req: Request, res: Response) {
|
||||
height: OG_HEIGHT,
|
||||
fonts: [
|
||||
{
|
||||
name: 'Circular',
|
||||
name: 'Nippo-Regular',
|
||||
data: fontData,
|
||||
style: 'normal',
|
||||
},
|
||||
{
|
||||
name: 'SourceCodePro',
|
||||
name: 'DepartureMono-Regular',
|
||||
data: monoFontData,
|
||||
style: 'normal',
|
||||
},
|
||||
@@ -213,7 +265,7 @@ export async function GET(req: Request, res: Response) {
|
||||
// Upload image to storage.
|
||||
const { error: storageError } = await supabaseAdminClient.storage
|
||||
.from('images')
|
||||
.upload(`launch-week/lw13/og/${ticketType}/${username}.png`, generatedTicketImage.body!, {
|
||||
.upload(`launch-week/lw14/og/${ticketType}/${username}.png`, generatedTicketImage.body!, {
|
||||
contentType: 'image/png',
|
||||
// cacheControl: `${60 * 60 * 24 * 7}`,
|
||||
cacheControl: `0`,
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import _announcement from './data/Announcement.json'
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
export interface AnnouncementProps {
|
||||
show: boolean
|
||||
text: string
|
||||
launchDate: string
|
||||
link: string
|
||||
badge?: string
|
||||
}
|
||||
|
||||
const announcement = _announcement as AnnouncementProps
|
||||
|
||||
interface AnnouncementComponentProps {
|
||||
show?: boolean
|
||||
className?: string
|
||||
link?: string
|
||||
}
|
||||
|
||||
const Announcement = ({
|
||||
show = true,
|
||||
className,
|
||||
children,
|
||||
link,
|
||||
}: PropsWithChildren<AnnouncementComponentProps>) => {
|
||||
const [hidden, setHidden] = useState(true)
|
||||
|
||||
const router = useRouter()
|
||||
const isLaunchWeekSection = router.pathname.includes('launch-week')
|
||||
|
||||
// override to hide announcement
|
||||
if (!show || !announcement.show) return null
|
||||
|
||||
// construct the key for the announcement, based on the title text
|
||||
const announcementKey = 'announcement_' + announcement.text.replace(/ /g, '')
|
||||
|
||||
// window.localStorage is kept inside useEffect
|
||||
// to prevent error
|
||||
useEffect(function () {
|
||||
if (!window.localStorage.getItem(announcementKey)) {
|
||||
return setHidden(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
function handleClose(event: any) {
|
||||
event.stopPropagation()
|
||||
|
||||
window.localStorage.setItem(announcementKey, 'hidden')
|
||||
return setHidden(true)
|
||||
}
|
||||
|
||||
// Always show if on LW section
|
||||
if (!isLaunchWeekSection && hidden) {
|
||||
return null
|
||||
} else {
|
||||
return (
|
||||
<div className={['relative z-40 w-full cursor-pointer', className].join(' ')}>
|
||||
{!isLaunchWeekSection && (
|
||||
<div
|
||||
className="absolute z-50 right-4 flex h-full items-center opacity-100 text-white transition-opacity hover:opacity-90"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<X size={16} />
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
<Link href={link ?? announcement.link} className="absolute inset-0 z-40"></Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Announcement
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"show": true,
|
||||
"text": "Supabase Launch Week 13: Day 1",
|
||||
"launchDate": "2023-08-07T09:00:00.000-07:00",
|
||||
"link": "https://supabase.com/launch-week#day-1",
|
||||
"badge": "Get your ticket"
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import Link from 'next/link'
|
||||
import { Button } from 'ui'
|
||||
import SectionContainer from '~/components/Layouts/SectionContainer'
|
||||
import { useSendTelemetryEvent } from '~/lib/telemetry'
|
||||
import AnnouncementBadge from '../Announcement/Badge'
|
||||
|
||||
const Hero = () => {
|
||||
const sendTelemetryEvent = useSendTelemetryEvent()
|
||||
@@ -15,6 +16,14 @@ const Hero = () => {
|
||||
<div className="mx-auto max-w-2xl lg:col-span-6 lg:flex lg:items-center justify-center text-center">
|
||||
<div className="relative z-10 lg:h-auto pt-[90px] lg:pt-[90px] lg:min-h-[300px] flex flex-col items-center justify-center sm:mx-auto md:w-3/4 lg:mx-0 lg:w-full gap-4 lg:gap-8">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="z-40 w-full flex justify-center -mt-4 lg:-mt-12 mb-8">
|
||||
<AnnouncementBadge
|
||||
className='font-["Departure_Mono"]'
|
||||
url="/launch-week"
|
||||
badge="Launch Week 14"
|
||||
announcement="Claim ticket"
|
||||
/>
|
||||
</div>
|
||||
<h1 className="text-foreground text-4xl sm:text-5xl sm:leading-none lg:text-7xl">
|
||||
<span className="block text-foreground">Build in a weekend</span>
|
||||
<span className="text-brand block md:ml-0">Scale to millions</span>
|
||||
|
||||
@@ -17,7 +17,7 @@ const LWHeader = ({ className }: { className?: string }) => {
|
||||
Launch Week <span className="font-mono">12</span>
|
||||
</h1>
|
||||
<p className="text-foreground-lighter md:text-xl max-w-xs md:max-w-md">
|
||||
Join us for a week of new features and find new ways to level up your development
|
||||
Join us for a week of announcements
|
||||
</p>
|
||||
</SectionContainer>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React from 'react'
|
||||
import { AnimatePresence, m, LazyMotion, domAnimation } from 'framer-motion'
|
||||
import { AnimatePresence, LazyMotion, domAnimation, m } from 'framer-motion'
|
||||
import { cn } from 'ui'
|
||||
import { DEFAULT_TRANSITION, INITIAL_BOTTOM, getAnimation } from '~/lib/animations'
|
||||
import { LW13_DATE } from '~/lib/constants'
|
||||
|
||||
import CanvasSingleMode from '~/components/LaunchWeek/13/Multiplayer/CanvasSingleMode'
|
||||
import ThreeTicketCanvas from '~/components/LaunchWeek/13/ThreeTicketCanvas'
|
||||
import SectionContainer from '~/components/Layouts/SectionContainer'
|
||||
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
|
||||
import useWinningChances from '~/components/LaunchWeek/hooks/useWinningChances'
|
||||
import SectionContainer from '~/components/Layouts/SectionContainer'
|
||||
|
||||
import TicketForm from '~/components/LaunchWeek/13/Ticket/TicketForm'
|
||||
import TicketSwagCtaBox from '~/components/LaunchWeek/13/Ticket/TicketSwagCtaBox'
|
||||
@@ -119,15 +118,27 @@ const TicketingFlow = () => {
|
||||
hasSecretTicket ? (
|
||||
<p>Got the gold ticket, {FIRST_NAME}!</p>
|
||||
) : (
|
||||
<p>Good to see you, {FIRST_NAME}!</p>
|
||||
<p className='font-["Departure_Mono"] uppercase'>
|
||||
Good to see you, {FIRST_NAME}!
|
||||
</p>
|
||||
)
|
||||
) : winningChances !== 2 ? (
|
||||
<>
|
||||
{hasSecretTicket && <p>{FIRST_NAME}, you're gold!</p>}
|
||||
{!hasSecretTicket && <p>Good to see you, {FIRST_NAME}!</p>}
|
||||
{hasSecretTicket && (
|
||||
<p className='font-["Departure_Mono"] uppercase'>
|
||||
{FIRST_NAME}, you're gold!
|
||||
</p>
|
||||
)}
|
||||
{!hasSecretTicket && (
|
||||
<p className='font-["Departure_Mono"] uppercase'>
|
||||
Good to see you, {FIRST_NAME}!
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p>Good to see you, {FIRST_NAME}!</p>
|
||||
<p className='font-["Departure_Mono"] uppercase'>
|
||||
Good to see you, {FIRST_NAME}!
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
101
apps/www/components/LaunchWeek/14/ActionButton.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { KeyboardEvent, ReactNode, useRef } from 'react'
|
||||
import { useKey } from 'react-use'
|
||||
import { cn } from 'ui'
|
||||
import { useCommandMenuOpen } from 'ui-patterns'
|
||||
|
||||
const actionButtonVariants = cva(
|
||||
'pl-1.5 pr-3 py-1.5 rounded shadow flex justify-center items-center gap-2 outline outline-1 outline-offset-[-1px] cursor-pointer flex-nowrap',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary:
|
||||
'bg-gradient-to-b from-emerald-400/0 via-emerald-400/30 to-emerald-400/0 shadow-[0px_0px_6px_0px_rgba(44,244,148,0.40)] outline-emerald-400/60',
|
||||
secondary:
|
||||
'bg-gradient-to-b from-neutral-600/0 via-neutral-600/30 to-neutral-600/0 shadow-[0px_0px_6px_0px_rgba(255,255,255,0.10)] outline-white/10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const iconVariants = cva(
|
||||
'w-5 h-5 px-2 rounded-sm outline outline-1 outline-offset-[-1px] inline-flex flex-col justify-center items-center gap-2',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: 'bg-emerald-950 outline-emerald-400/50',
|
||||
secondary: 'bg-neutral-800 outline-stone-500/50',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const textVariants = cva(
|
||||
'justify-center text-white text-xs leading-[20px] font-normal min-w-[108.25px] [@media(pointer:coarse)]:pl-2 text-nowrap',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: '[text-shadow:_0px_0px_10px_rgb(255_255_255_/_1.00)]',
|
||||
secondary: '[text-shadow:_0px_0px_4px_rgb(255_255_255_/_0.44)]',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ActionButtonProps extends VariantProps<typeof actionButtonVariants> {
|
||||
variant: 'primary' | 'secondary' | null | undefined
|
||||
icon: string
|
||||
children: ReactNode
|
||||
onClick?: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const ActionButton = ({
|
||||
variant,
|
||||
icon,
|
||||
children,
|
||||
className,
|
||||
onClick,
|
||||
}: ActionButtonProps) => {
|
||||
const buttonRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleKeyPress = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
onClick?.()
|
||||
}
|
||||
}
|
||||
|
||||
const isCommandMenuOpen = useCommandMenuOpen()
|
||||
useKey(icon.toLowerCase(), () => !isCommandMenuOpen && onClick?.(), { event: 'keydown' }, [
|
||||
isCommandMenuOpen,
|
||||
onClick,
|
||||
])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(actionButtonVariants({ variant }), className)}
|
||||
onClick={onClick}
|
||||
onKeyDown={handleKeyPress}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={typeof children === 'string' ? children : undefined}
|
||||
ref={buttonRef}
|
||||
>
|
||||
<div className={cn(iconVariants({ variant }), '[@media(pointer:coarse)]:hidden')}>
|
||||
<div className="text-center justify-center text-neutral-50 text-xs font-normal leading-none [text-shadow:_0px_0px_4px_rgb(255_255_255_/_0.25)]">
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
<div className={textVariants({ variant })}>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
131
apps/www/components/LaunchWeek/14/Header.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import { cn } from 'ui'
|
||||
import SectionContainerWithCn from '~/components/Layouts/SectionContainerWithCn'
|
||||
|
||||
export const TicketHeader = ({ children, hidden }: { children: ReactNode; hidden?: boolean }) => {
|
||||
if (hidden) return null
|
||||
|
||||
return (
|
||||
<SectionContainerWithCn
|
||||
height="none"
|
||||
className="grid lg:grid-cols-[127px_1fr_127px] grid-cols-[1fr_1fr] w-full justify-between gap-6 md:gap-8 px-0"
|
||||
>
|
||||
{children}
|
||||
</SectionContainerWithCn>
|
||||
)
|
||||
}
|
||||
|
||||
const SingleTick = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'w-px h-2.5 bg-neutral-500 shadow-[0px_0px_4px_0px_rgba(255,255,255,0.25)]',
|
||||
className
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const TicketHeaderClaim = () => {
|
||||
return (
|
||||
<div className="inline-flex justify-start items-center gap-8 w-full order-1 col-span-2 lg:col-auto lg:order-2">
|
||||
<div className="hidden gap-4 flex-wrap w-full h-2.5 overflow-hidden justify-end flex-shrink flex-1 md:flex">
|
||||
{Array.from({ length: 7 }).map((_, i) => (
|
||||
<SingleTick key={i} />
|
||||
))}
|
||||
</div>
|
||||
<div className="text-start md:text-center justify-center text-neutral-500 text-sm font-normal uppercase [text-shadow:_0px_0px_4px_rgb(255_255_255_/_0.25)] lg:text-nowrap flex-grow max-w-[220px] md:max-w-none">
|
||||
Launch Week is coming. Stay tuned!
|
||||
</div>
|
||||
<div className="hidden gap-4 flex-wrap w-full h-2.5 overflow-hidden flex-shrink flex-1 md:flex">
|
||||
{Array.from({ length: 7 }).map((_, i) => (
|
||||
<SingleTick key={i} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface TicketHeaderRemainingTimeProps {
|
||||
targetDate: string | Date
|
||||
}
|
||||
|
||||
export const TicketHeaderRemainingTime = ({
|
||||
targetDate: extTargetDate,
|
||||
}: TicketHeaderRemainingTimeProps) => {
|
||||
const [timeLeft, setTimeLeft] = useState<string>('---------------')
|
||||
|
||||
useEffect(() => {
|
||||
const calculateTimeLeft = () => {
|
||||
const targetDate = new Date(extTargetDate)
|
||||
const now = new Date()
|
||||
|
||||
// Calculate the time difference in milliseconds
|
||||
const difference = targetDate.getTime() - now.getTime()
|
||||
|
||||
if (difference <= 0) {
|
||||
setTimeLeft('00D:00H:00M:00S')
|
||||
return
|
||||
}
|
||||
|
||||
const days = Math.floor(difference / (1000 * 60 * 60 * 24))
|
||||
const hours = Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
|
||||
const minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60))
|
||||
const seconds = Math.floor((difference % (1000 * 60)) / 1000)
|
||||
|
||||
// Format the time left
|
||||
setTimeLeft(
|
||||
`${days.toString().padStart(2, '0')}D:${hours.toString().padStart(2, '0')}H:${minutes
|
||||
.toString()
|
||||
.padStart(2, '0')}M:${seconds.toString().padStart(2, '0')}S`
|
||||
)
|
||||
}
|
||||
|
||||
calculateTimeLeft()
|
||||
const interval = setInterval(calculateTimeLeft, 1000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [extTargetDate])
|
||||
|
||||
return (
|
||||
<div className="flex-1 order-2 lg:order-1">
|
||||
<div className="text-justify justify-center text-emerald-400 text-sm font-normal uppercase">
|
||||
{timeLeft}
|
||||
</div>
|
||||
<div className="text-neutral-500 text-justify justify-center text-xs font-normal uppercase">
|
||||
TIME LEFT
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const TicketHeaderDate = () => {
|
||||
const [time, setTime] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
const updateTime = () => {
|
||||
const now = new Date()
|
||||
const hours = now.getHours().toString().padStart(2, '0')
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0')
|
||||
const seconds = now.getSeconds().toString().padStart(2, '0')
|
||||
setTime(`${hours}:${minutes}:${seconds}`)
|
||||
}
|
||||
|
||||
updateTime()
|
||||
const interval = setInterval(updateTime, 1000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex-1 order-3 lg:order-3">
|
||||
<div className="text-end text-neutral-500 text-sm font-normal uppercase [text-shadow:_0px_0px_4px_rgb(255_255_255_/_0.25)]">
|
||||
{time}
|
||||
</div>
|
||||
|
||||
<div className="text-end text-neutral-500 text-xs font-normal uppercase [text-shadow:_0px_0px_4px_rgb(255_255_255_/_0.25)] text-nowrap">
|
||||
LOCAL TIME
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
87
apps/www/components/LaunchWeek/14/LwView.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import Image from 'next/image'
|
||||
import { ActionButton } from '~/components/LaunchWeek/14/ActionButton'
|
||||
import {
|
||||
TicketHeader,
|
||||
TicketHeaderClaim,
|
||||
TicketHeaderDate,
|
||||
TicketHeaderRemainingTime,
|
||||
} from '~/components/LaunchWeek/14/Header'
|
||||
import TicketCanvas from '~/components/LaunchWeek/14/TicketCanvas'
|
||||
import {
|
||||
TicketClaim,
|
||||
TicketClaimButtons,
|
||||
TicketClaimContent,
|
||||
TicketClaimMessage,
|
||||
} from '~/components/LaunchWeek/14/TicketClaim'
|
||||
import TicketCopy from '~/components/LaunchWeek/14/TicketCopy'
|
||||
import { TicketLayout, TicketLayoutCanvas } from '~/components/LaunchWeek/14/TicketLayout'
|
||||
import TicketShare from '~/components/LaunchWeek/14/TicketShare'
|
||||
import { Tunnel } from '~/components/LaunchWeek/14/Tunnel'
|
||||
import { TicketShareLayout } from '~/components/LaunchWeek/14/TicketShareLayout'
|
||||
import { useRegistration } from '~/components/LaunchWeek/14/hooks/use-registration'
|
||||
import useConfData from './hooks/use-conf-data'
|
||||
|
||||
const dates = [new Date('2025-03-31T07:00:00.000-08:00')]
|
||||
|
||||
export const LwView = () => {
|
||||
const [state] = useConfData()
|
||||
const register = useRegistration()
|
||||
|
||||
return (
|
||||
<TicketLayout>
|
||||
<TicketHeader hidden={true}>
|
||||
<TicketHeaderRemainingTime targetDate={dates[0]} />
|
||||
<TicketHeaderClaim />
|
||||
<TicketHeaderDate />
|
||||
</TicketHeader>
|
||||
<TicketLayoutCanvas narrow={true}>
|
||||
<TicketCanvas narrow={true} onUpgradeToSecret={register.upgradeTicket} />
|
||||
{state.claimFormState === 'visible' && (
|
||||
<TicketClaim>
|
||||
<div className="flex flex-col md:flex-row gap-12 lg:gap-2 grow w-full min-w-full max-w-full pt-16 md:pt-32 md:px-6 lg:px-0 lg:py-0 items-center">
|
||||
<div className='flex flex-col gap-2 w-full grow justify-center font-["Departure_Mono"]'>
|
||||
<h1 className="text-4xl uppercase tracking-wide pointer-events-none">
|
||||
<span className="flex gap-1 items-center">
|
||||
<Image
|
||||
src="/images/launchweek/14/logo-pixel-small-dark.png"
|
||||
width="18"
|
||||
height="20"
|
||||
className="w-auto h-5"
|
||||
alt=""
|
||||
/>
|
||||
Supabase
|
||||
</span>
|
||||
LaunchWeek 14
|
||||
<span className="block mt-6 text-foreground-lighter">MAR 31 — APR 04</span>
|
||||
</h1>
|
||||
<span className="block mt-6 text-foreground-lighter">4 AM / 7 AM PT</span>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 z-10 w-full grow justify-start lg:justify-end">
|
||||
<TicketClaimContent>
|
||||
<TicketClaimButtons>
|
||||
<ActionButton variant="primary" icon="T" onClick={() => register.signIn()}>
|
||||
CLAIM YOUR TICKET
|
||||
</ActionButton>
|
||||
</TicketClaimButtons>
|
||||
<TicketClaimMessage />
|
||||
</TicketClaimContent>
|
||||
</div>
|
||||
</div>
|
||||
</TicketClaim>
|
||||
)}
|
||||
{state.ticketVisibility && (
|
||||
<>
|
||||
<TicketShareLayout narrow>
|
||||
<TicketCopy />
|
||||
<TicketShare />
|
||||
</TicketShareLayout>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="w-full absolute bottom-[55%] md:bottom-[25%] lg:-bottom-8 left-0 right-0 -z-20">
|
||||
<Tunnel />
|
||||
</div>
|
||||
</TicketLayoutCanvas>
|
||||
</TicketLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { motion } from 'framer-motion'
|
||||
import Tilt from 'vanilla-tilt'
|
||||
import { useWindowSize } from 'react-use'
|
||||
import { cn } from 'ui'
|
||||
import { useBreakpoint } from 'common'
|
||||
|
||||
import { AdventDay } from '../data'
|
||||
import { AdventLink } from '../data/lw14_build_stage'
|
||||
|
||||
const BuildCard = ({ day, index }: { day: AdventDay; index: number }) => {
|
||||
const isTablet = useBreakpoint(1024)
|
||||
const tiltRef = useRef<HTMLDivElement>(null)
|
||||
const hiddenRef = useRef<HTMLDivElement>(null)
|
||||
const { width } = useWindowSize()
|
||||
const [hiddenHeight, setHiddenHeight] = useState(0)
|
||||
const transition = { type: 'spring', damping: 10, mass: 0.75, stiffness: 100, delay: index / 15 }
|
||||
const variants = {
|
||||
initial: {
|
||||
rotateY: -90,
|
||||
opacity: 0,
|
||||
},
|
||||
...(day.is_shipped && {
|
||||
reveal: {
|
||||
rotateY: 0,
|
||||
opacity: 1,
|
||||
transition,
|
||||
},
|
||||
}),
|
||||
}
|
||||
const isClientLibsCard = day.type === 'clientLibs'
|
||||
|
||||
useEffect(() => {
|
||||
if (tiltRef.current) {
|
||||
Tilt.init(tiltRef.current, {
|
||||
glare: false,
|
||||
max: isClientLibsCard ? 1 : 4,
|
||||
gyroscope: false,
|
||||
'full-page-listening': false,
|
||||
})
|
||||
}
|
||||
}, [tiltRef])
|
||||
|
||||
useEffect(() => {
|
||||
if (hiddenRef?.current) {
|
||||
const { height } = hiddenRef.current.getBoundingClientRect()
|
||||
setHiddenHeight(height)
|
||||
}
|
||||
}, [hiddenRef, width])
|
||||
|
||||
return (
|
||||
<div id={day.id} ref={tiltRef} className="absolute -inset-px group will-change">
|
||||
<motion.div
|
||||
className={cn(
|
||||
'opacity-0 flex flex-col justify-between w-full h-full p-6 rounded-xl bg-surface-75 transition-colors border border-strong hover:border-stronger overflow-hidden',
|
||||
isClientLibsCard && 'xl:flex-row'
|
||||
)}
|
||||
variants={variants}
|
||||
>
|
||||
<div className="relative w-full h-full flex flex-col flex-1">
|
||||
<div className="flex-1 opacity-30 group-hover:opacity-100 transition-opacity text-foreground-light">
|
||||
{day.icon}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'relative group-hover:!bottom-0 !ease-[.25,.25,0,1] duration-300 transition-all flex flex-col gap-1'
|
||||
)}
|
||||
style={{
|
||||
bottom: isTablet ? 0 : -hiddenHeight + 'px',
|
||||
}}
|
||||
>
|
||||
<h4 className="text-foreground text-lg leading-6">{day.title}</h4>
|
||||
<div
|
||||
ref={hiddenRef}
|
||||
className="relative z-10 !ease-[.25,.25,0,1] duration-300 transition-opacity opacity-100 lg:opacity-0 group-hover:opacity-100"
|
||||
style={{
|
||||
backfaceVisibility: 'hidden',
|
||||
transform: 'translateZ(0)',
|
||||
WebkitFontSmoothing: 'subpixel-antialiased',
|
||||
}}
|
||||
>
|
||||
<p className="text-foreground-lighter text-sm">{day.description}</p>
|
||||
<div className="flex gap-1 mt-3 flex-wrap">
|
||||
{day.links?.map((link: AdventLink) => (
|
||||
<Link
|
||||
key={link.url}
|
||||
href={link.url}
|
||||
target={link.target ?? '_self'}
|
||||
className="px-2 py-1 pointer-events-auto border transition-colors text-foreground-light bg-surface-100 hover:bg-surface-200 rounded text-xs"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isClientLibsCard && (
|
||||
<div className="flex xl:h-full order-first xl:order-last items-end justify-start xl:justify-end xl:w-2/3 gap-4 xl:gap-6 pb-4 mb-4 xl:pb-0 flex-wrap">
|
||||
{day.icons?.map((link: AdventLink) => (
|
||||
<Link
|
||||
key={link.url}
|
||||
href={link.url}
|
||||
title={link.label}
|
||||
target={link.target ?? '_self'}
|
||||
className="w-8 h-8 md:w-6 md:h-6 xl:w-14 xl:h-14 xl:p-1 inline-flex items-center justify-center pointer-events-auto transition-colors text-foreground-light hover:text-foreground"
|
||||
>
|
||||
{link.icon}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BuildCard
|
||||
@@ -0,0 +1,162 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import { ArrowRightIcon } from '@heroicons/react/outline'
|
||||
import { cn, Skeleton } from 'ui'
|
||||
import { Edit } from 'lucide-react'
|
||||
import { useBreakpoint } from 'common'
|
||||
|
||||
import { WeekDayProps } from '../data'
|
||||
import { DayLink } from '.'
|
||||
import CountdownComponent from 'ui-patterns/Banners/Countdown'
|
||||
|
||||
const DaySection = ({ day, className }: { day: WeekDayProps; className?: string }) => {
|
||||
const isMobile = useBreakpoint('sm')
|
||||
const cssGroup = 'group/d' + day.d
|
||||
|
||||
return (
|
||||
<section
|
||||
id={day.id}
|
||||
className={cn(
|
||||
'lw-nav-anchor border-b py-8 first:border-t border-muted dark:border-muted/50 text-foreground scroll-mt-16 grid grid-cols-1 gap-4 md:grid-cols-4 xl:grid-cols-3',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* Day title and links */}
|
||||
<div
|
||||
id={day.isToday ? 'today' : undefined}
|
||||
className="flex h-full scroll-mt-10 flex-col gap-4 items-between"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'text-sm inline uppercase font-mono dark:text-foreground-muted tracking-[0.1rem]',
|
||||
day.shipped && '!text-foreground'
|
||||
)}
|
||||
>
|
||||
{day.dd}, {day.date}
|
||||
</div>
|
||||
{!!day.links && (
|
||||
<ul className="flex-1 h-full w-full justify-end grid grid-cols-2 md:flex flex-col gap-1">
|
||||
{day.links?.map((link) => (
|
||||
<li key={link.href}>
|
||||
<DayLink {...link} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Day card */}
|
||||
<div className="flex md:col-span-3 xl:col-span-2">
|
||||
{day.shipped && day.steps.length > 0 ? (
|
||||
<Link
|
||||
href={day.blog!}
|
||||
className={cn(
|
||||
`
|
||||
bg-surface-75
|
||||
min-h-[210px] group sm:aspect-[3.67/1] relative overflow-hidden flex-1 flex flex-col justify-between
|
||||
hover:border-strong transition-colors border border-muted
|
||||
rounded-xl text-2xl bg-contain shadow-sm`,
|
||||
cssGroup
|
||||
)}
|
||||
>
|
||||
<div className="relative text-foreground-light p-4 sm:px-6 md:py-6 md:px-8 z-20 flex-grow flex flex-col items-start justify-between gap-2 w-full lg:w-1/2 text-left">
|
||||
<div className="relative w-full flex items-center gap-2 text-sm translate-x-0 !ease-[.24,0,.22,.99] duration-200 group-hover:-translate-x-6 transition-transform">
|
||||
<Edit className="w-4 min-w-4 group-hover:opacity-0 transition-opacity" />
|
||||
<span>Blog post</span>
|
||||
<ArrowRightIcon className="w-4 opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
<h2 className="text-lg leading-7 [&_strong]:font-normal [&_strong]:text-foreground">
|
||||
{day.description}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="relative z-10 border-b border-muted/40 sm:border-none w-full order-first aspect-[2/1] sm:aspect-auto sm:absolute sm:inset-0">
|
||||
<div className="hidden sm:block absolute inset-0 bg-gradient-to-r from-background-surface-75 from-0% via-background-surface-75 via-20% to-transparent to-75% w-full h-full z-0" />
|
||||
{day.steps[0]?.bg_layers &&
|
||||
day.steps[0]?.bg_layers?.map((layer, i) => (
|
||||
<>
|
||||
{!!layer.img && (
|
||||
<div
|
||||
key={`${day.title}-image-${i}?v=3`}
|
||||
className="absolute transition-opacity inset-0 w-full h-full -z-10 group-hover/d1:opacity-100"
|
||||
>
|
||||
<Image
|
||||
src={!!layer.mobileImg && isMobile ? layer.mobileImg : layer.img}
|
||||
className={`
|
||||
hidden dark:block absolute object-cover
|
||||
w-full h-full z-0 transition-all duration-300
|
||||
object-center sm:object-right
|
||||
`}
|
||||
fill
|
||||
sizes="100%"
|
||||
quality={100}
|
||||
alt={day.title}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!!layer.imgLight && (
|
||||
<div
|
||||
key={`${day.title}-image-${i}-light`}
|
||||
className="absolute sm:opacity-90 transition-opacity inset-0 w-full h-full -z-10 group-hover/d1:opacity-100"
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
!!layer.mobileImgLight && isMobile
|
||||
? layer.mobileImgLight
|
||||
: layer.imgLight
|
||||
}
|
||||
className={`
|
||||
dark:hidden absolute md:opacity-50 lg:opacity-100 object-cover
|
||||
w-full h-full z-0 transition-all duration-300
|
||||
object-center sm:object-right
|
||||
`}
|
||||
fill
|
||||
sizes="100%"
|
||||
quality={100}
|
||||
alt={day.title}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</Link>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
`min-h-[210px] group aspect-[3.67/1] relative overflow-hidden flex-1 flex flex-col justify-between
|
||||
bg-default border border-dashed border-strong dark:border-background-surface-300
|
||||
rounded-xl p-4 sm:p-6 md:p-8 text-2xl bg-contain`,
|
||||
cssGroup
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 h-5">
|
||||
<svg
|
||||
width="17"
|
||||
height="17"
|
||||
viewBox="0 0 17 17"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g opacity="0.5">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.32656 7.58047V5.98047C4.32656 3.77133 6.11742 1.98047 8.32656 1.98047C10.5357 1.98047 12.3266 3.77133 12.3266 5.98047V7.58047C13.2102 7.58047 13.9266 8.29681 13.9266 9.18047V13.1805C13.9266 14.0641 13.2102 14.7805 12.3266 14.7805H4.32656C3.44291 14.7805 2.72656 14.0641 2.72656 13.1805V9.18047C2.72656 8.29681 3.44291 7.58047 4.32656 7.58047ZM10.7266 5.98047V7.58047H5.92656V5.98047C5.92656 4.65499 7.00108 3.58047 8.32656 3.58047C9.65205 3.58047 10.7266 4.65499 10.7266 5.98047Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
{day.hasCountdown && <CountdownComponent date={day.published_at} showCard={false} />}
|
||||
</div>
|
||||
<div>
|
||||
<Skeleton className="w-full h-3 max-w-xs rounded-full will-change-contents" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default DaySection
|
||||
@@ -0,0 +1,84 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { IconDocumentation, IconMicSolid, IconProductHunt, IconYoutubeSolid, cn } from 'ui'
|
||||
import { Music } from 'lucide-react'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { StepLink } from '../data/lw14_data'
|
||||
import { ExpandableVideo } from 'ui-patterns/ExpandableVideo'
|
||||
|
||||
interface DayLink extends StepLink {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const DayLink = ({ type, icon, text, href = '', className }: DayLink) => {
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!isMounted) return null
|
||||
|
||||
const linkTypes = {
|
||||
blog: {
|
||||
icon: IconDocumentation,
|
||||
text: 'Blog Post',
|
||||
},
|
||||
docs: {
|
||||
icon: IconDocumentation,
|
||||
text: 'Docs',
|
||||
},
|
||||
productHunt: {
|
||||
icon: IconProductHunt,
|
||||
text: 'Product Hunt',
|
||||
},
|
||||
video: {
|
||||
icon: IconYoutubeSolid,
|
||||
text: 'Watch video',
|
||||
},
|
||||
podcast: {
|
||||
icon: Music,
|
||||
text: 'Podcast',
|
||||
},
|
||||
xSpace: {
|
||||
icon: IconMicSolid,
|
||||
text: 'X Space',
|
||||
},
|
||||
}
|
||||
const isTargetBlank = () => {
|
||||
switch (type) {
|
||||
case 'productHunt':
|
||||
case 'xSpace':
|
||||
case 'docs':
|
||||
return true
|
||||
}
|
||||
}
|
||||
const Icon = icon ?? linkTypes[type].icon
|
||||
const Text = () => <>{text ?? linkTypes[type]?.text}</>
|
||||
|
||||
const Component = type === 'video' ? 'div' : Link
|
||||
|
||||
const Trigger = ({ component: Comp, ...props }: any) => (
|
||||
<Comp
|
||||
className={cn(
|
||||
'py-1 flex gap-2 items-center text-foreground-lighter hover:text-foreground transition-colors text-sm',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="w-4 h-4 flex items-center justify-center">
|
||||
<Icon />
|
||||
</span>
|
||||
<Text />
|
||||
</Comp>
|
||||
)
|
||||
|
||||
if (type === 'video')
|
||||
return <ExpandableVideo videoId={href} trigger={<Trigger component={Component} />} />
|
||||
|
||||
return <Trigger href={href} target={isTargetBlank() ? '_blank' : '_self'} component={Component} />
|
||||
}
|
||||
|
||||
export default {
|
||||
DayLink,
|
||||
}
|
||||
4
apps/www/components/LaunchWeek/14/Releases/data/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as mainDays } from './lw14_data'
|
||||
export type { StepProps, StepLink, WeekDayProps } from './lw14_data'
|
||||
export { days as buildDays } from './lw14_build_stage'
|
||||
export type { AdventDay } from './lw14_build_stage'
|
||||
@@ -0,0 +1,58 @@
|
||||
// see apps/www/components/LaunchWeek/13/Releases/data/lw13_build_stage.tsx for reference
|
||||
|
||||
import { ReactNode } from 'react'
|
||||
import { type ClassValue } from 'clsx'
|
||||
|
||||
export interface AdventDay {
|
||||
icon?: ReactNode // use svg jsx with 34x34px viewport
|
||||
className?: ClassValue | string
|
||||
id: string
|
||||
title: string
|
||||
description?: string
|
||||
is_shipped: boolean
|
||||
links: AdventLink[]
|
||||
icons?: AdventLink[]
|
||||
type?: string
|
||||
}
|
||||
|
||||
export interface AdventLink {
|
||||
url: string
|
||||
label?: string
|
||||
icon?: any
|
||||
target?: '_blank'
|
||||
}
|
||||
|
||||
export const days: AdventDay[] = [
|
||||
{
|
||||
title: '',
|
||||
description: '',
|
||||
id: '',
|
||||
is_shipped: false,
|
||||
links: [],
|
||||
icon: null,
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
description: '',
|
||||
id: '',
|
||||
is_shipped: false,
|
||||
links: [],
|
||||
icon: null,
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
description: '',
|
||||
id: '',
|
||||
is_shipped: false,
|
||||
links: [],
|
||||
icon: null,
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
description: '',
|
||||
id: '',
|
||||
is_shipped: false,
|
||||
links: [],
|
||||
icon: null,
|
||||
},
|
||||
]
|
||||
182
apps/www/components/LaunchWeek/14/Releases/data/lw14_data.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
type StepLinkType = 'productHunt' | 'video' | 'docs' | 'xSpace' | 'blog' | 'podcast'
|
||||
|
||||
export interface StepLink {
|
||||
type: StepLinkType
|
||||
text?: string
|
||||
icon?: any
|
||||
href: string
|
||||
}
|
||||
|
||||
export interface StepProps {
|
||||
title: string
|
||||
icon?: string
|
||||
badge?: string
|
||||
blog?: string
|
||||
docs?: string
|
||||
description?: string
|
||||
github?: string
|
||||
hackernews?: string
|
||||
product_hunt?: string
|
||||
thumb?: string
|
||||
url?: string
|
||||
video?: string
|
||||
twitter_spaces?: string
|
||||
className?: string
|
||||
hideInBlog?: boolean
|
||||
bg_layers?: {
|
||||
img?: string
|
||||
mobileImg?: string
|
||||
imgLight?: string
|
||||
mobileImgLight?: string
|
||||
className?: string
|
||||
}[]
|
||||
steps?: StepProps[] | []
|
||||
links?: StepLink[]
|
||||
}
|
||||
|
||||
export interface WeekDayProps {
|
||||
id: string
|
||||
title: string
|
||||
date: string
|
||||
d: number
|
||||
dd: string
|
||||
published_at: string
|
||||
shipped: boolean // show card in layout
|
||||
isToday?: boolean // current active day
|
||||
hasCountdown?: boolean // use countdown only on "tomorrow"
|
||||
description: string | ReactNode
|
||||
links?: StepLink[] // types = 'productHunt' | 'video' | 'docs' | 'xSpace' | 'blog' | 'podcast'
|
||||
videoId?: string // youtube id
|
||||
videoThumbnail?: string
|
||||
blog: string
|
||||
steps: StepProps[] | []
|
||||
}
|
||||
|
||||
export const endOfLW13Hackathon = '2025-04-04T23:59:59.999-08:00'
|
||||
|
||||
const days: (isDark?: boolean) => WeekDayProps[] = (isDark = true) => [
|
||||
{
|
||||
id: 'day-1',
|
||||
d: 1,
|
||||
dd: 'Mon',
|
||||
shipped: false,
|
||||
isToday: false,
|
||||
hasCountdown: true,
|
||||
blog: '',
|
||||
date: '31 March',
|
||||
published_at: '2025-03-31T08:00:00.000-07:00',
|
||||
title: '',
|
||||
description: null,
|
||||
links: [
|
||||
// {
|
||||
// type: 'video',
|
||||
// href: '',
|
||||
// },
|
||||
// {
|
||||
// type: 'xSpace',
|
||||
// href: 'https://twitter.com/i/spaces/1yoJMyjzVWRJQ',
|
||||
// },
|
||||
],
|
||||
steps: [],
|
||||
},
|
||||
{
|
||||
id: 'day-2',
|
||||
d: 2,
|
||||
dd: 'Tue',
|
||||
shipped: false,
|
||||
isToday: false,
|
||||
hasCountdown: false,
|
||||
blog: '',
|
||||
date: '01 April',
|
||||
published_at: '2025-04-01T08:00:00.000-07:00',
|
||||
title: '',
|
||||
description: null,
|
||||
links: [
|
||||
// {
|
||||
// type: 'video',
|
||||
// href: '',
|
||||
// },
|
||||
// {
|
||||
// type: 'xSpace',
|
||||
// href: 'https://twitter.com/i/spaces/1yoJMyjzVWRJQ',
|
||||
// },
|
||||
],
|
||||
steps: [],
|
||||
},
|
||||
{
|
||||
id: 'day-3',
|
||||
d: 3,
|
||||
dd: 'Wed',
|
||||
shipped: false,
|
||||
isToday: false,
|
||||
hasCountdown: false,
|
||||
blog: '',
|
||||
date: '02 April',
|
||||
published_at: '2025-04-02T08:00:00.000-07:00',
|
||||
title: '',
|
||||
description: null,
|
||||
links: [
|
||||
// {
|
||||
// type: 'video',
|
||||
// href: '',
|
||||
// },
|
||||
// {
|
||||
// type: 'xSpace',
|
||||
// href: 'https://twitter.com/i/spaces/1yoJMyjzVWRJQ',
|
||||
// },
|
||||
],
|
||||
steps: [],
|
||||
},
|
||||
{
|
||||
id: 'day-4',
|
||||
d: 4,
|
||||
dd: 'Thu',
|
||||
shipped: false,
|
||||
isToday: false,
|
||||
hasCountdown: false,
|
||||
blog: '',
|
||||
date: '03 April',
|
||||
published_at: '2025-04-03T08:00:00.000-07:00',
|
||||
title: '',
|
||||
description: null,
|
||||
links: [
|
||||
// {
|
||||
// type: 'video',
|
||||
// href: '',
|
||||
// },
|
||||
// {
|
||||
// type: 'xSpace',
|
||||
// href: 'https://twitter.com/i/spaces/1yoJMyjzVWRJQ',
|
||||
// },
|
||||
],
|
||||
steps: [],
|
||||
},
|
||||
{
|
||||
id: 'day-5',
|
||||
d: 5,
|
||||
dd: 'Fri',
|
||||
shipped: false,
|
||||
isToday: false,
|
||||
hasCountdown: false,
|
||||
blog: '',
|
||||
date: '04 April',
|
||||
published_at: '2025-04-04T08:00:00.000-07:00',
|
||||
title: '',
|
||||
description: null,
|
||||
links: [
|
||||
// {
|
||||
// type: 'video',
|
||||
// href: '',
|
||||
// },
|
||||
// {
|
||||
// type: 'xSpace',
|
||||
// href: 'https://twitter.com/i/spaces/1yoJMyjzVWRJQ',
|
||||
// },
|
||||
],
|
||||
steps: [],
|
||||
},
|
||||
]
|
||||
|
||||
export default days
|
||||
123
apps/www/components/LaunchWeek/14/TicketCanvas.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { cn } from 'ui'
|
||||
import { useThreeJS } from './helpers'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import SceneRenderer from './utils/SceneRenderer'
|
||||
import TicketScene from './scenes/TicketScene'
|
||||
import useConfData from './hooks/use-conf-data'
|
||||
import HUDScene from './scenes/HUDScene'
|
||||
|
||||
interface TicketCanvasProps {
|
||||
className?: string
|
||||
onUpgradeToSecret?: () => void
|
||||
narrow: boolean
|
||||
}
|
||||
|
||||
const TicketCanvas = ({ className, onUpgradeToSecret, narrow }: TicketCanvasProps) => {
|
||||
const sceneRef = useRef<TicketScene | null>(null)
|
||||
const initQueue = useRef<{ init: Promise<void>; renderer: SceneRenderer }[]>([])
|
||||
const onUpgradeToSecretRef = useRef(onUpgradeToSecret)
|
||||
const [state, dispatch] = useConfData()
|
||||
const userData = state.userTicketData
|
||||
const initialSceneDataRef = useRef({
|
||||
visible: state.ticketVisibility,
|
||||
secret: state.userTicketData.secret,
|
||||
platinum: state.userTicketData.platinum,
|
||||
narrow: narrow,
|
||||
user: {
|
||||
id: state.userTicketData.id,
|
||||
name: state.userTicketData.name ?? state.userTicketData.username,
|
||||
ticketNumber: state.userTicketData.ticket_number,
|
||||
},
|
||||
})
|
||||
|
||||
const setup = useCallback(
|
||||
(container: HTMLElement) => {
|
||||
const uuid = Math.random().toString(36).substring(7)
|
||||
|
||||
const sceneRenderer = new SceneRenderer(container, initQueue.current, uuid)
|
||||
|
||||
const initPromise = sceneRenderer.init(async () => {
|
||||
dispatch({ type: 'TICKET_LOADING_START' })
|
||||
const scene = new TicketScene({
|
||||
defaultVisible: initialSceneDataRef.current.visible,
|
||||
defaultSecret: initialSceneDataRef.current.secret,
|
||||
defaultPlatinum: initialSceneDataRef.current.platinum,
|
||||
narrow: initialSceneDataRef.current.narrow,
|
||||
user: initialSceneDataRef.current.user,
|
||||
onSeatChartButtonClicked: () => {
|
||||
scene.showBackSide()
|
||||
scene.upgradeToSecret()
|
||||
onUpgradeToSecretRef.current?.()
|
||||
},
|
||||
onGoBackButtonClicked: () => {
|
||||
scene.showFrontSide()
|
||||
},
|
||||
})
|
||||
|
||||
await sceneRenderer.activateScene(scene, true)
|
||||
|
||||
sceneRef.current = scene
|
||||
|
||||
dispatch({ type: 'TICKET_LOADING_SUCCESS' })
|
||||
})
|
||||
|
||||
initQueue.current.push({ init: initPromise, renderer: sceneRenderer })
|
||||
|
||||
return sceneRenderer
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
async function updateTicket() {
|
||||
if (sceneRef.current) {
|
||||
sceneRef.current.setVisible(state.ticketVisibility)
|
||||
sceneRef.current.setTicketNumber(state.userTicketData.ticket_number ?? 0)
|
||||
sceneRef.current.setUserName(
|
||||
state.userTicketData.name ?? state.userTicketData.username ?? ''
|
||||
)
|
||||
|
||||
if (state.userTicketData.secret) await sceneRef.current.upgradeToSecret()
|
||||
if (state.userTicketData.platinum) await sceneRef.current.upgradeToPlatinum()
|
||||
|
||||
sceneRef.current.reloadTextures()
|
||||
}
|
||||
}
|
||||
|
||||
if (sceneRef.current) {
|
||||
void updateTicket()
|
||||
}
|
||||
}, [
|
||||
narrow,
|
||||
state.ticketVisibility,
|
||||
state.userTicketData.name,
|
||||
state.userTicketData.platinum,
|
||||
state.userTicketData.secret,
|
||||
state.userTicketData.ticket_number,
|
||||
state.userTicketData.username,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
onUpgradeToSecretRef.current = onUpgradeToSecret
|
||||
}, [onUpgradeToSecret])
|
||||
|
||||
const { containerRef } = useThreeJS(setup)
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-0 flex justify-center items-center overflow-hidden w-full h-full z-0',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="w-full h-full"
|
||||
onClick={(e) => {
|
||||
sceneRef.current?.click(e.nativeEvent)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TicketCanvas
|
||||
29
apps/www/components/LaunchWeek/14/TicketClaim.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { ReactNode } from 'react'
|
||||
import useConfData from './hooks/use-conf-data'
|
||||
|
||||
export const TicketClaim = ({ children, narrow }: { children: ReactNode; narrow?: boolean }) => {
|
||||
return <div className={'flex '}>{children}</div>
|
||||
}
|
||||
|
||||
export const TicketClaimContent = ({ children }: { children?: ReactNode }) => {
|
||||
return <div className="grid gap-6 content-center">{children}</div>
|
||||
}
|
||||
|
||||
export const TicketClaimMessage = () => {
|
||||
const [state] = useConfData()
|
||||
return (
|
||||
<div className="grid justify-center gap-3 md:px-16">
|
||||
<div className='md:text-center md:justify-center text-foreground-lighter md:text-sm font-["Departure_Mono"] leading-normal [text-shadow:_0px_0px_4px_rgb(255_255_255_/_0.25)] text-sm text-balance max-w-[310px] md:max-w-[400px] uppercase'>
|
||||
Share your ticket for a chance to win swag
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const TicketClaimButtons = ({ children }: { children?: ReactNode }) => {
|
||||
return (
|
||||
<div className="inline-flex md:justify-center md:items-center gap-2.5 flex-wrap">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
40
apps/www/components/LaunchWeek/14/TicketCopy.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { LW14_URL } from '~/lib/constants'
|
||||
import { Check, Copy } from 'lucide-react'
|
||||
import useConfData from './hooks/use-conf-data'
|
||||
import { cn } from 'ui'
|
||||
|
||||
export default function TicketCopy({ className }: { className?: string }) {
|
||||
const [state] = useConfData()
|
||||
const userData = state.userTicketData
|
||||
const { username } = userData
|
||||
const [copied, setCopied] = useState(false)
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const displayUrl = `.../launch-week/tickets/${username}`
|
||||
const link = `${LW14_URL}/tickets/${username}`
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
name="Copy"
|
||||
ref={buttonRef}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(link).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => {
|
||||
setCopied(false)
|
||||
}, 2000)
|
||||
})
|
||||
}}
|
||||
className={cn(
|
||||
'font-mono w-full px-2 lg:px-3.5 !pr-1 py-1 rounded-md bg-alternative-200 border flex gap-2 relative text-foreground-light hover:text-foreground text-xs pointer-events-auto justify-between items-center hover:border-stronger transition-all',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{displayUrl}</span>
|
||||
<div className="w-6 min-w-6 h-6 flex items-center justify-center flex-shrink-0 border border-strong rounded bg-muted hover:bg-selection hover:border-stronger">
|
||||
{copied ? <Check size={14} strokeWidth={3} /> : <Copy size={14} strokeWidth={1.5} />}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
23
apps/www/components/LaunchWeek/14/TicketLayout.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { cn } from 'ui'
|
||||
import SectionContainerWithCn from '~/components/Layouts/SectionContainerWithCn'
|
||||
|
||||
export const TicketLayout = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<SectionContainerWithCn height="narrow" className="font-mono z-20 relative">
|
||||
{children}
|
||||
</SectionContainerWithCn>
|
||||
)
|
||||
}
|
||||
|
||||
export const TicketLayoutCanvas = ({ children, narrow }: { children: ReactNode; narrow: true }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn('relative w-full h-[650px] lg:h-auto lg:aspect-[1.5841584158]', {
|
||||
['h-[530px] lg:aspect-[2.3] xl:aspect-[2.9473684211]']: narrow,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
111
apps/www/components/LaunchWeek/14/TicketShare.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { useBreakpoint } from 'common'
|
||||
import dayjs from 'dayjs'
|
||||
import { Check } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Button } from 'ui'
|
||||
import useConfData from './hooks/use-conf-data'
|
||||
import {
|
||||
LW14_TWEET_TEXT,
|
||||
LW14_TWEET_TEXT_PLATINUM,
|
||||
LW14_TWEET_TEXT_SECRET,
|
||||
LW14_URL,
|
||||
} from '~/lib/constants'
|
||||
import supabase from './supabase'
|
||||
|
||||
export default function TicketShare() {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const [state] = useConfData()
|
||||
const userData = state.userTicketData
|
||||
const { platinum, username, metadata, secret: hasSecretTicket } = userData
|
||||
const [_imgReady, setImgReady] = useState(false)
|
||||
const [_loading, setLoading] = useState(false)
|
||||
const isLessThanMd = useBreakpoint()
|
||||
const downloadLink = useRef<HTMLAnchorElement>()
|
||||
const link = `${LW14_URL}/tickets/${username}&t=${dayjs(new Date()).format('DHHmmss')}`
|
||||
const permalink = encodeURIComponent(link)
|
||||
const text = hasSecretTicket
|
||||
? LW14_TWEET_TEXT_SECRET
|
||||
: platinum
|
||||
? LW14_TWEET_TEXT_PLATINUM
|
||||
: LW14_TWEET_TEXT
|
||||
const encodedText = encodeURIComponent(text)
|
||||
const tweetUrl = `https://twitter.com/intent/tweet?url=${permalink}&text=${encodedText}`
|
||||
const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${permalink}&text=${encodedText}`
|
||||
const downloadUrl = `/api-v2/ticket-og?username=${encodeURIComponent(username ?? '')}`
|
||||
const TICKETS_TABLE = 'tickets'
|
||||
|
||||
useEffect(() => {
|
||||
setImgReady(false)
|
||||
|
||||
const img = new Image()
|
||||
|
||||
img.src = downloadUrl
|
||||
img.onload = () => {
|
||||
setImgReady(true)
|
||||
setLoading(false)
|
||||
if (downloadLink.current) {
|
||||
downloadLink.current.click()
|
||||
downloadLink.current = undefined
|
||||
}
|
||||
}
|
||||
}, [downloadUrl])
|
||||
|
||||
const handleShare = async (social: 'twitter' | 'linkedin') => {
|
||||
setTimeout(async () => {
|
||||
if (social === 'twitter') {
|
||||
await supabase
|
||||
.from(TICKETS_TABLE)
|
||||
.update({
|
||||
shared_on_twitter: 'now',
|
||||
metadata: { ...metadata, theme: resolvedTheme, hasSharedSecret: hasSecretTicket },
|
||||
})
|
||||
.eq('launch_week', 'lw14')
|
||||
.eq('username', username)
|
||||
} else if (social === 'linkedin') {
|
||||
await supabase
|
||||
.from(TICKETS_TABLE)
|
||||
.update({
|
||||
shared_on_linkedin: 'now',
|
||||
metadata: { ...metadata, theme: resolvedTheme, hasSharedSecret: hasSecretTicket },
|
||||
})
|
||||
.eq('launch_week', 'lw14')
|
||||
.eq('username', username)
|
||||
}
|
||||
|
||||
if (userData.shared_on_linkedin && userData.shared_on_twitter) {
|
||||
await fetch(`/api-v2/ticket-og?username=${username}&platinum=true`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-row flex-wrap justify-stretch w-full gap-2 pointer-events-auto font-["Departure_Mono"]'>
|
||||
<Button
|
||||
onClick={() => handleShare('twitter')}
|
||||
type={userData.shared_on_twitter ? 'secondary' : 'default'}
|
||||
icon={userData.shared_on_twitter && <Check strokeWidth={2} />}
|
||||
size={isLessThanMd ? 'tiny' : 'small'}
|
||||
className="px-2 lg:px-3.5 h-[28px] lg:h-[34px] uppercase flex-1 w-full"
|
||||
asChild
|
||||
>
|
||||
<Link href={tweetUrl} target="_blank">
|
||||
{userData.shared_on_twitter ? 'Shared on Twitter' : 'Share on Twitter'}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleShare('linkedin')}
|
||||
type={userData.shared_on_linkedin ? 'secondary' : 'default'}
|
||||
icon={userData.shared_on_linkedin && <Check strokeWidth={2} />}
|
||||
size={isLessThanMd ? 'tiny' : 'small'}
|
||||
className="px-2 lg:px-3.5 h-[28px] lg:h-[34px] uppercase flex-1 w-full"
|
||||
asChild
|
||||
>
|
||||
<Link href={linkedInUrl} target="_blank">
|
||||
{userData.shared_on_linkedin ? 'Shared on Linkedin' : 'Share on Linkedin'}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
51
apps/www/components/LaunchWeek/14/TicketShareLayout.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { cn } from 'ui'
|
||||
import useLw14ConfData from './hooks/use-conf-data'
|
||||
|
||||
export const TicketShareLayout = ({
|
||||
children,
|
||||
narrow,
|
||||
}: {
|
||||
children?: ReactNode
|
||||
narrow?: boolean
|
||||
}) => {
|
||||
const [{ userTicketData }] = useLw14ConfData()
|
||||
|
||||
const userName = userTicketData.name ?? userTicketData.username
|
||||
let userWelcomeText = userName ? <span>, {userName}!</span> : '!'
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute lg:relative lg:flex lg:flex-col lg:max-w-[450px] xl:max-w-[500px] lg:h-full left-0 right-0 grid justify-center gap-2 md:px-5 lg:px-0',
|
||||
narrow
|
||||
? 'top-[280px] md:top-[360px] lg:top-0 lg:left-0 lg:right-auto lg:justify-center'
|
||||
: 'top-[250px] xs:top-[290px] md:top-auto md:bottom-10 xl:bottom-16 2xl:bottom-20'
|
||||
)}
|
||||
>
|
||||
{narrow && (
|
||||
<>
|
||||
{/* <Image
|
||||
src={logo}
|
||||
alt="LW logo"
|
||||
className="size-12 hidden lg:block"
|
||||
width="48"
|
||||
height="48"
|
||||
/> */}
|
||||
<div className='uppercase text-3xl lg:text-4xl py-4 text-left font-["Departure_Mono"]'>
|
||||
Good to see you{userWelcomeText}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'text-xs pb-2 font-["Departure_Mono"] uppercase text-foreground-lighter',
|
||||
narrow ? 'text-left' : 'text-center'
|
||||
)}
|
||||
>
|
||||
Share your ticket for a chance to win swag
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
13
apps/www/components/LaunchWeek/14/Tunnel.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import Image from 'next/image'
|
||||
import React from 'react'
|
||||
import tunnelImage from './assets/tunnel-bg.png'
|
||||
|
||||
interface TunnelProps {
|
||||
width?: number
|
||||
height?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const Tunnel: React.FC<TunnelProps> = ({ className = '' }) => {
|
||||
return <Image src={tunnelImage} alt="" width="1120" height="380" className="w-full h-auto" />
|
||||
}
|
||||
BIN
apps/www/components/LaunchWeek/14/assets/logo.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
apps/www/components/LaunchWeek/14/assets/tunnel-bg.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
89
apps/www/components/LaunchWeek/14/effects/crt-shader.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
export const CRTShader = {
|
||||
uniforms: {
|
||||
tDiffuse: { value: null },
|
||||
time: { value: 0 },
|
||||
scanlineIntensity: { value: 0.6 },
|
||||
scanlineCount: { value: 360 },
|
||||
vignetteIntensity: { value: 0.5 },
|
||||
noiseIntensity: { value: 0.01 },
|
||||
flickerIntensity: { value: 0.01 },
|
||||
rgbShiftAmount: { value: 0.0 },
|
||||
// Add intensity control
|
||||
intensity: { value: 1.0 },
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
uniform sampler2D tDiffuse;
|
||||
uniform float time;
|
||||
uniform float scanlineIntensity;
|
||||
uniform float scanlineCount;
|
||||
uniform float vignetteIntensity;
|
||||
uniform float noiseIntensity;
|
||||
uniform float flickerIntensity;
|
||||
uniform float rgbShiftAmount;
|
||||
uniform float intensity;
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
// Random function
|
||||
// https://stackoverflow.com/a/10625698
|
||||
float random(vec2 st) {
|
||||
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Get original color
|
||||
vec4 originalColor = texture2D(tDiffuse, vUv);
|
||||
|
||||
// Skip effects if intensity is zero
|
||||
if (intensity <= 0.0) {
|
||||
gl_FragColor = originalColor;
|
||||
return;
|
||||
}
|
||||
|
||||
// Scale effect parameters by intensity
|
||||
float activeRGBShift = rgbShiftAmount * intensity;
|
||||
float activeScanlineIntensity = scanlineIntensity * intensity;
|
||||
float activeVignetteIntensity = vignetteIntensity * intensity;
|
||||
float activeNoiseIntensity = noiseIntensity * intensity;
|
||||
float activeFlickerIntensity = flickerIntensity * intensity;
|
||||
|
||||
// RGB shift effect - more subtle to preserve detail
|
||||
vec2 shiftR = vec2(activeRGBShift, 0.0);
|
||||
vec2 shiftG = vec2(0.0, activeRGBShift);
|
||||
|
||||
float r = texture2D(tDiffuse, vUv + shiftR).r;
|
||||
float g = texture2D(tDiffuse, vUv + shiftG).g;
|
||||
float b = texture2D(tDiffuse, vUv).b;
|
||||
|
||||
vec4 shiftedColor = vec4(r, g, b, originalColor.a);
|
||||
|
||||
// Scanline effect - thicker and more pronounced
|
||||
float scanlineFreq = scanlineCount * 0.7; // Reduce frequency for thicker lines
|
||||
float scanline = sin(vUv.y * scanlineFreq * 3.14159) * 0.5 + 0.5;
|
||||
scanline = pow(scanline, 0.7) * activeScanlineIntensity * 1.5; // Increase intensity and make lines thicker with lower power
|
||||
|
||||
// Apply scanline as a multiplicative overlay with stronger effect
|
||||
vec4 scanlineColor = mix(shiftedColor, shiftedColor * (1.0 - scanline), activeScanlineIntensity * 1.2);
|
||||
|
||||
// Vignette effect
|
||||
vec2 center = vec2(0.5, 0.5);
|
||||
float dist = distance(vUv, center);
|
||||
float vignette = smoothstep(0.3, 0.85, dist) * activeVignetteIntensity;
|
||||
|
||||
// Apply vignette as the final overlay effect
|
||||
vec4 finalColor = scanlineColor * (1.0 - vignette * 0.7);
|
||||
|
||||
// Blend between original and effect based on intensity
|
||||
vec4 final = mix(originalColor, finalColor, intensity);
|
||||
final.a = originalColor.a;
|
||||
gl_FragColor = final;
|
||||
}
|
||||
`,
|
||||
}
|
||||
112
apps/www/components/LaunchWeek/14/effects/glitch.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
DataTexture,
|
||||
FloatType,
|
||||
MathUtils,
|
||||
RedFormat,
|
||||
ShaderMaterial,
|
||||
UniformsUtils,
|
||||
WebGLRenderer,
|
||||
WebGLRenderTarget,
|
||||
} from 'three'
|
||||
import { Pass, FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass'
|
||||
import { DigitalGlitch } from 'three/examples/jsm/shaders/DigitalGlitch'
|
||||
|
||||
class GlitchPass extends Pass {
|
||||
uniforms: typeof DigitalGlitch.uniforms
|
||||
heightMap: DataTexture
|
||||
material: ShaderMaterial
|
||||
fsQuad: FullScreenQuad
|
||||
goWild: boolean
|
||||
curF: number
|
||||
randX: number
|
||||
intensity: number
|
||||
|
||||
constructor(dt_size = 64) {
|
||||
super()
|
||||
|
||||
const shader = DigitalGlitch
|
||||
|
||||
this.uniforms = UniformsUtils.clone(shader.uniforms)
|
||||
|
||||
this.heightMap = this.generateHeightmap(dt_size)
|
||||
|
||||
this.uniforms['tDisp'].value = this.heightMap
|
||||
|
||||
this.material = new ShaderMaterial({
|
||||
uniforms: this.uniforms,
|
||||
vertexShader: shader.vertexShader,
|
||||
fragmentShader: shader.fragmentShader,
|
||||
})
|
||||
|
||||
this.fsQuad = new FullScreenQuad(this.material)
|
||||
|
||||
this.goWild = false
|
||||
this.curF = 0
|
||||
this.randX = 0
|
||||
this.generateTrigger()
|
||||
this.intensity = 0
|
||||
}
|
||||
|
||||
render(renderer: WebGLRenderer, writeBuffer: WebGLRenderTarget, readBuffer: WebGLRenderTarget) {
|
||||
this.uniforms['tDiffuse'].value = readBuffer.texture
|
||||
this.uniforms['seed'].value = Math.random() //default seeding
|
||||
this.uniforms['byp'].value = 0
|
||||
|
||||
if (this.curF % this.randX < this.randX / 10) {
|
||||
this.uniforms['amount'].value = 0 * this.intensity
|
||||
this.uniforms['angle'].value = Math.PI
|
||||
this.uniforms['distortion_x'].value = MathUtils.randFloat(0, 0.05) * this.intensity
|
||||
this.uniforms['distortion_y'].value = MathUtils.randFloat(0, 0.05) * this.intensity
|
||||
this.uniforms['seed_x'].value = MathUtils.randFloat(-0.05, 0.05) * this.intensity
|
||||
this.uniforms['seed_y'].value = MathUtils.randFloat(-0.05, 0.05) * this.intensity
|
||||
this.curF = 0
|
||||
}
|
||||
|
||||
this.curF++
|
||||
|
||||
if (this.renderToScreen) {
|
||||
renderer.setRenderTarget(null)
|
||||
this.fsQuad.render(renderer)
|
||||
} else {
|
||||
renderer.setRenderTarget(writeBuffer)
|
||||
if (this.clear) renderer.clear()
|
||||
this.fsQuad.render(renderer)
|
||||
}
|
||||
}
|
||||
|
||||
generateTrigger() {
|
||||
this.randX = MathUtils.randInt(200, 240)
|
||||
}
|
||||
|
||||
generateHeightmap(dt_size: number) {
|
||||
const data_arr = new Float32Array(dt_size * dt_size)
|
||||
const length = dt_size * dt_size
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const val = MathUtils.randFloat(0, 1)
|
||||
data_arr[i] = val
|
||||
}
|
||||
|
||||
const texture = new DataTexture(data_arr, dt_size, dt_size, RedFormat, FloatType)
|
||||
texture.needsUpdate = true
|
||||
return texture
|
||||
}
|
||||
|
||||
setIntensity(amount: number) {
|
||||
this.intensity = amount < 0.01 ? 0 : amount / 4
|
||||
}
|
||||
|
||||
enable(value: boolean) {
|
||||
this.uniforms['byp'].value = value ? 0 : 1
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.material.dispose()
|
||||
|
||||
this.heightMap.dispose()
|
||||
|
||||
this.fsQuad.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
export { GlitchPass }
|
||||
238
apps/www/components/LaunchWeek/14/effects/helpers.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
export const shared = `
|
||||
vec3 mod289(vec3 x) {
|
||||
return x - floor(x * (1.0 / 289.0)) * 289.0;
|
||||
}
|
||||
|
||||
vec4 mod289(vec4 x) {
|
||||
return x - floor(x * (1.0 / 289.0)) * 289.0;
|
||||
}
|
||||
|
||||
vec4 permute(vec4 x)
|
||||
{
|
||||
return mod289(((x*34.0)+1.0)*x);
|
||||
}
|
||||
|
||||
vec4 taylorInvSqrt(vec4 r)
|
||||
{
|
||||
return 1.79284291400159 - 0.85373472095314 * r;
|
||||
}
|
||||
|
||||
`
|
||||
|
||||
export const simplexNoise3d = `
|
||||
//
|
||||
// Description : Array and textureless GLSL 2D/3D/4D simplex
|
||||
// noise functions.
|
||||
// Author : Ian McEwan, Ashima Arts.
|
||||
// Maintainer : ijm
|
||||
// Lastmod : 20110822 (ijm)
|
||||
// License : Copyright (C) 2011 Ashima Arts. All rights reserved.
|
||||
// Distributed under the MIT License. See LICENSE file.
|
||||
// https://github.com/ashima/webgl-noise
|
||||
//
|
||||
|
||||
float snoise3D(vec3 v)
|
||||
{
|
||||
const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
|
||||
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
|
||||
|
||||
// First corner
|
||||
vec3 i = floor(v + dot(v, C.yyy) );
|
||||
vec3 x0 = v - i + dot(i, C.xxx) ;
|
||||
|
||||
// Other corners
|
||||
vec3 g = step(x0.yzx, x0.xyz);
|
||||
vec3 l = 1.0 - g;
|
||||
vec3 i1 = min( g.xyz, l.zxy );
|
||||
vec3 i2 = max( g.xyz, l.zxy );
|
||||
|
||||
// x0 = x0 - 0.0 + 0.0 * C.xxx;
|
||||
// x1 = x0 - i1 + 1.0 * C.xxx;
|
||||
// x2 = x0 - i2 + 2.0 * C.xxx;
|
||||
// x3 = x0 - 1.0 + 3.0 * C.xxx;
|
||||
vec3 x1 = x0 - i1 + C.xxx;
|
||||
vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
|
||||
vec3 x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y
|
||||
|
||||
// Permutations
|
||||
i = mod289(i);
|
||||
vec4 p = permute( permute( permute(
|
||||
i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
|
||||
+ i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
|
||||
+ i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
|
||||
|
||||
// Gradients: 7x7 points over a square, mapped onto an octahedron.
|
||||
// The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
|
||||
float n_ = 0.142857142857; // 1.0/7.0
|
||||
vec3 ns = n_ * D.wyz - D.xzx;
|
||||
|
||||
vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7)
|
||||
|
||||
vec4 x_ = floor(j * ns.z);
|
||||
vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N)
|
||||
|
||||
vec4 x = x_ *ns.x + ns.yyyy;
|
||||
vec4 y = y_ *ns.x + ns.yyyy;
|
||||
vec4 h = 1.0 - abs(x) - abs(y);
|
||||
|
||||
vec4 b0 = vec4( x.xy, y.xy );
|
||||
vec4 b1 = vec4( x.zw, y.zw );
|
||||
|
||||
//vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
|
||||
//vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
|
||||
vec4 s0 = floor(b0)*2.0 + 1.0;
|
||||
vec4 s1 = floor(b1)*2.0 + 1.0;
|
||||
vec4 sh = -step(h, vec4(0.0));
|
||||
|
||||
vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
|
||||
vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;
|
||||
|
||||
vec3 p0 = vec3(a0.xy,h.x);
|
||||
vec3 p1 = vec3(a0.zw,h.y);
|
||||
vec3 p2 = vec3(a1.xy,h.z);
|
||||
vec3 p3 = vec3(a1.zw,h.w);
|
||||
|
||||
//Normalise gradients
|
||||
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
|
||||
p0 *= norm.x;
|
||||
p1 *= norm.y;
|
||||
p2 *= norm.z;
|
||||
p3 *= norm.w;
|
||||
|
||||
// Mix final noise value
|
||||
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
|
||||
m = m * m;
|
||||
return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
|
||||
dot(p2,x2), dot(p3,x3) ) );
|
||||
}
|
||||
|
||||
#pragma glslify: export(snoise)
|
||||
`
|
||||
export const periodicNoise3d = `
|
||||
//
|
||||
// GLSL textureless classic 3D noise "cnoise",
|
||||
// with an RSL-style periodic variant "pnoise".
|
||||
// Author: Stefan Gustavson (stefan.gustavson@liu.se)
|
||||
// Version: 2011-10-11
|
||||
//
|
||||
// Many thanks to Ian McEwan of Ashima Arts for the
|
||||
// ideas for permutation and gradient selection.
|
||||
//
|
||||
// Copyright (c) 2011 Stefan Gustavson. All rights reserved.
|
||||
// Distributed under the MIT license. See LICENSE file.
|
||||
// https://github.com/ashima/webgl-noise
|
||||
//
|
||||
|
||||
|
||||
vec3 fade(vec3 t) {
|
||||
return t*t*t*(t*(t*6.0-15.0)+10.0);
|
||||
}
|
||||
|
||||
// Classic Perlin noise, periodic variant
|
||||
float pnoise3D(vec3 P, vec3 rep)
|
||||
{
|
||||
vec3 Pi0 = mod(floor(P), rep); // Integer part, modulo period
|
||||
vec3 Pi1 = mod(Pi0 + vec3(1.0), rep); // Integer part + 1, mod period
|
||||
Pi0 = mod289(Pi0);
|
||||
Pi1 = mod289(Pi1);
|
||||
vec3 Pf0 = fract(P); // Fractional part for interpolation
|
||||
vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0
|
||||
vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
|
||||
vec4 iy = vec4(Pi0.yy, Pi1.yy);
|
||||
vec4 iz0 = Pi0.zzzz;
|
||||
vec4 iz1 = Pi1.zzzz;
|
||||
|
||||
vec4 ixy = permute(permute(ix) + iy);
|
||||
vec4 ixy0 = permute(ixy + iz0);
|
||||
vec4 ixy1 = permute(ixy + iz1);
|
||||
|
||||
vec4 gx0 = ixy0 * (1.0 / 7.0);
|
||||
vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5;
|
||||
gx0 = fract(gx0);
|
||||
vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);
|
||||
vec4 sz0 = step(gz0, vec4(0.0));
|
||||
gx0 -= sz0 * (step(0.0, gx0) - 0.5);
|
||||
gy0 -= sz0 * (step(0.0, gy0) - 0.5);
|
||||
|
||||
vec4 gx1 = ixy1 * (1.0 / 7.0);
|
||||
vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5;
|
||||
gx1 = fract(gx1);
|
||||
vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);
|
||||
vec4 sz1 = step(gz1, vec4(0.0));
|
||||
gx1 -= sz1 * (step(0.0, gx1) - 0.5);
|
||||
gy1 -= sz1 * (step(0.0, gy1) - 0.5);
|
||||
|
||||
vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);
|
||||
vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);
|
||||
vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);
|
||||
vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);
|
||||
vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);
|
||||
vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);
|
||||
vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);
|
||||
vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);
|
||||
|
||||
vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110)));
|
||||
g000 *= norm0.x;
|
||||
g010 *= norm0.y;
|
||||
g100 *= norm0.z;
|
||||
g110 *= norm0.w;
|
||||
vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111)));
|
||||
g001 *= norm1.x;
|
||||
g011 *= norm1.y;
|
||||
g101 *= norm1.z;
|
||||
g111 *= norm1.w;
|
||||
|
||||
float n000 = dot(g000, Pf0);
|
||||
float n100 = dot(g100, vec3(Pf1.x, Pf0.yz));
|
||||
float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z));
|
||||
float n110 = dot(g110, vec3(Pf1.xy, Pf0.z));
|
||||
float n001 = dot(g001, vec3(Pf0.xy, Pf1.z));
|
||||
float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z));
|
||||
float n011 = dot(g011, vec3(Pf0.x, Pf1.yz));
|
||||
float n111 = dot(g111, Pf1);
|
||||
|
||||
vec3 fade_xyz = fade(Pf0);
|
||||
vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z);
|
||||
vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y);
|
||||
float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
|
||||
return 2.2 * n_xyz;
|
||||
}
|
||||
|
||||
#pragma glslify: export(pnoise)
|
||||
`
|
||||
|
||||
export const blendSoftLight = `
|
||||
// The MIT License (MIT) Copyright (c) 2015 Matt DesLauriers
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
vec3 blendSoftLight(vec3 base, vec3 blend) {
|
||||
return mix(
|
||||
sqrt(base) * (2.0 * blend - 1.0) + 2.0 * base * (1.0 - blend),
|
||||
2.0 * base * blend + base * base * (1.0 - 2.0 * blend),
|
||||
step(base, vec3(0.5))
|
||||
);
|
||||
}
|
||||
`
|
||||
|
||||
export const luma = `
|
||||
// This software is released under the MIT license:
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
float luma(vec3 color) {
|
||||
return dot(color, vec3(0.299, 0.587, 0.114));
|
||||
}
|
||||
|
||||
float luma(vec4 color) {
|
||||
return dot(color.rgb, vec3(0.299, 0.587, 0.114));
|
||||
}
|
||||
`
|
||||
132
apps/www/components/LaunchWeek/14/effects/hud-shader.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { blendSoftLight, luma, periodicNoise3d, shared, simplexNoise3d } from './helpers'
|
||||
|
||||
export const HUDShader = {
|
||||
vertexShader: `
|
||||
precision mediump float;
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
uniform sampler2D tDiffuse;
|
||||
uniform vec2 resolution;
|
||||
uniform float vignetteRadius;
|
||||
uniform float vignetteSmoothness;
|
||||
uniform float time;
|
||||
|
||||
varying vec2 vUv;
|
||||
varying vec2 screenPosition;
|
||||
|
||||
${shared}
|
||||
${periodicNoise3d}
|
||||
${simplexNoise3d}
|
||||
${blendSoftLight}
|
||||
${luma}
|
||||
|
||||
// grain function
|
||||
// The MIT License (MIT) Copyright (c) 2015 Matt DesLauriers
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
float grain(vec2 texCoord, vec2 resolution, float frame, float multiplier) {
|
||||
vec2 mult = texCoord * resolution;
|
||||
float offset = snoise3D(vec3(mult / multiplier, frame));
|
||||
float n1 = pnoise3D(vec3(mult, offset), vec3(1.0/texCoord * resolution, 1.0));
|
||||
return n1 / 2.0 + 0.5;
|
||||
}
|
||||
|
||||
float grain(vec2 texCoord, vec2 resolution, float frame) {
|
||||
return grain(texCoord, resolution, frame, 2.5);
|
||||
}
|
||||
|
||||
float grain(vec2 texCoord, vec2 resolution) {
|
||||
return grain(texCoord, resolution, 0.0);
|
||||
}
|
||||
|
||||
// Vignette function
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2017 Tyler Lindberg
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
float vignette(vec2 uv, float radius, float smoothness) {
|
||||
float diff = radius - distance(uv, vec2(0.5, 0.5));
|
||||
return smoothstep(-smoothness, smoothness, diff);
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Sample the diffuse texture to get base color with alpha
|
||||
|
||||
vec4 blackBase = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
|
||||
float v = vignette(vUv, vignetteRadius, vignetteSmoothness);
|
||||
float invertedVignette = 1.0 - v;
|
||||
|
||||
float minAlpha = 0.3;
|
||||
blackBase.a *= mix(minAlpha, 1.0, invertedVignette);
|
||||
|
||||
vec4 textureColor = texture2D(tDiffuse, vUv);
|
||||
|
||||
vec4 stackedColor = vec4(
|
||||
mix(blackBase.rgb, textureColor.rgb, textureColor.a),
|
||||
mix(blackBase.a, max(blackBase.a, textureColor.a), 0.8)
|
||||
);
|
||||
|
||||
float grainSize = 3.0 + 4.0 * sin(time);
|
||||
vec3 g = vec3(grain(vUv, resolution / grainSize, time));
|
||||
|
||||
vec3 noiseColor = blendSoftLight(vec3(0.2,0.2,0.2), g);
|
||||
|
||||
float grainStrength = 0.02;
|
||||
stackedColor = vec4(
|
||||
mix(stackedColor.rgb, noiseColor, grainStrength),
|
||||
stackedColor.a
|
||||
);
|
||||
|
||||
gl_FragColor = stackedColor;
|
||||
}
|
||||
`,
|
||||
uniforms: {
|
||||
tDiffuse: { value: null as any },
|
||||
time: { value: 0.0 },
|
||||
resolution: { value: [70.0, 100.0] }, // Adjusted for Lygia noise scale
|
||||
vignetteSmoothness: { value: 0 },
|
||||
vignetteRadius: { value: 1 },
|
||||
},
|
||||
}
|
||||
437
apps/www/components/LaunchWeek/14/effects/transparent-bloom.ts
Normal file
@@ -0,0 +1,437 @@
|
||||
/**
|
||||
* ISC License
|
||||
*
|
||||
* Copyright 2021 mbalex99
|
||||
*
|
||||
* Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted,
|
||||
* provided that the above copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE
|
||||
* INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
|
||||
* FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
* ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
import {
|
||||
AdditiveBlending,
|
||||
Color,
|
||||
LinearFilter,
|
||||
MeshBasicMaterial,
|
||||
RGBAFormat,
|
||||
ShaderMaterial,
|
||||
Texture,
|
||||
UniformsUtils,
|
||||
Vector2,
|
||||
Vector3,
|
||||
WebGLRenderer,
|
||||
WebGLRenderTarget,
|
||||
} from 'three'
|
||||
|
||||
import { Pass } from 'three/examples/jsm/postprocessing/Pass'
|
||||
|
||||
// typescript definitions doesn't have FullScreenQuad
|
||||
//@ts-ignore
|
||||
import { FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass'
|
||||
|
||||
import { CopyShader } from 'three/examples/jsm/shaders/CopyShader.js'
|
||||
import { LuminosityHighPassShader } from 'three/examples/jsm/shaders/LuminosityHighPassShader.js'
|
||||
|
||||
/**
|
||||
* Thanks to https://github.com/mrdoob/three.js/issues/14104#issuecomment-429664412 for this fragmentShaderfix
|
||||
*
|
||||
* UnrealBloomPass is inspired by the bloom pass of Unreal Engine. It creates a
|
||||
* mip map chain of bloom textures and blurs them with different radii. Because
|
||||
* of the weighted combination of mips, and because larger blurs are done on
|
||||
* higher mips, this effect provides good quality and performance.
|
||||
*
|
||||
* Reference:
|
||||
* - https://docs.unrealengine.com/latest/INT/Engine/Rendering/PostProcessEffects/Bloom/
|
||||
*/
|
||||
class TransparentBloomPass extends Pass {
|
||||
strength: number
|
||||
radius: number
|
||||
threshold: number
|
||||
resolution: Vector2
|
||||
clearColor: Color
|
||||
renderTargetsHorizontal: any[]
|
||||
renderTargetsVertical: any[]
|
||||
nMips: number
|
||||
renderTargetBright: WebGLRenderTarget
|
||||
highPassUniforms: any
|
||||
materialHighPassFilter: ShaderMaterial
|
||||
separableBlurMaterials: any[]
|
||||
compositeMaterial: ShaderMaterial
|
||||
bloomTintColors: Vector3[]
|
||||
copyUniforms: any
|
||||
materialCopy: ShaderMaterial
|
||||
_oldClearColor: Color
|
||||
oldClearAlpha: number
|
||||
basic: MeshBasicMaterial
|
||||
fsQuad: FullScreenQuad
|
||||
static BlurDirectionX: any
|
||||
static BlurDirectionY: any
|
||||
constructor(resolution: Vector2, strength: number, radius: number, threshold: number) {
|
||||
super()
|
||||
|
||||
this.strength = strength !== undefined ? strength : 1
|
||||
this.radius = radius
|
||||
this.threshold = threshold
|
||||
this.resolution =
|
||||
resolution !== undefined ? new Vector2(resolution.x, resolution.y) : new Vector2(256, 256)
|
||||
|
||||
// create color only once here, reuse it later inside the render function
|
||||
this.clearColor = new Color(0, 0, 0)
|
||||
|
||||
// render targets
|
||||
const pars = {
|
||||
minFilter: LinearFilter,
|
||||
magFilter: LinearFilter,
|
||||
format: RGBAFormat,
|
||||
}
|
||||
this.renderTargetsHorizontal = []
|
||||
this.renderTargetsVertical = []
|
||||
this.nMips = 5
|
||||
let resx = Math.round(this.resolution.x / 2)
|
||||
let resy = Math.round(this.resolution.y / 2)
|
||||
|
||||
this.renderTargetBright = new WebGLRenderTarget(resx, resy, pars)
|
||||
this.renderTargetBright.texture.name = 'UnrealBloomPass.bright'
|
||||
this.renderTargetBright.texture.generateMipmaps = false
|
||||
|
||||
for (let i = 0; i < this.nMips; i++) {
|
||||
const renderTargetHorizonal = new WebGLRenderTarget(resx, resy, pars)
|
||||
|
||||
renderTargetHorizonal.texture.name = 'UnrealBloomPass.h' + i
|
||||
renderTargetHorizonal.texture.generateMipmaps = false
|
||||
|
||||
this.renderTargetsHorizontal.push(renderTargetHorizonal)
|
||||
|
||||
const renderTargetVertical = new WebGLRenderTarget(resx, resy, pars)
|
||||
|
||||
renderTargetVertical.texture.name = 'UnrealBloomPass.v' + i
|
||||
renderTargetVertical.texture.generateMipmaps = false
|
||||
|
||||
this.renderTargetsVertical.push(renderTargetVertical)
|
||||
|
||||
resx = Math.round(resx / 2)
|
||||
|
||||
resy = Math.round(resy / 2)
|
||||
}
|
||||
|
||||
// luminosity high pass material
|
||||
|
||||
if (LuminosityHighPassShader === undefined)
|
||||
console.error('THREE.UnrealBloomPass relies on LuminosityHighPassShader')
|
||||
|
||||
const highPassShader = LuminosityHighPassShader
|
||||
this.highPassUniforms = UniformsUtils.clone(highPassShader.uniforms)
|
||||
|
||||
this.highPassUniforms['luminosityThreshold'].value = threshold
|
||||
this.highPassUniforms['smoothWidth'].value = 0.01
|
||||
|
||||
this.materialHighPassFilter = new ShaderMaterial({
|
||||
uniforms: this.highPassUniforms,
|
||||
vertexShader: highPassShader.vertexShader,
|
||||
fragmentShader: highPassShader.fragmentShader,
|
||||
defines: {},
|
||||
})
|
||||
|
||||
// Gaussian Blur Materials
|
||||
this.separableBlurMaterials = []
|
||||
const kernelSizeArray = [3, 5, 7, 9, 11]
|
||||
resx = Math.round(this.resolution.x / 2)
|
||||
resy = Math.round(this.resolution.y / 2)
|
||||
|
||||
for (let i = 0; i < this.nMips; i++) {
|
||||
this.separableBlurMaterials.push(this.getSeperableBlurMaterial(kernelSizeArray[i]))
|
||||
|
||||
this.separableBlurMaterials[i].uniforms['texSize'].value = new Vector2(resx, resy)
|
||||
|
||||
resx = Math.round(resx / 2)
|
||||
|
||||
resy = Math.round(resy / 2)
|
||||
}
|
||||
|
||||
// Composite material
|
||||
this.compositeMaterial = this.getCompositeMaterial(this.nMips)
|
||||
this.compositeMaterial.uniforms['blurTexture1'].value = this.renderTargetsVertical[0].texture
|
||||
this.compositeMaterial.uniforms['blurTexture2'].value = this.renderTargetsVertical[1].texture
|
||||
this.compositeMaterial.uniforms['blurTexture3'].value = this.renderTargetsVertical[2].texture
|
||||
this.compositeMaterial.uniforms['blurTexture4'].value = this.renderTargetsVertical[3].texture
|
||||
this.compositeMaterial.uniforms['blurTexture5'].value = this.renderTargetsVertical[4].texture
|
||||
this.compositeMaterial.uniforms['bloomStrength'].value = strength
|
||||
this.compositeMaterial.uniforms['bloomRadius'].value = 0.1
|
||||
this.compositeMaterial.needsUpdate = true
|
||||
|
||||
const bloomFactors = [1.0, 0.8, 0.6, 0.4, 0.2]
|
||||
this.compositeMaterial.uniforms['bloomFactors'].value = bloomFactors
|
||||
this.bloomTintColors = [
|
||||
new Vector3(1, 1, 1),
|
||||
new Vector3(1, 1, 1),
|
||||
new Vector3(1, 1, 1),
|
||||
new Vector3(1, 1, 1),
|
||||
new Vector3(1, 1, 1),
|
||||
]
|
||||
this.compositeMaterial.uniforms['bloomTintColors'].value = this.bloomTintColors
|
||||
|
||||
// copy material
|
||||
if (CopyShader === undefined) {
|
||||
console.error('THREE.UnrealBloomPass relies on CopyShader')
|
||||
}
|
||||
|
||||
const copyShader = CopyShader
|
||||
|
||||
this.copyUniforms = UniformsUtils.clone(copyShader.uniforms)
|
||||
this.copyUniforms['opacity'].value = 1.0
|
||||
|
||||
this.materialCopy = new ShaderMaterial({
|
||||
uniforms: this.copyUniforms,
|
||||
vertexShader: copyShader.vertexShader,
|
||||
fragmentShader: copyShader.fragmentShader,
|
||||
blending: AdditiveBlending,
|
||||
depthTest: false,
|
||||
depthWrite: false,
|
||||
transparent: true,
|
||||
})
|
||||
|
||||
this.enabled = true
|
||||
this.needsSwap = false
|
||||
|
||||
this._oldClearColor = new Color()
|
||||
this.oldClearAlpha = 1
|
||||
|
||||
this.basic = new MeshBasicMaterial()
|
||||
|
||||
this.fsQuad = new FullScreenQuad(undefined)
|
||||
}
|
||||
|
||||
dispose() {
|
||||
for (let i = 0; i < this.renderTargetsHorizontal.length; i++) {
|
||||
this.renderTargetsHorizontal[i].dispose()
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.renderTargetsVertical.length; i++) {
|
||||
this.renderTargetsVertical[i].dispose()
|
||||
}
|
||||
|
||||
this.renderTargetBright.dispose()
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
let resx = Math.round(width / 2)
|
||||
let resy = Math.round(height / 2)
|
||||
|
||||
this.renderTargetBright.setSize(resx, resy)
|
||||
|
||||
for (let i = 0; i < this.nMips; i++) {
|
||||
this.renderTargetsHorizontal[i].setSize(resx, resy)
|
||||
this.renderTargetsVertical[i].setSize(resx, resy)
|
||||
|
||||
this.separableBlurMaterials[i].uniforms['texSize'].value = new Vector2(resx, resy)
|
||||
|
||||
resx = Math.round(resx / 2)
|
||||
resy = Math.round(resy / 2)
|
||||
}
|
||||
}
|
||||
|
||||
render(
|
||||
renderer: WebGLRenderer,
|
||||
writeBuffer: any,
|
||||
readBuffer: { texture: Texture },
|
||||
deltaTime: any,
|
||||
maskActive: any
|
||||
) {
|
||||
renderer.getClearColor(this._oldClearColor)
|
||||
this.oldClearAlpha = renderer.getClearAlpha()
|
||||
const oldAutoClear = renderer.autoClear
|
||||
renderer.autoClear = false
|
||||
|
||||
renderer.setClearColor(this.clearColor, 0)
|
||||
|
||||
if (maskActive) renderer.state.buffers.stencil.setTest(false)
|
||||
|
||||
// Render input to screen
|
||||
|
||||
if (this.renderToScreen) {
|
||||
this.fsQuad.material = this.basic
|
||||
this.basic.map = readBuffer.texture
|
||||
|
||||
renderer.setRenderTarget(null)
|
||||
renderer.clear()
|
||||
this.fsQuad.render(renderer)
|
||||
}
|
||||
|
||||
// 1. Extract Bright Areas
|
||||
|
||||
this.highPassUniforms['tDiffuse'].value = readBuffer.texture
|
||||
this.highPassUniforms['luminosityThreshold'].value = this.threshold
|
||||
this.fsQuad.material = this.materialHighPassFilter
|
||||
|
||||
renderer.setRenderTarget(this.renderTargetBright)
|
||||
renderer.clear()
|
||||
this.fsQuad.render(renderer)
|
||||
|
||||
// 2. Blur All the mips progressively
|
||||
|
||||
let inputRenderTarget = this.renderTargetBright
|
||||
|
||||
for (let i = 0; i < this.nMips; i++) {
|
||||
this.fsQuad.material = this.separableBlurMaterials[i]
|
||||
|
||||
this.separableBlurMaterials[i].uniforms['colorTexture'].value = inputRenderTarget.texture
|
||||
this.separableBlurMaterials[i].uniforms['direction'].value =
|
||||
TransparentBloomPass.BlurDirectionX
|
||||
renderer.setRenderTarget(this.renderTargetsHorizontal[i])
|
||||
renderer.clear()
|
||||
this.fsQuad.render(renderer)
|
||||
|
||||
this.separableBlurMaterials[i].uniforms['colorTexture'].value =
|
||||
this.renderTargetsHorizontal[i].texture
|
||||
this.separableBlurMaterials[i].uniforms['direction'].value =
|
||||
TransparentBloomPass.BlurDirectionY
|
||||
renderer.setRenderTarget(this.renderTargetsVertical[i])
|
||||
renderer.clear()
|
||||
this.fsQuad.render(renderer)
|
||||
|
||||
inputRenderTarget = this.renderTargetsVertical[i]
|
||||
}
|
||||
|
||||
// Composite All the mips
|
||||
|
||||
this.fsQuad.material = this.compositeMaterial
|
||||
this.compositeMaterial.uniforms['bloomStrength'].value = this.strength
|
||||
this.compositeMaterial.uniforms['bloomRadius'].value = this.radius
|
||||
this.compositeMaterial.uniforms['bloomTintColors'].value = this.bloomTintColors
|
||||
|
||||
renderer.setRenderTarget(this.renderTargetsHorizontal[0])
|
||||
renderer.clear()
|
||||
this.fsQuad.render(renderer)
|
||||
|
||||
// Blend it additively over the input texture
|
||||
|
||||
this.fsQuad.material = this.materialCopy
|
||||
this.copyUniforms['tDiffuse'].value = this.renderTargetsHorizontal[0].texture
|
||||
|
||||
if (maskActive) renderer.state.buffers.stencil.setTest(true)
|
||||
|
||||
if (this.renderToScreen) {
|
||||
renderer.setRenderTarget(null)
|
||||
this.fsQuad.render(renderer)
|
||||
} else {
|
||||
renderer.setRenderTarget(readBuffer as any)
|
||||
this.fsQuad.render(renderer)
|
||||
}
|
||||
|
||||
// Restore renderer settings
|
||||
|
||||
renderer.setClearColor(this._oldClearColor, this.oldClearAlpha)
|
||||
renderer.autoClear = oldAutoClear
|
||||
}
|
||||
|
||||
getSeperableBlurMaterial(kernelRadius: number) {
|
||||
return new ShaderMaterial({
|
||||
defines: {
|
||||
KERNEL_RADIUS: kernelRadius,
|
||||
SIGMA: kernelRadius,
|
||||
},
|
||||
|
||||
uniforms: {
|
||||
colorTexture: { value: null },
|
||||
texSize: { value: new Vector2(0.5, 0.5) },
|
||||
direction: { value: new Vector2(0.5, 0.5) },
|
||||
},
|
||||
|
||||
vertexShader: `varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
|
||||
}`,
|
||||
|
||||
fragmentShader: `#include <common>
|
||||
varying vec2 vUv;
|
||||
uniform sampler2D colorTexture;
|
||||
uniform vec2 texSize;
|
||||
uniform vec2 direction;
|
||||
|
||||
float gaussianPdf(in float x, in float sigma) {
|
||||
return 0.39894 * exp( -0.5 * x * x/( sigma * sigma))/sigma;
|
||||
}
|
||||
void main() {\n\
|
||||
vec2 invSize = 1.0 / texSize;\
|
||||
float fSigma = float(SIGMA);\
|
||||
float weightSum = gaussianPdf(0.0, fSigma);\
|
||||
float alphaSum = 0.0;\
|
||||
vec3 diffuseSum = texture2D( colorTexture, vUv).rgb * weightSum;\
|
||||
for( int i = 1; i < KERNEL_RADIUS; i ++ ) {\
|
||||
float x = float(i);\
|
||||
float w = gaussianPdf(x, fSigma);\
|
||||
vec2 uvOffset = direction * invSize * x;\
|
||||
vec4 sample1 = texture2D( colorTexture, vUv + uvOffset);\
|
||||
vec4 sample2 = texture2D( colorTexture, vUv - uvOffset);\
|
||||
diffuseSum += (sample1.rgb + sample2.rgb) * w;\
|
||||
alphaSum += (sample1.a + sample2.a) * w;\
|
||||
weightSum += 2.0 * w;\
|
||||
}\
|
||||
gl_FragColor = vec4(diffuseSum/weightSum, alphaSum/weightSum);\n\
|
||||
}`,
|
||||
})
|
||||
}
|
||||
|
||||
getCompositeMaterial(nMips: number) {
|
||||
return new ShaderMaterial({
|
||||
defines: {
|
||||
NUM_MIPS: nMips,
|
||||
},
|
||||
|
||||
uniforms: {
|
||||
blurTexture1: { value: null },
|
||||
blurTexture2: { value: null },
|
||||
blurTexture3: { value: null },
|
||||
blurTexture4: { value: null },
|
||||
blurTexture5: { value: null },
|
||||
dirtTexture: { value: null },
|
||||
bloomStrength: { value: 1.0 },
|
||||
bloomFactors: { value: null },
|
||||
bloomTintColors: { value: null },
|
||||
bloomRadius: { value: 0.0 },
|
||||
},
|
||||
|
||||
vertexShader: `varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
|
||||
}`,
|
||||
|
||||
fragmentShader: `varying vec2 vUv;
|
||||
uniform sampler2D blurTexture1;
|
||||
uniform sampler2D blurTexture2;
|
||||
uniform sampler2D blurTexture3;
|
||||
uniform sampler2D blurTexture4;
|
||||
uniform sampler2D blurTexture5;
|
||||
uniform sampler2D dirtTexture;
|
||||
uniform float bloomStrength;
|
||||
uniform float bloomRadius;
|
||||
uniform float bloomFactors[NUM_MIPS];
|
||||
uniform vec3 bloomTintColors[NUM_MIPS];
|
||||
|
||||
float lerpBloomFactor(const in float factor) {
|
||||
float mirrorFactor = 1.2 - factor;
|
||||
return mix(factor, mirrorFactor, bloomRadius);
|
||||
}
|
||||
|
||||
void main() {
|
||||
gl_FragColor = bloomStrength * ( lerpBloomFactor(bloomFactors[0]) * vec4(bloomTintColors[0], 1.0) * texture2D(blurTexture1, vUv) +
|
||||
lerpBloomFactor(bloomFactors[1]) * vec4(bloomTintColors[1], 1.0) * texture2D(blurTexture2, vUv) +
|
||||
lerpBloomFactor(bloomFactors[2]) * vec4(bloomTintColors[2], 1.0) * texture2D(blurTexture3, vUv) +
|
||||
lerpBloomFactor(bloomFactors[3]) * vec4(bloomTintColors[3], 1.0) * texture2D(blurTexture4, vUv) +
|
||||
lerpBloomFactor(bloomFactors[4]) * vec4(bloomTintColors[4], 1.0) * texture2D(blurTexture5, vUv) );
|
||||
}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
TransparentBloomPass.BlurDirectionX = new Vector2(1.0, 0.0)
|
||||
TransparentBloomPass.BlurDirectionY = new Vector2(0.0, 1.0)
|
||||
|
||||
export { TransparentBloomPass as UnrealBloomPass, TransparentBloomPass }
|
||||
88
apps/www/components/LaunchWeek/14/helpers.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { useRef, useEffect } from 'react'
|
||||
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||
|
||||
/**
|
||||
* Helper to simplifies controlling requestAnimationFrame
|
||||
* @param callback Animation callback function
|
||||
* @returns Object with start and stop functions
|
||||
*/
|
||||
export const createThreeAnimation = (callback: (time?: number) => void) => {
|
||||
const requestRef = { current: undefined } as { current: number | undefined }
|
||||
const previousTimeRef = { current: undefined } as { current: number | undefined }
|
||||
|
||||
const animate = (time: number) => {
|
||||
if (previousTimeRef.current !== undefined) {
|
||||
callback(time)
|
||||
}
|
||||
previousTimeRef.current = time
|
||||
requestRef.current = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
const start = () => {
|
||||
requestRef.current = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
if (requestRef.current) {
|
||||
cancelAnimationFrame(requestRef.current)
|
||||
}
|
||||
}
|
||||
|
||||
return { start, stop }
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to load a GLTF model
|
||||
* @param url URL of the GLTF model to load
|
||||
* @returns Promise that resolves with the loaded model
|
||||
*/
|
||||
export const loadGLTFModel = (url: string) => {
|
||||
return new Promise<GLTF>((resolve, reject) => {
|
||||
const loader = new GLTFLoader()
|
||||
loader.load(
|
||||
url,
|
||||
(gltf) => {
|
||||
resolve(gltf)
|
||||
},
|
||||
undefined,
|
||||
(error) => reject(error)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for Three.js setup and animation
|
||||
* @param setupCallback Callback function for setting up the scene
|
||||
* @returns Object with containerRef
|
||||
*/
|
||||
export const useThreeJS = (
|
||||
setupCallback: (container: HTMLElement) => {
|
||||
cleanup: () => void
|
||||
animate: (time?: number) => void
|
||||
}
|
||||
) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return
|
||||
|
||||
const container = containerRef.current
|
||||
const renderer = setupCallback(container)
|
||||
|
||||
// Set up animation loop
|
||||
const { start, stop } = createThreeAnimation(renderer.animate.bind(renderer))
|
||||
start()
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
stop()
|
||||
renderer.cleanup()
|
||||
}
|
||||
}, [setupCallback])
|
||||
|
||||
return { containerRef }
|
||||
}
|
||||
|
||||
export function colorObjToRgb(color: { rgb: number; alpha: number }) {
|
||||
return `rgb(${(color.rgb >> 16) & 255} ${(color.rgb >> 8) & 255} ${(color.rgb >> 0) & 255} / ${color.alpha})`
|
||||
}
|
||||
233
apps/www/components/LaunchWeek/14/hooks/use-conf-data.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
import { RealtimeChannel, Session, SupabaseClient } from '@supabase/supabase-js'
|
||||
import { useRouter } from 'next/router'
|
||||
import { createContext, Dispatch, useContext, useEffect, useMemo, useReducer } from 'react'
|
||||
|
||||
/**
|
||||
* This is copy of shared use-conf-data.ts. For laynch week 14 we need different ticket states.
|
||||
* To not break the existing functionality, we are creating a new context and hook.
|
||||
*/
|
||||
|
||||
export type TicketState = 'registration' | 'ticket-visible' | 'loading' | 'ticket-loading'
|
||||
|
||||
export type UserTicketData = {
|
||||
id?: string
|
||||
email?: string
|
||||
username?: string
|
||||
name?: string
|
||||
ticket_number?: number
|
||||
platinum?: boolean
|
||||
golden?: boolean
|
||||
referrals?: number
|
||||
bg_image_id?: number
|
||||
role?: string
|
||||
company?: string
|
||||
location?: string
|
||||
metadata?: {
|
||||
role?: string
|
||||
company?: string
|
||||
location?: string
|
||||
hasSecretTicket?: boolean
|
||||
hasSharedSecret?: boolean
|
||||
hideAvatar?: boolean
|
||||
hideMetadata?: boolean
|
||||
theme?: string
|
||||
}
|
||||
shared_on_twitter?: string
|
||||
shared_on_linkedin?: string
|
||||
secret?: boolean
|
||||
}
|
||||
|
||||
type LwAction =
|
||||
| { type: 'USER_TICKET_FETCH_STARTED' }
|
||||
| { type: 'USER_TICKET_FETCH_SUCCESS'; payload: UserTicketData }
|
||||
| { type: 'USER_TICKET_FETCH_ERROR'; payload: Error }
|
||||
| { type: 'USER_TICKET_UPDATED'; payload: UserTicketData }
|
||||
| { type: 'SESSION_UPDATED'; payload: Session | null }
|
||||
| { type: 'TICKET_LOADING_START' }
|
||||
| { type: 'TICKET_LOADING_SUCCESS' }
|
||||
| { type: 'TICKET_LOADING_ERROR'; payload?: Error }
|
||||
| { type: 'PARTYMODE_ENABLE'; payload: RealtimeChannel }
|
||||
| { type: 'PARTYMODE_DISABLE' }
|
||||
| { type: 'URL_PARAMS_LOADED'; payload: { referal?: string } }
|
||||
| {
|
||||
type: 'GAUGES_DATA_FETCHED'
|
||||
payload: {
|
||||
payloadSaturation?: number
|
||||
payloadFill?: number
|
||||
meetupsAmount?: number
|
||||
peopleOnline?: number
|
||||
}
|
||||
}
|
||||
|
||||
// Define state interface
|
||||
interface LwState {
|
||||
userTicketData: UserTicketData
|
||||
ticketState: TicketState
|
||||
session: Session | null
|
||||
userTicketDataState: 'unloaded' | 'loading' | 'error' | 'loaded'
|
||||
userTicketDataError: Error | null
|
||||
ticketLoadingState: 'unloaded' | 'loading' | 'error' | 'loaded'
|
||||
ticketVisibility: boolean
|
||||
claimFormState: 'initial' | 'visible' | 'hidden'
|
||||
partymodeStatus: 'on' | 'off'
|
||||
realtimeGaugesChannel: RealtimeChannel | null
|
||||
referal?: string
|
||||
gaugesData: {
|
||||
payloadSaturation: number | null
|
||||
payloadFill: number | null
|
||||
meetupsAmount: number | null
|
||||
peopleOnline: number | null
|
||||
} | null
|
||||
urlParamsLoaded: boolean
|
||||
}
|
||||
|
||||
export const lwReducer = (state: LwState, action: LwAction): LwState => {
|
||||
switch (action.type) {
|
||||
case 'SESSION_UPDATED':
|
||||
return {
|
||||
...state,
|
||||
session: action.payload,
|
||||
// Show claim form if session is not available. Form triggers authentication flow.
|
||||
claimFormState: !action.payload ? 'visible' : 'hidden',
|
||||
}
|
||||
case 'USER_TICKET_FETCH_STARTED':
|
||||
return { ...state, userTicketDataState: 'loading', userTicketDataError: null }
|
||||
case 'USER_TICKET_FETCH_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
userTicketData: action.payload,
|
||||
ticketVisibility: action.payload !== null && state.ticketLoadingState === 'loaded',
|
||||
}
|
||||
case 'USER_TICKET_FETCH_ERROR':
|
||||
return {
|
||||
...state,
|
||||
session: null,
|
||||
ticketState: 'registration',
|
||||
userTicketDataState: 'error',
|
||||
userTicketDataError: action.payload,
|
||||
}
|
||||
case 'USER_TICKET_UPDATED':
|
||||
return {
|
||||
...state,
|
||||
userTicketData: action.payload,
|
||||
userTicketDataState: Boolean(action.payload.id) ? 'loaded' : 'unloaded',
|
||||
userTicketDataError: null,
|
||||
}
|
||||
case 'TICKET_LOADING_START':
|
||||
return {
|
||||
...state,
|
||||
ticketLoadingState: 'loading',
|
||||
ticketVisibility: false,
|
||||
}
|
||||
case 'TICKET_LOADING_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
ticketLoadingState: 'loaded',
|
||||
ticketVisibility: Boolean(state.userTicketData.id),
|
||||
}
|
||||
case 'TICKET_LOADING_ERROR':
|
||||
return {
|
||||
...state,
|
||||
ticketLoadingState: 'error',
|
||||
ticketVisibility: false,
|
||||
claimFormState: 'visible',
|
||||
}
|
||||
case 'PARTYMODE_ENABLE': {
|
||||
return {
|
||||
...state,
|
||||
realtimeGaugesChannel: action.payload,
|
||||
partymodeStatus: 'on',
|
||||
}
|
||||
}
|
||||
case 'PARTYMODE_DISABLE': {
|
||||
return {
|
||||
...state,
|
||||
realtimeGaugesChannel: null,
|
||||
partymodeStatus: 'off',
|
||||
}
|
||||
}
|
||||
case 'GAUGES_DATA_FETCHED': {
|
||||
const nonNullableKeys = Object.fromEntries(
|
||||
Object.entries(action.payload).filter(([_, v]) => v !== undefined)
|
||||
) as typeof action.payload
|
||||
const newGaugeData = state.gaugesData
|
||||
? { ...state.gaugesData, ...nonNullableKeys }
|
||||
: {
|
||||
payloadSaturation: null,
|
||||
payloadFill: null,
|
||||
meetupsAmount: null,
|
||||
peopleOnline: null,
|
||||
...nonNullableKeys,
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
gaugesData: newGaugeData,
|
||||
}
|
||||
}
|
||||
case 'URL_PARAMS_LOADED': {
|
||||
return { ...state, urlParamsLoaded: true, referal: action.payload.referal }
|
||||
}
|
||||
default:
|
||||
action satisfies never
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export const Lw14ConfDataContext = createContext<[LwState, Dispatch<LwAction>] | null>(null)
|
||||
|
||||
function takeFirst(param: string | string[] | undefined): string | undefined {
|
||||
if (Array.isArray(param)) {
|
||||
return param[0]
|
||||
}
|
||||
|
||||
return param ?? undefined
|
||||
}
|
||||
|
||||
export const Lw14ConfDataProvider = ({
|
||||
children,
|
||||
initState,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
initState?: Partial<LwState>
|
||||
}) => {
|
||||
const { query, isReady } = useRouter()
|
||||
|
||||
const providerValue = useReducer(lwReducer, {
|
||||
userTicketData: {},
|
||||
ticketState: 'loading',
|
||||
session: null,
|
||||
ticketLoadingState: 'unloaded',
|
||||
ticketVisibility: false,
|
||||
userTicketDataState: 'unloaded',
|
||||
userTicketDataError: null,
|
||||
claimFormState: 'initial',
|
||||
realtimeGaugesChannel: null,
|
||||
partymodeStatus: 'off',
|
||||
gaugesData: null,
|
||||
urlParamsLoaded: false,
|
||||
...initState,
|
||||
})
|
||||
const [, dispatch] = providerValue
|
||||
|
||||
useEffect(() => {
|
||||
if (isReady) {
|
||||
dispatch({
|
||||
type: 'URL_PARAMS_LOADED',
|
||||
payload: { referal: takeFirst(query.referal) ?? takeFirst(query.username) },
|
||||
})
|
||||
}
|
||||
}, [dispatch, isReady, query.referal, query.username])
|
||||
|
||||
return (
|
||||
<Lw14ConfDataContext.Provider value={providerValue}>{children}</Lw14ConfDataContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default function useLw14ConfData() {
|
||||
const result = useContext(Lw14ConfDataContext)
|
||||
if (!result) {
|
||||
throw new Error('useLw14ConfData must be used within a Lw14ConfDataProvider')
|
||||
}
|
||||
return result
|
||||
}
|
||||
119
apps/www/components/LaunchWeek/14/hooks/use-partymode.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import useLw14ConfData from './use-conf-data'
|
||||
import supabase from '../supabase'
|
||||
import { REALTIME_CHANNEL_STATES, REALTIME_SUBSCRIBE_STATES } from '@supabase/supabase-js'
|
||||
|
||||
const LW14_TOPIC = 'lw14'
|
||||
const GAUGES_UPDATES_EVENT = 'gauges-update'
|
||||
|
||||
export const usePartymode = () => {
|
||||
const [state, dispatch] = useLw14ConfData()
|
||||
const [shouldInitialize, setShouldInitialize] = useState(
|
||||
state.partymodeStatus === 'on' && !state.realtimeGaugesChannel
|
||||
)
|
||||
|
||||
const createChannelAndSubscribe = useCallback(() => {
|
||||
const channel = supabase.channel(LW14_TOPIC, {
|
||||
config: {
|
||||
broadcast: {
|
||||
self: true,
|
||||
ack: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const userStatus = {}
|
||||
|
||||
return channel
|
||||
.on('broadcast', { event: GAUGES_UPDATES_EVENT }, (data) => {
|
||||
const { payload } = data
|
||||
|
||||
dispatch({
|
||||
type: 'GAUGES_DATA_FETCHED',
|
||||
payload: {
|
||||
payloadFill: payload.payload_fill,
|
||||
payloadSaturation: payload.payload_saturation,
|
||||
meetupsAmount: payload.meetups_amount,
|
||||
},
|
||||
})
|
||||
})
|
||||
.on('presence', { event: 'sync' }, () => {
|
||||
const newState = channel.presenceState()
|
||||
const uniqueUsers = new Set([
|
||||
...Object.entries(newState).map(([_, value]) => value[0].presence_ref),
|
||||
])
|
||||
|
||||
dispatch({
|
||||
type: 'GAUGES_DATA_FETCHED',
|
||||
payload: { peopleOnline: uniqueUsers.size },
|
||||
})
|
||||
})
|
||||
.subscribe(async (status, error) => {
|
||||
// console.log('Channel status', status, error)
|
||||
await channel.track(userStatus)
|
||||
|
||||
if (status === REALTIME_SUBSCRIBE_STATES.SUBSCRIBED) {
|
||||
dispatch({ type: 'PARTYMODE_ENABLE', payload: channel })
|
||||
}
|
||||
})
|
||||
}, [dispatch])
|
||||
|
||||
const toggle = useCallback(async () => {
|
||||
if (state.partymodeStatus === 'on') {
|
||||
await state.realtimeGaugesChannel?.unsubscribe()
|
||||
dispatch({ type: 'PARTYMODE_DISABLE' })
|
||||
} else {
|
||||
createChannelAndSubscribe()
|
||||
}
|
||||
}, [createChannelAndSubscribe, dispatch, state.partymodeStatus, state.realtimeGaugesChannel])
|
||||
|
||||
const fetchGaugesData = useCallback(async () => {
|
||||
const [{ data: payloadData, error: payloadError }, { data: meetupsData, error: meetupsError }] =
|
||||
await Promise.all([
|
||||
supabase.rpc('get_payload_data_for_lw14'),
|
||||
supabase.rpc('get_meetups_data_for_lw14'),
|
||||
])
|
||||
|
||||
if (payloadError || meetupsError) {
|
||||
console.error('Error fetching gauges data', payloadError, meetupsError)
|
||||
return
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'GAUGES_DATA_FETCHED',
|
||||
payload: {
|
||||
payloadFill: payloadData.payload_fill,
|
||||
payloadSaturation: payloadData.payload_saturation,
|
||||
meetupsAmount: meetupsData.meetups_amount,
|
||||
},
|
||||
})
|
||||
}, [dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
state.partymodeStatus === 'on' &&
|
||||
(!state.realtimeGaugesChannel ||
|
||||
state.realtimeGaugesChannel.state === REALTIME_CHANNEL_STATES.closed)
|
||||
) {
|
||||
const channel = createChannelAndSubscribe()
|
||||
void fetchGaugesData()
|
||||
return () => {
|
||||
if (
|
||||
state.partymodeStatus === 'off' &&
|
||||
state.realtimeGaugesChannel?.state === REALTIME_CHANNEL_STATES.joined
|
||||
)
|
||||
channel.unsubscribe()
|
||||
}
|
||||
}
|
||||
}, [
|
||||
createChannelAndSubscribe,
|
||||
fetchGaugesData,
|
||||
shouldInitialize,
|
||||
state.partymodeStatus,
|
||||
state.realtimeGaugesChannel,
|
||||
])
|
||||
|
||||
return {
|
||||
toggle,
|
||||
}
|
||||
}
|
||||
214
apps/www/components/LaunchWeek/14/hooks/use-registration.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { REALTIME_CHANNEL_STATES, RealtimeChannel, SupabaseClient } from '@supabase/supabase-js'
|
||||
|
||||
import useConfData from './use-conf-data'
|
||||
import { LW14_URL } from '~/lib/constants'
|
||||
import supabase from '../supabase'
|
||||
|
||||
function subscribeToTicketChanges(
|
||||
username: string,
|
||||
onChange: (payload: any) => void
|
||||
): RealtimeChannel {
|
||||
return supabase
|
||||
.channel('changes')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'UPDATE',
|
||||
schema: 'public',
|
||||
table: 'tickets',
|
||||
filter: `username=eq.${username}`,
|
||||
},
|
||||
onChange
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
interface RegistrationProps {
|
||||
onRegister?: () => void
|
||||
onError?: (error: any) => void
|
||||
}
|
||||
|
||||
export const useRegistration = ({ onError, onRegister }: RegistrationProps = {}) => {
|
||||
// const [realtimeChannel, setRealtimeChannel] = useState<RealtimeChannel | null>(null)
|
||||
const [
|
||||
{ userTicketData: userData, session, userTicketDataState, urlParamsLoaded, referal },
|
||||
dispatch,
|
||||
] = useConfData()
|
||||
const sessionUser = session?.user
|
||||
const callbacksRef = useRef({ onError, onRegister })
|
||||
|
||||
// Triggered on session
|
||||
const fetchOrCreateUser = useCallback(async () => {
|
||||
if (['loading', 'loaded'].includes(userTicketDataState)) return
|
||||
if (!urlParamsLoaded) return
|
||||
|
||||
if (!sessionUser) {
|
||||
console.warn('Cannot fetch user without session. Skipping...')
|
||||
return
|
||||
}
|
||||
|
||||
const username = sessionUser.user_metadata?.user_name as string | undefined
|
||||
const name = sessionUser.user_metadata.full_name
|
||||
const email = sessionUser.email
|
||||
const userId = sessionUser.id
|
||||
|
||||
if (!username) {
|
||||
throw new Error('Username is required')
|
||||
}
|
||||
|
||||
if (!userData.id) {
|
||||
dispatch({ type: 'USER_TICKET_FETCH_STARTED' })
|
||||
|
||||
// console.log('Inserting ticket for user', username, referal)
|
||||
|
||||
const { error: ticketInsertError } = await supabase
|
||||
.from('tickets')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
launch_week: 'lw14',
|
||||
email,
|
||||
name,
|
||||
username,
|
||||
referred_by: referal ?? null,
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
// If error because of duplicate email, ignore and proceed, otherwise sign out.
|
||||
if (ticketInsertError && ticketInsertError?.code !== '23505') {
|
||||
dispatch({ type: 'USER_TICKET_FETCH_ERROR', payload: ticketInsertError })
|
||||
callbacksRef.current.onError?.(ticketInsertError)
|
||||
return supabase.auth.signOut()
|
||||
}
|
||||
|
||||
const { data, error: ticketsViewError } = await supabase
|
||||
.from('tickets_view')
|
||||
.select('*')
|
||||
.eq('launch_week', 'lw14')
|
||||
.eq('username', username)
|
||||
.single()
|
||||
|
||||
if (ticketsViewError) {
|
||||
dispatch({ type: 'USER_TICKET_FETCH_ERROR', payload: ticketsViewError })
|
||||
callbacksRef.current.onError?.(ticketsViewError)
|
||||
return
|
||||
}
|
||||
|
||||
dispatch({ type: 'USER_TICKET_FETCH_SUCCESS', payload: data })
|
||||
|
||||
await prefetchData(username)
|
||||
}
|
||||
|
||||
callbacksRef.current.onRegister?.()
|
||||
}, [dispatch, referal, sessionUser, urlParamsLoaded, userData.id, userTicketDataState])
|
||||
|
||||
async function prefetchData(username: string) {
|
||||
// Prefetch GitHub avatar
|
||||
// new Image().src = `https://github.com/${username}.png`
|
||||
|
||||
// Prefetch the twitter share URL to eagerly generate the page
|
||||
await fetch(`/api-v2/ticket-og?username=${username}`)
|
||||
}
|
||||
|
||||
const handleGithubSignIn = useCallback(async () => {
|
||||
let redirectTo = `${LW14_URL}`
|
||||
|
||||
if (referal) {
|
||||
redirectTo += `?referal=${referal}`
|
||||
}
|
||||
|
||||
const response = await supabase?.auth.signInWithOAuth({
|
||||
provider: 'github',
|
||||
options: {
|
||||
redirectTo,
|
||||
},
|
||||
})
|
||||
|
||||
if (response) {
|
||||
if (response.error) {
|
||||
callbacksRef.current?.onError?.(response.error)
|
||||
return
|
||||
}
|
||||
}
|
||||
}, [referal])
|
||||
|
||||
useEffect(() => {
|
||||
fetchOrCreateUser()
|
||||
|
||||
const username = sessionUser?.user_metadata?.user_name as string | undefined
|
||||
|
||||
if (username) {
|
||||
const channel = subscribeToTicketChanges(username, (payload) => {
|
||||
const platinum = !!payload.new.shared_on_twitter && !!payload.new.shared_on_linkedin
|
||||
const secret = !!payload.new.game_won_at
|
||||
|
||||
dispatch({
|
||||
type: 'USER_TICKET_UPDATED',
|
||||
payload: {
|
||||
...payload.new,
|
||||
platinum,
|
||||
secret,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
// Cleanup realtime subscription on unmount
|
||||
channel?.unsubscribe()
|
||||
}
|
||||
}
|
||||
}, [dispatch, fetchOrCreateUser, sessionUser?.user_metadata?.user_name])
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.getSession().then(({ data, error }) => {
|
||||
if (error) console.error('Session error', error)
|
||||
dispatch({ type: 'SESSION_UPDATED', payload: data.session })
|
||||
})
|
||||
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
dispatch({ type: 'SESSION_UPDATED', payload: session })
|
||||
|
||||
if (session && window.location.hash.includes('access_token')) {
|
||||
window.history.replaceState(null, '', window.location.pathname + window.location.search)
|
||||
}
|
||||
})
|
||||
|
||||
return () => subscription.unsubscribe()
|
||||
}, [dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
callbacksRef.current = { onError, onRegister }
|
||||
})
|
||||
|
||||
const upgradeTicket = async () => {
|
||||
if (userData.id) {
|
||||
if (userData.secret) {
|
||||
// console.log('User already has a secret ticket')
|
||||
return
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from('tickets')
|
||||
.update({ game_won_at: new Date() })
|
||||
.eq('launch_week', 'lw14')
|
||||
.eq('username', userData.username)
|
||||
|
||||
if (error) {
|
||||
return console.error('Failed to upgrade user ticket', error)
|
||||
}
|
||||
|
||||
// Trigger og-image ticket generation
|
||||
await fetch(`/api-v2/ticket-og?username=${userData.username}&secret=true`)
|
||||
} else {
|
||||
console.warn('Cannot upgrade ticket without user data')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
signIn: handleGithubSignIn,
|
||||
upgradeTicket,
|
||||
}
|
||||
}
|
||||
1089
apps/www/components/LaunchWeek/14/scenes/HUDScene.ts
Normal file
1210
apps/www/components/LaunchWeek/14/scenes/TicketScene.ts
Normal file
16
apps/www/components/LaunchWeek/14/styles.css
Normal file
@@ -0,0 +1,16 @@
|
||||
/* Font face definitions for Launch Week 14 */
|
||||
@font-face {
|
||||
font-family: 'Departure Mono';
|
||||
src: url('/fonts/launchweek/14/DepartureMono-Regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nippo-Variable';
|
||||
src: url('/fonts/launchweek/14/Nippo-Variable.woff2') format('woff2');
|
||||
font-weight: 400 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
9
apps/www/components/LaunchWeek/14/supabase.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js'
|
||||
import supabase from '~/lib/supabase'
|
||||
|
||||
/**
|
||||
* Override supabase types similar to previous use-conf-data.ts.
|
||||
* Current apps/www supabase instance uses old types.
|
||||
* Since we don't know final db layout let's leave it as any.
|
||||
*/
|
||||
export default supabase as SupabaseClient
|
||||
295
apps/www/components/LaunchWeek/14/utils/SceneRenderer.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
import {
|
||||
ColorManagement,
|
||||
HalfFloatType,
|
||||
PerspectiveCamera,
|
||||
Scene,
|
||||
SRGBColorSpace,
|
||||
WebGLRenderer,
|
||||
WebGLRenderTarget,
|
||||
} from 'three'
|
||||
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer'
|
||||
|
||||
export interface MousePositionState {
|
||||
clientX: number
|
||||
clientY: number
|
||||
isWithinContainer: boolean
|
||||
containerX: number
|
||||
containerY: number
|
||||
mouseIntensity: number
|
||||
}
|
||||
|
||||
export interface BaseScene {
|
||||
getId(): string
|
||||
/**
|
||||
* Initialize the scene with the given context
|
||||
*/
|
||||
setup(context: SceneRenderer): Promise<Scene | void>
|
||||
|
||||
/**
|
||||
* Update the scene for each animation frame
|
||||
*/
|
||||
update(context: SceneRenderer, dt?: number): void
|
||||
|
||||
/**
|
||||
* Clean up resources when the scene is destroyed
|
||||
*/
|
||||
cleanup(): void
|
||||
|
||||
resize(ev: UIEvent): void
|
||||
|
||||
/**
|
||||
* Handle device pixel ratio changes
|
||||
*/
|
||||
devicePixelRatioChanged?(newPixelRatio: number, oldPixelRatio: number): void
|
||||
}
|
||||
|
||||
class SceneRenderer {
|
||||
renderer: WebGLRenderer
|
||||
composer: EffectComposer
|
||||
camera: PerspectiveCamera
|
||||
mainThreeJsScene: Scene | null = null
|
||||
activeScenes: BaseScene[] = []
|
||||
cachedContainerBBox: DOMRect
|
||||
mousePositionState: MousePositionState
|
||||
mouseIntensityDecay = 0.98
|
||||
mouseIntensityGainRate = 0.003
|
||||
currentPixelRatio: number
|
||||
|
||||
private _resizeHandler: ((ev: UIEvent) => void) | null = null
|
||||
private _mouseMoveHandler: ((ev: MouseEvent) => void) | null = null
|
||||
private _mediaQueryListHandler: ((ev: MediaQueryListEvent) => void) | null = null
|
||||
private _isDisposed = false
|
||||
private _isInitialized = false
|
||||
|
||||
constructor(
|
||||
public container: HTMLElement,
|
||||
private waitFor?: { init: Promise<void>; renderer: SceneRenderer }[],
|
||||
private uuid?: string
|
||||
) {
|
||||
this.renderer = new WebGLRenderer({ antialias: true, alpha: true })
|
||||
this.composer = new EffectComposer(this.renderer)
|
||||
|
||||
this.camera = new PerspectiveCamera(
|
||||
75,
|
||||
this.container.clientWidth / this.container.clientHeight,
|
||||
0.1,
|
||||
1000
|
||||
)
|
||||
|
||||
this.mousePositionState = {
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
isWithinContainer: false,
|
||||
containerX: 0,
|
||||
containerY: 0,
|
||||
mouseIntensity: 0,
|
||||
}
|
||||
|
||||
this.cachedContainerBBox = this.container.getBoundingClientRect()
|
||||
this.currentPixelRatio = window.devicePixelRatio
|
||||
}
|
||||
|
||||
async init(sceneInitializer: () => Promise<void>) {
|
||||
// console.log('SCENE RENDERER: Init call', this.waitFor?.length, this.uuid)
|
||||
await Promise.allSettled(
|
||||
this.waitFor?.filter((t) => t.renderer !== this).map((t) => t.init) || []
|
||||
)
|
||||
|
||||
if (this._isDisposed) {
|
||||
// console.log('SCENE RENDERER: Already disposed before sceneInitializer', this.uuid)
|
||||
return
|
||||
}
|
||||
|
||||
// console.log('SCENE RENDERER: Waited for all pending inits', this.waitFor?.length, this.uuid)
|
||||
|
||||
await sceneInitializer()
|
||||
|
||||
if (this._isDisposed) {
|
||||
// console.log('SCENE RENDERER: Already disposed after sceneInitializer', this.uuid)
|
||||
return
|
||||
}
|
||||
|
||||
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight)
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio)
|
||||
this.renderer.setClearColor(0x000000, 0) // Set transparent background
|
||||
this.renderer.autoClear = false // Prevent automatic clearing
|
||||
this.renderer.outputColorSpace = SRGBColorSpace
|
||||
ColorManagement.enabled = true
|
||||
|
||||
// Initialize composer with correct size and pixel ratio
|
||||
this.composer.setSize(this.container.clientWidth, this.container.clientHeight)
|
||||
this.composer.setPixelRatio(window.devicePixelRatio)
|
||||
|
||||
this.container.appendChild(this.renderer.domElement)
|
||||
|
||||
this._resizeHandler = this._resize.bind(this)
|
||||
window.addEventListener('resize', this._resizeHandler)
|
||||
|
||||
this._mouseMoveHandler = this._updateMousePosition.bind(this)
|
||||
window.addEventListener('mousemove', this._mouseMoveHandler)
|
||||
|
||||
// Disable it since it brakes Chrome browser
|
||||
// // Add device pixel ratio change listener
|
||||
// this._mediaQueryListHandler = this._handleDevicePixelRatioChange.bind(this)
|
||||
// window
|
||||
// .matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
|
||||
// .addEventListener('change', this._mediaQueryListHandler)
|
||||
}
|
||||
|
||||
async activateScene(scene: BaseScene, main?: boolean) {
|
||||
if (this._isDisposed) {
|
||||
// console.log('SCENE RENDERER: Already disposed before activateScene', this.uuid)
|
||||
return
|
||||
}
|
||||
|
||||
const threeScene = await scene.setup(this)
|
||||
|
||||
if (this._isDisposed) {
|
||||
// console.log('SCENE RENDERER: Already disposed after activateScene', this.uuid)
|
||||
return
|
||||
}
|
||||
|
||||
if (main && threeScene) {
|
||||
this.mainThreeJsScene = threeScene
|
||||
}
|
||||
|
||||
const newId = scene.getId()
|
||||
const existingSceneId = this.activeScenes.findIndex((s) => s.getId() === newId)
|
||||
if (existingSceneId > -1) {
|
||||
const sceneToRemove = this.activeScenes[existingSceneId]
|
||||
this.activeScenes[existingSceneId] = scene
|
||||
sceneToRemove.cleanup()
|
||||
} else {
|
||||
this.activeScenes.push(scene)
|
||||
}
|
||||
}
|
||||
|
||||
animate(time?: number) {
|
||||
// Clear the renderer before rendering
|
||||
this.renderer.clear()
|
||||
|
||||
this._decayMouseIntensity()
|
||||
|
||||
for (const activeScene of this.activeScenes) {
|
||||
activeScene.update(this, time)
|
||||
}
|
||||
this.composer.render(time)
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
// console.log('SCENE RENDERER: Cleanup', this.uuid)
|
||||
|
||||
this._isDisposed = true
|
||||
this.composer.dispose()
|
||||
this.renderer.dispose()
|
||||
|
||||
try {
|
||||
this.container.removeChild(this.renderer.domElement)
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
|
||||
if (this._resizeHandler) {
|
||||
window.removeEventListener('resize', this._resizeHandler)
|
||||
this._resizeHandler = null
|
||||
}
|
||||
|
||||
if (this._mouseMoveHandler) {
|
||||
window.removeEventListener('mousemove', this._mouseMoveHandler)
|
||||
this._mouseMoveHandler = null
|
||||
}
|
||||
|
||||
if (this._mediaQueryListHandler) {
|
||||
window
|
||||
.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
|
||||
.removeEventListener('change', this._mediaQueryListHandler)
|
||||
this._mediaQueryListHandler = null
|
||||
}
|
||||
|
||||
if (this.waitFor) {
|
||||
// Mutate array in place instead of replacing reference
|
||||
this.waitFor.splice(
|
||||
0,
|
||||
this.waitFor.length,
|
||||
...this.waitFor.filter((t) => t.renderer === this)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private _resize(ev: UIEvent) {
|
||||
this.camera.aspect = this.container.clientWidth / this.container.clientHeight
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight)
|
||||
this.composer.setSize(this.container.clientWidth, this.container.clientHeight)
|
||||
this.cachedContainerBBox = this.container.getBoundingClientRect()
|
||||
|
||||
for (const activeScene of this.activeScenes) {
|
||||
activeScene.resize(ev)
|
||||
}
|
||||
}
|
||||
|
||||
private _handleDevicePixelRatioChange(ev: MediaQueryListEvent) {
|
||||
const oldPixelRatio = this.currentPixelRatio
|
||||
const newPixelRatio = window.devicePixelRatio
|
||||
|
||||
// Update renderer and composer pixel ratio
|
||||
this.renderer.setPixelRatio(newPixelRatio)
|
||||
this.composer.setPixelRatio(newPixelRatio)
|
||||
|
||||
// Store the new pixel ratio
|
||||
this.currentPixelRatio = newPixelRatio
|
||||
|
||||
// Notify all active scenes about the pixel ratio change
|
||||
for (const activeScene of this.activeScenes) {
|
||||
if (typeof activeScene.devicePixelRatioChanged === 'function') {
|
||||
activeScene.devicePixelRatioChanged(newPixelRatio, oldPixelRatio)
|
||||
}
|
||||
}
|
||||
|
||||
// console.log(
|
||||
// `SCENE RENDERER: Device pixel ratio changed from ${oldPixelRatio} to ${newPixelRatio}`
|
||||
// )
|
||||
}
|
||||
|
||||
private _decayMouseIntensity() {
|
||||
if (this.mousePositionState.mouseIntensity > 0) {
|
||||
this.mousePositionState.mouseIntensity *= this.mouseIntensityDecay
|
||||
if (this.mousePositionState.mouseIntensity < 0.0000001) {
|
||||
this.mousePositionState.mouseIntensity = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _updateMousePosition(ev: MouseEvent) {
|
||||
this.cachedContainerBBox = this.container.getBoundingClientRect()
|
||||
const dx = ev.clientX - this.mousePositionState.clientX
|
||||
const dy = ev.clientY - this.mousePositionState.clientY
|
||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
// Update intensity based on movement (clamped between 0 and 1)
|
||||
this.mousePositionState.mouseIntensity = Math.min(
|
||||
1.0,
|
||||
Math.max(0, this.mousePositionState.mouseIntensity + distance * this.mouseIntensityGainRate)
|
||||
)
|
||||
|
||||
// Update last position
|
||||
this.mousePositionState.clientX = ev.clientX
|
||||
this.mousePositionState.clientY = ev.clientY
|
||||
|
||||
const rect = this.cachedContainerBBox
|
||||
if (!rect) {
|
||||
return
|
||||
}
|
||||
const isWithinContainer =
|
||||
ev.clientX >= rect.left &&
|
||||
ev.clientX <= rect.right &&
|
||||
ev.clientY >= rect.top &&
|
||||
ev.clientY <= rect.bottom
|
||||
|
||||
this.mousePositionState.isWithinContainer = isWithinContainer
|
||||
this.mousePositionState.containerX = ((ev.clientX - rect.left) / rect.width) * 2 - 1
|
||||
this.mousePositionState.containerY = -((ev.clientY - rect.top) / rect.height) * 2 + 1
|
||||
}
|
||||
}
|
||||
|
||||
export default SceneRenderer
|
||||
11
apps/www/components/LaunchWeek/14/utils/ticketThemes.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
export const themes = () => ({
|
||||
regular: {
|
||||
TICKET_FOREGROUND: '#1F1F1F',
|
||||
},
|
||||
platinum: {
|
||||
TICKET_FOREGROUND: '#1F1F1F',
|
||||
},
|
||||
secret: {
|
||||
TICKET_FOREGROUND: '#1F1F1F',
|
||||
},
|
||||
})
|
||||
@@ -7,6 +7,10 @@ interface Props {
|
||||
id?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* This component doesn't use cn so parent tailwind classes aren't applied correctly
|
||||
* At this point it's dangerous to fix it because it could break the layout at many places
|
||||
*/
|
||||
const SectionContainer = forwardRef(
|
||||
({ children, className, id }: Props, ref: Ref<HTMLDivElement>) => (
|
||||
<div
|
||||
@@ -22,4 +26,6 @@ const SectionContainer = forwardRef(
|
||||
)
|
||||
)
|
||||
|
||||
SectionContainer.displayName = 'SectionContainer'
|
||||
|
||||
export default SectionContainer
|
||||
|
||||
45
apps/www/components/Layouts/SectionContainerWithCn.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Ref, forwardRef } from 'react'
|
||||
import { cn } from 'ui'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
const sectionContainerVariants = cva('max-w-7xl relative mx-auto px-6', {
|
||||
variants: {
|
||||
width: {
|
||||
normal: 'lg:px-16 xl:px-20',
|
||||
smallScreenFull: 'max-w-full lg:container px-0',
|
||||
},
|
||||
height: {
|
||||
normal: 'py-16 md:py-24 lg:py-24',
|
||||
narrow: 'py-6 md:py-8',
|
||||
none: '',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
width: 'normal',
|
||||
height: 'normal',
|
||||
},
|
||||
})
|
||||
|
||||
interface Props extends VariantProps<typeof sectionContainerVariants> {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
id?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* To have tailwind classes applied correctly, use this component instead of SectionContainer
|
||||
*
|
||||
* @param width - 'normal' (default) or 'full'
|
||||
* @param height - 'normal' (default) or 'narrow'
|
||||
*/
|
||||
const SectionContainerWithCn = forwardRef(
|
||||
({ children, className, id, width, height }: Props, ref: Ref<HTMLDivElement>) => (
|
||||
<div ref={ref} id={id} className={cn(sectionContainerVariants({ width, height }), className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
|
||||
SectionContainerWithCn.displayName = 'SectionContainerWithCn'
|
||||
|
||||
export default SectionContainerWithCn
|
||||
@@ -35,7 +35,7 @@ export const SITE_ORIGIN =
|
||||
? `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL}`
|
||||
: 'http://localhost:3000'
|
||||
|
||||
export const LW_URL = `${SITE_ORIGIN}/launch-week`
|
||||
export const LW_URL = `${SITE_ORIGIN}/launch-week/13`
|
||||
|
||||
export const TWITTER_USER_NAME = 'supabase'
|
||||
export const LW7_DATE = 'April 10th 2023'
|
||||
@@ -58,4 +58,17 @@ export const TWEET_TEXT =
|
||||
'Launch Week 13 is just around the corner at @supabase. \nClaim your ticket and stay tuned for all the announcements! \n#launchweek'
|
||||
export const TWEET_TEXT_PLATINUM = `Just conquered a platinum @supabase Launch Week 13 ticket. Share twice to get one! \n#launchweek`
|
||||
export const TWEET_TEXT_SECRET = `Found the secret golden ticket for @supabase's Launch Week 13. \nCan you find it? \n#launchweek`
|
||||
|
||||
// todo: update dates
|
||||
export const LW14_DATE = '31 March — 4 April / 7am PT'
|
||||
export const LW14_LAUNCH_DATE = '2025-03-31T07:00:00.000-07:00'
|
||||
export const LW14_LAUNCH_DATE_END = '2025-04-04T23:59:59.000-07:00'
|
||||
export const LW14_TITLE = 'Launch Week 14'
|
||||
export const LW14_TWEET_TEXT =
|
||||
'Launch Week 14 is just around the corner at @supabase. \nClaim your ticket and stay tuned for all the announcements! \n#launchweek'
|
||||
export const LW14_TWEET_TEXT_PLATINUM =
|
||||
'Just conquered a platinum @supabase Launch Week 14 ticket. Share twice to get one! \n#launchweek'
|
||||
export const LW14_TWEET_TEXT_SECRET = `Found the secret golden ticket for @supabase's Launch Week 14. \nCan you find it? \n#launchweek`
|
||||
export const LW14_URL = `${SITE_ORIGIN}/launch-week`
|
||||
|
||||
export const SITE_NAME = 'Supabase'
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"@vercel/og": "^0.6.2",
|
||||
"ai-commands": "workspace:*",
|
||||
"animejs": "^3.2.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"classnames": "^2.3.1",
|
||||
"clsx": "^1.2.1",
|
||||
"cobe": "^0.6.2",
|
||||
|
||||
@@ -24,6 +24,7 @@ import MetaFaviconsPagesRouter, {
|
||||
import { WwwCommandMenu } from '~/components/CommandMenu'
|
||||
import { API_URL, APP_NAME, DEFAULT_META_DESCRIPTION } from '~/lib/constants'
|
||||
import useDarkLaunchWeeks from '../hooks/useDarkLaunchWeeks'
|
||||
import { LW14Announcement } from 'ui-patterns/Banners/LW14Announcement'
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
const router = useRouter()
|
||||
@@ -93,6 +94,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<CommandProvider>
|
||||
<LW14Announcement />
|
||||
<SonnerToaster position="top-right" />
|
||||
<Component {...pageProps} />
|
||||
<WwwCommandMenu />
|
||||
|
||||
132
apps/www/pages/launch-week/13/index.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
// import { GetServerSideProps } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import { Session } from '@supabase/supabase-js'
|
||||
import { LW13_DATE, LW13_TITLE, LW_URL, SITE_ORIGIN } from '~/lib/constants'
|
||||
import supabase from '~/lib/supabase'
|
||||
|
||||
import DefaultLayout from '~/components/Layouts/Default'
|
||||
import { TicketState, ConfDataContext, UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
|
||||
import SectionContainer from '~/components/Layouts/SectionContainer'
|
||||
// import { Meetup } from '~/components/LaunchWeek/13/LWMeetups'
|
||||
import LWStickyNav from '~/components/LaunchWeek/13/Releases/LWStickyNav'
|
||||
import LWHeader from '~/components/LaunchWeek/13/Releases/LWHeader'
|
||||
import MainStage from '~/components/LaunchWeek/13/Releases/MainStage'
|
||||
|
||||
const BuildStage = dynamic(() => import('~/components/LaunchWeek/13/Releases/BuildStage'))
|
||||
const CTABanner = dynamic(() => import('~/components/CTABanner'))
|
||||
const TicketingFlow = dynamic(() => import('~/components/LaunchWeek/13/Ticket/TicketingFlow'))
|
||||
// const LW13Meetups = dynamic(
|
||||
// () => import('~/components/LaunchWeek/13/LWMeetups')
|
||||
// )
|
||||
|
||||
export default function LaunchWeekIndex() {
|
||||
const { query } = useRouter()
|
||||
|
||||
const TITLE = `${LW13_TITLE} | ${LW13_DATE}`
|
||||
const DESCRIPTION = 'Join us for a week of announcing new features, every day at 7 AM PT.'
|
||||
const OG_IMAGE = `${SITE_ORIGIN}/images/launchweek/12/lw13-og.png?lw=12`
|
||||
|
||||
const ticketNumber = query.ticketNumber?.toString()
|
||||
const [session, setSession] = useState<Session | null>(null)
|
||||
const [showCustomizationForm, setShowCustomizationForm] = useState<boolean>(false)
|
||||
|
||||
const defaultUserData = {
|
||||
id: query.id?.toString(),
|
||||
ticket_number: ticketNumber ? parseInt(ticketNumber, 10) : undefined,
|
||||
name: query.name?.toString(),
|
||||
username: query.username?.toString(),
|
||||
platinum: !!query.platinum,
|
||||
}
|
||||
|
||||
const [userData, setUserData] = useState<UserData>(defaultUserData)
|
||||
const [ticketState, setTicketState] = useState<TicketState>('loading')
|
||||
|
||||
useEffect(() => {
|
||||
if (supabase) {
|
||||
supabase.auth.getSession().then(({ data: { session } }) => setSession(session))
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session)
|
||||
})
|
||||
|
||||
return () => subscription.unsubscribe()
|
||||
}
|
||||
}, [supabase])
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.user) {
|
||||
if (userData?.id) {
|
||||
return setTicketState('ticket')
|
||||
}
|
||||
return setTicketState('loading')
|
||||
}
|
||||
if (!session) return setTicketState('registration')
|
||||
}, [session, userData])
|
||||
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
title={TITLE}
|
||||
description={DESCRIPTION}
|
||||
openGraph={{
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
url: LW_URL,
|
||||
images: [
|
||||
{
|
||||
url: OG_IMAGE,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<ConfDataContext.Provider
|
||||
value={{
|
||||
supabase,
|
||||
session,
|
||||
userData,
|
||||
setUserData,
|
||||
ticketState,
|
||||
setTicketState,
|
||||
showCustomizationForm,
|
||||
setShowCustomizationForm,
|
||||
}}
|
||||
>
|
||||
<DefaultLayout>
|
||||
<LWStickyNav />
|
||||
<LWHeader />
|
||||
<MainStage className="relative z-10" />
|
||||
<BuildStage />
|
||||
{/* <SectionContainer id="meetups" className="scroll-mt-[60px] lw-nav-anchor">
|
||||
<LW13Meetups meetups={meetups} />
|
||||
</SectionContainer> */}
|
||||
<SectionContainer
|
||||
className="relative !max-w-none border-t border-muted !py-8 scroll-mt-[60px] lw-nav-anchor overflow-hidden"
|
||||
id="ticket"
|
||||
>
|
||||
<TicketingFlow />
|
||||
</SectionContainer>
|
||||
<CTABanner />
|
||||
</DefaultLayout>
|
||||
</ConfDataContext.Provider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// export const getServerSideProps: GetServerSideProps = async () => {
|
||||
// const { data: meetups } = await supabase!
|
||||
// .from('meetups')
|
||||
// .select('*')
|
||||
// .eq('launch_week', 'lw13')
|
||||
// .neq('is_published', false)
|
||||
// .order('start_at')
|
||||
|
||||
// return {
|
||||
// props: {
|
||||
// meetups,
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
@@ -1,38 +1,17 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
// import { GetServerSideProps } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import { Session } from '@supabase/supabase-js'
|
||||
import { LW13_DATE, LW13_TITLE, LW_URL, SITE_ORIGIN } from '~/lib/constants'
|
||||
import supabase from '~/lib/supabase'
|
||||
|
||||
import { LW14_DATE, LW14_TITLE, LW14_URL, SITE_ORIGIN } from '~/lib/constants'
|
||||
import { LwView } from '~/components/LaunchWeek/14/LwView'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Lw14ConfDataProvider } from '~/components/LaunchWeek/14/hooks/use-conf-data'
|
||||
import DefaultLayout from '~/components/Layouts/Default'
|
||||
import { TicketState, ConfDataContext, UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
|
||||
import SectionContainer from '~/components/Layouts/SectionContainer'
|
||||
// import { Meetup } from '~/components/LaunchWeek/13/LWMeetups'
|
||||
import LWStickyNav from '~/components/LaunchWeek/13/Releases/LWStickyNav'
|
||||
import LWHeader from '~/components/LaunchWeek/13/Releases/LWHeader'
|
||||
import MainStage from '~/components/LaunchWeek/13/Releases/MainStage'
|
||||
|
||||
const BuildStage = dynamic(() => import('~/components/LaunchWeek/13/Releases/BuildStage'))
|
||||
const CTABanner = dynamic(() => import('~/components/CTABanner'))
|
||||
const TicketingFlow = dynamic(() => import('~/components/LaunchWeek/13/Ticket/TicketingFlow'))
|
||||
// const LW13Meetups = dynamic(
|
||||
// () => import('~/components/LaunchWeek/13/LWMeetups')
|
||||
// )
|
||||
|
||||
export default function LaunchWeekIndex() {
|
||||
const { query } = useRouter()
|
||||
|
||||
const TITLE = `${LW13_TITLE} | ${LW13_DATE}`
|
||||
const Lw14Page = () => {
|
||||
const TITLE = `${LW14_TITLE} | ${LW14_DATE}`
|
||||
const DESCRIPTION = 'Join us for a week of announcing new features, every day at 7 AM PT.'
|
||||
const OG_IMAGE = `${SITE_ORIGIN}/images/launchweek/12/lw13-og.png?lw=12`
|
||||
const OG_IMAGE = `${SITE_ORIGIN}/images/launchweek/14/lw14-og.png?lw=14`
|
||||
|
||||
const { query } = useRouter()
|
||||
const ticketNumber = query.ticketNumber?.toString()
|
||||
const [session, setSession] = useState<Session | null>(null)
|
||||
const [showCustomizationForm, setShowCustomizationForm] = useState<boolean>(false)
|
||||
|
||||
const defaultUserData = {
|
||||
id: query.id?.toString(),
|
||||
ticket_number: ticketNumber ? parseInt(ticketNumber, 10) : undefined,
|
||||
@@ -41,32 +20,6 @@ export default function LaunchWeekIndex() {
|
||||
platinum: !!query.platinum,
|
||||
}
|
||||
|
||||
const [userData, setUserData] = useState<UserData>(defaultUserData)
|
||||
const [ticketState, setTicketState] = useState<TicketState>('loading')
|
||||
|
||||
useEffect(() => {
|
||||
if (supabase) {
|
||||
supabase.auth.getSession().then(({ data: { session } }) => setSession(session))
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session)
|
||||
})
|
||||
|
||||
return () => subscription.unsubscribe()
|
||||
}
|
||||
}, [supabase])
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.user) {
|
||||
if (userData?.id) {
|
||||
return setTicketState('ticket')
|
||||
}
|
||||
return setTicketState('loading')
|
||||
}
|
||||
if (!session) return setTicketState('registration')
|
||||
}, [session, userData])
|
||||
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
@@ -75,7 +28,7 @@ export default function LaunchWeekIndex() {
|
||||
openGraph={{
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
url: LW_URL,
|
||||
url: LW14_URL,
|
||||
images: [
|
||||
{
|
||||
url: OG_IMAGE,
|
||||
@@ -83,50 +36,14 @@ export default function LaunchWeekIndex() {
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<ConfDataContext.Provider
|
||||
value={{
|
||||
supabase,
|
||||
session,
|
||||
userData,
|
||||
setUserData,
|
||||
ticketState,
|
||||
setTicketState,
|
||||
showCustomizationForm,
|
||||
setShowCustomizationForm,
|
||||
}}
|
||||
>
|
||||
<DefaultLayout>
|
||||
<LWStickyNav />
|
||||
<LWHeader />
|
||||
<MainStage className="relative z-10" />
|
||||
<BuildStage />
|
||||
{/* <SectionContainer id="meetups" className="scroll-mt-[60px] lw-nav-anchor">
|
||||
<LW13Meetups meetups={meetups} />
|
||||
</SectionContainer> */}
|
||||
<SectionContainer
|
||||
className="relative !max-w-none border-t border-muted !py-8 scroll-mt-[60px] lw-nav-anchor overflow-hidden"
|
||||
id="ticket"
|
||||
>
|
||||
<TicketingFlow />
|
||||
</SectionContainer>
|
||||
<CTABanner />
|
||||
|
||||
<Lw14ConfDataProvider initState={{ userTicketData: defaultUserData, partymodeStatus: 'on' }}>
|
||||
<DefaultLayout className='font-["Departure_Mono"] lg:py-32 border-b pb-0'>
|
||||
<LwView />
|
||||
</DefaultLayout>
|
||||
</ConfDataContext.Provider>
|
||||
</Lw14ConfDataProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// export const getServerSideProps: GetServerSideProps = async () => {
|
||||
// const { data: meetups } = await supabase!
|
||||
// .from('meetups')
|
||||
// .select('*')
|
||||
// .eq('launch_week', 'lw13')
|
||||
// .neq('is_published', false)
|
||||
// .order('start_at')
|
||||
|
||||
// return {
|
||||
// props: {
|
||||
// meetups,
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
export default Lw14Page
|
||||
|
||||
@@ -1,45 +1,27 @@
|
||||
import { useState } from 'react'
|
||||
import { GetStaticProps, GetStaticPaths } from 'next'
|
||||
import dayjs from 'dayjs'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import Link from 'next/link'
|
||||
import dayjs from 'dayjs'
|
||||
import Error from 'next/error'
|
||||
import { createClient, Session } from '@supabase/supabase-js'
|
||||
import { Button } from 'ui'
|
||||
import { LW_URL, SITE_ORIGIN } from '~/lib/constants'
|
||||
import supabase from '~/lib/supabase'
|
||||
import { Database } from '~/lib/database.types'
|
||||
import { AnimatePresence, m, LazyMotion, domAnimation } from 'framer-motion'
|
||||
import { DEFAULT_TRANSITION, INITIAL_BOTTOM, getAnimation } from '~/lib/animations'
|
||||
|
||||
import { GetStaticPaths, GetStaticProps } from 'next'
|
||||
import { LW14_DATE, LW14_TITLE, LW14_URL, SITE_ORIGIN } from '~/lib/constants'
|
||||
import { LwView } from '~/components/LaunchWeek/14/LwView'
|
||||
import {
|
||||
Lw14ConfDataProvider,
|
||||
UserTicketData,
|
||||
} from '~/components/LaunchWeek/14/hooks/use-conf-data'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import DefaultLayout from '~/components/Layouts/Default'
|
||||
import SectionContainer from '~/components/Layouts/SectionContainer'
|
||||
import { TicketState, ConfDataContext, UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
|
||||
import CanvasSingleMode from '~/components/LaunchWeek/13/Multiplayer/CanvasSingleMode'
|
||||
import ThreeTicketCanvas from '~/components/LaunchWeek/13/ThreeTicketCanvas'
|
||||
import { Tunnel } from '~/components/LaunchWeek/14/Tunnel'
|
||||
|
||||
interface Props {
|
||||
user: UserData
|
||||
users: UserData[]
|
||||
user: UserTicketData
|
||||
ogImageUrl: string
|
||||
}
|
||||
|
||||
export default function UsernamePage({ user, ogImageUrl }: Props) {
|
||||
const { username, name, platinum, secret } = user
|
||||
|
||||
const ticketType = secret ? 'secret' : platinum ? 'platinum' : 'regular'
|
||||
|
||||
const DISPLAY_NAME = name || username
|
||||
const TITLE = `${DISPLAY_NAME ? DISPLAY_NAME.split(' ')[0] + '’s' : 'Get your'} Launch Week Ticket`
|
||||
const DESCRIPTION = `Claim your Supabase Launch Week 13 ticket for a chance to win supa swag.`
|
||||
|
||||
const [session] = useState<Session | null>(null)
|
||||
const [ticketState, setTicketState] = useState<TicketState>('ticket')
|
||||
|
||||
const transition = DEFAULT_TRANSITION
|
||||
const initial = INITIAL_BOTTOM
|
||||
const animate = getAnimation({ duration: 1 })
|
||||
const exit = { opacity: 0, transition: { ...transition, duration: 0.2 } }
|
||||
const Lw14Page = ({ user, ogImageUrl }: Props) => {
|
||||
const username = user?.username
|
||||
const TITLE = `${LW14_TITLE} | ${LW14_DATE}`
|
||||
const DESCRIPTION = 'Join us for a week of announcing new features, every day at 7 AM PT.'
|
||||
const PAGE_URL = `${LW14_URL}/tickets/${username}`
|
||||
|
||||
if (!username) {
|
||||
return <Error statusCode={404} />
|
||||
@@ -49,101 +31,54 @@ export default function UsernamePage({ user, ogImageUrl }: Props) {
|
||||
<>
|
||||
<NextSeo
|
||||
title={TITLE}
|
||||
description={DESCRIPTION}
|
||||
openGraph={{
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
url: `${LW_URL}/tickets/${username}`,
|
||||
url: PAGE_URL,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
height: 628,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<ConfDataContext.Provider
|
||||
value={{
|
||||
supabase,
|
||||
session,
|
||||
userData: user,
|
||||
setUserData: () => null,
|
||||
ticketState,
|
||||
setTicketState,
|
||||
}}
|
||||
>
|
||||
<DefaultLayout className="lg:h-[calc(100dvh-65px)] min-h-[calc(100vh)] md:min-h-[calc(100vh-65px)] overflow-hidden">
|
||||
<SectionContainer className="relative h-full flex-1 pt-4 md:pt-4 pointer-events-none">
|
||||
<div className="relative z-10 flex h-full">
|
||||
<LazyMotion features={domAnimation}>
|
||||
<AnimatePresence mode="wait" key={ticketState}>
|
||||
<m.div
|
||||
key="ticket"
|
||||
initial={initial}
|
||||
animate={animate}
|
||||
exit={exit}
|
||||
className="w-full flex-1 h-full flex flex-col lg:flex-row items-center lg:justify-center lg:items-center gap-8 md:gap-10 lg:gap-32 text-foreground text-center md:text-left"
|
||||
>
|
||||
<div className="w-full lg:w-full h-full mt-3 md:mt-6 lg:mt-0 max-w-lg flex flex-col items-center justify-center gap-3"></div>
|
||||
<div className="lg:h-full w-full max-w-lg gap-8 flex flex-col items-center justify-center lg:items-start lg:justify-center text-center lg:text-left">
|
||||
<div className="flex flex-col items-center justify-center lg:justify-start lg:items-start gap-2 text-foreground text-center md:text-left max-w-sm">
|
||||
<h1 className="text-foreground text-2xl">
|
||||
Join {DISPLAY_NAME?.split(' ')[0]} <br /> for Launch Week 13
|
||||
</h1>
|
||||
<span className="text-foreground-lighter">
|
||||
Claim your own ticket for a chance to win limited swag and to follow all
|
||||
the announcements.
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<Button type="primary" asChild size="small">
|
||||
<Link
|
||||
href={`${LW_URL}${username ? '?referral=' + username : ''}`}
|
||||
className="pointer-events-auto"
|
||||
>
|
||||
Claim your ticket
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</m.div>
|
||||
</AnimatePresence>
|
||||
</LazyMotion>
|
||||
</div>
|
||||
</SectionContainer>
|
||||
<CanvasSingleMode />
|
||||
<ThreeTicketCanvas
|
||||
username={DISPLAY_NAME ?? ''}
|
||||
className="relative -mt-40 -mb-20 lg:my-0 lg:absolute"
|
||||
ticketPosition="left"
|
||||
ticketType={ticketType}
|
||||
sharePage={true}
|
||||
/>
|
||||
|
||||
<Lw14ConfDataProvider initState={{ partymodeStatus: 'on' }}>
|
||||
<DefaultLayout className='font-["Departure_Mono"] lg:pt-32 border-b pb-0 md:pb-16 lg:!pb-[230px]'>
|
||||
<LwView />
|
||||
</DefaultLayout>
|
||||
</ConfDataContext.Provider>
|
||||
</Lw14ConfDataProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Lw14Page
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ({ params }) => {
|
||||
const username = params?.username?.toString() || null
|
||||
let user
|
||||
|
||||
const supabaseAdmin = createClient<Database>(
|
||||
const supabaseAdmin = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.LIVE_SUPABASE_COM_SERVICE_ROLE_KEY!
|
||||
)
|
||||
|
||||
// fetch the normal ticket
|
||||
// stores the og images in supabase storage
|
||||
fetch(`${SITE_ORIGIN}/api-v2/ticket-og?username=${encodeURIComponent(username ?? '')}`)
|
||||
fetch(
|
||||
// @ts-ignore
|
||||
`${SITE_ORIGIN}/api-v2/ticket-og?username=${encodeURIComponent(username ?? '')}`
|
||||
)
|
||||
|
||||
// fetch a specific user
|
||||
if (username) {
|
||||
const { data } = await supabaseAdmin!
|
||||
const { data, error } = await supabaseAdmin!
|
||||
.from('tickets_view')
|
||||
.select('name, username, ticket_number, metadata, platinum, secret, role, company, location')
|
||||
.eq('launch_week', 'lw13')
|
||||
.eq('launch_week', 'lw14')
|
||||
.eq('username', username)
|
||||
.single()
|
||||
|
||||
@@ -164,14 +99,11 @@ export const getStaticProps: GetStaticProps = async ({ params }) => {
|
||||
}
|
||||
|
||||
const ticketType = user?.secret ? 'secret' : user?.platinum ? 'platinum' : 'regular'
|
||||
const ogImageUrl = `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/images/launch-week/lw13/og/${ticketType}/${username}.png?t=${dayjs(new Date()).format('DHHmmss')}`
|
||||
const ogImageUrl = `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/images/launch-week/lw14/og/${ticketType}/${username}.png?t=${dayjs(new Date()).format('DHHmmss')}`
|
||||
|
||||
return {
|
||||
props: {
|
||||
user: {
|
||||
...user,
|
||||
username,
|
||||
},
|
||||
user,
|
||||
ogImageUrl,
|
||||
key: username,
|
||||
},
|
||||
|
||||
BIN
apps/www/public/fonts/launchweek/14/DepartureMono-Regular.otf
Normal file
BIN
apps/www/public/fonts/launchweek/14/DepartureMono-Regular.woff
Normal file
BIN
apps/www/public/fonts/launchweek/14/DepartureMono-Regular.woff2
Normal file
BIN
apps/www/public/fonts/launchweek/14/Nippo-Regular.otf
Normal file
BIN
apps/www/public/fonts/launchweek/14/Nippo-Variable.ttf
Normal file
BIN
apps/www/public/fonts/launchweek/14/Nippo-Variable.woff
Normal file
BIN
apps/www/public/fonts/launchweek/14/Nippo-Variable.woff2
Normal file
1
apps/www/public/images/launchweek/14/Arial_Regular.json
Normal file
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 62 KiB |
BIN
apps/www/public/images/launchweek/14/logo-pixel-small-dark.png
Normal file
|
After Width: | Height: | Size: 296 B |
BIN
apps/www/public/images/launchweek/14/logo-pixel-small-light.png
Normal file
|
After Width: | Height: | Size: 346 B |
BIN
apps/www/public/images/launchweek/14/og-14-platinum.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
apps/www/public/images/launchweek/14/og-14-regular.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
apps/www/public/images/launchweek/14/og-14-secret.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
apps/www/public/images/launchweek/14/ticket-model.glb
Normal file
36
packages/ui-patterns/Banners/Countdown.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import Countdown from 'react-countdown'
|
||||
import { CountdownWidget } from '../CountdownWidget'
|
||||
|
||||
const CountdownComponent = ({
|
||||
date,
|
||||
showCard = true,
|
||||
}: {
|
||||
date: string | number | Date
|
||||
showCard?: boolean
|
||||
}) => {
|
||||
if (!date) return null
|
||||
|
||||
const renderer = ({ days, hours, minutes, seconds, completed }: any) => {
|
||||
if (completed) {
|
||||
// Render a completed state
|
||||
return null
|
||||
} else {
|
||||
// Render countdown
|
||||
return (
|
||||
<CountdownWidget
|
||||
days={days}
|
||||
hours={hours}
|
||||
minutes={minutes}
|
||||
seconds={seconds}
|
||||
showCard={showCard}
|
||||
className="text-emerald-400 [text-shadow:0_0_15px_rgba(52,211,153,0.8)]"
|
||||
dividerClassName="text-emerald-400 [text-shadow:0_0_15px_rgba(52,211,153,0.8)]"
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return <Countdown date={new Date(date)} renderer={renderer} />
|
||||
}
|
||||
|
||||
export default CountdownComponent
|
||||
10
packages/ui-patterns/Banners/LW14Announcement.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Announcement } from 'ui/src/layout/banners'
|
||||
import LW14Banner from './LW14Banner'
|
||||
|
||||
export const LW14Announcement = () => {
|
||||
return (
|
||||
<Announcement show={true} announcementKey="announcement_lw14_countdown">
|
||||
<LW14Banner />
|
||||
</Announcement>
|
||||
)
|
||||
}
|
||||
@@ -2,22 +2,21 @@
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { Button } from '../../../components/Button/Button'
|
||||
import { cn } from '../../../lib/utils/cn'
|
||||
import announcement from '../data/Announcement.json'
|
||||
import announcement from './data.json'
|
||||
import Image from 'next/image'
|
||||
import { useTheme } from 'next-themes'
|
||||
// import Countdown from './Countdown'
|
||||
import { Button } from 'ui/src/components/Button'
|
||||
import { cn } from 'ui'
|
||||
|
||||
const LW13BGDark =
|
||||
const LW14BGDark =
|
||||
'https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/images/launch-week/lw13/assets/lw13-banner-dark.png?t=2024-11-22T23%3A10%3A37.646Z'
|
||||
const LW13BGLight =
|
||||
const LW14BGLight =
|
||||
'https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/images/launch-week/lw13/assets/lw13-banner-light.png?t=2024-11-22T23%3A10%3A37.646Z'
|
||||
|
||||
export function LW13CountdownBanner() {
|
||||
export function LW14Banner() {
|
||||
const pathname = usePathname()
|
||||
const { resolvedTheme } = useTheme()
|
||||
const bgImage = resolvedTheme?.includes('dark') ? LW13BGDark : LW13BGLight
|
||||
const bgImage = resolvedTheme?.includes('dark') ? LW14BGDark : LW14BGLight
|
||||
const isHomePage = pathname === '/'
|
||||
const isLaunchWeekPage =
|
||||
pathname === '/launch-week' || pathname?.includes('/launch-week/tickets/')
|
||||
@@ -27,7 +26,7 @@ export function LW13CountdownBanner() {
|
||||
if (isLaunchWeekPage || isHomePage) return null
|
||||
|
||||
return (
|
||||
<div className="relative w-full p-2 flex items-center group justify-center text-foreground bg-alternative border-b border-muted transition-colors overflow-hidden">
|
||||
<div className='relative font-["Departure_Mono"] w-full p-2 flex items-center group justify-center text-foreground bg-alternative border-b border-muted transition-colors overflow-hidden'>
|
||||
<div className="relative z-10 flex items-center justify-center">
|
||||
<div
|
||||
className={cn(
|
||||
@@ -35,7 +34,7 @@ export function LW13CountdownBanner() {
|
||||
isLaunchWeekSection && '!justify-center'
|
||||
)}
|
||||
>
|
||||
<p className="flex gap-1.5 items-center">{announcement.text}</p>
|
||||
<p className="flex gap-1.5 items-center ">{announcement.text}</p>
|
||||
<div className="hidden sm:block text-foreground-lighter">A week of new features</div>
|
||||
<Button size="tiny" type="default" className="px-2 !leading-none text-xs" asChild>
|
||||
<Link href={announcement.link}>{announcement.cta}</Link>
|
||||
@@ -55,4 +54,4 @@ export function LW13CountdownBanner() {
|
||||
)
|
||||
}
|
||||
|
||||
export default LW13CountdownBanner
|
||||
export default LW14Banner
|
||||
6
packages/ui-patterns/Banners/data.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"text": "Launch Week 14",
|
||||
"launchDate": "2025-03-31T07:00:00.000-08:00",
|
||||
"link": "/launch-week",
|
||||
"cta": "Learn more"
|
||||
}
|
||||
@@ -8,6 +8,7 @@ interface CountdownWidgetProps {
|
||||
seconds?: string
|
||||
showCard?: boolean
|
||||
className?: string
|
||||
dividerClassName?: string
|
||||
size?: 'small' | 'large'
|
||||
}
|
||||
|
||||
@@ -18,11 +19,20 @@ export function CountdownWidget({
|
||||
seconds,
|
||||
showCard = true,
|
||||
className,
|
||||
dividerClassName,
|
||||
size,
|
||||
}: CountdownWidgetProps) {
|
||||
const isLarge = size === 'large'
|
||||
const Colon = () => (
|
||||
<span className={cn('text-xs mx-px text-foreground-lighter', isLarge && 'text-lg')}>:</span>
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs mx-px text-foreground-lighter',
|
||||
isLarge && 'text-lg',
|
||||
dividerClassName
|
||||
)}
|
||||
>
|
||||
:
|
||||
</span>
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,17 +7,15 @@ import { cn } from 'ui/src/lib/utils/cn'
|
||||
import { Button } from 'ui/src/components/Button/Button'
|
||||
import { LOCAL_STORAGE_KEYS } from 'common'
|
||||
import { useTheme } from 'next-themes'
|
||||
import announcement from 'ui/src/layout/banners/data/Announcement.json'
|
||||
import announcement from '../Banners/data.json'
|
||||
import CountdownComponent from '../Banners/Countdown'
|
||||
import './styles.css'
|
||||
|
||||
const LW13BGDark =
|
||||
'https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/images/launch-week/lw13/assets/lw13-bg-dark.png?t=2024-11-22T23%3A10%3A37.646Z'
|
||||
const LW13BGLight =
|
||||
'https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/images/launch-week/lw13/assets/lw13-bg-light.png?t=2024-11-22T23%3A10%3A37.646Z'
|
||||
const LW14BG = `/docs/img/launchweek/14/promo-banner-bg.png`
|
||||
|
||||
const PromoToast = () => {
|
||||
const [visible, setVisible] = useState(false)
|
||||
const { resolvedTheme } = useTheme()
|
||||
const bgImage = resolvedTheme?.includes('dark') ? LW13BGDark : LW13BGLight
|
||||
|
||||
useEffect(() => {
|
||||
const shouldHide =
|
||||
@@ -43,20 +41,19 @@ const PromoToast = () => {
|
||||
visible && 'opacity-100 translate-y-0'
|
||||
)}
|
||||
>
|
||||
<div className="relative z-10 text-foreground-lighter leading-3 flex flex-col font-mono uppercase tracking-wide w-full text-xs">
|
||||
<div className="text-foreground uppercase tracking-wider text-lg -mb-1">Launch Week 13</div>
|
||||
<p className="text-foreground-lighter uppercase tracking-wider text-xl md:text-lg leading-snug">
|
||||
2—6 Dec
|
||||
<div className="relative z-10 text-foreground-lighter uppercase flex flex-col text-sm w-full font-mono mb-2">
|
||||
<span className="mb-1">31 APR - 04 MAR / 7AM PT</span>
|
||||
|
||||
<p className="relative z-10 text-foreground flex flex-col text-xl w-full leading-7 font-['Departure_Mono']">
|
||||
Launch Week 14
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative z-10 text-foreground-lighter flex flex-col text-sm uppercase font-mono tracking-widest w-full -mt-1">
|
||||
A week of new features
|
||||
<CountdownComponent date={new Date(announcement.launchDate)} showCard={false} />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex items-center space-x-2 mt-2">
|
||||
<div className="relative z-10 flex items-center space-x-2">
|
||||
<Button asChild type="secondary">
|
||||
<Link target="_blank" rel="noreferrer" href={`https://supabase.com${announcement.link}`}>
|
||||
Learn more
|
||||
Claim ticket
|
||||
</Link>
|
||||
</Button>
|
||||
<Button type="default" onClick={handleHide}>
|
||||
@@ -64,7 +61,7 @@ const PromoToast = () => {
|
||||
</Button>
|
||||
</div>
|
||||
<Image
|
||||
src={bgImage}
|
||||
src={LW14BG}
|
||||
alt=""
|
||||
fill
|
||||
sizes="100%"
|
||||
|
||||
16
packages/ui-patterns/PromoToast/styles.css
Normal file
@@ -0,0 +1,16 @@
|
||||
/* Font face definitions for Launch Week 14 */
|
||||
@font-face {
|
||||
font-family: 'Departure Mono';
|
||||
src: url('/fonts/launchweek/14/DepartureMono-Regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nippo-Variable';
|
||||
src: url('/fonts/launchweek/14/Nippo-Variable.woff2') format('woff2');
|
||||
font-weight: 400 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
"monaco-editor": "*",
|
||||
"next-themes": "*",
|
||||
"openai": "^4.20.1",
|
||||
"react-countdown": "^2.3.5",
|
||||
"react-error-boundary": "^4.0.12",
|
||||
"react-hook-form": "^7.45.0",
|
||||
"react-intersection-observer": "^9.8.2",
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useEffect, useState } from 'react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { cn } from '../../lib/utils/cn'
|
||||
import _announcement from './data/Announcement.json'
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
export interface AnnouncementProps {
|
||||
@@ -16,12 +15,11 @@ export interface AnnouncementProps {
|
||||
badge?: string
|
||||
}
|
||||
|
||||
const announcement = _announcement as AnnouncementProps
|
||||
|
||||
interface AnnouncementComponentProps {
|
||||
show?: boolean
|
||||
dismissable?: boolean
|
||||
className?: string
|
||||
announcementKey: `announcement_${string}`
|
||||
}
|
||||
|
||||
const Announcement = ({
|
||||
@@ -29,6 +27,7 @@ const Announcement = ({
|
||||
dismissable = true,
|
||||
className,
|
||||
children,
|
||||
announcementKey,
|
||||
}: PropsWithChildren<AnnouncementComponentProps>) => {
|
||||
const [hidden, setHidden] = useState(true)
|
||||
|
||||
@@ -36,19 +35,19 @@ const Announcement = ({
|
||||
const isLaunchWeekSection = pathname?.includes('launch-week') ?? false
|
||||
|
||||
// override to hide announcement
|
||||
if (!show || !announcement.show) return null
|
||||
if (!show) return null
|
||||
|
||||
// construct the key for the announcement, based on the title text
|
||||
const announcementKey = 'announcement_' + announcement.text.replace(/ /g, '')
|
||||
const announcementKeyNoSpaces = announcementKey.replace(/ /g, '')
|
||||
|
||||
// window.localStorage is kept inside useEffect
|
||||
// to prevent error
|
||||
useEffect(function () {
|
||||
if (window.localStorage.getItem(announcementKey) === 'hidden') {
|
||||
if (window.localStorage.getItem(announcementKeyNoSpaces) === 'hidden') {
|
||||
setHidden(true)
|
||||
}
|
||||
|
||||
if (!window.localStorage.getItem(announcementKey)) {
|
||||
if (!window.localStorage.getItem(announcementKeyNoSpaces)) {
|
||||
setHidden(false)
|
||||
}
|
||||
}, [])
|
||||
@@ -56,7 +55,7 @@ const Announcement = ({
|
||||
function handleClose(event: any) {
|
||||
event.stopPropagation()
|
||||
|
||||
window.localStorage.setItem(announcementKey, 'hidden')
|
||||
window.localStorage.setItem(announcementKeyNoSpaces, 'hidden')
|
||||
return setHidden(true)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"show": true,
|
||||
"text": "Launch Week 13",
|
||||
"launchDate": "2024-12-02T07:00:00.000-08:00",
|
||||
"link": "/launch-week",
|
||||
"cta": "Learn more"
|
||||
}
|
||||
8
pnpm-lock.yaml
generated
@@ -1206,6 +1206,9 @@ importers:
|
||||
animejs:
|
||||
specifier: ^3.2.2
|
||||
version: 3.2.2
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
classnames:
|
||||
specifier: ^2.3.1
|
||||
version: 2.3.2
|
||||
@@ -1958,6 +1961,9 @@ importers:
|
||||
react:
|
||||
specifier: '*'
|
||||
version: 18.2.0
|
||||
react-countdown:
|
||||
specifier: ^2.3.5
|
||||
version: 2.3.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react-dom:
|
||||
specifier: '*'
|
||||
version: 18.2.0(react@18.2.0)
|
||||
@@ -22514,7 +22520,7 @@ snapshots:
|
||||
|
||||
browserslist@4.23.3:
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001651
|
||||
caniuse-lite: 1.0.30001695
|
||||
electron-to-chromium: 1.5.13
|
||||
node-releases: 2.0.18
|
||||
update-browserslist-db: 1.1.0(browserslist@4.23.3)
|
||||
|
||||
@@ -4,3 +4,5 @@ values
|
||||
('New York', 'USA', 'lw12', now(), true),
|
||||
('London', 'UK', 'lw12', now(), true),
|
||||
('Singapore', 'Singapore', 'lw12', now(), true);
|
||||
|
||||
insert into public.launch_weeks (id) values ('lw14');
|
||||
|
||||