Feat/whale init (#28243)

* duplicate page

* whale init

* table background

* prepare

* sync undocumented remote schema from master

* revert remote schema sync

* add lw12 tickets migration file

* ticket init

* set up lw12 ticket layout

* finish ticket layout and customization form

* lw12 ticket og

* ticket styling

* code highlight border

* launch week migrations

* local dev progress

* keep local gh config

* comment section

* comment section

* comment out username.tsx

* remove lw/ticekts temp

* update copy

* fix migration view

* redirect to try

* use misc

* lw12 og

* share

* share correct

* env var

* username page

* update example env

* push new db schema

* trigger deploy

* update ticket og

* ticket themes

* bypass browser

* change env var name

* fix

* fix

* process env

* create client server side

* lw announcements

* promoToast

* animated bg

* update ticket og bg

* minor details

* secret ticket

* social share text

* social share textgst

* flow text

* update og

* update og handler

* Update index.ts

* use functions.invoke and use generic supabase URL

* Update index.ts

* Update handler.tsx

* Update package-lock.json

* Update handler.tsx

* add next api route

* moved to vercel edge function

* set revalidate

* Update route.tsx

* Delete lw-ticket-og.tsx

* Update route.tsx

* Update [username].tsx

* Update [username].tsx

* add more fetches

* Update turbo.json

* ticket themes updated

* copy and layout updated

* Update index.tsx

* Update Ticket.tsx

* Update TicketingFlow.tsx

* update countdown

* updated bg

* small updates

* moat

* Update [username].tsx

* Update 20240723155310_add_lw12_ticketing_schema.sql

* optimistic ticket stuff og generation

* Update index.tsx

* updated layout

* update themes in og

* Update constants.ts

* attr renamed

* Update TicketingFlow.tsx

* Update TicketActions.tsx

* grammar

* moar updates

* Update TicketActions.tsx

* Update TicketActions2.tsx

* Update TicketActions.tsx

* Update Hero.tsx

* Update TicketingFlow.tsx

* remove console logs

---------

Co-authored-by: Francesco Sansalvadore <f.sansalvadore@gmail.com>
This commit is contained in:
Jonathan Summers-Muir
2024-08-01 01:59:01 +08:00
committed by GitHub
parent 0f2f7b99a5
commit 1f1f8a5fa4
158 changed files with 4901 additions and 7403 deletions

View File

@@ -1,6 +1,7 @@
import { CommandMenuProvider } from '@ui-patterns/Cmdk'
import { ThemeProvider } from 'common'
import { PortalToast } from 'ui'
import { PromoToast } from 'ui-patterns'
import { type PropsWithChildren } from 'react'
import SiteLayout from '~/layouts/SiteLayout'
@@ -26,6 +27,7 @@ function GlobalProviders({ children }: PropsWithChildren) {
<div className="flex flex-col">
<SiteLayout>
<PortalToast />
<PromoToast />
{children}
</SiteLayout>
<ThemeSandbox />

View File

@@ -53,6 +53,7 @@ const nextConfig = {
'img.youtube.com',
'archbee-image-uploads.s3.amazonaws.com',
'obuldanrptloktxcffvn.supabase.co',
'xguihxuzqibwxjnimxev.supabase.co',
],
},
// TODO: @next/mdx ^13.0.2 only supports experimental mdxRs flag. next ^13.0.2 will stop warning about this being unsupported.

View File

@@ -8,4 +8,5 @@ NEXT_PUBLIC_REFERENCE_DOCS_URL="https://localhost:3010"
NEXT_PUBLIC_STUDIO_URL="http://localhost:8082"
NEXT_PUBLIC_SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhndWloeHV6cWlid3hqbmlteGV2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NzUwOTQ4MzUsImV4cCI6MTk5MDY3MDgzNX0.0PMlOxtKL4O9GGZuAP_Xl4f-Tut1qOnW4bNEmAtoB8w"
NEXT_PUBLIC_SUPABASE_URL="https://xguihxuzqibwxjnimxev.supabase.co"
SUPABASE_COM_SERVICE_ROLE_KEY="secret"
NEXT_PUBLIC_URL="http://localhost:3000"

View File

@@ -0,0 +1,550 @@
import React from 'react'
import { ImageResponse } from '@vercel/og'
import { createClient } from '@supabase/supabase-js'
import { themes } from '~/components/LaunchWeek/12/Ticket/ticketThemes'
export const runtime = 'edge' // 'nodejs' is the default
export const dynamic = 'force-dynamic' // defaults to auto
export const fetchCache = 'force-no-store'
export const revalidate = 0
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL
const STORAGE_URL = `${SUPABASE_URL}/storage/v1/object/public/images/launch-week/lw12`
// 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 = 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())
// const BUCKET_FOLDER_VERSION = 'v1'
const LW_TABLE = 'tickets'
const LW_MATERIALIZED_VIEW = 'tickets_view'
export async function GET(req: Request, res: Response) {
const url = new URL(req.url)
console.log(process.env.NEXT_PUBLIC_SUPABASE_URL)
const username = url.searchParams.get('username') ?? url.searchParams.get('amp;username')
const assumePlatinum = url.searchParams.get('platinum') ?? url.searchParams.get('amp;platinum')
const userAgent = req.headers.get('user-agent')
try {
if (!username) throw new Error('missing username param')
const supabaseAdminClient = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL as string,
process.env.LIVE_SUPABASE_COM_SERVICE_ROLE_KEY as string
)
// Track social shares
if (userAgent?.toLocaleLowerCase().includes('twitter')) {
await supabaseAdminClient
.from(LW_TABLE)
.update({ shared_on_twitter: 'now' })
.eq('launch_week', 'lw12')
.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', 'lw12')
.eq('username', username)
.is('shared_on_linkedin', null)
}
// Get ticket data
const { data: user, error } = await supabaseAdminClient
.from(LW_MATERIALIZED_VIEW)
.select(
'id, name, ticket_number, shared_on_twitter, shared_on_linkedin, platinum, secret, role, company, location'
)
.eq('launch_week', 'lw12')
.eq('username', username)
.maybeSingle()
if (error) console.log('fetch error', error.message)
if (!user) throw new Error(error?.message ?? 'user not found')
const {
name,
ticket_number: ticketNumber,
secret,
platinum: isPlatinum,
shared_on_twitter: sharedOnTwitter,
shared_on_linkedin: sharedOnLinkedIn,
} = user
console.log(user)
const platinum = isPlatinum ?? (!!sharedOnTwitter && !!sharedOnLinkedIn) ?? false
if (assumePlatinum && !platinum)
return await fetch(`${STORAGE_URL}/assets/platinum_no_meme.jpg`)
// Generate image and upload to storage.
const ticketType = secret ? 'secret' : platinum ? 'platinum' : 'regular'
const STYLING_CONFIG = {
BACKGROUND: themes[ticketType].OG_BACKGROUND,
FOREGROUND: themes[ticketType].TICKET_FOREGROUND,
FOREGROUND_LIGHT: themes[ticketType].TICKET_FOREGROUND_LIGHT,
TICKET_BORDER: themes[ticketType].TICKET_BORDER,
TICKET_FOREGROUND: themes[ticketType].TICKET_FOREGROUND,
TICKET_BACKGROUND: themes[ticketType].TICKET_BACKGROUND,
TICKET_BACKGROUND_CODE: themes[ticketType].TICKET_BACKGROUND_CODE,
TICKET_FOREGROUND_LIGHT: themes[ticketType].TICKET_FOREGROUND_LIGHT,
BORDER: themes[ticketType].TICKET_BORDER,
CODE_LINE_NUMBER: themes[ticketType].CODE_LINE_NUMBER,
CODE_BASE: themes[ticketType].CODE_THEME['hljs'].color,
CODE_HIGHLIGHT: themes[ticketType].CODE_HIGHLIGHT_BACKGROUND,
CODE_FUNCTION: themes[ticketType].CODE_THEME['hljs'].color,
CODE_VARIABLE: themes[ticketType].CODE_THEME['hljs'].color,
CODE_METHOD: themes[ticketType].CODE_THEME['hljs'].color,
CODE_EXPRESSION: themes[ticketType].CODE_THEME['hljs-keyword'].color,
CODE_STRING: themes[ticketType].CODE_THEME['hljs-string'].color,
CODE_NUMBER: themes[ticketType].CODE_THEME['hljs'].color,
CODE_NULL: themes[ticketType].CODE_THEME['hljs'].color,
JSON_KEY: themes[ticketType].CODE_THEME['hljs-attr'].color,
}
const fontData = await font
const monoFontData = await mono_font
const OG_WIDTH = 1200
const OG_HEIGHT = 628
const OG_PADDING_X = 60
const OG_PADDING_Y = 60
const TICKET_WIDTH = 550
const TICKET_RATIO = 396 / 613
const TICKET_HEIGHT = TICKET_WIDTH / TICKET_RATIO
const TICKET_POS_TOP = OG_PADDING_Y
const TICKET_POS_LEFT = 540
const LOGO_WIDTH = 40
const LOGO_RATIO = 436 / 449
const DISPLAY_NAME = name || username
const FIRST_NAME = DISPLAY_NAME?.split(' ')[0]
const BACKGROUND = {
regular: {
LOGO: `${STORAGE_URL}/assets/supabase/supabase-logo-icon.png`,
BACKGROUND_GRID: `${STORAGE_URL}/assets/bg-dark.png?t=2024-07-26T11%3A13%3A36.534Z`,
},
platinum: {
LOGO: `${STORAGE_URL}/assets/supabase/supabase-logo-icon.png`,
BACKGROUND_GRID: `${STORAGE_URL}/assets/bg-dark.png?t=2024-07-26T11%3A13%3A36.534Z`,
},
secret: {
LOGO: `${STORAGE_URL}/assets/supabase/supabase-logo-icon-white.png`,
BACKGROUND_GRID: `${STORAGE_URL}/assets/bg-light.png`,
},
}
const lineNumberStyle = {
paddingLeft: 24,
width: 46,
color: STYLING_CONFIG.CODE_LINE_NUMBER,
}
const generatedTicketImage = new ImageResponse(
(
<>
<div
style={{
width: '1200px',
height: '628px',
position: 'relative',
fontFamily: '"Circular"',
color: STYLING_CONFIG.FOREGROUND,
backgroundColor: STYLING_CONFIG.BACKGROUND,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
padding: '60px',
justifyContent: 'space-between',
}}
>
{/* Background */}
<img
width="1202"
height="632"
style={{
position: 'absolute',
top: '-1px',
left: '-1px',
bottom: '-1px',
right: '-1px',
zIndex: '0',
opacity: ticketType === 'secret' ? 0.2 : 0.5,
background: STYLING_CONFIG.BACKGROUND,
backgroundSize: 'cover',
}}
src={BACKGROUND[ticketType].BACKGROUND_GRID}
/>
{/* Ticket */}
<div
style={{
display: 'flex',
position: 'absolute',
zIndex: '1',
top: TICKET_POS_TOP,
left: TICKET_POS_LEFT,
width: TICKET_WIDTH,
height: TICKET_HEIGHT,
margin: 0,
borderRadius: '20px',
fontSize: 18,
background: STYLING_CONFIG.TICKET_BACKGROUND_CODE,
color: STYLING_CONFIG.TICKET_FOREGROUND,
border: `1px solid ${STYLING_CONFIG.TICKET_BORDER}`,
boxShadow: '0px 0px 45px rgba(0, 0, 0, 0.15)',
}}
tw="flex flex-col overflow-hidden"
>
<span
tw="uppercase p-6"
style={{
fontSize: 18,
letterSpacing: 2,
color: STYLING_CONFIG.FOREGROUND,
}}
>
Launch Week 12
<span tw="pl-2" style={{ color: STYLING_CONFIG.TICKET_FOREGROUND_LIGHT }}>
Ticket
</span>
</span>
{/* Request code snippet */}
<div
style={{ fontFamily: '"SourceCodePro"', lineHeight: '130%' }}
tw="p-6 pt-0 flex flex-row w-full"
>
<div tw="w-6 flex flex-col" style={{ color: STYLING_CONFIG.CODE_LINE_NUMBER }}>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
</div>
<div
tw="flex flex-col"
style={{
color: STYLING_CONFIG.CODE_BASE,
}}
>
<span>
<span style={{ color: STYLING_CONFIG.CODE_EXPRESSION }}>await</span>{' '}
<span style={{ color: STYLING_CONFIG.CODE_FUNCTION }} tw="ml-3">
supabase
</span>
</span>
<span tw="pl-4">
<span>.</span>
<span style={{ color: STYLING_CONFIG.CODE_METHOD }}>from</span>
<span>&#40;</span>
<span style={{ color: STYLING_CONFIG.CODE_STRING }}>'tickets'</span>
<span>&#41;</span>
</span>
<span tw="pl-4">
<span>.</span>
<span style={{ color: STYLING_CONFIG.CODE_METHOD }}>select</span>
<span>&#40;</span>
<span style={{ color: STYLING_CONFIG.CODE_STRING }}>'*'</span>
<span>&#41;</span>
</span>
<span tw="pl-4">
<span>.</span>
<span style={{ color: STYLING_CONFIG.CODE_METHOD }}>eq</span>
<span>&#40;</span>
<span style={{ color: STYLING_CONFIG.CODE_STRING }}>'username'</span>
<span tw="mr-3">,</span>
<span style={{ color: STYLING_CONFIG.CODE_NUMBER }}>{username}</span>
<span>&#41;</span>
</span>
<span tw="pl-4">
<span>.</span>
<span style={{ color: STYLING_CONFIG.CODE_METHOD }}>single</span>
<span>&#40;</span>
<span>&#41;</span>
</span>
</div>
</div>
{/* Response Json */}
<div
style={{
fontFamily: '"SourceCodePro"',
lineHeight: '130%',
background: STYLING_CONFIG.TICKET_BACKGROUND,
borderTop: `1px solid ${STYLING_CONFIG.TICKET_BORDER}`,
}}
tw="py-6 flex flex-col flex-grow w-full"
>
<div
tw="flex px-6 mb-4 uppercase"
style={{
lineHeight: '100%',
fontSize: 16,
color: STYLING_CONFIG.TICKET_FOREGROUND_LIGHT,
}}
>
TICKET RESPONSE
</div>
<div
tw="flex flex-col w-full"
style={{
color: STYLING_CONFIG.CODE_BASE,
}}
>
<div tw="flex">
<span style={lineNumberStyle}>1</span>
<span>&#123;</span>
</div>
<div tw="flex">
<span style={lineNumberStyle}>2</span>
<span>
<span tw="ml-6" style={{ color: STYLING_CONFIG.JSON_KEY }}>
"data"
</span>
<span tw="mr-2">:</span>
<span>&#123;</span>
</span>
</div>
<div
tw="flex flex-col w-full"
style={{
background: STYLING_CONFIG.CODE_HIGHLIGHT,
borderLeft: `1px solid ${STYLING_CONFIG.CODE_BASE}`,
}}
>
<div tw="flex">
<span style={lineNumberStyle}>3</span>
<span>
<span tw="ml-12 mr-2" style={{ color: STYLING_CONFIG.JSON_KEY }}>
"name"
</span>
<span>:</span>
<span tw="ml-2" style={{ color: STYLING_CONFIG.CODE_STRING }}>
"{name}"
</span>
<span>,</span>
</span>
</div>
<div tw="flex">
<span style={lineNumberStyle}>4</span>
<span>
<span tw="ml-12 mr-2" style={{ color: STYLING_CONFIG.JSON_KEY }}>
"username"
</span>
<span>:</span>
<span tw="ml-2" style={{ color: STYLING_CONFIG.CODE_STRING }}>
"{username}"
</span>
<span>,</span>
</span>
</div>
<div tw="flex">
<span style={lineNumberStyle}>6</span>
<span>
<span tw="ml-12 mr-2" style={{ color: STYLING_CONFIG.JSON_KEY }}>
"ticket_number"
</span>
<span>:</span>
<span tw="ml-2" style={{ color: STYLING_CONFIG.CODE_STRING }}>
"{ticketNumber}"
</span>
<span>,</span>
</span>
</div>
<div tw="flex">
<span style={lineNumberStyle}>7</span>
<span>
<span tw="ml-12 mr-2" style={{ color: STYLING_CONFIG.JSON_KEY }}>
"role"
</span>
<span>:</span>
<span tw="ml-2" style={{ color: STYLING_CONFIG.CODE_STRING }}>
"{user.role}"
</span>
<span>,</span>
</span>
</div>
<div tw="flex">
<span style={lineNumberStyle}>8</span>
<span>
<span tw="ml-12 mr-2" style={{ color: STYLING_CONFIG.JSON_KEY }}>
"company"
</span>
<span>:</span>
<span tw="ml-2" style={{ color: STYLING_CONFIG.CODE_STRING }}>
"{user.company}"
</span>
<span>,</span>
</span>
</div>
<div tw="flex">
<span style={lineNumberStyle}>9</span>
<span>
<span tw="ml-12 mr-2" style={{ color: STYLING_CONFIG.JSON_KEY }}>
"location"
</span>
<span>:</span>
<span tw="ml-2" style={{ color: STYLING_CONFIG.CODE_STRING }}>
"{user.location}"
</span>
<span>,</span>
</span>
</div>
</div>
<div tw="flex">
<span style={lineNumberStyle}>10</span>
<span tw="ml-6">&#125;,</span>
</div>
<div tw="flex">
<span style={lineNumberStyle}>11</span>
<span>
<span tw="ml-6" style={{ color: STYLING_CONFIG.JSON_KEY }}>
"error"
</span>
<span>:</span>
<span tw="ml-2" style={{ color: STYLING_CONFIG.CODE_NULL }}>
null
</span>
</span>
</div>
<div tw="flex">
<span style={lineNumberStyle}>12</span>
<span tw="ml-2">&#125;</span>
</div>
</div>
</div>
</div>
<div
style={{
position: 'absolute',
top: OG_PADDING_Y,
left: OG_PADDING_X,
bottom: OG_PADDING_Y,
display: 'flex',
flexDirection: 'column',
width: TICKET_POS_LEFT - OG_PADDING_X,
alignItems: 'flex-start',
justifyContent: 'center',
letterSpacing: '0.15rem',
lineHeight: '110%',
}}
>
<div
style={{
display: 'flex',
position: 'absolute',
top: 10,
left: 0,
marginBottom: '40',
}}
>
<img
src={BACKGROUND[ticketType].LOGO}
width={LOGO_WIDTH}
height={LOGO_WIDTH / LOGO_RATIO}
alt="logo"
/>
</div>
<p
style={{
display: 'flex',
flexDirection: 'column',
marginBottom: 60,
fontSize: 38,
letterSpacing: '0',
color: STYLING_CONFIG.FOREGROUND_LIGHT,
}}
>
<span
style={{
display: 'flex',
margin: 0,
color: STYLING_CONFIG.FOREGROUND_LIGHT,
}}
>
Join {FIRST_NAME} for
</span>
<span
style={{
display: 'flex',
margin: 0,
color: STYLING_CONFIG.FOREGROUND,
}}
>
Launch Week 12
</span>
</p>
<p
style={{
margin: '0',
fontFamily: '"SourceCodePro"',
fontSize: 26,
textTransform: 'uppercase',
color: STYLING_CONFIG.FOREGROUND_LIGHT,
}}
>
August 12-16 / 7AM PT
</p>
</div>
</div>
</>
),
{
width: OG_WIDTH,
height: OG_HEIGHT,
fonts: [
{
name: 'Circular',
data: fontData,
style: 'normal',
},
{
name: 'SourceCodePro',
data: monoFontData,
style: 'normal',
},
],
headers: {
'content-type': 'image/png',
'cache-control': 'public, max-age=31536000, s-maxage=31536000, no-transform, immutable',
'cdn-cache-control': 'max-age=31536000',
},
}
)
// [Note] Uncomment only for local testing to return the image directly and skip storage upload.
// return await generatedTicketImage
// Upload image to storage.
const { error: storageError } = await supabaseAdminClient.storage
.from('images')
.upload(`launch-week/lw12/og/${ticketType}/${username}.png`, generatedTicketImage.body!, {
contentType: 'image/png',
// cacheControl: `${60 * 60 * 24 * 7}`,
cacheControl: `0`,
// Update cached og image, people might need to update info
upsert: true,
})
if (storageError) throw new Error(`storageError: ${storageError.message}`)
const NEW_TIMESTAMP = new Date()
return await fetch(`${STORAGE_URL}/og/${ticketType}/${username}.png?t=${NEW_TIMESTAMP}`)
} catch (error: any) {
return new Response(JSON.stringify({ error: error.message }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
})
}
}

View File

