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>
No files matched your search
@@ -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>
|
||||
|
||||
|
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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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" />
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 78 KiB |
@@ -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;
|
||||
}
|
||||
`,
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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));
|
||||
}
|
||||
`
|
||||
@@ -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 },
|
||||
},
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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})`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 />
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"glyphs": {"0": {"ha": 833, "x_min": 99, "x_max": 735, "o": "m 417 -17 q 183 103 268 -17 q 99 446 99 224 q 183 786 99 669 q 417 903 268 903 q 650 786 565 903 q 735 446 735 669 q 650 103 735 224 q 417 -17 565 -17 m 417 75 q 569 165 510 75 q 628 446 628 256 q 613 610 628 542 q 569 724 597 679 q 501 790 540 769 q 417 811 463 811 q 332 790 371 811 q 265 724 293 769 q 221 610 236 679 q 206 446 206 542 q 265 165 206 256 q 417 75 324 75 m 417 363 q 356 387 382 363 q 331 456 331 411 q 356 523 331 499 q 417 547 382 547 q 477 523 451 547 q 503 456 503 499 q 477 387 503 411 q 417 363 451 363 z "}, "1": {"ha": 833, "x_min": 135, "x_max": 736, "o": "m 135 0 l 135 94 l 390 94 l 390 747 l 192 747 l 192 821 q 324 847 271 832 q 419 886 376 863 l 504 886 l 504 94 l 736 94 l 736 0 l 135 0 z "}, "2": {"ha": 833, "x_min": 96, "x_max": 725, "o": "m 101 0 l 101 68 q 301 249 214 168 q 447 397 388 329 q 536 522 506 464 q 567 636 567 581 q 519 761 567 713 q 374 810 472 810 q 258 778 311 810 q 161 703 204 747 l 96 768 q 224 866 156 829 q 388 903 293 903 q 508 884 454 903 q 599 831 561 865 q 656 749 636 797 q 676 642 676 700 q 647 516 676 579 q 563 385 617 453 q 433 245 508 318 q 268 90 358 172 q 351 96 310 93 q 433 99 393 99 l 725 99 l 725 0 l 101 0 z "}, "3": {"ha": 833, "x_min": 79, "x_max": 719, "o": "m 399 -17 q 206 22 283 -17 q 79 108 128 60 l 138 183 q 244 110 182 142 q 392 78 306 78 q 476 90 438 78 q 544 122 515 101 q 588 173 572 143 q 604 240 604 203 q 587 312 604 279 q 531 368 569 344 q 431 404 492 392 q 282 417 369 417 l 282 504 q 416 517 361 504 q 506 551 471 529 q 556 604 540 574 q 572 669 572 635 q 523 772 572 735 q 392 810 474 810 q 274 785 329 810 q 174 719 218 760 l 113 792 q 239 872 168 840 q 396 903 310 903 q 512 888 458 903 q 605 844 565 872 q 667 774 644 815 q 689 678 689 732 q 636 546 689 597 q 500 467 583 494 l 500 461 q 584 433 544 451 q 654 385 624 414 q 702 319 685 357 q 719 236 719 282 q 694 131 719 178 q 626 51 669 83 q 524 1 582 18 q 399 -17 465 -17 z "}, "4": {"ha": 833, "x_min": 54, "x_max": 757, "o": "m 511 0 l 511 244 l 54 244 l 54 321 l 497 886 l 619 886 l 619 336 l 757 336 l 757 244 l 619 244 l 619 0 l 511 0 m 181 336 l 511 336 l 511 611 q 514 690 513 647 q 518 768 515 732 l 511 768 q 467 703 490 736 q 421 639 443 671 l 181 336 z "}, "5": {"ha": 833, "x_min": 78, "x_max": 725, "o": "m 397 -17 q 290 -6 338 -17 q 203 21 242 4 q 134 59 165 38 q 78 104 103 81 l 135 179 q 182 141 157 160 q 237 108 207 122 q 303 86 267 94 q 385 78 340 78 q 473 92 432 78 q 544 133 514 107 q 592 197 575 160 q 610 281 610 235 q 551 424 610 372 q 392 475 492 475 q 297 461 336 475 q 211 421 258 447 l 150 460 l 179 886 l 681 886 l 681 788 l 281 788 l 257 525 q 333 551 296 542 q 421 561 369 561 q 540 544 485 561 q 637 494 596 528 q 701 408 678 460 q 725 283 725 356 q 698 156 725 211 q 625 62 671 100 q 520 3 579 24 q 397 -17 461 -17 z "}, "6": {"ha": 833, "x_min": 107, "x_max": 742, "o": "m 446 -17 q 312 10 374 -17 q 204 90 250 36 q 133 223 158 143 q 107 410 107 303 q 138 637 107 543 q 219 790 168 731 q 338 876 271 849 q 478 903 406 903 q 620 876 560 903 q 725 808 681 849 l 661 738 q 581 788 626 769 q 485 807 535 807 q 383 788 432 807 q 298 724 335 768 q 238 606 261 679 q 214 426 215 533 q 333 510 268 479 q 465 542 399 542 q 577 524 526 542 q 665 473 628 507 q 722 388 701 439 q 742 268 742 336 q 717 152 742 204 q 653 63 693 100 q 559 4 613 25 q 446 -17 506 -17 m 446 74 q 519 88 485 74 q 579 127 554 101 q 619 188 604 153 q 633 268 633 224 q 585 408 633 360 q 444 456 536 456 q 331 428 392 456 q 218 336 271 400 q 290 140 231 207 q 446 74 349 74 z "}, "7": {"ha": 833, "x_min": 97, "x_max": 739, "o": "m 314 0 q 338 230 319 126 q 390 426 356 333 q 478 607 425 519 q 608 788 532 694 l 97 788 l 97 886 l 739 886 l 739 815 q 594 624 651 715 q 503 437 538 532 q 453 236 468 342 q 433 0 438 131 l 314 0 z "}, "8": {"ha": 833, "x_min": 94, "x_max": 738, "o": "m 419 -17 q 284 1 344 -17 q 182 52 224 19 q 117 129 140 85 q 94 226 94 174 q 111 308 94 272 q 154 374 128 344 q 215 424 181 403 q 285 463 249 446 l 285 468 q 190 553 229 503 q 150 675 150 604 q 170 768 150 726 q 227 840 190 810 q 314 886 264 869 q 424 903 364 903 q 540 885 489 903 q 626 837 590 868 q 680 762 661 806 q 699 667 699 718 q 659 549 699 604 q 564 456 619 494 l 564 450 q 632 411 600 432 q 688 364 664 390 q 724 302 711 338 q 738 219 738 267 q 716 126 738 169 q 654 51 694 83 q 555 1 614 19 q 419 -17 496 -17 m 489 483 q 600 661 600 564 q 554 773 600 728 q 421 818 508 818 q 300 779 346 818 q 254 675 254 740 q 272 606 254 635 q 322 556 290 578 q 397 516 354 533 q 489 483 439 499 m 422 68 q 575 111 522 68 q 628 224 628 154 q 606 301 628 269 q 547 354 585 332 q 459 394 510 376 q 350 431 408 411 q 242 349 285 394 q 199 238 199 304 q 259 115 199 163 q 422 68 319 68 z "}, "9": {"ha": 833, "x_min": 93, "x_max": 728, "o": "m 390 431 q 503 458 443 431 q 617 549 563 486 q 545 746 603 679 q 389 813 488 813 q 315 799 349 813 q 256 759 281 785 q 216 697 231 733 q 201 618 201 661 q 249 478 201 526Line truncated
|
||||
|
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 |
|
After Width: | Height: | Size: 296 B |
|
After Width: | Height: | Size: 346 B |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 29 KiB |
@@ -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
|
||||
@@ -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
|
||||
@@ -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%"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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');
|
||||