From ed53ae1ea16393c67bcc2928eede3ee4e8f87813 Mon Sep 17 00:00:00 2001 From: Guus van Meerveld Date: Fri, 15 Mar 2024 00:32:45 +0100 Subject: [PATCH] add context menu, add context menu to video component --- package.json | 3 +- src/app/providers/ContextMenuProvider.tsx | 62 ++++++++++ .../{providers.tsx => providers/index.tsx} | 5 +- src/app/trending/Trending.tsx | 2 +- src/components/ContextMenu.tsx | 31 +++-- src/components/Video.tsx | 112 ++++++++++++------ src/hooks/useContextMenuStore.ts | 27 +++++ src/typings/contextMenu.ts | 7 ++ src/utils/formatUploadedTime.ts | 7 ++ yarn.lock | 12 ++ 10 files changed, 211 insertions(+), 57 deletions(-) create mode 100644 src/app/providers/ContextMenuProvider.tsx rename src/app/{providers.tsx => providers/index.tsx} (74%) create mode 100644 src/hooks/useContextMenuStore.ts create mode 100644 src/typings/contextMenu.ts create mode 100644 src/utils/formatUploadedTime.ts diff --git a/package.json b/package.json index 68f54b2..4249ff0 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "react-dom": "^18", "react-icons": "^5.0.1", "use-debounce": "^10.0.0", - "zod": "^3.22.4" + "zod": "^3.22.4", + "zustand": "^4.5.2" }, "devDependencies": { "@tanstack/react-query-devtools": "^5.27.8", diff --git a/src/app/providers/ContextMenuProvider.tsx b/src/app/providers/ContextMenuProvider.tsx new file mode 100644 index 0000000..add2f8c --- /dev/null +++ b/src/app/providers/ContextMenuProvider.tsx @@ -0,0 +1,62 @@ +import useContextMenuStore from "@/hooks/useContextMenuStore"; +import { Component } from "@/typings/component"; +import { Listbox, ListboxItem } from "@nextui-org/listbox"; +import { useCallback, useEffect } from "react"; + +const Menu: Component = () => { + const shouldShow = useContextMenuStore((state) => state.show); + const menu = useContextMenuStore((state) => state.items); + const hide = useContextMenuStore((state) => state.hide); + + const location = useContextMenuStore((state) => state.location); + + const handleClick = useCallback(() => { + if (shouldShow) hide(); + }, [hide, shouldShow]); + + useEffect(() => { + window.addEventListener("click", handleClick); + + return () => { + window.removeEventListener("click", handleClick); + }; + }, [hide, shouldShow]); + + return ( +
+ + {menu.map((item) => ( + { + if (item.onClick) { + item.onClick(); + hide(); + } + }} + showDivider={item.showDivider} + key={item.key} + href={item.href} + > + {item.title} + + ))} + +
+ ); +}; + +export const ContextMenuProvider: Component = ({ children }) => { + return ( + <> + {children} + + + ); +}; diff --git a/src/app/providers.tsx b/src/app/providers/index.tsx similarity index 74% rename from src/app/providers.tsx rename to src/app/providers/index.tsx index edc8fc6..09107a2 100644 --- a/src/app/providers.tsx +++ b/src/app/providers/index.tsx @@ -3,13 +3,16 @@ import { NextUIProvider } from "@nextui-org/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { ContextMenuProvider } from "./ContextMenuProvider"; export function Providers({ children }: { children: React.ReactNode }) { const queryClient = new QueryClient(); return ( - {children} + + {children} + ); } diff --git a/src/app/trending/Trending.tsx b/src/app/trending/Trending.tsx index 5fd1c63..36526ee 100644 --- a/src/app/trending/Trending.tsx +++ b/src/app/trending/Trending.tsx @@ -42,7 +42,7 @@ export const Trending: Component = ({}) => { refetch, data } = useQuery({ - queryKey: ["trending", region], + queryKey: ["trending", region?.code], queryFn: () => { if (region === null) return; diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx index fa7baf8..772c048 100644 --- a/src/components/ContextMenu.tsx +++ b/src/components/ContextMenu.tsx @@ -1,23 +1,22 @@ +import useContextMenuStore from "@/hooks/useContextMenuStore"; import { Component } from "@/typings/component"; -import { Listbox, ListboxItem } from "@nextui-org/listbox"; - -export interface ContextMenuItem { - title: string; - key: string; - href?: string; - onClick?: () => any; -} +import { ContextMenuItem } from "@/typings/contextMenu"; export const ContextMenu: Component<{ menu: ContextMenuItem[] }> = ({ - menu + menu, + children }) => { + const showContextMenu = useContextMenuStore((state) => state.showContextMenu); + return ( - - {menu.map((item) => ( - - {item.title} - - ))} - +
{ + e.preventDefault(); + + showContextMenu(e.pageX, e.pageY, menu); + }} + > + {children} +
); }; diff --git a/src/components/Video.tsx b/src/components/Video.tsx index 95ade86..f64b3e6 100644 --- a/src/components/Video.tsx +++ b/src/components/Video.tsx @@ -6,52 +6,88 @@ import { Divider } from "@nextui-org/divider"; import Link from "next/link"; import formatViewCount from "@/utils/formatViewCount"; import formatDuration from "@/utils/formatDuration"; +import { ContextMenu } from "./ContextMenu"; +import formatUploadedTime from "@/utils/formatUploadedTime"; +import { Tooltip } from "@nextui-org/tooltip"; +import { ContextMenuItem } from "@/typings/contextMenu"; export const Video: Component<{ data: VideoPreview }> = ({ data: video }) => { - const handleContextMenu = () => {}; + const url = `/watch?v=${video.id}`; + const channelUrl = `/channel/${video.author.id}`; + + const menuItems: ContextMenuItem[] = [ + { title: "Go to video", key: "gotoVideo", href: url }, + { + title: "Copy video id", + key: "videoId", + onClick: () => { + navigator.clipboard.writeText(video.id); + }, + showDivider: true + }, + { + title: "Open thumbnail", + key: "thumbnail", + href: video.thumbnail + }, + { + title: "Copy thumnail url", + key: "thumbnailUrl", + onClick: () => { + navigator.clipboard.writeText(video.thumbnail); + }, + showDivider: true + }, + { title: "Go to channel", key: "gotoChannel", href: channelUrl }, + { + title: "Copy channel id", + key: "channelId", + onClick: () => { + navigator.clipboard.writeText(video.author.id); + } + } + ]; return ( - - { - e.preventDefault(); - handleContextMenu(); - }} - > - - {video.title} -

- {formatDuration(video.duration)} -

-
- - -
-

- {video.title} + + + + + {video.title} +

+ {formatDuration(video.duration)}

-
-

- {video.author.name} -

-

- {video.uploaded.toLocaleDateString()} + + + +

+

+ {video.title}

+
+

+ {video.author.name} +

+ +

+ {formatUploadedTime(video.uploaded)} +

+
-

- Views: {formatViewCount(video.views)} -

+

+ Views: {formatViewCount(video.views)} +

+
-
- - + + + ); }; diff --git a/src/hooks/useContextMenuStore.ts b/src/hooks/useContextMenuStore.ts new file mode 100644 index 0000000..613a57e --- /dev/null +++ b/src/hooks/useContextMenuStore.ts @@ -0,0 +1,27 @@ +import { ContextMenuItem } from "@/typings/contextMenu"; +import { create } from "zustand"; + +interface Location { + x: number; + y: number; +} + +interface ContextMenuStore { + show: boolean; + location: Location; + items: ContextMenuItem[]; + showContextMenu: (x: number, y: number, items: ContextMenuItem[]) => void; + hide: () => void; +} + +const useContextMenuStore = create((set) => ({ + show: false, + location: { x: 0, y: 0 }, + items: [], + showContextMenu(x, y, items) { + set({ show: true, location: { x, y }, items }); + }, + hide: () => set({ show: false }) +})); + +export default useContextMenuStore; diff --git a/src/typings/contextMenu.ts b/src/typings/contextMenu.ts new file mode 100644 index 0000000..97ec625 --- /dev/null +++ b/src/typings/contextMenu.ts @@ -0,0 +1,7 @@ +export interface ContextMenuItem { + title: string; + key: string; + showDivider?: boolean; + href?: string; + onClick?: () => any; +} diff --git a/src/utils/formatUploadedTime.ts b/src/utils/formatUploadedTime.ts new file mode 100644 index 0000000..d22fa4f --- /dev/null +++ b/src/utils/formatUploadedTime.ts @@ -0,0 +1,7 @@ +import { DateTime } from "luxon"; + +const formatUploadedTime = (uploaded: Date): string => { + return DateTime.fromJSDate(uploaded).toRelative() ?? ""; +}; + +export default formatUploadedTime; diff --git a/yarn.lock b/yarn.lock index 81b82cf..ec2f04a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6486,6 +6486,11 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" +use-sync-external-store@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -6794,3 +6799,10 @@ zod@^3.22.4: version "3.22.4" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg== + +zustand@^4.5.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.2.tgz#fddbe7cac1e71d45413b3682cdb47b48034c3848" + integrity sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g== + dependencies: + use-sync-external-store "1.2.0"