@@ -1,5 +1,4 @@
import Link from 'next/link'
import { useTheme } from 'next-themes'
import { Badge, IconDiscord, IconGitHubSolid, IconTwitterX, IconYoutubeSolid, cn } from 'ui'
import Image from 'next/image'
import { useRouter } from 'next/router'
@@ -10,6 +9,7 @@ import footerData from 'data/Footer'
import * as supabaseLogoWordmarkDark from 'common/assets/images/supabase-logo-wordmark--dark.png'
import * as supabaseLogoWordmarkLight from 'common/assets/images/supabase-logo-wordmark--light.png'
import { ThemeToggle } from 'ui-patterns/ThemeToggle'
import useDarkLaunchWeeks from '../../hooks/useDarkLaunchWeeks'
interface Props {
className?: string
@@ -17,12 +17,11 @@ interface Props {
}
const Footer = (props: Props) => {
const { resolvedTheme } = useTheme()
const { pathname } = useRouter()
const isLaunchWeek = pathname.includes('/launch-week')
const isDarkLaunchWeek = useDarkLaunchWeeks()
const isGAWeek = pathname.includes('/ga-week')
const forceDark = isLaunchWeek || pathname === '/'
const forceDark = isDarkLaunchWeek || pathname === '/'
if (props.hideFooter) {
return null
@@ -32,7 +31,7 @@ const Footer = (props: Props) => {
<footer
className={cn(
'bg-alternative',
isLaunchWeek && 'bg-[#060809]',
isDarkLaunchWeek && 'bg-[#060809]',
isGAWeek && 'dark:bg-alternative',
props.className
)}

View File

@@ -5,6 +5,7 @@ import { useTelemetryProps } from 'common/hooks/useTelemetryProps'
import gaEvents from '~/lib/gaEvents'
import { Button, IconBookOpen } from 'ui'
import SectionContainer from '~/components/Layouts/SectionContainer'
import AnnouncementBadge from '~/components/Announcement/Badge'
const Hero = () => {
const router = useRouter()
@@ -21,6 +22,13 @@ 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 mb-8 lg:mb-8">
<AnnouncementBadge
url="/launch-week"
badge="Launch Week 12"
announcement="Claim your ticket"
/>
</div> */}
<h1 className="text-foreground text-4xl sm:text-5xl sm:leading-none lg:text-7xl">
<span className="block text-[#F4FFFA00] bg-clip-text bg-gradient-to-b from-foreground to-foreground-light">
Build in a weekend

View File

@@ -1,116 +0,0 @@
import { useEffect, useRef, useState } from 'react'
import Image from 'next/image'
import { cn } from 'ui'
import { Pencil, X } from 'lucide-react'
import Tilt from 'vanilla-tilt'
import { useBreakpoint, useParams } from 'common'
import Panel from '~/components/Panel'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import TicketProfile from './TicketProfile'
import TicketCustomizationForm from './TicketCustomizationForm'
import TicketNumber from './TicketNumber'
export default function Ticket() {
const ticketRef = useRef<HTMLDivElement>(null)
const { userData: user, showCustomizationForm, setShowCustomizationForm } = useConfData()
const isMobile = useBreakpoint()
const {
platinum = false,
bg_image_id: bgImageId = '1',
secret: hasSecretTicket,
ticketNumber,
} = user
const [imageHasLoaded, setImageHasLoaded] = useState(false)
const params = useParams()
const sharePage = !!params.username
const ticketType = hasSecretTicket ? 'secret' : platinum ? 'platinum' : 'regular'
const fallbackImg = `/images/launchweek/11/tickets/shape/lw11_ticket_${ticketType}.png`
const ticketBg = {
regular: {
background: `/images/launchweek/11/tickets/shape/lw11_ticket_regular.png`,
},
platinum: {
background: `/images/launchweek/11/tickets/shape/lw11_ticket_platinum.png`,
},
secret: {
background: `/images/launchweek/11/tickets/shape/lw11_ticket_purple.png`,
},
}
function handleCustomizeTicket() {
setShowCustomizationForm && setShowCustomizationForm(!showCustomizationForm)
}
useEffect(() => {
if (ticketRef.current && !window.matchMedia('(pointer: coarse)').matches) {
Tilt.init(ticketRef.current, {
glare: true,
max: 3,
gyroscope: true,
'max-glare': 0.2,
'full-page-listening': true,
})
}
}, [ticketRef])
return (
<div
ref={ticketRef}
className="relative w-auto h-auto flex justify-center rounded-xl overflow-hidden will-change-transform"
style={{ transformStyle: 'preserve-3d', transform: 'perspective(1000px)' }}
>
<Panel
hasShimmer
outerClassName="flex relative flex-col w-[360px] h-auto max-h-[680px] rounded-3xl !shadow-xl !p-0"
innerClassName="flex relative flex-col justify-between w-full transition-colors aspect-[396/613] rounded-xl dark:bg-[#020405] text-left text-sm group/ticket"
shimmerFromColor="hsl(var(--border-strong))"
shimmerToColor="hsl(var(--background-default))"
style={{ transform: 'translateZ(-10px)' }}
>
<TicketNumber
number={ticketNumber}
platinum={platinum}
secret={hasSecretTicket}
className="absolute z-20 top-6 left-6"
/>
<TicketProfile className="absolute inset-0 h-full p-6 top-20 bottom-20 z-30 flex flex-col justify-between w-full flex-1 overflow-hidden" />
<Image
src={ticketBg[ticketType].background}
alt={`Launch Week X ticket background #${bgImageId}`}
placeholder="blur"
blurDataURL={fallbackImg}
onLoad={() => setImageHasLoaded(true)}
loading="eager"
fill
sizes="100%"
className={cn(
'absolute inset-0 object-cover object-right opacity-0 transition-opacity duration-1000',
imageHasLoaded && 'opacity-100',
isMobile && 'object-left-top'
)}
priority
quality={100}
/>
{/* Edit hover button */}
{!sharePage && (
<>
<button
className="absolute z-40 inset-0 w-full h-full outline-none"
onClick={handleCustomizeTicket}
/>
<div className="flex md:translate-y-3 opacity-100 md:opacity-0 group-hover/ticket:opacity-100 group-hover/ticket:md:translate-y-0 transition-all absolute z-30 right-4 top-4 md:inset-0 m-auto w-10 h-10 rounded-full items-center justify-center bg-surface-100 dark:bg-[#020405] border shadow-lg text-foreground">
{!showCustomizationForm ? <Pencil className="w-4" /> : <X className="w-4" />}
</div>
</>
)}
</Panel>
<div className="absolute top-0 left-auto right-auto mx-auto w-[20%] aspect-square -translate-y-[65%] dark:bg-[#060809] z-40 rounded-b-[100px]" />
{!sharePage && (
<TicketCustomizationForm className="absolute inset-0 top-auto z-40 order-last md:order-first" />
)}
</div>
)
}

View File

@@ -1,114 +0,0 @@
import { useEffect, useRef, useState } from 'react'
import Link from 'next/link'
import {
SPECIAL_ANNOUNCEMENT_URL,
TWEET_TEXT,
TWEET_TEXT_PLATINUM,
TWEET_TEXT_SECRET,
} from '~/lib/constants'
import { Button, IconLinkedinSolid, IconTwitterX, cn } from 'ui'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import { useParams } from '~/hooks/useParams'
import { useBreakpoint } from 'common'
import dayjs from 'dayjs'
export default function TicketActions() {
const { userData, supabase } = useConfData()
const { platinum, username, metadata, secret: hasSecretTicket } = userData
const [_imgReady, setImgReady] = useState(false)
const [_loading, setLoading] = useState(false)
const isTablet = useBreakpoint(1280)
const downloadLink = useRef<HTMLAnchorElement>()
const link = `${SPECIAL_ANNOUNCEMENT_URL}/tickets/${username}?lw=11${
hasSecretTicket ? '&secret=true' : platinum ? `&platinum=true` : ''
}&t=${dayjs(new Date()).format('DHHmmss')}`
const permalink = encodeURIComponent(link)
const text = hasSecretTicket ? TWEET_TEXT_SECRET : platinum ? TWEET_TEXT_PLATINUM : 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}`
const downloadUrl = `https://obuldanrptloktxcffvn.supabase.co/functions/v1/lw11-og?username=${encodeURIComponent(
username ?? ''
)}`
const params = useParams()
const sharePage = !!params.username
const LW_TABLE = 'lw11_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') => {
if (!supabase) return
setTimeout(async () => {
if (social === 'twitter') {
await supabase
.from(LW_TABLE)
.update({
sharedOnTwitter: 'now',
metadata: { ...metadata, hasSharedSecret: hasSecretTicket },
})
.eq('username', username)
// window.open(tweetUrl, '_blank')
} else if (social === 'linkedin') {
await supabase
.from(LW_TABLE)
.update({
sharedOnLinkedIn: 'now',
metadata: { ...metadata, hasSharedSecret: hasSecretTicket },
})
.eq('username', username)
// window.open(linkedInUrl, '_blank')
}
})
}
return (
<div
className={cn(
'w-full gap-3 flex flex-col md:flex-row items-center',
sharePage ? 'justify-center' : 'justify-between'
)}
>
<div className="flex w-full gap-2">
<Button
onClick={() => handleShare('twitter')}
type={userData.sharedOnTwitter ? 'secondary' : 'default'}
icon={<IconTwitterX className="text-light w-3 h-3" />}
size={isTablet ? 'tiny' : 'tiny'}
block
asChild
>
<Link href={tweetUrl} target="_blank">
Share on X
</Link>
</Button>
<Button
onClick={() => handleShare('linkedin')}
type={userData.sharedOnLinkedIn ? 'secondary' : 'default'}
icon={<IconLinkedinSolid className="text-light w-3 h-3" />}
size={isTablet ? 'tiny' : 'tiny'}
block
asChild
>
<Link href={linkedInUrl} target="_blank">
Share on Linkedin
</Link>
</Button>
</div>
</div>
)
}

View File

@@ -1,13 +0,0 @@
import Ticket from './Ticket'
import TicketCopy from './TicketCopy'
export default function TicketContainer() {
return (
<div className="flex flex-col w-full items-center mx-auto max-w-xl gap-3 group group-hover">
<Ticket />
<div className="flex flex-col md:flex-row gap-2 items-center justify-center mx-auto max-w-full">
<TicketCopy />
</div>
</div>
)
}

View File

@@ -1,39 +0,0 @@
import { useState, useRef } from 'react'
import { SPECIAL_ANNOUNCEMENT_URL } from '~/lib/constants'
import { Check, Copy } from 'lucide-react'
import useConfData from '../../hooks/use-conf-data'
export default function TicketCopy() {
const { userData } = useConfData()
const { username, platinum, secret } = userData
const [copied, setCopied] = useState(false)
const buttonRef = useRef<HTMLButtonElement>(null)
const hasSecretTicket = secret
const url = `${SPECIAL_ANNOUNCEMENT_URL}/tickets/${username}?lw=11${
hasSecretTicket ? '&secret=true' : platinum ? `&platinum=true` : ''
}`
return (
<div className="h-full w-full">
<button
type="button"
name="Copy"
ref={buttonRef}
onClick={() => {
navigator.clipboard.writeText(url).then(() => {
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 2000)
})
}}
className="w-full h-full flex justify-center md:justify-start items-center gap-2 relative text-foreground-light hover:text-foreground text-xs"
>
<div className="w-4 min-w-4 flex-shrink-0">
{copied ? <Check size={14} strokeWidth={1.5} /> : <Copy size={14} strokeWidth={1.5} />}
</div>
<span className="truncate">{url}</span>
</button>
</div>
)
}

View File

@@ -1,36 +0,0 @@
import React from 'react'
import TicketNumber from './TicketNumber'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import { cn } from 'ui'
export default function TicketFooter() {
const { userData: user } = useConfData()
const { ticketNumber, golden, metadata } = user
const hasLightTicket = golden || metadata?.hasSecretTicket
const SupabaseLogo = hasLightTicket
? '/images/launchweek/lwx/logos/supabase_lwx_logo_light.png'
: '/images/launchweek/lwx/logos/supabase_lwx_logo_dark.png'
return (
<div
className={cn(
'relative z-10 w-full flex flex-col gap-2 font-mono text-foreground leading-none uppercase tracking-[3px]',
hasLightTicket ? 'text-[#11181C]' : 'text-white'
)}
>
{/* <Image
src={SupabaseLogo}
alt="Supabase Logo for Launch Week X"
width="30"
height="30"
className="mb-1 hidden md:block"
priority
quality={100}
/> */}
<TicketNumber number={ticketNumber} />
{/* <span>Launch Week X</span>
<span>{LWX_DATE}</span> */}
</div>
)
}

View File

@@ -1,31 +0,0 @@
import { cn } from 'ui'
type Props = {
number: number | undefined
secret?: boolean
platinum?: boolean
className?: string
}
export default function TicketNumber({
number,
platinum = false,
secret = false,
className,
}: Props) {
const numDigits = `${number}`.length
const prefix = `0000000`.slice(numDigits)
const ticketNumberText = `NO ${prefix}${number}`
return (
<span
className={cn(
'w-[max-content] leading-[1] font-mono tracking-[.15rem] text-sm',
secret ? 'text-[#8b818c]' : platinum ? 'text-[#999a9b]' : 'text-[#616464]',
className
)}
>
{ticketNumberText}
</span>
)
}

View File

@@ -1,160 +0,0 @@
import React, { useRef } from 'react'
import { AnimatePresence, m, LazyMotion, domAnimation } from 'framer-motion'
import { Badge, cn } from 'ui'
import { DEFAULT_TRANSITION, INITIAL_BOTTOM, getAnimation } from '~/lib/animations'
import { LW11_DATE, LW11_LAUNCH_DATE_END } from '~/lib/constants'
import useWinningChances from '../../hooks/useWinningChances'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import SectionContainer from '~/components/Layouts/SectionContainer'
import TicketContainer from '~/components/LaunchWeek/11/Ticket/TicketContainer'
import LW11Background from '../LW11Background'
import TicketForm from './TicketForm'
import CountdownComponent from '../Countdown'
import TicketActions from './TicketActions'
const TicketingFlow = () => {
const sectionRef = useRef<HTMLDivElement>(null)
const { ticketState, userData } = useConfData()
const isLoading = ticketState === 'loading'
const isRegistering = ticketState === 'registration'
const hasTicket = ticketState === 'ticket'
const hasPlatinumTicket = userData.platinum
const hasSecretTicket = userData.secret
const metadata = userData?.metadata
const transition = DEFAULT_TRANSITION
const initial = INITIAL_BOTTOM
const animate = getAnimation({ duration: 1 })
const exit = { opacity: 0, transition: { ...transition, duration: 0.2 } }
const winningChances = useWinningChances()
return (
<>
<SectionContainer ref={sectionRef} className="relative !pt-8 lg:!pt-20 gap-5 h-full flex-1">
<div className="relative z-10 flex flex-col h-full">
<h1 className="sr-only">Supabase Special Announcement | {LW11_DATE}</h1>
<div className="relative z-10 w-full h-full flex flex-col justify-center gap-5 md:gap-10">
<LazyMotion features={domAnimation}>
<AnimatePresence mode="wait" key={ticketState}>
{isLoading && (
<m.div
key="loading"
initial={exit}
animate={animate}
exit={exit}
className="relative w-full min-h-[400px] mx-auto py-16 md:py-24 flex flex-col items-center gap-6 text-foreground"
>
<div className="hidden">
<TicketForm />
</div>
<svg
className="animate-spinner opacity-50 w-5 h-5 md:w-6 md:h-6"
width="100%"
height="100%"
viewBox="0 0 62 61"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M61 31C61 14.4315 47.5685 1 31 1C14.4315 1 1 14.4315 1 31"
stroke="white"
strokeWidth="2"
/>
</svg>
</m.div>
)}
{isRegistering && (
<m.div
key="registration"
initial={initial}
animate={animate}
exit={exit}
className={cn(
'w-full min-h-[400px] max-w-md mx-auto flex flex-col justify-center gap-8 lg:gap-12 opacity-0 invisible',
!hasTicket && 'opacity-100 visible'
)}
>
<div className="flex flex-col gap-2">
<h2 className="text-foreground text-2xl">Time to participate has expired</h2>
<span className="font-mono text-foreground-lighter text-xs leading-3">
See you next Launch Week.
</span>
</div>
</m.div>
)}
{hasTicket && (
<m.div
key="ticket"
initial={initial}
animate={animate}
exit={exit}
className="w-full flex-1 min-h-[400px] flex flex-col xl:flex-row items-center xl:justify-center xl:items-center gap-8 md:gap-10 xl:gap-20 text-foreground text-center md:text-left"
>
<div className="w-full lg:w-auto h-full mt-3 md:mt-6 xl:mt-0 max-w-lg flex flex-col items-center">
<TicketContainer />
</div>
<div className="order-first xl:h-full w-full max-w-lg gap-3 flex flex-col items-center justify-center xl:items-start xl:justify-start text-center xl:text-left">
{hasSecretTicket && <Badge variant="outline">Secret ticket</Badge>}
{hasPlatinumTicket ? (
<div>
{hasSecretTicket && !metadata?.hasSharedSecret ? (
<p className="text-2xl mb-1">
Share again to boost your chance of winning!
</p>
) : (
<p className="text-2xl mb-1">Thanks for sharing!</p>
)}
<p className="text-foreground-lighter">
Stay tuned after GA Week to find out if you're a lucky winner.
</p>
</div>
) : winningChances !== 2 ? (
<div>
{!hasSecretTicket && (
<p className="text-2xl mb-1">@{userData.username}, you're in!</p>
)}
<p className="text-foreground-lighter">
Now share your ticket to have a chance of winning AirPods Max and other
limited swag.
</p>
</div>
) : (
<div>
<p className="text-2xl mb-1">@{userData.username}, almost there!</p>
<p className="text-foreground-lighter">
Keep sharing to max out your chances of winning AirPods Max and other
limited swag.
</p>
</div>
)}
<div className="w-full my-3">
<TicketActions />
</div>
<div className="flex flex-col">
<span className="font-mono text-foreground-lighter text-xs leading-3">
Time to participate
</span>
<CountdownComponent date={LW11_LAUNCH_DATE_END} showCard={false} />
</div>
</div>
</m.div>
)}
</AnimatePresence>
</LazyMotion>
</div>
</div>
</SectionContainer>
<LW11Background
className={cn(
'absolute z-0 inset-0 w-full h-full flex items-center justify-center opacity-100 transition-opacity',
hasTicket && 'opacity-20'
)}
/>
</>
)
}
export default TicketingFlow

View File

@@ -0,0 +1,41 @@
import React from 'react'
import Countdown from 'react-countdown'
import { CountdownWidget } from 'ui-patterns/CountdownWidget'
const CountdownComponent = ({
date,
showCard = true,
className,
size = 'small',
}: {
date: string | number | Date
showCard?: boolean
className?: string
size?: 'small' | 'large'
}) => {
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={className}
size={size}
/>
)
}
}
return <Countdown date={new Date(date)} renderer={renderer} />
}
export default CountdownComponent

View File

@@ -0,0 +1,38 @@
import React from 'react'
import { TextLink, cn } from 'ui'
import CountdownComponent from './Countdown'
import { LW11_LAUNCH_DATE_END } from '~/lib/constants'
const HackathonCallout = ({ className }: { className?: string }) => {
const isActive = new Date() < new Date(LW11_LAUNCH_DATE_END)
return (
<div
className={cn(
'font-mono uppercase tracking-[1px] py-8 scroll-mt-16 flex flex-col md:flex-row justify-between gap-2',
className
)}
>
<div className="!text-foreground [&_*]:text-foreground text-sm flex flex-col sm:flex-row sm:items-center sm:gap-3">
{isActive ? (
<>
Hackathon ends in <CountdownComponent date={LW11_LAUNCH_DATE_END} showCard={false} />
</>
) : (
'Open Source Hackathon 2024'
)}
</div>
<div className="!m-0 flex items-center">
<TextLink
label="Learn more"
url="/blog/supabase-oss-hackathon"
target="_blank"
hasChevron
className="m-0"
/>
</div>
</div>
)
}
export default HackathonCallout

View File

@@ -0,0 +1,44 @@
import React from 'react'
import { cn } from 'ui'
import { range } from 'lodash'
interface Props {
className?: string
}
const LW12Background = ({ className }: Props) => {
return (
<div className={cn('absolute inset-0 w-full h-full flex flex-col', className)}>
{range(0, 3).map((_) => (
<div className="w-fit h-full max-h-full flex flex-col gap-0 animate-marquee-vertical will-change-transform mx-auto">
<img
src="/images/launchweek/12/bg-light.svg"
className="dark:hidden block relative inset-0 w-full overflow-hidden object-cover"
/>
<img
src="/images/launchweek/12/bg-dark.svg"
className="dark:block hidden relative inset-0 w-full overflow-hidden object-cover"
/>
{/* <img
src="/images/launchweek/12/bg-light.svg"
className="dark:hidden block relative inset-0 w-full overflow-hidden object-cover"
/>
<img
src="/images/launchweek/12/bg-dark.svg"
className="dark:block hidden relative inset-0 w-full overflow-hidden object-cover"
/>
<img
src="/images/launchweek/12/bg-light.svg"
className="dark:hidden block relative inset-0 w-full overflow-hidden object-cover"
/>
<img
src="/images/launchweek/12/bg-dark.svg"
className="dark:block hidden relative inset-0 w-full overflow-hidden object-cover"
/> */}
</div>
))}
</div>
)
}
export default LW12Background

View File

@@ -0,0 +1,152 @@
import Link from 'next/link'
import React, { useEffect, useState } from 'react'
import { cn } from 'ui'
import useConfData from '../hooks/use-conf-data'
import { SupabaseClient } from '@supabase/supabase-js'
import { ArrowRight } from 'lucide-react'
export interface Meetup {
id?: any
location: string
is_live: boolean
link: string
display_info: string
date: string
}
function addHours(date: Date, hours: number) {
const dateCopy = new Date(date)
dateCopy.setHours(dateCopy.getHours() + hours)
return dateCopy
}
const LWMeetups = ({ meetups }: { meetups?: Meetup[] }) => {
const { supabase } = useConfData()
const now = new Date(Date.now())
const [meets, setMeets] = useState<Meetup[]>(meetups ?? [])
const [realtimeChannel, setRealtimeChannel] = useState<ReturnType<
SupabaseClient['channel']
> | null>(null)
const [activeMeetup, setActiveMeetup] = useState<Meetup>(meets[0])
useEffect(() => {
// Listen to realtime changes
if (supabase && !realtimeChannel) {
const channel = supabase
.channel('meetups')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'meetups',
filter: undefined,
},
async () => {
const { data: newMeets } = await supabase
.from('meetups')
.select('*')
.eq('launch_week', 'lw12')
.neq('is_published', false)
setMeets(newMeets?.sort((a, b) => (new Date(a.date) > new Date(b.date) ? 1 : -1))!)
}
)
.subscribe(async (status) => {
if (status !== 'SUBSCRIBED') {
return null
}
})
setRealtimeChannel(channel)
}
return () => {
// Cleanup realtime subscription on unmount
realtimeChannel?.unsubscribe()
}
}, [])
function handleSelectMeetup(meetup: Meetup) {
setActiveMeetup(meetup)
}
return (
<div className="max-w-7xl mx-auto grid grid-cols-1 xl:grid-cols-12 gap-8 text-foreground-lighter">
<div className="mb-4 col-span-1 xl:col-span-4 flex flex-col max-w-lg">
<h2 className="text-sm font-mono uppercase tracking-[1px] mb-4">Community meetups</h2>
<p className="text-base xl:max-w-md mb-2">
Join our live community-driven meetups to Launch Week 12 with the community, listen to
tech talks and grab some swag.
</p>
</div>
<div className="col-span-1 xl:col-span-7 xl:col-start-6 w-full max-w-4xl flex flex-wrap gap-x-3 gap-y-1">
{meets &&
meets
?.sort((a, b) => (new Date(a.date) > new Date(b.date) ? 1 : -1))
.map((meetup: Meetup, i: number) => {
const startAt = new Date(meetup.date)
const endAt = addHours(new Date(meetup.date), 3)
const after = now > startAt
const before3H = now < endAt
const liveNow = after && before3H
return (
<>
<button
key={meetup.id}
onClick={() => handleSelectMeetup(meetup)}
onMouseDown={() => handleSelectMeetup(meetup)}
title={liveNow ? 'Live now' : undefined}
className={cn(
'h-10 group inline-flex md:hidden items-center flex-wrap text-3xl',
'text-foreground-muted hover:!text-foreground !leading-none transition-colors',
meetup.id === activeMeetup?.id && '!text-foreground',
liveNow && 'text-foreground-light'
)}
>
{liveNow && (
<div className="w-2 h-2 rounded-full bg-brand mr-2 mb-4 animate-pulse" />
)}
<span>{meetup.location}</span>
{i !== meets.length - 1 && ', '}
</button>
<Link
key={`meetup-link-${meetup.id}`}
href={meetup.link ?? ''}
target="_blank"
onClick={() => handleSelectMeetup(meetup)}
onMouseOver={() => handleSelectMeetup(meetup)}
title={liveNow ? 'Live now' : undefined}
className={cn(
'hidden h-10 group md:inline-flex items-center flex-wrap text-4xl',
'text-foreground-muted hover:!text-foreground !leading-none transition-colors',
meetup.id === activeMeetup?.id && '!text-foreground',
liveNow && 'text-foreground-light'
)}
>
{liveNow && (
<div className="w-2 h-2 rounded-full bg-brand mr-2 mb-4 animate-pulse" />
)}
<span>{meetup.location}</span>
{i !== meets.length - 1 && ', '}
</Link>
</>
)
})}
</div>
<Link
href={activeMeetup?.link ?? '#'}
target="_blank"
className="col-span-1 xl:col-span-7 xl:col-start-6 w-full max-w-4xl text-sm flex-1 inline-flex flex-wrap items-center gap-1"
>
{activeMeetup?.display_info}{' '}
<span className="inline">
<ArrowRight className="w-3 md:hidden" />
</span>
</Link>
</div>
)
}
export default LWMeetups

View File

@@ -0,0 +1,101 @@
import React from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { buildDays, mainDays } from './Releases/data'
const LW11Summary = () => {
const days = mainDays()
return (
<div className="w-full border bg-alternative-200 flex flex-col rounded-lg text-foreground-lighter mt-12">
<div className="w-full p-4 flex justify-between items-center">
<Link
href="/ga-week"
className="flex items-center gap-1.5 leading-none uppercase text-xs opacity-80 transition-opacity hover:opacity-100"
>
<Image
src="/images/launchweek/ga/ga-black.svg"
alt="GA logo"
className="dark:hidden w-5 aspect-[104/57] h-auto"
priority
width={30}
height={30}
/>
<Image
src="/images/launchweek/ga/ga-white.svg"
alt="GA logo"
className="hidden dark:block w-5 aspect-[104/57] h-auto"
priority
width={30}
height={30}
/>
<span className="text-foreground tracking-[1px] font-mono">Week</span>
</Link>
<div className="font-mono uppercase tracking-wide text-xs">15-19 April</div>
</div>
<div className="pb-4 border-t p-4">
<ul className="flex flex-col gap-2">
{days.map(
(day, i: number) =>
day.shipped && (
<ol key={day.id}>
<Link href={day.blog} className="group flex py-1 gap-2 hover:text-foreground">
<span className="shrink-0 text-sm font-mono uppercase leading-6">
Day {i + 1} -
</span>
<span className="leading-6">{day.title}</span>
</Link>
</ol>
)
)}
</ul>
</div>
<div className="w-[calc(100%+2px)] bg-surface-100 flex flex-col gap-2 -m-px border rounded-lg">
<div className="p-4">
<div className="font-mono uppercase text-xs text-foreground tracking-wide">
Build Stage
</div>
<ul className="flex flex-col gap-2 mt-4">
{buildDays.map(
(day, i: number) =>
day.is_shipped && (
<ol key={day.id}>
<Link
href={day.links[0].url}
className="relative flex items-center justify-between group w-full py-1 hover:text-foreground"
>
<span className="relative">
<span className="font-mono uppercase mr-2">
{i + 1 < 10 ? '0' : ''}
{i + 1} -
</span>
{day.title}
</span>
</Link>
</ol>
)
)}
<ol className="border-t pt-4 mt-2">
<Link
href="/blog/supabase-oss-hackathon"
className="relative flex items-center justify-between group w-full py-1 hover:text-foreground"
>
Open Source Hackathon 2024
</Link>
</ol>
<ol>
<Link
href="/ga-week#meetups"
className="relative flex items-center justify-between group w-full py-1 hover:text-foreground"
>
Community Meetups
</Link>
</ol>
</ul>
</div>
</div>
</div>
)
}
export default LW11Summary

View File

@@ -0,0 +1,14 @@
import React from 'react'
interface Props {
text: string
className?: string
}
export default function LabelBadge({ text, className }: Props) {
return (
<span className={['text-sm', className].join(' ')}>
<span className="text-foreground-lighter">{text}</span>
</span>
)
}

View File

@@ -0,0 +1,16 @@
export function LaunchWeekLogoHeader() {
return (
<div className="flex flex-col gap-1 md:gap-2 items-center justify-end">
<div className="opacity-0 !animate-[fadeIn_0.5s_cubic-bezier(0.25,0.25,0,1)_0.5s_both] px-2 flex flex-col items-center text-center gap-3">
<h1 className="sr-only font-normal uppercase text-[28px] sm:text-[32px]">Launch week 8</h1>
<p className="text-white radial-gradient-text-600 text-lg sm:text-2xl">
<span className="block">August 7th11th, 2023</span>
</p>
<div className="text-[#9296AA]">
<p>A week of announcing new features has come to an end.</p>
<p>Thanks to everyone who participated.</p>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,33 @@
import { useTheme } from 'next-themes'
import React from 'react'
import { cn } from 'ui'
import Panel from '~/components/Panel'
export default function LaunchWeekPrizeCard({
content,
className,
contentClassName,
}: {
content: any
className?: string
contentClassName?: string
}) {
const { resolvedTheme } = useTheme()
return (
<Panel
hasShimmer
outerClassName={cn('relative rounded-lg overflow-hidden', className)}
innerClassName={cn(
'relative h-full flex flex-col rounded-lg overflow-hidden',
contentClassName
)}
shimmerToColor={
resolvedTheme?.includes('dark') ? 'hsl(var(--background-alternative-default))' : undefined
}
shimmerFromColor={resolvedTheme?.includes('dark') ? 'hsl(var(--border-default))' : undefined}
>
{content}
</Panel>
)
}

View File

@@ -0,0 +1,82 @@
import React from 'react'
import { cn } from 'ui'
import LabelBadge from './LabelBadge'
import LaunchWeekPrizeCard from './LaunchWeekPrizeCard'
import Image from 'next/image'
import PrizeActions from './Releases/PrizeActions'
export default function LaunchWeekPrizeSection({ className }: { className?: string }) {
return (
<div
id="prizes"
className={cn(
'relative text-left flex flex-col max-w-7xl mx-auto gap-2 scroll-mt-[66px] text-foreground-lighter',
className
)}
>
<h2 className="w-full text-sm font-mono uppercase tracking-[1px]">Awards</h2>
<div className="w-full pt-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mx-auto h-auto text-foreground">
<LaunchWeekPrizeCard
className="col-span-full md:col-span-2"
contentClassName="flex flex-col justify-between"
content={
<div className="w-full h-auto lg:min-h-[400px] flex flex-col lg:flex-row items-stretch rounded-lg overflow-hidden">
<div className="relative w-full pl-4 xl:px-4 lg:w-2/3 border-b lg:border-none border-muted aspect-[3/1] top-0 md:-bottom-8 overflow-hidden">
<Image
src="/images/launchweek/11/airpods-max-alpha.png"
alt="Supabase AirPod Max prize"
draggable={false}
width={300}
height={300}
className="hidden md:block absolute object-cover scale-50 lg:scale-100 lg:object-top w-[90%] h-full opacity-90 dark:opacity-50 pointer-events-none"
/>
<Image
src="/images/launchweek/11/airpods-max-alpha-crop.png"
alt="Supabase AirPod Max prize"
draggable={false}
width={300}
height={300}
className="md:hidden absolute mx-auto object-cover inset-x-0 lg:object-top w-auto h-full opacity-90 dark:opacity-50 pointer-events-none"
/>
</div>
<div className="flex flex-col lg:w-1/2 gap-1 p-4 md:p-8 lg:pl-0 lg:h-full">
<div className="flex flex-col gap-2 flex-grow">
<LabelBadge text="5 sets" />
<p className="xl:mt-4 text-foreground">Win AirPods Max</p>
<p className="text-foreground-lighter text-sm">
Secure your ticket to enter our random prize pool, and amplify your odds by
sharing. Or if you don't leave anything up for chance - join our Hackathon and
showcase your creations. With luck or skill, you could snag these top-tier
headphones!
</p>
</div>
</div>
</div>
}
/>
<div className="w-full flex flex-col gap-4 items-stretch">
<LaunchWeekPrizeCard
className="flex-grow"
content={
<div className="p-4 md:p-6 flex flex-col gap-2 text-sm items-start justify-between h-full">
<LabelBadge text="30 t-shirts" />
<p>Supabase T-shirts</p>
</div>
}
/>
<LaunchWeekPrizeCard
className="flex-grow"
content={
<div className="p-4 md:p-6 flex flex-col gap-2 text-sm items-start justify-between h-full">
<LabelBadge text="25 pins" />
<p>Supabase Pins</p>
</div>
}
/>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,75 @@
import React, { FC } from 'react'
import { buildDays as days } from '~/components/LaunchWeek/11/Releases/data'
import SectionContainer from '~/components/Layouts/SectionContainer'
import AdventCard from './components/AdventCard'
import { motion, useInView } from 'framer-motion'
import { cn } from 'ui'
const BuildStage: FC = () => {
const ref = React.useRef(null)
const isInView = useInView(ref, { margin: '-25%', once: true })
const variants = {
reveal: {
transition: {
type: 'spring',
damping: 10,
mass: 0.75,
stiffness: 100,
staggerChildren: 0.08,
},
},
}
return (
<>
<SectionContainer className="!max-w-none lg:!container lwx-nav-anchor" id="build-stage">
<h3 className="text-foreground uppercase font-mono pb-4 md:pb-8 text-sm tracking-[0.1rem]">
Build Stage
</h3>
<motion.ul
ref={ref}
variants={variants}
animate={isInView ? 'reveal' : 'initial'}
className="w-full grid gap-2 sm:gap-3 xl:gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
>
{days.map((day, i) => (
<li
key={`${day.id}-${i}`}
className={cn(
'relative flex flex-col w-full aspect-square rounded-xl border border-dashed border-muted bg-surface-100/10 col-span-1',
day.className
)}
data-delay={i}
>
<AdventCard day={day} index={i} />
{!day.is_shipped && (
<div className="absolute m-4 md:m-6 lg:m-8">
<svg
width="16"
height="17"
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g opacity="0.5">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.9999 7.55557V5.95557C3.9999 3.74643 5.79076 1.95557 7.9999 1.95557C10.209 1.95557 11.9999 3.74643 11.9999 5.95557V7.55557C12.8836 7.55557 13.5999 8.27191 13.5999 9.15557V13.1556C13.5999 14.0392 12.8836 14.7556 11.9999 14.7556H3.9999C3.11625 14.7556 2.3999 14.0392 2.3999 13.1556V9.15557C2.3999 8.27191 3.11625 7.55557 3.9999 7.55557ZM10.3999 5.95557V7.55557H5.5999V5.95557C5.5999 4.63008 6.67442 3.55557 7.9999 3.55557C9.32539 3.55557 10.3999 4.63008 10.3999 5.95557Z"
fill="hsl(var(--foreground-lighter))"
/>
</g>
</svg>
</div>
)}
</li>
))}
</motion.ul>
</SectionContainer>
</>
)
}
export default BuildStage

View File

@@ -0,0 +1,92 @@
import React from 'react'
import { ArrowRight } from 'lucide-react'
import { cn } from 'ui'
import { ExpandableVideo } from 'ui-patterns'
import { WeekDayProps } from './data'
import { DayLink } from './components'
import Link from 'next/link'
const LW11Day1 = ({
day,
className,
cardClassName,
}: {
day: WeekDayProps
className?: string
cardClassName?: string
}) => (
<section
id={day.id}
className={cn(
'lwx-nav-anchor border-b py-8 first:border-t dark:border-[#111718] scroll-mt-16 grid grid-cols-1 gap-4 md: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="md:max-w-xs flex flex-col gap-4">
<ExpandableVideo
videoId={day.videoId ?? ''}
imgUrl={day.videoThumbnail}
imgOverlayText="Watch announcement"
priority
/>
</div>
{!!day.links && (
<ul className="flex-1 h-full w-full justify-end xs:grid grid-cols-2 lg:grid-cols-3 flex flex-col gap-1">
{day.links?.map((link) => (
<li key={link.href}>
<DayLink {...link} />
</li>
))}
</ul>
)}
</div>
{/* Card */}
<div
className={cn(
`group relative overflow-hidden flex-1 flex col-span-2
md:px-4 text-2xl`,
cardClassName
)}
>
<div className="relative text-sm w-full grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 rounded-xl border gap-px bg-border overflow-hidden">
{day.steps?.map((step) => (
<Link
href={step.url!}
key={step.title}
target="_blank"
className="flex group/step flex-col gap-2 p-4 transition-colors bg-surface-75 hover:bg-surface-100 overflow-hidden border-0"
>
<div className="flex-1 flex justify-between items-start">
<div className="flex items-center gap-1 mb-4 transition-colors text-foreground-light group-hover/step:text-foreground group-focus-visible/step:text-foreground">
<svg
className="h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 16 16"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1"
d={step.icon}
stroke="currentColor"
/>
</svg>
<span>{step.title}</span>
</div>
<ArrowRight className="w-4 ml-2 -mt-px opacity-0 -rotate-45 translate-y-1 -translate-x-1 text-foreground-light transition-all will-change-transform group-hover/step:opacity-100 group-hover/step:translate-y-0 group-hover/step:translate-x-0" />
</div>
<p className="text-foreground">{step.description}</p>
</Link>
))}
</div>
</div>
</section>
)
export default LW11Day1

View File

@@ -0,0 +1,52 @@
import React from 'react'
import { Button, cn } from 'ui'
import SectionContainer from '~/components/Layouts/SectionContainer'
// import LW11Background from '../LW11Background'
import Image from 'next/image'
import Link from 'next/link'
const LW11Header = ({ className }: { className?: string }) => {
return (
<div className={cn('relative w-full overflow-visible pt-10 sm:pt-8', className)}>
<SectionContainer className="h-full flex flex-col items-start gap-4 !max-w-none lg:!container !pb-4 md:!pb-10">
<Image
src="/images/launchweek/ga/ga-black.svg"
alt="GA logo"
className="dark:hidden w-20 md:w-24 aspect-[104/57] h-auto"
priority
quality={100}
width={300}
height={300}
/>
<Image
src="/images/launchweek/ga/ga-white.svg"
alt="GA logo"
className="hidden dark:block w-20 md:w-24 aspect-[104/57] h-auto"
priority
quality={100}
width={300}
height={300}
/>
<p className="text-foreground-lighter text-xl md:text-2xl max-w-2xl">
Supabase is{' '}
<strong className="text-foreground font-normal">
officially launching into General Availability
</strong>
. <br className="hidden sm:block" /> Join us in this major milestone and explore{' '}
<br className="hidden sm:block" /> the exciting features that come with it.
</p>
<Button asChild size="small" type="alternative">
<Link href="/ga">Read full announcement</Link>
</Button>
</SectionContainer>
<div className="absolute z-0 inset-0 w-full h-full overflow-hidden pointer-events-none">
<div className="absolute z-0 inset-0 w-full aspect-video">
{/* <LW11Background className="absolute z-0 inset-0 w-full flex items-center justify-center opacity-100 transition-opacity h-full" /> */}
</div>
<div className="absolute inset-0 bg-[linear-gradient(to_top,#060809)_0%,transparent_100%)]" />
</div>
</div>
)
}
export default LW11Header

View File

@@ -0,0 +1,127 @@
import React, { FC, useEffect, useRef } from 'react'
import Link from 'next/link'
import { WeekDayProps, mainDays } from './data'
import { cn } from 'ui'
import { isBrowser } from 'common'
import SectionContainer from '~/components/Layouts/SectionContainer'
import useConfData from '../../hooks/use-conf-data'
import Image from 'next/image'
const LWXStickyNav: FC = () => {
const days = mainDays()
const { ticketState, userData } = useConfData()
const hasPlatinumTicket = userData.platinum
const hasSecretTicket = userData.secret
const hasTicket = ticketState === 'ticket'
const USER = userData?.name || userData?.username
const DISPLAY_NAME = USER && USER.split(' ')[0]
const OFFSET = 66
const anchors = useRef<NodeListOf<HTMLHeadingElement> | null>(null)
const links = useRef<NodeListOf<HTMLHeadingElement> | null>(null)
const handleScroll = () => {
let newActiveAnchor: string = ''
anchors.current?.forEach((anchor) => {
const { y: offsetFromTop } = anchor.getBoundingClientRect()
if (offsetFromTop - OFFSET < 0) {
newActiveAnchor = anchor.id
}
})
links.current?.forEach((link) => {
link.classList.remove('!text-foreground')
const sanitizedHref = decodeURI(link.getAttribute('href') ?? '')
.split('#')
.splice(-1)
.join('')
const isMatch = sanitizedHref === newActiveAnchor
if (isMatch) {
link.classList.add('!text-foreground')
}
})
}
useEffect(() => {
if (!isBrowser) return
anchors.current = document.querySelectorAll('.lwx-nav-anchor')
links.current = document.querySelectorAll('.lwx-sticky-nav li a')
window.addEventListener('scroll', handleScroll)
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [])
function getTicketIcon() {
const getImgPath = (type: string) =>
`/images/launchweek/11/tickets/icon/ticket-icon-${type}.png`
if (hasSecretTicket) return getImgPath('secret')
if (hasPlatinumTicket) return getImgPath('platinum')
return getImgPath('regular')
}
return (
<div className="absolute inset-0 pointer-events-none w-full h-full">
<nav className="sticky z-30 top-0 bg-alternative/90 backdrop-blur-sm pointer-events-auto w-full border-t border-b dark:border-[#111718] h-[60px] flex items-center">
<SectionContainer className="!max-w-none !py-0 lg:!container flex items-center justify-between font-mono gap-4 md:gap-8 text-sm">
<div className="flex items-center gap-4 md:gap-8">
<ul className="lwx-sticky-nav hidden md:flex items-center gap-2 md:gap-4 text-foreground-muted">
{days.map((day: WeekDayProps) => (
<li key={day.id}>
<Link
href={`#${day.id}`}
className={cn(
'p-1 transition-colors hover:text-foreground flex items-center',
day.isToday && 'text-foreground-light'
)}
>
{day.dd}{' '}
{day.isToday && (
<span
title="Live"
className="w-1 h-1 ml-1 animate-pulse rounded-full bg-brand mb-2 block"
/>
)}
</Link>
</li>
))}
<li>
<Link href="#build-stage" className="p-1 transition-colors hover:text-foreground">
Build Stage
</Link>
</li>
</ul>
</div>
<div>
{hasTicket && (
<Link
href="#ticket"
className="flex items-center gap-2 text-xs text-foreground-light hover:text-foreground transition-colors"
>
{DISPLAY_NAME}'s ticket
<Image
src={getTicketIcon()}
alt=""
width={24}
height={24}
aria-hidden
className="w-auto h-4 shadow"
priority
/>
</Link>
)}
</div>
</SectionContainer>
</nav>
</div>
)
}
export default LWXStickyNav

