Merge branch 'master' into feat/column-order-refactored

This commit is contained in:
Jonathan Summers-Muir authored and GitHub committed 2025-05-05 21:53:30 +08:00
commit 7cc20535d5
111 files changed
+547 -4101

No files matched your search

@@ -1,15 +1,17 @@
import { Command, Search, Menu } from 'lucide-react'
import { memo, useState } from 'react'
import dynamic from 'next/dynamic'
import Image from 'next/image'
import Link from 'next/link'
import type { FC } from 'react'
import { memo, useState } from 'react'
import { Command, Search, Menu } from 'lucide-react'
import { useIsLoggedIn, useIsUserLoading } from 'common'
import { useIsLoggedIn, useIsUserLoading, useUser } from 'common'
import { Button, buttonVariants, cn } from 'ui'
import { CommandMenuTrigger } from 'ui-patterns/CommandMenu'
import { AuthenticatedDropdownMenu, CommandMenuTrigger } from 'ui-patterns'
import GlobalNavigationMenu from './GlobalNavigationMenu'
import useDropdownMenu from './useDropdownMenu'
import type { FC } from 'react'
const GlobalMobileMenu = dynamic(() => import('./GlobalMobileMenu'))
const TopNavDropdown = dynamic(() => import('./TopNavDropdown'))
@@ -17,6 +19,8 @@ const TopNavBar: FC = () => {
const isLoggedIn = useIsLoggedIn()
const isUserLoading = useIsUserLoading()
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const user = useUser()
const menu = useDropdownMenu(user)
return (
<>
@@ -95,7 +99,11 @@ const TopNavBar: FC = () => {
<Link href="/dev-secret-auth">Dev-only secret sign-in</Link>
</Button>
)}
<TopNavDropdown />
{isLoggedIn ? (
<AuthenticatedDropdownMenu menu={menu} user={user} site="docs" />
) : (
<TopNavDropdown />
)}
</div>
</div>
</nav>
@@ -19,7 +19,6 @@ import {
cn,
themes,
} from 'ui'
import MenuIconPicker from './MenuIconPicker'
const menu = [
@@ -70,7 +69,7 @@ const TopNavDropdown = () => {
<Menu size={18} strokeWidth={1} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-64">
<DropdownMenuContent side="bottom" align="end" className="w-52">
{menu.map((menuSection, sectionIdx) => (
<Fragment key={`topnav--${sectionIdx}`}>
{sectionIdx !== 0 && <DropdownMenuSeparator key={`topnav--${sectionIdx}`} />}
@@ -0,0 +1,80 @@
'use client'
import type { User } from '@supabase/supabase-js'
import { LogOut, Globe, LifeBuoy, Settings, UserIcon, Database } from 'lucide-react'
import { logOut } from 'common'
import type { menuItem } from 'ui-patterns/AuthenticatedDropdownMenu'
import { IconGitHub } from './MenuIcons'
const useDropdownMenu = (user: User | null) => {
const menu: menuItem[][] = [
[
{
label: user?.email ?? "You're logged in",
type: 'text',
icon: UserIcon,
},
{
label: 'Account Preferences',
icon: Settings,
href: 'https://supabase.com/dashboard/account/me',
},
{
label: 'All Projects',
icon: Database,
href: 'https://supabase.com/dashboard/projects',
},
],
[
{
label: 'Supabase.com',
icon: Globe,
href: 'https://supabase.com',
otherProps: {
target: '_blank',
rel: 'noreferrer noopener',
},
},
{
label: 'GitHub',
icon: IconGitHub as any,
href: 'https://github.com/supabase/supabase',
otherProps: {
target: '_blank',
rel: 'noreferrer noopener',
},
},
{
label: 'Support',
icon: LifeBuoy,
href: 'https://supabase.com/support',
otherProps: {
target: '_blank',
rel: 'noreferrer noopener',
},
},
],
[
{
label: 'Theme',
type: 'theme',
},
],
[
{
label: 'Logout',
type: 'button',
icon: LogOut,
onClick: async () => {
await logOut()
window.location.reload()
},
},
],
]
return menu
}
export default useDropdownMenu
@@ -15,7 +15,7 @@ import CopyToClipboard from 'react-copy-to-clipboard'
import { withErrorBoundary } from 'react-error-boundary'
import { proxy, useSnapshot } from 'valtio'
import { useIsLoggedIn, useIsUserLoading } from 'common'
import { LOCAL_STORAGE_KEYS, useIsLoggedIn, useIsUserLoading } from 'common'
import { Button_Shadcn_ as Button, Input_Shadcn_ as Input, cn } from 'ui'
import {
@@ -36,7 +36,7 @@ import { useOrganizationsQuery } from '~/lib/fetch/organizations'
import { type SupavisorConfigData, useSupavisorConfigQuery } from '~/lib/fetch/pooler'
import { useProjectApiQuery } from '~/lib/fetch/projectApi'
import { isProjectPaused, type ProjectsData, useProjectsQuery } from '~/lib/fetch/projects'
import { LOCAL_STORAGE_KEYS, retrieve, storeOrRemoveNull } from '~/lib/storage'
import { retrieve, storeOrRemoveNull } from '~/lib/storage'
import { useOnLogout } from '~/lib/userAuth'
type ProjectOrgDataState =
+2 -2
View File
@@ -1,9 +1,9 @@
'use client'
import { useQueryClient } from '@tanstack/react-query'
import { AuthProvider } from 'common'
import { AuthProvider, LOCAL_STORAGE_KEYS } from 'common'
import { type PropsWithChildren, useCallback } from 'react'
import { LOCAL_STORAGE_KEYS, remove } from '~/lib/storage'
import { remove } from '~/lib/storage'
import { useOnLogout } from '~/lib/userAuth'
/**
+4 -9
View File
@@ -1,11 +1,6 @@
export const LOCAL_STORAGE_KEYS = {
SAVED_ORG: 'docs.ui.user.selected.org',
SAVED_PROJECT: 'docs.ui.user.selected.project',
SAVED_BRANCH: 'docs.ui.user.selected.branch',
} as const
import { LOCAL_STORAGE_KEYS } from 'common'
type LocalStorageKey = (typeof LOCAL_STORAGE_KEYS)[keyof typeof LOCAL_STORAGE_KEYS]
type StorageType = 'local' | 'session'
function getStorage(storageType: StorageType) {
@@ -17,7 +12,7 @@ export function store(storageType: StorageType, key: LocalStorageKey, value: str
const storage = getStorage(storageType)
try {
storage.setItem(key, value)
storage.setItem(key as string, value)
} catch {
console.error(`Failed to set storage item with key "${key}"`)
}
@@ -26,13 +21,13 @@ export function store(storageType: StorageType, key: LocalStorageKey, value: str
export function retrieve(storageType: StorageType, key: LocalStorageKey) {
if (typeof window === 'undefined') return
const storage = getStorage(storageType)
return storage.getItem(key)
return storage.getItem(key as string)
}
export function remove(storageType: StorageType, key: LocalStorageKey) {
if (typeof window === 'undefined') return
const storage = getStorage(storageType)
return storage.removeItem(key)
return storage.removeItem(key as string)
}
export function storeOrRemoveNull(
@@ -2,12 +2,11 @@ import { ArrowLeft, ArrowRight, HelpCircle } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useParams } from 'common'
import { formatFilterURLParams } from 'components/grid/SupabaseGrid.utils'
import { useTableFilter } from 'components/grid/hooks/useTableFilter'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useTableEditorQuery } from 'data/table-editor/table-editor-query'
import { isTableLike } from 'data/table-editor/table-editor-types'
import { THRESHOLD_COUNT, useTableRowsCountQuery } from 'data/table-rows/table-rows-count-query'
import { useUrlState } from 'hooks/ui/useUrlState'
import { RoleImpersonationState } from 'lib/role-impersonation'
import { useRoleImpersonationStateSnapshot } from 'state/role-impersonation-state'
import { useTableEditorStateSnapshot } from 'state/table-editor'
@@ -41,8 +40,7 @@ const Pagination = () => {
// rowsCountEstimate is only applicable to table entities
const rowsCountEstimate = isTableLike(selectedTable) ? selectedTable.live_rows_estimate : null
const [{ filter }] = useUrlState({ arrayKeys: ['filter'] })
const filters = formatFilterURLParams(filter as string[])
const { filters } = useTableFilter()
const page = snap.page
const roleImpersonationState = useRoleImpersonationStateSnapshot()
@@ -61,7 +59,7 @@ const Pagination = () => {
{
projectRef: project?.ref,
connectionString: project?.connectionString,
tableId: id,
tableId: snap.table.id,
filters,
enforceExactCount: snap.enforceExactCount,
roleImpersonationState: roleImpersonationState as RoleImpersonationState,
@@ -1,6 +1,5 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { SupportCategories } from '@supabase/shared-types/out/constants'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
@@ -27,6 +26,7 @@ import {
Input_Shadcn_,
Separator,
} from 'ui'
import { LOCAL_STORAGE_KEYS } from 'common'
const setDeletionRequestFlag = () => {
const expiryDate = new Date()
@@ -5,7 +5,7 @@ import SVG from 'react-inlinesvg'
import { DEFAULT_SIDEBAR_BEHAVIOR } from 'components/interfaces/Sidebar'
import Panel from 'components/ui/Panel'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { BASE_PATH, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { BASE_PATH } from 'lib/constants'
import {
Label_Shadcn_,
RadioGroup_Shadcn_,
@@ -20,6 +20,7 @@ import {
Theme,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { LOCAL_STORAGE_KEYS } from 'common'
export const ThemeSettings = () => {
const [mounted, setMounted] = useState(false)
@@ -1,8 +1,7 @@
import { LOCAL_STORAGE_KEYS } from 'common'
import { noop } from 'lodash'
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
const FLY_POSTGRES_DEPRECATION_WARNING_KEY = LOCAL_STORAGE_KEYS.FLY_POSTGRES_DEPRECATION_WARNING
// [Joshen] This file is meant to be dynamic - update this as and when we need to use the NoticeBanner
@@ -1,8 +1,7 @@
import { noop } from 'lodash'
import { FeatureFlagContext } from 'common'
import { useFlag } from 'hooks/ui/useFlag'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { IS_PLATFORM } from 'lib/constants'
import { EMPTY_OBJ } from 'lib/void'
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react'
import { APISidePanelPreview } from './APISidePanelPreview'
@@ -11,6 +10,7 @@ import { InlineEditorPreview } from './InlineEditorPreview'
import { LayoutUpdatePreview } from './LayoutUpdatePreview'
import { SqlEditorTabsPreview } from './SqlEditorTabs'
import { TableEditorTabsPreview } from './TableEditorTabs'
import { LOCAL_STORAGE_KEYS } from 'common'
export const FEATURE_PREVIEWS = [
{
@@ -2,10 +2,11 @@ import { ExternalLink, Eye, EyeOff, FlaskConical } from 'lucide-react'
import Link from 'next/link'
import { useEffect } from 'react'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useFlag } from 'hooks/ui/useFlag'
import { IS_PLATFORM } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { removeTabsByEditor } from 'state/tabs'
import { Badge, Button, Modal, ScrollArea, cn } from 'ui'
@@ -1,10 +1,11 @@
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { BASE_PATH, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { BASE_PATH } from 'lib/constants'
import { ExternalLink, X } from 'lucide-react'
import Image from 'next/image'
import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Badge, Button } from 'ui'
import { useIsNewLayoutEnabled } from './FeaturePreviewContext'
import { LOCAL_STORAGE_KEYS } from 'common'
export const LayoutUpdatePreview = () => {
return (
@@ -2,13 +2,13 @@ import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect } from 'react'
import { toast } from 'sonner'
import { useIsLoggedIn, useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useIsLoggedIn, useParams } from 'common'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import useLatest from 'hooks/misc/useLatest'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { IS_PLATFORM } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { useIsNewLayoutEnabled } from './FeaturePreview/FeaturePreviewContext'
@@ -2,7 +2,6 @@ import { isEmpty, noop } from 'lodash'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { Modal } from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
@@ -5,7 +5,7 @@ import { UIEvent, useEffect, useMemo, useRef, useState } from 'react'
import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid'
import { toast } from 'sonner'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import AlertError from 'components/ui/AlertError'
@@ -18,7 +18,6 @@ import { useUserDeleteMutation } from 'data/auth/user-delete-mutation'
import { useUsersCountQuery } from 'data/auth/users-count-query'
import { User, useUsersInfiniteQuery } from 'data/auth/users-infinite-query'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import {
Button,
cn,
@@ -6,7 +6,7 @@ import { useEffect, useMemo, useState } from 'react'
import ReactFlow, { Background, BackgroundVariant, MiniMap, useReactFlow } from 'reactflow'
import 'reactflow/dist/style.css'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import AlertError from 'components/ui/AlertError'
@@ -16,7 +16,6 @@ import { useSchemasQuery } from 'data/database/schemas-query'
import { useTablesQuery } from 'data/tables/tables-query'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { toast } from 'sonner'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from 'ui'
import { SchemaGraphLegend } from './SchemaGraphLegend'
@@ -4,9 +4,9 @@ import { uniqBy } from 'lodash'
import { Edge, Node, Position } from 'reactflow'
import 'reactflow/dist/style.css'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { tryParseJson } from 'lib/helpers'
import { TABLE_NODE_ROW_HEIGHT, TABLE_NODE_WIDTH, TableNodeData } from './SchemaTableNode'
import { LOCAL_STORAGE_KEYS } from 'common'
const NODE_SEP = 25
const RANK_SEP = 50
@@ -37,10 +37,10 @@ import { MouseEventHandler, useCallback, useEffect, useState } from 'react'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, cn } from 'ui'
import { RoleImpersonationSelector } from '../RoleImpersonationSelector'
import styles from './graphiql.module.css'
import { LOCAL_STORAGE_KEYS } from 'common'
export interface GraphiQLProps {
fetcher: Fetcher
@@ -3,12 +3,12 @@ import { useRouter } from 'next/router'
import { useState } from 'react'
import { toast } from 'sonner'
import { LOCAL_STORAGE_KEYS } from 'common'
import { useIsNewLayoutEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useOrganizationDeleteMutation } from 'data/organizations/organization-delete-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { Button, Form, Input, Modal } from 'ui'
const DeleteOrganizationButton = () => {
@@ -3,7 +3,7 @@ import { useRouter } from 'next/router'
import { useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useIsNewLayoutEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import {
ScaffoldActionsContainer,
@@ -22,7 +22,6 @@ import { usePermissionsQuery } from 'data/permissions/permissions-query'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useProfile } from 'lib/profile'
import { Input } from 'ui-patterns/DataInputs/Input'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
@@ -4,14 +4,14 @@ import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
import { formatDatabaseID } from 'data/read-replicas/replicas.utils'
import { executeSql } from 'data/sql/execute-sql-query'
import { DbQueryHook } from 'hooks/analytics/useDbQuery'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { IS_PLATFORM } from 'lib/constants'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import {
Button,
@@ -2,14 +2,13 @@ import { ArrowDown, ArrowUp, RefreshCw } from 'lucide-react'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { DownloadResultsButton } from 'components/ui/DownloadResultsButton'
import { FilterPopover } from 'components/ui/FilterPopover'
import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query'
import { DbQueryHook } from 'hooks/analytics/useDbQuery'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import {
Button,
DropdownMenu,
@@ -3,10 +3,9 @@ import { debounce } from 'lodash'
import { useRouter } from 'next/router'
import { MutableRefObject, useEffect, useRef } from 'react'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useProfile } from 'lib/profile'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
@@ -8,7 +8,7 @@ import { useRouter } from 'next/router'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import ResizableAIWidget from 'components/ui/AIEditor/ResizableAIWidget'
import { GridFooter } from 'components/ui/GridFooter'
import { useSqlTitleGenerateMutation } from 'data/ai/sql-title-mutation'
@@ -24,7 +24,7 @@ import { useOrgOptedIntoAi } from 'hooks/misc/useOrgOptedIntoAi'
import { useSchemasForAi } from 'hooks/misc/useSchemasForAi'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { BASE_PATH, IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { BASE_PATH, IS_PLATFORM } from 'lib/constants'
import { formatSql } from 'lib/formatSql'
import { detectOS, uuidv4 } from 'lib/helpers'
import { useProfile } from 'lib/profile'
@@ -2,13 +2,12 @@ import dayjs from 'dayjs'
import { ArrowUpDown, X } from 'lucide-react'
import { useMemo } from 'react'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import BarChart from 'components/ui/Charts/BarChart'
import NoDataPlaceholder from 'components/ui/Charts/NoDataPlaceholder'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useFlag } from 'hooks/ui/useFlag'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import Link from 'next/link'
import {
Badge,
@@ -1,11 +1,11 @@
import { AlignLeft, Check, Heart, Keyboard, MoreVertical } from 'lucide-react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { RoleImpersonationPopover } from 'components/interfaces/RoleImpersonationSelector'
import DatabaseSelector from 'components/ui/DatabaseSelector'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { IS_PLATFORM } from 'lib/constants'
import { detectOS } from 'lib/helpers'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import {
@@ -1,4 +1,5 @@
import { Monaco } from '@monaco-editor/react'
import { LOCAL_STORAGE_KEYS } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import getPgsqlCompletionProvider from 'components/ui/CodeEditor/Providers/PgSQLCompletionProvider'
import getPgsqlSignatureHelpProvider from 'components/ui/CodeEditor/Providers/PgSQLSignatureHelpProvider'
@@ -7,7 +8,6 @@ import { useKeywordsQuery } from 'data/database/keywords-query'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useTableColumnsQuery } from 'data/database/table-columns-query'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { formatSql } from 'lib/formatSql'
import { IDisposable } from 'monaco-editor'
import { useEffect, useRef } from 'react'
@@ -14,7 +14,7 @@ import Link from 'next/link'
import { useRouter } from 'next/router'
import { ComponentProps, ComponentPropsWithoutRef, FC, ReactNode, useEffect } from 'react'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import {
generateOtherRoutes,
generateProductRoutes,
@@ -30,7 +30,6 @@ import { useLints } from 'hooks/misc/useLints'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useFlag } from 'hooks/ui/useFlag'
import { Home } from 'icons'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import {
Button,
@@ -7,7 +7,8 @@ import { PropsWithChildren, useEffect } from 'react'
import { useIsNewLayoutEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { withAuth } from 'hooks/misc/withAuth'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { IS_PLATFORM } from 'lib/constants'
import { LOCAL_STORAGE_KEYS } from 'common'
import { useAppStateSnapshot } from 'state/app-state'
import { cn, NavMenu, NavMenuItem } from 'ui'
import {
@@ -6,9 +6,9 @@ import NoPermission from 'components/ui/NoPermission'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { withAuth } from 'hooks/misc/withAuth'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import ProjectLayout from '../ProjectLayout/ProjectLayout'
import { LogsSidebarMenuV2 } from './LogsSidebarMenuV2'
import { LOCAL_STORAGE_KEYS } from 'common'
interface LogsLayoutProps {
title?: string
@@ -7,7 +7,8 @@ import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useShowLayoutHeader } from 'hooks/misc/useShowLayoutHeader'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { IS_PLATFORM } from 'lib/constants'
import { LOCAL_STORAGE_KEYS } from 'common'
export const HomeIcon = () => {
const newLayoutPreview = useIsNewLayoutEnabled()
@@ -11,7 +11,7 @@ import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import type { Route } from 'components/ui/ui.types'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { LOCAL_STORAGE_KEYS } from 'common'
interface NavigationIconButtonProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
route: Route
@@ -1,15 +1,14 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useDebounce } from '@uidotdev/usehooks'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { FilePlus, FolderPlus, Plus, X } from 'lucide-react'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useProfile } from 'lib/profile'
import { getAppStateSnapshot } from 'state/app-state'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
@@ -1,4 +1,4 @@
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useIsSQLEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import DownloadSnippetModal from 'components/interfaces/SQLEditor/DownloadSnippetModal'
import { MoveQueryModal } from 'components/interfaces/SQLEditor/MoveQueryModal'
@@ -17,7 +17,6 @@ import { Snippet, SnippetFolder, useSQLSnippetFoldersQuery } from 'data/content/
import { useSqlSnippetsQuery } from 'data/content/sql-snippets-query'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useProfile } from 'lib/profile'
import uuidv4 from 'lib/uuid'
import { Eye, EyeOffIcon, Heart, Unlock } from 'lucide-react'
@@ -16,8 +16,7 @@ import {
TabsTrigger_Shadcn_,
} from 'ui'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
const ApiKeysLayout = ({ children }: PropsWithChildren) => {
const { ref: projectRef } = useParams()
@@ -3,12 +3,13 @@ import * as Sentry from '@sentry/nextjs'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { useParams, useTelemetryCookie, useUser } from 'common'
import { LOCAL_STORAGE_KEYS, useParams, useTelemetryCookie, useUser } from 'common'
import { useSendGroupsIdentifyMutation } from 'data/telemetry/send-groups-identify-mutation'
import { useSendGroupsResetMutation } from 'data/telemetry/send-groups-reset-mutation'
import { usePrevious } from 'hooks/deprecated'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { IS_PLATFORM } from 'lib/constants'
const getAnonId = async (id: string) => {
const encoder = new TextEncoder()
@@ -3,7 +3,7 @@ import { toast } from 'sonner'
import { handleError, post } from 'data/fetchers'
import type { ResponseError } from 'types'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { LOCAL_STORAGE_KEYS } from 'common'
export type GitHubAuthorizationCreateVariables = {
code: string
+1 -1
View File
@@ -1,4 +1,4 @@
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { LOCAL_STORAGE_KEYS } from 'common'
import { useLocalStorage } from './useLocalStorage'
export type LastSignInType = 'github' | 'email' | 'sso' | null
@@ -1,8 +1,7 @@
import { parseAsString, useQueryState } from 'nuqs'
import { useEffect, useMemo } from 'react'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
/**
* This hook wraps useQueryState because useQueryState imports app router for some reason which breaks the SSR in
+1 -1
View File
@@ -1,5 +1,5 @@
import { LOCAL_STORAGE_KEYS } from 'common'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { Dispatch, SetStateAction } from 'react'
/**
+1 -2
View File
@@ -1,10 +1,9 @@
import { useQueryClient } from '@tanstack/react-query'
import { AuthProvider as AuthProviderInternal, gotrueClient } from 'common'
import { AuthProvider as AuthProviderInternal, clearLocalStorage, gotrueClient } from 'common'
import { PropsWithChildren, useCallback, useEffect } from 'react'
import { toast } from 'sonner'
import { GOTRUE_ERRORS, IS_PLATFORM } from './constants'
import { clearLocalStorage } from './local-storage'
export const AuthProvider = ({ children }: PropsWithChildren<{}>) => {
// Check for unverified GitHub users after a GitHub sign in
-59
View File
@@ -38,65 +38,6 @@ export const STRIPE_PUBLIC_KEY =
export const USAGE_APPROACHING_THRESHOLD = 0.75
export const LOCAL_STORAGE_KEYS = {
AI_ASSISTANT_STATE: (projectRef: string | undefined) =>
`supabase-ai-assistant-state-${projectRef}`,
SIDEBAR_BEHAVIOR: 'supabase-sidebar-behavior',
EDITOR_PANEL_STATE: 'supabase-editor-panel-state',
UI_PREVIEW_API_SIDE_PANEL: 'supabase-ui-api-side-panel',
UI_PREVIEW_CLS: 'supabase-ui-cls',
UI_PREVIEW_INLINE_EDITOR: 'supabase-ui-preview-inline-editor',
UI_ONBOARDING_NEW_PAGE_SHOWN: 'supabase-ui-onboarding-new-page-shown',
UI_TABLE_EDITOR_TABS: 'supabase-ui-table-editor-tabs',
UI_SQL_EDITOR_TABS: 'supabase-ui-sql-editor-tabs',
UI_NEW_LAYOUT_PREVIEW: 'supabase-ui-new-layout-preview',
NEW_LAYOUT_NOTICE_ACKNOWLEDGED: 'new-layout-notice-acknowledge',
DASHBOARD_HISTORY: (ref: string) => `dashboard-history-${ref}`,
STORAGE_PREFERENCE: (ref: string) => `storage-explorer-${ref}`,
SQL_EDITOR_INTELLISENSE: 'supabase_sql-editor-intellisense-enabled',
SQL_EDITOR_SPLIT_SIZE: 'supabase_sql-editor-split-size',
// Key to track which schemas are ok to be sent to AI. The project ref is intentionally put at the end for easier search in the browser console.
SQL_EDITOR_AI_SCHEMA: (ref: string) => `supabase_sql-editor-ai-schema-enabled-${ref}`,
SQL_EDITOR_AI_OPEN: 'supabase_sql-editor-ai-open',
SQL_EDITOR_LAST_SELECTED_DB: (ref: string) => `sql-editor-last-selected-db-${ref}`,
SQL_EDITOR_SQL_BLOCK_ACKNOWLEDGED: (ref: string) => `sql-editor-sql-block-acknowledged-${ref}`,
SQL_EDITOR_SECTION_STATE: (ref: string) => `sql-editor-section-state-${ref}`,
SQL_EDITOR_SORT: (ref: string) => `sql-editor-sort-${ref}`,
LOG_EXPLORER_SPLIT_SIZE: 'supabase_log-explorer-split-size',
GRAPHIQL_RLS_BYPASS_WARNING: 'graphiql-rls-bypass-warning-dismissed',
CLS_DIFF_WARNING: 'cls-diff-warning-dismissed',
CLS_SELECT_STAR_WARNING: 'cls-select-star-warning-dismissed',
QUERY_PERF_SHOW_BOTTOM_SECTION: 'supabase-query-perf-show-bottom-section',
// Key to track account deletion requests
ACCOUNT_DELETION_REQUEST: 'supabase-account-deletion-request',
// Used for storing a user id when sending reports to Sentry. The id is hashed for anonymity.
SENTRY_USER_ID: 'supabase-sentry-user-id',
// Used for storing the last sign in method used by the user
LAST_SIGN_IN_METHOD: 'supabase-last-sign-in-method',
// Key to track the last selected schema. The project ref is intentionally put at the end for easier search in the browser console.
LAST_SELECTED_SCHEMA: (ref: string) => `last-selected-schema-${ref}`,
// Track position of nodes for schema visualizer
SCHEMA_VISUALIZER_POSITIONS: (ref: string, schemaId: number) =>
`schema-visualizer-positions-${ref}-${schemaId}`,
// Used for allowing the main nav panel to expand on hover
EXPAND_NAVIGATION_PANEL: 'supabase-expand-navigation-panel',
GITHUB_AUTHORIZATION_STATE: 'supabase-github-authorization-state',
// Notice banner keys
FLY_POSTGRES_DEPRECATION_WARNING: 'fly-postgres-deprecation-warning-dismissed',
AUTH_USERS_COLUMNS_CONFIGURATION: (ref: string) => `supabase-auth-users-columns-${ref}`,
// api keys view switcher for new and legacy api keys
API_KEYS_VIEW: (ref: string) => `supabase-api-keys-view-${ref}`,
// last visited logs page
LAST_VISITED_LOGS_PAGE: 'supabase-last-visited-logs-page',
LAST_VISITED_ORGANIZATION: 'last-visited-organization',
}
export const OPT_IN_TAGS = {
AI_SQL: 'AI_SQL_GENERATOR_OPT_IN',
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { LOCAL_STORAGE_KEYS } from './constants'
import { LOCAL_STORAGE_KEYS } from 'common'
import { makeRandomString } from './helpers'
const GITHUB_INTEGRATION_APP_NAME =
-27
View File
@@ -1,27 +0,0 @@
import { LOCAL_STORAGE_KEYS as COMMON_LOCAL_STORAGE_KEYS } from 'common'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
const LOCAL_STORAGE_KEYS_ALLOWLIST = [
'graphiql:theme',
'theme',
'supabaseDarkMode',
'supabase.dashboard.auth.debug',
'supabase.dashboard.auth.navigatorLock.disabled',
COMMON_LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT,
LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL,
LOCAL_STORAGE_KEYS.UI_PREVIEW_INLINE_EDITOR,
LOCAL_STORAGE_KEYS.UI_TABLE_EDITOR_TABS,
LOCAL_STORAGE_KEYS.UI_SQL_EDITOR_TABS,
LOCAL_STORAGE_KEYS.UI_NEW_LAYOUT_PREVIEW,
LOCAL_STORAGE_KEYS.UI_PREVIEW_INLINE_EDITOR,
LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS,
LOCAL_STORAGE_KEYS.LAST_SIGN_IN_METHOD,
]
export function clearLocalStorage() {
for (const key in localStorage) {
if (!LOCAL_STORAGE_KEYS_ALLOWLIST.includes(key)) {
localStorage.removeItem(key)
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { components } from 'api-types'
import { hasConsented } from 'common'
import { handleError, post } from 'data/fetchers'
import { IS_PLATFORM } from './constants'
import { LOCAL_STORAGE_KEYS, hasConsented } from 'common'
type TrackFeatureFlagVariables = components['schemas']['TelemetryFeatureFlagBodyDto']
@@ -42,11 +42,12 @@ import { downloadBucketObject } from 'data/storage/bucket-object-download-mutati
import { StorageObject, listBucketObjects } from 'data/storage/bucket-objects-list-mutation'
import { Bucket } from 'data/storage/buckets-query'
import { moveStorageObject } from 'data/storage/object-move-mutation'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { IS_PLATFORM } from 'lib/constants'
import { tryParseJson } from 'lib/helpers'
import { lookupMime } from 'lib/mime'
import Link from 'next/link'
import { Button, SONNER_DEFAULT_DURATION, SonnerProgress } from 'ui'
import { LOCAL_STORAGE_KEYS } from 'common'
type CachedFile = { id: string; fetchedAt: number; expiresIn: number; url: string }
@@ -1,4 +1,4 @@
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { AlertCircle, XIcon } from 'lucide-react'
import Link from 'next/link'
import { useCallback, useMemo, useState } from 'react'
@@ -27,7 +27,6 @@ import { useTablePrivilegesQuery } from 'data/privileges/table-privileges-query'
import { useTablesQuery } from 'data/tables/tables-query'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useAppStateSnapshot } from 'state/app-state'
import type { NextPageWithLayout } from 'types'
@@ -6,7 +6,8 @@ import { useRouter } from 'next/router'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
import { IS_PLATFORM, useParams } from 'common'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS, useParams } from 'common'
import {
LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD,
LOGS_TABLES,
@@ -41,7 +42,6 @@ import useLogsQuery from 'hooks/analytics/useLogsQuery'
import { useLogsUrlState } from 'hooks/analytics/useLogsUrlState'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useUpgradePrompt } from 'hooks/misc/useUpgradePrompt'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { uuidv4 } from 'lib/helpers'
import { useProfile } from 'lib/profile'
import type { LogSqlSnippets, NextPageWithLayout } from 'types'
@@ -1,11 +1,10 @@
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import DefaultLayout from 'components/layouts/DefaultLayout'
import LogsLayout from 'components/layouts/LogsLayout/LogsLayout'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import type { NextPageWithLayout } from 'types'
export const LogPage: NextPageWithLayout = () => {
@@ -1,5 +1,5 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import LegacyAPIKeys from 'components/interfaces/APIKeys/LegacyAPIKeys'
import { PublishableAPIKeys } from 'components/interfaces/APIKeys/PublishableAPIKeys'
@@ -9,7 +9,6 @@ import ApiKeysLayout from 'components/layouts/project/[ref]/settings/APIKeysLayo
import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import type { NextPageWithLayout } from 'types'
import { Separator } from 'ui'
+2 -1
View File
@@ -13,9 +13,10 @@ import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useAutoProjectsPrefetch } from 'data/projects/projects-query'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS, PROJECT_STATUS } from 'lib/constants'
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
import type { NextPageWithLayout } from 'types'
import { cn } from 'ui'
import { LOCAL_STORAGE_KEYS } from 'common'
const ProjectsPage: NextPageWithLayout = () => {
const newLayoutPreview = useIsNewLayoutEnabled()
+1 -2
View File
@@ -3,8 +3,7 @@ import type { Message as MessageType } from 'ai/react'
import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'
import { proxy, snapshot, subscribe, useSnapshot } from 'valtio'
import { debounce } from 'lodash'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { LOCAL_STORAGE_KEYS } from 'common'
type SuggestionsType = {
title: string
+1 -1
View File
@@ -1,7 +1,7 @@
import { proxy, snapshot, subscribe, useSnapshot } from 'valtio'
import { LOCAL_STORAGE_KEYS as COMMON_LOCAL_STORAGE_KEYS, LOCAL_STORAGE_KEYS } from 'common'
import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.queries'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
export type Template = {
name: string
+1 -1
View File
@@ -8,7 +8,7 @@ import {
STORAGE_SORT_BY_ORDER,
STORAGE_VIEWS,
} from 'components/to-be-cleaned/Storage/Storage.constants'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { LOCAL_STORAGE_KEYS } from 'common'
import { tryParseJson } from 'lib/helpers'
const DEFAULT_PREFERENCES = {
@@ -1,41 +0,0 @@
import React from 'react'
import Countdown from 'react-countdown'
import { CountdownWidget } from 'ui-patterns/CountdownWidget'
const CountdownComponent = ({
date,
showCard = true,
className,
size = 'small',
}: {
date: string | number | Date
showCard?: boolean
className?: string
size?: 'small' | 'large'
}) => {
if (!date) return null
const renderer = ({ days, hours, minutes, seconds, completed }: any) => {
if (completed) {
// Render a completed state
return null
} else {
// Render countdown
return (
<CountdownWidget
days={days}
hours={hours}
minutes={minutes}
seconds={seconds}
showCard={showCard}
className={className}
size={size}
/>
)
}
}
return <Countdown date={new Date(date)} renderer={renderer} />
}
export default CountdownComponent
@@ -1,11 +1,7 @@
import React from 'react'
import { TextLink, cn } from 'ui'
import CountdownComponent from './Countdown'
import { LW11_LAUNCH_DATE_END } from '~/lib/constants'
const HackathonCallout = ({ className }: { className?: string }) => {
const isActive = new Date() < new Date(LW11_LAUNCH_DATE_END)
return (
<div
className={cn(
@@ -14,13 +10,7 @@ const HackathonCallout = ({ className }: { className?: string }) => {
)}
>
<div className="!text-foreground [&_*]:text-foreground text-sm flex flex-col sm:flex-row sm:items-center sm:gap-3">
{isActive ? (
<>
Hackathon ends in <CountdownComponent date={LW11_LAUNCH_DATE_END} showCard={false} />
</>
) : (
'Open Source Hackathon 2024'
)}
Open Source Hackathon 2024
</div>
<div className="!m-0 flex items-center">
<TextLink
@@ -1,20 +0,0 @@
export function Dot(x, y, w, h, opacity, animationConfig, color = '255,255,255') {
this.x = x
this.y = y
this.w = w
this.h = h
this.opacity = opacity
this.anim = animationConfig
this.isVert = this.anim?.direction === 'vertical'
this.draw = function (c) {
c.fillRect(this.x, this.y, this.w, this.h)
c.fillStyle = `rgba(${color},${this.opacity})`
c.fill()
}
this.update = function (c, clock) {
this.draw(c, clock)
}
}
@@ -1,168 +0,0 @@
import React, { useEffect, useState } from 'react'
import anime from 'animejs'
import { useWindowSize } from 'react-use'
import { isBrowser } from 'common'
import { Dot } from './Dot'
import { cn } from 'ui'
const defaultConfig = {
dotGrid: 40,
percentageLarge: 0.98,
percentageAnimated: 0.6,
randomizeLargeDots: 3,
randomizeSmallDots: 0.5,
minSpeed: 1,
maxSpeed: 2,
minOscillation: 1,
maxOscillation: 12,
minDelay: -3000,
maxDelay: 15000,
minDuration: 200,
maxDuration: 10000,
}
const LW11Background = ({ className }: { className?: string }) => {
const canvasRef = React.useRef<HTMLCanvasElement>(null)
const [size, setSize] = useState({ w: 1200, h: 800 })
const [config] = useState(defaultConfig)
const [hasInitialized, setHasInitialized] = useState(false)
const Window = useWindowSize()
const DOT_AREA = config.dotGrid
let c = canvasRef.current?.getContext('2d')
let dotsArray: any[] = []
function init() {
if (!c) return
setHasInitialized(true)
c = canvasRef.current?.getContext('2d')!
c.clearRect(0, 0, size.w, size.h)
dotsArray = []
const GRID_COLS = Math.floor(size.w / DOT_AREA)
const GRID_ROWS = Math.floor(size.h / DOT_AREA)
for (let i = 0; i < GRID_COLS; i++) {
for (let j = 0; j < GRID_ROWS; j++) {
const isLarge = Math.random() > config.percentageLarge
const isAnimated = isLarge || Math.random() > config.percentageAnimated
const direction = isAnimated ? (Math.random() > 0.5 ? 'vertical' : 'horizontal') : undefined
const speed = isAnimated ? anime.random(config.minSpeed, config.maxSpeed) : undefined
const opacity = isLarge ? 1 : anime.random(0.4, 1)
const isReverse = isAnimated ? Math.random() > 0.5 : undefined
const oscillation = isAnimated
? anime.random(config.minOscillation, config.maxOscillation).toFixed()
: undefined
const dotSize = isLarge
? Math.random() * config.randomizeLargeDots
: Math.random() * config.randomizeSmallDots
const endPos = {
x: anime
.random(
DOT_AREA * 1 - DOT_AREA / 2 + dotSize / 2,
DOT_AREA * 10 - DOT_AREA / 2 + dotSize / 2
)
.toFixed(),
y: anime
.random(
DOT_AREA * 1 - DOT_AREA / 2 + dotSize / 2,
DOT_AREA * 10 - DOT_AREA / 2 + dotSize / 2
)
.toFixed(),
}
const delay = anime.random(config.minDelay, config.maxDelay)
const duration = anime.random(config.minDuration, config.maxDuration)
const animationConfig: any = isAnimated
? {
direction,
speed,
isReverse,
oscillation,
endPos,
delay,
duration,
}
: undefined
const x = (size.w / GRID_COLS) * i + DOT_AREA / 2 - dotSize / 2
const y = (size.h / GRID_ROWS) * j + DOT_AREA / 2 - dotSize / 2
const w = dotSize
const h = dotSize
dotsArray.push(new Dot(x, y, w, h, opacity, animationConfig))
}
}
animate(0)
}
function animate(clock?: number) {
if (!isBrowser) return
for (let i = 0; i < dotsArray.length; i++) {
dotsArray[i].update(c, clock)
}
const tl = anime
.timeline({
targets: dotsArray.filter((dot) => dot.anim),
loop: true,
direction: 'alternate',
autoplay: true,
update: renderParticule,
})
.add(
{
x: (p: any) =>
!p.isVert ? `${p.anim?.isReverse ? '+' : '-'}=${DOT_AREA * p.anim.oscillation}` : p.x,
y: (p: any) =>
p.isVert ? `${p.anim?.isReverse ? '+' : '-'}=${DOT_AREA * p.anim.oscillation}` : p.y,
duration: (p: any) => p.anim?.duration,
delay: (p: any) => p.anim?.delay - 1000,
easing: 'easeInOutExpo',
},
'-=1000'
)
tl.play()
}
function renderParticule(anim: any) {
if (!isBrowser) return
c?.clearRect(0, 0, size.w, size.h)
for (let i = 0; i < dotsArray.length; i++) {
dotsArray[i].update(c, 0)
}
for (var i = 0; i < anim.animatables.length; i++) {
anim.animatables[i].target.update(c, 0)
}
}
function resize() {
setSize({ w: Window.width, h: Window.height })
init()
}
useEffect(() => {
if (!isBrowser) return
resize()
}, [Window])
useEffect(() => {
resize()
init()
}, [])
!hasInitialized && init()
return (
<canvas
ref={canvasRef}
className={cn('opacity-0 animate-fade-in duration-1000 w-full h-full', className)}
width={size.w}
height={size.h}
/>
)
}
export default LW11Background
@@ -1,7 +1,6 @@
import React from 'react'
import { Button, cn } from 'ui'
import SectionContainer from '~/components/Layouts/SectionContainer'
import LW11Background from '../LW11Background'
import Image from 'next/image'
import Link from 'next/link'
@@ -39,12 +38,6 @@ const LW11Header = ({ className }: { className?: string }) => {
<Link href="/ga">Read full announcement</Link>
</Button>
</SectionContainer>
<div className="absolute z-0 inset-0 w-full h-full overflow-hidden pointer-events-none">
<div className="absolute z-0 inset-0 w-full aspect-video">
<LW11Background className="absolute z-0 inset-0 w-full flex items-center justify-center opacity-100 transition-opacity h-full" />
</div>
<div className="absolute inset-0 bg-[linear-gradient(to_top,#060809)_0%,transparent_100%)]" />
</div>
</div>
)
}
@@ -7,7 +7,6 @@ import { Edit } from 'lucide-react'
import { useBreakpoint } from 'common'
import { WeekDayProps } from '../data'
import CountdownComponent from '../../Countdown'
import { DayLink } from '.'
const DaySection = ({ day, className }: { day: WeekDayProps; className?: string }) => {
@@ -130,7 +129,6 @@ const DaySection = ({ day, className }: { day: WeekDayProps; className?: string
/>
</g>
</svg>
{day.hasCountdown && <CountdownComponent date={day.published_at} showCard={false} />}
</div>
</div>
)}
@@ -1,99 +0,0 @@
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/lwx'
const getTicketImageUrl = (username: string, isGold: boolean) =>
`${STORAGE_URL}/tickets/${isGold ? 'platinum' : 'regular'}/${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) ??
'/images/launchweek/lwx/tickets/placeholder.png'
return (
<Link
href={`/launch-week/tickets/${user.username}`}
key={`${user.username}-000${i}`}
onClick={() => window.scrollTo(0, 0)}
legacyBehavior
passHref
>
<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 ?? ''}
fill
sizes="100%"
className="object-cover object-center"
placeholder="blur"
blurDataURL="/images/launchweek/lwx/tickets/placeholder.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-[#1c1c1c] to-[#121212] 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>
)
}
@@ -7,7 +7,6 @@ import { Edit } from 'lucide-react'
import { useBreakpoint } from 'common'
import { WeekDayProps } from '../data'
// import CountdownComponent from '../../Countdown'
import { DayLink } from '.'
const DaySection = ({ day, className }: { day: WeekDayProps; className?: string }) => {
@@ -1,41 +0,0 @@
import React from 'react'
import Countdown from 'react-countdown'
import { CountdownWidget } from 'ui-patterns/CountdownWidget'
const CountdownComponent = ({
date,
showCard = true,
className,
size = 'small',
}: {
date: string | number | Date
showCard?: boolean
className?: string
size?: 'small' | 'large'
}) => {
if (!date) return null
const renderer = ({ days, hours, minutes, seconds, completed }: any) => {
if (completed) {
// Render a completed state
return null
} else {
// Render countdown
return (
<CountdownWidget
days={days}
hours={hours}
minutes={minutes}
seconds={seconds}
showCard={showCard}
className={className}
size={size}
/>
)
}
}
return <Countdown date={new Date(date)} renderer={renderer} />
}
export default CountdownComponent
@@ -1,373 +0,0 @@
'use client'
import { useEffect, useRef, useState, useCallback, ReactElement } from 'react'
import {
REALTIME_LISTEN_TYPES,
REALTIME_PRESENCE_LISTEN_EVENTS,
REALTIME_SUBSCRIBE_STATES,
RealtimeChannel,
} from '@supabase/supabase-js'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import { v4 as uuidv4 } from 'uuid'
import { useTheme } from 'next-themes'
import { getColor } from './randomColor'
import { Coordinates, Payload, User } from './types'
import { cloneDeep, throttle } from 'lodash'
import Cursor from './Cursor'
export const GRID_SIZE = 100
export const CELL_SIZE = 40
export const CANVAS_WIDTH = 1800
export const CANVAS_HEIGHT = 1600
export const HOVER_DURATION = 100
export const FADE_DURATION = 300
const MAX_ROOM_USERS = 50
const MAX_EVENTS_PER_SECOND = 10
export const X_THRESHOLD = 25
export const Y_THRESHOLD = 65
interface CellState {
isHovered: boolean
fadeStartTime: number | null
color: string
}
export const INTERACTIVE_GRID_COLORS = (isDark: boolean) => ({
GRID_STROKE: isDark ? '#242424' : '#EDEDED',
CURRENT_USER_HOVER: isDark ? '#00311D' : '#72E3AD',
})
// Generate a random user id
const userId = uuidv4()
export default function InteractiveGrid() {
const { supabase, userData } = useConfData()
const { resolvedTheme } = useTheme()
const isDarkTheme = resolvedTheme?.includes('dark')!
const canvasRef = useRef<HTMLCanvasElement>(null)
const [hoveredCells, setHoveredCells] = useState<Map<string, CellState>>(new Map())
const animationFrameRef = useRef<number>()
// These states will be managed via ref as they're mutated within event listeners
const usersRef = useRef<{ [key: string]: User }>({})
const mousePositionRef = useRef<Coordinates>()
const joinTimestampRef = useRef<number>()
const [mousePosition, _setMousePosition] = useState<Coordinates>()
const [isInitialStateSynced, setIsInitialStateSynced] = useState<boolean>(false)
const [roomId, setRoomId] = useState<undefined | string>(undefined)
const [users, setUsers] = useState<{ [key: string]: User }>({})
const setMousePosition = (coordinates: Coordinates) => {
// if (!mousePositionRef.current) return
mousePositionRef.current = coordinates
_setMousePosition(coordinates)
}
const mapInitialUsers = (userChannel: RealtimeChannel, roomId: string) => {
const state = userChannel.presenceState()
const _users = state[roomId]
if (!_users) return
if (_users) {
setUsers((existingUsers) => {
const updatedUsers = _users.reduce(
(acc: { [key: string]: User }, { user_id: userId }: any, index: number) => {
acc[userId] = existingUsers[userId] || {
x: 0,
y: 0,
}
return acc
},
{}
)
usersRef.current = updatedUsers
return updatedUsers
})
}
}
useEffect(() => {
let roomChannel: RealtimeChannel
if (!roomId) {
joinTimestampRef.current = performance.now()
/*
Client is joining 'rooms' channel to examine existing rooms and their users
and then the channel is removed once a room is selected
*/
roomChannel = supabase?.channel('lw13_rooms')!
roomChannel
.on(REALTIME_LISTEN_TYPES.PRESENCE, { event: REALTIME_PRESENCE_LISTEN_EVENTS.SYNC }, () => {
let newRoomId
const state = roomChannel.presenceState()
// User will be assigned an existing room with the fewest users
if (!newRoomId) {
const [mostVacantRoomId, users] =
Object.entries(state).sort(([, a], [, b]) => a.length - b.length)[0] ?? []
if (users && users.length < MAX_ROOM_USERS) {
newRoomId = mostVacantRoomId
}
}
// Generate an id if no existing rooms are available
setRoomId(newRoomId ?? uuidv4())
})
.subscribe()
} else {
// When user has been placed in a room
roomChannel = supabase?.channel('lw13_rooms', { config: { presence: { key: roomId } } })!
roomChannel.on(
REALTIME_LISTEN_TYPES.PRESENCE,
{ event: REALTIME_PRESENCE_LISTEN_EVENTS.SYNC },
() => {
setIsInitialStateSynced(true)
mapInitialUsers(roomChannel, roomId)
}
)
roomChannel.subscribe(async (status: `${REALTIME_SUBSCRIBE_STATES}`) => {
if (status === REALTIME_SUBSCRIBE_STATES.SUBSCRIBED)
await roomChannel.track({ user_id: userId })
})
}
// Must properly remove subscribed channel
return () => {
roomChannel && supabase?.removeChannel(roomChannel)
}
}, [supabase, roomId])
useEffect(() => {
// if (!roomId || !isInitialStateSynced) return
let messageChannel: RealtimeChannel
let setMouseEvent: (e: MouseEvent) => void = () => {}
messageChannel = supabase?.channel(`chat_messages:${roomId}`)!
// Listen for cursor positions from other users in the room
messageChannel.on(
REALTIME_LISTEN_TYPES.BROADCAST,
{ event: 'POS' },
(payload: Payload<{ user_id: string } & Coordinates>) => {
setUsers((users) => {
const userId = payload!.payload!.user_id
const existingUser = users[userId]
if (existingUser) {
const x =
(payload?.payload?.x ?? 0) - X_THRESHOLD > window.innerWidth
? window.innerWidth - X_THRESHOLD
: payload?.payload?.x
const y =
(payload?.payload?.y ?? 0 - Y_THRESHOLD) > window.innerHeight
? window.innerHeight - Y_THRESHOLD
: payload?.payload?.y
users[userId] = { ...existingUser, ...{ x, y } }
users = cloneDeep(users)
}
return users
})
}
)
messageChannel?.subscribe((status: `${REALTIME_SUBSCRIBE_STATES}`) => {
// Lodash throttle will be removed once realtime-js client throttles on the channel level
const sendMouseBroadcast = throttle(({ x, y }) => {
messageChannel
.send({
type: 'broadcast',
event: 'POS',
payload: { user_id: userId, x, y },
})
.catch(() => {})
}, 1000 / MAX_EVENTS_PER_SECOND)
setMouseEvent = (e: MouseEvent) => {
const top = window.pageYOffset || document.documentElement.scrollTop
const [x, y] = [e.clientX, e.clientY - Y_THRESHOLD + top]
if (status === REALTIME_SUBSCRIBE_STATES.SUBSCRIBED) {
sendMouseBroadcast({ x, y })
}
setMousePosition({ x, y })
}
window.addEventListener('mousemove', setMouseEvent)
})
return () => {
window.removeEventListener('mousemove', setMouseEvent)
messageChannel && supabase?.removeChannel(messageChannel)
}
}, [roomId, isInitialStateSynced])
const setCellHovered = useCallback((key: string, isHovered: boolean, color: string) => {
setHoveredCells((prev) => {
const newState = new Map(prev)
if (isHovered) {
newState.set(key, { isHovered: true, fadeStartTime: null, color })
} else {
const existingCell = newState.get(key)
if (existingCell && existingCell.isHovered) {
newState.set(key, {
isHovered: false,
fadeStartTime: Date.now() + HOVER_DURATION,
color,
})
}
}
return newState
})
}, [])
const drawGrid = useCallback(
(ctx: CanvasRenderingContext2D, currentTime: number) => {
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)
ctx.lineWidth = 0.15
ctx.strokeStyle = INTERACTIVE_GRID_COLORS(isDarkTheme).GRID_STROKE
for (let x = 0; x < GRID_SIZE; x++) {
for (let y = 0; y < GRID_SIZE; y++) {
const cellX = x * CELL_SIZE
const cellY = y * CELL_SIZE
ctx.strokeRect(cellX, cellY, CELL_SIZE, CELL_SIZE)
const cellKey = `${x},${y}`
const cellState = hoveredCells.get(cellKey)
if (cellState) {
let opacity = 0.5
if (!cellState.isHovered && cellState.fadeStartTime) {
const fadeElapsed = currentTime - cellState.fadeStartTime
if (fadeElapsed >= 0) {
opacity = Math.max(0, 0.5 * (1 - fadeElapsed / FADE_DURATION))
if (opacity === 0) {
setHoveredCells((prev) => {
const newState = new Map(prev)
newState.delete(cellKey)
return newState
})
}
}
}
ctx.fillStyle = `rgba(${parseInt(cellState.color.slice(1, 3), 16)}, ${parseInt(cellState.color.slice(3, 5), 16)}, ${parseInt(cellState.color.slice(5, 7), 16)}, ${opacity})`
ctx.fillRect(cellX, cellY, CELL_SIZE, CELL_SIZE)
}
}
}
},
[hoveredCells, isDarkTheme]
)
const animate = useCallback(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
const currentTime = Date.now()
drawGrid(ctx, currentTime)
animationFrameRef.current = requestAnimationFrame(animate)
}, [drawGrid])
useEffect(() => {
animate()
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current)
}
}
}, [animate])
const handleMouseMove = useCallback(
(e: React.MouseEvent<HTMLCanvasElement>) => {
handleUserTrail(e.nativeEvent.offsetX, e.nativeEvent.offsetY)
},
[hoveredCells, setCellHovered, isDarkTheme]
)
const handleUserTrail = (
xPos: number,
yPos: number,
color = INTERACTIVE_GRID_COLORS(isDarkTheme).CURRENT_USER_HOVER
) => {
const x = Math.floor(xPos / CELL_SIZE)
const y = Math.floor(yPos / CELL_SIZE)
const cellKey = `${x},${y}`
if (x >= 0 && x < GRID_SIZE && y >= 0 && y < GRID_SIZE) {
setCellHovered(cellKey, true, color)
hoveredCells.forEach((_, key) => {
if (key !== cellKey) {
setCellHovered(key, false, color)
}
})
}
}
const handleMouseLeave = useCallback(() => {
hoveredCells.forEach((_, key) => {
setCellHovered(key, false, INTERACTIVE_GRID_COLORS(isDarkTheme).CURRENT_USER_HOVER)
})
}, [hoveredCells, setCellHovered, userData])
return (
<div className="absolute inset-0 w-screen h-screen flex justify-center items-center max-w-screen max-h-screen">
<canvas
ref={canvasRef}
width={CANVAS_WIDTH}
height={CANVAS_HEIGHT}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className="shadow-lg"
/>
{/* Current user cursor */}
{/* <Cursor
key={userId}
x={mousePosition?.x}
y={mousePosition?.y}
color={getColor('brand').bg}
hue={getColor('brand').hue}
message={''}
isTyping={false}
isCurrentUser={true}
/> */}
{/* Online users cursors */}
{Object.entries(users).reduce((acc, [userId, data]) => {
const { x, y, message, isTyping } = data
if (x && y) {
acc.push(
<Cursor
key={userId}
x={x}
y={y}
color={getColor('gray').bg}
hue={getColor('gray').hue}
message={message || ''}
isTyping={isTyping || false}
isCurrentUser={false}
/>
)
}
return acc
}, [] as ReactElement[])}
</div>
)
}
@@ -1,219 +0,0 @@
'use client'
import { useEffect, useRef, useState, useCallback } from 'react'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import { v4 as uuidv4 } from 'uuid'
import { useTheme } from 'next-themes'
import Cursor from './Cursor'
import { getColor } from './randomColor'
import { Coordinates } from './types'
import {
HOVER_DURATION,
CANVAS_WIDTH,
CANVAS_HEIGHT,
GRID_SIZE,
CELL_SIZE,
FADE_DURATION,
Y_THRESHOLD,
} from './CanvasPartyMode'
interface CellState {
isHovered: boolean
fadeStartTime: number | null
color: string
}
export default function InteractiveGridSingle() {
const { supabase, userData } = useConfData()
const { resolvedTheme } = useTheme()
const isDarkTheme = resolvedTheme?.includes('dark')!
const canvasRef = useRef<HTMLCanvasElement>(null)
const [hoveredCells, setHoveredCells] = useState<Map<string, CellState>>(new Map())
const [userColors, setUserColors] = useState<Record<string, string>>({})
const animationFrameRef = useRef<number>()
const [mousePosition, _setMousePosition] = useState<Coordinates>()
const mousePositionRef = useRef<Coordinates>()
const currentUserIdRef = useRef(userData?.id || uuidv4())
const CURRENT_USER_ID = currentUserIdRef.current
const setMousePosition = (coordinates: Coordinates) => {
mousePositionRef.current = coordinates
_setMousePosition(coordinates)
}
const INTERACTIVE_GRID_COLORS = (isDark: boolean) => ({
GRID_STROKE: isDark ? '#242424' : '#EDEDED',
CURRENT_USER_HOVER: isDark ? '#242424' : '#d3d3d3',
})
const getUserColor = useCallback(
(userId: string | undefined) => {
if (!userId) {
return INTERACTIVE_GRID_COLORS(isDarkTheme).CURRENT_USER_HOVER // fallback color
}
if (userColors[userId]) {
return userColors[userId]
}
const colors = [INTERACTIVE_GRID_COLORS(isDarkTheme).CURRENT_USER_HOVER]
const color = colors[userId.charCodeAt(0) % colors.length]
setUserColors((prev) => ({ ...prev, [userId]: color }))
return color
},
[userColors]
)
const setCellHovered = useCallback((key: string, isHovered: boolean, color: string) => {
setHoveredCells((prev) => {
const newState = new Map(prev)
if (isHovered) {
newState.set(key, { isHovered: true, fadeStartTime: null, color })
} else {
const existingCell = newState.get(key)
if (existingCell && existingCell.isHovered) {
newState.set(key, {
isHovered: false,
fadeStartTime: Date.now() + HOVER_DURATION,
color,
})
}
}
return newState
})
}, [])
const drawGrid = useCallback(
(ctx: CanvasRenderingContext2D, currentTime: number) => {
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)
ctx.lineWidth = 0.15
ctx.strokeStyle = INTERACTIVE_GRID_COLORS(isDarkTheme).GRID_STROKE
for (let x = 0; x < GRID_SIZE; x++) {
for (let y = 0; y < GRID_SIZE; y++) {
const cellX = x * CELL_SIZE
const cellY = y * CELL_SIZE
ctx.strokeRect(cellX, cellY, CELL_SIZE, CELL_SIZE)
const cellKey = `${x},${y}`
const cellState = hoveredCells.get(cellKey)
if (cellState) {
let opacity = 0.5
if (!cellState.isHovered && cellState.fadeStartTime) {
const fadeElapsed = currentTime - cellState.fadeStartTime
if (fadeElapsed >= 0) {
opacity = Math.max(0, 0.5 * (1 - fadeElapsed / FADE_DURATION))
if (opacity === 0) {
setHoveredCells((prev) => {
const newState = new Map(prev)
newState.delete(cellKey)
return newState
})
}
}
}
ctx.fillStyle = `rgba(${parseInt(cellState.color.slice(1, 3), 16)}, ${parseInt(cellState.color.slice(3, 5), 16)}, ${parseInt(cellState.color.slice(5, 7), 16)}, ${opacity})`
ctx.fillRect(cellX, cellY, CELL_SIZE, CELL_SIZE)
}
}
}
},
[hoveredCells, getUserColor, isDarkTheme]
)
const animate = useCallback(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
const currentTime = Date.now()
drawGrid(ctx, currentTime)
animationFrameRef.current = requestAnimationFrame(animate)
}, [drawGrid])
useEffect(() => {
animate()
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current)
}
}
}, [animate])
const handleMouseMove = useCallback(
(e: React.MouseEvent<HTMLCanvasElement>) => {
const x = Math.floor(e.nativeEvent.offsetX / CELL_SIZE)
const y = Math.floor(e.nativeEvent.offsetY / CELL_SIZE)
const cellKey = `${x},${y}`
const userColor = getUserColor(CURRENT_USER_ID)
if (x >= 0 && x < GRID_SIZE && y >= 0 && y < GRID_SIZE) {
setCellHovered(cellKey, true, userColor)
hoveredCells.forEach((_, key) => {
if (key !== cellKey) {
setCellHovered(key, false, userColor)
}
})
}
},
[hoveredCells, setCellHovered, getUserColor]
)
const handleMouseLeave = useCallback(() => {
hoveredCells.forEach((_, key) => {
setCellHovered(key, false, getUserColor(CURRENT_USER_ID))
})
}, [hoveredCells, setCellHovered, getUserColor, userData])
useEffect(() => {
let setMouseEvent: (e: MouseEvent) => void = () => {}
setMouseEvent = (e: MouseEvent) => {
const top = window.pageYOffset || document.documentElement.scrollTop
const [x, y] = [e.clientX, e.clientY - Y_THRESHOLD + top]
setMousePosition({ x, y })
}
window.addEventListener('mousemove', setMouseEvent)
return () => {
window.removeEventListener('mousemove', setMouseEvent)
}
}, [supabase])
return (
<div className="absolute inset-0 w-screen h-screen flex justify-center items-center max-w-screen max-h-screen">
<canvas
ref={canvasRef}
width={CANVAS_WIDTH}
height={CANVAS_HEIGHT}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className="border border-gray-300 shadow-lg pointer-events-auto"
/>
{/* Current user cursor */}
{/* <Cursor
x={mousePosition?.x}
y={mousePosition?.y}
color={getColor('brand').bg}
hue={getColor('brand').hue}
message={''}
isTyping={false}
isCurrentUser={true}
/> */}
</div>
)
}
function onlyUnique(value: any, index: number, array: any[]) {
return array.indexOf(value) === index
}
@@ -1,47 +0,0 @@
// import { IconLoader } from '@supabase/ui'
import { FC, RefObject } from 'react'
import { Message } from './types'
interface Props {
messages: Message[]
chatboxRef: RefObject<any>
messagesInTransit: string[]
areMessagesFetched: boolean
}
const Chatbox: FC<Props> = ({ messages, chatboxRef, messagesInTransit, areMessagesFetched }) => {
return (
<div className="flex flex-col rounded-md break-all">
<div
className="space-y-1 py-2 px-4 w-[400px]"
style={{ backgroundColor: 'rgba(0, 207, 144, 0.05)' }}
>
{!areMessagesFetched ? (
<div className="flex items-center space-x-2">
{/* <IconLoader className="animate-spin text-scale-1200" size={14} /> */}
<p className="text-sm text-scale-1100">Loading messages</p>
</div>
) : messages.length === 0 && messagesInTransit.length === 0 ? (
<div className="text-scale-1200 text-sm opacity-75">
<span>Type anything to start chatting 🥳</span>
</div>
) : (
<div />
)}
{messages.map((message) => (
<p key={message.id} className="text-scale-1200 text-sm whitespace-pre-line">
{message.message}
</p>
))}
{messagesInTransit.map((message, idx: number) => (
<p key={`transit-${idx}`} className="text-sm text-scale-1100">
{message}
</p>
))}
{/* <div ref={chatboxRef} className="!mt-0" /> */}
</div>
</div>
)
}
export default Chatbox
@@ -1,115 +0,0 @@
import { useBreakpoint } from 'common'
import { FC, useEffect, useRef, useState } from 'react'
import { cn } from 'ui'
interface Props {
x?: number
y?: number
color: string
hue: string
message: string
isCurrentUser: boolean
isTyping: boolean
isCancelled?: boolean
isLocalClient?: boolean
onUpdateMessage?: (message: string) => void
}
const MAX_DURATION = 4000
const Cursor: FC<Props> = ({
x,
y,
color,
hue,
message,
isCurrentUser,
isTyping,
isCancelled,
isLocalClient,
onUpdateMessage = () => {},
}) => {
const _isLocalClient = !x || !y
const inputRef = useRef() as any
const timeoutRef = useRef() as any
const chatBubbleRef = useRef() as any
const isMobile = useBreakpoint('sm')
const [isTouchDevice, setIsTouchDevice] = useState(false)
const [hideInput, setHideInput] = useState(false)
const [showMessageBubble, setShowMessageBubble] = useState(false)
useEffect(() => {
if (isTyping) {
setShowMessageBubble(true)
if (timeoutRef.current) clearTimeout(timeoutRef.current)
if (isLocalClient) {
if (inputRef.current) inputRef.current.focus()
setHideInput(false)
}
} else {
if (!message || isCancelled) {
setShowMessageBubble(false)
} else {
if (timeoutRef.current) clearTimeout(timeoutRef.current)
if (isLocalClient) setHideInput(true)
const timeoutId = setTimeout(() => {
setShowMessageBubble(false)
}, MAX_DURATION)
timeoutRef.current = timeoutId
}
}
}, [isLocalClient, isTyping, isCancelled, message, inputRef])
useEffect(() => {
// touchscreen
if (window.matchMedia('(pointer: coarse)').matches && isMobile) {
setIsTouchDevice(true)
}
}, [isMobile])
if (isCurrentUser && isTouchDevice) return null
return (
<>
{!_isLocalClient && (
<svg
width="18"
height="21"
viewBox="0 0 18 21"
fill="none"
className={cn(
'absolute top-0 left-0 transform transition pointer-events-none z-0',
isCurrentUser && 'transition-none z-40'
)}
style={{ color, transform: `translateX(${x}px) translateY(${y}px)` }}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4.19777 1.00263C3.10663 0.0999637 1.45741 0.876037 1.45741 2.29215V18.767C1.45741 20.368 3.48943 21.0551 4.4611 19.7827L8.48462 14.5138C8.59593 14.368 8.76885 14.2825 8.95226 14.2825H15.6018C17.1701 14.2825 17.877 12.3191 16.6686 11.3194L4.19777 1.00263Z"
fill={hue}
stroke={color}
strokeWidth="1"
strokeLinejoin="round"
/>
</svg>
)}
<div
ref={chatBubbleRef}
className={cn(
'transition-all absolute top-0 left-0 py-2 rounded-full shadow-md',
'flex items-center justify-between px-4 space-x-2 pointer-events-none',
`${showMessageBubble ? 'opacity-100' : 'opacity-0'}`,
`${_isLocalClient && !hideInput ? 'w-[280px]' : 'max-w-[280px] overflow-hidden'}`
)}
style={{
backgroundColor: color,
transform: `translateX(${(x || 0) + 20}px) translateY(${(y || 0) + 20}px)`,
}}
></div>
</>
)
}
export default Cursor
@@ -1,32 +0,0 @@
import { sampleSize } from 'lodash'
const colors = {
brand: {
bg: 'hsl(var(--brand-default))',
hue: 'hsl(var(--brand-500))',
},
gray: {
bg: 'hsl(var(--foreground-muted))',
hue: 'hsl(var(--background-surface-100))',
},
}
export const getRandomUniqueColor = (currentColors: string[]) => {
const colorNames = Object.values(colors).map((col) => col.bg)
const uniqueColors = colorNames.filter((color: string) => !currentColors.includes(color))
const uniqueColor = uniqueColors[Math.floor(Math.random() * uniqueColors.length)]
const uniqueColorSet = Object.values(colors).find((color) => color.bg === uniqueColor)
return uniqueColorSet || getRandomColor()
}
export const getRandomColors = (qty: number) => {
return sampleSize(Object.values(colors), qty)
}
export const getRandomColor = () => {
return Object.values(colors)[Math.floor(Math.random() * Object.values(colors).length)]
}
export const getColor = (color: 'brand' | 'gray') => {
return colors[color]
}
@@ -1,23 +0,0 @@
export interface Coordinates {
x: number | undefined
y: number | undefined
}
export interface Message {
id: number
user_id: string
message: string
}
export interface Payload<T> {
type: string
event: string
payload?: T
}
export interface User extends Coordinates {
color: string
hue: string
isTyping?: boolean
message?: string
}
@@ -1,8 +1,6 @@
import React from 'react'
import { cn } from 'ui'
import SectionContainer from '~/components/Layouts/SectionContainer'
import CanvasSingleMode from '../Multiplayer/CanvasSingleMode'
// import LW12Background from '../LW12Background'
const LWHeader = ({ className }: { className?: string }) => {
return (
@@ -12,12 +10,6 @@ const LWHeader = ({ className }: { className?: string }) => {
className
)}
>
<div className="absolute z-0 inset-0 w-full h-full">
<div className="absolute z-0 inset-0 w-full !pointer-events-none">
<CanvasSingleMode />
</div>
<div className="absolute inset-0 w-full h-full bg-gradient-to-t from-background to-transparent !pointer-events-none" />
</div>
<SectionContainer className="h-full flex flex-col items-start justify-end gap-3 !max-w-none lg:!container !pb-4 md:!pb-10 !pointer-events-none">
<h1 className="text-3xl uppercase tracking-wide pointer-events-none">
Launch Week <span className="font-mono">13</span>
@@ -7,7 +7,6 @@ import { Edit } from 'lucide-react'
import { useBreakpoint } from 'common'
import { WeekDayProps } from '../data'
import CountdownComponent from '../../Countdown'
import { DayLink } from '.'
const DaySection = ({ day, className }: { day: WeekDayProps; className?: string }) => {
@@ -147,7 +146,6 @@ const DaySection = ({ day, className }: { day: WeekDayProps; className?: string
/>
</g>
</svg>
{day.hasCountdown && <CountdownComponent date={day.published_at} showCard={false} />}
</div>
<div>
<Skeleton className="w-full h-3 max-w-xs rounded-full will-change-contents" />
@@ -1,544 +0,0 @@
'use client'
import React, { useEffect, useRef } from 'react'
import { useTheme } from 'next-themes'
import { cn, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from 'ui'
import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js'
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js'
import { useKey } from 'react-use'
import useConfData from '../hooks/use-conf-data'
import useLwGame, { VALID_KEYS } from '../hooks/useLwGame'
const ThreeTicketCanvas: React.FC<{
username: string
className?: string
ticketType?: 'regular' | 'platinum' | 'secret'
ticketPosition?: 'left' | 'right'
sharePage?: boolean
}> = ({
username = 'Francesco Sansalvadore',
className,
ticketType = 'regular',
ticketPosition = 'right',
sharePage = false,
}) => {
const { userData } = useConfData()
const { resolvedTheme } = useTheme()
const isDarkTheme = resolvedTheme?.includes('dark')!
const inputRef = useRef(null)
const canvasRef = useRef<HTMLDivElement>(null)
const ticketRef = useRef<THREE.Mesh | null>(null)
const animationFrameRef = useRef<number>()
const targetRotation = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
const targetScale = useRef<number>(0)
const groupYRotation = useRef<number>(0)
const isFlipped = useRef(false)
const dragStartX = useRef<number | null>(null)
const dragDelta = useRef<number>(0)
const isDragging = useRef<boolean>(false)
const currentValue = useRef<string>('')
const flipDirection = useRef<number>(0)
const {
isGameMode,
setIsGameMode,
winningPhrase,
handleIndexCount,
setGameState,
value,
phraseLength,
REGEXP_ONLY_CHARS,
hasWon,
} = useLwGame(inputRef)
const CANVAS_HEIGHT = 600
const FLIP_DELTA = 20 // Swipe px threshold for flipping
const LINE_HEIGHT = 1.6
const SCALE_VARIATION_ON_INTERACTION = 0.025
const TICKET_FONT_PADDING_LEFT = -6.4
const DISPLAY_NAME = username?.split(' ').reverse() || []
const ALIGN_RIGHT = ticketPosition === 'right'
const IS_PLATINUM = ticketType === 'platinum'
const IS_SECRET = ticketType === 'secret'
const TEXT_Z_POSITION = 0
const FOOTER_CONTENT = [
{
text: 'LAUNCH WEEK 13',
position: { x: TICKET_FONT_PADDING_LEFT, y: -6.8, z: TEXT_Z_POSITION },
size: 0.63,
},
{
text: '2-6 DEC / 7AM PT',
position: { x: TICKET_FONT_PADDING_LEFT, y: -7.7, z: TEXT_Z_POSITION },
size: 0.55,
},
]
const CONFIG = {
regular: {
ticketColor: isDarkTheme ? 0xf3f3f3 : 0x121212,
ticketForeground: isDarkTheme ? 0x171717 : 0xffffff,
},
platinum: {
ticketColor: isDarkTheme ? 0x9ea1a1 : 0x9ea1a1,
ticketForeground: 0x0f0f0f,
},
secret: {
ticketColor: isDarkTheme ? 0xe7a938 : 0xf2c66d,
ticketForeground: 0x0f0f0f,
},
}
const isDesktop = (width: number) => width > 1024
const getTicketScale = (width: number) => (isDesktop(width) ? 0.35 : 0.3)
const getTicketXPosition = (width: number, isRight: boolean) =>
isDesktop(width) ? (isRight ? 5 : -5) : 0
useKey('Escape', () => {
resetRotation()
})
// Reset handler with smooth transition
const resetRotation = () => {
targetRotation.current = { x: 0, y: 0 }
isFlipped.current = false
setIsGameMode(false)
}
useEffect(() => {
if (!canvasRef.current) return
let scale = getTicketScale(window.innerWidth)
targetScale.current = scale
const calculateDesktopWidth = () => window.innerWidth
const initialCanvasWidth = calculateDesktopWidth()
const initialCanvasHeight = CANVAS_HEIGHT
const ticketYIdleRotation = 0
// Initialize scene, camera, and renderer
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(
25,
initialCanvasWidth / initialCanvasHeight,
0.1,
1000
)
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
renderer.setSize(initialCanvasWidth, initialCanvasHeight)
renderer.setPixelRatio(window.devicePixelRatio)
renderer.shadowMap.enabled = true
canvasRef.current.appendChild(renderer.domElement)
// Camera setup
const cameraDistance = 30
camera.position.z = cameraDistance
// Rest of your existing setup code (loader, materials, etc.)
const gltfLoader = new GLTFLoader()
// Texture credits to Freepik: https://www.freepik.com/free-photo/golden-wall-background_1213228.htm
const metalTexture = new THREE.TextureLoader().load(
'/images/launchweek/13/ticket/metal-texture.jpg'
)
const goldTexture = new THREE.TextureLoader().load(
'/images/launchweek/13/ticket/gold-texture.jpg'
)
const metalMaterial = new THREE.MeshStandardMaterial({
color: CONFIG[ticketType].ticketColor,
map: IS_SECRET ? goldTexture : IS_PLATINUM ? metalTexture : undefined,
bumpMap: IS_SECRET ? goldTexture : IS_PLATINUM ? metalTexture : undefined,
metalnessMap: IS_SECRET ? goldTexture : metalTexture,
// roughnessMap: IS_SECRET ? goldTexture : metalTexture,
metalness: IS_SECRET ? 0.8 : IS_PLATINUM ? 1.0 : isDarkTheme ? 0.2 : 0.9,
roughness: IS_SECRET ? 0.12 : IS_PLATINUM ? 0.22 : isDarkTheme ? 0.2 : 0.5,
bumpScale: IS_SECRET ? 0.02 : IS_PLATINUM ? 0.0045 : undefined,
})
// Load Ticket model, fonts and textures
let ticket3DImport: THREE.Mesh
const ticketGroup = new THREE.Group()
const ticketScale = getTicketScale(window.innerWidth)
ticketGroup.scale.set(ticketScale, ticketScale, ticketScale)
ticketGroup.position.x = getTicketXPosition(window.innerWidth, ALIGN_RIGHT)
gltfLoader.load('/images/launchweek/13/ticket/3D-ticket.glb', (gltf) => {
ticket3DImport = gltf.scene.children[0] as THREE.Mesh
ticket3DImport.rotation.x = Math.PI * 0.5
ticket3DImport.traverse((child) => {
if (child) {
// @ts-ignore
child.material = metalMaterial
child.castShadow = true
child.receiveShadow = true
}
})
ticketGroup.add(ticket3DImport)
scene.add(ticketGroup)
ticketRef.current = ticket3DImport
camera.lookAt(ticket3DImport.position)
})
const textMaterial = new THREE.MeshStandardMaterial({
color: CONFIG[ticketType].ticketForeground,
metalness: 0.2,
roughness: 0.35,
})
// Load font and add text geometry
const fontLoader = new FontLoader()
// Load Inter font
fontLoader.load('/images/launchweek/13/ticket/Murecho_Regular.json', (font) => {
DISPLAY_NAME.map((text, index) => {
// Front
const textGeometry = new TextGeometry(text, {
font,
size: 1.2,
height: 0.2,
})
const textMesh = new THREE.Mesh(textGeometry, textMaterial)
textMesh.updateMatrix()
textMesh.position.set(
TICKET_FONT_PADDING_LEFT,
-5 + LINE_HEIGHT * (index + 1),
TEXT_Z_POSITION
)
textMesh.castShadow = true
ticketGroup.add(textMesh)
})
// Back
if (!sharePage) {
const backTextGeometry = new TextGeometry('Type the secret code', {
font,
size: 0.6,
height: 0.2,
})
const backTextMesh = new THREE.Mesh(backTextGeometry, textMaterial)
backTextMesh.updateMatrix()
backTextMesh.position.set(4.0, 2, -TEXT_Z_POSITION)
backTextMesh.rotation.y = Math.PI
backTextMesh.castShadow = true
ticketGroup.add(backTextMesh)
}
})
// Load mono font
fontLoader.load('/images/launchweek/13/ticket/SourceCodePro_Regular.json', (font) => {
// Front
FOOTER_CONTENT.map((line) => {
const textGeometry = new TextGeometry(line.text, {
font,
size: line.size,
height: 0.2,
})
const textMesh = new THREE.Mesh(textGeometry, textMaterial)
textMesh.updateMatrix()
textMesh.position.set(line.position.x, line.position.y, line.position.z)
textMesh.castShadow = true
ticketGroup.add(textMesh)
})
// Back
if (!sharePage) {
winningPhrase.flat().map((letter, index) => {
const letterGeometry = new TextGeometry(letter.toUpperCase(), {
font,
size: 2.2,
height: 0.2,
})
const letterMaterial = new THREE.MeshStandardMaterial({
color: CONFIG[ticketType].ticketForeground,
metalness: 0.2,
roughness: 0.35,
})
const backTextMesh = new THREE.Mesh(letterGeometry, letterMaterial)
backTextMesh.updateMatrix()
backTextMesh.position.set(3.9 - index * 1.8, -2, -TEXT_Z_POSITION)
backTextMesh.rotation.y = Math.PI
backTextMesh.name = `letter-${index}`
backTextMesh.material.transparent = true
backTextMesh.material.opacity = 0
ticketGroup.add(backTextMesh)
})
}
})
ticketGroup.updateMatrix()
// Environment Map
const envMapLoader = new THREE.TextureLoader()
envMapLoader.load('/images/launchweek/13/ticket/env-map.jpg', (texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping
scene.environment = texture
metalMaterial.envMap = texture
metalMaterial.envMapIntensity = IS_SECRET
? isDarkTheme
? 1.4
: 0.9
: isDarkTheme
? 2.8
: 0.75
})
// Lights
const ambientLight = new THREE.AmbientLight(
0xffffff,
IS_SECRET ? (isDarkTheme ? 1.3 : 1.2) : IS_PLATINUM ? (isDarkTheme ? 1.7 : 3.2) : 2
)
scene.add(ambientLight)
const spotLight = new THREE.SpotLight(0xffffff, isDarkTheme ? 0.8 : 0.2)
spotLight.position.z = ticketGroup.position.z + 8
spotLight.position.x = ticketGroup.position.x + 10
spotLight.angle = (Math.PI / 2) * 0.5
spotLight.castShadow = true
spotLight.lookAt(ticketGroup.position)
scene.add(spotLight)
const getTicketScreenPosition = () => {
if (!ticketRef.current) return null
const vector = new THREE.Vector3()
ticketRef.current.getWorldPosition(vector)
vector.project(camera)
const x = (vector.x * 0.5 + 0.5) * initialCanvasWidth
const y = -(vector.y * 0.5 - 0.5) * initialCanvasHeight
return { x, y }
}
const handleKeyDown = () => {
currentValue.current.split('').map((_letter, index) => {
const letterToShow = ticketGroup.getObjectByName(`letter-${index}`)
letterToShow?.traverse((child) => {
if (child) {
// @ts-ignore
child.material.opacity = 1
}
})
})
}
const animate = () => {
// Smooth rotation
if (!isDragging.current) {
ticketGroup.rotation.x += (targetRotation.current.x - ticketGroup.rotation.x) * 0.18
ticketGroup.rotation.y +=
ticketYIdleRotation + (targetRotation.current.y - ticketGroup.rotation.y) * 0.18
}
if (ticketGroup.scale.x < targetScale.current) {
scale += 0.0025
} else if (ticketGroup.scale.x > targetScale.current) {
scale -= 0.0025
} else {
scale = targetScale.current
}
ticketGroup.scale.set(scale, scale, scale)
groupYRotation.current = ticketGroup.rotation.y
handleKeyDown()
renderer.render(scene, camera)
animationFrameRef.current = requestAnimationFrame(animate)
}
// Start animation
animate()
const handleMouseMove = (e: MouseEvent) => handlePointerMove(e.clientX, e.clientY)
const handleTouchMove = (e: TouchEvent) => {
const touch = e.touches[0]
handlePointerMove(touch.clientX, touch.clientY)
}
// Tilt and drag logic
const handlePointerMove = (clientX: number, clientY: number) => {
if (!canvasRef.current) return
if (isDragging.current) {
// Update drag delta
if (dragStartX.current !== null) {
dragDelta.current = clientX - dragStartX.current
ticketGroup.rotation.y = targetRotation.current.y + dragDelta.current * 0.01
}
} else {
// Get canvas bounds
const canvasRect = canvasRef.current.getBoundingClientRect()
// Calculate center of the canvas
const centerX = canvasRect.left + canvasRect.width / 2
const centerY = canvasRect.top + canvasRect.height / 2
const ticketPosition = getTicketScreenPosition() || { x: centerX, y: centerY }
// Calculate distance from cursor to center of ticket
const deltaX = clientX - (canvasRect.left + ticketPosition.x)
const deltaY = clientY - (canvasRect.top + ticketPosition.y)
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
// Maximum distance for sensitivity calculation (diagonal of the canvas)
const maxDistance = Math.sqrt(
Math.pow(canvasRect.width / 2, 2) + Math.pow(canvasRect.height / 2, 2)
)
// Calculate sensitivity based on distance (inverse relationship)
const sensitivity = 0.002 * (1 - Math.min(distance / maxDistance, 1))
// Update target rotation with distance-based sensitivity
targetRotation.current.y =
deltaX * sensitivity +
(isFlipped.current ? (flipDirection.current < 0 ? -Math.PI : Math.PI) : 0)
targetRotation.current.x = deltaY * sensitivity * 0.3
}
}
const handleMouseDown = (e: MouseEvent) => handlePointerDown(e.clientX)
const handleTouchStart = (e: TouchEvent) => {
const touch = e.touches[0]
handlePointerDown(touch.clientX)
}
const handlePointerDown = (clientX: number) => {
isDragging.current = true
dragStartX.current = clientX
targetScale.current += SCALE_VARIATION_ON_INTERACTION
}
const handleMouseUp = () => handlePointerUp()
const handleTouchEnd = () => handlePointerUp()
const handlePointerUp = () => {
if (!isDragging.current) return
isDragging.current = false
targetScale.current -= SCALE_VARIATION_ON_INTERACTION
// Detect swipe gesture
if (Math.abs(dragDelta.current) > FLIP_DELTA) {
// Flip the ticket
isFlipped.current = !isFlipped.current
if (!userData.secret) setIsGameMode(isFlipped.current)
const sign = Math.sign(dragDelta.current)
flipDirection.current = sign
targetRotation.current.y += sign * Math.PI // Add full flip
} else {
// Reset rotation
ticketGroup.rotation.y = targetRotation.current.y
}
// Reset drag state
dragStartX.current = null
dragDelta.current = 0
}
// Handle window resize
const handleResize = () => {
const newWidth = calculateDesktopWidth()
ticketGroup.position.x = getTicketXPosition(window.innerWidth, ALIGN_RIGHT)
const tickeScale = getTicketScale(window.innerWidth)
ticketGroup.scale.set(tickeScale, tickeScale, tickeScale)
camera.aspect = newWidth / CANVAS_HEIGHT
camera.updateProjectionMatrix()
renderer.setSize(newWidth, CANVAS_HEIGHT)
}
function onKeyDown(event: KeyboardEvent) {
if (isGameMode) return
const newKey = event.key.toLocaleLowerCase()
if (!(event.metaKey || event.ctrlKey) && VALID_KEYS.includes(newKey)) {
targetScale.current += SCALE_VARIATION_ON_INTERACTION
}
setTimeout(() => {
targetScale.current = getTicketScale(window.innerWidth)
}, 100)
}
// Event listeners
window.addEventListener('resize', handleResize)
window.addEventListener('mousedown', handleMouseDown)
window.addEventListener('mouseup', handleMouseUp)
window.addEventListener('mousemove', handleMouseMove)
// window.addEventListener('mouseout', resetRotation)
window.addEventListener('touchmove', handleTouchMove)
window.addEventListener('touchstart', handleTouchStart)
window.addEventListener('touchend', handleTouchEnd)
window.addEventListener('touchcancel', resetRotation)
window.addEventListener('keydown', onKeyDown)
// Cleanup
return () => {
window.removeEventListener('resize', handleResize)
window.removeEventListener('mousedown', handleMouseDown)
window.removeEventListener('mouseup', handleMouseUp)
window.removeEventListener('mousemove', handleMouseMove)
// window.removeEventListener('mouseout', resetRotation)
window.removeEventListener('touchmove', handleTouchMove)
window.removeEventListener('touchstart', handleTouchStart)
window.removeEventListener('touchend', handleTouchEnd)
window.removeEventListener('touchcancel', resetRotation)
window.removeEventListener('keydown', onKeyDown)
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current)
}
canvasRef.current?.removeChild(renderer.domElement)
renderer.dispose()
}
}, [username, isDarkTheme, ticketType])
useEffect(() => {
currentValue.current = value
}, [value])
return (
<div
className={cn(
'w-screen absolute inset-0 lg:h-full lg:min-h-full lg:max-h-[1000px] flex justify-end items-center overflow-hidden pointer-events-none',
className
)}
>
<div ref={canvasRef} className="w-full lg:h-full" />
{isGameMode && !hasWon && !sharePage && (
<InputOTP
ref={inputRef}
maxLength={phraseLength}
pattern={REGEXP_ONLY_CHARS}
autoFocus
containerClassName={cn(
'!absolute flex justify-center w-full inset-0 bottom-[33%] top-auto lg:!invisible lg:!opacity-0',
!hasWon && 'lw13-game-input'
)}
inputMode="text"
value={value}
spellCheck={false}
onComplete={() => setGameState('winner')}
>
{winningPhrase.map((word, w_idx) => (
<>
<InputOTPGroup key={`${word}-${word.join('')}-${w_idx}`}>
{word.map((_, c_idx) => {
// index is sum of every letter of every previous word + index of current wo
const currentIndex = handleIndexCount(w_idx, c_idx)
return (
<InputOTPSlot
key={`otp-${currentIndex}`}
index={currentIndex}
className="!text-base bg-background"
/>
)
})}
</InputOTPGroup>
{w_idx !== winningPhrase.length - 1 && <InputOTPSeparator className="mx-1" />}
</>
))}
</InputOTP>
)}
</div>
)
}
export default ThreeTicketCanvas
@@ -1,105 +0,0 @@
import { useBreakpoint } from 'common'
import dayjs from 'dayjs'
import { Check } from 'lucide-react'
import { useTheme } from 'next-themes'
import Link from 'next/link'
import { useEffect, useRef, useState } from 'react'
import { Button, cn } from 'ui'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import { LW_URL, TWEET_TEXT, TWEET_TEXT_PLATINUM, TWEET_TEXT_SECRET } from '~/lib/constants'
import useLWPartyMode from '../useLWPartyMode'
export default function TicketActions() {
const { resolvedTheme } = useTheme()
const { userData, supabase } = useConfData()
const { platinum, username, metadata, secret: hasSecretTicket } = userData
const [_imgReady, setImgReady] = useState(false)
const [_loading, setLoading] = useState(false)
const isLessThanMd = useBreakpoint()
const downloadLink = useRef<HTMLAnchorElement>()
const link = `${LW_URL}/tickets/${username}?lw=13${
hasSecretTicket ? '&secret=true' : platinum ? `&platinum=true` : ''
}&t=${dayjs(new Date()).format('DHHmmss')}`
const permalink = encodeURIComponent(link)
const text = hasSecretTicket ? TWEET_TEXT_SECRET : platinum ? TWEET_TEXT_PLATINUM : TWEET_TEXT
const encodedText = encodeURIComponent(text)
const tweetUrl = `https://twitter.com/intent/tweet?url=${permalink}&text=${encodedText}`
const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${permalink}&text=${encodedText}`
const downloadUrl = `/api-v2/ticket-og?username=${encodeURIComponent(username ?? '')}`
const TICKETS_TABLE = 'tickets'
useEffect(() => {
setImgReady(false)
const img = new Image()
img.src = downloadUrl
img.onload = () => {
setImgReady(true)
setLoading(false)
if (downloadLink.current) {
downloadLink.current.click()
downloadLink.current = undefined
}
}
}, [downloadUrl])
const handleShare = async (social: 'twitter' | 'linkedin') => {
if (!supabase) return
setTimeout(async () => {
if (social === 'twitter') {
await supabase
.from(TICKETS_TABLE)
.update({
shared_on_twitter: 'now',
metadata: { ...metadata, theme: resolvedTheme, hasSharedSecret: hasSecretTicket },
})
.eq('launch_week', 'lw13')
.eq('username', username)
} else if (social === 'linkedin') {
await supabase
.from(TICKETS_TABLE)
.update({
shared_on_linkedin: 'now',
metadata: { ...metadata, theme: resolvedTheme, hasSharedSecret: hasSecretTicket },
})
.eq('launch_week', 'lw13')
.eq('username', username)
}
if (userData.shared_on_linkedin && userData.shared_on_twitter) {
await fetch(`/api-v2/ticket-og?username=${username}&platinum=true`)
}
})
}
return (
<div className="flex flex-row flex-wrap justify-start w-full gap-2 pointer-events-auto">
<Button
onClick={() => handleShare('twitter')}
type={userData.shared_on_twitter ? 'secondary' : 'default'}
icon={userData.shared_on_twitter && <Check strokeWidth={2} />}
size={isLessThanMd ? 'tiny' : 'small'}
className="px-2 lg:px-3.5 h-[28px] lg:h-[34px] flex-grow"
asChild
>
<Link href={tweetUrl} target="_blank">
{userData.shared_on_twitter ? 'Shared on Twitter' : 'Share on Twitter'}
</Link>
</Button>
<Button
onClick={() => handleShare('linkedin')}
type={userData.shared_on_linkedin ? 'secondary' : 'default'}
icon={userData.shared_on_linkedin && <Check strokeWidth={2} />}
size={isLessThanMd ? 'tiny' : 'small'}
className="px-2 lg:px-3.5 h-[28px] lg:h-[34px] flex-grow"
asChild
>
<Link href={linkedInUrl} target="_blank">
{userData.shared_on_linkedin ? 'Shared on Linkedin' : 'Share on Linkedin'}
</Link>
</Button>
</div>
)
}
@@ -1,40 +0,0 @@
import { useState, useRef } from 'react'
import { LW_URL } from '~/lib/constants'
import { Check, Copy } from 'lucide-react'
import useConfData from '../../hooks/use-conf-data'
export default function TicketCopy() {
const { userData } = useConfData()
const { username, platinum, secret } = userData
const [copied, setCopied] = useState(false)
const buttonRef = useRef<HTMLButtonElement>(null)
const hasSecretTicket = secret
const displayUrl = `.../launch-week/tickets/${username}?lw=13${
hasSecretTicket ? '&secret=true' : platinum ? `&platinum=true` : ''
}`
const url = `${LW_URL}/tickets/${username}?lw=13${
hasSecretTicket ? '&secret=true' : platinum ? `&platinum=true` : ''
}`
return (
<button
type="button"
name="Copy"
ref={buttonRef}
onClick={() => {
navigator.clipboard.writeText(url).then(() => {
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 2000)
})
}}
className="font-mono w-full flex-grow px-2 lg:px-3.5 !pr-1 py-1 rounded-md bg-alternative-200 border flex gap-2 relative text-foreground-light hover:text-foreground text-xs pointer-events-auto justify-between items-center hover:border-stronger transition-all max-w-[80vw]"
>
<span className="truncate">{displayUrl}</span>
<div className="w-6 min-w-6 h-6 flex items-center justify-center flex-shrink-0 border border-strong rounded bg-muted hover:bg-selection hover:border-stronger">
{copied ? <Check size={14} strokeWidth={3} /> : <Copy size={14} strokeWidth={1.5} />}
</div>
</button>
)
}
@@ -1,171 +0,0 @@
import { useState, useEffect } from 'react'
import { SupabaseClient } from '@supabase/supabase-js'
import { useRouter } from 'next/router'
import { LW_URL } from '~/lib/constants'
import { Button } from 'ui'
import { useKey } from 'react-use'
import { useCommandMenuOpen } from 'ui-patterns'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import { useTheme } from 'next-themes'
type FormState = 'default' | 'loading' | 'error'
export default function TicketForm() {
const { resolvedTheme } = useTheme()
const [formState, setFormState] = useState<FormState>('default')
const [realtimeChannel, setRealtimeChannel] = useState<ReturnType<
SupabaseClient['channel']
> | null>(null)
const [errorMsg] = useState('')
const { supabase, session, setUserData, setTicketState, userData } = useConfData()
const router = useRouter()
// Triggered on session
async function fetchOrCreateUser() {
if (supabase && session?.user && !userData.id) {
const username = session.user.user_metadata.user_name
const name = session.user.user_metadata.full_name
const email = session.user.email
const userId = session.user.id
if (!userData.id) {
await supabase
.from('tickets')
.insert({
user_id: userId,
launch_week: 'lw13',
email,
name,
username,
metadata: { theme: resolvedTheme },
referred_by: router.query?.referral ?? null,
})
.select()
.single()
.then(({ data, error }: any) => {
console.log(data, error, username, userData)
fetchUser({ error, username })
})
}
}
}
const fetchUser = async ({ error, username }: any) => {
if (!supabase) return
// If error because of duplicate email, ignore and proceed, otherwise sign out.
if (error && error?.code !== '23505') {
setFormState('error')
return supabase.auth.signOut()
}
const { data } = await supabase
.from('tickets_view')
.select('*')
.eq('launch_week', 'lw13')
.eq('username', username)
.single()
.throwOnError()
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}?came_from_signup=true`).catch((_) => {})
await fetch(`/api-v2/ticket-og?username=${username}`)
if (!realtimeChannel) {
const channel = supabase
.channel('changes')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'tickets',
filter: `username=eq.${username}`,
},
(payload: any) => {
const platinum = !!payload.new.shared_on_twitter && !!payload.new.shared_on_linkedin
const secret = !!payload.new.game_won_at
setUserData({
...payload.new,
platinum,
secret,
})
}
)
.subscribe()
setRealtimeChannel(channel)
}
}
useEffect(() => {
fetchOrCreateUser()
return () => {
// Cleanup realtime subscription on unmount
realtimeChannel?.unsubscribe()
}
}, [session])
const isCommandMenuOpen = useCommandMenuOpen()
useKey('t', () => !isCommandMenuOpen && handleGithubSignIn(), {}, [isCommandMenuOpen])
async function handleGithubSignIn() {
if (formState !== 'default') {
setFormState('default')
return
}
setFormState('loading')
setTicketState('loading')
const redirectTo = `${LW_URL}${userData.username ? '?referral=' + userData.username : ''}`
supabase?.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo,
},
})
}
return formState === 'error' ? (
<div className="">
<div>{errorMsg}</div>
<Button
type="secondary"
onClick={() => {
setFormState('default')
setTicketState('registration')
}}
>
Try Again
</Button>
</div>
) : (
<div className="flex flex-col gap-10 items-start justify-center relative z-20 pointer-events-auto">
<Button
size="small"
disabled={formState === 'loading' || Boolean(session)}
onClick={handleGithubSignIn}
loading={formState === 'loading'}
type="default"
className="sm:pl-1"
>
<div className="flex items-center">
<div className="relative h-6 w-6 border rounded bg-surface-75 mr-2 uppercase hidden sm:flex items-center justify-center">
T
</div>
Claim Ticket
</div>
</Button>
</div>
)
}
@@ -1,65 +0,0 @@
import React, { useEffect, useState } from 'react'
import { SupabaseClient } from '@supabase/supabase-js'
import { cn } from 'ui'
import { Dot } from 'lucide-react'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
const TicketPresence = (props: { className?: string }) => {
const { supabase, ticketState } = useConfData()
const hasTicket = ticketState === 'ticket'
const [realtimeChannel, setRealtimeChannel] = useState<ReturnType<
SupabaseClient['channel']
> | null>(null)
const [onlineUsers, setOnlineUsers] = useState<any[]>([])
const isSingular = onlineUsers.length === 1
const lowTraffic = onlineUsers.length < 5
useEffect(() => {
// Listen to realtime presence
if (!realtimeChannel && supabase) {
const lw13Room = supabase?.channel('lw13_rooms', {
config: { broadcast: { self: true, ack: true } },
})
setRealtimeChannel(lw13Room)
const userStatus = {}
lw13Room
?.on('presence', { event: 'sync' }, () => {
const newState = lw13Room.presenceState()
const users = [...Object.entries(newState).map(([_, value]) => value[0])]
const uniqueUsrs = users.filter(onlyUnique)
setOnlineUsers(uniqueUsrs)
})
.subscribe(async (status) => {
if (status !== 'SUBSCRIBED') {
return null
}
await lw13Room.track(userStatus)
})
}
return () => {
realtimeChannel?.unsubscribe()
}
}, [])
return (
<div
className={cn(
'text-foreground-lighter text-xs flex items-center transition-opacity opacity-100',
hasTicket && 'text-sm opacity-80',
props.className
)}
>
<Dot className="text-brand animate-pulse -ml-2" />
{onlineUsers.length} {isSingular ? 'person is' : 'people are'} online
</div>
)
}
function onlyUnique(value: any, index: number, array: any[]) {
return array.indexOf(value) === index
}
export default TicketPresence
@@ -1,21 +0,0 @@
import { cn } from 'ui'
import TicketActions from './TicketActions'
import TicketCopy from './TicketCopy'
interface Props {
className?: string
}
export default function TicketSwagCtaBox({ className }: Props) {
return (
<div
className={cn(
'bg-alternative/70 backdrop-blur-sm border border-overlay w-full h-auto flex flex-col gap-2 p-2 rounded-lg overflow-hidden shadow-xl',
className
)}
>
<TicketActions />
<TicketCopy />
</div>
)
}
@@ -1,168 +0,0 @@
import { AnimatePresence, LazyMotion, domAnimation, m } from 'framer-motion'
import { cn } from 'ui'
import { DEFAULT_TRANSITION, INITIAL_BOTTOM, getAnimation } from '~/lib/animations'
import { LW13_DATE } from '~/lib/constants'
import CanvasSingleMode from '~/components/LaunchWeek/13/Multiplayer/CanvasSingleMode'
import ThreeTicketCanvas from '~/components/LaunchWeek/13/ThreeTicketCanvas'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import useWinningChances from '~/components/LaunchWeek/hooks/useWinningChances'
import SectionContainer from '~/components/Layouts/SectionContainer'
import TicketForm from '~/components/LaunchWeek/13/Ticket/TicketForm'
import TicketSwagCtaBox from '~/components/LaunchWeek/13/Ticket/TicketSwagCtaBox'
import TicketPresence from './TicketPresence'
const TicketingFlow = () => {
const { ticketState, userData } = useConfData()
const isLoading = ticketState === 'loading'
const isRegistering = ticketState === 'registration'
const hasTicket = ticketState === 'ticket'
const hasPlatinumTicket = userData.platinum
const hasSecretTicket = userData.secret
const ticketType = hasSecretTicket ? 'secret' : hasPlatinumTicket ? 'platinum' : 'regular'
const transition = DEFAULT_TRANSITION
const initial = INITIAL_BOTTOM
const animate = getAnimation({ duration: 1 })
const exit = { opacity: 0, transition: { ...transition, duration: 0.2 } }
const winningChances = useWinningChances()
const DISPLAY_NAME = userData?.name || userData?.username
const FIRST_NAME = DISPLAY_NAME?.split(' ')[0]
return (
<>
<SectionContainer className="relative h-full lg:min-h-[600px] lg:max-h-[600px] flex-1 pointer-events-none">
<div className="relative z-10 flex items-center h-full">
<h1 className="sr-only">Supabase Launch Week 13 | {LW13_DATE}</h1>
<LazyMotion features={domAnimation}>
<AnimatePresence mode="wait" key={ticketState}>
{isLoading && (
<m.div
key="loading"
initial={exit}
animate={animate}
exit={exit}
className="relative w-full lg:min-h-[400px] max-h-[400px] mx-auto py-16 md:py-24 flex flex-col items-center gap-6 text-foreground"
>
<div className="hidden">
<TicketForm />
</div>
<svg
className="animate-spinner opacity-50 w-5 h-5 md:w-6 md:h-6"
width="100%"
height="100%"
viewBox="0 0 62 61"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M61 31C61 14.4315 47.5685 1 31 1C14.4315 1 1 14.4315 1 31"
stroke="white"
strokeWidth="2"
/>
</svg>
</m.div>
)}
{isRegistering && (
<m.div
key="registration"
initial={initial}
animate={animate}
exit={exit}
className={cn(
'mx-auto w-full max-w-4xl lg:min-h-[400px] max-h-[400px] h-full text-left grid md:grid-cols-2 justify-center gap-8 xl:gap-24 opacity-0 invisible',
!hasTicket && 'opacity-100 visible',
'justify-items-end'
)}
>
<div className="h-full w-full max-w-xs md:max-w-none flex flex-col justify-center gap-2 md:gap-6">
<div className="flex flex-col gap-2 md:gap-6">
<div className="flex flex-col flex-wrap gap-0">
<h2 className="text-foreground uppercase tracking-wider text-2xl">
Launch Week 13
</h2>
<p className="text-foreground-lighter uppercase tracking-wider text-xl md:text-2xl">
{LW13_DATE}
</p>
</div>
<div className="flex flex-col gap-2 text-foreground-lighter text-sm xl:text-base">
You can still claim a ticket thoughout the week.
</div>
<div className="flex flex-col md:flex-row gap-4 mt-2 md:mt-0 items-start md:items-center">
<TicketPresence className="opacity-0 invisible translate-y-1 transition-all" />
</div>
</div>
</div>
<div className="h-full min-h-[400px] aspect-[0.8/1] rounded-lg border border-dashed border-strong flex items-center justify-center bg-surface-75/25 backdrop-blur-xl">
<TicketForm />
</div>
</m.div>
)}
{hasTicket && (
<m.div
key="ticket"
initial={initial}
animate={animate}
exit={exit}
className="w-full max-w-5xl mx-auto lg:min-h-[400px] xl:pl-8 h-full flex flex-col lg:flex-row items-center lg:justify-start gap-8 md:gap-10 lg:gap-32 text-foreground text-center md:text-left"
>
<div className="order-first lg:h-full w-full max-w-md xl:max-w-2xl gap-8 flex flex-col items-center justify-center lg:items-start lg:justify-center text-center lg:text-left">
<div className="flex flex-col gap-1 max-w-md">
{/* Header */}
<div className="text-2xl">
{hasPlatinumTicket ? (
hasSecretTicket ? (
<p>Got the gold ticket, {FIRST_NAME}!</p>
) : (
<p className='font-["Departure_Mono"] uppercase'>
Good to see you, {FIRST_NAME}!
</p>
)
) : winningChances !== 2 ? (
<>
{hasSecretTicket && (
<p className='font-["Departure_Mono"] uppercase'>
{FIRST_NAME}, you're gold!
</p>
)}
{!hasSecretTicket && (
<p className='font-["Departure_Mono"] uppercase'>
Good to see you, {FIRST_NAME}!
</p>
)}
</>
) : (
<p className='font-["Departure_Mono"] uppercase'>
Good to see you, {FIRST_NAME}!
</p>
)}
</div>
</div>
<div className="flex flex-col gap-4 mt-2 md:mt-0 md:mb-12 items-center lg:items-start">
<TicketSwagCtaBox />
<TicketPresence className="opacity-0 invisible translate-y-1 transition-all" />
</div>
</div>
</m.div>
)}
</AnimatePresence>
</LazyMotion>
</div>
</SectionContainer>
<CanvasSingleMode />
{hasTicket && (
<ThreeTicketCanvas
username={DISPLAY_NAME ?? ''}
ticketType={ticketType}
className="relative -mt-44 -mb-10 lg:my-0 lg:absolute"
/>
)}
</>
)
}
export default TicketingFlow
@@ -1,11 +0,0 @@
export const themes = (isDark?: boolean) => ({
regular: {
TICKET_FOREGROUND: isDark ? '#1F1F1F' : '#FAFAFA',
},
platinum: {
TICKET_FOREGROUND: '#1F1F1F',
},
secret: {
TICKET_FOREGROUND: '#1F1F1F',
},
})
@@ -1,30 +0,0 @@
import { useEffect, useState } from 'react'
import { useKey } from 'react-use'
import { useCommandMenuOpen } from 'ui-patterns'
import useLwGame from '../hooks/useLwGame'
import useConfData from '../hooks/use-conf-data'
const useLWPartyMode = (disabled?: boolean) => {
const [isPartyMode, setIsPartyMode] = useState<boolean>(true)
const { ticketState } = useConfData()
const isCommandMenuOpen = useCommandMenuOpen()
const { isGameMode } = useLwGame()
const enableTrigger = !disabled && !isCommandMenuOpen && !isGameMode
useKey('p', () => enableTrigger && setIsPartyMode(!isPartyMode), {}, [
isPartyMode,
isCommandMenuOpen,
isGameMode,
])
useKey('Escape', () => !isGameMode && setIsPartyMode(false), {}, [isPartyMode, isGameMode])
useEffect(() => {
setIsPartyMode(isPartyMode)
}, [ticketState])
return { isPartyMode, setIsPartyMode }
}
export default useLWPartyMode
@@ -1,61 +0,0 @@
import React, { useState } from 'react'
// import TicketHeader from '../Ticket/TicketHeader'
// import TicketNumber from '../Ticket/TicketNumber'
import { UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
import Image from 'next/image'
interface Props {
user: UserData
}
export function TicketBrick({ user }: Props) {
const [isLoading, setLoading] = useState(true)
const golden = user.shared_on_linkedin && user.shared_on_twitter
// reg_bg_57.png
const baseImagePath = `https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/lw7/tickets_bg/`
const randomNumber = Math.floor(Math.random() * 200) + 1
const goldRandomNumber = Math.floor(Math.random() * 56) + 1
return (
<>
<div
key={user.name}
className="relative w-[450px] h-[230px] border border-red-100 rounded-2xl overflow-hidden"
>
<Image
src={
golden
? `${baseImagePath}/reg_bg_${randomNumber}.png`
: `${baseImagePath}/gold/gold_bg_${goldRandomNumber}.png`
}
layout="fill"
objectFit="cover"
className={[
'duration-700 ease-in-out',
isLoading ? 'grayscale blur-2xl scale-110' : 'grayscale-0 blur-0 scale-100',
].join(' ')}
onLoadingComplete={() => setLoading(false)}
alt=""
/>
<div className="z-20 relative">
{/* <TicketHeader size="small" /> */}
<div className="rounded-full grid gap-4 mx-auto justify-center mt-8">
<img
src={`https://github.com/${user.username}.png`}
alt={user.username}
className="w-20 h-20 rounded-full mx-auto"
/>
<div className="text-center">
<h3 className="gradient-text-100 text-xl">{user.name ? user.name : user.username}</h3>
{user.name && <p className="gradient-text-100 text-sm">@{user.username}</p>}
</div>
</div>
{/* <TicketNumber number={user.ticketNumber} size="small" /> */}
</div>
</div>
</>
)
}
@@ -1,51 +0,0 @@
import Link from 'next/link'
import React 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-20 pt-28 md:pt-40">
<div className="mx-auto bg-[#1C1C1C]">
<div className="max-w-[38rem] mb-8 mx-auto">
<h2 className="text-center radial-gradient-text-500">
Explore the unique tickets generated by our 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-[#1C1C1C] before:to-[#1C1C1C00] 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-[#1C1C1C] after:to-[#1C1C1C00] 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={16000} />}
</div>
<div className="flex justify-center w-full mx-auto mt-2 lg:mt-4">
<Button
asChild
type="outline"
size="medium"
onClick={() => window.scrollTo(0, 0)}
className="text-white"
>
<Link href="/launch-week/7/tickets">View all tickets</Link>
</Button>
</div>
</div>
</div>
)
}
@@ -1,93 +0,0 @@
import { useEffect, useRef, useState } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
import { useWindowSize } from 'react-use'
import { Swiper, SwiperSlide } from 'swiper/react'
import 'swiper/css'
import SwiperCore from 'swiper'
import { Autoplay } from 'swiper/modules'
SwiperCore.use([Autoplay])
interface Props {
users: UserData[]
reverse?: boolean
speed?: number
}
export function TicketBrickWallSlider({ users, reverse, speed = 10000 }: Props) {
const ref = useRef(null)
const [swiperInstance, setSwiperInstance] = useState<SwiperCore | any | null>(null)
const { width } = useWindowSize()
const STORAGE_URL = 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/lw7'
const BUCKET_FOLDER_VERSION = 'v3'
const getOgUrl = (username: string, isGold: boolean) =>
`${STORAGE_URL}/tickets/gallery/${
isGold ? 'golden' : 'regular'
}/${BUCKET_FOLDER_VERSION}/${username}.png`
useEffect(() => {
// trigger autoplay if viewport resize
if (swiperInstance) swiperInstance?.autoplay?.run()
}, [width])
return (
<div ref={ref} className="relative h-auto w-full m-0 overflow-hidden mb-2.5">
<div className="flex swiper-transition-linear ticket-brick-swiper">
<Swiper
onSwiper={(swiper) => setSwiperInstance(swiper)}
centeredSlides={true}
spaceBetween={10}
slidesPerView={5}
speed={speed}
loop={true}
watchOverflow
threshold={2}
updateOnWindowResize
allowTouchMove={false}
autoplay={{
delay: 0,
disableOnInteraction: true,
reverseDirection: reverse,
}}
breakpoints={{
320: {
slidesPerView: 2.5,
},
720: {
slidesPerView: 3.5,
},
1440: {
slidesPerView: 5,
},
}}
>
{users.map((user, i) => (
<SwiperSlide key={user.username}>
<Link
href={`/launch-week/7/tickets/${user.username}`}
key={user.username}
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]">
<Image
src={getOgUrl(user.username!, !!user.golden)}
alt={user.username ?? ''}
fill
placeholder="blur"
blurDataURL="/images/blur.png"
className="absolute inset-[1px] rounded-md md:rounded-lg object-cover object-center"
/>
</div>
</div>
</Link>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
)
}
@@ -1,3 +0,0 @@
.swiper-wrapper .swiper-slide {
width: 300px !important;
}
@@ -1,20 +0,0 @@
import React from 'react'
import useWinningChances from '../hooks/useWinningChances'
export default function ReferralIndicator() {
const winningChances = useWinningChances()
return (
<div className="bg-[#EAEAEA75] rounded-full mt-4 flex w-full">
<div
className={[
'bg-gradient-to-l from-[#A141E4] w-1/3 to-[#6300F569] flex items-center rounded-full pl-4 pr-6 py-1.5 text-xs shadow-[inset_0_0_2px_#ffffff50,0_0_1px_#00000050]',
winningChances === 2 && 'w-2/3',
winningChances === 3 && '!w-full bg-gradient-to-l from-[#E4B641] to-[#F5BF0069]',
].join(' ')}
>
<span style={{ textShadow: '0 0 4px #00000050' }}>{winningChances}x chance</span>
</div>
</div>
)
}
@@ -1,96 +0,0 @@
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/lw7'
const BUCKET_FOLDER_VERSION = 'v3'
const getOgUrl = (username: string, isGold: boolean) =>
`${STORAGE_URL}/tickets/gallery/${
isGold ? 'golden' : 'regular'
}/${BUCKET_FOLDER_VERSION}/${username}.png`
const isMobile = useBreakpoint(768)
const isTablet = useBreakpoint(1024)
return (
<div
className={[
'grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 mx-[-10vw] gap-3 py-12 relative',
].join(' ')}
>
{loadedUsers?.map((user, i) => {
const rowTickets = isMobile ? 2 : isTablet ? 3 : 5
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
return (
<Link
href={`/launch-week/7/tickets/${user.username}`}
key={`${user.username}-000${i}`}
onClick={() => window.scrollTo(0, 0)}
legacyBehavior
passHref
>
<motion.a
className="relative w-full p-[1px] rounded-lg sm:rounded-xl overflow-hidden hover:cursor-pointer bg-gradient-to-b from-[#ffffff60] to-[#ffffff10]"
initial={{ opacity: 0, y: 20, x: isOddRow ? 30 : -30 }}
animate={{
opacity: !isTablet && (i === 0 || i === 4) ? 0 : 1,
y: 0,
x: isOddRow ? 30 : -30,
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={getOgUrl(user.username!, !!user.golden)}
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-[#ffffff60] to-[#ffffff10] p-[1px] overflow-hidden">
<div className="absolute inset-[1px] rounded-lg sm:rounded-xl bg-gradient-to-b from-[#3d3c3c] to-[#2f2e2e]" />
</div>
</motion.div>
))}
</div>
)
}
@@ -1,35 +0,0 @@
import React from 'react'
import Countdown from 'react-countdown'
import { CountdownWidget } from 'ui-patterns/CountdownWidget'
const CountdownComponent = ({
date,
showCard = true,
}: {
date: string | number | Date
showCard?: boolean
}) => {
if (!date) return null
const renderer = ({ days, hours, minutes, seconds, completed }: any) => {
if (completed) {
// Render a completed state
return null
} else {
// Render countdown
return (
<CountdownWidget
days={days}
hours={hours}
minutes={minutes}
seconds={seconds}
showCard={showCard}
/>
)
}
}
return <Countdown date={new Date(date)} renderer={renderer} />
}
export default CountdownComponent
@@ -1,103 +0,0 @@
import Link from 'next/link'
import React, { useEffect, useState } from 'react'
import { Badge } from 'ui'
import useConfData from '../hooks/use-conf-data'
import { SupabaseClient } from '@supabase/supabase-js'
export interface Meetup {
id?: any
title: string
isLive: boolean
link: string
display_info: string
start_at: string
}
const LW8Meetups = ({ meetups }: { meetups?: Meetup[] }) => {
const { supabase } = useConfData()
const [meets, setMeets] = useState<Meetup[]>(meetups ?? [])
const [realtimeChannel, setRealtimeChannel] = useState<ReturnType<
SupabaseClient['channel']
> | null>(null)
useEffect(() => {
// Listen to realtime changes
if (supabase && !realtimeChannel) {
const channel = supabase
.channel('changes')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'lw8_meetups',
},
async () => {
const { data: newMeets } = await supabase.from('lw8_meetups').select('*')
setMeets(newMeets!)
}
)
.subscribe()
setRealtimeChannel(channel)
}
return () => {
// Cleanup realtime subscription on unmount
realtimeChannel?.unsubscribe()
}
}, [])
return (
<div className="max-w-6xl mx-auto grid grid-cols-1 xl:grid-cols-12 gap-8 xl:gap-8">
<div className="col-span-1 xl:col-span-4 flex flex-col justify-center max-w-lg">
<h2 className="text-2xl sm:text-3xl xl:text-4xl tracking-[-.5px]">Community meetups</h2>
<p className="text-[#9296AA] text-base py-3 xl:max-w-md">
We celebrated LW8 with our first-ever live community meetups across various locations.
</p>
</div>
<div className="col-span-1 xl:col-span-7 xl:col-start-6 w-full max-w-4xl flex flex-col justify-between items-stretch">
{meets &&
meets
?.sort((a, b) => (new Date(a.start_at) > new Date(b.start_at) ? 1 : -1))
.map(({ display_info, link, isLive, title }: Meetup) => (
<Link
key={title}
href={link ?? '#'}
target="_blank"
className={[
'w-full group py-0 flex items-center gap-2 md:gap-4 text-lg sm:text-2xl xl:text-4xl border-b border-[#111718]',
'hover:text-foreground',
isLive ? 'text-foreground-light' : 'text-[#56646B]',
!link && 'pointer-events-none',
].join(' ')}
>
<div className="flex items-center gap-2 md:gap-4 py-2 md:py-0">
<span>{title}</span>
{isLive && <Badge>Live now</Badge>}
<span className="hidden md:inline opacity-0 -translate-x-2 transition-all md:group-hover:opacity-100 group-hover:translate-x-0">
<svg
width="47"
height="47"
viewBox="0 0 47 47"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M33.0387 16.2422L40.6691 23.8727M40.6691 23.8727L33.0387 31.5031M40.6691 23.8727L6.33203 23.8727"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</div>
{display_info && <span className="text-sm text-right flex-1">{display_info}</span>}
</Link>
))}
</div>
</div>
)
}
export default LW8Meetups
@@ -1,50 +0,0 @@
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-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">
<Button
asChild
type="outline"
size="medium"
onClick={() => window.scrollTo(0, 0)}
className="text-white"
>
<Link href="/launch-week/8/tickets">View all tickets</Link>
</Button>
</div>
</div>
</div>
)
}
@@ -1,82 +0,0 @@
import Image from 'next/image'
import Link from 'next/link'
import { cn } from 'ui'
import { UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
import 'swiper/css'
import { Swiper, SwiperSlide } from 'swiper/react'
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={cn(
'flex swiper-transition-linear ticket-brick-swiper',
reverse && '-translate-x-20 w-[calc(100vw+400px)]'
)}
>
<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/8/tickets/${user.username}`}
key={user.username}
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) ??
'/images/launchweek/8/lw8-ticket-empty.jpg'
}
alt={user.username ?? ''}
fill
sizes="100%"
placeholder="blur"
blurDataURL="/images/launchweek/8/lw8-ticket-empty.jpg"
className="absolute inset-[1px] rounded-md md:rounded-lg object-cover object-center"
/>
</div>
</div>
</Link>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
)
}
@@ -1,101 +0,0 @@
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/lwx'
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) ??
'/images/launchweek/8/lw8-ticket-empty.jpg'
return (
<Link
href={`/launch-week/8/tickets/${user.username}`}
key={`${user.username}-000${i}`}
onClick={() => window.scrollTo(0, 0)}
legacyBehavior
passHref
>
<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 ?? ''}
fill
className="object-cover object-center"
placeholder="blur"
blurDataURL="/images/launchweek/8/lw8-ticket-empty.jpg"
/>
</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-[#1c1c1c] to-[#121212] 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>
)
}
@@ -1,3 +0,0 @@
.swiper-wrapper .swiper-slide {
width: 300px !important;
}
@@ -1,35 +0,0 @@
import React from 'react'
import Countdown from 'react-countdown'
import { CountdownWidget } from 'ui-patterns/CountdownWidget'
const CountdownComponent = ({
date,
showCard = true,
}: {
date: string | number | Date
showCard?: boolean
}) => {
if (!date) return null
const renderer = ({ days, hours, minutes, seconds, completed }: any) => {
if (completed) {
// Render a completed state
return null
} else {
// Render countdown
return (
<CountdownWidget
days={days}
hours={hours}
minutes={minutes}
seconds={seconds}
showCard={showCard}
/>
)
}
}
return <Countdown date={new Date(date)} renderer={renderer} />
}
export default CountdownComponent
@@ -1,183 +0,0 @@
import React, { useEffect, useRef, useState } from 'react'
import Image from 'next/image'
import { cn } from 'ui'
import useConfData from '../hooks/use-conf-data'
import { useBreakpoint } from 'common'
interface Props {
className?: string
isGameMode?: boolean
}
const LWXBackground = ({ className, isGameMode = false }: Props) => {
const { ticketState } = useConfData()
const containerRef = useRef(null)
const isMobile = useBreakpoint()
const ref = useRef(null)
const [isActive, setIsActive] = useState(false)
const [gradientPos, setGradientPos] = useState({ x: 0, y: 0 })
const hasTicket = ticketState === 'ticket'
const handleGlow = (event: any) => {
if (isMobile) return setGradientPos({ x: 70, y: 40 })
if (!ref.current || !containerRef.current || hasTicket || isGameMode) return null
const containerRefElement = containerRef.current as HTMLDivElement
const {
x: contX,
y: contY,
width: containerWidth,
height: containerHeight,
} = containerRefElement.getBoundingClientRect()
const xCont = event.clientX - contX
const yCont = event.clientY - contY
const isContainerHovered =
xCont > -3 && xCont < containerWidth + 3 && yCont > -3 && yCont < containerHeight + 3
setIsActive(isContainerHovered)
if (!isContainerHovered) return
const svgElement = ref.current as SVGElement
const { x: svgX, y: svgY } = svgElement.getBoundingClientRect()
const x = event.clientX - svgX - 50
const y = event.clientY - svgY - 50
setGradientPos({ x, y })
}
useEffect(() => {
if (typeof window === 'undefined') return
window.addEventListener('mousemove', handleGlow)
return () => {
window.removeEventListener('mousemove', handleGlow)
}
}, [ticketState, isMobile])
const gradientTransformBg = `translate(${gradientPos?.x} ${gradientPos?.y}) rotate(67.9381) scale(182.424)`
const gradientTransformStroke = `translate(${gradientPos?.x} ${gradientPos?.y}) rotate(71.6455) scale(105.635)`
return (
<div className={cn('w-full h-[620px] md:!h-[720px] !min-h-[350px] absolute', className)}>
<svg
ref={ref}
width="100%"
height="100%"
className={cn(
`
m-auto
w-[136px] h-[136px] mt-[157px]
sm:w-[142px] sm:h-[142px] sm:mt-[153px]
md:w-[247px] md:h-[247px] md:translate-y-3 md:mt-auto
xl:w-[285px] xl:h-[285px] xl:translate-y-2
2xl:w-[342px] 2xl:h-[342px] 2xl:translate-y-px
`,
isGameMode && 'hidden'
)}
viewBox="0 0 186 185"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g filter="url(#filter0_d_399_21473)">
<path
d="M134.141 10.0457H175.282V51.1867L134.141 92.3278V51.1867H92.9999L134.141 10.0457Z"
fill="url(#paint0_radial_399_21473)"
fillOpacity="0.01"
shapeRendering="crispEdges"
/>
<path
d="M51.8588 174.61L92.9999 133.469H51.8588V92.3278L10.7178 133.469V174.61H51.8588Z"
fill="url(#paint1_radial_399_21473)"
fillOpacity="0.01"
shapeRendering="crispEdges"
/>
<path
d="M51.8588 10.0457L92.9999 51.1867L134.141 92.3278L175.282 133.469V174.61H134.141L92.9999 133.469L51.8588 92.3278L10.7178 51.1867V10.0457L51.8588 10.0457Z"
fill="url(#paint2_radial_399_21473)"
fillOpacity="0.01"
shapeRendering="crispEdges"
/>
<path
d="M175.782 10.0457V9.54565H175.282H134.141H133.934L133.787 9.6921L92.9999 50.4796L52.2124 9.6921L52.0659 9.54565L51.8588 9.54565L10.7178 9.54567L10.2178 9.54567V10.0457V51.1867V51.3938L10.3642 51.5403L51.1517 92.3278L10.3642 133.115L10.2178 133.262V133.469V174.61V175.11H10.7178H51.8588H52.0659L52.2124 174.963L92.9999 134.176L133.787 174.963L133.934 175.11H134.141H175.282H175.782V174.61V133.469V133.262L175.636 133.115L134.848 92.3278L175.636 51.5403L175.782 51.3938V51.1867V10.0457ZM52.3588 132.969V93.5349L91.7928 132.969H52.3588ZM133.641 91.1207L94.207 51.6867H133.641V91.1207Z"
stroke="url(#paint3_radial_399_21473)"
strokeOpacity="0.5"
shapeRendering="crispEdges"
/>
</g>
<defs>
<radialGradient
id="paint0_radial_399_21473"
cx="0"
cy="0"
r="2"
gradientUnits="userSpaceOnUse"
gradientTransform={gradientTransformBg}
>
<stop stopColor="#FFFEFE" />
<stop offset="1" stopColor="#060809" />
</radialGradient>
<radialGradient
id="paint1_radial_399_21473"
cx="0"
cy="0"
r="2"
gradientUnits="userSpaceOnUse"
gradientTransform={gradientTransformBg}
>
<stop stopColor="#FFFEFE" />
<stop offset="1" stopColor="#060809" />
</radialGradient>
<radialGradient
id="paint2_radial_399_21473"
cx="0"
cy="0"
r="2"
gradientUnits="userSpaceOnUse"
gradientTransform={gradientTransformBg}
>
<stop stopColor="#FFFEFE" />
<stop offset="1" stopColor="#060809" />
</radialGradient>
<radialGradient
id="paint3_radial_399_21473"
cx="0"
cy="0"
r="2"
gradientUnits="userSpaceOnUse"
gradientTransform={gradientTransformStroke}
>
<stop stopColor="hsl(var(--foreground-lighter))" />
<stop offset="1" stopColor="hsl(var(--border-default))" stopOpacity="0" />
</radialGradient>
</defs>
</svg>
<Image
src="/images/launchweek/lwx/lwx_bg.svg"
alt=""
className={cn(
'absolute w-full !h-[400px] md:!h-full inset-0 mt-10 object-cover object-center overflow-visible',
isGameMode && 'opacity-30'
)}
role="presentation"
fill
priority
ref={containerRef}
/>
<Image
src="/images/launchweek/lwx/lwx_bg.svg"
alt=""
className="absolute block md:hidden pointer-events-none w-full !h-[400px] md:!h-full inset-0 mt-10 object-cover object-center overflow-visible"
role="presentation"
fill
priority
/>
</div>
)
}
export default LWXBackground
@@ -5,7 +5,6 @@ import Link from 'next/link'
import { cn } from 'ui'
import { DayLink } from '.'
import CountdownComponent from '../../Countdown'
import { WeekDayProps } from '../data'
import { Edit2 } from 'lucide-react'
@@ -117,7 +116,6 @@ const DaySection = ({ day }: { day: WeekDayProps }) => {
/>
</g>
</svg>
{day.hasCountdown && <CountdownComponent date={day.published_at} showCard={false} />}
</div>
</div>
)}
@@ -1,99 +0,0 @@
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/lwx'
const getTicketImageUrl = (username: string, isGold: boolean) =>
`${STORAGE_URL}/tickets/${isGold ? 'platinum' : 'regular'}/${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) ??
'/images/launchweek/lwx/tickets/placeholder.png'
return (
<Link
href={`/launch-week/x/tickets/${user.username}`}
key={`${user.username}-000${i}`}
onClick={() => window.scrollTo(0, 0)}
legacyBehavior
passHref
>
<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 ?? ''}
fill
sizes="100%"
className="object-cover object-center"
placeholder="blur"
blurDataURL="/images/launchweek/lwx/tickets/placeholder.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-[#1c1c1c] to-[#121212] 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>
)
}
+11 -4
View File
@@ -4,7 +4,7 @@ import { useRouter } from 'next/router'
import React, { useState } from 'react'
import { useWindowSize } from 'react-use'
import { useIsLoggedIn } from 'common'
import { useIsLoggedIn, useUser } from 'common'
import { Button, buttonVariants, cn } from 'ui'
import {
NavigationMenu,
@@ -23,6 +23,8 @@ import MenuItem from './MenuItem'
import MobileMenu from './MobileMenu'
import RightClickBrandLogo from './RightClickBrandLogo'
import { useSendTelemetryEvent } from '~/lib/telemetry'
import useDropdownMenu from './useDropdownMenu'
import { AuthenticatedDropdownMenu } from 'ui-patterns'
interface Props {
hideNavbar: boolean
@@ -37,6 +39,8 @@ const Nav = ({ hideNavbar, stickyNavbar = true }: Props) => {
const isLoggedIn = useIsLoggedIn()
const menu = getMenu()
const sendTelemetryEvent = useSendTelemetryEvent()
const user = useUser()
const userMenu = useDropdownMenu(user)
const isHomePage = router.pathname === '/'
const isLaunchWeekPage = router.pathname.includes('/launch-week')
@@ -130,9 +134,12 @@ const Nav = ({ hideNavbar, stickyNavbar = true }: Props) => {
<GitHubButton />
{isLoggedIn ? (
<Button className="hidden lg:block" asChild>
<Link href="/dashboard/projects">Dashboard</Link>
</Button>
<>
<Button className="hidden lg:block" asChild>
<Link href="/dashboard/projects">Dashboard</Link>
</Button>
<AuthenticatedDropdownMenu menu={userMenu} user={user} site="www" />
</>
) : (
<>
<Button type="default" className="hidden lg:block" asChild>
@@ -0,0 +1,62 @@
'use client'
import type { User } from '@supabase/supabase-js'
import { Command, Database, LogOut, Settings, UserIcon } from 'lucide-react'
import { logOut } from 'common'
import type { menuItem } from 'ui-patterns/AuthenticatedDropdownMenu'
import { useSetCommandMenuOpen } from 'ui-patterns'
import { useRouter } from 'next/router'
const useDropdownMenu = (user: User | null) => {
const router = useRouter()
const setCommandMenuOpen = useSetCommandMenuOpen()
const menu: menuItem[][] = [
[
{
label: user?.email ?? "You're logged in",
type: 'text',
icon: UserIcon,
},
{
label: 'Account Preferences',
icon: Settings,
href: 'https://supabase.com/dashboard/account/me',
},
{
label: 'All Projects',
icon: Database,
href: 'https://supabase.com/dashboard/projects',
},
{
label: 'Command Menu',
icon: Command,
type: 'button',
onClick: () => setCommandMenuOpen(true),
shortcut: '⌘K',
},
],
[
{
label: 'Theme',
type: 'theme',
},
],
[
{
label: 'Logout',
type: 'button',
icon: LogOut,
onClick: async () => {
await logOut()
router.reload()
},
},
],
]
return menu
}
export default useDropdownMenu
-1
View File
@@ -45,7 +45,6 @@ export const LWX_DATE = 'Dec 11-15 / 8am PT'
export const LWX_LAUNCH_DATE = '2023-12-11T08:00:00.000-07:00'
export const LW11_DATE = 'April 15-19 / 7am PT'
export const LW11_LAUNCH_DATE = '2024-04-15T07:00:00.000-07:00'
export const LW11_LAUNCH_DATE_END = '2024-04-21T23:59:59.000-07:00'
export const LW12_DATE = 'August 12-16 / 7am PT'
export const LW12_LAUNCH_DATE = '2024-08-12T07:00:00.000-07:00'
export const LW12_LAUNCH_DATE_END = '2024-08-18T23:59:59.000-07:00'
Loaded 100 of 111 files, more files were not shown because too many files have changed in this diff. Show more