mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 06:09:08 +03:00
Merge branch 'master' into feat/column-order-refactored
This commit is contained in:
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 =
|
||||
|
||||
@@ -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'
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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,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,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,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
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
import { LOCAL_STORAGE_KEYS } from './constants'
|
||||
import { LOCAL_STORAGE_KEYS } from 'common'
|
||||
import { makeRandomString } from './helpers'
|
||||
|
||||
const GITHUB_INTEGRATION_APP_NAME =
|
||||
|
||||
@@ -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,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'
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
Reference in new issue
Block a user