View File

@@ -0,0 +1,32 @@
import React from 'react'
import { mainDays } from './data'
import SectionContainer from '~/components/Layouts/SectionContainer'
import DaySection from './components/DaySection'
import LW11Day1 from './Day1'
import HackathonCallout from '../HackathonCallout'
import { cn } from 'ui'
import { useTheme } from 'next-themes'
const MainStage = ({ className }: { className?: string }) => {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme?.includes('dark')
const [day1, ...days] = mainDays(isDark!)
return (
<SectionContainer
className={cn('relative !max-w-none !py-0 lg:!container', className)}
id="main-stage"
>
<LW11Day1 day={day1} className="!border-t-0" cardClassName="md:-mx-4" />
<HackathonCallout />
<div>
{days.map((day) => (
<DaySection day={day} key={day.dd} />
))}
</div>
</SectionContainer>
)
}
export default MainStage

View File

@@ -0,0 +1,15 @@
import Link from 'next/link'
import { Button } from 'ui'
export default function PrizeActions() {
return (
<div className="w-full gap-2 flex items-center">
<Button onClick={() => null} type="secondary" size="tiny" asChild>
<Link href="/ga-week#ticket">Claim your ticket</Link>
</Button>
<Button onClick={() => null} type="default" size="tiny">
<Link href="/blog/supabase-oss-hackathon">Join Hackathon</Link>
</Button>
</div>
)
}

View File

