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;