add context menu, add context menu to video component
continuous-integration/drone/push Build is failing Details

nextui
Guus van Meerveld 1 month ago
parent 0efd9ed1f8
commit ed53ae1ea1

@ -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",

@ -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 (
<div
style={{
top: location.y,
left: location.x,
display: shouldShow ? "block" : "none"
}}
className="bg-background border-small max-w-xs px-1 py-2 rounded-small border-default-200 absolute z-10"
>
<Listbox aria-label="Context Menu">
{menu.map((item) => (
<ListboxItem
onClick={() => {
if (item.onClick) {
item.onClick();
hide();
}
}}
showDivider={item.showDivider}
key={item.key}
href={item.href}
>
{item.title}
</ListboxItem>
))}
</Listbox>
</div>
);
};
export const ContextMenuProvider: Component = ({ children }) => {
return (
<>
{children}
<Menu />
</>
);
};

@ -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 (
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={false} />
<NextUIProvider>{children}</NextUIProvider>
<NextUIProvider>
<ContextMenuProvider>{children}</ContextMenuProvider>
</NextUIProvider>
</QueryClientProvider>
);
}

@ -42,7 +42,7 @@ export const Trending: Component = ({}) => {
refetch,
data
} = useQuery({
queryKey: ["trending", region],
queryKey: ["trending", region?.code],
queryFn: () => {
if (region === null) return;

@ -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 (
<Listbox aria-label="Context Menu">
{menu.map((item) => (
<ListboxItem onClick={item.onClick} key={item.key} href={item.href}>
{item.title}
</ListboxItem>
))}
</Listbox>
<div
onContextMenu={(e) => {
e.preventDefault();
showContextMenu(e.pageX, e.pageY, menu);
}}
>
{children}
</div>
);
};

@ -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 (
<Link href={`/watch?v=${video.id}`}>
<Card
radius="lg"
onContextMenu={(e) => {
e.preventDefault();
handleContextMenu();
}}
>
<CardBody>
<Image
alt={video.title}
className="object-cover"
height={400}
src={video.thumbnail}
width={600}
/>
<p className="text-small rounded-md z-10 absolute bottom-5 right-5 bg-content2 p-1">
{formatDuration(video.duration)}
</p>
</CardBody>
<Divider />
<CardFooter>
<div className="max-w-full">
<p title={video.title} className="truncate">
{video.title}
<Link href={url}>
<ContextMenu menu={menuItems}>
<Card radius="lg">
<CardBody>
<Image
alt={video.title}
className="object-cover"
height={400}
src={video.thumbnail}
width={600}
/>
<p className="text-small rounded-md z-10 absolute bottom-5 right-5 bg-content2 p-1">
{formatDuration(video.duration)}
</p>
<div className="flex flex-row gap-2 justify-start overflow-scroll">
<p className="text-small font-semibold tracking-tight text-default-400">
{video.author.name}
</p>
<p className="text-small tracking-tight text-default-400">
{video.uploaded.toLocaleDateString()}
</CardBody>
<Divider />
<CardFooter>
<div className="max-w-full">
<p title={video.title} className="truncate">
{video.title}
</p>
<div className="flex flex-row gap-2 justify-start overflow-scroll">
<p className="text-small font-semibold tracking-tight text-default-400">
{video.author.name}
</p>
<Tooltip showArrow content={video.uploaded.toLocaleString()}>
<p className="text-small tracking-tight text-default-400">
{formatUploadedTime(video.uploaded)}
</p>
</Tooltip>
<p className="text-small tracking-tight text-default-400">
Views: {formatViewCount(video.views)}
</p>
<p className="text-small tracking-tight text-default-400">
Views: {formatViewCount(video.views)}
</p>
</div>
</div>
</div>
</CardFooter>
</Card>
</CardFooter>
</Card>
</ContextMenu>
</Link>
);
};

@ -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<ContextMenuStore>((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;

@ -0,0 +1,7 @@
export interface ContextMenuItem {
title: string;
key: string;
showDivider?: boolean;
href?: string;
onClick?: () => any;
}

@ -0,0 +1,7 @@
import { DateTime } from "luxon";
const formatUploadedTime = (uploaded: Date): string => {
return DateTime.fromJSDate(uploaded).toRelative() ?? "";
};
export default formatUploadedTime;

@ -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"

Loading…
Cancel
Save