init commit,
This commit is contained in:
47
99_references/supabase-realtime-demo/components/Chatbox.tsx
Normal file
47
99_references/supabase-realtime-demo/components/Chatbox.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
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 max-h-[235px] overflow-y-scroll">
|
||||
<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
|
140
99_references/supabase-realtime-demo/components/Cursor.tsx
Normal file
140
99_references/supabase-realtime-demo/components/Cursor.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { FC, FormEvent, useEffect, useRef, useState } from 'react'
|
||||
|
||||
interface Props {
|
||||
x?: number
|
||||
y?: number
|
||||
color: string
|
||||
hue: string
|
||||
message: string
|
||||
isTyping: boolean
|
||||
isCancelled?: boolean
|
||||
isLocalClient?: boolean
|
||||
onUpdateMessage?: (message: string) => void
|
||||
}
|
||||
|
||||
const MAX_MESSAGE_LENGTH = 70
|
||||
const MAX_DURATION = 4000
|
||||
const MAX_BUBBLE_WIDTH_THRESHOLD = 280 + 50
|
||||
const MAX_BUBBLE_HEIGHT_THRESHOLD = 40 + 50
|
||||
|
||||
const Cursor: FC<Props> = ({
|
||||
x,
|
||||
y,
|
||||
color,
|
||||
hue,
|
||||
message,
|
||||
isTyping,
|
||||
isCancelled,
|
||||
isLocalClient,
|
||||
onUpdateMessage = () => {},
|
||||
}) => {
|
||||
// Don't show cursor for the local client
|
||||
const _isLocalClient = !x || !y || isLocalClient
|
||||
const inputRef = useRef() as any
|
||||
const timeoutRef = useRef() as any
|
||||
const chatBubbleRef = useRef() as any
|
||||
|
||||
const [flipX, setFlipX] = useState(false)
|
||||
const [flipY, setFlipY] = 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(() => {
|
||||
// [Joshen] Experimental: dynamic flipping to ensure that chat
|
||||
// bubble always stays within the viewport, comment this block
|
||||
// out if the effect seems weird.
|
||||
setFlipX((x || 0) + MAX_BUBBLE_WIDTH_THRESHOLD >= window.innerWidth)
|
||||
setFlipY((y || 0) + MAX_BUBBLE_HEIGHT_THRESHOLD >= window.innerHeight)
|
||||
}, [x, y, isTyping, chatBubbleRef])
|
||||
|
||||
return (
|
||||
<>
|
||||
{!_isLocalClient && (
|
||||
<svg
|
||||
width="18"
|
||||
height="24"
|
||||
viewBox="0 0 18 24"
|
||||
fill="none"
|
||||
className="absolute top-0 left-0 transform transition pointer-events-none"
|
||||
style={{ color, transform: `translateX(${x}px) translateY(${y}px)` }}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.717 2.22918L15.9831 15.8743C16.5994 16.5083 16.1503 17.5714 15.2661 17.5714H9.35976C8.59988 17.5714 7.86831 17.8598 7.3128 18.3783L2.68232 22.7C2.0431 23.2966 1 22.8434 1 21.969V2.92626C1 2.02855 2.09122 1.58553 2.717 2.22918Z"
|
||||
fill={color}
|
||||
stroke={hue}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
<div
|
||||
ref={chatBubbleRef}
|
||||
className={[
|
||||
'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'}`,
|
||||
].join(' ')}
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
transform: `translateX(${
|
||||
(x || 0) + (flipX ? -chatBubbleRef.current.clientWidth - 20 : 20)
|
||||
}px) translateY(${(y || 0) + (flipY ? -chatBubbleRef.current.clientHeight - 20 : 20)}px)`,
|
||||
}}
|
||||
>
|
||||
{_isLocalClient && !hideInput ? (
|
||||
<>
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={message}
|
||||
className="w-full outline-none bg-transparent border-none text-white"
|
||||
onChange={(e: FormEvent<HTMLInputElement>) => {
|
||||
const text = e.currentTarget.value
|
||||
if (text.length <= MAX_MESSAGE_LENGTH) onUpdateMessage(e.currentTarget.value)
|
||||
}}
|
||||
/>
|
||||
<p className="text-xs" style={{ color: hue }}>
|
||||
{message.length}/{MAX_MESSAGE_LENGTH}
|
||||
</p>
|
||||
</>
|
||||
) : message.length ? (
|
||||
<div className="truncate text-white">{message}</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="loader-dots block relative h-6 w-10">
|
||||
<div className="absolute top-0 my-2.5 w-1.5 h-1.5 rounded-full bg-white opacity-75"></div>
|
||||
<div className="absolute top-0 my-2.5 w-1.5 h-1.5 rounded-full bg-white opacity-75"></div>
|
||||
<div className="absolute top-0 my-2.5 w-1.5 h-1.5 rounded-full bg-white opacity-75"></div>
|
||||
<div className="absolute top-0 my-2.5 w-1.5 h-1.5 rounded-full bg-white opacity-75"></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Cursor
|
@@ -0,0 +1,71 @@
|
||||
import { IconSun, IconMoon } from '@supabase/ui'
|
||||
import { useEffect } from 'react'
|
||||
import { useTheme } from '../lib/ThemeProvider'
|
||||
|
||||
function DarkModeToggle() {
|
||||
const { isDarkMode, toggleTheme } = useTheme()
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
localStorage.setItem('supabaseDarkMode', (!isDarkMode).toString())
|
||||
toggleTheme()
|
||||
|
||||
const key = localStorage.getItem('supabaseDarkMode')
|
||||
document.documentElement.className = key === 'true' ? 'dark' : ''
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const key = localStorage.getItem('supabaseDarkMode')
|
||||
if (key && key == 'false') {
|
||||
document.documentElement.className = ''
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed="false"
|
||||
className={`
|
||||
relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer
|
||||
transition-colors ease-in-out duration-200 focus:outline-none ${
|
||||
isDarkMode
|
||||
? 'bg-scale-500 hover:bg-scale-700'
|
||||
: 'bg-scale-900 hover:bg-scale-1100'
|
||||
}
|
||||
`}
|
||||
onClick={() => toggleDarkMode()}
|
||||
>
|
||||
<span className="sr-only">Toggle Themes</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`
|
||||
relative
|
||||
${
|
||||
isDarkMode ? 'translate-x-5' : 'translate-x-0'
|
||||
} inline-block h-5 w-5 rounded-full
|
||||
bg-white dark:bg-scale-300 shadow-lg transform ring-0 transition ease-in-out duration-200
|
||||
`}
|
||||
>
|
||||
<IconSun
|
||||
className={
|
||||
'absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-scale-900 ' +
|
||||
(!isDarkMode ? 'opacity-100' : 'opacity-0')
|
||||
}
|
||||
strokeWidth={2}
|
||||
size={12}
|
||||
/>
|
||||
<IconMoon
|
||||
className={
|
||||
'absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-scale-900 ' +
|
||||
(isDarkMode ? 'opacity-100' : 'opacity-0')
|
||||
}
|
||||
strokeWidth={2}
|
||||
size={14}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DarkModeToggle
|
12
99_references/supabase-realtime-demo/components/Loader.tsx
Normal file
12
99_references/supabase-realtime-demo/components/Loader.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
const Loader = () => {
|
||||
return (
|
||||
<div className="bg-scale-200 h-screen w-screen flex flex-col items-center justify-center space-y-4">
|
||||
<span className="flex h-5 w-5 relative">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-900 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-full w-full bg-green-900" />
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loader
|
38
99_references/supabase-realtime-demo/components/Users.tsx
Normal file
38
99_references/supabase-realtime-demo/components/Users.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { FC } from 'react'
|
||||
import { User } from '../types'
|
||||
|
||||
interface Props {
|
||||
users: Record<string, User>
|
||||
}
|
||||
|
||||
const Users: FC<Props> = ({ users }) => {
|
||||
return (
|
||||
<div className="relative">
|
||||
{Object.entries(users).map(([userId, userData], idx) => {
|
||||
return (
|
||||
<div key={userId} className="relative">
|
||||
<div
|
||||
key={userId}
|
||||
className={[
|
||||
'transition-all absolute right-0 h-8 w-8 bg-scale-1200 rounded-full bg-center bg-[length:50%_50%]',
|
||||
'bg-no-repeat shadow-md flex items-center justify-center',
|
||||
].join(' ')}
|
||||
style={{
|
||||
border: `1px solid ${userData.hue}`,
|
||||
background: userData.color,
|
||||
transform: `translateX(${Math.abs(idx - (Object.keys(users).length - 1)) * -20}px)`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ background: userData.color }}
|
||||
className="w-7 h-7 animate-ping-once rounded-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Users
|
@@ -0,0 +1,180 @@
|
||||
import { FC, useState, memo } from 'react'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
IconMinimize2,
|
||||
IconMaximize2,
|
||||
IconGitHub,
|
||||
IconTwitter,
|
||||
} from '@supabase/ui'
|
||||
import supabaseClient from '../client'
|
||||
import { useTheme } from '../lib/ThemeProvider'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const WaitlistPopover: FC<Props> = ({}) => {
|
||||
const { isDarkMode } = useTheme()
|
||||
const [isExpanded, setIsExpanded] = useState(true)
|
||||
const [isSuccess, setIsSuccess] = useState(false)
|
||||
const [error, setError] = useState<any>()
|
||||
|
||||
const initialValues = { email: '' }
|
||||
|
||||
const getGeneratedTweet = () => {
|
||||
return `Join me to experience Realtime by Supabase!%0A%0A${window.location.href}`
|
||||
}
|
||||
|
||||
const onValidate = (values: any) => {
|
||||
const errors = {} as any
|
||||
const emailValidateRegex =
|
||||
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
|
||||
if (!emailValidateRegex.test(values.email)) errors.email = 'Please enter a valid email'
|
||||
return errors
|
||||
}
|
||||
|
||||
const onSubmit = async (values: any, { setSubmitting, resetForm }: any) => {
|
||||
setIsSuccess(false)
|
||||
setError(undefined)
|
||||
setSubmitting(true)
|
||||
const { error } = await supabaseClient.from('waitlist').insert([{ email: values.email }])
|
||||
if (!error) {
|
||||
resetForm()
|
||||
setIsSuccess(true)
|
||||
} else {
|
||||
setError(error)
|
||||
}
|
||||
setSubmitting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-scale-200 border border-scale-500 dark:border-scale-300 p-6 rounded-md w-[400px] space-y-8 transition-all ${
|
||||
isExpanded ? 'max-h-[600px]' : 'max-h-[70px]'
|
||||
} duration-500 overflow-hidden shadow-2xl dark:shadow-lg`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<Image
|
||||
src={isDarkMode ? `/img/supabase-dark.svg` : `/img/supabase-light.svg`}
|
||||
alt="supabase"
|
||||
height={20}
|
||||
width={100}
|
||||
/>
|
||||
<div
|
||||
className={`transition relative -top-[1px] ${
|
||||
!isExpanded ? 'opacity-100' : 'opacity-0'
|
||||
} space-x-2 flex items-center`}
|
||||
>
|
||||
<p className={`transition-all text-scale-900 text-sm ${isExpanded ? '-ml-2' : 'ml-0'}`}>
|
||||
/
|
||||
</p>
|
||||
<p
|
||||
className={`transition-all text-scale-1200 text-sm ${isExpanded ? '-ml-2' : 'ml-0'}`}
|
||||
>
|
||||
Realtime
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded ? (
|
||||
<IconMinimize2
|
||||
className="transition-all text-scale-900 cursor-pointer hover:text-scale-1200 hover:scale-105"
|
||||
strokeWidth={2}
|
||||
size={16}
|
||||
onClick={() => setIsExpanded(false)}
|
||||
/>
|
||||
) : (
|
||||
<IconMaximize2
|
||||
className="transition-all text-scale-900 cursor-pointer hover:text-scale-1200 hover:scale-105"
|
||||
strokeWidth={2}
|
||||
size={16}
|
||||
onClick={() => setIsExpanded(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-scale-1200 text-2xl">Realtime</h1>
|
||||
</div>
|
||||
<p className="text-sm text-scale-900">
|
||||
Realtime collaborative app to display broadcast, presence, and database listening over
|
||||
WebSockets
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://www.producthunt.com/posts/realtime-multiplayer-by-supabase?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-realtime-multiplayer-by-supabase"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=339695&theme=dark"
|
||||
alt="Realtime Multiplayer by Supabase - Easily build real-time apps that enables user collaboration | Product Hunt"
|
||||
style={{ width: '250px', height: '54px' }}
|
||||
width="250"
|
||||
height="54"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="https://github.com/supabase/realtime" passHref>
|
||||
<Button as="a" type="default" icon={<IconGitHub />}>
|
||||
View on GitHub
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href={`https://twitter.com/intent/tweet?text=${getGeneratedTweet()}`} passHref>
|
||||
<Button as="a" type="alternative" icon={<IconTwitter />}>
|
||||
Invite on Twitter
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form validateOnBlur initialValues={initialValues} validate={onValidate} onSubmit={onSubmit}>
|
||||
{({ isSubmitting }: any) => {
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
size="small"
|
||||
placeholder="example@email.com"
|
||||
autoComplete="off"
|
||||
actions={[
|
||||
<Button
|
||||
className="mr-0.5"
|
||||
key="submit"
|
||||
htmlType="submit"
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Get early access
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
{isSuccess && (
|
||||
<p className="text-sm text-green-1000 mt-2">
|
||||
Thank you for submitting your interest!
|
||||
</p>
|
||||
)}
|
||||
{error?.message.includes('duplicate key') && (
|
||||
<p className="text-sm text-red-900 mt-2">
|
||||
Email has already been registered for waitlist
|
||||
</p>
|
||||
)}
|
||||
{error && !error?.message.includes('duplicate key') && (
|
||||
<p className="text-sm text-red-900 mt-2">Unable to register email for waitlist</p>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(WaitlistPopover)
|
Reference in New Issue
Block a user