diff --git a/app/layout.tsx b/app/layout.tsx index 2f1c5f5..35221a0 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,6 +5,8 @@ import Background from '@/components/background'; import { ThemeProvider } from '@/components/theme-provider'; import VoidAnimatedCursor from '@/components/void-animated-cursor'; import { Toaster } from '@/components/ui/toaster'; +import { TooltipProvider } from '@/components/ui/tooltip'; +import { cn } from '@/lib/utils'; const inter = Inter({ subsets: ['latin'] }); @@ -21,7 +23,7 @@ export default function RootLayout({ }>) { return ( - + - {children} + {children} diff --git a/components/poll-section.tsx b/components/poll-section.tsx index 0ee7cb2..5a32b25 100644 --- a/components/poll-section.tsx +++ b/components/poll-section.tsx @@ -1,15 +1,25 @@ -import { useCallback, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import UrlInput from './url-input'; import { vidParser } from '@/lib/vid-parser'; -import { LiveMetadata } from '@/types/liveChat'; +import { LiveMetadata, MessageData, PollUserData } from '@/types/liveChat'; import { useLiveChat } from '@/hooks/use-livechat'; import { useToast } from './ui/use-toast'; -import Image from 'next/image'; import { Label } from './ui/label'; import { Card, CardContent, CardHeader, CardTitle } from './ui/card'; import { Input } from './ui/input'; import { Button } from './ui/button'; import { ArrowBigRightDashIcon, PlayIcon, StopCircleIcon } from 'lucide-react'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog'; import { Bar } from 'react-chartjs-2'; import { faker } from '@faker-js/faker'; import { @@ -24,6 +34,8 @@ import { import { randomRGBAColor } from '@/lib/random-rgba-color'; import LiveStreamMetadataCard from './livestream-metadata-card'; import Spinner from './spinner'; +import { defaultBaseInterval, isNumeric } from '@/lib/utils'; +import dayjs from 'dayjs'; interface UrlInputSectionProps { currentPassphrase: string; @@ -41,7 +53,6 @@ ChartJS.register( ); const PollSection = ({ currentPassphrase }: UrlInputSectionProps) => { - // section 1 const { toast } = useToast(); const [isLoading, setIsLoading] = useState(false); const [isReady, setIsReady] = useState(false); @@ -51,49 +62,61 @@ const PollSection = ({ currentPassphrase }: UrlInputSectionProps) => { const [activeChatMessageId, setActiveChatMessageId] = useState< string | undefined >(); - const { fetchLiveChatMessage, fetchLiveStreamingDetails, extractMessage } = - useLiveChat(currentPassphrase); + const [liveMetadata, setLiveMetadata] = useState(); const [pollStatus, setPollStatus] = useState('prepare'); + const pollStatusRef = useRef(pollStatus); + pollStatusRef.current = pollStatus; + const barChartRef = useRef>(null); + const inputRef = useRef(null); const [numOfOptions, setNumOfOptions] = useState(0); + const [barColor, setBarColor] = useState<{ + bar: string[]; + border: string[]; + }>({ bar: [], border: [] }); const handleTerminateProcess = useCallback(async () => { setIsReady(false); }, []); + const [pollData, setPollData] = useState({}); + const [pollStartDate, setPollStartDate] = useState(); + + const { fetchLiveChatMessage, fetchLiveStreamingDetails, extractMessage } = + useLiveChat(currentPassphrase); + const handleUrlSubmit = useCallback(async () => { // start / stop if (isReady) { - // onOpen(); await handleTerminateProcess(); - } else { - setIsLoading(true); - // check live url vid - const vid = vidParser(urlInputValue); - if (vid == null || vid.length === 0) { - toast({ - title: '🚨 Oops...', - description: 'Invalid youtube live url format', - }); - setIsLoading(false); - return; - } - - // check vid is correct - const result = await fetchLiveStreamingDetails(vid); - if (!result.success) { - setIsLoading(false); - toast({ title: '🚨 Oops...', description: result.message }); - return; - } - - setActiveChatMessageId(result.activeLiveChatId); - setLiveMetadata({ title: result.title, thumbnail: result.thumbnail }); + return; + } + setIsLoading(true); + // check live url vid + const vid = vidParser(urlInputValue); + if (vid == null || vid.length === 0) { + toast({ + title: '🚨 Oops...', + description: 'Invalid youtube live url format', + }); + setIsLoading(false); + return; + } - // all green, reset any error flag - setIsReady(true); + // check vid is correct + const result = await fetchLiveStreamingDetails(vid); + if (!result.success) { setIsLoading(false); + toast({ title: '🚨 Oops...', description: result.message }); + return; } + + setActiveChatMessageId(result.activeLiveChatId); + setLiveMetadata({ title: result.title, thumbnail: result.thumbnail }); + + // all green, reset any error flag + setIsReady(true); + setIsLoading(false); }, [ fetchLiveStreamingDetails, handleTerminateProcess, @@ -102,19 +125,160 @@ const PollSection = ({ currentPassphrase }: UrlInputSectionProps) => { urlInputValue, ]); - const chartData = useMemo(() => { - // assume 10 - const labels = Array.from(Array(numOfOptions).keys()).map((i) => i + 1); + const intervalLiveChatMessage = useCallback( + async (chatId: string, nextToken?: string) => { + if (!readyRef.current || pollStatusRef.current !== 'start') { + Promise.resolve(); + return; + } + const d = await fetchLiveChatMessage(chatId, nextToken); + if (!d.success) { + setIsLoading(false); + toast({ title: '🚨 Oops...', description: d.message }); + return; + } + + const pollingMs = d.pollingIntervalMillis + defaultBaseInterval; + const nextPageToken = d.nextPageToken; + + const newData: MessageData[] = d.items.map((it: any) => ({ + key: it.id, + uid: it.authorDetails.channelId, + name: it.authorDetails.displayName, + pic: it.authorDetails.profileImageUrl, + message: extractMessage(it), + type: it.snippet.type, + time: it.snippet.publishedAt, + isChatOwner: it.authorDetails.isChatOwner, + isChatSponsor: it.authorDetails.isChatSponsor, + isChatModerator: it.authorDetails.isChatModerator, + })); + + const existedPollData = pollData; + // filter out old message + const latestData = newData.filter((it) => + dayjs(it.time).isAfter(pollStartDate) + ); + + latestData.map((it) => { + if (isNumeric(it.message)) { + // within valid range? + const value = +it.message; + if (value > 0 && value <= numOfOptions) { + existedPollData[it.uid] = value; + } + } + }); + + const data = new Array(numOfOptions).fill(0); + Object.values(existedPollData).forEach((v) => { + if (v > 0 && v <= numOfOptions) { + data[v - 1]++; + } + }); + if (barChartRef.current) { + barChartRef.current.data.datasets[0].data = data; + barChartRef.current.update(); + } + + setPollData(existedPollData); + + setTimeout(async () => { + await intervalLiveChatMessage(chatId, nextPageToken); + }, pollingMs); + }, + [ + extractMessage, + fetchLiveChatMessage, + numOfOptions, + pollData, + pollStartDate, + toast, + ] + ); + + // NOTE: start fetching message when poll start + useEffect(() => { + if (!isReady || activeChatMessageId == null || pollStatus !== 'start') + return; + (async () => { + await intervalLiveChatMessage(activeChatMessageId); + })(); + }, [ + activeChatMessageId, + extractMessage, + fetchLiveChatMessage, + intervalLiveChatMessage, + isReady, + pollStatus, + ]); + + const sortChartResult = useCallback(() => { + const data = new Array(numOfOptions).fill(0); + + // MOCKData: {'Sawa': 5, 'userA': 5, 'Sam': 1, 'userB': 5, 'userC': 2} + Object.values(pollData).forEach((v) => { + if (v > 0 && v <= numOfOptions) { + data[v - 1]++; + } + }); + + const arrayOfObj = Array.from(Array(numOfOptions).keys()) + .map((i) => i + 1) + .map((value, index) => { + return { + label: value, + data: data[index] || 0, + borderColor: barColor.border[index], + backgroundColor: barColor.bar[index], + }; + }); + + const sortedArrayOfObj = arrayOfObj.sort((a, b) => + a.data === b.data ? 0 : a.data > b.data ? -1 : 1 + ); + + const newArrayLabel: string[] = []; + const newArrayData: number[] = []; + const newBarColor: string[] = []; + const newBorderColor: string[] = []; + sortedArrayOfObj.forEach(function (d, index) { + if (index === 0) { + const highestVoteLabel = `👑${d.label}`; + newArrayLabel.push(highestVoteLabel); + } else { + newArrayLabel.push(`${d.label}`); + } + newArrayData.push(d.data); + newBarColor.push(d.backgroundColor); // seems buggy here + newBorderColor.push(d.borderColor); + }); + + if (barChartRef.current) { + barChartRef.current.data.datasets[0].data = newArrayData; + barChartRef.current.data.datasets[0].backgroundColor = newBarColor; + barChartRef.current.data.datasets[0].borderColor = newBorderColor; + barChartRef.current.data.labels = newArrayLabel; + barChartRef.current.update(); + } + }, [barColor?.bar, barColor?.border, numOfOptions, pollData]); + + const chartInitData = useMemo(() => { + const labels = Array.from(Array(numOfOptions).keys()).map( + (i) => `${i + 1}` + ); const color = randomRGBAColor(numOfOptions); - console.log('label', labels); + setBarColor(color); + return { labels, datasets: [ { label: 'Poll', - data: labels.map(() => faker.number.int({ min: 0, max: 300 })), + data: [], backgroundColor: color.bar, borderColor: color.border, + maxBarThickness: 24, }, ], }; @@ -164,10 +328,11 @@ const PollSection = ({ currentPassphrase }: UrlInputSectionProps) => { { - setNumOfOptions(+event.target.value); - console.log(+event.target.value); + setNumOfOptions(Math.abs(+event.target.value)); }} disabled={pollStatus !== 'prepare'} /> @@ -175,7 +340,17 @@ const PollSection = ({ currentPassphrase }: UrlInputSectionProps) => { className='mt-8 flex w-32 self-end' disabled={pollStatus !== 'prepare'} onClick={() => { + // simple validation + if (numOfOptions === 0) { + toast({ + title: '🚨 Oops...', + description: + 'Require to fill in valid number of options', + }); + return; + } setPollStatus('start'); + setPollStartDate(dayjs()); }} > Start Poll @@ -187,17 +362,27 @@ const PollSection = ({ currentPassphrase }: UrlInputSectionProps) => { {pollStatus !== 'prepare' && ( - - 2️⃣ Retrieving response - + {pollStatus === 'start' && ( + + 2️⃣ Retrieving response + + )} + {pollStatus === 'stop' && 3️⃣ End} - + {pollStatus === 'start' && ( )} {pollStatus === 'stop' && ( - + + + + + + + Confirmation + + Creating next new poll will discard current poll + records. This webapp will not keep the poll records + and this action cannot be undone. + + + + Cancel + { + setPollStatus('prepare'); + setPollData({}); + setNumOfOptions(0); + if (inputRef.current) { + inputRef.current.value = ''; + } + }} + > + Continue + + + + )} diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..5cba559 --- /dev/null +++ b/components/ui/alert-dialog.tsx @@ -0,0 +1,141 @@ +'use client'; + +import * as React from 'react'; +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; + +import { cn } from '@/lib/utils'; +import { buttonVariants } from '@/components/ui/button'; + +const AlertDialog = AlertDialogPrimitive.Root; + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; + +const AlertDialogPortal = AlertDialogPrimitive.Portal; + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +AlertDialogHeader.displayName = 'AlertDialogHeader'; + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +AlertDialogFooter.displayName = 'AlertDialogFooter'; + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName; + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/components/url-input.tsx b/components/url-input.tsx index 508d68a..dc867f9 100644 --- a/components/url-input.tsx +++ b/components/url-input.tsx @@ -5,6 +5,11 @@ import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { ChevronRightIcon } from 'lucide-react'; import { Input, InputProps } from '@/components/ui/input'; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip'; import { cn } from '@/lib/utils'; import Spinner from './spinner'; @@ -43,7 +48,14 @@ const UrlInput = forwardRef( {isLoading ? ( ) : isReady ? ( - + + + + + +

Terminate current process

+
+
) : ( )} diff --git a/lib/random-rgba-color.ts b/lib/random-rgba-color.ts index 828410c..dec60f7 100644 --- a/lib/random-rgba-color.ts +++ b/lib/random-rgba-color.ts @@ -4,14 +4,16 @@ export const randomRGBAColor = (n: number) => { const colors = []; const borderColor = []; - for (let i = 0; i < n; i += 1) { - const randAttrColor = randomColor({ format: 'rgba', alpha: 0.5 }); + const randAttrColor = randomColor({ + format: 'rgba', + alpha: 0.5, + hue: 'red', + luminosity: 'light', + count: n, + }); - const colorString = randAttrColor.replace('0.5', '1.0'); - const borderColorString = randAttrColor; - colors.push(colorString); - borderColor.push(borderColorString); - } + const colorString = randAttrColor.map((it) => it.replace('0.5', '1.0')); + const borderColorString = randAttrColor; - return { bar: colors, border: borderColor }; + return { bar: colorString, border: borderColorString }; }; diff --git a/lib/utils.ts b/lib/utils.ts index 9ad0df4..ca72f51 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,6 +1,13 @@ +import { PollUserData } from '@/types/liveChat'; import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +export const defaultBaseInterval = 200; + +export const isNumeric = (value: string) => { + return !isNaN(parseFloat(value)) && isFinite(value as any); +}; diff --git a/package-lock.json b/package-lock.json index b15aac7..38cf728 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@faker-js/faker": "^8.4.1", + "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-slot": "^1.0.2", @@ -17,6 +18,7 @@ "chart.js": "^4.4.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "dayjs": "^1.11.10", "lucide-react": "^0.343.0", "next": "14.1.0", "next-themes": "^0.2.1", @@ -501,6 +503,34 @@ "@babel/runtime": "^7.13.10" } }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.5.tgz", + "integrity": "sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", @@ -610,6 +640,42 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", + "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dismissable-layer": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", @@ -637,6 +703,48 @@ } } }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", + "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", + "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-id": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", @@ -1290,6 +1398,17 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz", + "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -1828,6 +1947,11 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1894,6 +2018,11 @@ "node": ">=6" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2769,6 +2898,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/get-symbol-description": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", @@ -3069,6 +3206,14 @@ "node": ">= 0.4" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -4435,6 +4580,73 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.5.tgz", + "integrity": "sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==", + "dependencies": { + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", + "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "dependencies": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -5335,6 +5547,47 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.1.tgz", + "integrity": "sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", + "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 3612d46..4aed50e 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@faker-js/faker": "^8.4.1", + "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-slot": "^1.0.2", @@ -20,6 +21,7 @@ "chart.js": "^4.4.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "dayjs": "^1.11.10", "lucide-react": "^0.343.0", "next": "14.1.0", "next-themes": "^0.2.1", diff --git a/types/liveChat.ts b/types/liveChat.ts index c30b167..3b4de3a 100644 --- a/types/liveChat.ts +++ b/types/liveChat.ts @@ -2,3 +2,26 @@ export interface LiveMetadata { title: string; thumbnail: string; } + +export type MessageType = + | 'membershipGiftingEvent' + | 'superChatEvent' + | 'memberMilestoneChatEvent' + | 'textMessageEvent' + | 'giftMembershipReceivedEvent' + | 'newSponsorEvent'; + +export interface MessageData { + key: string; + uid: string; + name: string; + message: string; + type: MessageType; + pic: string; + time: string; + isChatOwner: boolean; // channel owner + isChatSponsor: boolean; // channel membership + isChatModerator: boolean; // channel mod +} + +export type PollUserData = Record;