create main menu and dropdown menu

This commit is contained in:
yzned
2025-07-20 19:53:42 +03:00
committed by yzned
parent bba27be7e2
commit 0ce3c0307f
10 changed files with 240 additions and 66 deletions

View File

@@ -0,0 +1,71 @@
import { Dropdown } from "../ui/dropdown";
import { cn } from "@/lib/utils";
import { Link, useMatchRoute } from "@tanstack/react-router";
import { useState } from "react";
import ThreeDots from "@/icons/three-dots.svg?react";
import Edit from "@/icons/edit.svg?react";
import Share from "@/icons/share.svg?react";
import Trash from "@/icons/trash.svg?react";
export const ChatCell = ({ chat }: { chat: { id: string; title: string } }) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const matchRoute = useMatchRoute();
const match = matchRoute({ to: "/interaction-panel/$id", fuzzy: true });
const activeChatId = match ? match.id : undefined;
const DROPDOWN_OPTIONS = [
{
name: "Share",
icon: <Share />,
onClick: () => console.log("1"),
},
{
name: "Rename",
icon: <Edit />,
onClick: () => console.log("2"),
},
{
name: "Delete",
icon: <Trash />,
onClick: () => console.log("3"),
className: "text-feedback-negative-900",
},
];
return (
<div
data-active={activeChatId === chat.id}
className="data-[active=true]:bg-fill-150 block rounded-md h-10 hover:bg-fill-100 cursor-pointer transition-colors group flex items-center"
>
<Link
to="/interaction-panel/$id"
params={{ id: chat.id }}
viewTransition={{ types: ["warp"] }}
key={chat.id}
className="flex-1 h-full items-center flex pl-3"
>
<div className="text-sm font-medium text-text-light-900 truncate">
{chat.title}
</div>
</Link>
<Dropdown
options={DROPDOWN_OPTIONS}
isOpen={isDropdownOpen}
onToggle={() => setIsDropdownOpen(!isDropdownOpen)}
className="cursor-pointer flex items-center pr-3 justify-end w-5 h-full"
>
<ThreeDots
className={cn(
"duration-300",
isDropdownOpen
? "opacity-100"
: "opacity-0 group-hover:opacity-100",
)}
/>
</Dropdown>
</div>
);
};

View File