@@ -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/lw12_build_stage'
const AdventCard = ({ 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 AdventCard

View File

@@ -0,0 +1,142 @@
import React, { useEffect, useState } from 'react'
import Link from 'next/link'
import Image from 'next/image'
import { ArrowRightIcon } from '@heroicons/react/outline'
import { cn } from 'ui'
import { Edit } from 'lucide-react'
import { useBreakpoint } from 'common'
import { WeekDayProps } from '../data'
import CountdownComponent from '../../Countdown'
import { DayLink } from '.'
const DaySection = ({ day, className }: { day: WeekDayProps; className?: string }) => {
const isMobile = useBreakpoint(639)
const cssGroup = 'group/d' + day.d
const [isMounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!isMounted) return null
return (
<section
id={day.id}
className={cn(
'lwx-nav-anchor border-b py-8 first:border-t dark:border-[#111718] text-foreground dark:text-[#575E61] scroll-mt-16 grid grid-cols-1 gap-4 md: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 col-span-2">
{day.shipped && day.steps.length > 0 ? (
<Link
href={day.blog!}
className={cn(
`
dark:bg-[#111415] hover:dark:bg-[#121516] sm:!bg-transparent
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-lg`,
cssGroup
)}
>
<div className="relative text-foreground-light p-4 sm:p-6 md:p-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">
{day.steps[0]?.bg_layers &&
day.steps[0]?.bg_layers?.map(
(layer, i) =>
!!layer.img && (
<div
key={`${day.title}-image-${i}`}
className="absolute sm:opacity-90 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={`
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-end
bg-surface-100/10 border border-dashed border-strong dark:border-[#14191B] dark:text-[#8B9092]
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>
)}
</div>
</section>
)
}
export default DaySection

View File

@@ -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/lw12_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,
}

View File

@@ -0,0 +1,4 @@
export { default as mainDays } from './lw12_data'
export type { StepProps, StepLink, WeekDayProps } from './lw12_data'
export { days as buildDays } from './lw12_build_stage'
export type { AdventDay } from './lw12_build_stage'

View File

@@ -0,0 +1,290 @@
// see apps/www/components/LaunchWeek/X/Releases/data/lwx_advent_days.tsx for reference
import { ReactNode } from 'react'
export interface AdventDay {
icon?: ReactNode // use svg jsx with 34x34px viewport
className?: 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: 'PostgreSQL Index Advisor',
description: 'A PostgreSQL extension for recommending indexes to improve query performance.',
id: 'pg-index-advisor',
is_shipped: true,
links: [
{
url: 'https://github.com/supabase/index_advisor',
label: 'Learn more',
target: '_blank',
},
],
icon: (
<svg
width="34"
height="32"
viewBox="0 0 34 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.43881 3.75378C4.10721 1.93324 5.84055 0.723145 7.77992 0.723145H15.6033V0.734736H17.0394C23.8756 0.734736 29.4173 6.27652 29.4173 13.1127V20.1749C29.4173 20.7272 28.9696 21.1749 28.4173 21.1749C27.8651 21.1749 27.4173 20.7272 27.4173 20.1749V13.1127C27.4173 7.38109 22.771 2.73474 17.0394 2.73474H15.4396C15.3877 2.73474 15.3366 2.73078 15.2868 2.72314H7.77992C6.6793 2.72314 5.6956 3.40989 5.31627 4.44308L2.7812 11.3479C2.37375 12.4577 2.69516 13.7038 3.58855 14.4781L5.32807 15.9856C6.12772 16.6786 6.58711 17.6847 6.58709 18.7428L6.58706 21.5134C6.58702 23.8192 8.45627 25.6885 10.7621 25.6885C11.4007 25.6885 11.9184 25.1708 11.9184 24.5322L11.9185 12.1874C11.9185 9.59233 12.955 7.10481 14.7977 5.27761C15.1899 4.88873 15.823 4.8914 16.2119 5.28357C16.6008 5.67574 16.5981 6.3089 16.2059 6.69777C14.742 8.14943 13.9185 10.1257 13.9185 12.1874L13.9184 24.5323C13.9184 26.2754 12.5053 27.6885 10.7621 27.6885C7.35169 27.6885 4.58701 24.9238 4.58706 21.5134L4.58709 18.7428C4.5871 18.2647 4.37953 17.8101 4.01822 17.497L2.27871 15.9894C0.757203 14.6708 0.209829 12.5486 0.90374 10.6586L3.43881 3.75378ZM16.539 18.5225C17.0348 18.2791 17.634 18.4838 17.8773 18.9796C19.1969 21.6686 21.9313 23.3727 24.9267 23.3726L32.8043 23.3726C33.3566 23.3725 33.8043 23.8203 33.8043 24.3725C33.8044 24.9248 33.3566 25.3725 32.8044 25.3726L29.4081 25.3726C29.4142 25.4172 29.4173 25.4628 29.4173 25.5091C29.4173 29.0627 26.1868 31.4165 22.6091 31.4165C19.2966 31.4165 16.5385 29.0518 15.9271 25.9188C15.8213 25.3767 16.175 24.8516 16.717 24.7458C17.2591 24.64 17.7843 24.9936 17.89 25.5357C18.3217 27.7475 20.2716 29.4165 22.6091 29.4165C25.447 29.4165 27.4173 27.6256 27.4173 25.5091C27.4173 25.4628 27.4205 25.4172 27.4266 25.3726L24.9267 25.3726C21.1684 25.3727 17.7375 23.2346 16.0818 19.8607C15.8385 19.3649 16.0432 18.7658 16.539 18.5225Z"
fill="hsl(var(--foreground-light))"
/>
<path
d="M21.7224 13.0006C21.7224 13.6338 22.2358 14.1472 22.869 14.1472C23.5022 14.1472 24.0156 13.6338 24.0156 13.0006C24.0156 12.3674 23.5022 11.854 22.869 11.854C22.2358 11.854 21.7224 12.3674 21.7224 13.0006Z"
fill="hsl(var(--foreground-light))"
/>
</svg>
),
},
{
title: 'Branching now Publicly Available',
description: 'Supabase Branching is now available on Pro Plan and above.',
id: 'branching',
is_shipped: true,
icon: (
<svg
width="31"
height="21"
viewBox="0 0 31 21"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M25.8657 12.5851C24.9374 12.5878 24.0363 12.8984 23.3035 13.4682C22.5707 14.038 22.0477 14.8349 21.8164 15.7339H17.469C16.6342 15.733 15.8338 15.4009 15.2435 14.8106C14.6532 14.2203 14.3211 13.4199 14.3202 12.5851V8.38674C14.3162 7.24753 13.938 6.1412 13.2437 5.23796H21.8164C22.0718 6.22737 22.6794 7.08964 23.5251 7.66315C24.3709 8.23666 25.3967 8.48203 26.4104 8.35327C27.4242 8.22452 28.3561 7.73047 29.0316 6.96374C29.7071 6.197 30.0798 5.21023 30.0798 4.18837C30.0798 3.16652 29.7071 2.17974 29.0316 1.41301C28.3561 0.646277 27.4242 0.152228 26.4104 0.0234703C25.3967 -0.105288 24.3709 0.140085 23.5251 0.713594C22.6794 1.2871 22.0718 2.14938 21.8164 3.13878H8.9232C8.66774 2.14938 8.0602 1.2871 7.21446 0.713594C6.36872 0.140085 5.34285 -0.105288 4.32914 0.0234703C3.31543 0.152228 2.38348 0.646277 1.70798 1.41301C1.03247 2.17974 0.65979 3.16652 0.65979 4.18837C0.65979 5.21023 1.03247 6.197 1.70798 6.96374C2.38348 7.73047 3.31543 8.22452 4.32914 8.35327C5.34285 8.48203 6.36872 8.23666 7.21446 7.66315C8.0602 7.08964 8.66774 6.22737 8.9232 5.23796H9.07225C9.90707 5.23888 10.7074 5.57092 11.2978 6.16123C11.8881 6.75154 12.2201 7.55191 12.221 8.38674V12.5851C12.2227 13.9764 12.7761 15.3103 13.7599 16.2942C14.7437 17.278 16.0776 17.8314 17.469 17.8331H21.8164C22.014 18.5916 22.4203 19.2796 22.9892 19.8188C23.5581 20.3581 24.2669 20.727 25.035 20.8836C25.803 21.0403 26.5996 20.9784 27.3343 20.7051C28.069 20.4317 28.7123 19.9578 29.1911 19.3372C29.67 18.7166 29.9652 17.9741 30.0433 17.1942C30.1214 16.4142 29.9792 15.6279 29.6329 14.9247C29.2865 14.2215 28.7499 13.6295 28.084 13.2159C27.4181 12.8024 26.6496 12.5838 25.8657 12.5851ZM25.8657 2.08919C26.2809 2.08919 26.6867 2.21231 27.032 2.44297C27.3772 2.67363 27.6462 3.00148 27.8051 3.38505C27.964 3.76863 28.0056 4.1907 27.9246 4.5979C27.8436 5.0051 27.6436 5.37914 27.3501 5.67272C27.0565 5.96629 26.6824 6.16622 26.2752 6.24722C25.868 6.32822 25.446 6.28665 25.0624 6.12776C24.6788 5.96888 24.351 5.69982 24.1203 5.35462C23.8896 5.00941 23.7665 4.60355 23.7665 4.18837C23.7672 3.63183 23.9885 3.09827 24.3821 2.70473C24.7756 2.3112 25.3092 2.08983 25.8657 2.08919ZM4.87388 6.28755C4.4587 6.28755 4.05285 6.16444 3.70764 5.93378C3.36243 5.70312 3.09337 5.37527 2.93449 4.99169C2.77561 4.60812 2.73404 4.18604 2.81503 3.77884C2.89603 3.37164 3.09596 2.9976 3.38954 2.70403C3.68311 2.41045 4.05715 2.21052 4.46435 2.12953C4.87155 2.04853 5.29363 2.0901 5.6772 2.24898C6.06078 2.40786 6.38863 2.67692 6.61929 3.02213C6.84995 3.36734 6.97306 3.77319 6.97306 4.18837C6.97251 4.74494 6.75117 5.27855 6.35761 5.6721C5.96406 6.06566 5.43045 6.287 4.87388 6.28755ZM25.8657 18.8826C25.4505 18.8826 25.0447 18.7595 24.6995 18.5289C24.3543 18.2982 24.0852 17.9704 23.9263 17.5868C23.7674 17.2032 23.7259 16.7811 23.8069 16.3739C23.8879 15.9667 24.0878 15.5927 24.3814 15.2991C24.6749 15.0055 25.049 14.8056 25.4562 14.7246C25.8634 14.6436 26.2855 14.6852 26.669 14.8441C27.0526 15.003 27.3805 15.272 27.6111 15.6172C27.8418 15.9624 27.9649 16.3683 27.9649 16.7835C27.9641 17.34 27.7427 17.8735 27.3492 18.267C26.9557 18.6605 26.4222 18.8819 25.8657 18.8826Z"
fill="currentColor"
/>
</svg>
),
links: [
{
url: '/blog/branching-publicly-available',
label: 'Blog post',
target: '_blank',
},
],
},
{
title: 'Oriole joins Supabase',
description:
'The Oriole team are joining Supabase to build a faster storage engine for Postgres.',
id: 'oriole',
is_shipped: true,
links: [
{
url: '/blog/supabase-acquires-oriole',
label: 'Blog post',
target: '_blank',
},
],
icon: (
<svg
width="49"
height="50"
viewBox="0 0 49 50"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M32.3419 16.1687C26.9198 19.0762 22.2245 19.1919 20.5547 18.8863C24.0908 22.894 28.1618 23.1755 29.7552 22.8152C37.4684 22.442 40.855 13.0158 48.2546 13.2545C46.6043 11.4734 44.4237 11.05 43.5397 11.0609C39.6868 10.8581 35.3857 14.269 32.3419 16.1687Z"
fill="hsl(var(--foreground-light))"
/>
<path
d="M12.6959 13.353C17.8299 18.0154 25.4872 16.6927 28.6741 15.4485C25.7928 15.1342 22.0602 11.6504 20.554 9.94776C15.0031 4.03282 7.47323 1.59481 0.253906 6.21518C4.37942 6.80454 6.27846 7.52486 12.6959 13.353Z"
fill="hsl(var(--foreground-light))"
/>
<path
d="M24.5485 2.22059C21.6148 -0.555946 15.8172 0.496169 13.2852 1.36929C17.4762 1.36929 22.8022 7.61206 24.9414 10.7334C27.6059 14.037 30.8974 13.9871 32.2101 13.5493C31.1624 12.8158 29.7217 10.1441 29.1324 8.89988C27.194 5.18037 25.2688 2.89722 24.5485 2.22059Z"
fill="hsl(var(--foreground-light))"
/>
<path
d="M31.1956 7.73838C30.7536 5.49555 28.9582 3.13734 27.8886 1.82766C30.4359 1.82766 35.7101 3.85375 34.6335 7.26286C34.162 9.88223 34.0878 12.196 34.1096 13.0255C32.3809 11.7158 31.4532 9.04546 31.1956 7.73838Z"
fill="hsl(var(--foreground-light))"
/>
</svg>
),
},
{
title: 'Supabase on AWS Marketplace',
description:
'Supabase is now available on the AWS Marketplace, Simplifying Procurement for Enterprise Customers.',
id: 'aws-marketplace',
is_shipped: true,
icon: (
<svg
width="41"
height="33"
viewBox="0 0 41 33"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.00976 32.2636C10.4187 32.2636 11.5609 31.1214 11.5609 29.7124C11.5609 28.3035 10.4187 27.1613 9.00976 27.1613C7.6008 27.1613 6.45862 28.3035 6.45862 29.7124C6.45862 31.1214 7.6008 32.2636 9.00976 32.2636Z"
fill="currentColor"
/>
<path
d="M23.6109 32.2636C25.0199 32.2636 26.1621 31.1214 26.1621 29.7124C26.1621 28.3035 25.0199 27.1613 23.6109 27.1613C22.202 27.1613 21.0598 28.3035 21.0598 29.7124C21.0598 31.1214 22.202 32.2636 23.6109 32.2636Z"
fill="currentColor"
/>
<path
d="M40.1662 6.15511H28.876L24.3709 21.5705H7.92416L5.1559 12.5058H0.813538L5.26446 26.0214H27.0305L32.0785 9.62901H40.1662V6.15511Z"
fill="currentColor"
/>
<path
d="M16.7175 8.92336L21.6026 11.2031L26.4878 3.38686H16.7175V8.92336Z"
fill="currentColor"
/>
<path
d="M26.4878 8.92336L21.6026 11.2031V3.44114L26.4878 3.38686V8.92336Z"
fill="currentColor"
/>
<path
d="M21.6027 0.889999L16.7175 3.38685L21.6027 5.61232L26.4878 3.38685L21.6027 0.889999Z"
fill="currentColor"
/>
<path
d="M5.64447 8.92336L10.4753 11.2031L15.4148 3.38686H5.64447V8.92336Z"
fill="currentColor"
/>
<path d="M15.4148 8.92336L10.4754 11.2031V3.38686H15.4148V8.92336Z" fill="currentColor" />
<path
d="M10.4753 0.889999L5.64444 3.38685L10.4753 5.61232L15.4148 3.38685L10.4753 0.889999Z"
fill="currentColor"
/>
<path
d="M11.1809 18.0423L16.0661 20.3221L20.9513 12.343H11.1809V18.0423Z"
fill="currentColor"
/>
<path d="M20.9513 18.0423L16.0661 20.3221V12.343H20.9513V18.0423Z" fill="currentColor" />
<path
d="M16.0661 10.0632L20.9513 12.343L16.0661 14.7313L11.181 12.343L16.0661 10.0632Z"
fill="currentColor"
/>
</svg>
),
links: [
{
url: '/blog/supabase-aws-marketplace',
label: 'Blog post',
target: '_blank',
},
],
},
{
title: 'Supabase Bootstrap',
description:
'Launch a new hosted Supabase project directly from the CLI using pre-built applications.',
id: 'supabase-bootstrap',
is_shipped: true,
icon: (
<svg
width="37"
height="37"
viewBox="0 0 37 37"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.17327 36.1237C8.80564 36.1237 8.44388 36.0998 8.08939 36.0536L8.01632 36.6138C7.23514 36.5119 6.48595 36.3082 5.7825 36.0165L5.9989 35.4946C5.32499 35.2152 4.69568 34.8493 4.12483 34.4106L3.78061 34.8585C3.16696 34.387 2.61683 33.8368 2.14527 33.2232L2.59321 32.879C2.15454 32.3081 1.78864 31.6788 1.50918 31.0049L0.987341 31.2213C0.695631 30.5179 0.491954 29.7687 0.390057 28.9875L0.95024 28.9144C0.904 28.5599 0.880114 28.1982 0.880114 27.8305V26.6573H0.315186V24.3107H0.880114V21.9642H0.315186V19.6176H0.880114V17.2711H0.315186V14.9246H0.880114V12.578H0.315186V10.2315H0.880114V9.05822C0.880114 8.69059 0.904 8.32883 0.95024 7.97434L0.390057 7.90127C0.491954 7.12009 0.69563 6.3709 0.987342 5.66745L1.50918 5.88385C1.78864 5.20994 2.15454 4.58063 2.59321 4.00978L2.14527 3.66556C2.61683 3.05191 3.16696 2.50178 3.78061 2.03022L4.12484 2.47816C4.69568 2.03949 5.325 1.67359 5.9989 1.39413L5.7825 0.87229C6.48596 0.580579 7.23515 0.376903 8.01632 0.275005L8.08939 0.835189C8.44388 0.788948 8.80565 0.765063 9.17327 0.765063H10.3465V0.200134H12.6931V0.765063H15.0396V0.200134H17.3862V0.765063H19.7327V0.200134H22.0792V0.765063H24.4258V0.200134H26.7723V0.765063H27.9456C28.3132 0.765063 28.675 0.788948 29.0295 0.835189L29.1025 0.275005C29.8837 0.376903 30.6329 0.58058 31.3364 0.872291L31.12 1.39413C31.7939 1.67359 32.4232 2.03949 32.994 2.47816L33.3383 2.03022C33.9519 2.50178 34.502 3.05191 34.9736 3.66556L34.5257 4.00978C34.9643 4.58063 35.3302 5.20994 35.6097 5.88385L36.1315 5.66745C36.4232 6.37091 36.6269 7.12009 36.7288 7.90127L36.1686 7.97434C36.2149 8.32883 36.2387 8.69059 36.2387 9.05822V10.2315H36.8037V12.578H36.2387V14.9246H36.8037V17.2711H36.2387V19.6177H36.8037V21.9642H36.2387V24.3107H36.8037V26.6573H36.2387V27.8305C36.2387 28.1982 36.2149 28.5599 36.1686 28.9144L36.7288 28.9875C36.6269 29.7687 36.4232 30.5179 36.1315 31.2213L35.6097 31.0049C35.3302 31.6788 34.9643 32.3081 34.5256 32.879L34.9736 33.2232C34.502 33.8369 33.9519 34.387 33.3383 34.8585L32.994 34.4106C32.4232 34.8493 31.7939 35.2152 31.12 35.4946L31.3364 36.0165C30.6329 36.3082 29.8837 36.5119 29.1025 36.6138L29.0295 36.0536C28.675 36.0998 28.3132 36.1237 27.9456 36.1237H26.7723V36.6886H24.4258V36.1237H22.0792V36.6886H19.7327V36.1237H17.3862V36.6886H15.0396V36.1237H12.6931V36.6886H10.3465V36.1237H9.17327Z"
stroke="currentColor"
strokeWidth="1.12986"
strokeDasharray="2.26 2.26"
/>
<path
d="M9.26641 19.2458V19.3588H9.37939C9.97925 19.3588 10.5345 19.4987 10.9374 19.817C11.3359 20.1319 11.5997 20.6326 11.5997 21.3882V24.273C11.5997 25.606 11.9385 26.6254 12.6104 27.3112C13.2828 27.9973 14.2709 28.3314 15.5309 28.3314H15.8066H15.9196V28.2184V26.6275V26.5146H15.8066H15.5309C14.7589 26.5146 14.2791 26.3472 13.9872 26.0195C13.6924 25.6884 13.5651 25.1676 13.5651 24.4003V20.9427C13.5651 20.2088 13.3456 19.5929 12.9437 19.1269C12.63 18.7631 12.2096 18.4957 11.7071 18.3337C12.2096 18.1716 12.63 17.9042 12.9437 17.5404C13.3456 17.0744 13.5651 16.4585 13.5651 15.7246V12.2458C13.5651 11.4785 13.6924 10.9577 13.9872 10.6266C14.2791 10.2989 14.7589 10.1315 15.5309 10.1315H15.8066H15.9196V10.0185V8.42764V8.31466H15.8066H15.5309C14.2709 8.31466 13.2828 8.64876 12.6104 9.33493C11.9385 10.0207 11.5997 11.0401 11.5997 12.3731V15.2791C11.5997 16.0347 11.3359 16.5354 10.9374 16.8503C10.5345 17.1686 9.97925 17.3085 9.37939 17.3085H9.26641V17.4215V19.2458ZM27.8522 17.4215V17.3085H27.7392C27.1394 17.3085 26.5841 17.1686 26.1812 16.8503C25.7827 16.5354 25.5189 16.0347 25.5189 15.2791V12.3731C25.5189 11.0401 25.1802 10.0207 24.5082 9.33493C23.8359 8.64876 22.8477 8.31466 21.5877 8.31466H21.312H21.199V8.42764V10.0185V10.1315H21.312H21.5877C22.3598 10.1315 22.8395 10.2989 23.1314 10.6266C23.4262 10.9577 23.5535 11.4785 23.5535 12.2458V15.7246C23.5535 16.4585 23.7731 17.0744 24.1749 17.5404C24.4886 17.9042 24.909 18.1716 25.4115 18.3337C24.909 18.4957 24.4886 18.7631 24.1749 19.1269C23.7731 19.5929 23.5535 20.2088 23.5535 20.9427V24.4003C23.5535 25.1676 23.4262 25.6884 23.1314 26.0195C22.8395 26.3472 22.3598 26.5146 21.5877 26.5146H21.312H21.199V26.6275V28.2184V28.3314H21.312H21.5877C22.8477 28.3314 23.8359 27.9973 24.5082 27.3112C25.1802 26.6254 25.5189 25.606 25.5189 24.273V21.3882C25.5189 20.6326 25.7827 20.1319 26.1812 19.817C26.5841 19.4987 27.1394 19.3588 27.7392 19.3588H27.8522V19.2458V17.4215Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="0.225972"
/>
</svg>
),
links: [
{
url: '/blog/supabase-bootstrap',
label: 'Blog post',
target: '_blank',
},
],
},
{
title: 'Supabase Swift',
description: 'Supabase Swift is now officially supported.',
id: 'supabase-swift',
is_shipped: true,
icon: (
<svg
width="37"
height="37"
viewBox="0 0 37 37"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M36.5368 9.18927V27.6498C36.5368 28.4764 36.4679 29.2685 36.3301 30.0607C36.1924 30.8528 35.9513 31.6105 35.5724 32.3338C35.1936 33.0226 34.7458 33.677 34.1603 34.2625C33.6093 34.848 32.9549 35.2957 32.2316 35.6746C31.5083 36.0535 30.7506 36.2945 29.9585 36.4323C29.1775 36.5681 28.3631 36.6035 27.5812 36.6375L27.5476 36.639H9.05265C8.22606 36.639 7.43391 36.5701 6.64176 36.4323C5.84961 36.2945 5.0919 36.0535 4.36863 35.6746C3.67981 35.2957 3.02543 34.848 2.43993 34.2625C1.85443 33.7114 1.40669 33.0571 1.02783 32.3338C0.648979 31.6105 0.407891 30.8528 0.270126 30.0607C0.134298 29.2797 0.0989065 28.4652 0.0649292 27.6832L0.0634766 27.6498V9.15483C0.0979178 8.32824 0.132361 7.53609 0.270126 6.74394C0.407891 5.95179 0.648979 5.19408 1.02783 4.47082C1.40669 3.78199 1.85443 3.12761 2.43993 2.54211C2.50881 2.47322 2.5863 2.40434 2.66379 2.33546C2.74128 2.26658 2.81878 2.1977 2.88766 2.12882C3.36984 1.74996 3.85202 1.40555 4.40308 1.13002C4.48918 1.09558 4.58389 1.05253 4.67861 1.00947C4.77332 0.966423 4.86803 0.92337 4.95413 0.888929C5.50519 0.682281 6.0907 0.510077 6.6762 0.406753C7.24546 0.306296 7.84728 0.270952 8.41835 0.237415C8.43464 0.236458 8.45091 0.235502 8.46715 0.234547C8.63935 0.200106 8.846 0.200104 9.05265 0.200104H27.5476C28.3742 0.200104 29.1663 0.268988 29.9585 0.406753C30.7506 0.544518 31.5083 0.785601 32.2316 1.16445C32.9204 1.54331 33.5748 1.99105 34.1603 2.57655C34.7458 3.12761 35.1936 3.78199 35.5724 4.50526C35.9513 5.22853 36.1924 5.98624 36.3301 6.77839C36.4659 7.55938 36.5013 8.37386 36.5353 9.1558L36.5368 9.18927ZM29.0286 22.3114L28.9253 22.7247C32.0939 26.5821 31.2328 30.7151 30.7507 29.9918C29.0975 26.8232 26.0666 27.6153 24.5168 28.4075C24.4479 28.4419 24.379 28.485 24.3101 28.528C24.2413 28.5711 24.1724 28.6141 24.1035 28.6486C24.1035 28.683 24.0691 28.683 24.0691 28.683C20.866 30.4051 16.5264 30.5084 12.1868 28.6486C8.53605 27.0643 5.53966 24.3779 3.61095 21.2782C4.60974 22.0014 5.67742 22.6558 6.81399 23.1724C11.3947 25.3422 16.0098 25.17 19.2817 23.1724C14.6322 19.5905 10.7403 14.941 7.77834 11.1524C7.22728 10.498 6.71066 9.77478 6.26292 9.05151C9.84481 12.289 15.4587 16.3875 17.4908 17.5241C13.2201 13.0123 9.43152 7.43277 9.60372 7.60498C16.3542 14.3899 22.5881 18.2473 22.5881 18.2473C22.8292 18.3506 23.0014 18.454 23.1391 18.5573C23.2769 18.2129 23.3802 17.8685 23.4836 17.5241C24.5512 13.5978 23.3458 9.08595 20.5905 5.3663C26.8588 9.12039 30.544 16.2842 29.0286 22.3114Z"
fill="currentColor"
/>
</svg>
),
links: [
{
url: '/blog/supabase-swift',
label: 'Blog post',
target: '_blank',
},
],
},
{
className: 'col-span-full min-h-[290px] xl:min-h-0 xl:col-span-2 sm:aspect-auto',
title: 'Top 10 Launches from Supabase GA Week',
description: 'A recap of the most important launches and updates from the week.',
id: 'ga-week-summary',
is_shipped: true,
icon: (
<svg
width="43"
height="43"
viewBox="0 0 97 96"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M45.1845 28.8958C46.1844 27.6366 48.2117 28.3265 48.2358 29.9343L48.3161 42.1777H64.0052C66.8691 42.1777 68.4663 45.4854 66.6855 47.7284L51.5973 66.731C50.5975 67.9901 48.5702 67.3003 48.5461 65.6926L48.3627 53.4492H32.7766C29.9127 53.4492 28.3154 50.1414 30.0963 47.8985L45.1845 28.8958Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M55.6755 2.97563C51.6248 -0.991875 45.1451 -0.991877 41.0944 2.97563L34.7454 9.19414C33.669 10.2485 32.2267 10.8459 30.72 10.8615L21.8335 10.9537C16.1638 11.0126 11.5819 15.5944 11.5231 21.2642L11.4309 30.1507C11.4152 31.6574 10.8178 33.0997 9.76348 34.1761L3.54496 40.525C-0.422539 44.5758 -0.422541 51.0555 3.54496 55.1062L9.76348 61.4551C10.8178 62.5316 11.4152 63.9739 11.4309 65.4805L11.5231 74.3671C11.5819 80.0368 16.1638 84.6187 21.8335 84.6775L30.72 84.7697C32.2267 84.7854 33.669 85.3828 34.7454 86.4371L41.0944 92.6556C45.1451 96.6231 51.6248 96.6231 55.6755 92.6556L62.0245 86.4371C63.1009 85.3828 64.5432 84.7854 66.0499 84.7697L74.9364 84.6775C80.6061 84.6187 85.188 80.0368 85.2468 74.3671L85.3391 65.4805C85.3547 63.9739 85.9521 62.5316 87.0064 61.4551L93.2249 55.1062C97.1924 51.0555 97.1925 44.5758 93.2249 40.525L87.0064 34.1761C85.9521 33.0997 85.3547 31.6574 85.3391 30.1507L85.2468 21.2642C85.188 15.5944 80.6061 11.0126 74.9364 10.9537L66.0499 10.8615C64.5432 10.8459 63.1009 10.2485 62.0245 9.19414L55.6755 2.97563ZM44.299 6.24742C46.5692 4.02384 50.2007 4.02384 52.4709 6.24742L58.8199 12.4659C60.7405 14.3471 63.314 15.4131 66.0023 15.441L74.8889 15.5332C78.0665 15.5662 80.6344 18.1341 80.6673 21.3117L80.7596 30.1982C80.7875 32.8866 81.8534 35.46 83.7346 37.3807L89.9532 43.7296C92.1767 45.9998 92.1767 49.6314 89.9531 51.9016L83.7346 58.2506C81.8534 60.1712 80.7875 62.7447 80.7596 65.433L80.6673 74.3195C80.6344 77.4971 78.0665 80.065 74.8889 80.098L66.0023 80.1902C63.314 80.2181 60.7405 81.2841 58.8199 83.1653L52.4709 89.3838C50.2007 91.6074 46.5692 91.6074 44.299 89.3838L37.95 83.1653C36.0294 81.2841 33.4559 80.2181 30.7676 80.1902L21.881 80.098C18.7034 80.065 16.1355 77.4971 16.1026 74.3195L16.0103 65.433C15.9824 62.7447 14.9165 60.1712 13.0353 58.2505L6.81676 51.9016C4.59317 49.6314 4.59317 45.9998 6.81676 43.7296L13.0353 37.3807C14.9165 35.46 15.9824 32.8865 16.0103 30.1982L16.1026 21.3117C16.1355 18.1341 18.7034 15.5662 21.881 15.5332L30.7676 15.441C33.4559 15.4131 36.0294 14.3471 37.95 12.4659L44.299 6.24742Z"
fill="currentColor"
/>
</svg>
),
links: [
{
url: '/blog/ga-week-summary',
label: 'Blog post',
target: '_blank',
},
],
},
]

View File

@@ -0,0 +1,312 @@
import { ReactNode } from 'react'
import { products } from 'shared-data/products'
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
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 endOfLWXHackathon = '2024-04-17T23:59:59.999-08:00'
const days: (isDark?: boolean) => WeekDayProps[] = (isDark = true) => [
{
id: 'day-1',
d: 1,
dd: 'Mon',
shipped: true,
isToday: false,
blog: '/ga',
hasCountdown: false,
date: '15 April',
published_at: '2024-04-15T08:00:00.000-07:00',
videoId: 'bRtdk8D4X8w',
videoThumbnail: '/images/launchweek/11/video-cover.jpg',
title: 'Supabase is officially launching into General Availability',
description: 'Supabase is officially launching into General Availability',
links: [
{
type: 'xSpace',
href: 'https://supabase.link/twitter-space-ga',
},
],
steps: [
{
icon: products.database.icon[16],
title: 'Database',
description: 'Fully portable Postgres Database',
url: 'https://supabase.com/docs/guides/database/overview',
},
{
icon: products.authentication.icon[16],
title: 'Auth',
description: 'User management out of the box',
url: 'https://supabase.com/docs/guides/auth',
},
{
icon: products.storage.icon[16],
title: 'Storage',
description: 'Serverless storage for any media',
url: 'https://supabase.com/docs/guides/storage',
},
{
icon: products.functions.icon[16],
title: 'Edge Functions',
description: 'Deploy code globally on the edge',
url: 'https://supabase.com/docs/guides/functions',
},
{
icon: products.realtime.icon[16],
title: 'Realtime',
description: 'Synchronize and broadcast events',
url: 'https://supabase.com/docs/guides/realtime',
},
{
icon: products.vector.icon[16],
title: 'Vector',
description: 'AI toolkit to manage embeddings',
url: 'https://supabase.com/docs/guides/ai',
},
],
},
{
id: 'day-2',
d: 2,
dd: 'Tue',
shipped: true,
isToday: false,
hasCountdown: false,
blog: '/blog/ai-inference-now-available-in-supabase-edge-functions',
date: '16 April',
published_at: '2024-04-16T08:00:00.000-07:00',
title: 'Supabase Functions now supports AI models',
description: (
<>
Supabase Functions now supports <strong>AI models</strong>
</>
),
links: [
{
type: 'blog',
href: '/blog/ai-inference-now-available-in-supabase-edge-functions',
},
{
type: 'video',
href: 'w4Rr_1whU-U',
},
{
type: 'xSpace',
href: 'https://supabase.link/twitter-space-ga-week-2',
},
],
steps: [
{
title: 'Supabase Functions now supports AI models',
blog: '#',
bg_layers: [
{
img: isDark
? '/images/launchweek/11/days/d2-dark.svg'
: '/images/launchweek/11/days/d2-light.svg',
mobileImg: isDark
? '/images/launchweek/11/days/d2-dark-mobile.svg'
: '/images/launchweek/11/days/d2-light-mobile.svg',
},
],
steps: [],
},
],
},
{
id: 'day-3',
d: 3,
dd: 'Wed',
shipped: true,
isToday: false,
hasCountdown: false,
blog: '/blog/anonymous-sign-ins',
date: '17 April',
published_at: '2024-04-17T08:00:00.000-07:00',
title: 'Supabase Auth now supports Anonymous sign-ins',
description: (
<>
Supabase Auth now supports <strong>Anonymous sign-ins</strong>
</>
),
links: [
{
type: 'blog',
href: '/blog/anonymous-sign-ins',
},
{
type: 'video',
href: 'WNN7Pp5Ftk4',
},
{
type: 'xSpace',
href: 'https://supabase.link/twitter-space-ga-week-3',
},
],
steps: [
{
title: 'Supabase Auth now supports Anonymous sign-ins',
blog: '/blog/anonymous-sign-ins',
bg_layers: [
{
img: isDark
? '/images/launchweek/11/days/d3-dark.svg'
: '/images/launchweek/11/days/d3-light.svg',
mobileImg: isDark
? '/images/launchweek/11/days/d3-dark-mobile.svg'
: '/images/launchweek/11/days/d3-light-mobile.svg',
},
],
steps: [],
},
],
},
{
id: 'day-4',
d: 4,
dd: 'Thu',
shipped: true,
isToday: false,
hasCountdown: false,
blog: '/blog/s3-compatible-storage',
date: '18 April',
published_at: '2024-04-18T08:00:00.000-07:00',
title: 'Supabase Storage: now supports the S3 protocol',
description: (
<>
Supabase Storage: now supports the <strong>S3 protocol</strong>
</>
),
links: [
{
type: 'blog',
href: '/blog/s3-compatible-storage',
},
{
type: 'video',
href: 'WvvGhcNeSPk',
},
{
type: 'xSpace',
href: 'https://supabase.link/twitter-space-ga-week-4',
},
],
steps: [
{
title: 'Supabase Storage: now supports the S3 protocol',
blog: '/blog/s3-compatible-storage',
bg_layers: [
{
img: isDark
? '/images/launchweek/11/days/d4-dark.svg'
: '/images/launchweek/11/days/d4-light.svg',
mobileImg: isDark
? '/images/launchweek/11/days/d4-dark-mobile.svg'
: '/images/launchweek/11/days/d4-light-mobile.svg',
},
],
steps: [],
},
],
},
{
id: 'day-5',
d: 5,
dd: 'Fri',
shipped: true,
isToday: false,
hasCountdown: false,
blog: '/blog/security-performance-advisor',
date: '19 April',
published_at: '2024-04-19T08:00:00.000-07:00',
title: 'Supabase Security Advisor & Performance Advisor',
description: (
<>
Supabase <strong>Security Advisor</strong> & <strong>Performance Advisor</strong>
</>
),
links: [
{
type: 'blog',
href: '/blog/security-performance-advisor',
},
{
type: 'video',
href: 'NZEbVe47DfA',
},
{
type: 'xSpace',
href: 'https://supabase.link/twitter-space-ga-week-5',
},
],
steps: [
{
title: 'Supabase Storage: now supports the S3 protocol',
blog: '/blog/s3-compatible-storage',
bg_layers: [
{
img: isDark
? '/images/launchweek/11/days/d5-dark.svg'
: '/images/launchweek/11/days/d5-light.svg',
mobileImg: isDark
? '/images/launchweek/11/days/d5-dark-mobile.svg'
: '/images/launchweek/11/days/d5-light-mobile.svg',
},
],
steps: [],
},
],
},
]
export default days

View File

@@ -22,7 +22,7 @@ interface Props {
const LWXGame = ({ setIsGameMode }: Props) => {
const { supabase, userData: user } = useConfData()
const phrase = process.env.NEXT_PUBLIC_LWX_GAME_WORD ?? 'its_just_postgres'
const phrase = process.env.NEXT_PUBLIC_LW_GAME_WORD ?? 'its_just_postgres'
const inputRef = useRef(null)
const winningPhrase = phrase?.split('_').map((word) => word.split(''))
const phraseLength = phrase?.replaceAll('_', '').split('').length
@@ -72,15 +72,18 @@ const LWXGame = ({ setIsGameMode }: Props) => {
if (supabase) {
if (user.id) {
await supabase
.from('lw11_tickets')
.update({ gameWonAt: new Date() })
.from('tickets')
.update({ game_won_at: new Date() })
.eq('launch_week', 'lw12')
.eq('username', user.username)
.then((res) => {
if (res.error) return console.log('error', res.error)
setIsGameMode(false)
})
await fetch(`/api-v2/ticket-og?username=${user.username}&secret=true`)
} else {
localStorage.setItem('lw11_secret', 'true')
localStorage.setItem('lw12_secret', 'true')
handleGithubSignIn()
}
@@ -136,7 +139,7 @@ const LWXGame = ({ setIsGameMode }: Props) => {
{winningCompliment}
</p>
<p className="text-foreground-lighter font-san text-sm">
Claim and share the secret ticket to boost your chances of winning swag.
Claim and share the secret ticket to boost your chances of winning limited swag.
</p>
</div>
<div

View File

@@ -0,0 +1,172 @@
import { useEffect, useRef, useState } from 'react'
import { codeBlock } from 'common-tags'
import { CodeBlock } from 'ui'
import { range } from 'lodash'
import { Pencil, X } from 'lucide-react'
import Tilt from 'vanilla-tilt'
import { useParams } from 'common'
import Panel from '~/components/Panel'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import TicketCustomizationForm from './TicketCustomizationForm'
import { themes as ticketThemes } from './ticketThemes'
export default function Ticket() {
const ticketRef = useRef<HTMLDivElement>(null)
const { userData: user, showCustomizationForm, setShowCustomizationForm } = useConfData()
const { platinum = false, secret: hasSecretTicket, ticket_number: ticketNumber, username } = user
const params = useParams()
const [responseTime, setResponseTime] = useState<{ start: number; end: number | undefined }>({
start: performance.now(),
end: undefined,
})
const sharePage = !!params.username
const ticketType = hasSecretTicket ? 'secret' : platinum ? 'platinum' : 'regular'
const TICKET_THEME = ticketThemes[ticketType]
function handleCustomizeTicket() {
setShowCustomizationForm && setShowCustomizationForm(!showCustomizationForm)
}
useEffect(() => {
user && setResponseTime((prev) => ({ ...prev, end: performance.now() }))
}, [user.id])
useEffect(() => {
if (ticketRef.current && !window.matchMedia('(pointer: coarse)').matches) {
Tilt.init(ticketRef.current, {
glare: true,
max: 3,
gyroscope: true,
'max-glare': 0.1,
'full-page-listening': true,
})
}
}, [ticketRef])
const code = codeBlock`await supabase
.from('tickets')
.select('*')
.eq('launch_week', 'lw12')
.eq('username', ${username})
.single()
`
const HAS_ROLE = user.role
const HAS_COMPANY = user.company
const HAS_LOCATION = user.location
// Keep following indentation for proper json layout with conditionals
const responseJson = codeBlock`
{
"data": {
"name": "${user.name}",
"username": "${username}",
"ticket_number": "${ticketNumber}",
${HAS_ROLE && ` "role": "${user.role}",\n`}${HAS_COMPANY && ` "company": "${user.company}",\n`}${HAS_LOCATION && ` "location": "${user.location}",\n`}},
"error": null
}
`
function getLinesToHighlight() {
let arr: any[] = range(0, 3)
const STARTING_LINE = 3
if (HAS_ROLE) arr.push(null)
if (HAS_COMPANY) arr.push(null)
if (HAS_LOCATION) arr.push(null)
return arr.map((_, i) => i + STARTING_LINE)
}
const LINES_TO_HIGHLIGHT = getLinesToHighlight()
const resTime = (responseTime.end! - responseTime.start).toFixed()
return (
<div
ref={ticketRef}
className="relative w-auto h-auto flex justify-center rounded-xl overflow-hidden will-change-transform"
style={{ transformStyle: 'preserve-3d', transform: 'perspective(1000px)' }}
>
<Panel
outerClassName="dark flex relative flex-col w-[360px] border h-auto max-h-[680px] rounded-xl !shadow-xl !p-0"
innerClassName="flex relative flex-col w-full transition-colors aspect-[3/4] rounded-xl text-left text-sm group/ticket"
shimmerFromColor="hsl(var(--border-strong))"
shimmerToColor="hsl(var(--background-default))"
style={{ transform: 'translateZ(-10px)', borderColor: TICKET_THEME.TICKET_BORDER }}
innerStyle={{ background: TICKET_THEME.TICKET_BACKGROUND }}
>
<div
className="w-full p-4 border-b flex flex-col gap-4"
style={{
color: TICKET_THEME.TICKET_BACKGROUND_CODE,
borderColor: TICKET_THEME.TICKET_BORDER,
backgroundColor: TICKET_THEME.TICKET_BACKGROUND_CODE,
}}
>
<span
className="uppercase tracking-wider"
style={{ color: TICKET_THEME.TICKET_FOREGROUND }}
>
<strong className="font-medium">Launch Week</strong> 12 Ticket
</span>
<CodeBlock
language="jsx"
hideCopy
theme={TICKET_THEME.CODE_THEME}
styleConfig={{
lineNumber: TICKET_THEME.CODE_LINE_NUMBER,
}}
className="not-prose !p-0 !bg-transparent border-none [&>code>span>span]:!leading-3 [&>code>span>span]:!min-w-2"
>
{code}
</CodeBlock>
</div>
<div className="w-full py-4 flex-grow flex flex-col gap-4">
<span
className="px-4 uppercase tracking-wider text-xs font-mono"
style={{ color: TICKET_THEME.CODE_LINE_NUMBER }}
>
TICKET RESPONSE
</span>
{user && (
<CodeBlock
language="json"
hideCopy
theme={TICKET_THEME.CODE_THEME}
linesToHighlight={LINES_TO_HIGHLIGHT}
styleConfig={{
lineNumber: TICKET_THEME.CODE_LINE_NUMBER,
highlightBackgroundColor: TICKET_THEME.CODE_HIGHLIGHT_BACKGROUND,
highlightBorderColor: TICKET_THEME.CODE_HIGHLIGHT_BORDER,
}}
highlightBorder
className="not-prose !p-0 !bg-transparent border-none [&>code>span>span]:!leading-3 [&>code>span>span]:!min-w-2 [&>code>span]:!pl-4"
>
{responseJson}
</CodeBlock>
)}
{/* <span className="px-4 text-xs" style={{ color: TICKET_THEME.TICKET_FOREGROUND_LIGHT }}>
{resTime}ms <span className="uppercase">Response time</span>
</span> */}
</div>
{/* Edit hover button */}
{!sharePage && (
<>
<button
className="absolute z-40 inset-0 w-full h-full outline-none"
onClick={handleCustomizeTicket}
/>
<div className="flex md:translate-y-3 opacity-100 md:opacity-0 group-hover/ticket:opacity-100 group-hover/ticket:md:translate-y-0 transition-all absolute z-30 right-4 top-4 md:inset-0 m-auto w-10 h-10 rounded-full items-center justify-center bg-surface-100 dark:bg-[#020405] border shadow-lg text-foreground">
{!showCustomizationForm ? <Pencil className="w-4" /> : <X className="w-4" />}
</div>
</>
)}
</Panel>
{!sharePage && (
<TicketCustomizationForm className="absolute inset-0 top-auto z-40 order-last md:order-first" />
)}
</div>
)
}

View File

@@ -0,0 +1,29 @@
import NextImage from 'next/image'
export default function TicketActions() {
return (
<div className="bg-surface-75 border border-muted w-full h-auto flex flex-row rounded-lg overflow-hidden gap-3 items-center pr-12">
<div className="relative flex items-center justify-center h-auto w-2/5 object-center border-muted overflow-hidden">
<NextImage
src="/images/launchweek/12/lw12-backpack-crop.png"
alt="Supabase LW12 Wandrd backpack"
draggable={false}
width={300}
height={300}
className="object-top mx-auto inset-x-0 w-auto h-full opacity-90 dark:opacity-50 pointer-events-none"
/>
</div>
<p className="text-foreground-light text-sm ">
Share your ticket to increase your chances of winning a{' '}
<a
href="https://www.wandrd.com/products/prvke?variant=39289416089680"
target="_blank"
className="text-foreground hover:text-brand transition"
>
Wandrd backpack
</a>{' '}
and other limited swag.
</p>
</div>
)
}

View File

@@ -0,0 +1,137 @@
import { useEffect, useRef, useState } from 'react'
import dayjs from 'dayjs'
import NextImage from 'next/image'
import Link from 'next/link'
import { LW_URL, TWEET_TEXT, TWEET_TEXT_PLATINUM, TWEET_TEXT_SECRET } from '~/lib/constants'
import { Button, cn, IconCheck } from 'ui'
import { useParams } from '~/hooks/useParams'
import LaunchWeekPrizeCard from '../LaunchWeekPrizeCard'
import TicketCopy from './TicketCopy'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
export default function TicketActions2() {
const { userData, supabase } = useConfData()
const { platinum, username, metadata, secret: hasSecretTicket } = userData
const [_imgReady, setImgReady] = useState(false)
const [_loading, setLoading] = useState(false)
const downloadLink = useRef<HTMLAnchorElement>()
const link = `${LW_URL}/tickets/${username}?lw=12${
hasSecretTicket ? '&secret=true' : platinum ? `&platinum=true` : ''
}&t=${dayjs(new Date()).format('DHHmmss')}`
const permalink = encodeURIComponent(link)
const text = hasSecretTicket ? TWEET_TEXT_SECRET : platinum ? TWEET_TEXT_PLATINUM : 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}`
const downloadUrl = `${process.env.NEXT_PUBLIC_SUPABASE_URL}/functions/v1/lw12-og?username=${encodeURIComponent(
username ?? ''
)}`
const params = useParams()
const sharePage = !!params.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') => {
if (!supabase) return
setTimeout(async () => {
if (social === 'twitter') {
await supabase
.from(TICKETS_TABLE)
.update({
shared_on_twitter: 'now',
metadata: { ...metadata, hasSharedSecret: hasSecretTicket },
})
.eq('launch_week', 'lw12')
.eq('username', username)
} else if (social === 'linkedin') {
await supabase
.from(TICKETS_TABLE)
.update({
shared_on_linkedin: 'now',
metadata: { ...metadata, hasSharedSecret: hasSecretTicket },
})
.eq('launch_week', 'lw12')
.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-col gap-4 ">
{/* <LabelBadge text="5 sets" /> */}
{/* <p className="text-foreground-light text-sm">
Boost your chances of winning Supabase LW12 Wandrd backpack and other awards.
</p> */}
<div
className={cn(
'w-full gap-2 flex flex-col items-center',
sharePage ? 'justify-center' : 'justify-between'
)}
>
<div className="flex flex-row w-full gap-2">
<Button
type="secondary"
size="small"
className="px-2 lg:px-3.5 h-[28px] lg:h-[34px] opacity-50"
disabled
icon={<IconCheck strokeWidth={3} className="hidden lg:block" />}
>
Ticket claimed
</Button>
<Button
onClick={() => handleShare('twitter')}
type={userData.shared_on_twitter ? 'secondary' : 'default'}
icon={
userData.shared_on_twitter && (
<IconCheck strokeWidth={3} className="hidden lg:block" />
)
}
size="small"
className="px-2 lg:px-3.5 h-[28px] lg:h-[34px]"
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 && (
<IconCheck strokeWidth={3} className="hidden lg:block" />
)
}
size="small"
className="px-2 lg:px-3.5 h-[28px] lg:h-[34px]"
asChild
>
<Link href={linkedInUrl} target="_blank">
{userData.shared_on_linkedin ? 'Shared on Linkedin' : 'Share on Linkedin'}
</Link>
</Button>
</div>{' '}
</div>
</div>
)
}

View File

@@ -0,0 +1,18 @@
import { Badge } from 'ui'
import Ticket from './Ticket'
import useConfData from '../../hooks/use-conf-data'
export default function TicketContainer() {
const { userData } = useConfData()
const hasSecretTicket = userData.secret
const hasPlatinumTicket = userData.platinum && !hasSecretTicket
return (
<div className="flex flex-col w-full items-center mx-auto max-w-xl gap-3 group group-hover">
{hasSecretTicket && <Badge variant="outline">Secret ticket</Badge>}
{hasPlatinumTicket && <Badge variant="outline">Platinum ticket</Badge>}
<Ticket />
</div>
)
}

View File

@@ -0,0 +1,46 @@
import { useState, useRef } from 'react'
import { LW_URL } from '~/lib/constants'
import { Check, Copy } from 'lucide-react'
import useConfData from '../../hooks/use-conf-data'
export default function TicketCopy() {
const { userData } = useConfData()
const { username, platinum, secret } = userData
const [copied, setCopied] = useState(false)
const buttonRef = useRef<HTMLButtonElement>(null)
const hasSecretTicket = secret
const displayUrl = `.../launch-week/tickets/${username}?lw=12${
hasSecretTicket ? '&secret=true' : platinum ? `&platinum=true` : ''
}`
const url = `${LW_URL}/tickets/${username}?lw=12${
hasSecretTicket ? '&secret=true' : platinum ? `&platinum=true` : ''
}`
return (
// <div className="h-full w-full">
<button
type="button"
name="Copy"
ref={buttonRef}
onClick={() => {
navigator.clipboard.writeText(url).then(() => {
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 2000)
})
}}
className="font-mono w-full flex justify-center items-center gap-2 relative text-foreground-light hover:text-foreground text-sm"
>
<div className="w-4 min-w-4 flex-shrink-0">
{copied ? (
<Check size={14} strokeWidth={3} className="text-foreground" />
) : (
<Copy size={14} strokeWidth={1.5} />
)}
</div>
<span className="truncate">{displayUrl}</span>
</button>
// </div>
)
}

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'
import { Button, Checkbox, Input, cn } from 'ui'
import { Button, Input, cn } from 'ui'
import { Check } from 'lucide-react'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import { useBreakpoint, useDebounce } from 'common'
@@ -14,9 +14,9 @@ const TicketCustomizationForm = ({ className }: { className?: string }) => {
setShowCustomizationForm,
} = useConfData()
const defaultFormValues = {
role: user.metadata?.role,
company: user.metadata?.company,
hideAvatar: !!user.metadata?.hideAvatar,
role: user.role,
company: user.company,
location: user.location,
}
const [formData, setFormData] = useState(defaultFormValues)
const [formState, setFormState] = useState<'idle' | 'saved' | 'saving' | 'error'>('idle')
@@ -32,17 +32,13 @@ const TicketCustomizationForm = ({ className }: { className?: string }) => {
const handleFormSubmit = async () => {
setFormState('saving')
const payload = {
metadata: {
...user.metadata,
...formData,
},
}
const payload = formData
if (supabase) {
await supabase
.from('lw11_tickets')
.from('tickets')
.update(payload)
.eq('launch_week', 'lw12')
.eq('username', user.username)
.then((res) => {
if (res.error) return setFormState('error')
@@ -125,23 +121,39 @@ const TicketCustomizationForm = ({ className }: { className?: string }) => {
/>
}
/>
<Checkbox
name="hideAVatar"
label="Hide avatar"
checked={formData.hideAvatar}
<Input
size="small"
type="text"
placeholder="location (optional)"
value={formData.location}
maxLength={25}
onChange={(event) => {
handleInputChange('hideAvatar', event.target.checked)
handleInputChange('location', event.target.value)
}}
onFocus={() =>
!showCustomizationForm && setShowCustomizationForm && setShowCustomizationForm(true)
}
inputClassName={cn(IS_SAVING && 'text-foreground-lighter')}
icon={
<Check
strokeWidth={2}
className={cn(
'w-3',
!!formData.location ? 'text-brand' : 'text-foreground-lighter',
IS_SAVING && 'text-background-surface-300'
)}
/>
}
/>
<div className="flex items-center justify-center md:justify-between gap-2 mt-2">
<div className="hidden md:inline opacity-0 animate-fade-in text-xs text-foreground-light">
<div className="flex flex-col lg:flex-row items-center justify-center md:justify-between gap-2 mt-2">
<div className="inline opacity-0 animate-fade-in text-xs text-foreground-light">
{IS_SAVED && (
<span className="hidden md:inline opacity-0 animate-fade-in text-xs text-foreground-light">
<span className="inline opacity-0 animate-fade-in text-xs text-foreground-light">
Saved
</span>
)}
{HAS_ERROR && (
<span className="hidden md:inline opacity-0 animate-fade-in text-xs text-foreground-light">
<span className="inline opacity-0 animate-fade-in text-xs text-foreground-light">
Something went wrong
</span>
)}

View File

@@ -19,76 +19,90 @@ export default function TicketForm() {
const router = useRouter()
// Triggered on session
async function fetchUser() {
async function fetchOrCreateUser() {
if (supabase && session?.user && !userData.id) {
const username = session.user.user_metadata.user_name
const name = session.user.user_metadata.full_name
const email = session.user.email
const userId = session.user.id
await supabase
.from('lw11_tickets')
.insert({
email,
name,
username,
referred_by: router.query?.referral ?? null,
})
.eq('email', email)
.select()
.single()
.then(async ({ error }: any) => {
// If error because of duplicate email, ignore and proceed, otherwise sign out.
if (error && error?.code !== '23505') {
setFormState('error')
return supabase.auth.signOut()
if (!userData.id) {
await supabase
.from('tickets')
.insert({
user_id: userId,
launch_week: 'lw12',
email,
name,
username,
referred_by: router.query?.referral ?? null,
})
.eq('email', email)
.select()
.single()
.then(({ error }: any) => fetchUser({ error, username }))
}
}
}
const fetchUser = async ({ error, username }: any) => {
if (!supabase) return
// If error because of duplicate email, ignore and proceed, otherwise sign out.
if (error && error?.code !== '23505') {
setFormState('error')
return supabase.auth.signOut()
}
const { data } = await supabase
.from('tickets_view')
.select('*')
.eq('launch_week', 'lw12')
.eq('username', username)
.single()
if (data) setUserData(data)
setFormState('default')
// Prefetch GitHub avatar
new Image().src = `https://github.com/${username}.png`
console.log('about to fetch username page to generate the og')
// Prefetch the twitter share URL to eagerly generate the page
// fetch(`/launch-week/tickets/${username}?came_from_signup=true`).catch((_) => {})
await fetch(`/api-v2/ticket-og?username=${username}`)
console.log('should have fetched')
if (!realtimeChannel) {
const channel = supabase
.channel('changes')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'tickets',
filter: `username=eq.${username}`,
},
(payload: any) => {
const platinum = !!payload.new.shared_on_twitter && !!payload.new.shared_on_linkedin
const secret = !!payload.new.game_won_at
setUserData({
...payload.new,
platinum,
secret,
})
}
const { data } = await supabase
.from('lw11_tickets_platinum')
.select('*')
.eq('username', username)
.single()
if (data) {
setUserData(data)
}
setFormState('default')
// Prefetch GitHub avatar
new Image().src = `https://github.com/${username}.png`
// Prefetch the twitter share URL to eagerly generate the page
fetch(`/launch-week/tickets/${username}`).catch((_) => {})
if (!realtimeChannel) {
const channel = supabase
.channel('changes')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'lw11_tickets',
filter: `username=eq.${username}`,
},
(payload: any) => {
const platinum = !!payload.new.sharedOnTwitter && !!payload.new.sharedOnLinkedIn
const secret = !!payload.new.gameWonAt
setUserData({
...payload.new,
platinum,
secret,
})
}
)
.subscribe()
setRealtimeChannel(channel)
}
})
)
.subscribe()
setRealtimeChannel(channel)
}
}
useEffect(() => {
fetchUser()
fetchOrCreateUser()
return () => {
// Cleanup realtime subscription on unmount
@@ -105,7 +119,7 @@ export default function TicketForm() {
setFormState('loading')
setTicketState('loading')
const redirectTo = `${SITE_ORIGIN}/ga-week/${
const redirectTo = `${SITE_ORIGIN}/launch-week/${
userData.username ? '?referral=' + userData.username : ''
}`
@@ -133,13 +147,11 @@ export default function TicketForm() {
) : (
<div className="flex flex-col gap-10 items-start justify-center relative z-20">
<Button
size="tiny"
type="alternative"
size="small"
disabled={formState === 'loading' || Boolean(session)}
onClick={handleGithubSignIn}
iconLeft={session && <CheckCircle />}
loading={formState === 'loading'}
className="px-4 h-auto !py-1.5"
>
Claim your ticket
</Button>

View File

@@ -1,8 +1,8 @@
import React, { useEffect, useState } from 'react'
import { SupabaseClient } from '@supabase/supabase-js'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import solutions from '~/data/Solutions'
import { cn } from 'ui'
import { Dot } from 'lucide-react'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
const TicketPresence = (props: { className?: string }) => {
const { supabase, ticketState } = useConfData()
@@ -47,28 +47,14 @@ const TicketPresence = (props: { className?: string }) => {
return (
<div
className={cn(
'text-foreground-muted text-xs flex items-center transition-opacity',
'text-foreground-lighter text-xs flex items-center transition-opacity',
hasTicket && 'text-sm opacity-80',
props.className
)}
>
<svg
className="h-5 w-5 stroke-foreground-lighter animate-pulse mr-2"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
stroke="text-foreground"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d={solutions.realtime.icon}
/>
</svg>
<Dot className="text-brand animate-pulse -ml-2" />
{onlineUsers.length} {isSingular ? 'person is' : 'people are'}{' '}
{hasTicket ? 'customizing' : 'generating'} their ticket right now
{hasTicket ? 'customizing' : 'generating'} their ticket
</div>
)
}

View File

@@ -0,0 +1,193 @@
import React from 'react'
import dynamic from 'next/dynamic'
import { AnimatePresence, m, LazyMotion, domAnimation } from 'framer-motion'
import { Badge, cn } from 'ui'
import { DEFAULT_TRANSITION, INITIAL_BOTTOM, getAnimation } from '~/lib/animations'
import { LW12_DATE, LW12_LAUNCH_DATE } from '~/lib/constants'
import useWinningChances from '../../hooks/useWinningChances'
import useLwGame from '../../hooks/useLwGame'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import SectionContainer from '~/components/Layouts/SectionContainer'
import TicketContainer from './TicketContainer'
import TicketForm from './TicketForm'
import CountdownComponent from '../Countdown'
import TicketPresence from './TicketPresence'
import TicketActions from './TicketActions'
import LW12Background from '../LW12Background'
import TicketCopy from './TicketCopy'
import TicketActions2 from './TicketActions2'
const LWGame = dynamic(() => import('./LW12Game'))
const TicketingFlow = () => {
const { ticketState, userData, showCustomizationForm } = useConfData()
const { isGameMode, setIsGameMode } = useLwGame(ticketState !== 'ticket' || showCustomizationForm)
const isLoading = !isGameMode && ticketState === 'loading'
const isRegistering = !isGameMode && ticketState === 'registration'
const hasTicket = !isGameMode && ticketState === 'ticket'
const hasPlatinumTicket = userData.platinum
const hasSecretTicket = userData.secret
const transition = DEFAULT_TRANSITION
const initial = INITIAL_BOTTOM
const animate = getAnimation({ duration: 1 })
const exit = { opacity: 0, transition: { ...transition, duration: 0.2 } }
const winningChances = useWinningChances()
const DISPLAY_NAME = userData?.name || userData?.username
const FIRST_NAME = DISPLAY_NAME?.split(' ')[0]
return (
<>
<SectionContainer className="relative h-full flex-1">
<div className="relative z-10 flex h-full">
<h1 className="sr-only">Supabase Launch Week 12 | {LW12_DATE}</h1>
<LazyMotion features={domAnimation}>
<AnimatePresence mode="wait" key={ticketState}>
{isLoading && (
<m.div
key="loading"
initial={exit}
animate={animate}
exit={exit}
className="relative w-full min-h-[400px] mx-auto py-16 md:py-24 flex flex-col items-center gap-6 text-foreground"
>
<div className="hidden">
<TicketForm />
</div>
<svg
className="animate-spinner opacity-50 w-5 h-5 md:w-6 md:h-6"
width="100%"
height="100%"
viewBox="0 0 62 61"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M61 31C61 14.4315 47.5685 1 31 1C14.4315 1 1 14.4315 1 31"
stroke="white"
strokeWidth="2"
/>
</svg>
</m.div>
)}
{isRegistering && (
<m.div
key="registration"
initial={initial}
animate={animate}
exit={exit}
className={cn(
'w-full min-h-[400px] max-w-3xl mx-auto text-left md:text-center flex flex-col md:items-center justify-center gap-6 lg:gap-8 opacity-0 invisible',
!isGameMode && !hasTicket && 'opacity-100 visible'
)}
>
<div>
<CountdownComponent
date={LW12_LAUNCH_DATE}
showCard={false}
className="[&_*]:leading-4 text-foreground-lighter"
size="large"
/>
</div>
<div className="flex flex-col md:items-center gap-6">
<div className="flex flex-col md:flex-row flex-wrap gap-2 md:gap-6 uppercase text-2xl tracking-wider">
<h2 className="text-foreground">
<strong className="font-medium">Launch Week</strong> 12
</h2>
<span className="h-full border-r border-foreground hidden md:inline" />
<p className="text-foreground-light">{LW12_DATE}</p>
</div>
<p className="text-foreground-lighter text-lg ">
Join us for a week of new features and level up your development
</p>
</div>
<TicketForm />
</m.div>
)}
{hasTicket && (
<m.div
key="ticket"
initial={initial}
animate={animate}
exit={exit}
className="w-full flex-1 min-h-[400px] h-full flex flex-col xl:flex-row items-center xl:justify-center xl:items-center gap-8 md:gap-10 xl:gap-32 text-foreground text-center md:text-left"
>
<div className="w-full lg:w-auto h-full mt-3 md:mt-6 xl:mt-0 max-w-lg flex flex-col items-center justify-center gap-3">
<TicketContainer />
{/* {!hasPlatinumTicket && <TicketPresence />} */}
{/* // not sure why this was only non platinum */}
<TicketPresence />
<TicketCopy />
</div>
<div className="order-first xl:h-full w-full max-w-lg gap-8 flex flex-col items-center justify-center xl:items-start xl:justify-center text-center xl:text-left">
{hasPlatinumTicket ? (
<div>
{hasSecretTicket ? (
<p className="text-2xl mb-1">Share the secret ticket to beat the odds</p>
) : (
<p className="text-2xl mb-1">Thanks for sharing</p>
)}
<p className="text-2xl text-foreground-light">
Follow Launch Week 12 announcements to find out if you're a lucky winner.
</p>
</div>
) : winningChances !== 2 ? (
<div>
{hasSecretTicket && (
<p className="text-2xl mb-1">You found a secret ticket</p>
)}
{!hasSecretTicket && (
<p className="text-2xl mb-1">You're in {FIRST_NAME}!</p>
)}
<p className="text-2xl text-foreground-light">
Share your ticket to win limited swag.
</p>
</div>
) : (
<div>
<p className="text-2xl mb-1">Almost there {FIRST_NAME}!</p>
<p className="text-2xl text-foreground-light">
Share on {!userData.shared_on_linkedin ? 'LinkedIn' : 'Twitter'} to
increase your chances of winning limited swag.
</p>
</div>
)}
<TicketActions2 />
<div className="w-full my-3">
<TicketActions />
</div>
<CountdownComponent date={LW12_LAUNCH_DATE} showCard={false} />
</div>
</m.div>
)}
{/* {!showCustomizationForm && isGameMode && (
<m.div
key="ticket"
initial={initial}
animate={animate}
exit={exit}
className="w-full flex justify-center text-foreground !h-[500px]"
>
<LWGame setIsGameMode={setIsGameMode} />
</m.div>
)} */}
</AnimatePresence>
</LazyMotion>
</div>
</SectionContainer>
<LW12Background
className={cn(
'opacity-100 transition-opacity',
hasTicket || (isGameMode && 'opacity-80 dark:opacity-60')
)}
/>
</>
)
}
export default TicketingFlow

View File

@@ -0,0 +1,164 @@
export const themes = {
regular: {
OG_BACKGROUND: '#121212',
TICKET_BORDER: '#242424',
TICKET_FOREGROUND: '#FAFAFA',
TICKET_FOREGROUND_LIGHT: '#B4B4B4',
TICKET_BACKGROUND: '#1F1F1F',
TICKET_BACKGROUND_CODE: '#171717',
CODE_HIGHLIGHT_BACKGROUND: '#292929',
CODE_HIGHLIGHT_BORDER: '',
CODE_LINE_NUMBER: '#4D4D4D',
CODE_THEME: {
hljs: {
color: '#fff', // corresponds to --ray-foreground
},
'hljs-keyword': {
color: '#bda4ff', // corresponds to --ray-token-keyword
},
'hljs-number': {
color: '#ffffff', // corresponds to --ray-token-constant
},
'hljs-string': {
color: '#ffcda1', // corresponds to --ray-token-string
fontWeight: 'bold',
},
'hljs-comment': {
color: 'purple', // corresponds to --ray-token-comment
},
'hljs-params': {
color: '#ffffff', // corresponds to --ray-token-parameter
},
'hljs-function': {
color: '3ecf8e', // corresponds to --ray-token-function
},
'hljs-variable': {
color: '#ffcda1', // corresponds to --ray-token-string-expression
},
'hljs-punctuation': {
color: '#ffffff', // corresponds to --ray-token-punctuation
},
'hljs-link': {
color: '#ffffff', // corresponds to --ray-token-link
},
'hljs-literal': {
color: '#ffffff', // corresponds to --ray-token-number
},
'hljs-attr': {
color: '#3ecf8e', // corresponds to --ray-token-property
},
},
},
platinum: {
OG_BACKGROUND: '#121212',
TICKET_BORDER: '#242424',
TICKET_FOREGROUND: '#FAFAFA',
TICKET_FOREGROUND_LIGHT: '#888888',
TICKET_BACKGROUND: '#212427',
TICKET_BACKGROUND_CODE: '#24292D',
CODE_HIGHLIGHT_BACKGROUND: '#272B2E',
CODE_HIGHLIGHT_BORDER: '',
CODE_LINE_NUMBER: '#4D4D4D',
CODE_THEME: {
hljs: {
color: '#abb2bf', // General text color
},
'hljs-keyword': {
color: '#c678dd', // Keywords like "await"
},
'hljs-string': {
color: '#98c379', // Strings
fontWeight: 'bold',
},
'hljs-title': {
color: '#e06c75', // Titles (like function names)
},
'hljs-variable': {
color: '#e06c75', // Variables
},
'hljs-attr': {
color: '#d19a66', // Attributes
},
'hljs-number': {
color: '#d19a66', // Numbers
},
'hljs-comment': {
color: '#5c6370', // Comments
},
'hljs-function': {
color: '#61afef', // Function names
},
'hljs-params': {
color: '#abb2bf', // Parameters
},
'hljs-punctuation': {
color: '#abb2bf', // Punctuation
},
'hljs-meta': {
color: '#abb2bf', // Meta
},
'hljs-literal': {
color: '#56b6c2', // Literal values
},
'hljs-link': {
color: '#61afef', // Links
},
},
},
secret: {
OG_BACKGROUND: '#0F2BE6',
TICKET_BORDER: '#3059F2',
TICKET_FOREGROUND: '#EDEDED',
TICKET_FOREGROUND_LIGHT: '#EDEDED',
TICKET_BACKGROUND: '#0F2BE6',
TICKET_BACKGROUND_CODE: '#0000B4',
CODE_HIGHLIGHT_BACKGROUND: '#3059F2',
CODE_HIGHLIGHT_BORDER: '#73B2FA',
CODE_LINE_NUMBER: '#5F7BF6',
CODE_THEME: {
hljs: {
color: '#fff', // General text color
},
'hljs-keyword': {
color: '#fff', // Keywords like "await"
},
'hljs-string': {
color: 'white', // Strings
fontWeight: 'bold',
},
'hljs-title': {
color: '#e06c75', // Titles (like function names)
},
'hljs-variable': {
color: '#e06c75', // Variables
},
'hljs-attr': {
color: '#fff', // Attributes
},
'hljs-number': {
color: '#d19a66', // Numbers
},
'hljs-comment': {
color: '#5c6370', // Comments
},
'hljs-function': {
color: '#61afef', // Function names
},
'hljs-params': {
color: '#abb2bf', // Parameters
},
'hljs-punctuation': {
color: '#abb2bf', // Punctuation
},
'hljs-meta': {
color: '#abb2bf', // Meta
},
'hljs-literal': {
color: '#56b6c2', // Literal values
},
'hljs-link': {
color: '#61afef', // Links
},
},
},
}

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'
import TicketHeader from '../Ticket/TicketHeader'
import TicketNumber from '../Ticket/TicketNumber'
// import TicketHeader from '../Ticket/TicketHeader'
// import TicketNumber from '../Ticket/TicketNumber'
import { UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
import Image from 'next/image'
@@ -11,7 +11,7 @@ interface Props {
export function TicketBrick({ user }: Props) {
const [isLoading, setLoading] = useState(true)
const golden = user.sharedOnLinkedIn && user.sharedOnTwitter
const golden = user.shared_on_linkedin && user.shared_on_twitter
// reg_bg_57.png
const baseImagePath = `https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/lw7/tickets_bg/`
@@ -41,7 +41,7 @@ export function TicketBrick({ user }: Props) {
alt=""
/>
<div className="z-20 relative">
<TicketHeader size="small" />
{/* <TicketHeader size="small" /> */}
<div className="rounded-full grid gap-4 mx-auto justify-center mt-8">
<img
src={`https://github.com/${user.username}.png`}
@@ -53,7 +53,7 @@ export function TicketBrick({ user }: Props) {
{user.name && <p className="gradient-text-100 text-sm">@{user.username}</p>}
</div>
</div>
<TicketNumber number={user.ticketNumber} size="small" />
{/* <TicketNumber number={user.ticketNumber} size="small" /> */}
</div>
</div>
</>

View File

@@ -1,189 +0,0 @@
import cn from 'classnames'
import { useRef, useState } from 'react'
import styles from './ticket.module.css'
import styleUtils from '../../utils.module.css'
import TicketVisual from './TicketVisual'
import TicketActions from './TicketActions'
import TicketCopy from './ticket-copy'
import { UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
import ReferralIndicator from '../ReferralIndicator'
import useWinningChances from '../../hooks/useWinningChances'
import { SITE_URL } from '~/lib/constants'
import { useBreakpoint } from 'common/hooks/useBreakpoint'
type TicketGenerationState = 'default' | 'loading'
type Props = {
username: UserData['username']
ticketNumber: UserData['ticketNumber']
name: UserData['name']
golden: UserData['golden']
bgImageId: UserData['bg_image_id']
referrals: number
sharePage?: boolean
}
export default function Ticket({
username,
name,
ticketNumber,
sharePage,
golden,
bgImageId,
referrals,
}: Props) {
const isMobile = useBreakpoint(1023)
const [ticketGenerationState, setTicketGenerationState] =
useState<TicketGenerationState>('default')
const divRef = useRef<HTMLDivElement>(null)
const winningChances = useWinningChances()
return (
<div
className={[
`relative w-full max-w-screen md:max-w-[700px] lg:max-w-[1100px] flex flex-col items-center lg:grid lg:grid-cols-12 gap-4 lg:gap-8 lg:p-2 rounded-3xl backdrop-blur lg:items-stretch h-auto"`,
!isMobile && styles['ticket-hero'],
].join(' ')}
id="wayfinding--ticket-visual-wrapper-container"
>
<div
className={cn(styles['ticket-visual-wrapper'], 'flex-1 col-span-8')}
id="wayfinding--ticket-visual-wrapper"
>
<div
className={cn(
styles['ticket-visual'],
styleUtils.appear,
styleUtils['appear-fourth'],
'relative flex flex-col items-center gap-2 w-full h-fit rounded-xl'
)}
id="wayfinding--ticket-visual-outer-container"
>
<TicketVisual
username={username ?? undefined}
name={name ?? undefined}
ticketNumber={ticketNumber ?? 0}
ticketGenerationState={ticketGenerationState}
setTicketGenerationState={setTicketGenerationState}
golden={golden}
bgImageId={bgImageId}
/>
{username && (
<div className="w-full">
<TicketCopy username={username} isGolden={golden} />
</div>
)}
</div>
</div>
<div
ref={divRef}
className={[
'flex flex-col !w-full h-full justify-center col-span-full p-2 pt-4 lg:p-0 lg:col-span-4 mt-1 lg:m-0 lg:pr-8 max-h-[400px] rounded-3xl backdrop-blur lg:backdrop-blur-none',
isMobile && styles['ticket-hero'],
].join(' ')}
>
<div className="text-foreground flex flex-col w-full items-center text-white text-center lg:text-left lg:items-start gap-2 lg:gap-3 mb-3 lg:mb-6">
<h1
className={cn(
styleUtils.appear,
styleUtils['appear-first'],
'text-xl text-white lg:text-3xl '
)}
>
{!sharePage ? (
<>
{name ? (
<>
{winningChances === 1 && (
<span className="text-2xl tracking-[0.02rem] leading-7 block">
You're <span className="gradient-text-purple-800">in the draw!</span> <br />
Now make it gold.
</span>
)}
{winningChances === 2 && (
<span className="text-2xl tracking-[0.02rem] leading-7 block">
You've <span className="gradient-text-purple-800">doubled</span> your{' '}
<br className="hidden lg:inline" />
chance!
<br className="inline lg:hidden" /> Almost{' '}
<span className={styles['gold-text']}>gold</span>.
</span>
)}
{winningChances === 3 && (
<span className="text-2xl tracking-[0.02rem] leading-7 block">
You're <span className={styles['gold-text']}>gold</span>!<br />
You've maxed your <br className="hidden lg:inline" /> chances of winning!
</span>
)}
</>
) : (
<span className="text-2xl leading-7 block">
Generate your ticket. <br />
Win the <span className="gradient-text-purple-800">SupaKeyboard</span>.
</span>
)}
</>
) : (
<span className="tracking-[-0.02px] leading-7 block">
{name ? name : username}'s <br className="hidden lg:inline" />
unique ticket
</span>
)}
</h1>
<div className="text-base text-white leading-5 max-w-[520px]">
{!sharePage ? (
<>
{golden ? (
<p>
Join us on April 16th for Launch Week 7's final day and find out if you are one
of the lucky winners.
</p>
) : username ? (
<p>
Why stop there? Increase your chances of winning by sharing your unique ticket.
Get sharing!
</p>
) : (
<p>
We have some fantastic swag up for grabs, including 3 limited-edition mechanical
keyboards that you won't want to miss.
</p>
)}
</>
) : (
<>
<p>
Get yours and win some fantastic swag, including a limited-edition mechanical
keyboard that you won't want to miss.
</p>
<div className="mt-4 lg:mt-8 rounded-full bg-[#E6E8EB] py-1 px-3 -mb-3 border border-[#bbbbbb] text-xs transition-all ease-out hover:bg-[#dfe1e3]">
<a
href={`${SITE_URL}/${username ? '?referral=' + username : ''}`}
className={`flex items-center justify-center gap-2 text-[#2e2e2e]`}
>
Go to Launch Week 7
</a>
</div>
</>
)}
</div>
{!sharePage && username && <ReferralIndicator />}
</div>
<div>
{username && (
<TicketActions
username={username}
golden={golden}
ticketGenerationState={ticketGenerationState}
setTicketGenerationState={setTicketGenerationState}
/>
)}
</div>
</div>
</div>
)
}

View File

@@ -1,126 +0,0 @@
import { useEffect, useRef, useState } from 'react'
import { SITE_URL, TWEET_TEXT, TWEET_TEXT_GOLDEN } from '~/lib/constants'
import { IconCheckCircle } from 'ui'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import { useParams } from '~/hooks/useParams'
import TicketForm from './TicketForm'
type TicketGenerationState = 'default' | 'loading'
type Props = {
username: string
golden?: boolean
ticketGenerationState?: TicketGenerationState
setTicketGenerationState: (ticketGenerationState: TicketGenerationState) => void
}
export default function TicketActions({
username,
golden = false,
ticketGenerationState,
setTicketGenerationState,
}: Props) {
const [imgReady, setImgReady] = useState(false)
const [loading, setLoading] = useState(false)
const downloadLink = useRef<HTMLAnchorElement>()
const permalink = (medium: string) =>
encodeURIComponent(`${SITE_URL}/tickets/${username}?lw=7${golden ? `&golden=true` : ''}`)
const text = encodeURIComponent(golden ? TWEET_TEXT_GOLDEN : TWEET_TEXT)
const { userData } = useConfData()
const tweetUrl = `https://twitter.com/intent/tweet?url=${permalink(
'twitter'
)}&via=supabase&text=${text}`
const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${permalink('linkedin')}`
const downloadUrl = `https://obuldanrptloktxcffvn.supabase.co/functions/v1/lw7-ticket-og?username=${encodeURIComponent(
username
)}`
const params = useParams()
const sharePage = params.username
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])
return (
<div className="grid gap-1 grid-cols-1 sm:grid-cols-3 lg:grid-cols-1">
{!sharePage ? (
<>
<div className="rounded-full bg-[#E6E8EB] text-background-surface-300 py-1 px-3 border border-[#dfe1e3] text-xs mb-1">
<div className="flex items-center justify-center gap-2">
<div className="text-muted">
<IconCheckCircle size={10} strokeWidth={1} />
</div>
Connect with GitHub
</div>
</div>
<div
className={`rounded-full ${
userData.sharedOnTwitter ? 'bg-[#E6E8EB] text-background-surface-300' : 'text-white'
} text-background-surface-300 py-1 px-3 border border-[#dfe1e3] text-xs mb-1 transition-all ease-out hover:bg-[#dfe1e3]`}
>
<a
href={tweetUrl}
rel="noopener noreferrer prefetch"
target="_blank"
className={`flex items-center justify-center gap-2 ${
userData.sharedOnTwitter
? 'text-background-surface-300'
: 'text-white hover:text-background-surface-300'
}`}
>
{userData.sharedOnTwitter && (
<div className="text-muted">
<IconCheckCircle size={10} strokeWidth={1} />
</div>
)}
Share on Twitter
</a>
</div>
<div
className={`rounded-full ${
userData.sharedOnLinkedIn ? 'bg-[#E6E8EB] text-background-surface-300' : 'text-white'
} text-background-surface-300 py-1 px-3 border border-[#dfe1e3] text-xs mb-1 transition-all ease-out hover:bg-[#dfe1e3]`}
>
<a
href={linkedInUrl}
rel="noopener noreferrer prefetch"
target="_blank"
className={`flex items-center justify-center gap-2 ${
userData.sharedOnLinkedIn
? 'text-background-surface-300'
: 'text-white hover:text-background-surface-300'
}`}
>
{userData.sharedOnLinkedIn && (
<div className="text-muted">
<IconCheckCircle size={10} strokeWidth={1} />
</div>
)}
Share on Linkedin
</a>
</div>
</>
) : (
!username && (
<TicketForm
defaultUsername={username ?? undefined}
ticketGenerationState={ticketGenerationState}
setTicketGenerationState={setTicketGenerationState}
/>
)
)}
</div>
)
}

View File

@@ -1,51 +0,0 @@
import { useState } from 'react'
import { TicketState, ConfDataContext, UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
import Ticket from './ActualTicket'
import Form from './form'
import { SupabaseClient, Session } from '@supabase/supabase-js'
type Props = {
supabase: SupabaseClient
session: Session | null
defaultUserData: UserData
sharePage?: boolean
defaultTicketState?: TicketState
}
export default function Conf({
supabase,
session,
defaultUserData,
sharePage,
defaultTicketState = 'registration',
}: Props) {
const [userData, setUserData] = useState<UserData>(defaultUserData)
const [ticketState, setTicketState] = useState<TicketState>(defaultTicketState)
return (
<ConfDataContext.Provider
value={{
supabase,
session,
userData,
setUserData,
ticketState,
setTicketState,
}}
>
{ticketState === 'registration' && !sharePage ? (
<Form align={defaultTicketState === 'registration' ? 'Center' : 'Left'} />
) : (
<Ticket
username={userData.username}
name={userData.name}
ticketNumber={userData.ticketNumber}
sharePage={sharePage}
golden={userData.golden}
bgImageId={userData.bg_image_id}
referrals={userData.referrals ?? 0}
/>
)}
</ConfDataContext.Provider>
)
}

View File

@@ -1,177 +0,0 @@
import { useState, useRef, useEffect } from 'react'
import { useRouter } from 'next/router'
import cn from 'classnames'
import { SITE_ORIGIN } from '~/lib/constants'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import formStyles from './form.module.css'
import ticketFormStyles from './ticket-form.module.css'
import { Button, IconCheckCircle, IconLoader } from 'ui'
import { SupabaseClient } from '@supabase/supabase-js'
type FormState = 'default' | 'loading' | 'error'
type TicketGenerationState = 'default' | 'loading'
type Props = {
defaultUsername?: string
ticketGenerationState?: TicketGenerationState
setTicketGenerationState: any
}
export default function TicketForm({ defaultUsername = '', setTicketGenerationState }: Props) {
const [username, setUsername] = useState(defaultUsername)
const [formState, setFormState] = useState<FormState>('default')
const [errorMsg] = useState('')
const { supabase, session, setUserData, setTicketState, userData } = useConfData()
const [realtimeChannel, setRealtimeChannel] = useState<ReturnType<
SupabaseClient['channel']
> | null>(null)
const formRef = useRef<HTMLFormElement>(null)
const router = useRouter()
useEffect(() => {
if (supabase && session?.user && !userData.id) {
document.body.classList.add('ticket-generated')
const username = session.user.user_metadata.user_name
setUsername(username)
const name = session.user.user_metadata.full_name
const email = session.user.email
supabase
.from('lw7_tickets')
.insert({ email, name, username, referred_by: router.query?.referral ?? null })
.eq('email', email)
.select()
.single()
.then(async ({ error }: any) => {
// If error because of duplicate email, ignore and proceed, otherwise sign out.
if (error && error?.code !== '23505') return supabase.auth.signOut()
const { data } = await supabase
.from('lw7_tickets_golden')
.select('*')
.eq('username', username)
.single()
if (data) {
setUserData(data)
}
setFormState('default')
// Prefetch GitHub avatar
new Image().src = `https://github.com/${username}.png`
// Prefetch the twitter share URL to eagerly generate the page
fetch(`/launch-week/7/tickets/${username}`).catch((_) => {})
// Prefetch ticket og image.
fetch(
`https://obuldanrptloktxcffvn.supabase.co/functions/v1/lw7-ticket-og?username=${encodeURIComponent(
username ?? ''
)}`
).catch((_) => {})
setTicketState('ticket')
// Listen to realtime changes
if (!realtimeChannel && !data?.golden) {
const channel = supabase
.channel('changes')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'lw7_tickets',
filter: `username=eq.${username}`,
},
(payload: any) => {
const golden = !!payload.new.sharedOnTwitter && !!payload.new.sharedOnLinkedIn
setUserData({
...payload.new,
golden,
})
if (golden) {
channel.unsubscribe()
}
}
)
.subscribe()
setRealtimeChannel(channel)
}
})
}
return () => {
// Cleanup realtime subscription on unmount
realtimeChannel?.unsubscribe()
}
}, [session])
return formState === 'error' ? (
<div className="h-full">
<div className={cn(formStyles['form-row'], ticketFormStyles['form-row'])}>
<div className={cn(formStyles['input-label'], formStyles.error)}>
<div className={cn(formStyles.input, formStyles['input-text'])}>{errorMsg}</div>
<button
type="button"
className={cn(formStyles.submit, formStyles.error)}
onClick={() => {
setFormState('default')
setTicketGenerationState('default')
}}
>
Try Again
</button>
</div>
</div>
</div>
) : (
<form
ref={formRef}
onSubmit={async (e) => {
e.preventDefault()
if (formState !== 'default') {
setTicketGenerationState('default')
setFormState('default')
return
}
setFormState('loading')
setTicketGenerationState('loading')
await supabase?.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${SITE_ORIGIN}/launch-week/${
userData.username ? '?referral=' + userData.username : ''
}`,
},
})
}}
className="flex flex-col items-center xl:block relative z-20"
id="wayfinding--connect-with-github-form"
>
<div className="flex flex-col gap-3">
<div>
<Button
type="secondary"
htmlType="submit"
disabled={formState === 'loading' || Boolean(session)}
>
<span className={`${username && 'text-muted'}`}>
{session ? (
<>
<IconCheckCircle />
Connect with GitHub
</>
) : (
<span className="flex items-center gap-2">
{formState === 'loading' && <IconLoader size={14} className="animate-spin" />}
Connect with GitHub
</span>
)}
</span>
{session ? <span className={ticketFormStyles.checkIcon}></span> : null}
</Button>
</div>
{/* {!session && <p className={'text-xs text-muted'}>Only public info will be used.</p>} */}
</div>
</form>
)
}

View File

@@ -1,19 +0,0 @@
import React from 'react'
import { LW7_DATE } from '~/lib/constants'
interface Props {
size?: 'default' | 'small'
}
export default function TicketHeader({ size }: Props) {
return (
<div className="flex w-full justify-center pt-4 md:pt-6" id="wayfinding--ticket-header">
<div className="flex flex-col md:flex-row gap-1 justify-center items-center md:gap-5">
<img
className={size === 'small' ? 'w-[165px]' : 'w-[220px] md:w-[230px]'}
src={`/images/launchweek/ticket-header-logo.png`}
/>
<span className="text-white text-xs md:text-sm">{LW7_DATE}</span>
</div>
</div>
)
}

View File

@@ -1,95 +0,0 @@
type Props = {
number: number | undefined
size?: 'default' | 'small'
}
export default function TicketNumber({ number, size }: Props) {
const numDigits = `${number}`.length
const prefix = `00000000`.slice(numDigits)
const ticketNumberText = `${prefix}${number}`
return (
<>
<div
className="z-10 absolute md:flex inset-0 top-auto md:left-auto md:right-0 md:top-0 md:w-[110px] md:h-100% text-foreground"
id="wayfinding--ticket-number-outer"
>
<div
className={[
'flex flex-col md:flex-row items-center justify-center w-full text-center',
size === 'small' ? 'text-[20px]' : 'text-[32px] md:text-[42px] ',
].join(' ')}
id="wayfinding--ticket-number-inner"
>
{/* Mobile line */}
<div className="block md:hidden">
<svg
width="234"
height="2"
viewBox="0 0 234 2"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0.0390625 1L233.294 0.99999"
stroke="url(#paint0_linear_1701_100991)"
strokeWidth="0.71381"
strokeDasharray="1.43 1.43"
/>
<defs>
<linearGradient
id="paint0_linear_1701_100991"
x1="0.0390625"
y1="0.5"
x2="233.294"
y2="0.49999"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="white" stopOpacity="0" />
<stop offset="0.53125" stopColor="white" />
<stop offset="1" stopColor="white" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
</div>
{/* Vertical line: desktop */}
<div
className="h-full hidden md:flex items-center absolute left-0"
id="wayfinding--ticket-stitch"
>
<svg
width="2"
height={size === 'small' ? '210' : '328'}
viewBox="0 0 2 328"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0.601562 0.988281V327.763"
stroke="url(#paint0_linear_1736_81388)"
strokeDasharray="2 2"
/>
<defs>
<linearGradient
id="paint0_linear_1736_81388"
x1="1.10156"
y1="0.988281"
x2="1.10156"
y2="327.763"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="white" stopOpacity="0" />
<stop offset="0.53125" stopColor="white" />
<stop offset="1" stopColor="white" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
</div>
<div className="md:absolute px-2 py-8 md:w-[max-content] leading-[1] md:transform md:rotate-90 md:origin-center bg-gradient-to-r from-[#F8F9FA] via-[#F8F9FA] to-[#F8F9FA50] bg-clip-text text-[#F8F9FA50] text-center">
{ticketNumberText}
</div>
</div>
</div>
</>
)
}

View File

@@ -1,86 +0,0 @@
// import GithubIcon from '~/components/LaunchWeek/Ticket/icons/icon-github'
import cn from 'classnames'
import TicketForm from './TicketForm'
// import IconAvatar from '~/components/LaunchWeek/Ticket/icons/icon-avatar'
import styles from './ticket-profile.module.css'
import Image from 'next/image'
type TicketGenerationState = 'default' | 'loading'
type Props = {
name?: string
username?: string
size?: number
ticketGenerationState?: TicketGenerationState
setTicketGenerationState?: (ticketGenerationState: TicketGenerationState) => void
golden?: boolean
}
// The middle part of the ticket
// avatar / Yourn Name / Username
export default function TicketProfile({
name,
username,
size = 1,
ticketGenerationState,
setTicketGenerationState,
golden = false,
}: Props) {
return (
<div className="grid gap-4 items-center justify-center px-2" id="wayfinding--ticket-middle">
{username && (
<span
className={cn('rounded-full inline-block mx-auto', styles.wrapper, styles.rounded, {
[styles.show]: ticketGenerationState === 'loading',
})}
>
{username ? (
<Image
src={`https://github.com/${username}.png`}
alt={username}
layout="fill"
objectFit="contain"
priority
className={styles.image}
/>
) : (
<>
{/* <span
className={cn(
styles.image,
golden ? styles['empty-icon--golden'] : styles['empty-icon']
)}
>
{ <IconAvatar />}
</span> */}
</>
)}
</span>
)}
<div>
{username ? (
<p
className={`${cn(
styles.name,
{ [styles['name-blank']]: !username },
{ [styles['name-golden']]: golden }
)} text-foreground text-center`}
>
<div
className={`${cn(styles.skeleton, styles.wrapper, {
[styles.show]: ticketGenerationState === 'loading',
})} text-3xl sm:text-4xl bg-gradient-to-r from-[#F8F9FA] via-[#F8F9FA] to-[#F8F9FA60] bg-clip-text text-transparent text-center`}
>
{name || username || 'Your Name'}
<p>{name && <p className="gradient-text-100 text-sm">@{username}</p>}</p>
</div>
</p>
) : (
<TicketForm
defaultUsername={username ?? undefined}
setTicketGenerationState={setTicketGenerationState}
/>
)}
</div>
</div>
)
}

View File

@@ -1,132 +0,0 @@
import styles from './ticket-visual.module.css'
import TicketProfile from './TicketProfile'
import TicketNumber from './TicketNumber'
import Tilt from 'vanilla-tilt'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import TicketHeader from './TicketHeader'
import { useEffect, useRef, useState } from 'react'
import Image from 'next/image'
type TicketGenerationState = 'default' | 'loading'
type Props = {
size?: number
name?: string
ticketNumber?: number
bgImageId?: number
username?: string
ticketGenerationState?: TicketGenerationState
setTicketGenerationState?: any
golden?: boolean
}
export default function TicketVisual({
size = 1,
name,
username,
bgImageId,
ticketNumber,
ticketGenerationState = 'default',
setTicketGenerationState,
golden = false,
}: Props) {
const { session } = useConfData()
const [imageIsLoading, setImageIsLoading] = useState(true)
const ticketRef = useRef<HTMLDivElement>(null)
const storageBaseFilepath = `https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/lw7/tickets_bg`
const ticketBg = {
regular: {
image: `${storageBaseFilepath}/blurred/regular/jpg/reg_bg_${bgImageId}.jpg`,
overlay: `/images/launchweek/seven/ticket-overlay-reg.png`,
},
gold: {
image: `${storageBaseFilepath}/blurred/golden/jpg/gold_bg_${bgImageId}.jpg`,
overlay: `/images/launchweek/seven/ticket-overlay-gold.png`,
},
}
useEffect(() => {
if (ticketRef.current && !window.matchMedia('(pointer: coarse)').matches) {
Tilt.init(ticketRef.current, {
glare: !golden,
max: 4,
gyroscope: true,
'max-glare': 0.3,
'full-page-listening': true,
})
}
}, [ticketRef])
return (
<div
className="flex relative flex-col w-[300px] md:w-full md:max-w-none h-auto"
style={{
transform: 'translate3d(0, 0, 100px)',
transformStyle: 'preserve-3d',
}}
>
<div
ref={ticketRef}
className={[
styles.visual,
golden ? styles['visual--gold'] : '',
session ? styles['visual--logged-in'] : '',
!golden && 'overflow-hidden',
'flex relative flex-col justify-between w-full pt-[150%] md:pt-[50%] bg-gradient-to-b from-[#ffffff80] to-[#ffffff20] before:rounded-2xl h-0 box-border',
].join(' ')}
style={{
['--size' as string]: size,
}}
id="wayfinding--ticket-visual-inner-container"
>
<div className="absolute inset-0 h-[calc(100%-100px)] z-10 flex flex-col items-center justify-between w-full md:h-full flex-1 md:pl-[6%] md:pr-[15%] overflow-hidden">
{username && <TicketHeader />}
<div
className="flex-1 w-full h-full md:h-auto flex py-6 md:py-4 flex-col justify-center"
id="wayfinding--TicketProfile-container"
>
<TicketProfile
name={name}
username={username}
size={size}
ticketGenerationState={ticketGenerationState}
setTicketGenerationState={setTicketGenerationState}
golden={golden}
/>
</div>
</div>
<TicketNumber number={ticketNumber} />
<div
id="wayfinding--ticket-dynamic-bg-image"
className="absolute inset-[1px] z-0 rounded-2xl overflow-hidden"
>
{username && (
<Image
src={golden ? ticketBg.gold.overlay : ticketBg.regular.overlay}
layout="fill"
objectFit="cover"
placeholder="blur"
blurDataURL="/images/blur.png"
className="absolute inset-[1px] z-[1]"
alt=""
/>
)}
<Image
src={golden ? ticketBg.gold.image : ticketBg.regular.image}
layout="fill"
objectFit="cover"
placeholder="blur"
blurDataURL="/images/blur.png"
priority
className={[
'duration-700 ease-in-out transform transition-all',
imageIsLoading ? 'grayscale blur-xl scale-110' : 'scale-100 grayscale-0 blur-0',
].join(' ')}
onLoadingComplete={() => setImageIsLoading(false)}
alt=""
/>
</div>
</div>
</div>
)
}

View File

@@ -1,11 +0,0 @@
/* .container {
margin: auto 0;
padding: 0 var(--space-4x);
}
@media (min-width: 768px) {
.container {
margin: auto;
padding: 0 var(--space-8x);
}
} */

View File

@@ -1,5 +0,0 @@
import styles from './conf-container.module.css'
export default function ConfContainer({ children }: { children: React.ReactNode }) {
return children
}

View File

@@ -1,248 +0,0 @@
/* .form {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-bottom: 12px;
gap: 12px;
} */
/* .formLeft {
align-items: center;
}
.formCenter {
align-items: center;
} */
/* @media (min-width: 1200px) {
.formLeft {
align-items: start;
}
.formCenter {
align-items: center;
}
} */
/*
.formInfo {
max-width: 520px;
} */
.formInfo h3 {
font-size: 1.25em;
font-weight: 400;
}
.formInfo p {
font-size: 1.1em;
line-height: 1.4;
margin-bottom: 32px;
color: var(--lw-secondary-color);
}
@media (min-width: 1200px) {
/* .form.share-page {
justify-content: flex-start;
} */
}
/* .formInfoLeft {
text-align: center;
}
.formInfoCenter {
text-align: center;
margin: 0 auto;
} */
@media (min-width: 1200px) {
.formInfoLeft {
text-align: left;
}
.formInfoCenter {
text-align: center;
margin: 0 auto;
}
}
.input {
border: none;
width: 100%;
background: transparent;
outline: none;
height: 56px;
padding-left: 15px;
padding-right: 15px;
font-size: var(--text-md);
color: #fff;
font-family: inherit;
}
@media (min-width: 768px) {
.input {
width: calc(100% - var(--space-32x));
padding-right: 0;
}
}
.input::placeholder {
color: var(--lw-secondary-color);
}
.input-label {
background-color: var(--gray);
border-radius: var(--space-2x);
border: 1px solid transparent;
transition:
border-color 0.2s ease,
background-color 0.2s ease;
display: block;
}
.input-label.focused {
background-color: #33373c;
}
.input-label.error {
background: red;
}
.input-label.success {
background: #0070f3;
}
.input-text {
display: flex;
align-items: center;
width: 100%;
}
.form-row {
position: relative;
max-width: 400px;
width: 100%;
}
@media (min-width: 768px) {
.form-row {
max-width: 480px;
}
}
.submit {
width: 100%;
height: 56px;
margin-top: var(--space-4x);
border-radius: var(--space-2x);
border: 1px solid hsl(var(--brand-default));
background: hsl(var(--brand-default));
cursor: pointer;
font-family: inherit;
font-size: var(--text-md);
letter-spacing: -0.02em;
outline: none;
font-weight: 500;
color: white;
transition:
background-color 0.2s ease,
color 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
}
.submit.generate-with-github {
display: flex;
margin-bottom: 12px;
position: relative;
}
@media (min-width: 768px) {
.submit.register {
width: 120px;
height: 40px;
margin-top: 0;
position: absolute;
border-radius: 5px;
right: var(--space-2x);
top: var(--space-2x);
}
}
.submit.loading {
cursor: default;
}
.submit.default:hover,
.submit.default:focus {
background: black;
color: hsl(var(--brand-default));
}
.submit.default.generate-with-github:hover path,
.submit.default.generate-with-github:focus path {
fill: var(--brand);
}
.submit.error:hover,
.submit.error:focus {
background: #000;
color: #fff;
}
.submit.default:disabled,
.submit.default:disabled:hover,
.submit.default:disabled:focus {
cursor: default;
background: var(--gray);
border-color: var(--gray);
color: #fff;
justify-content: flex-start;
overflow: hidden;
}
.submit.default.generate-with-github:disabled path,
.submit.default.generate-with-github:disabled:hover path,
.submit.default.generate-with-github:disabled:focus path {
fill: #fff;
}
.submit.default.generate-with-github.not-allowed:disabled {
cursor: not-allowed;
}
@media (min-width: 1200px) {
.form-row {
margin: 0;
}
.submit.generate-with-github {
width: 240px;
}
}
.stage-btn {
background: #323332;
border: 2px solid #34b27b;
}
.github-wrapper {
display: flex;
align-items: center;
flex-direction: column;
}
.or-divider {
width: 100%;
text-align: center;
color: var(--lw-secondary-color);
margin: var(--space-4x) 0;
}
@media (min-width: 1200px) {
.github-wrapper {
flex-direction: row;
}
.or-divider {
width: 240px;
margin: 0;
}
}

View File

@@ -1,202 +0,0 @@
import { useState, useCallback } from 'react'
import cn from 'classnames'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import { useRouter } from 'next/router'
import styleUtils from '../../utils.module.css'
import styles from './form.module.css'
import useEmailQueryParam from '~/components/LaunchWeek/hooks/use-email-query-param'
import { IconLoader } from '~/../../packages/ui'
type FormState = 'default' | 'loading' | 'error'
export type ConfUser = {
id?: string
email?: string
ticketNumber?: number
name?: string
username?: string
createdAt?: number
golden?: boolean
}
type Props = {
sharePage?: boolean
align?: 'Left' | 'Center'
}
export default function Form({ sharePage, align = 'Center' }: Props) {
const [email, setEmail] = useState('')
const [errorMsg, setErrorMsg] = useState('')
const [errorTryAgain, setErrorTryAgain] = useState(false)
const [focused, setFocused] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [formState, setFormState] = useState<FormState>('default')
const { setTicketState, setUserData, session, userData, supabase } = useConfData()
const router = useRouter()
const isCaptchaEnabled = false
// NOTE: this is not in use anymore as we're only allowing GitHub signups since LW7!
async function register(email: string, token?: string): Promise<ConfUser> {
const { error } = await supabase!.from('lw6_tickets').insert({ email })
if (error) {
// console.log({ error })
return {
id: 'new',
ticketNumber: 1234,
name: '',
username: '',
golden: false,
}
}
const { data } = await supabase!.from('lw6_tickets_golden').select('*').limit(1).single()
return {
id: data?.id ?? 'new',
ticketNumber: data?.ticketNumber ?? 1234,
name: data?.name ?? '',
username: data?.username ?? '',
golden: data?.golden ?? false,
}
}
const handleRegister = useCallback(
(token?: string) => {
register(email, token)
.then(async (params) => {
if (!params) {
throw new Error()
}
if (sharePage) {
const queryString = Object.keys(params)
.map(
(key) =>
`${encodeURIComponent(key)}=${encodeURIComponent(
params[key as keyof typeof params] || ''
)}`
)
.join('&')
await router.replace(`/launch-week/7/tickets?${queryString}`, '/launch-week/7/tickets')
} else {
setUserData(params)
setTicketState('ticket')
}
})
.catch(async (err) => {
let message = 'Error! Please try again.'
setErrorMsg(message)
setFormState('error')
})
},
[email, router, setTicketState, setUserData, sharePage]
)
const onSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault()
if (formState === 'default') {
setFormState('loading')
if (isCaptchaEnabled) {
// return executeCaptcha()
}
return handleRegister()
} else {
setFormState('default')
}
},
[formState, isCaptchaEnabled, handleRegister]
)
const onTryAgainClick = useCallback((e: React.MouseEvent) => {
e.preventDefault()
setFormState('default')
setErrorTryAgain(true)
// resetCaptcha()
}, [])
useEmailQueryParam('email', setEmail)
return (
<div className="flex flex-col gap-8">
<div
className={cn(
styleUtils['appear-fifth'],
'flex flex-col gap-2 items-center xl:items-start',
align === 'Left' ? 'text-center xl:text-left' : 'text-center'
)}
>
<p className="text-foreground-lighter text-base max-w-[420px]">
Register to get your ticket and stay tuned all week for daily announcements
</p>
</div>
{formState === 'error' ? (
<div
className={cn(styles.form, {
[styles['share-page']]: sharePage,
})}
>
<div className={styles['form-row']}>
<div className={cn(styles['input-label'], styles.error)}>
<div className={cn(styles.input, styles['input-text'])}>{errorMsg}</div>
<button
type="button"
className={cn(styles.submit, styles.register, styles.error)}
onClick={onTryAgainClick}
>
Try Again
</button>
</div>
</div>
</div>
) : (
<form
className="relative mx-auto xl:mx-0 w-full md:w-auto md:min-w-[320px] md:max-w-[420px]"
onSubmit={onSubmit}
>
<input
className={`
transition-all
border border-background-surface-100 bg-background h-10
focus:border-default focus:ring-background-surface-100
text-foreground text-base rounded-full w-full px-5
`}
type="email"
autoComplete="email"
id="email-input-field"
value={email}
onChange={(e) => setEmail(e.target.value)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
placeholder="Enter email"
aria-label="Your email address"
required
/>
<button
type="submit"
className={[
'transition-all',
'absolute bg-surface-100 text-foreground border border-control text-sm hover:bg-overlay-hover',
'rounded-full px-4',
'focus:invalid:border-default focus:invalid:ring-background-surface-100',
'absolute right-1 my-auto h-8 top-0 bottom-0',
].join(' ')}
disabled={formState === 'loading'}
>
{formState === 'loading' ? (
<div className="flex items-center gap-2">
<IconLoader size={14} className="animate-spin" /> Registering
</div>
) : (
'Register'
)}
</button>
{/* <Captcha ref={captchaRef} onVerify={handleRegister} /> */}
</form>
)}
</div>
)
}

View File

@@ -1,48 +0,0 @@
.loading {
display: inline-flex;
align-items: center;
--loading-dots-height: auto;
--loading-dots-size: 2px;
height: var(--loading-dots-height);
}
.loading .spacer {
margin-right: var(--space-3x);
}
.loading span {
animation-name: blink;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-fill-mode: both;
width: var(--loading-dots-size);
height: var(--loading-dots-size);
border-radius: 50%;
background-color: var(--accents-6);
display: inline-block;
margin: 0 1px;
}
.loading.reverse span {
background-color: var(--accents-2);
}
.loading span:nth-of-type(2) {
animation-delay: 0.2s;
}
.loading span:nth-of-type(3) {
animation-delay: 0.4s;
}
@keyframes blink {
0% {
opacity: 0.2;
}
20% {
opacity: 1;
}
100% {
opacity: 0.2;
}
}

View File

@@ -1,27 +0,0 @@
import { toPixels as px } from '~/lib/helpers'
import cn from 'classnames'
import styles from './loading-dots.module.css'
interface Props {
size?: number
height?: number | string
reverse?: boolean
children?: React.ReactNode
}
export default function LoadingDots({ size = 2, height, children, reverse }: Props) {
return (
<span
className={cn(styles.loading, { [styles.reverse]: reverse })}
style={{
['--loading-dots-height' as string]: height ? px(height) : undefined,
['--loading-dots-size' as string]: size !== 2 ? px(size) : undefined,
}}
>
{children && <div className={styles.spacer}>{children}</div>}
<span />
<span />
<span />
</span>
)
}

View File

@@ -1,70 +0,0 @@
.button {
border-radius: var(--space-2x);
border: 1px solid #fff;
color: #000;
background: #fff;
cursor: pointer;
font-family: inherit;
font-size: var(--text-md);
letter-spacing: -0.02em;
font-weight: 500;
outline: none;
transition: background-color 0.2s ease;
display: grid;
grid-template-columns: 24px 1fr;
text-align: center;
align-items: center;
padding: 0 var(--space-4x);
height: 55px;
width: 100%;
max-width: 400px;
margin: var(--space-2x) auto;
}
.loading {
grid-template-columns: 1fr;
justify-items: center;
}
@media (min-width: 768px) {
.button {
width: 200px;
margin: 0 6px;
}
}
.button:hover,
.button:focus {
background: black;
color: #fff;
}
.first {
animation-delay: 2s;
}
.second {
animation-delay: 2.2s;
}
.third {
animation-delay: 2.4s;
}
:global(.ticket-generated) .first {
animation-delay: 0.6s;
}
:global(.ticket-generated) .second {
animation-delay: 0.8s;
}
:global(.ticket-generated) .third {
animation-delay: 1s;
}
.linkedin-button {
display: none !important;
}
@media (min-width: 768px) {
.linkedin-button {
display: grid !important;
}
}

View File

@@ -1,190 +0,0 @@
.wrapper {
color: var(--lw-secondary-color);
display: flex;
align-items: center;
justify-content: center;
animation-delay: 2.6s;
flex-direction: column;
}
:global(.ticket-generated) .wrapper {
animation-delay: 1.2s;
}
.label {
width: 100%;
margin-bottom: var(--space-2x);
}
.label-wrapper {
display: flex;
align-items: center;
max-width: 400px;
width: 100%;
justify-content: space-between;
}
@media (min-width: 768px) {
.wrapper {
flex-direction: row;
}
.label {
width: auto;
margin-right: 10px;
margin-bottom: 0px;
}
.label-wrapper {
width: auto;
}
}
.field {
position: relative;
border-radius: var(--space-2x);
overflow: hidden;
background: var(--gray);
}
@media (min-width: 768px) {
.field {
padding-right: 50px;
}
.field.desktop-copy-disabled {
padding-right: 0px;
}
}
.mobile-copy {
margin-bottom: 10px;
display: flex;
}
.mobile-copy-disabled.mobile-copy-disabled {
display: none;
}
.mobile-copy * {
color: var(--lw-secondary-color);
transition: color 0.2s ease;
}
.mobile-copy:hover * {
color: #fff;
transition: color 0.2s ease;
}
.field * {
color: var(--lw-secondary-color);
transition: color 0.2s ease;
}
.field:hover * {
color: #fff;
transition: color 0.2s ease;
}
.url {
width: calc(100vw - 36px);
user-select: all;
overflow-x: scroll;
white-space: nowrap;
display: block;
padding: var(--space-2x) 14px;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
@media (min-width: 436px) {
.url {
width: 400px;
}
}
@media (min-width: 768px) {
.url {
width: 400px;
}
.mobile-copy {
margin-bottom: 0;
display: none;
}
}
.url::-webkit-scrollbar {
display: none;
}
/* .fade {
position: absolute;
background: linear-gradient(90deg, rgba(37, 39, 41, 0%) 0%, #252729 100%);
top: 0;
bottom: 0;
right: 0px;
width: 50px;
z-index: 1;
display: block;
border-radius: var(--space-2x);
pointer-events: none;
} */
.desktop-copy.desktop-copy-disabled {
display: none;
}
@media (min-width: 768px) {
.fade {
right: 50px;
}
.fade.desktop-copy-disabled {
right: 0px;
}
}
.copied {
display: flex;
z-index: 2;
align-items: center;
justify-content: flex-end;
opacity: 0;
transition: opacity 0.2s ease !important;
}
.copied.visible {
opacity: 1;
}
.copy-button {
background: none;
outline: none;
border: none;
cursor: pointer;
z-index: 2;
border-radius: var(--space-2x);
width: 40px;
display: flex;
align-items: center;
justify-content: center;
margin-right: -10px;
}
@media (min-width: 768px) {
.copied {
position: absolute;
right: 50px;
top: 0px;
bottom: 0px;
background: linear-gradient(90deg, rgba(37, 39, 41, 0%) 0px, #252729 40px);
padding-left: 50px;
}
.copy-button {
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
margin-right: 0px;
width: 50px;
}
}

View File

@@ -1,92 +0,0 @@
import { useEffect, useState, useRef } from 'react'
import cn from 'classnames'
import { SITE_URL } from '~/lib/constants'
import styleUtils from '../../utils.module.css'
import { IconCopy, IconCheck } from 'ui'
type Props = {
username: string
isGolden?: boolean
}
export default function TicketCopy({ username, isGolden }: Props) {
const [fadeOpacity, setFadeOpacity] = useState(1)
const [scrolling, setScrolling] = useState(false)
const [copyEnabled, setCopyEnabled] = useState(false)
const [copied, setCopied] = useState(false)
const scrollRef = useRef<HTMLParagraphElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
const url = `${SITE_URL}/tickets/${username}?lw=7${isGolden ? `&golden=true` : ''}`
useEffect(() => {
if (navigator.clipboard) {
setCopyEnabled(true)
}
}, [])
const copyButton = (
<button
type="button"
name="Copy"
className="bg-[#E6E8EB] text-[#2e2e2e] text-xs w-21 flex items-center cursor-pointer py-1 px-2 rounded-full"
ref={buttonRef}
onClick={() => {
navigator.clipboard.writeText(url).then(() => {
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 2000)
})
}}
>
<div className="flex items-center gap-1 ">
{copied ? (
<>
<IconCheck size={14} /> Copied!
</>
) : (
<>
<IconCopy size={14} /> Copy your unique URL{' '}
</>
)}
</div>
</button>
)
return (
<div
className={cn(
styleUtils.appear,
styleUtils['appear-third'],
'bg-background h-8 rounded-full border border-[#E3E3E370] w-full overflow-hidden'
)}
id="wayfinding--ticket-copy"
>
<div className="px-3 h-full flex items-center gap-3 w-full truncate relative pr-20 bg-[#D9D9D94D]">
<div className="flex items-center truncate">
<p
className={['text-xs font-mono text-[#ededed] truncate'].join(' ')}
ref={scrollRef}
onScroll={() => {
if (!scrolling) {
setScrolling(true)
const animationFrame = requestAnimationFrame(() => {
const scrollableWidth =
(scrollRef.current?.scrollWidth || 0) - (scrollRef.current?.clientWidth || 0)
setFadeOpacity(
(scrollableWidth - (scrollRef.current?.scrollLeft || 0)) /
(scrollableWidth || 1)
)
cancelAnimationFrame(animationFrame)
setScrolling(false)
})
}
}}
>
{url}
</p>
</div>
<div className="absolute right-1 with-auto height-auto flex items-center">{copyButton}</div>
</div>
</div>
)
}

View File

@@ -1,65 +0,0 @@
.githubIcon {
margin-left: 12px;
margin-right: var(--space-4x);
display: inline-flex;
}
.checkIcon {
position: absolute;
right: var(--space-4x);
display: inline-flex;
}
.generateWithGithub {
display: flex;
align-items: center;
font-weight: 500;
}
.stageIcon {
position: absolute;
left: var(--space-4x);
display: inline-flex;
}
.form-row {
margin-left: auto;
margin-right: auto;
max-width: 600px;
}
.description {
color: var(--secondary-color);
margin: 0;
text-align: center;
margin-left: var(--space-4x);
}
@media (min-width: 768px) {
.form-row {
margin-bottom: 20px;
}
}
@media (min-width: 1200px) {
.form-row {
margin: 0;
}
.description {
text-align: left;
}
.generateWithGithub {
width: 100%;
}
}
.learn-more {
color: #fff;
}
.learn-more:hover {
text-decoration: underline;
color: #fff;
}

View File

@@ -1,33 +0,0 @@
.background {
width: 2000px;
height: 1000px;
background: #000;
color: #fff;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.page {
width: 1700px;
font-family:
'Inter',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
'Roboto',
'Oxygen',
'Ubuntu',
'Cantarell',
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
'Noto Sans',
'Noto Sans JP',
'Noto Sans KR',
'Noto Sans SC',
'Noto Sans TC',
'Noto Sans HK',
sans-serif;
}

View File

@@ -1,37 +0,0 @@
import { useRouter } from 'next/router'
import Head from 'next/head'
import TicketVisual from './TicketVisual'
import styles from './ticket-image.module.css'
export default function TicketImage() {
const { query } = useRouter()
if (query.ticketNumber) {
return (
<div className={styles.background}>
<div className={styles.page}>
<Head>
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&Noto+Sans+HK:wght@700&family=Noto+Sans+JP:wght@700&family=Noto+Sans+KR:wght@700&family=Noto+Sans+SC:wght@700&family=Noto+Sans+TC:wght@700&family=Noto+Sans:wght@700&display=swap"
rel="stylesheet"
/>
</Head>
{/* <TicketVisual
size={1700 / 650}
username={query.username ? query.username.toString() : undefined}
ticketNumber={parseInt(query.ticketNumber.toString(), 10)}
name={
query.name
? query.name?.toString()
: query.username
? query.username.toString()
: undefined
}
golden={query.golden ? true : false}
/> */}
</div>
</div>
)
}
return <></>
}

View File

@@ -1,30 +0,0 @@
import styles from './ticket-info.module.css'
import cn from 'classnames'
import { LW7_DATE, SITE_URL } from '~/lib/constants'
const siteUrl = new URL(SITE_URL)
const siteUrlForTicket = `${siteUrl.host}${siteUrl.pathname}`.replace(/\/$/, '')
export default function TicketInfoFooter({
logoTextSecondaryColor = 'var(--accents-5)',
golden = false,
}) {
return (
<div className=" flex gap-0" id="wayfinding--TicketInfo-footer">
<div
className={`${cn(styles.date, { [styles['date-golden']]: golden })} text-sm mr-4 ${
golden && '!text-white'
}`}
>
<div>{LW7_DATE}</div>
</div>
<div
className={`${cn(styles.date, { [styles['date-golden']]: golden })} text-sm ${
golden && '!text-white'
}`}
>
{/* <div>supabase.com/launch-week</div> */}
</div>
</div>
)
}

View File

@@ -1,83 +0,0 @@
.info {
}
@media (min-width: 768px) {
.info {
/* display: grid;
grid-template-columns: 1fr 2fr;
gap: calc(var(--space-4x) * var(--size));
font-size: calc(1em * var(--size)); */
padding: 0;
margin-bottom: calc(32px * var(--size));
margin-left: calc(160px * var(--size));
}
}
@media (max-width: 768px) {
.info {
flex-direction: column;
align-items: center;
}
}
.logo {
/* margin-bottom: var(--space-6x); */
/* max-width: 96px; */
font-size: 18px;
}
/* @media (min-width: 768px) {
.logo {
margin-bottom: 0;
font-size: calc(16px * var(--size));
}
} */
.date {
/* text-transform: uppercase; */
/* font-size: calc(18px * var(--size)); */
color: var(--lw-secondary-color);
}
.date-golden {
color: var(--gold-primary);
}
@media (min-width: 768px) {
.date {
margin-bottom: 0;
line-height: 1.15;
/* font-size: calc(20px * var(--size)); */
}
}
.created-by {
display: flex;
align-items: center;
color: var(--accents-4);
}
@media (min-width: 768px) {
.created-by {
margin-right: var(--space-4x);
}
}
.created-by-text {
white-space: nowrap;
margin-right: var(--space);
}
/* .created-by-logo {
height: calc(16px * var(--size));
display: inline-flex;
} */
.url {
color: var(--lw-secondary-color);
}
.url-golden {
color: var(--gold-primary);
}

View File

@@ -1,34 +0,0 @@
// NOTE: When you export SVG from Figma, you must:
// - Update width and height as 100%
// - Change id to "mobile-..."
// - Change url(#) to url(#mobile-)
// This is because if a page has two same IDs it will fail.
export default function TicketMonoMobile({ golden = false }: { golden?: boolean }) {
const frameColor = golden ? '#F2C94C' : '#3fcf8e'
const perforationColor = golden ? 'var(--gold-accent)' : '#252729'
return (
<svg
width="100%"
height="100%"
viewBox="0 0 330 100"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* <path
fillRule="evenodd"
clipRule="evenodd"
d="M2.9193e-06 540C3.40212e-06 551.046 8.95431 560 20 560L138 560C138 545.088 150.088 533 165 533C179.912 533 192 545.088 192 560L310 560C321.046 560 330 551.046 330 540L330 20C330 8.95427 321.046 -1.40334e-05 310 -1.35505e-05L192 -8.39259e-06C192 14.9117 179.912 27 165 27C150.088 27 138 14.9117 138 -6.03217e-06L20 -8.74228e-07C8.95428 -3.91405e-07 -2.41646e-05 8.95428 -2.36041e-05 20L2.9193e-06 540Z"
fill={frameColor}
/> */}
{/* <path
fillRule="evenodd"
clipRule="evenodd"
d="M5 539C5 547.837 12.1634 555 21 555L133.388 555C135.789 539.702 149.028 528 165 528C180.972 528 194.211 539.702 196.612 555L309 555C317.837 555 325 547.837 325 539L325 21C325 12.1634 317.837 4.99999 309 4.99999L196.612 4.99999C194.211 20.2981 180.972 32 165 32C149.028 32 135.789 20.2982 133.388 4.99999L21 5C12.1634 5 4.99998 12.1635 4.99998 21L5 539Z"
fill="black"
/> */}
<path d="M326 446H5" stroke={perforationColor} strokeDasharray="6 6" />
</svg>
)
}

View File

@@ -1,23 +0,0 @@
export default function TicketMono({ golden = false }: { golden?: boolean }) {
const frameColor = golden ? '#F2C94C' : '#252729'
const perforationColor = golden ? 'var(--gold-accent)' : '#252729'
return (
<svg
width="100%"
height="auto"
viewBox="0 0 650 100"
fill="none"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="none"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M21 5C12.1634 5 5 12.1634 5 21V133.388C20.2981 135.789 32 149.028 32 165C32 180.972 20.2981 194.211 5 196.612V309C5 317.837 12.1634 325 21 325H629C637.837 325 645 317.837 645 309V196.612C629.702 194.211 618 180.972 618 165C618 149.028 629.702 135.789 645 133.388V21C645 12.1634 637.837 5 629 5H21Z"
// fill="black"
/>
</svg>
)
}

View File

@@ -1,149 +0,0 @@
.image {
width: 50px;
height: 50px;
border-radius: 50%;
border: 1px solid #999;
}
@media (min-width: 768px) {
.image {
width: calc(100px * var(--size));
height: calc(100px * var(--size));
}
}
.name {
font-size: 24px;
display: inline-block;
line-height: 1.15;
letter-spacing: -0.02em;
margin: 0;
}
.name-golden {
color: var(--gold-primary);
}
@media (min-width: 768px) {
.name {
font-size: calc(var(--space-8x) * var(--size));
margin-bottom: 5px;
}
}
.username {
display: inline-block;
font-size: calc(1em * var(--size));
color: var(--lw-secondary-color);
display: flex;
align-items: center;
margin: 0;
}
.username-golden {
color: var(--gold-secondary);
}
.githubIcon {
margin-right: calc(5px * var(--size));
display: inline-flex;
}
.githubIcon-golden svg {
color: var(--gold-secondary);
opacity: 0.5;
}
.empty-icon {
display: block;
background: linear-gradient(320deg, #121212, #191919);
width: 60px;
}
.empty-icon--golden {
display: block;
background: linear-gradient(90deg, #fff9eb, #e2ba52);
width: 60px;
border: 1px solid #ecc154ad;
}
@media (min-width: 768px) {
.empty-icon {
width: calc(80px * var(--size));
}
.empty-icon--golden {
width: calc(80px * var(--size));
}
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/*
.skeleton {
display: flex;
align-items: center;
border-radius: 25px;
} */
.skeleton.loaded {
width: unset !important;
}
.skeleton:not(.wrapper):not(.show) {
display: none;
}
.wrapper:not(.show)::before {
content: none;
}
.skeleton:not(.wrapper):not(.loaded) {
border-radius: var(--space);
background-image: linear-gradient(270deg, #252729, #34383d, #34383d, #252729);
background-size: 200% 100%;
animation: loading 2s ease-in-out infinite;
}
.wrapper {
position: relative;
}
.wrapper::before {
content: '';
position: absolute;
top: -1px;
left: -1px;
right: 0;
bottom: 0;
border-radius: var(--space);
z-index: 100;
background-image: linear-gradient(270deg, #111111, #333333, #333333, #111111);
background-size: 200% 100%;
animation: loading 2s ease-in-out infinite;
}
.inline {
display: inline-block !important;
}
.rounded,
.rounded.wrapper::before {
width: calc(80px * var(--size));
height: calc(80px * var(--size));
border-radius: 50% !important;
}
@media (min-width: 768px) {
.rounded,
.rounded.wrapper::before {
width: calc(110px * var(--size));
height: calc(110px * var(--size));
}
}

View File

@@ -1,119 +0,0 @@
.visual {
transform: translateZ(0);
background-image: linear-gradient(154.77deg, #cb88ff 29.4%, #cb88ff 120.47%);
box-shadow:
0px 1px 27px #9e44ef10,
0 0 0 1px rgba(121, 89, 134, 0.237),
inset 0 0px 0 -2px rgba(0, 0, 0, 0.55);
/* border: 1px solid rgba(0, 0, 0, 0.05); */
border-radius: 16px;
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
/* border: 1px solid #1c1c1c; */
/* animation: 2s ease-in-out infinite alternate; */
background-repeat: no-repeat;
background-size: contain;
}
.visual--gold {
background: radial-gradient(#fcdf9d, #dbb333);
}
.visual--gold::before {
content: ' ';
position: absolute;
inset: 0;
filter: drop-shadow(0 0 6px #fff) drop-shadow(0 0 6px #d99f0c) drop-shadow(0 0 9px #dbb333);
background: radial-gradient(#fcdf9d, #dbb333);
border: 1px solid rgba(255, 255, 0, 0.629);
animation: opacity-pulse 2s ease-in-out infinite alternate;
}
@keyframes opacity-pulse {
from {
opacity: 0.5;
}
to {
opacity: 1;
}
}
@keyframes svg-shadow {
from {
filter: drop-shadow(0 0 2px #fff) drop-shadow(0 0 4px #d99f0c) drop-shadow(0 0 6px #d99f0c);
}
to {
filter: drop-shadow(0 0 10px #fff) drop-shadow(0 0 6px #d99f0c) drop-shadow(0 0 9px #d99f0c);
}
}
@keyframes blank-svg-shadow {
from {
filter: drop-shadow(0 0 2px #bada55) drop-shadow(0 0 2px #bada55) drop-shadow(0 0 2px #bada55);
}
to {
filter: drop-shadow(0 0 3px #bada55) drop-shadow(0 0 2px #bada55) drop-shadow(0 0 2px #bada55);
}
}
.logo {
z-index: 1;
position: absolute;
top: 0;
width: 100%;
height: 100%;
}
.logo img {
margin-top: calc(64px * var(--size));
margin-left: calc(32px * var(--size));
margin-left: calc(32px * var(--size));
width: calc(272px * var(--size));
}
@media (min-width: 768px) {
.logo img {
margin-top: calc(32px * var(--size));
width: calc(280px * var(--size));
margin-left: calc(160px * var(--size));
}
}
.profile {
z-index: 1;
position: absolute;
top: 90px;
width: 100%;
padding: 46px 29px;
}
@media (min-width: 768px) {
.profile {
top: 50%;
transform: translateY(-50%);
padding-left: 65px;
}
}
.info {
z-index: 1;
position: absolute;
bottom: 40px;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
@media (min-width: 768px) {
.info {
bottom: 0;
}
}

View File

@@ -1,151 +0,0 @@
.ticket-hero {
background: linear-gradient(
137.46deg,
rgba(255, 255, 255, 0) 6.19%,
rgba(255, 255, 255, 0) 103.29%
);
z-index: 10;
box-shadow:
0 1px 1px rgba(131, 6, 184, 0.08),
0 2px 2px rgba(128, 6, 184, 0.08),
inset 0 0.5px 0px 0.5px rgba(255, 255, 255, 0.1),
inset 0 -3px 0 -2px rgba(255, 255, 255, 0.08),
0px 4px 20px rgba(118, 7, 170, 0.1);
}
.hero {
line-height: 1.15;
letter-spacing: -0.05em;
font-weight: 400;
margin: 0;
text-align: center;
}
@media (min-width: 768px) {
.hero {
line-height: 1;
}
}
.description {
font-size: 20px;
line-height: 1.4;
color: var(--lw-secondary-color);
text-align: center;
margin: 30px auto;
max-width: 350px;
}
@media (min-width: 768px) {
.description {
font-size: 24px;
max-width: 415px;
}
}
.ticket-text {
margin-bottom: 30px;
}
.ticket-layout {
display: grid;
grid-template-columns: 1fr;
gap: 45px;
margin: auto;
padding: var(--space-4x);
padding-top: 64px;
padding-bottom: 64px;
}
@media (min-width: 1200px) {
.ticket-layout {
padding-top: 0;
padding-bottom: 0;
}
}
.ticket-share-layout {
align-items: center;
margin-bottom: auto;
}
.ticket-visual:before {
left: 50%;
top: -7.5%;
transform: translateZ(0) translateX(-50%) rotate(90deg);
}
.ticket-visual:after {
left: 50%;
bottom: -7.5%;
transform: translateZ(0) translateX(-50%) rotate(90deg);
}
@media (min-width: 768px) {
.ticket-visual:before {
top: 50%;
left: -7%;
transform: translateZ(0) translateY(-50%);
}
.ticket-visual:after {
top: 50%;
left: 96%;
transform: translateZ(0) translateY(-50%);
}
}
.ticket-actions {
margin-top: 70px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.ticket-copy {
margin-top: 30px;
}
.ticket-actions-placeholder {
height: 280px;
}
@media (min-width: 768px) {
.ticket-actions {
flex-direction: row;
}
.ticket-actions-placeholder {
height: 157px;
}
}
@media (min-width: 1200px) {
.ticket-layout {
grid-template-columns: 1fr 1fr;
}
.hero {
text-align: left;
}
.description {
text-align: left;
margin: 30px 0;
}
}
.ticket-visual-wrapper {
width: 100%;
}
.gold-text {
color: #f4cf72;
text-shadow:
0 0 1px rgba(0, 0, 0, 0.5),
0 0 4px rgba(0, 0, 0, 0.1);
}
@media (min-width: 768px) {
.gold-text {
color: #e4b641;
}
}

View File

@@ -1,79 +0,0 @@
import styles from './ticket-visual.module.css'
import TicketProfile from './TicketProfile'
import TicketNumber from './TicketNumber'
import { UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
import TicketHeader from './TicketHeader'
import Image from 'next/image'
import TicketForm from './TicketForm'
import TicketFooter from './TicketFooter'
type TicketGenerationState = 'default' | 'loading'
type Props = {
user: UserData
ticketGenerationState?: TicketGenerationState
setTicketGenerationState?: any
}
export default function TicketVisual({
user,
ticketGenerationState = 'default',
setTicketGenerationState,
}: Props) {
const { username, golden = false, bg_image_id: bgImageId, ticketNumber } = user
const ticketBg = {
regular: {
background: `/images/launchweek/8/ticket-bg/regular.png`,
},
golden: {
background: `/images/launchweek/8/ticket-bg/golden.png`,
},
}
const CURRENT_TICKET = golden ? 'golden' : 'regular'
const CURRENT_TICKET_BG = ticketBg[CURRENT_TICKET].background
return (
<div className="flex relative flex-col w-[300px] h-auto md:w-full md:max-w-none backdrop-blur-md">
<div
className={[
styles.visual,
golden ? styles['visual--gold'] : '',
!golden && 'overflow-hidden',
'flex relative flex-col justify-between w-full aspect-[1/1.6] md:aspect-[1.935/1] bg-gradient-to-b from-[#ffffff80] to-[#ffffff20] before:rounded-[13px] box-border backdrop-blur-md rounded-xl',
].join(' ')}
>
{username ? (
<div className="absolute inset-0 h-full px-4 pb-6 z-10 flex flex-col items-center justify-between w-full md:h-full flex-1 md:pb-0 md:pl-8 md:pr-[15%] overflow-hidden">
<TicketHeader golden={golden} />
<div className="flex-1 w-full h-full md:h-auto flex py-6 md:py-4 flex-col justify-center">
<TicketProfile
user={user}
ticketGenerationState={ticketGenerationState}
setTicketGenerationState={setTicketGenerationState}
golden={golden}
/>
</div>
<TicketFooter />
<TicketNumber number={ticketNumber} golden={golden} />
<div className="absolute z-500 inset-[1px] overflow-hidden rounded-xl">
<Image
src={CURRENT_TICKET_BG}
alt="ticket background"
layout="fill"
objectFit="cover"
objectPosition="center"
quality={100}
/>
</div>
</div>
) : (
<TicketForm
defaultUsername={username ?? undefined}
setTicketGenerationState={setTicketGenerationState}
/>
)}
</div>
</div>
)
}

View File

@@ -1,119 +0,0 @@
import { useEffect, useRef, useState } from 'react'
import { SITE_URL, TWEET_TEXT, TWEET_TEXT_GOLDEN } from '~/lib/constants'
import { IconCheckCircle } from 'ui'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import { useParams } from '~/hooks/useParams'
import TicketForm from './TicketForm'
type TicketGenerationState = 'default' | 'loading'
type Props = {
username: string
golden?: boolean
ticketGenerationState?: TicketGenerationState
setTicketGenerationState: (ticketGenerationState: TicketGenerationState) => void
}
export default function TicketActions({
username,
golden = false,
ticketGenerationState,
setTicketGenerationState,
}: Props) {
const [_imgReady, setImgReady] = useState(false)
const [_loading, setLoading] = useState(false)
const downloadLink = useRef<HTMLAnchorElement>()
const link = `${SITE_URL}/tickets/${username}?lw=8${golden ? `&golden=true` : ''}`
const permalink = encodeURIComponent(link)
const text = golden ? TWEET_TEXT_GOLDEN : TWEET_TEXT
const encodedText = encodeURIComponent(text)
const { userData, supabase } = useConfData()
const tweetUrl = `https://twitter.com/intent/tweet?url=${permalink}&via=supabase&text=${encodedText}`
const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${permalink}`
const downloadUrl = `https://obuldanrptloktxcffvn.supabase.co/functions/v1/lw8-ticket?username=${encodeURIComponent(
username
)}`
const params = useParams()
const sharePage = params.username
const LW_TABLE = 'lw8_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') => {
if (!supabase) return
if (social === 'twitter') {
await supabase.from(LW_TABLE).update({ sharedOnTwitter: 'now' }).eq('username', username)
window.open(tweetUrl, '_blank')
} else if (social === 'linkedin') {
await supabase.from(LW_TABLE).update({ sharedOnLinkedIn: 'now' }).eq('username', username)
window.open(linkedInUrl, '_blank')
}
}
return (
<div className="grid gap-1 grid-cols-1 sm:grid-cols-3">
{!sharePage ? (
<>
<div className="rounded bg-[#E6E8EB] text-background-surface-300 py-1 px-3 border border-[#3e3e3e] text-xs mb-1">
<div className="flex items-center justify-center gap-2">
<div className="text-background-surface-100">
<IconCheckCircle size={10} strokeWidth={1.5} />
</div>
Connect with GitHub
</div>
</div>
<button
onClick={() => handleShare('twitter')}
className={[
`flex items-center justify-center gap-2 rounded text-background-surface-300 py-1 px-3 border border-[#3e3e3e] text-xs mb-1 transition-all ease-out hover:text-background-alternative hover:bg-[#dfe1e3]`,
userData.sharedOnTwitter ? 'bg-[#E6E8EB] text-background-surface-300' : 'text-white',
].join(' ')}
>
{userData.sharedOnTwitter && (
<div className="text-muted">
<IconCheckCircle size={10} strokeWidth={1.5} />
</div>
)}
Share on Twitter
</button>
<button
onClick={() => handleShare('linkedin')}
className={[
`flex items-center justify-center gap-2 rounded text-background-surface-300 py-1 px-3 border border-[#3e3e3e] text-xs mb-1 transition-all ease-out hover:text-background-alternative hover:bg-[#dfe1e3]`,
userData.sharedOnLinkedIn ? 'bg-[#E6E8EB] text-background-surface-300' : 'text-white',
].join(' ')}
>
{userData.sharedOnLinkedIn && (
<div className="text-muted">
<IconCheckCircle size={10} strokeWidth={1.5} />
</div>
)}
Share on Linkedin
</button>
</>
) : (
!username && (
<TicketForm
defaultUsername={username ?? undefined}
ticketGenerationState={ticketGenerationState}
setTicketGenerationState={setTicketGenerationState}
/>
)
)}
</div>
)
}

View File

@@ -1,179 +0,0 @@
import { useState } from 'react'
import { SupabaseClient } from '@supabase/supabase-js'
import Link from 'next/link'
import cn from 'classnames'
import { Button } from 'ui'
import styles from './ticket.module.css'
import styleUtils from '../../utils.module.css'
import Ticket from './Ticket'
import TicketActions from '~/components/LaunchWeek/8/Ticket/TicketActions'
import TicketCopy from '~/components/LaunchWeek/8/Ticket/ticket-copy'
import { UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
import useWinningChances from '~/components/LaunchWeek/hooks/useWinningChances'
import { SITE_URL } from '~/lib/constants'
import { useBreakpoint } from 'common/hooks/useBreakpoint'
import TicketCustomizationForm from './TicketCustomizationForm'
import TicketDisclaimer from './TicketDisclaimer'
type TicketGenerationState = 'default' | 'loading'
type Props = {
user: UserData
supabase: SupabaseClient
referrals: number
sharePage?: boolean
}
export default function TicketContainer({ user, sharePage, referrals, supabase }: Props) {
const { username, name, golden } = user
const isMobile = useBreakpoint(1023)
const [ticketGenerationState, setTicketGenerationState] =
useState<TicketGenerationState>('default')
const winningChances = useWinningChances()
if (!user.username)
return (
<div className="w-full flex items-center justify-center min-h-[400px]">
<div
className={cn(
styles['ticket-visual'],
styleUtils.appear,
styleUtils['appear-first'],
'relative flex flex-col items-center gap-2 w-full max-w-2xl'
)}
>
<Ticket
user={user}
ticketGenerationState={ticketGenerationState}
setTicketGenerationState={setTicketGenerationState}
/>
<TicketDisclaimer className="mt-4" />
</div>
</div>
)
return (
<div
className={[
`relative w-full max-w-sm md:max-w-[700px] lg:max-w-[1100px] min-h-[400px] flex flex-col items-center lg:grid lg:grid-cols-12 gap-4 lg:p-4 rounded-3xl backdrop-blur lg:items-stretch h-auto`,
!isMobile && styles['ticket-hero'],
].join(' ')}
>
<div
className={[
'flex flex-col !w-full h-full justify-center max-w-lg lg:max-w-none col-span-full p-6 lg:col-span-4 rounded-3xl backdrop-blur lg:backdrop-blur-none',
isMobile && styles['ticket-hero'],
].join(' ')}
>
<div className="text-foreground flex flex-col w-full items-center text-center lg:text-left lg:items-start gap-3">
<h1 className={cn('text-2xl tracking-[-0.02rem] leading-7 block text-white')}>
{!sharePage ? (
name ? (
<>
{winningChances === 1 && (
<>
<span className="text-white">You're in! </span>
Now make it unique and share.
</>
)}
{winningChances === 2 && (
<>
<span className="text-white">That's x2!</span>
<br className="inline lg:hidden" /> Share again to get a golden ticket.
</>
)}
{winningChances === 3 && (
<>
You have a <span className={styles['gold-text']}>golden </span>
chance of winning!
</>
)}
</>
) : (
<>
Generate your ticket. <br />
Win the <span className="gradient-text-purple-800">SupaKeyboard</span>.
</>
)
) : (
<>
{name ? name : username}'s <br className="hidden lg:inline" />
unique ticket
</>
)}
</h1>
<div className="text-sm text-foreground-light leading-5">
{!sharePage ? (
golden ? (
<p>
Join us on August 11th for Launch Week 8's final day and find out if you are one
of the lucky winners.
</p>
) : (
<p>
Customize your ticket and boost your chances of winning{' '}
<Link href="#lw8-prizes" className="underline hover:text-foreground">
limited edition awards
</Link>{' '}
by sharing it with the community.
</p>
)
) : (
<>
<p>
Generate and share your own custom ticket for a chance to win{' '}
<Link href="#lw8-prizes" className="underline hover:text-foreground">
awesome swag
</Link>
.
</p>
<Button type="secondary" asChild>
<a
href={`${SITE_URL}/${username ? '?referral=' + username : ''}`}
className="w-full mt-4 lg:mt-8"
>
Join Launch Week 8
</a>
</Button>
</>
)}
</div>
{!sharePage && user.username && (
<TicketCustomizationForm user={user} supabase={supabase} />
)}
</div>
</div>
<div className="w-full flex-1 col-span-8 lg:-mt-12">
<div
className={cn(
styles['ticket-visual'],
styleUtils.appear,
styleUtils['appear-first'],
'relative flex flex-col items-center gap-4 w-full'
)}
>
<Ticket
user={user}
ticketGenerationState={ticketGenerationState}
setTicketGenerationState={setTicketGenerationState}
/>
{username && (
<div className="w-full">
<TicketActions
username={username}
golden={golden}
ticketGenerationState={ticketGenerationState}
setTicketGenerationState={setTicketGenerationState}
/>
<TicketCopy username={username} isGolden={golden} />
</div>
)}
</div>
</div>
</div>
)
}

View File

@@ -1,145 +0,0 @@
import React, { useState } from 'react'
import { Badge, IconCheck, Input } from 'ui'
import { UserData } from '../../hooks/use-conf-data'
import { SupabaseClient } from '@supabase/supabase-js'
import { useDebounce } from 'common'
interface Props {
supabase: SupabaseClient
user: UserData
}
const TicketCustomizationForm = ({ supabase, user }: Props) => {
const defaultFormValues = {
role: user.metadata?.role,
company: user.metadata?.company,
location: user.metadata?.location,
}
const [formData, setFormData] = useState(defaultFormValues)
const [formState, setFormState] = useState<'idle' | 'saved' | 'saving' | 'error'>('idle')
const IS_SAVING = formState === 'saving'
const IS_SAVED = formState === 'saved'
const HAS_ERROR = formState === 'error'
const handleInputChange = (name: string, value: string) => {
setFormData((prev) => ({ ...prev, [name]: value }))
}
const handleFormSubmit = async () => {
setFormState('saving')
const payload = { metadata: formData }
if (supabase) {
await supabase
.from('lw8_tickets')
.update(payload)
.eq('username', user.username)
.then((res) => {
if (res.error) return setFormState('error')
setFormState('saved')
setTimeout(() => {
setFormState('idle')
}, 1800)
})
}
}
const debouncedChangeHandler = useDebounce(handleFormSubmit, 1200)
return (
<form className="w-full flex flex-col gap-2 mt-4" onChange={() => debouncedChangeHandler()}>
<div className="flex items-center justify-between">
{!IS_SAVED && !HAS_ERROR && (
<span className="opacity-0 animate-fade-in text-foreground-lighter text-xs">
Connected account
</span>
)}
{IS_SAVED && <span className="opacity-0 animate-fade-in text-xs text-brand">Saved</span>}
{HAS_ERROR && (
<span className="opacity-0 animate-fade-in text-xs text-tomato-900">
Something went wrong
</span>
)}
<Badge variant="brand">@{user.username}</Badge>
</div>
<Input
className="[&_input]:border-background"
size="small"
type="text"
required
disabled
placeholder="name"
value={user.name}
icon={<IconCheck strokeWidth={2} className="w-3 text-brand" />}
/>
<Input
className="[&_input]:border-background"
size="small"
type="text"
placeholder="role (optional)"
value={formData.role}
onChange={(event) => {
handleInputChange('role', event.target.value)
}}
disabled={IS_SAVING}
maxLength={30}
icon={
<IconCheck
strokeWidth={2}
className={[
'w-3',
IS_SAVING && 'text-background-surface-300',
!!formData.role ? 'text-brand' : 'text-background-surface-300',
].join(' ')}
/>
}
/>
<Input
className="[&_input]:border-background"
size="small"
type="text"
placeholder="company (optional)"
value={formData.company}
maxLength={30}
onChange={(event) => {
handleInputChange('company', event.target.value)
}}
disabled={IS_SAVING}
icon={
<IconCheck
strokeWidth={2}
className={[
'w-3',
IS_SAVING && 'text-background-surface-300',
!!formData.company ? 'text-brand' : 'text-background-surface-300',
].join(' ')}
/>
}
/>
<Input
className="[&_input]:border-background"
size="small"
type="text"
placeholder="location (optional)"
value={formData.location}
onChange={(event) => {
handleInputChange('location', event.target.value)
}}
disabled={IS_SAVING}
maxLength={20}
icon={
<IconCheck
strokeWidth={2}
className={[
'w-3 flex spin',
IS_SAVING && 'text-background-surface-300',
!!formData.location ? 'text-brand' : 'text-background-surface-300',
].join(' ')}
/>
}
/>
</form>
)
}
export default TicketCustomizationForm

View File

@@ -1,7 +0,0 @@
const TicketDisclaimer = ({ className, children }: { className?: string; children?: any }) => (
<p className={['text-sm text-center text-[#9296AA90]', className].join(' ')}>
{children || 'By registering you accept to receive email updates on Supabase Launch Week.'}
</p>
)
export default TicketDisclaimer

View File

@@ -1,13 +0,0 @@
import React from 'react'
import { LW8_DATE } from '~/lib/constants'
interface Props {}
export default function TicketFooter({}: Props) {
return (
<div className="relative z-10 w-full flex flex-col md:flex-row gap-4 md:gap-8 mb-4 md:mb-6 text-foreground-light text-xs font-mono uppercase tracking-widest">
<span>{LW8_DATE}</span>
<span>supabase.com/launch-week</span>
</div>
)
}

View File

@@ -1,170 +0,0 @@
import { useState, useRef, useEffect } from 'react'
import { useRouter } from 'next/router'
import cn from 'classnames'
import { SITE_ORIGIN } from '~/lib/constants'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import formStyles from './form.module.css'
import ticketFormStyles from './ticket-form.module.css'
import { Button, IconCheckCircle, IconLoader } from 'ui'
import { SupabaseClient } from '@supabase/supabase-js'
type FormState = 'default' | 'loading' | 'error'
type TicketGenerationState = 'default' | 'loading'
type Props = {
defaultUsername?: string
ticketGenerationState?: TicketGenerationState
setTicketGenerationState: any
}
export default function TicketForm({ defaultUsername = '', setTicketGenerationState }: Props) {
const [username, setUsername] = useState(defaultUsername)
const [formState, setFormState] = useState<FormState>('default')
const [errorMsg] = useState('')
const { supabase, session, setUserData, setTicketState, userData } = useConfData()
const [realtimeChannel, setRealtimeChannel] = useState<ReturnType<
SupabaseClient['channel']
> | null>(null)
const formRef = useRef<HTMLFormElement>(null)
const router = useRouter()
useEffect(() => {
if (supabase && session?.user && !userData.id) {
document.body.classList.add('ticket-generated')
const username = session.user.user_metadata.user_name
setUsername(username)
const name = session.user.user_metadata.full_name
const email = session.user.email
supabase
.from('lw8_tickets')
.insert({ email, name, username, referred_by: router.query?.referral ?? null })
.eq('email', email)
.select()
.single()
.then(async ({ error }: any) => {
// If error because of duplicate email, ignore and proceed, otherwise sign out.
if (error && error?.code !== '23505') return supabase.auth.signOut()
const { data } = await supabase
.from('lw8_tickets_golden')
.select('*')
.eq('username', username)
.single()
if (data) {
setUserData(data)
}
setFormState('default')
// Prefetch GitHub avatar
new Image().src = `https://github.com/${username}.png`
// Prefetch the twitter share URL to eagerly generate the page
fetch(`/launch-week/8/tickets/${username}`).catch((_) => {})
setTicketState('ticket')
// Listen to realtime changes
if (!realtimeChannel && !data?.golden) {
const channel = supabase
.channel('changes')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'lw8_tickets',
filter: `username=eq.${username}`,
},
(payload: any) => {
const golden = !!payload.new.sharedOnTwitter && !!payload.new.sharedOnLinkedIn
setUserData({
...payload.new,
golden,
})
if (golden) {
channel.unsubscribe()
}
}
)
.subscribe()
setRealtimeChannel(channel)
}
})
}
return () => {
// Cleanup realtime subscription on unmount
realtimeChannel?.unsubscribe()
}
}, [session])
return formState === 'error' ? (
<div className="h-full">
<div className={cn(formStyles['form-row'], ticketFormStyles['form-row'])}>
<div className={cn(formStyles['input-label'], formStyles.error)}>
<div className={cn(formStyles.input, formStyles['input-text'])}>{errorMsg}</div>
<button
type="button"
className={cn(formStyles.submit, formStyles.error)}
onClick={() => {
setFormState('default')
setTicketGenerationState('default')
}}
>
Try Again
</button>
</div>
</div>
</div>
) : (
<form
ref={formRef}
onSubmit={async (e) => {
e.preventDefault()
if (formState !== 'default') {
setTicketGenerationState('default')
setFormState('default')
return
}
setFormState('loading')
setTicketGenerationState('loading')
await supabase?.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${SITE_ORIGIN}/launch-week/${
userData.username ? '?referral=' + userData.username : ''
}`,
},
})
}}
className="flex flex-col h-full items-center justify-center relative z-20"
>
<div className="flex flex-col gap-3">
<div>
<Button
type="secondary"
htmlType="submit"
disabled={formState === 'loading' || Boolean(session)}
>
<span className={`flex items-center gap-2 ${username && 'text-muted'}`}>
{session ? (
<>
<IconCheckCircle />
Connect with GitHub
</>
) : (
<span className="flex items-center gap-2">
{formState === 'loading' && <IconLoader size={14} className="animate-spin" />}
Connect with GitHub
</span>
)}
</span>
{session ? <span className={ticketFormStyles.checkIcon}></span> : null}
</Button>
</div>
{/* {!session && <p className={'text-xs text-muted'}>Only public info will be used.</p>} */}
</div>
</form>
)
}

View File

@@ -1,20 +0,0 @@
import Image from 'next/image'
import React from 'react'
interface Props {
golden?: boolean
}
export default function TicketHeader({ golden = false }: Props) {
return (
<div className="relative z-10 w-full flex mt-4 md:mt-6 h-10">
<Image
src={`/images/launchweek/8/lw8-logo${golden ? '-gold' : ''}.png`}
alt="Launch Week 8 logo"
layout="fill"
objectFit="contain"
objectPosition="left"
/>
</div>
)
}

View File

@@ -1,31 +0,0 @@
import styles from './ticket.module.css'
type Props = {
number: number | undefined
golden?: boolean
}
export default function TicketNumber({ number, golden = false }: Props) {
const numDigits = `${number}`.length
const prefix = `00000000`.slice(numDigits)
const ticketNumberText = `NO ${prefix}${number}`
return (
<>
<div className="z-10 mt-2 md:mt-0 md:absolute md:flex inset-0 items-center justify-center top-auto md:left-auto md:right-0 md:top-0 md:w-[90px] md:h-100% text-foreground">
<span
className={[
`
md:absolute text-[16px] md:text-[22px] w-full px-2 py-8 md:w-[max-content] leading-[1]
md:transform md:-rotate-90 md:origin-center
text-foreground-light text-center font-mono tracking-[0.8rem]
`,
golden ? styles['ticket-number-gold'] : styles['ticket-number'],
].join(' ')}
>
{ticketNumberText}
</span>
</div>
</>
)
}

View File

@@ -1,42 +0,0 @@
import { UserData } from '../../hooks/use-conf-data'
type TicketGenerationState = 'default' | 'loading'
type Props = {
user: UserData
ticketGenerationState?: TicketGenerationState
setTicketGenerationState?: (ticketGenerationState: TicketGenerationState) => void
golden?: boolean
}
export default function TicketProfile({ user }: Props) {
const { username, name, metadata } = user
const HAS_ROLE = !!metadata?.role
const HAS_COMPANY = !!metadata?.company
const HAS_LOCATION = !!metadata?.location
const HAS_NO_META = !HAS_ROLE && !HAS_COMPANY && !HAS_LOCATION
return (
<div className="relative z-10 flex gap-4 items-center px-2">
<div className="text-foreground-light text-sm md:text-base flex flex-col gap-2">
<p className="text-3xl sm:text-4xl">{name || username || 'Your Name'}</p>
{HAS_NO_META && username && <p>@{username}</p>}
<div>
{HAS_ROLE && <span>{metadata?.role}</span>}
{HAS_COMPANY && (
<span>
{HAS_ROLE && ' '}
<span>at</span> {metadata?.company}
</span>
)}
{HAS_LOCATION && (
<span>
{' '}
{(HAS_ROLE || HAS_COMPANY) && '—'} {metadata?.location}
</span>
)}
</div>
</div>
</div>
)
}

View File

@@ -1,248 +0,0 @@
/* .form {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-bottom: 12px;
gap: 12px;
} */
/* .formLeft {
align-items: center;
}
.formCenter {
align-items: center;
} */
/* @media (min-width: 1200px) {
.formLeft {
align-items: start;
}
.formCenter {
align-items: center;
}
} */
/*
.formInfo {
max-width: 520px;
} */
.formInfo h3 {
font-size: 1.25em;
font-weight: 400;
}
.formInfo p {
font-size: 1.1em;
line-height: 1.4;
margin-bottom: 32px;
color: var(--lw-secondary-color);
}
@media (min-width: 1200px) {
/* .form.share-page {
justify-content: flex-start;
} */
}
/* .formInfoLeft {
text-align: center;
}
.formInfoCenter {
text-align: center;
margin: 0 auto;
} */
@media (min-width: 1200px) {
.formInfoLeft {
text-align: left;
}
.formInfoCenter {
text-align: center;
margin: 0 auto;
}
}
.input {
border: none;
width: 100%;
background: transparent;
outline: none;
height: 56px;
padding-left: 15px;
padding-right: 15px;
font-size: var(--text-md);
color: #fff;
font-family: inherit;
}
@media (min-width: 768px) {
.input {
width: calc(100% - var(--space-32x));
padding-right: 0;
}
}
.input::placeholder {
color: var(--lw-secondary-color);
}
.input-label {
background-color: var(--gray);
border-radius: var(--space-2x);
border: 1px solid transparent;
transition:
border-color 0.2s ease,
background-color 0.2s ease;
display: block;
}
.input-label.focused {
background-color: #33373c;
}
.input-label.error {
background: red;
}
.input-label.success {
background: #0070f3;
}
.input-text {
display: flex;
align-items: center;
width: 100%;
}
.form-row {
position: relative;
max-width: 400px;
width: 100%;
}
@media (min-width: 768px) {
.form-row {
max-width: 480px;
}
}
.submit {
width: 100%;
height: 56px;
margin-top: var(--space-4x);
border-radius: var(--space-2x);
border: 1px solid hsl(var(--brand-default));
background: hsl(var(--brand-default));
cursor: pointer;
font-family: inherit;
font-size: var(--text-md);
letter-spacing: -0.02em;
outline: none;
font-weight: 500;
color: white;
transition:
background-color 0.2s ease,
color 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
}
.submit.generate-with-github {
display: flex;
margin-bottom: 12px;
position: relative;
}
@media (min-width: 768px) {
.submit.register {
width: 120px;
height: 40px;
margin-top: 0;
position: absolute;
border-radius: 5px;
right: var(--space-2x);
top: var(--space-2x);
}
}
.submit.loading {
cursor: default;
}
.submit.default:hover,
.submit.default:focus {
background: black;
color: hsl(var(--brand-default));
}
.submit.default.generate-with-github:hover path,
.submit.default.generate-with-github:focus path {
fill: hsl(var(--brand-default));
}
.submit.error:hover,
.submit.error:focus {
background: #000;
color: #fff;
}
.submit.default:disabled,
.submit.default:disabled:hover,
.submit.default:disabled:focus {
cursor: default;
background: var(--gray);
border-color: var(--gray);
color: #fff;
justify-content: flex-start;
overflow: hidden;
}
.submit.default.generate-with-github:disabled path,
.submit.default.generate-with-github:disabled:hover path,
.submit.default.generate-with-github:disabled:focus path {
fill: #fff;
}
.submit.default.generate-with-github.not-allowed:disabled {
cursor: not-allowed;
}
@media (min-width: 1200px) {
.form-row {
margin: 0;
}
.submit.generate-with-github {
width: 240px;
}
}
.stage-btn {
background: #323332;
border: 2px solid #34b27b;
}
.github-wrapper {
display: flex;
align-items: center;
flex-direction: column;
}
.or-divider {
width: 100%;
text-align: center;
color: var(--lw-secondary-color);
margin: var(--space-4x) 0;
}
@media (min-width: 1200px) {
.github-wrapper {
flex-direction: row;
}
.or-divider {
width: 240px;
margin: 0;
}
}

View File

@@ -1,70 +0,0 @@
.button {
border-radius: var(--space-2x);
border: 1px solid #fff;
color: #000;
background: #fff;
cursor: pointer;
font-family: inherit;
font-size: var(--text-md);
letter-spacing: -0.02em;
font-weight: 500;
outline: none;
transition: background-color 0.2s ease;
display: grid;
grid-template-columns: 24px 1fr;
text-align: center;
align-items: center;
padding: 0 var(--space-4x);
height: 55px;
width: 100%;
max-width: 400px;
margin: var(--space-2x) auto;
}
.loading {
grid-template-columns: 1fr;
justify-items: center;
}
@media (min-width: 768px) {
.button {
width: 200px;
margin: 0 6px;
}
}
.button:hover,
.button:focus {
background: black;
color: #fff;
}
.first {
animation-delay: 2s;
}
.second {
animation-delay: 2.2s;
}
.third {
animation-delay: 2.4s;
}
:global(.ticket-generated) .first {
animation-delay: 0.6s;
}
:global(.ticket-generated) .second {
animation-delay: 0.8s;
}
:global(.ticket-generated) .third {
animation-delay: 1s;
}
.linkedin-button {
display: none !important;
}
@media (min-width: 768px) {
.linkedin-button {
display: grid !important;
}
}

View File

@@ -1,190 +0,0 @@
.wrapper {
color: var(--lw-secondary-color);
display: flex;
align-items: center;
justify-content: center;
animation-delay: 2.6s;
flex-direction: column;
}
:global(.ticket-generated) .wrapper {
animation-delay: 1.2s;
}
.label {
width: 100%;
margin-bottom: var(--space-2x);
}
.label-wrapper {
display: flex;
align-items: center;
max-width: 400px;
width: 100%;
justify-content: space-between;
}
@media (min-width: 768px) {
.wrapper {
flex-direction: row;
}
.label {
width: auto;
margin-right: 10px;
margin-bottom: 0px;
}
.label-wrapper {
width: auto;
}
}
.field {
position: relative;
border-radius: var(--space-2x);
overflow: hidden;
background: var(--gray);
}
@media (min-width: 768px) {
.field {
padding-right: 50px;
}
.field.desktop-copy-disabled {
padding-right: 0px;
}
}
.mobile-copy {
margin-bottom: 10px;
display: flex;
}
.mobile-copy-disabled.mobile-copy-disabled {
display: none;
}
.mobile-copy * {
color: var(--lw-secondary-color);
transition: color 0.2s ease;
}
.mobile-copy:hover * {
color: #fff;
transition: color 0.2s ease;
}
.field * {
color: var(--lw-secondary-color);
transition: color 0.2s ease;
}
.field:hover * {
color: #fff;
transition: color 0.2s ease;
}
.url {
width: calc(100vw - 36px);
user-select: all;
overflow-x: scroll;
white-space: nowrap;
display: block;
padding: var(--space-2x) 14px;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
@media (min-width: 436px) {
.url {
width: 400px;
}
}
@media (min-width: 768px) {
.url {
width: 400px;
}
.mobile-copy {
margin-bottom: 0;
display: none;
}
}
.url::-webkit-scrollbar {
display: none;
}
/* .fade {
position: absolute;
background: linear-gradient(90deg, rgba(37, 39, 41, 0%) 0%, #252729 100%);
top: 0;
bottom: 0;
right: 0px;
width: 50px;
z-index: 1;
display: block;
border-radius: var(--space-2x);
pointer-events: none;
} */
.desktop-copy.desktop-copy-disabled {
display: none;
}
@media (min-width: 768px) {
.fade {
right: 50px;
}
.fade.desktop-copy-disabled {
right: 0px;
}
}
.copied {
display: flex;
z-index: 2;
align-items: center;
justify-content: flex-end;
opacity: 0;
transition: opacity 0.2s ease !important;
}
.copied.visible {
opacity: 1;
}
.copy-button {
background: none;
outline: none;
border: none;
cursor: pointer;
z-index: 2;
border-radius: var(--space-2x);
width: 40px;
display: flex;
align-items: center;
justify-content: center;
margin-right: -10px;
}
@media (min-width: 768px) {
.copied {
position: absolute;
right: 50px;
top: 0px;
bottom: 0px;
background: linear-gradient(90deg, rgba(37, 39, 41, 0%) 0px, #252729 40px);
padding-left: 50px;
}
.copy-button {
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
margin-right: 0px;
width: 50px;
}
}

View File

@@ -1,85 +0,0 @@
import { useEffect, useState, useRef } from 'react'
import cn from 'classnames'
import { SITE_URL } from '~/lib/constants'
import styleUtils from '../../utils.module.css'
import { IconCopy, IconCheck } from 'ui'
type Props = {
username: string
isGolden?: boolean
}
export default function TicketCopy({ username, isGolden }: Props) {
const [fadeOpacity, setFadeOpacity] = useState(1)
const [scrolling, setScrolling] = useState(false)
const [copyEnabled, setCopyEnabled] = useState(false)
const [copied, setCopied] = useState(false)
const scrollRef = useRef<HTMLParagraphElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
const url = `${SITE_URL}/tickets/${username}?lw=8${isGolden ? `&golden=true` : ''}`
useEffect(() => {
if (navigator.clipboard) {
setCopyEnabled(true)
}
}, [])
return (
<div
className={cn(styleUtils.appear, styleUtils['appear-second'], 'h-8 w-full overflow-hidden')}
>
<button
type="button"
name="Copy"
ref={buttonRef}
onClick={() => {
navigator.clipboard.writeText(url).then(() => {
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 2000)
})
}}
className="h-full flex items-center gap-3 w-full truncate relative pr-20 text-foreground-light hover:text-foreground"
>
<div className="flex items-center truncate">
<p
className={['text-xs truncate'].join(' ')}
ref={scrollRef}
onScroll={() => {
if (!scrolling) {
setScrolling(true)
const animationFrame = requestAnimationFrame(() => {
const scrollableWidth =
(scrollRef.current?.scrollWidth || 0) - (scrollRef.current?.clientWidth || 0)
setFadeOpacity(
(scrollableWidth - (scrollRef.current?.scrollLeft || 0)) /
(scrollableWidth || 1)
)
cancelAnimationFrame(animationFrame)
setScrolling(false)
})
}
}}
>
{url}
</p>
</div>
<div className="absolute right-0 with-auto height-auto flex items-center">
<div className="bg-[#E6E8EB] text-[#2e2e2e] text-xs flex items-center cursor-pointer py-1 px-2 rounded">
<div className="flex items-center gap-1">
{copied ? (
<>
<IconCheck size={14} strokeWidth={1.5} /> Copied!
</>
) : (
<>
<IconCopy size={14} strokeWidth={1.5} /> Copy
</>
)}
</div>
</div>
</div>
</button>
</div>
)
}

View File

@@ -1,65 +0,0 @@
.githubIcon {
margin-left: 12px;
margin-right: var(--space-4x);
display: inline-flex;
}
.checkIcon {
position: absolute;
right: var(--space-4x);
display: inline-flex;
}
.generateWithGithub {
display: flex;
align-items: center;
font-weight: 500;
}
.stageIcon {
position: absolute;
left: var(--space-4x);
display: inline-flex;
}
.form-row {
margin-left: auto;
margin-right: auto;
max-width: 600px;
}
.description {
color: var(--secondary-color);
margin: 0;
text-align: center;
margin-left: var(--space-4x);
}
@media (min-width: 768px) {
.form-row {
margin-bottom: 20px;
}
}
@media (min-width: 1200px) {
.form-row {
margin: 0;
}
.description {
text-align: left;
}
.generateWithGithub {
width: 100%;
}
}
.learn-more {
color: #fff;
}
.learn-more:hover {
text-decoration: underline;
color: #fff;
}

View File

@@ -1,149 +0,0 @@
.image {
width: 50px;
height: 50px;
border-radius: 50%;
border: 1px solid #999;
}
@media (min-width: 768px) {
.image {
width: calc(100px * var(--size));
height: calc(100px * var(--size));
}
}
.name {
font-size: 24px;
display: inline-block;
line-height: 1.15;
letter-spacing: -0.02em;
margin: 0;
}
.name-golden {
color: var(--gold-primary);
}
@media (min-width: 768px) {
.name {
font-size: calc(var(--space-8x) * var(--size));
margin-bottom: 5px;
}
}
.username {
display: inline-block;
font-size: calc(1em * var(--size));
color: var(--lw-secondary-color);
display: flex;
align-items: center;
margin: 0;
}
.username-golden {
color: var(--gold-secondary);
}
.githubIcon {
margin-right: calc(5px * var(--size));
display: inline-flex;
}
.githubIcon-golden svg {
color: var(--gold-secondary);
opacity: 0.5;
}
.empty-icon {
display: block;
background: linear-gradient(320deg, #121212, #191919);
width: 60px;
}
.empty-icon--golden {
display: block;
background: linear-gradient(90deg, #fff9eb, #e2ba52);
width: 60px;
border: 1px solid #ecc154ad;
}
@media (min-width: 768px) {
.empty-icon {
width: calc(80px * var(--size));
}
.empty-icon--golden {
width: calc(80px * var(--size));
}
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/*
.skeleton {
display: flex;
align-items: center;
border-radius: 25px;
} */
.skeleton.loaded {
width: unset !important;
}
.skeleton:not(.wrapper):not(.show) {
display: none;
}
.wrapper:not(.show)::before {
content: none;
}
.skeleton:not(.wrapper):not(.loaded) {
border-radius: var(--space);
background-image: linear-gradient(270deg, #252729, #34383d, #34383d, #252729);
background-size: 200% 100%;
animation: loading 2s ease-in-out infinite;
}
.wrapper {
position: relative;
}
.wrapper::before {
content: '';
position: absolute;
top: -1px;
left: -1px;
right: 0;
bottom: 0;
border-radius: var(--space);
z-index: 100;
background-image: linear-gradient(270deg, #111111, #333333, #333333, #111111);
background-size: 200% 100%;
animation: loading 2s ease-in-out infinite;
}
.inline {
display: inline-block !important;
}
.rounded,
.rounded.wrapper::before {
width: calc(80px * var(--size));
height: calc(80px * var(--size));
border-radius: 50% !important;
}
@media (min-width: 768px) {
.rounded,
.rounded.wrapper::before {
width: calc(110px * var(--size));
height: calc(110px * var(--size));
}
}

View File

@@ -1,71 +0,0 @@
.visual {
/* transform: translateZ(0); */
background: linear-gradient(
30deg,
hsla(274, 100%, 77%, 0.05) 0%,
hsla(200, 43%, 1%, 0.2) 30%,
hsla(200, 43%, 1%, 0.2) 70%,
hsla(151, 50%, 53%, 0.1) 100%
),
hsla(200, 43%, 1%, 0.8);
/* background: hsla(200, 43%, 1%, 0.2); */
box-shadow:
0px 1px 27px rgba(158, 68, 239, 0.06),
/* 0 0 0 1px rgba(121, 89, 134, 0.25), */ 0 10px 20px 0px rgba(20, 17, 21, 0.8),
inset 0 0.2px 1px 0.2px rgba(214, 210, 210, 0.4),
inset -0.4px -0.4px 1px -0.3px rgba(74, 252, 175, 0.4);
/* border: 1px solid rgba(0, 0, 0, 0.05); */
/* border-radius: 16px; */
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
/* border: 1px solid #1c1c1c; */
/* animation: 2s ease-in-out infinite alternate; */
background-repeat: no-repeat;
background-size: contain;
}
.visual--gold {
background: radial-gradient(#fcdf9d50, #dbb33340);
}
.visual--gold::before {
content: ' ';
position: absolute;
inset: 0;
filter: drop-shadow(0 0 60px #fcdf9d);
background: radial-gradient(#fcdf9d50, #dbb33320);
/* border: 1px solid #dbb33320; */
}
@keyframes opacity-pulse {
from {
opacity: 0.5;
}
to {
opacity: 1;
}
}
@keyframes svg-shadow {
from {
filter: drop-shadow(0 0 2px #fff) drop-shadow(0 0 4px #d99f0c) drop-shadow(0 0 6px #d99f0c);
}
to {
filter: drop-shadow(0 0 10px #fff) drop-shadow(0 0 6px #d99f0c) drop-shadow(0 0 9px #d99f0c);
}
}
@keyframes blank-svg-shadow {
from {
filter: drop-shadow(0 0 2px #bada55) drop-shadow(0 0 2px #bada55) drop-shadow(0 0 2px #bada55);
}
to {
filter: drop-shadow(0 0 3px #bada55) drop-shadow(0 0 2px #bada55) drop-shadow(0 0 2px #bada55);
}
}

View File

@@ -1,160 +0,0 @@
.ticket-hero {
background: linear-gradient(137.46deg, rgba(125, 196, 176, 0.04) 0%, rgba(255, 255, 255, 0) 100%),
linear-gradient(137.46deg, rgba(255, 255, 255, 0) 6.19%, rgba(255, 255, 255, 0) 103.29%);
z-index: 10;
box-shadow:
0 1px 1px rgba(131, 6, 184, 0.1),
0 1px 1px rgba(128, 6, 184, 0.1),
0 20px 30px -30px rgba(76, 195, 138, 0.05),
0 80px 40px -40px rgba(128, 6, 184, 0.04),
inset 0.25px 0.3px 0px 0.3px rgba(209, 243, 227, 0.1),
inset 0.25px 0.3px 0px 0.3px rgba(76, 195, 138, 0.02),
inset 0 -3px 0 -2px rgba(255, 255, 255, 0.08),
0px 4px 20px rgba(118, 7, 170, 0.1);
}
.hero {
line-height: 1.15;
letter-spacing: -0.05em;
font-weight: 400;
margin: 0;
text-align: center;
}
@media (min-width: 768px) {
.hero {
line-height: 1;
}
}
.description {
font-size: 20px;
line-height: 1.4;
color: var(--lw-secondary-color);
text-align: center;
margin: 30px auto;
max-width: 350px;
}
@media (min-width: 768px) {
.description {
font-size: 24px;
max-width: 415px;
}
}
.ticket-text {
margin-bottom: 30px;
}
.ticket-layout {
display: grid;
grid-template-columns: 1fr;
gap: 45px;
margin: auto;
padding: var(--space-4x);
padding-top: 64px;
padding-bottom: 64px;
}
@media (min-width: 1200px) {
.ticket-layout {
padding-top: 0;
padding-bottom: 0;
}
}
.ticket-share-layout {
align-items: center;
margin-bottom: auto;
}
.ticket-visual:before {
left: 50%;
top: -7.5%;
transform: translateZ(0) translateX(-50%) rotate(90deg);
}
.ticket-visual:after {
left: 50%;
bottom: -7.5%;
transform: translateZ(0) translateX(-50%) rotate(90deg);
}
@media (min-width: 768px) {
.ticket-visual:before {
top: 50%;
left: -7%;
transform: translateZ(0) translateY(-50%);
}
.ticket-visual:after {
top: 50%;
left: 96%;
transform: translateZ(0) translateY(-50%);
}
}
.ticket-actions {
margin-top: 70px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.ticket-copy {
margin-top: 30px;
}
.ticket-actions-placeholder {
height: 280px;
}
@media (min-width: 768px) {
.ticket-actions {
flex-direction: row;
}
.ticket-actions-placeholder {
height: 157px;
}
}
@media (min-width: 1200px) {
.ticket-layout {
grid-template-columns: 1fr 1fr;
}
.hero {
text-align: left;
}
.description {
text-align: left;
margin: 30px 0;
}
}
.gold-text {
color: #f4cf72;
text-shadow:
0 0 1px rgba(0, 0, 0, 0.5),
0 0 4px rgba(0, 0, 0, 0.1);
}
@media (min-width: 768px) {
.gold-text {
color: #e4b641;
}
}
.ticket-number {
text-shadow:
0 0px 2px rgba(22, 22, 22, 0.9),
0 0px 6px rgba(131, 6, 184, 0.9),
0 0px 12px rgba(217, 195, 226, 0.5);
}
.ticket-number-gold {
text-shadow:
0 0px 2px rgba(22, 22, 22, 0.9),
0 0px 6px rgba(255, 195, 125, 0.9),
0 0px 12px rgba(226, 224, 195, 0.5);
}

View File

@@ -1,213 +0,0 @@
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react'
import { Button, cn } from 'ui'
import { SITE_ORIGIN } from '~/lib/constants'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import { VALID_KEYS } from '~/components/LaunchWeek/hooks/useLwGame'
const COMPLIMENTS = [
'Congratulations!',
'You won!',
'Hurray!',
'Great job!',
'Nice job!',
"That's right!",
]
interface Props {
setIsGameMode: Dispatch<SetStateAction<boolean>>
}
const LWXGame = ({ setIsGameMode }: Props) => {
const { supabase, userData: user } = useConfData()
const word = process.env.NEXT_PUBLIC_LWX_GAME_WORD ?? 'database'
const winningWord = word?.split('')
const [currentWord, setCurrentWord] = useState<string[]>(Array(winningWord.length))
const [gameState, setGameState] = useState<'playing' | 'winner' | 'loading'>('playing')
const [hasKeyDown, setHasKeyDown] = useState(false)
const [attempts, setAttempts] = useState(1)
const hasWon = currentWord.join('') === winningWord.join('')
const winningCompliment = useMemo(
() => COMPLIMENTS[Math.floor(Math.random() * COMPLIMENTS.length)],
[]
)
const searchAndAddToCurrentWord = (key: string) => {
setAttempts(attempts + 1)
for (let index = 0; index < winningWord?.length; index++) {
const isAlreadyPresent = key === currentWord[index]
if (isAlreadyPresent) return
const isMatch = key === winningWord[index]
if (isMatch) {
const newCurrentWord = currentWord
newCurrentWord[index] = key
setCurrentWord(newCurrentWord)
if (hasWon) setGameState('winner')
}
}
}
function onKeyDown(event: KeyboardEvent) {
const newKey = event.key.toLocaleLowerCase()
if (!(event.metaKey || event.ctrlKey) && VALID_KEYS.includes(newKey)) {
setHasKeyDown(true)
searchAndAddToCurrentWord(newKey)
}
setTimeout(() => {
setHasKeyDown(false)
}, 100)
}
useEffect(() => {
window.addEventListener('keydown', onKeyDown)
return () => window.removeEventListener('keydown', onKeyDown)
}, [onKeyDown])
async function handleGithubSignIn() {
const redirectTo = `${SITE_ORIGIN}/launch-week/${
user.username ? '?referral=' + user.username : ''
}`
supabase?.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo,
},
})
}
const handleClaimTicket = async (e: any) => {
e.preventDefault()
setGameState('loading')
if (supabase) {
if (user.id) {
await supabase
.from('lwx_tickets')
.update({ metadata: { ...user.metadata, hasSecretTicket: hasWon } })
.eq('username', user.username)
.then((res) => {
if (res.error) return console.log('error', res.error)
setIsGameMode(false)
})
} else {
localStorage.setItem('lwx_hasSecretTicket', 'true')
handleGithubSignIn()
}
}
}
if (gameState === 'loading')
return (
<div className="relative w-full mt-[100px] md:mt-44 lg:mt-32 xl:mt-32 2xl:mt-[120px] flex flex-col items-center gap-6 text-foreground">
<svg
className="animate-spinner opacity-50 w-5 h-5 md:w-6 md:h-6"
width="100%"
height="100%"
viewBox="0 0 62 61"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M61 31C61 14.4315 47.5685 1 31 1C14.4315 1 1 14.4315 1 31"
stroke="white"
strokeWidth="2"
/>
</svg>
</div>
)
return (
<div className="flex flex-col items-center text-center gap-12 md:gap-16">
<div className="flex flex-col items-center h-10 text-foreground-light">
<div
className={cn(
'absolute flex flex-col gap-2 opacity-0 translate-y-2 transition-all',
hasWon && 'opacity-100 translate-y-0'
)}
>
<p className="tracking-wider text-foreground text-lg font-mono uppercase">
{winningCompliment}
</p>
<p className="text-foreground-lighter font-san text-sm">
Claim and share the secret ticket to boost your chances of winning swag.
</p>
</div>
<div
className={cn(
'absolute flex justify-center opacity-0 translate-y-2 transition-all',
!hasWon && 'opacity-100 translate-y-0'
)}
>
Guess the word
</div>
</div>
<div className="flex items-center justify-center gap-2 flex-wrap font-mono h-16">
{winningWord.map((letter, i) => {
const isMatch = letter === currentWord[i]
return (
<div
key={`${currentWord[i]}-${i}`}
className={cn(
'w-6 md:w-14 aspect-square bg-[#06080930] backdrop-blur-sm flex items-center hover:border-strong justify-center uppercase border rounded-sm md:rounded-lg transition-colors',
isMatch && 'border-stronger bg-foreground text-[#060809]',
hasWon && 'animate-pulse !border-foreground',
hasKeyDown && 'border-strong'
)}
>
{currentWord[i]}
</div>
)
})}
</div>
<form onSubmit={handleClaimTicket} className="flex flex-col items-center h-10">
<Button
type="secondary"
onClick={() => null}
htmlType="submit"
className={cn(
'absolute opacity-0 translate-y-2 transition-all',
hasWon && 'opacity-100 translate-y-0'
)}
>
Claim secret ticket
</Button>
</form>
<div className="flex gap-4 md:gap-10 items-center h-10 text-xs text-foreground-lighter">
<div className="flex items-center gap-2">
<Button
type="outline"
onClick={() => null}
disabled
className="pointer-events-none"
size="tiny"
>
A-Z
</Button>
<span>Play</span>
</div>
<div className="flex items-center gap-2 text-foreground-muted">
<Button
type="outline"
onClick={() => null}
disabled
className="pointer-events-none"
size="tiny"
>
Esc
</Button>
<span>Exit</span>
</div>
</div>
</div>
)
}
export default LWXGame

View File

@@ -1,84 +0,0 @@
import { useState } from 'react'
import Image from 'next/image'
import { IconEdit2, IconX, cn } from 'ui'
import Panel from '~/components/Panel'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import TicketProfile from './TicketProfile'
import TicketFooter from './TicketFooter'
import { useBreakpoint, useParams } from 'common'
export default function Ticket() {
const { userData: user, showCustomizationForm, setShowCustomizationForm } = useConfData()
const isMobile = useBreakpoint()
const { golden = false, bg_image_id: bgImageId = '1', metadata } = user
const [imageHasLoaded, setImageHasLoaded] = useState(false)
const params = useParams()
const sharePage = !!params.username
const ticketType = metadata?.hasSecretTicket ? 'secret' : golden ? 'platinum' : 'regular'
const fallbackImg = `/images/launchweek/lwx/tickets/lwx_ticket_bg_${ticketType}.png`
const ticketBg = {
regular: {
background: `/images/launchweek/lwx/tickets/lwx_ticket_bg_regular.png`,
background_mobile: `/images/launchweek/lwx/tickets/lwx_ticket_regular_mobile.png`,
},
platinum: {
background: `/images/launchweek/lwx/tickets/lwx_ticket_bg_platinum.png`,
background_mobile: `/images/launchweek/lwx/tickets/lwx_ticket_platinum_mobile.png`,
},
secret: {
background: `/images/launchweek/lwx/tickets/lwx_ticket_bg_secret.png`,
background_mobile: `/images/launchweek/lwx/tickets/lwx_ticket_secret_mobile.png`,
},
}
function handleCustomizeTicket() {
setShowCustomizationForm && setShowCustomizationForm(!showCustomizationForm)
}
return (
<Panel
hasShimmer
outerClassName="flex relative flex-col w-[300px] h-auto max-h-[480px] md:w-full md:max-w-none rounded-3xl !shadow-xl"
innerClassName="flex relative flex-col justify-between w-full transition-colors aspect-[1/1.6] md:aspect-[1.967/1] rounded-3xl bg-[#020405] text-left text-sm group/ticket"
shimmerFromColor="hsl(var(--border-strong))"
shimmerToColor="hsl(var(--background-default))"
>
{/* Edit hover button */}
{!sharePage && (
<>
<button
className="absolute z-40 inset-0 w-full h-full outline-none"
onClick={handleCustomizeTicket}
/>
<div className="hidden md:flex opacity-0 translate-y-3 group-hover/ticket:opacity-100 group-hover/ticket:translate-y-0 transition-all absolute z-30 inset-0 m-auto w-10 h-10 rounded-full items-center justify-center bg-[#020405] border shadow-lg text-foreground">
{!showCustomizationForm ? <IconEdit2 className="w-4" /> : <IconX className="w-4" />}
</div>
</>
)}
<div className="absolute inset-0 h-full p-6 md:p-10 z-30 flex flex-col justify-end md:justify-between w-full md:h-full flex-1 overflow-hidden">
<TicketProfile />
<TicketFooter />
</div>
<Image
src={ticketBg[ticketType][!isMobile ? 'background' : 'background_mobile']}
alt={`Launch Week X ticket background #${bgImageId}`}
placeholder="blur"
blurDataURL={fallbackImg}
onLoad={() => setImageHasLoaded(true)}
loading="eager"
fill
className={cn(
'absolute inset-0 object-cover object-right opacity-0 transition-opacity duration-1000',
imageHasLoaded && 'opacity-100',
isMobile && 'object-left-top'
)}
priority
quality={100}
/>
</Panel>
)
}

View File

@@ -1,97 +0,0 @@
import { useEffect, useRef, useState } from 'react'
import Link from 'next/link'
import { SITE_URL, TWEET_TEXT, TWEET_TEXT_GOLDEN, TWEET_TEXT_SECRET } from '~/lib/constants'
import { Button, IconLinkedinSolid, IconTwitterX, cn } from 'ui'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import { useParams } from '~/hooks/useParams'
import { useBreakpoint } from 'common'
export default function TicketActions() {
const { userData, supabase } = useConfData()
const { golden, username, metadata } = userData
const [_imgReady, setImgReady] = useState(false)
const [_loading, setLoading] = useState(false)
const isTablet = useBreakpoint(1280)
const downloadLink = useRef<HTMLAnchorElement>()
const hasSecretTicket = metadata?.hasSecretTicket
const link = `${SITE_URL}/tickets/${username}?lw=x${
hasSecretTicket ? '&secret=true' : golden ? `&platinum=true` : ''
}`
const permalink = encodeURIComponent(link)
const text = hasSecretTicket ? TWEET_TEXT_SECRET : golden ? TWEET_TEXT_GOLDEN : 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}`
const downloadUrl = `https://obuldanrptloktxcffvn.supabase.co/functions/v1/lwx-og?username=${encodeURIComponent(
username ?? ''
)}`
const params = useParams()
const sharePage = !!params.username
const LW_TABLE = 'lwx_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') => {
if (!supabase) return
setTimeout(async () => {
if (social === 'twitter') {
await supabase.from(LW_TABLE).update({ sharedOnTwitter: 'now' }).eq('username', username)
// window.open(tweetUrl, '_blank')
} else if (social === 'linkedin') {
await supabase.from(LW_TABLE).update({ sharedOnLinkedIn: 'now' }).eq('username', username)
// window.open(linkedInUrl, '_blank')
}
})
}
return (
<div
className={cn(
'w-full gap-3 flex flex-col md:flex-row items-center',
sharePage ? 'justify-center' : 'justify-between'
)}
>
<div className="flex w-full gap-2">
<Button
onClick={() => handleShare('twitter')}
type={userData.sharedOnTwitter ? 'secondary' : 'default'}
icon={<IconTwitterX className="text-light w-3" />}
size={isTablet ? 'tiny' : 'tiny'}
block
asChild
>
<Link href={tweetUrl} target="_blank">
Share on X
</Link>
</Button>
<Button
onClick={() => handleShare('linkedin')}
type={userData.sharedOnLinkedIn ? 'secondary' : 'default'}
icon={<IconLinkedinSolid className="text-light w-3" />}
size={isTablet ? 'tiny' : 'tiny'}
block
asChild
>
<Link href={linkedInUrl} target="_blank">
Share on Linkedin
</Link>
</Button>
</div>
</div>
)
}

View File

@@ -1,19 +0,0 @@
import { useParams } from 'common'
import Ticket from './Ticket'
import TicketCustomizationForm from './TicketCustomizationForm'
import TicketCopy from './TicketCopy'
export default function TicketContainer() {
const params = useParams()
const sharePage = !!params.username
return (
<div className="flex flex-col w-full items-center mx-auto max-w-2xl gap-3 group group-hover">
{!sharePage && <TicketCustomizationForm className="order-last md:order-first" />}
<Ticket />
<div className="flex flex-col md:flex-row gap-2 items-center justify-center mx-auto max-w-full">
<TicketCopy sharePage={sharePage} />
</div>
</div>
)
}

View File

@@ -1,52 +0,0 @@
import { useEffect, useState, useRef } from 'react'
import { SITE_URL } from '~/lib/constants'
import useConfData from '../../hooks/use-conf-data'
import { IconCheck, IconCopy, cn } from 'ui'
export default function TicketCopy({ sharePage }: { sharePage: boolean }) {
const { userData } = useConfData()
const { username, golden, metadata } = userData
const [_copyEnabled, setCopyEnabled] = useState(false)
const [copied, setCopied] = useState(false)
const buttonRef = useRef<HTMLButtonElement>(null)
const hasSecretTicket = metadata?.hasSecretTicket
const url = `${SITE_URL}/x/tickets/${username}?lw=x${
hasSecretTicket ? '&secret=true' : golden ? `&platinum=true` : ''
}`
useEffect(() => {
if (navigator.clipboard) {
setCopyEnabled(true)
}
}, [])
return (
<div
className={cn('h-full w-full overflow-hidden max-w-full', sharePage ? 'w-auto' : 'w-full')}
>
<button
type="button"
name="Copy"
ref={buttonRef}
onClick={() => {
navigator.clipboard.writeText(url).then(() => {
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 2000)
})
}}
className="w-full h-full flex justify-center md:justify-start items-center gap-2 relative text-foreground-light hover:text-foreground text-xs"
>
<div className="w-4 min-w-4 flex-shrink-0">
{copied ? (
<IconCheck size={14} strokeWidth={1.5} />
) : (
<IconCopy size={14} strokeWidth={1.5} />
)}
</div>
<span className="truncate">{url}</span>
</button>
</div>
)
}

View File

@@ -1,137 +0,0 @@
import React, { useState } from 'react'
import { Button, IconCheck, Input, cn } from 'ui'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import { useBreakpoint, useDebounce } from 'common'
import { useKey } from 'react-use'
const TicketCustomizationForm = ({ className }: { className?: string }) => {
const isMobile = useBreakpoint()
const {
supabase,
userData: user,
showCustomizationForm,
setShowCustomizationForm,
} = useConfData()
const defaultFormValues = {
role: user.metadata?.role,
company: user.metadata?.company,
}
const [formData, setFormData] = useState(defaultFormValues)
const [formState, setFormState] = useState<'idle' | 'saved' | 'saving' | 'error'>('idle')
const IS_SAVING = formState === 'saving'
const IS_SAVED = formState === 'saved'
const HAS_ERROR = formState === 'error'
useKey('Escape', () => setShowCustomizationForm && setShowCustomizationForm(false))
const handleInputChange = (name: string, value: string) => {
setFormData((prev) => ({ ...prev, [name]: value }))
}
const handleFormSubmit = async () => {
setFormState('saving')
const payload = {
metadata: {
...user.metadata,
...formData,
},
}
if (supabase) {
await supabase
.from('lwx_tickets')
.update(payload)
.eq('username', user.username)
.then((res) => {
if (res.error) return setFormState('error')
setFormState('saved')
setTimeout(() => {
setFormState('idle')
}, 1200)
})
}
}
const debouncedChangeHandler = useDebounce(handleFormSubmit, 1200)
return (
<form
className={cn(
'w-full grid grid-cols-1 md:grid-cols-3 gap-2 max-w-[300px] md:max-w-none mx-auto -mt-10 transition-all opacity-0 translate-y-3',
(isMobile || showCustomizationForm) && 'opacity-100 translate-y-0',
isMobile && 'mt-2',
className
)}
onChange={() => debouncedChangeHandler()}
onSubmit={(e) => e.preventDefault()}
>
<Input
className="[&_input]:border-background"
size="small"
type="text"
placeholder="role (optional)"
value={formData.role}
onChange={(event) => {
handleInputChange('role', event.target.value)
}}
disabled={IS_SAVING}
maxLength={25}
icon={
<IconCheck
strokeWidth={2}
className={cn(
'w-3',
IS_SAVING && 'text-background-surface-300',
!!formData.role ? 'text-brand' : 'text-background-surface-300'
)}
/>
}
/>
<Input
className="[&_input]:border-background"
size="small"
type="text"
placeholder="company (optional)"
value={formData.company}
maxLength={25}
onChange={(event) => {
handleInputChange('company', event.target.value)
}}
disabled={IS_SAVING}
icon={
<IconCheck
strokeWidth={2}
className={cn(
'w-3',
IS_SAVING && 'text-background-surface-300',
!!formData.company ? 'text-brand' : 'text-background-surface-300'
)}
/>
}
/>
<div className="flex items-center justify-center md:justify-end gap-2">
{IS_SAVED && (
<span className="hidden md:inline opacity-0 animate-fade-in text-xs text-foreground-light">
Saved
</span>
)}
{HAS_ERROR && (
<span className="hidden md:inline opacity-0 animate-fade-in text-xs text-foreground">
Something went wrong
</span>
)}
<Button
type="outline"
size="tiny"
htmlType="submit"
block={isMobile}
onClick={() => setShowCustomizationForm && setShowCustomizationForm(false)}
>
Done
</Button>
</div>
</form>
)
}
export default TicketCustomizationForm

View File

@@ -1,38 +0,0 @@
import React from 'react'
import { LWX_DATE } from '~/lib/constants'
import TicketNumber from './TicketNumber'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import Image from 'next/image'
import { cn } from 'ui'
export default function TicketFooter() {
const { userData: user } = useConfData()
const { ticketNumber, golden, metadata } = user
const hasLightTicket = golden || metadata?.hasSecretTicket
const SupabaseLogo = hasLightTicket
? '/images/launchweek/lwx/logos/supabase_lwx_logo_light.png'
: '/images/launchweek/lwx/logos/supabase_lwx_logo_dark.png'
return (
<div
className={cn(
'relative z-10 w-full flex flex-col gap-2 font-mono text-foreground leading-none uppercase tracking-[3px]',
hasLightTicket ? 'text-[#11181C]' : 'text-white'
)}
>
<Image
src={SupabaseLogo}
alt="Supabase Logo for Launch Week X"
width="30"
height="30"
className="mb-1 hidden md:block"
priority
quality={100}
/>
<TicketNumber number={ticketNumber} />
<span>Launch Week X</span>
<span>{LWX_DATE}</span>
</div>
)
}

Some files were not shown because too many files have changed in this diff Show More