mirror of
https://github.com/supabase/supabase.git
synced 2026-10-16 22:59:11 +03:00
Merge pull request #16022 from supabase/feat/lw8-ticketing
Feat/lw8 ticketing
This commit is contained in:
151 files changed
+5202
-522
No files matched your search
@@ -8,6 +8,7 @@ import Head from 'next/head'
|
||||
import { PropsWithChildren, memo } from 'react'
|
||||
import Footer from '~/components/Navigation/Footer'
|
||||
import { menuState, useMenuLevelId, useMenuMobileOpen } from '~/hooks/useMenuState'
|
||||
import { Announcement, LW8CountdownBanner } from 'ui'
|
||||
|
||||
const levelsData = {
|
||||
home: {
|
||||
@@ -326,6 +327,9 @@ const SiteLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
<title>Supabase Docs</title>
|
||||
</Head>
|
||||
<main>
|
||||
<Announcement>
|
||||
<LW8CountdownBanner />
|
||||
</Announcement>
|
||||
<div className="flex flex-row h-screen">
|
||||
<NavContainer />
|
||||
<Container>
|
||||
|
||||
@@ -57,7 +57,7 @@ And voila, we had a baseline for generating variations:
|
||||
|
||||
The final visual that you see everywhere won us over because of its vibrant swirls. While each swirl looks slightly different, a consistent style is still maintained throughout.
|
||||
|
||||
Fortunately, we didn't require full-width images for each ticket. Upscaled MidJourney images sufficed. Nevertheless, we had to recreate the main visual on the landing page in vectors to use it effectively. You can view it in full glory at [https://supabase.com/launch-week](https://supabase.com/launch-week).
|
||||
Fortunately, we didn't require full-width images for each ticket. Upscaled MidJourney images sufficed. Nevertheless, we had to recreate the main visual on the landing page in vectors to use it effectively. You can view it in full glory at [https://supabase.com/launch-week/7](https://supabase.com/launch-week/7).
|
||||
|
||||

|
||||
|
||||
@@ -258,13 +258,13 @@ const channel = supabase
|
||||
.subscribe()
|
||||
```
|
||||
|
||||
Interested to know how this fits within your Next.js application? Find the code [here](https://github.com/supabase/supabase/blob/master/apps/www/components/LaunchWeek/Ticket/TicketForm.tsx#L70-L102).
|
||||
Interested to know how this fits within your Next.js application? Find the code [here](https://github.com/supabase/supabase/blob/master/apps/www/components/LaunchWeek/7/Ticket/TicketForm.tsx#L70-L102).
|
||||
|
||||
## Displaying the images
|
||||
|
||||
We have some images, so how can we now display them somewhere? [@Francesco Sansalvadore](https://twitter.com/frnk_snslvdr) saw one of our team members trying to “swipe” on the tickets slider component on the [launch week page](https://supabase.com/launch-week/tickets) and he thought, why not feature all the fantastic people who generated tickets on a single page?
|
||||
We have some images, so how can we now display them somewhere? [@Francesco Sansalvadore](https://twitter.com/frnk_snslvdr) saw one of our team members trying to “swipe” on the tickets slider component on the [launch week page](https://supabase.com/launch-week/7/tickets) and he thought, why not feature all the fantastic people who generated tickets on a single page?
|
||||
|
||||
We built a [ticket wall](https://supabase.com/launch-week/tickets) that you can scroll endlessly. We approached it with an infinite scroll technique, lazy-loading a few tickets at a time.
|
||||
We built a [ticket wall](https://supabase.com/launch-week/7/tickets) that you can scroll endlessly. We approached it with an infinite scroll technique, lazy-loading a few tickets at a time.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import { Button } from 'ui'
|
||||
|
||||
const CTABanner = (props: any) => {
|
||||
const { darkerBg } = props
|
||||
interface Props {
|
||||
className?: string
|
||||
darkerBg?: boolean
|
||||
}
|
||||
|
||||
const CTABanner = ({ darkerBg, className }: Props) => {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
bg-scale-200 grid grid-cols-12 items-center gap-4 border-t py-32 text-center
|
||||
${darkerBg ? 'dark:bg-dark-900' : ''} px-16
|
||||
`}
|
||||
className={[
|
||||
`bg-scale-200 grid grid-cols-12 items-center gap-4 border-t py-32 text-center px-16`,
|
||||
darkerBg && 'dark:bg-dark-900',
|
||||
className,
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="col-span-12">
|
||||
<h2 className="h2">
|
||||
<span className="text-scale-900">Build in a weekend,</span>
|
||||
<span className="text-scale-1200 dark:text-white"> scale to millions</span>
|
||||
<span className="text-scale-1200 dark:text-white block sm:inline">
|
||||
{' '}
|
||||
scale to millions
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="col-span-12 mt-4">
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { useTheme } from 'common/Providers'
|
||||
import { IconSun, IconMoon } from 'ui'
|
||||
|
||||
function DarkModeToggle() {
|
||||
function DarkModeToggle({ disabled = false }: { disabled?: boolean }) {
|
||||
const { isDarkMode, toggleTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className={`flex items-center ${disabled && 'opacity-30'}`}>
|
||||
<IconSun className="text-scale-900" strokeWidth={2} />
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed="false"
|
||||
disabled={disabled}
|
||||
className={`
|
||||
relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent
|
||||
transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||
isDarkMode ? 'bg-scale-500' : 'bg-scale-900'
|
||||
} mx-5
|
||||
} ${disabled && '!cursor-not-allowed'} mx-5
|
||||
`}
|
||||
onClick={() => toggleTheme()}
|
||||
>
|
||||
|
||||
@@ -17,7 +17,7 @@ interface Props {
|
||||
const Footer = (props: Props) => {
|
||||
const { isDarkMode } = useTheme()
|
||||
const { pathname } = useRouter()
|
||||
const isLaunchWeekPage = pathname.includes('launch-week')
|
||||
const isLaunchWeekPage = pathname.includes('launch-week') || pathname === '/'
|
||||
|
||||
return (
|
||||
<footer
|
||||
@@ -149,7 +149,7 @@ const Footer = (props: Props) => {
|
||||
</div>
|
||||
<div className="border-scale-500 dark:border-scale-600 mt-32 flex justify-between border-t pt-8">
|
||||
<small className="small">© Supabase Inc</small>
|
||||
<DarkModeToggle />
|
||||
<DarkModeToggle disabled={isLaunchWeekPage} />
|
||||
</div>
|
||||
</SectionContainer>
|
||||
</footer>
|
||||
|
||||
@@ -3,31 +3,59 @@ import { useRouter } from 'next/router'
|
||||
import Telemetry, { TelemetryEvent } from '~/lib/telemetry'
|
||||
import { useTelemetryProps } from 'common/hooks/useTelemetryProps'
|
||||
import gaEvents from '~/lib/gaEvents'
|
||||
import { Button, IconBookOpen } from 'ui'
|
||||
import { Badge, Button, IconArrowRight, IconBookOpen } from 'ui'
|
||||
import SectionContainer from '~/components/Layouts/SectionContainer'
|
||||
import HeroFrameworks from './HeroFrameworks'
|
||||
import styles from './hero.module.css'
|
||||
import Image from 'next/image'
|
||||
import { range } from 'lodash'
|
||||
import ParticlesCanvas from '../LaunchWeek/8/AnimatedParticles/ParticlesCanvas'
|
||||
import { useBreakpoint } from 'common'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
const Hero = () => {
|
||||
const router = useRouter()
|
||||
const [showAnnouncement, setShowAnnouncement] = useState(false)
|
||||
const telemetryProps = useTelemetryProps()
|
||||
const isMobile = useBreakpoint()
|
||||
const sendTelemetryEvent = async (event: TelemetryEvent) => {
|
||||
await Telemetry.sendEvent(event, telemetryProps, router)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== undefined) {
|
||||
const lw8AnnouncementKey = window.localStorage.getItem('announcement_SupabaseLaunchWeek8')
|
||||
setShowAnnouncement(!!lw8AnnouncementKey)
|
||||
}
|
||||
}, [showAnnouncement])
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="relative -mt-[65px]">
|
||||
<SectionContainer className="py-12 md:py-16 lg:py-20">
|
||||
<div className="relative">
|
||||
<div className="mx-auto">
|
||||
<div className="mx-auto max-w-2xl lg:col-span-6 lg:flex lg:items-center justify-center text-center">
|
||||
<div
|
||||
className={[
|
||||
'appear-first lg:h-[50vh] lg:min-h-[300px] lg:max-h-[450px] 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',
|
||||
'relative z-10 appear-first lg:h-auto pt-[250px] lg:pt-[250px] 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',
|
||||
styles['hero-text'],
|
||||
].join(' ')}
|
||||
>
|
||||
<div>
|
||||
<div className="flex flex-col items-center">
|
||||
{showAnnouncement && (
|
||||
<Link href="/launch-week">
|
||||
<a className="group w-auto rounded-full p-1 mb-6 mt-8 md:mb-10 bg-gradient-to-b from-[#1b1f2124] to-[#02040550] hover:from-[#ffffff15] hover:to-[#ffffff05] transition-all backdrop-blur-lg border flex items-center justify-between gap-2 md:gap-4 text-scale-1100 hover:text-scale-1200 text-sm">
|
||||
<div className="text-scale-200 rounded-full bg-brand-900 px-3 py-1 flex items-center justify-center">
|
||||
Get you ticket
|
||||
</div>
|
||||
<span>Launch Week 8 - August 7th</span>
|
||||
<IconArrowRight
|
||||
size={16}
|
||||
className="transform mr-2 transition-transform translate-x-0 sm:-translate-x-1 group-hover:translate-x-0"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
<h1 className="text-scale-1200 text-4xl sm:text-5xl sm:leading-none lg:text-7xl">
|
||||
<span className="block text-[#F4FFFA00] bg-clip-text bg-gradient-to-b from-scale-1200 to-scale-1200 dark:to-scale-1100">
|
||||
Build in a weekend
|
||||
@@ -63,12 +91,35 @@ const Hero = () => {
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<HeroFrameworks className="mt-4 lg:mt-6" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -top-2 md:top-0 -left-10 -right-10 md:left-0 md:right-0 h-[500px] lg:h-[550px] z-0">
|
||||
<ParticlesCanvas users={range(0, 60)} />
|
||||
<Image
|
||||
src="/images/launchweek/8/stars.svg"
|
||||
alt="starts background"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="opacity-70"
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
</SectionContainer>
|
||||
<div className="absolute w-full aspect-[1/1] md:aspect-[1.5/1] lg:aspect-[2.5/1] inset-0 z-0">
|
||||
<Image
|
||||
src="/images/launchweek/8/LW8-gradient.png"
|
||||
layout="fill"
|
||||
objectFit={isMobile ? 'cover' : 'contain'}
|
||||
objectPosition="top"
|
||||
priority
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute top-0 left-0 right-0 h-screen bg-gradient-to-b from-[#020405] to-transparent -z-10" />
|
||||
<div className="w-1/2 container mx-auto h-px bg-gradient-to-r from-transparent via-scale-600 to-transparent" />
|
||||
</div>
|
||||
)
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -2,7 +2,7 @@ import { motion } from 'framer-motion'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import days from '~/components/LaunchWeek/lw7_days'
|
||||
import days from '~/components/LaunchWeek/7/lw7_days'
|
||||
|
||||
const BlogLinks = () => {
|
||||
const activeDays = days.filter((day) => Date.parse(day.publishedAt) <= Date.now())
|
||||
|
||||
File renamed without changes.
File renamed without changes.
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { Announcement as AnnouncementType } from '../types'
|
||||
import { Announcement as AnnouncementType } from '../../types'
|
||||
|
||||
export const Announcement = (props: AnnouncementType) => {
|
||||
const containerClasses = []
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { NewspaperIcon } from '@heroicons/react/outline'
|
||||
import { Article } from '../types'
|
||||
import { Article } from '../../types'
|
||||
|
||||
const ArticleButton = (props: Article) => {
|
||||
return (
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { NewspaperIcon } from '@heroicons/react/outline'
|
||||
import Link from 'next/link'
|
||||
import { Article } from '../types'
|
||||
import { Article } from '../../types'
|
||||
|
||||
const ArticleButtonListItem = (props: Article) => {
|
||||
return (
|
||||
File renamed without changes.
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { SparklesIcon } from '@heroicons/react/outline'
|
||||
import { Article } from '../types'
|
||||
import { Article } from '../../types'
|
||||
|
||||
const ProductButton = (props: Article) => {
|
||||
return (
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { SparklesIcon } from '@heroicons/react/outline'
|
||||
import Link from 'next/link'
|
||||
import { Article } from '../types'
|
||||
import { Article } from '../../types'
|
||||
|
||||
export const ProductButtonListItem = (props: Article) => {
|
||||
return (
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react'
|
||||
import TicketHeader from '../Ticket/TicketHeader'
|
||||
import TicketNumber from '../Ticket/TicketNumber'
|
||||
import { UserData } from '../Ticket/hooks/use-conf-data'
|
||||
import { UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
|
||||
import Image from 'next/image'
|
||||
|
||||
interface Props {
|
||||
+1
-1
@@ -2,7 +2,7 @@ import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import { Button } from 'ui'
|
||||
import { useBreakpoint } from 'common/hooks/useBreakpoint'
|
||||
import { UserData } from '../Ticket/hooks/use-conf-data'
|
||||
import { UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
|
||||
import { TicketBrickWallSlider } from './TicketBrickWallSlider'
|
||||
|
||||
type user = UserData
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { UserData } from '../Ticket/hooks/use-conf-data'
|
||||
import { UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
|
||||
import { useWindowSize } from 'react-use'
|
||||
|
||||
import { Swiper, SwiperSlide } from 'swiper/react'
|
||||
@@ -34,7 +34,7 @@ export function TicketBrickWallSlider({ users, reverse, speed = 10000 }: Props)
|
||||
|
||||
return (
|
||||
<div ref={ref} className="relative h-auto w-full m-0 overflow-hidden mb-2.5">
|
||||
<div className="flex swiper-transition-linear">
|
||||
<div className="flex swiper-transition-linear ticket-brick-swiper">
|
||||
<Swiper
|
||||
onSwiper={(swiper) => setSwiperInstance(swiper)}
|
||||
centeredSlides={true}
|
||||
@@ -65,7 +65,7 @@ export function TicketBrickWallSlider({ users, reverse, speed = 10000 }: Props)
|
||||
>
|
||||
{users.map((user, i) => (
|
||||
<SwiperSlide key={user.username}>
|
||||
<Link href={`/launch-week/tickets/${user.username}`} key={user.username}>
|
||||
<Link href={`/launch-week/7/tickets/${user.username}`} key={user.username}>
|
||||
<a className="relative !w-[230px] md:w-[450px] !h-[200px] rounded-md md:rounded-lg transition-transform">
|
||||
<div className="relative w-full pt-[50%] transform rounded-md md:rounded-lg overflow-hidden bg-gradient-to-b from-[#ffffff60] to-[#ffffff10]">
|
||||
<div className="absolute inset-[1px] w-full h-full rounded-md md:rounded-lg overflow-hidden p-[1px]">
|
||||
File renamed without changes.
+1
-1
@@ -3,7 +3,7 @@ import { useState } from 'react'
|
||||
import { PlayIcon, TruckIcon, XIcon } from '@heroicons/react/outline'
|
||||
import { Badge, Modal } from 'ui'
|
||||
|
||||
import { Article, Product, WeekDayProps } from '../types'
|
||||
import { Article, Product, WeekDayProps } from '../../types'
|
||||
import _days from './../days.json'
|
||||
import ArticleButtonListItem from './ArticleButtonListItem'
|
||||
import ProductButtonListItem from './ProductButtonListItem'
|
||||
File renamed without changes.
File renamed without changes.
+1
-1
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import LabelBadge from '../LabelBadge'
|
||||
import LabelBadge from './LabelBadge'
|
||||
import LaunchWeekPrizeCard from './LaunchWeekPrizeCard'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import useConfData from '~/components/LaunchWeek/Ticket//hooks/use-conf-data'
|
||||
import useWinningChances from './Ticket/hooks/useWinningChances'
|
||||
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
|
||||
import useWinningChances from '../hooks/useWinningChances'
|
||||
|
||||
export default function ReferralIndicator() {
|
||||
const winningChances = useWinningChances()
|
||||
+2
-2
@@ -11,8 +11,8 @@ import {
|
||||
StyledArticleBadge,
|
||||
} from './components'
|
||||
|
||||
import { WeekDayProps } from '~/components/LaunchWeek/lw7_days'
|
||||
import { opacityVariant4, scaleOpacityVariant2 } from './LW7Releases'
|
||||
import { WeekDayProps } from '~/components/LaunchWeek/7/lw7_days'
|
||||
import { opacityVariant4, scaleOpacityVariant2 } from '.'
|
||||
|
||||
import styles from './day5.module.css'
|
||||
import Link from 'next/link'
|
||||
File renamed without changes.
File renamed without changes.
+1
-1
@@ -3,7 +3,7 @@ import { useEffect } from 'react'
|
||||
import { Accordion } from 'ui'
|
||||
import Lottie from 'lottie-react'
|
||||
|
||||
import days, { WeekDayProps, endOfLW7 } from '~/components/LaunchWeek/lw7_days'
|
||||
import days, { WeekDayProps, endOfLW7 } from '~/components/LaunchWeek/7/lw7_days'
|
||||
import SectionContainer from '~/components/Layouts/SectionContainer'
|
||||
import {
|
||||
AccordionHeader,
|
||||
File renamed without changes.
File renamed without changes.
+3
-3
@@ -1,14 +1,14 @@
|
||||
import cn from 'classnames'
|
||||
import { useRef, useState } from 'react'
|
||||
import styles from './ticket.module.css'
|
||||
import styleUtils from './utils.module.css'
|
||||
import styleUtils from '../../utils.module.css'
|
||||
import TicketVisual from './TicketVisual'
|
||||
import TicketActions from './TicketActions'
|
||||
import TicketCopy from './ticket-copy'
|
||||
|
||||
import { UserData } from './hooks/use-conf-data'
|
||||
import { UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
|
||||
import ReferralIndicator from '../ReferralIndicator'
|
||||
import useWinningChances from './hooks/useWinningChances'
|
||||
import useWinningChances from '../../hooks/useWinningChances'
|
||||
import { SITE_URL } from '~/lib/constants'
|
||||
import { useBreakpoint } from 'common/hooks/useBreakpoint'
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
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/Ticket//hooks/use-conf-data'
|
||||
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
|
||||
import { useParams } from '~/hooks/useParams'
|
||||
import TicketForm from './TicketForm'
|
||||
|
||||
@@ -62,7 +62,7 @@ export default function TicketActions({
|
||||
<div className="text-scale-900">
|
||||
<IconCheckCircle size={10} strokeWidth={1} />
|
||||
</div>
|
||||
Connect with Github
|
||||
Connect with GitHub
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { PageState, ConfDataContext, UserData } from './hooks/use-conf-data'
|
||||
import { PageState, ConfDataContext, UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
|
||||
import Ticket from './ActualTicket'
|
||||
import Form from './form'
|
||||
import { SupabaseClient, Session } from '@supabase/supabase-js'
|
||||
+5
-5
@@ -2,7 +2,7 @@ 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/Ticket//hooks/use-conf-data'
|
||||
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'
|
||||
@@ -40,7 +40,7 @@ export default function TicketForm({ defaultUsername = '', setTicketGenerationSt
|
||||
.eq('email', email)
|
||||
.select()
|
||||
.single()
|
||||
.then(async ({ error }) => {
|
||||
.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
|
||||
@@ -79,7 +79,7 @@ export default function TicketForm({ defaultUsername = '', setTicketGenerationSt
|
||||
table: 'lw7_tickets',
|
||||
filter: `username=eq.${username}`,
|
||||
},
|
||||
(payload) => {
|
||||
(payload: any) => {
|
||||
const golden = !!payload.new.sharedOnTwitter && !!payload.new.sharedOnLinkedIn
|
||||
setUserData({
|
||||
...payload.new,
|
||||
@@ -157,12 +157,12 @@ export default function TicketForm({ defaultUsername = '', setTicketGenerationSt
|
||||
{session ? (
|
||||
<>
|
||||
<IconCheckCircle />
|
||||
Connect with Github
|
||||
Connect with GitHub
|
||||
</>
|
||||
) : (
|
||||
<span className="flex items-center gap-2">
|
||||
{formState === 'loading' && <IconLoader size={14} className="animate-spin" />}
|
||||
Connect with Github
|
||||
Connect with GitHub
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { DATE } from '~/lib/constants'
|
||||
import { LW7_DATE } from '~/lib/constants'
|
||||
|
||||
interface Props {
|
||||
size?: 'default' | 'small'
|
||||
@@ -12,7 +12,7 @@ export default function TicketHeader({ size }: Props) {
|
||||
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">{DATE}</span>
|
||||
<span className="text-white text-xs md:text-sm">{LW7_DATE}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
File renamed without changes.
File renamed without changes.
+1
-1
@@ -2,7 +2,7 @@ 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/Ticket/hooks/use-conf-data'
|
||||
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
|
||||
import TicketHeader from './TicketHeader'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+3
-3
@@ -1,10 +1,10 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import cn from 'classnames'
|
||||
import useConfData from '~/components/LaunchWeek/Ticket/hooks/use-conf-data'
|
||||
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
|
||||
import { useRouter } from 'next/router'
|
||||
import styleUtils from './utils.module.css'
|
||||
import styleUtils from '../../utils.module.css'
|
||||
import styles from './form.module.css'
|
||||
import useEmailQueryParam from '~/components/LaunchWeek/Ticket/hooks/use-email-query-param'
|
||||
import useEmailQueryParam from '~/components/LaunchWeek/hooks/use-email-query-param'
|
||||
import { IconLoader } from '~/../../packages/ui'
|
||||
|
||||
type FormState = 'default' | 'loading' | 'error'
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { SITE_URL } from '~/lib/constants'
|
||||
import styleUtils from './utils.module.css'
|
||||
import styleUtils from '../../utils.module.css'
|
||||
import { IconCopy, IconCheck } from 'ui'
|
||||
|
||||
type Props = {
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-2
@@ -1,6 +1,6 @@
|
||||
import styles from './ticket-info.module.css'
|
||||
import cn from 'classnames'
|
||||
import { DATE, SITE_URL } from '~/lib/constants'
|
||||
import { LW7_DATE, SITE_URL } from '~/lib/constants'
|
||||
|
||||
const siteUrl = new URL(SITE_URL)
|
||||
const siteUrlForTicket = `${siteUrl.host}${siteUrl.pathname}`.replace(/\/$/, '')
|
||||
@@ -16,7 +16,7 @@ export default function TicketInfoFooter({
|
||||
golden && '!text-white'
|
||||
}`}
|
||||
>
|
||||
<div>{DATE}</div>
|
||||
<div>{LW7_DATE}</div>
|
||||
</div>
|
||||
<div
|
||||
className={`${cn(styles.date, { [styles['date-golden']]: golden })} text-sm ${
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-2
@@ -2,7 +2,7 @@ import { motion } from 'framer-motion'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useBreakpoint } from 'common/hooks/useBreakpoint'
|
||||
import { UserData } from './hooks/use-conf-data'
|
||||
import { UserData } from '../hooks/use-conf-data'
|
||||
|
||||
interface Props {
|
||||
loadedUsers: UserData[]
|
||||
@@ -36,7 +36,7 @@ export default function TicketsGrid({ loadedUsers, isLoading, pageCount, offset
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/launch-week/tickets/${user.username}`}
|
||||
href={`/launch-week/7/tickets/${user.username}`}
|
||||
key={`${user.username}-000${i}`}
|
||||
onClick={() => window.scrollTo(0, 0)}
|
||||
>
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,49 @@
|
||||
import React, { useRef } from 'react'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
|
||||
interface Props {
|
||||
animate?: boolean
|
||||
children: any
|
||||
config: any
|
||||
user?: any
|
||||
}
|
||||
|
||||
const Particle = ({ animate = true, children, config }: Props) => {
|
||||
const particle = useRef<any>(null)
|
||||
|
||||
const pathOffsetShape =
|
||||
Math.pow(
|
||||
Math.random() * config.xRandomnessShape,
|
||||
config.xRandomness - 1 + 1 - config.xRandomness / 2
|
||||
) * config.xThickness
|
||||
|
||||
const pathOffset = pathOffsetShape * (pathOffsetShape < 1 || Math.random() < 0.5 ? 1 : -1)
|
||||
const verticalRandomness = Math.random() * (config.yThickness - 1) + 1 - config.yThickness / 2
|
||||
|
||||
const speed = Math.random() * (config.min_speed - config.max_speed) + config.max_speed
|
||||
|
||||
const circumference = (config.widthRadius * Math.PI * 2) / 100
|
||||
const delayOffsetFactor = 100
|
||||
const delayOffset = Math.random() * delayOffsetFactor
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (animate) {
|
||||
const timer = clock.getElapsedTime() * speed + delayOffset
|
||||
// When the loop count is even, draw bottom 8 shape
|
||||
// if odd, draw top 8 shape
|
||||
const isEven = Math.floor(timer / circumference) % 2 == 0
|
||||
particle.current.position.x = isEven
|
||||
? Math.sin(timer) * config.widthRadius * config.widthRatio + pathOffset
|
||||
: Math.sin(timer) * config.widthRadius + pathOffset
|
||||
particle.current.position.y = isEven
|
||||
? Math.cos(timer) * config.bottomHeightRadius -
|
||||
config.bottomHeightRadius +
|
||||
verticalRandomness
|
||||
: -Math.cos(timer) * config.topHeightRadius + config.topHeightRadius + verticalRandomness
|
||||
}
|
||||
})
|
||||
|
||||
return <mesh ref={particle}>{children}</mesh>
|
||||
}
|
||||
|
||||
export default Particle
|
||||
@@ -0,0 +1,94 @@
|
||||
import React, { useRef, useEffect, useState } from 'react'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import { Html } from '@react-three/drei'
|
||||
|
||||
interface Props {
|
||||
animate?: boolean
|
||||
children: any
|
||||
config: any
|
||||
user: any
|
||||
}
|
||||
|
||||
const Particle = ({ animate = true, user, children, config }: Props) => {
|
||||
const particle = useRef<any>(null)
|
||||
const isDebugMode = !user?.username
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (isHovered) {
|
||||
setTimeout(() => {
|
||||
setIsHovered(false)
|
||||
}, 1000)
|
||||
}
|
||||
}, [isHovered])
|
||||
|
||||
const timestamp = new Date(user.createdAt).getTime()
|
||||
const unixTimestamp = Math.floor(timestamp / 1000)
|
||||
const lastDigit = parseInt(timestamp.toString().split('')?.pop() as string)
|
||||
|
||||
const pathOffsetShape = isDebugMode
|
||||
? Math.pow(
|
||||
Math.random() * config.xRandomnessShape,
|
||||
config.xRandomness - 1 + 1 - config.xRandomness / 2
|
||||
) * config.xThickness
|
||||
: Math.pow(
|
||||
Math.random() * config.xRandomnessShape,
|
||||
config.xRandomness - 1 + 1 - config.xRandomness / 2
|
||||
) * config.xThickness
|
||||
|
||||
const pathOffset = pathOffsetShape * (pathOffsetShape < 1 || Math.random() < 0.5 ? 1 : -1)
|
||||
const verticalRandomness = isDebugMode
|
||||
? Math.random() * (config.yThickness - 1) + 1 - config.yThickness / 2
|
||||
: Math.random() * (config.yThickness - 1) + 1 - config.yThickness / 2
|
||||
|
||||
const smol = (unixTimestamp / 10000000000) * -1
|
||||
const sub1 = 1 / smol
|
||||
|
||||
console.log(smol, sub1)
|
||||
const speed = isDebugMode
|
||||
? Math.random() * (config.min_speed - config.max_speed) + config.max_speed
|
||||
: sub1 * (config.min_speed - config.max_speed) + config.max_speed
|
||||
// Math.random() * (config.min_speed - config.max_speed) + config.max_speed
|
||||
|
||||
const circumference = (config.widthRadius * Math.PI * 2) / 100
|
||||
const delayOffsetFactor = 100
|
||||
const delayOffset = isDebugMode ? Math.random() : unixTimestamp * delayOffsetFactor
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (animate) {
|
||||
const timer = clock.getElapsedTime() * speed + delayOffset
|
||||
// When the loop count is even, draw bottom 8 shape
|
||||
// if odd, draw top 8 shape
|
||||
const isEven = Math.floor(timer / circumference) % 2 == 0
|
||||
particle.current.position.x = isEven
|
||||
? Math.sin(timer) * config.widthRadius * config.widthRatio + pathOffset
|
||||
: Math.sin(timer) * config.widthRadius + pathOffset
|
||||
particle.current.position.y = isEven
|
||||
? Math.cos(timer) * config.bottomHeightRadius -
|
||||
config.bottomHeightRadius +
|
||||
verticalRandomness
|
||||
: -Math.cos(timer) * config.topHeightRadius + config.topHeightRadius + verticalRandomness
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<mesh ref={particle} onPointerEnter={() => !isDebugMode && setIsHovered(true)}>
|
||||
{children}
|
||||
|
||||
{!isDebugMode && isHovered && user.username && (
|
||||
<Html
|
||||
as="div"
|
||||
position={[1, 0, 0]}
|
||||
wrapperClass={[
|
||||
`invisible hidden text-[11px] text-[#aaaec2] bg-black items-center justify-center rounded-full py-1 px-2 [&>div]:!relative`,
|
||||
isHovered && '!visible !flex',
|
||||
].join(' ')}
|
||||
>
|
||||
@{user.username}
|
||||
</Html>
|
||||
)}
|
||||
</mesh>
|
||||
)
|
||||
}
|
||||
|
||||
export default Particle
|
||||
@@ -0,0 +1,140 @@
|
||||
import React, { useMemo, useEffect, useState } from 'react'
|
||||
import { Canvas } from '@react-three/fiber'
|
||||
import { AdditiveBlending } from 'three'
|
||||
import Particle from './Particle'
|
||||
import useParticlesConfig from './hooks/useParticlesConfig'
|
||||
import { SupabaseClient } from '@supabase/supabase-js'
|
||||
|
||||
const ParticlesCanvas = ({ supabase, users }: { supabase?: SupabaseClient; users: any }) => {
|
||||
const isWindowUndefined = typeof window === 'undefined'
|
||||
if (isWindowUndefined) return null
|
||||
|
||||
const canvasRef = React.useRef(null)
|
||||
|
||||
const [animate, setAnimate] = useState<boolean>(true)
|
||||
const { config, handleSetConfig, particles, setParticles, isDebugMode } =
|
||||
useParticlesConfig(users)
|
||||
const [realtimeChannel, setRealtimeChannel] = useState<ReturnType<
|
||||
(typeof supabase | any)['channel']
|
||||
> | null>(null)
|
||||
|
||||
const loadUsers = async () => {
|
||||
return await supabase!.from('lw8_tickets_golden').select('id, golden', { count: 'exact' })
|
||||
}
|
||||
|
||||
// Update particles live when new tickets are generated
|
||||
useEffect(() => {
|
||||
if (!!supabase) {
|
||||
const channel = supabase
|
||||
.channel('lw8_tickets_changes')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
table: 'lw8_tickets_golden',
|
||||
},
|
||||
async () => {
|
||||
const { data: users } = await loadUsers()
|
||||
|
||||
handleSetConfig('particles', users?.length)
|
||||
setParticles((prev: any) => users! ?? prev)
|
||||
}
|
||||
)
|
||||
.subscribe()
|
||||
setRealtimeChannel(channel)
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Cleanup realtime subscription on unmount
|
||||
realtimeChannel?.unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// stop animation if canvas if is not in viewport
|
||||
// to avoid unnecessary computations
|
||||
const handleScroll = () => {
|
||||
if (canvasRef.current && typeof window !== 'undefined') {
|
||||
const rect = (canvasRef.current as HTMLDivElement)?.getBoundingClientRect()
|
||||
const isInView = rect.bottom > 0
|
||||
|
||||
setAnimate(isInView)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}, [])
|
||||
|
||||
/* ThreeJs setup */
|
||||
|
||||
const Geometry = useMemo(
|
||||
() => () => <circleGeometry args={[config.particlesSize, config.particlesSides]} />,
|
||||
[]
|
||||
)
|
||||
const GoldGeometry = useMemo(
|
||||
() => () => <circleGeometry args={[config.goldParticlesSize, config.particlesSides]} />,
|
||||
[]
|
||||
)
|
||||
const Material = () =>
|
||||
useMemo(
|
||||
() => (
|
||||
<meshStandardMaterial
|
||||
color={config.color}
|
||||
blending={config.particlesBlending ? AdditiveBlending : undefined}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
)
|
||||
const GoldMaterial = () =>
|
||||
useMemo(
|
||||
() => (
|
||||
<meshPhysicalMaterial
|
||||
color={config.colorGold}
|
||||
metalness={35}
|
||||
blending={config.particlesBlending ? AdditiveBlending : undefined}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<Canvas
|
||||
ref={canvasRef}
|
||||
dpr={[1, 2]}
|
||||
camera={{ fov: 75, position: [0, 0, 500] }}
|
||||
className="relative z-30"
|
||||
>
|
||||
<ambientLight intensity={config.lightIntensity} />
|
||||
<group position={[0, 70, 0]} scale={[0.9, 0.9, 0.9]}>
|
||||
{/* Animated 8 shape particles */}
|
||||
{particles?.map((user: any, index: number) => (
|
||||
<Particle
|
||||
key={`particle-${user.username ?? index}`}
|
||||
user={user}
|
||||
config={config}
|
||||
animate={animate}
|
||||
>
|
||||
{(!!user.id && user.golden) || (config.showGold && Math.random() <= 0.5) ? (
|
||||
<>
|
||||
<GoldGeometry />
|
||||
<GoldMaterial />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Geometry />
|
||||
<Material />
|
||||
</>
|
||||
)}
|
||||
</Particle>
|
||||
))}
|
||||
</group>
|
||||
</Canvas>
|
||||
)
|
||||
}
|
||||
|
||||
export default ParticlesCanvas
|
||||
@@ -0,0 +1,182 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { range } from 'lodash'
|
||||
|
||||
let defaultConfig = {
|
||||
particles: 20,
|
||||
particlesSize: 1.2,
|
||||
goldParticlesSize: 1.5,
|
||||
particlesSides: 5,
|
||||
particlesBlending: true,
|
||||
lightIntensity: 0.3,
|
||||
widthRadius: 100,
|
||||
widthRatio: 1.2,
|
||||
topHeightRadius: 80,
|
||||
bottomHeightRadius: 100,
|
||||
trails: 0,
|
||||
trailWidth: 40,
|
||||
trailLength: 100,
|
||||
trailDecay: 450,
|
||||
color: 'white',
|
||||
colorGold: '#b89d18',
|
||||
xThickness: 7,
|
||||
xRandomnessFactor: 2.2,
|
||||
xRandomnessShape: 2.2,
|
||||
xRandomness: 5,
|
||||
yThickness: 20,
|
||||
max_speed: 0.1,
|
||||
min_speed: -0.1,
|
||||
showGold: true,
|
||||
}
|
||||
|
||||
const useParticlesConfig = (users: any): any => {
|
||||
const isWindowUndefined = typeof window === 'undefined'
|
||||
if (isWindowUndefined) return null
|
||||
const hash = window.location.hash
|
||||
const isDebugMode = hash.includes('#debug')
|
||||
const [particles, setParticles] = useState<any[]>(users)
|
||||
|
||||
const [config, setConfig] = useState(defaultConfig)
|
||||
|
||||
const handleSetConfig = (name: string, value: any) => {
|
||||
setConfig((prevConfig) => ({ ...prevConfig, [name]: value }))
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
if (!isDebugMode) return
|
||||
const dat = await import('dat.gui')
|
||||
const gui = new dat.GUI()
|
||||
const particlesFolder = gui.addFolder('Particles')
|
||||
const shapeFolder = gui.addFolder('Shape')
|
||||
const speedFolder = gui.addFolder('Speed')
|
||||
// const trailFolder = gui.addFolder('Trail')
|
||||
gui.width = 500
|
||||
particlesFolder
|
||||
.add(config, 'particles')
|
||||
.min(1)
|
||||
.max(5000)
|
||||
.step(1)
|
||||
.name('Count')
|
||||
.onChange((value) => {
|
||||
handleSetConfig('particles', value)
|
||||
setParticles(range(0, value))
|
||||
})
|
||||
particlesFolder
|
||||
.add(config, 'particlesSize')
|
||||
.min(1)
|
||||
.max(10)
|
||||
.step(0.05)
|
||||
.name('Size')
|
||||
.onChange((value) => handleSetConfig('particlesSize', value))
|
||||
particlesFolder
|
||||
.add(config, 'goldParticlesSize')
|
||||
.min(1)
|
||||
.max(10)
|
||||
.step(0.05)
|
||||
.name('Gold Particles Size')
|
||||
.onChange((value) => handleSetConfig('goldParticlesSize', value))
|
||||
particlesFolder
|
||||
.add(config, 'particlesSides')
|
||||
.min(3)
|
||||
.max(20)
|
||||
.step(1)
|
||||
.name('Sides')
|
||||
.onChange((value) => handleSetConfig('particlesSides', value))
|
||||
particlesFolder
|
||||
.add(config, 'lightIntensity')
|
||||
.min(0)
|
||||
.max(10)
|
||||
.step(0.05)
|
||||
.name('Light intensity')
|
||||
.onChange((value) => handleSetConfig('lightIntensity', value))
|
||||
particlesFolder
|
||||
.add(config, 'showGold')
|
||||
.name('Show Gold Particles')
|
||||
.onChange((value) => handleSetConfig('showGold', value))
|
||||
particlesFolder
|
||||
.add(config, 'particlesBlending')
|
||||
.name('Blending')
|
||||
.onChange((value) => handleSetConfig('particlesBlending', value))
|
||||
shapeFolder
|
||||
.add(config, 'widthRadius')
|
||||
.min(1)
|
||||
.max(200)
|
||||
.step(1)
|
||||
.name('Width Radius')
|
||||
.onChange((value) => handleSetConfig('widthRadius', value))
|
||||
shapeFolder
|
||||
.add(config, 'widthRatio')
|
||||
.min(0.5)
|
||||
.max(3)
|
||||
.step(0.01)
|
||||
.name('Top/Bottom Ratio')
|
||||
.onChange((value) => handleSetConfig('widthRatio', value))
|
||||
shapeFolder
|
||||
.add(config, 'topHeightRadius')
|
||||
.min(1)
|
||||
.max(200)
|
||||
.step(1)
|
||||
.name('Height Radius - Top')
|
||||
.onChange((value) => handleSetConfig('topHeightRadius', value))
|
||||
shapeFolder
|
||||
.add(config, 'bottomHeightRadius')
|
||||
.min(1)
|
||||
.max(200)
|
||||
.step(1)
|
||||
.name('Height Radius - Bottom')
|
||||
.onChange((value) => handleSetConfig('bottomHeightRadius', value))
|
||||
shapeFolder
|
||||
.add(config, 'xThickness')
|
||||
.min(1)
|
||||
.max(100)
|
||||
.step(0.1)
|
||||
.name('Stroke Width')
|
||||
.onChange((value) => handleSetConfig('xThickness', value))
|
||||
shapeFolder
|
||||
.add(config, 'xRandomnessShape')
|
||||
.min(0)
|
||||
.max(5)
|
||||
.step(0.001)
|
||||
.name('Randomness shape')
|
||||
.onChange((value) => handleSetConfig('xRandomnessShape', value))
|
||||
shapeFolder
|
||||
.add(config, 'xRandomness')
|
||||
.min(0)
|
||||
.max(50)
|
||||
.step(0.01)
|
||||
.name('Randomness')
|
||||
.onChange((value) => handleSetConfig('xRandomness', value))
|
||||
shapeFolder
|
||||
.add(config, 'yThickness')
|
||||
.min(1)
|
||||
.max(50)
|
||||
.step(0.1)
|
||||
.name('y thickness')
|
||||
.onChange((value) => handleSetConfig('yThickness', value))
|
||||
speedFolder
|
||||
.add(config, 'min_speed')
|
||||
.min(-6)
|
||||
.max(6)
|
||||
.step(0.01)
|
||||
.name('Min speed')
|
||||
.onChange((value) => handleSetConfig('min_speed', value))
|
||||
speedFolder
|
||||
.add(config, 'max_speed')
|
||||
.min(-6)
|
||||
.max(6)
|
||||
.step(0.01)
|
||||
.name('Max speed')
|
||||
.onChange((value) => handleSetConfig('max_speed', value))
|
||||
|
||||
particlesFolder.open()
|
||||
shapeFolder.open()
|
||||
speedFolder.open()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
init()
|
||||
}, [])
|
||||
|
||||
return { config, handleSetConfig, particles, setParticles, isDebugMode }
|
||||
}
|
||||
|
||||
export default useParticlesConfig
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import Countdown from 'react-countdown'
|
||||
import { CountdownWidget } from 'ui'
|
||||
import { LW8_LAUNCH_DATE } from '~/lib/constants'
|
||||
|
||||
const CountdownComponent = () => {
|
||||
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} />
|
||||
}
|
||||
}
|
||||
|
||||
return <Countdown date={new Date(LW8_LAUNCH_DATE)} renderer={renderer} />
|
||||
}
|
||||
|
||||
export default CountdownComponent
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function LabelBadge({ text, className }: Props) {
|
||||
return (
|
||||
<span className={['bg-[#222428] rounded px-1 py-[1px] text-xs', className].join(' ')}>
|
||||
<span className="text-scale-1000">{text}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import Link from 'next/link'
|
||||
import CountdownComponent from './Countdown'
|
||||
|
||||
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">
|
||||
<div className="">
|
||||
<CountdownComponent />
|
||||
</div>
|
||||
<h1 className="sr-only font-normal uppercase text-[28px] sm:text-[32px]">Launch week 8</h1>
|
||||
<p className="text-white radial-gradient-text-scale-600 text-lg sm:text-2xl">
|
||||
<span className="block">August 7th–11th, 2023</span>
|
||||
<span>9:00 AM PT</span>
|
||||
</p>
|
||||
<div className="text-[#9296AA]">
|
||||
Join us in a week of announcing new features. <br className="hidden md:block" />
|
||||
Connect with GitHub to generate your unique ticket and contribute to the constellation.
|
||||
<span className="inline sm:block">
|
||||
{' '}
|
||||
You might also win{' '}
|
||||
<Link href="#lw8-prizes">
|
||||
<a className="underline transition-opacity hover:text-scale-1200">swag</a>
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import Image from 'next/image'
|
||||
import React from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
type animateFromType = 'left' | 'right' | 'up' | 'down' | 'opacityOnly'
|
||||
|
||||
export default function LaunchWeekPrizeCard({
|
||||
imageUrl = '',
|
||||
imageWrapperClassName,
|
||||
content,
|
||||
className,
|
||||
animateFrom,
|
||||
imgObjectPosition = 'center',
|
||||
}: {
|
||||
imageUrl: string
|
||||
imageWrapperClassName?: string
|
||||
content: any
|
||||
className?: string
|
||||
animateFrom?: animateFromType
|
||||
imgObjectPosition?: string
|
||||
}) {
|
||||
const finalState = { x: 0, y: 0, opacity: 1 }
|
||||
|
||||
function getAnimationFrom(animationDirection: animateFromType) {
|
||||
switch (animationDirection) {
|
||||
case 'left':
|
||||
return { x: -20, opacity: 0 }
|
||||
case 'up':
|
||||
return { y: -20, opacity: 0 }
|
||||
case 'right':
|
||||
return { x: 20, opacity: 0 }
|
||||
case 'down':
|
||||
return { y: 20, opacity: 0 }
|
||||
case 'opacityOnly':
|
||||
return { opacity: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={[
|
||||
'relative p-[1px] bg-gradient-to-b from-[#22282a] to-[#030A0C] rounded-lg overflow-hidden shadow-lg',
|
||||
className && className,
|
||||
].join(' ')}
|
||||
initial={animateFrom ? getAnimationFrom(animateFrom) : finalState}
|
||||
whileInView={finalState}
|
||||
viewport={{ once: true, margin: '-50px' }}
|
||||
transition={{ type: 'spring', bounce: 0, delay: 0.5 }}
|
||||
>
|
||||
<div className="relative h-full flex flex-col bg-[#030A0C] rounded-lg overflow-hidden">
|
||||
<div
|
||||
className={[
|
||||
"relative w-full flex-grow before:content[' '] before:absolute before:inset-0 before:z-10 before:bg-gradient-to-t before:from-[#030A0C] before:via-transparent",
|
||||
imageWrapperClassName && imageWrapperClassName,
|
||||
].join(' ')}
|
||||
>
|
||||
<Image
|
||||
src={imageUrl}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
objectPosition={imgObjectPosition}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-4 flex flex-col gap-2 items-start">{content && content}</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import React from 'react'
|
||||
import LabelBadge from './LabelBadge'
|
||||
import LaunchWeekPrizeCard from './LaunchWeekPrizeCard'
|
||||
import Image from 'next/image'
|
||||
|
||||
export default function LaunchWeekPrizeSection({
|
||||
className,
|
||||
ticket,
|
||||
}: {
|
||||
className?: string
|
||||
ticket?: any
|
||||
}) {
|
||||
const Ticket = ticket
|
||||
|
||||
return (
|
||||
<div id="lw8-prizes" className={['relative scroll-mt-[66px]', className].join(' ')}>
|
||||
<div className="absolute z-0 w-full aspect-[1.75/1] top-0 items-center justify-center">
|
||||
<Image
|
||||
src="/images/launchweek/8/swag-bg.png"
|
||||
alt="prizes background"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
objectPosition="top"
|
||||
quality={100}
|
||||
/>
|
||||
</div>
|
||||
<div className="!max-w-[100vw]">
|
||||
<div className="w-full container mx-auto h-px bg-gradient-to-r from-transparent via-scale-600 to-transparent" />
|
||||
<div className="text-center relative z-10 text-white">
|
||||
<div className="max-w-[38rem] mx-auto flex flex-col items-center gap-4 px-4">
|
||||
<div className="relative z-10 w-full h-[100px] md:h-[130px] rounded-sm flex mt-16 items-center justify-center">
|
||||
<Image
|
||||
src="/images/launchweek/8/swag-illustration.png"
|
||||
alt="prizes"
|
||||
layout="fill"
|
||||
objectFit="contain"
|
||||
quality={100}
|
||||
/>
|
||||
</div>
|
||||
<h2 className="text-4xl">Get your winning ticket</h2>
|
||||
<p className="text-[#9296AA]">
|
||||
Join us on August 11th for Launch Week 8's final day
|
||||
<br className="hidden md:inline-block" /> and find out if you are one of the lucky
|
||||
winners. Get sharing!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{ticket && <Ticket />}
|
||||
<div className="pt-8 lg:pt-16 px-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 max-w-7xl mx-auto h-auto text-white">
|
||||
<LaunchWeekPrizeCard
|
||||
className="col-span-full"
|
||||
imageUrl="/images/launchweek/8/swag/deskmat-lg.jpg"
|
||||
imageWrapperClassName="w-full aspect-[1.9/1] md:h-[300px]"
|
||||
imgObjectPosition="center"
|
||||
content={
|
||||
<h3 className="w-full text-sm md:text-center flex md:justify-center items-center gap-4">
|
||||
Supabase Desk Mat <LabelBadge text="10 mats" />
|
||||
</h3>
|
||||
}
|
||||
animateFrom="down"
|
||||
/>
|
||||
<LaunchWeekPrizeCard
|
||||
imageUrl="/images/launchweek/8/swag/shirt.jpg"
|
||||
imageWrapperClassName="w-full aspect-[1.9/1]"
|
||||
imgObjectPosition="right"
|
||||
content={
|
||||
<h3 className="text-sm flex items-center gap-4">
|
||||
Supabase T-Shirt <LabelBadge text="20 t-shirts" />
|
||||
</h3>
|
||||
}
|
||||
animateFrom="down"
|
||||
/>
|
||||
<LaunchWeekPrizeCard
|
||||
imageUrl="/images/launchweek/8/swag/cap.jpg"
|
||||
imageWrapperClassName="w-full aspect-[1.9/1]"
|
||||
animateFrom="down"
|
||||
content={
|
||||
<h3 className="text-sm flex items-center gap-4">
|
||||
Supabase Cap <LabelBadge text="25 caps" />
|
||||
</h3>
|
||||
}
|
||||
/>
|
||||
<LaunchWeekPrizeCard
|
||||
imageUrl="/images/launchweek/8/swag/stickers.jpg"
|
||||
imageWrapperClassName="w-full aspect-[1.9/1]"
|
||||
animateFrom="down"
|
||||
content={
|
||||
<h3 className="text-sm flex items-center gap-4">
|
||||
Supabase Sticker Pack <LabelBadge text="50 packs" />
|
||||
</h3>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
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.functions.supabase.co/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)
|
||||
.is('sharedOnTwitter', null)
|
||||
} else if (social === 'linkedin') {
|
||||
const res = await supabase
|
||||
.from(LW_TABLE)
|
||||
.update({ sharedOnLinkedIn: 'now' })
|
||||
.eq('username', username)
|
||||
.is('sharedOnLinkedIn', null)
|
||||
|
||||
console.log('shareing on LinkedIn', res)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-1 grid-cols-1 sm:grid-cols-3">
|
||||
{!sharePage ? (
|
||||
<>
|
||||
<div className="rounded bg-[#E6E8EB] text-scale-500 py-1 px-3 border border-[#3e3e3e] text-xs mb-1">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div className="text-scale-300">
|
||||
<IconCheckCircle size={10} strokeWidth={1.5} />
|
||||
</div>
|
||||
Connect with GitHub
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
onKeyUp={() => handleShare('twitter')}
|
||||
href={tweetUrl}
|
||||
rel="noopener noreferrer prefetch"
|
||||
target="_blank"
|
||||
className={[
|
||||
`flex items-center justify-center gap-2 rounded text-scale-500 py-1 px-3 border border-[#3e3e3e] text-xs mb-1 transition-all ease-out hover:text-scale-100 hover:bg-[#dfe1e3]`,
|
||||
userData.sharedOnTwitter ? 'bg-[#E6E8EB] text-scale-500' : 'text-white',
|
||||
].join(' ')}
|
||||
>
|
||||
{userData.sharedOnTwitter && (
|
||||
<div className="text-scale-900">
|
||||
<IconCheckCircle size={10} strokeWidth={1.5} />
|
||||
</div>
|
||||
)}
|
||||
Share on Twitter
|
||||
</a>
|
||||
<a
|
||||
onKeyUp={() => handleShare('linkedin')}
|
||||
href={linkedInUrl}
|
||||
rel="noopener noreferrer prefetch"
|
||||
target="_blank"
|
||||
className={[
|
||||
`flex items-center justify-center gap-2 rounded text-scale-500 py-1 px-3 border border-[#3e3e3e] text-xs mb-1 transition-all ease-out hover:text-scale-100 hover:bg-[#dfe1e3]`,
|
||||
userData.sharedOnLinkedIn ? 'bg-[#E6E8EB] text-scale-500' : 'text-white',
|
||||
].join(' ')}
|
||||
>
|
||||
{userData.sharedOnLinkedIn && (
|
||||
<div className="text-scale-900">
|
||||
<IconCheckCircle size={10} strokeWidth={1.5} />
|
||||
</div>
|
||||
)}
|
||||
Share on Linkedin
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
!username && (
|
||||
<TicketForm
|
||||
defaultUsername={username ?? undefined}
|
||||
ticketGenerationState={ticketGenerationState}
|
||||
setTicketGenerationState={setTicketGenerationState}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
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()
|
||||
|
||||
console.log(supabase)
|
||||
|
||||
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-scale-1200 flex flex-col w-full items-center text-white 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-scale-1100 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">
|
||||
<a className="underline hover:text-scale-1200">limited edition awards</a>
|
||||
</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">
|
||||
<a className="underline hover:text-scale-1200">awesome swag</a>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
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-scale-1000 text-xs">
|
||||
Connected account
|
||||
</span>
|
||||
)}
|
||||
{IS_SAVED && (
|
||||
<span className="opacity-0 animate-fade-in text-xs text-brand-900">Saved</span>
|
||||
)}
|
||||
{HAS_ERROR && (
|
||||
<span className="opacity-0 animate-fade-in text-xs text-tomato-900">
|
||||
Something went wrong
|
||||
</span>
|
||||
)}
|
||||
<Badge color="brand">@{user.username}</Badge>
|
||||
</div>
|
||||
<Input
|
||||
className="[&_input]:border-scale-200"
|
||||
size="small"
|
||||
type="text"
|
||||
required
|
||||
disabled
|
||||
placeholder="name"
|
||||
value={user.name}
|
||||
icon={<IconCheck strokeWidth={2} className="w-3 text-brand-900" />}
|
||||
/>
|
||||
<Input
|
||||
className="[&_input]:border-scale-200"
|
||||
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-scale-300',
|
||||
!!formData.role ? 'text-brand-900' : 'text-scale-300',
|
||||
].join(' ')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
className="[&_input]:border-scale-200"
|
||||
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-scale-300',
|
||||
!!formData.company ? 'text-brand-900' : 'text-scale-300',
|
||||
].join(' ')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
className="[&_input]:border-scale-200"
|
||||
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-scale-300',
|
||||
!!formData.location ? 'text-brand-900' : 'text-scale-300',
|
||||
].join(' ')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default TicketCustomizationForm
|
||||
@@ -0,0 +1,7 @@
|
||||
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
|
||||
@@ -0,0 +1,13 @@
|
||||
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-scale-1100 text-xs font-mono uppercase tracking-widest">
|
||||
<span>{LW8_DATE}</span>
|
||||
<span>supabase.com/launch-week</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
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'
|
||||
|
||||
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, setPageState, userData } = useConfData()
|
||||
const [realtimeChannel, setRealtimeChannel] = useState<ReturnType<
|
||||
(typeof supabase)['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/tickets/${username}`).catch((_) => {})
|
||||
|
||||
setPageState('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-scale-900'}`}>
|
||||
{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-scale-900'}>Only public info will be used.</p>} */}
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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% dark:text-white">
|
||||
<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-scale-1100 text-center font-mono tracking-[0.8rem]
|
||||
`,
|
||||
golden ? styles['ticket-number-gold'] : styles['ticket-number'],
|
||||
].join(' ')}
|
||||
>
|
||||
{ticketNumberText}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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, ticketGenerationState, golden = false }: 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-scale-1100 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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
/* .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 var(--brand);
|
||||
background: var(--brand);
|
||||
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: var(--brand);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
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-scale-1100 hover:text-scale-1200"
|
||||
>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
.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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
.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);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import Link from 'next/link'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { Button } from 'ui'
|
||||
import { useBreakpoint } from 'common/hooks/useBreakpoint'
|
||||
import { UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
|
||||
import { TicketBrickWallSlider } from './TicketBrickWallSlider'
|
||||
|
||||
type user = UserData
|
||||
|
||||
interface Props {
|
||||
users: user[]
|
||||
}
|
||||
|
||||
export default function TicketBrickWall({ users }: Props) {
|
||||
const isMobile = useBreakpoint(768)
|
||||
|
||||
return (
|
||||
<div className="relative pb-16 pt-16 md:pt-28">
|
||||
<div className="mx-auto">
|
||||
<div className="max-w-[38rem] mb-8 mx-auto">
|
||||
<h2 className="text-center radial-gradient-text-scale-500">Tickets from the community</h2>
|
||||
</div>
|
||||
<div
|
||||
className="grid py-4 relative
|
||||
before:content-[' '] before:absolute before:inset-0 before:right-auto before:w-1/4 before:bg-gradient-to-r before:from-[#020405] before:to-[#02040500] before:z-10 before:pointer-events-none
|
||||
after:content-[' '] after:absolute after:inset-0 after:left-auto after:w-1/4 after:bg-gradient-to-l after:from-[#020405] after:to-[#02040500] after:z-10 after:pointer-events-none
|
||||
"
|
||||
>
|
||||
<TicketBrickWallSlider users={users.slice(0, isMobile ? 5 : 7)} />
|
||||
<TicketBrickWallSlider
|
||||
users={users.slice(isMobile ? 5 : 7, isMobile ? 11 : 14)}
|
||||
reverse
|
||||
/>
|
||||
{isMobile && <TicketBrickWallSlider users={users.slice(11, 17)} speed={6000} />}
|
||||
</div>
|
||||
<div className="flex justify-center w-full mx-auto mt-2 lg:mt-4">
|
||||
<Link href="/launch-week/tickets">
|
||||
<a>
|
||||
<Button
|
||||
type="outline"
|
||||
size="medium"
|
||||
onClick={() => window.scrollTo(0, 0)}
|
||||
className="text-white"
|
||||
>
|
||||
View all tickets
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
|
||||
|
||||
import { Swiper, SwiperSlide } from 'swiper/react'
|
||||
|
||||
import 'swiper/swiper.min.css'
|
||||
|
||||
interface Props {
|
||||
users: UserData[]
|
||||
reverse?: boolean
|
||||
speed?: number
|
||||
animate?: boolean
|
||||
}
|
||||
|
||||
export function TicketBrickWallSlider({ users, reverse, speed = 50000, animate }: Props) {
|
||||
const STORAGE_URL = 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/lw8'
|
||||
const BUCKET_FOLDER_VERSION = 'v1'
|
||||
const getOgUrl = (username: string, isGold: boolean) =>
|
||||
`${STORAGE_URL}/tickets/${
|
||||
isGold ? 'golden' : 'regular'
|
||||
}/${BUCKET_FOLDER_VERSION}/${username}.png`
|
||||
|
||||
return (
|
||||
<div className="relative h-auto w-full m-0 overflow-hidden mb-2.5">
|
||||
<div
|
||||
className={[
|
||||
'flex swiper-transition-linear ticket-brick-swiper',
|
||||
reverse && '-translate-x-20 w-[calc(100vw+400px)]',
|
||||
].join(' ')}
|
||||
>
|
||||
<Swiper
|
||||
centeredSlides={true}
|
||||
spaceBetween={10}
|
||||
slidesPerView={5}
|
||||
loop={true}
|
||||
watchOverflow
|
||||
threshold={2}
|
||||
updateOnWindowResize
|
||||
breakpoints={{
|
||||
320: {
|
||||
slidesPerView: reverse ? 4.5 : 2.5,
|
||||
},
|
||||
720: {
|
||||
slidesPerView: reverse ? 5.5 : 3.5,
|
||||
},
|
||||
1440: {
|
||||
slidesPerView: reverse ? 6.5 : 5,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{users.map((user, i) => (
|
||||
<SwiperSlide key={user.username}>
|
||||
<Link href={`/launch-week/tickets/${user.username}`} key={user.username}>
|
||||
<a className="relative !w-[230px] md:w-[450px] !h-[200px] rounded-md md:rounded-lg transition-transform">
|
||||
<div className="relative w-full pt-[50%] transform rounded-md md:rounded-lg overflow-hidden bg-gradient-to-b from-[#22282a] to-[#030A0C]">
|
||||
<div className="absolute inset-[1px] w-[calc(100%-2px)] h-[calc(100%-2px)] rounded-md md:rounded-lg overflow-hidden p-[1px]">
|
||||
<Image
|
||||
src={getOgUrl(user.username!, !!user.golden)}
|
||||
alt={user.username}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
objectPosition="center"
|
||||
placeholder="blur"
|
||||
blurDataURL="/images/blur.png"
|
||||
className="absolute inset-[1px] rounded-md md:rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useBreakpoint } from 'common/hooks/useBreakpoint'
|
||||
import { UserData } from '../hooks/use-conf-data'
|
||||
|
||||
interface Props {
|
||||
loadedUsers: UserData[]
|
||||
isLoading: boolean
|
||||
pageCount: number
|
||||
offset: number
|
||||
}
|
||||
|
||||
export default function TicketsGrid({ loadedUsers, isLoading, pageCount, offset }: Props) {
|
||||
const STORAGE_URL = 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/lw8'
|
||||
const BUCKET_FOLDER_VERSION = 'v1'
|
||||
const getTicketImageUrl = (username: string, isGold: boolean) =>
|
||||
`${STORAGE_URL}/tickets/${
|
||||
isGold ? 'golden' : 'regular'
|
||||
}/${BUCKET_FOLDER_VERSION}/${username}.png`
|
||||
const isMobile = useBreakpoint(1024)
|
||||
const isTablet = useBreakpoint(1280)
|
||||
const horizontalOffset = isMobile ? 8 : 20
|
||||
|
||||
return (
|
||||
<div
|
||||
className={['grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 py-12 relative'].join(' ')}
|
||||
>
|
||||
{loadedUsers?.map((user, i) => {
|
||||
const rowTickets = isMobile ? 2 : isTablet ? 3 : 4
|
||||
const divider = Math.floor(i / rowTickets)
|
||||
const isOddRow = divider % 2 === 0
|
||||
// Delay should be no more than pageCount for new loaded users
|
||||
const recalculatedDelay = i >= pageCount * 2 ? (i - pageCount * (offset - 1)) / 15 : i / 15
|
||||
const imgUrl = getTicketImageUrl(user.username!, !!user.golden)
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/launch-week/tickets/${user.username}`}
|
||||
key={`${user.username}-000${i}`}
|
||||
onClick={() => window.scrollTo(0, 0)}
|
||||
>
|
||||
<motion.a
|
||||
className="relative w-full p-[1px] rounded-lg sm:rounded-xl overflow-hidden hover:cursor-pointer bg-gradient-to-b from-[#58585860] to-[#ffffff10]"
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 20,
|
||||
x: isOddRow ? horizontalOffset : -horizontalOffset,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
x: isOddRow ? horizontalOffset : -horizontalOffset,
|
||||
transition: {
|
||||
duration: 0.4,
|
||||
ease: [0.24, 0.25, 0.05, 1],
|
||||
delay: recalculatedDelay,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="relative inset-0 w-full pt-[50%] bg-[#2f2e2e] overflow-hidden rounded-lg sm:rounded-xl">
|
||||
<Image
|
||||
src={imgUrl}
|
||||
alt={user.username}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
objectPosition="center"
|
||||
placeholder="blur"
|
||||
blurDataURL="/images/blur.png"
|
||||
/>
|
||||
</div>
|
||||
</motion.a>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
{isLoading &&
|
||||
Array.from({ length: pageCount }, (_, i) => (
|
||||
<motion.div
|
||||
className="relative rounded-lg sm:rounded-xl overflow-hidden h-0 w-full pt-[50%]"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.4,
|
||||
ease: [0.24, 0.25, 0.05, 1],
|
||||
delay: i / 15,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 rounded-lg sm:rounded-xl bg-gradient-to-b from-[#58585820] to-[#ffffff10] p-[1px] overflow-hidden">
|
||||
<div className="absolute inset-[1px] rounded-lg sm:rounded-xl bg-gradient-to-b from-[#1c1c1c] to-[#121212]" />
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.swiper-wrapper .swiper-slide {
|
||||
width: 300px !important;
|
||||
}
|
||||
+7
-1
@@ -5,18 +5,24 @@ export type PageState = 'registration' | 'ticket'
|
||||
|
||||
export type UserData = {
|
||||
id?: string
|
||||
email?: string
|
||||
ticketNumber?: number
|
||||
username?: string
|
||||
name?: string
|
||||
golden?: boolean
|
||||
referrals?: number
|
||||
bg_image_id?: number
|
||||
metadata?: {
|
||||
role?: string
|
||||
company?: string
|
||||
location?: string
|
||||
}
|
||||
sharedOnTwitter?: string
|
||||
sharedOnLinkedIn?: string
|
||||
}
|
||||
|
||||
type ConfDataContextType = {
|
||||
supabase: SupabaseClient
|
||||
supabase: SupabaseClient | any
|
||||
session: Session | null
|
||||
userData: UserData
|
||||
setUserData: React.Dispatch<React.SetStateAction<UserData>>
|
||||
File renamed without changes.
Loaded 100 of 151 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user