@@ -4,16 +4,19 @@ import Plus from "@/icons/plus.svg?react";
import Books from "@/icons/books.svg?react";
import Discord from "@/icons/discord.svg?react";
import Profile from "@/icons/profile.svg?react";
import ThreeDots from "@/icons/three-dots.svg?react";
import Settings from "@/icons/settings.svg?react";
import SignOut from "@/icons/sign-out.svg?react";
import { LINKS } from "@/lib/constants";
import { useEffect, useRef, useState, type ReactNode } from "react";
import { Button, type ButtonVariantType } from "../ui/button";
import { Link, useMatchRoute, useParams } from "@tanstack/react-router";
import { Link } from "@tanstack/react-router";
import Logo from "@/icons/logo.svg?react";
import LogoWithText from "@/icons/logo-with-text.svg?react";
import Sidebar from "@/icons/sidebar.svg?react";
import { ChatCell } from "./chat-cell";
import { Dropdown } from "../ui/dropdown";
interface MenuItemType extends ButtonVariantType {
name: string;
@@ -21,27 +24,6 @@ interface MenuItemType extends ButtonVariantType {
path: string;
}
const MENU_ITEMS: MenuItemType[] = [
{
name: "New Chat",
icon: <Plus className="scale-80" />,
path: "/",
variant: "primary",
},
{
name: "All chats",
icon: <Books className="scale-90" />,
path: "/all-chats",
variant: "secondary",
},
{
name: "Support",
icon: <Discord width={12} height={12} className="text-[#5865F2]" />,
path: LINKS.discord,
variant: "secondary",
},
];
const MainMenu = observer(() => {
const { isMainMenuOpen } = useAccountStore();
@@ -61,6 +43,8 @@ const MOCK_CHATS = Array.from({ length: 20 }, (_, i) => ({
}));
const MainMenuContent = observer(() => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const { isMainMenuOpen, setIsMainMenuOpen } = useAccountStore();
const containerRef = useRef<HTMLDivElement>(null);
const [hasScrolled, setHasScrolled] = useState(false);
@@ -81,6 +65,25 @@ const MainMenuContent = observer(() => {
};
}, []);
const DROPDOWN_OPTIONS = [
{
name: "user-mail",
icon: <Profile />,
onClick: () => console.log("1"),
className: "text-text-light-500",
},
{
name: "Settings",
icon: <Settings />,
onClick: () => console.log("2"),
},
{
name: "Sigh out",
icon: <SignOut />,
onClick: () => console.log("3"),
},
];
return (
<div
data-open={isMainMenuOpen}
@@ -145,20 +148,28 @@ const MainMenuContent = observer(() => {
</ul>
</div>
<footer className="sticky bottom-0 z-10 bg-white w-full p-4 border-t border-fill-150">
<Button
data-open={isMainMenuOpen}
className="w-8 h-8 p-2 transition-all data-[open=true]:w-full data-[open=true]:h-10 items-center justify-start data-[open=true]:py-2 data-[open=true]:px-4"
variant="secondary"
<footer className="sticky bottom-0 z-10 bg-white w-full p-4 border-t border-fill-150 w-full">
<Dropdown
options={DROPDOWN_OPTIONS}
isOpen={isDropdownOpen}
onToggle={() => setIsDropdownOpen(!isDropdownOpen)}
classNameContent="min-w-[200px]"
classNameTrigger="w-[195px]"
>
<Profile />
<div
<Button
data-open={isMainMenuOpen}
className="data-[open=true]:opacity-100 opacity-0 transition-opacity duration-200 font-[500] text-[14px]"
className="w-8 h-8 p-2 transition-all data-[open=true]:w-full data-[open=true]:h-10 items-center justify-start data-[open=true]:py-2 data-[open=true]:px-4"
variant="secondary"
>
username
</div>
</Button>
<Profile />
<div
data-open={isMainMenuOpen}
className="data-[open=true]:opacity-100 opacity-0 transition-opacity duration-200 font-[500] text-[14px]"
>
username
</div>
</Button>
</Dropdown>
</footer>
</div>
);
@@ -190,37 +201,25 @@ const MenuItem = ({ item }: { item: MenuItemType }) => {
);
};
const ChatCell = ({ chat }: { chat: { id: string; title: string } }) => {
const matchRoute = useMatchRoute();
const match = matchRoute({ to: "/interaction-panel/$id", fuzzy: true });
const activeChatId = match ? match.id : undefined;
return (
<div
data-active={activeChatId === chat.id}
className="data-[active=true]:bg-fill-150 block rounded-md h-10 hover:bg-fill-100 cursor-pointer transition-colors group flex items-center"
>
<Link
to="/interaction-panel/$id"
params={{ id: chat.id }}
viewTransition={{ types: ["warp"] }}
key={chat.id}
className="flex-1 h-full items-center flex pl-3"
>
<div className="text-sm font-medium text-text-light-900 truncate">
{chat.title}
</div>
</Link>
<button
className="opacity-0 group-hover:opacity-100 duration-300 cursor-pointer flex items-center pr-3 justify-end w-5 h-full"
type="button"
>
<ThreeDots />
</button>
</div>
);
};
const MENU_ITEMS: MenuItemType[] = [
{
name: "New Chat",
icon: <Plus className="scale-80" />,
path: "/",
variant: "primary",
},
{
name: "All chats",
icon: <Books className="scale-90" />,
path: "/all-chats",
variant: "secondary",
},
{
name: "Support",
icon: <Discord width={12} height={12} className="text-[#5865F2]" />,
path: LINKS.discord,
variant: "secondary",
},
];
export { MainMenu, MenuItem, MENU_ITEMS, MainMenuContent };

View File

@@ -0,0 +1,85 @@
"use client";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { FC, type ReactNode } from "react";
import clsx from "clsx";
import { cn } from "@/lib/utils";
interface DropdownOption {
name: string;
icon?: ReactNode;
onClick: () => void;
className?: string;
}
interface DropdownProps {
children: ReactNode;
options: DropdownOption[];
isOpen: boolean;
onToggle: () => void;
classNameContent?: string;
classNameTrigger?: string;
}
const Dropdown: FC<DropdownProps> = ({
children,
options = [],
isOpen,
onToggle,
classNameContent = "",
classNameTrigger = "",
}) => {
return (
<DropdownMenu.Root open={isOpen} onOpenChange={onToggle}>
<DropdownMenu.Trigger asChild>
<button
type="button"
className={clsx(
"inline-flex items-center focus:outline-none",
classNameTrigger,
)}
aria-haspopup="true"
aria-expanded={isOpen}
>
{children}
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
className={clsx(
"z-50 overflow-hidden p-2 bg-white border border-fill-150 rounded-[16px] min-w-[130px]",
classNameContent,
)}
side="bottom"
align="start"
style={{ boxShadow: "0px 4px 10px 0px #00000014" }}
sideOffset={8}
>
{options.map((option, index) => (
<DropdownMenu.Item
key={`${option.name}-${index}`}
onSelect={(e) => {
e.preventDefault();
option.onClick();
}}
className={cn(
"focus:outline-none flex items-center gap-2 h-8 px-2 text-sm text-text-light-900 hover:bg-fill-100 cursor-pointer rounded-[8px] transition-colors",
option.className,
)}
>
{option.icon && (
<span className="w-4 h-4 flex items-center justify-center text-inherit">
{option.icon}
</span>
)}
<span>{option.name}</span>
</DropdownMenu.Item>
))}
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
};
export { Dropdown };