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();
- }}
- >
-
-
-
- {formatDuration(video.duration)}
-
-
-
-
-
-
- {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"