mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-09-11 14:36:35 +03:00
feat(nav): 🚚 Move Brain and User buttons to the sidebar in the chat (#1262)
* 🚚 Move Brain and User buttons to the sidebar in the chat * 🚨 fix linter warnings * 🚚 Move sidebar actions to a dedicated component * ✅ Fix failing tests * 💄 Style sidebar buttons * 🚚 move nav components under ChatsListItem and User components accordingly * Display email in the user button * ♻️ use the UserData hook in the UserPage component * Do not display the top navbar on the chat page * ⚰️ remove the social icons at the bottom of the chat sidebar * ⚡️UserButton: get email from the Supabase context instead of a call api * 🚨 Fix unit-tests * 💄 Crop email in UserButton if necessary * UserButton: display empty string if the email is undefined
This commit is contained in:
parent
479f6af8ec
commit
da6d5b698d
@ -18,6 +18,8 @@ describe("DeleteOrUnsubscribeConfirmationModal", () => {
|
||||
isOpen={isOpen}
|
||||
setOpen={setOpen}
|
||||
onConfirm={onDelete}
|
||||
isOwnedByCurrentUser={true}
|
||||
isDeleteOrUnsubscribeRequestPending={false}
|
||||
/>
|
||||
);
|
||||
expect(getByTestId("modal-description")).toBeDefined();
|
||||
@ -31,6 +33,8 @@ describe("DeleteOrUnsubscribeConfirmationModal", () => {
|
||||
isOpen={isOpen}
|
||||
setOpen={setOpen}
|
||||
onConfirm={onDelete}
|
||||
isOwnedByCurrentUser={true}
|
||||
isDeleteOrUnsubscribeRequestPending={false}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -3,10 +3,18 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { act, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
BrainContextMock,
|
||||
BrainProviderMock,
|
||||
} from "@/lib/context/BrainProvider/mocks/BrainProviderMock";
|
||||
import {
|
||||
ChatContextMock,
|
||||
ChatProviderMock,
|
||||
} from "@/lib/context/ChatProvider/mocks/ChatProviderMock";
|
||||
import { SupabaseContextMock } from "@/lib/context/SupabaseProvider/mocks/SupabaseProviderMock";
|
||||
vi.mock("@/lib/context/SupabaseProvider/supabase-provider", () => ({
|
||||
SupabaseContext: SupabaseContextMock,
|
||||
}));
|
||||
|
||||
import * as useChatsListModule from "../hooks/useChatsList";
|
||||
import { ChatsList } from "../index";
|
||||
@ -59,6 +67,19 @@ vi.mock("@/lib/hooks", async () => {
|
||||
vi.mock("@/lib/context/ChatProvider/ChatProvider", () => ({
|
||||
ChatContext: ChatContextMock,
|
||||
}));
|
||||
vi.mock("@/lib/context/BrainProvider/brain-provider", () => ({
|
||||
BrainContext: BrainContextMock,
|
||||
}));
|
||||
|
||||
const mockUseSupabase = vi.fn(() => ({
|
||||
session: {
|
||||
user: { email: "email@domain.com" },
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/context/SupabaseProvider", () => ({
|
||||
useSupabase: () => mockUseSupabase(),
|
||||
}));
|
||||
|
||||
describe("ChatsList", () => {
|
||||
afterEach(() => {
|
||||
@ -69,7 +90,9 @@ describe("ChatsList", () => {
|
||||
const { getByTestId } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ChatProviderMock>
|
||||
<ChatsList />
|
||||
<BrainProviderMock>
|
||||
<ChatsList />
|
||||
</BrainProviderMock>
|
||||
</ChatProviderMock>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
@ -87,7 +110,9 @@ describe("ChatsList", () => {
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ChatProviderMock>
|
||||
<ChatsList />
|
||||
<BrainProviderMock>
|
||||
<ChatsList />
|
||||
</BrainProviderMock>
|
||||
</ChatProviderMock>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
@ -105,7 +130,9 @@ describe("ChatsList", () => {
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ChatProviderMock>
|
||||
(<ChatsList />)
|
||||
<BrainProviderMock>
|
||||
<ChatsList />
|
||||
</BrainProviderMock>
|
||||
</ChatProviderMock>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
@ -124,11 +151,14 @@ describe("ChatsList", () => {
|
||||
getChats: () => getChatsMock(),
|
||||
}),
|
||||
}));
|
||||
|
||||
await act(() =>
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ChatProviderMock>
|
||||
<ChatsList />
|
||||
<BrainProviderMock>
|
||||
<ChatsList />
|
||||
</BrainProviderMock>
|
||||
</ChatProviderMock>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
|
@ -0,0 +1,19 @@
|
||||
import Link from "next/link";
|
||||
import { FaBrain } from "react-icons/fa";
|
||||
|
||||
import { sidebarLinkStyle } from "@/app/chat/components/ChatsList/components/ChatsListItem/styles/SidebarLinkStyle";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
|
||||
export const BrainManagementButton = (): JSX.Element => {
|
||||
const { currentBrainId } = useBrainContext();
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/brains-management/${currentBrainId ?? ""}`}
|
||||
className={sidebarLinkStyle}
|
||||
>
|
||||
<FaBrain className="w-8 h-8" />
|
||||
<span>My Brains</span>
|
||||
</Link>
|
||||
);
|
||||
};
|
@ -1,38 +0,0 @@
|
||||
import { DISCORD_URL, GITHUB_URL, TWITTER_URL } from "@/lib/config/CONSTANTS";
|
||||
|
||||
export const MiniFooter = (): JSX.Element => {
|
||||
return (
|
||||
<footer className="bg-white dark:bg-black border-t dark:border-white/10 mt-auto py-4">
|
||||
<div className="max-w-screen-xl mx-auto flex justify-center items-center gap-4">
|
||||
<a
|
||||
href={GITHUB_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Quivr GitHub"
|
||||
>
|
||||
<img
|
||||
className="h-4 w-auto dark:invert"
|
||||
src="/github.svg"
|
||||
alt="GitHub"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={TWITTER_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Quivr Twitter"
|
||||
>
|
||||
<img className="h-4 w-auto" src="/twitter.svg" alt="Twitter" />
|
||||
</a>
|
||||
<a
|
||||
href={DISCORD_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Quivr Discord"
|
||||
>
|
||||
<img className="h-4 w-auto" src="/discord.svg" alt="Discord" />
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
@ -0,0 +1,14 @@
|
||||
import { BrainManagementButton } from "@/app/chat/components/ChatsList/components/ChatsListItem/components/BrainManagementButton";
|
||||
|
||||
import { UserButton } from "./UserButton";
|
||||
|
||||
export const SidebarActions = (): JSX.Element => {
|
||||
return (
|
||||
<div className="bg-white dark:bg-black border-t dark:border-white/10 mt-auto py-4">
|
||||
<div className="max-w-screen-xl mx-auto flex justify-center items-center gap-4 flex-col p-5">
|
||||
<BrainManagementButton />
|
||||
<UserButton />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,19 @@
|
||||
import Link from "next/link";
|
||||
import { MdPerson } from "react-icons/md";
|
||||
|
||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||
|
||||
import { sidebarLinkStyle } from "../styles/SidebarLinkStyle";
|
||||
|
||||
export const UserButton = (): JSX.Element => {
|
||||
const { session } = useSupabase();
|
||||
|
||||
return (
|
||||
<Link aria-label="account" className={sidebarLinkStyle} href={"/user"}>
|
||||
<MdPerson className="text-4xl" />
|
||||
<span className="text-ellipsis overflow-hidden">
|
||||
{session?.user.email ?? ""}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
export const sidebarLinkStyle =
|
||||
"w-full rounded-2xl px-5 py-2 text-base flex justify-start items-center gap-4 bg-gray-100 dark:bg-black/10 hover:bg-gray-200 dark:hover:bg-black/20 focus:outline-none";
|
@ -8,7 +8,7 @@ import { useChatsContext } from "@/lib/context/ChatsProvider/hooks/useChatsConte
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { ChatsListItem } from "./components/ChatsListItem";
|
||||
import { MiniFooter } from "./components/ChatsListItem/components/MiniFooter";
|
||||
import { SidebarActions } from "./components/ChatsListItem/components/SidebarActions";
|
||||
import { NewChatButton } from "./components/NewChatButton";
|
||||
import { useChatsList } from "./hooks/useChatsList";
|
||||
import {
|
||||
@ -61,7 +61,7 @@ export const ChatsList = (): JSX.Element => {
|
||||
? "10px 10px 16px rgba(0, 0, 0, 0)"
|
||||
: "10px 10px 16px rgba(0, 0, 0, 0.5)",
|
||||
}}
|
||||
className={cn("overflow-hidden flex flex-col flex-1")}
|
||||
className={cn("overflow-hidden flex flex-col flex-1 max-w-xs")}
|
||||
data-testid="chats-list"
|
||||
>
|
||||
<div className="flex flex-col flex-1 h-full">
|
||||
@ -106,7 +106,7 @@ export const ChatsList = (): JSX.Element => {
|
||||
<ChatsListItem key={chat.chat_id} chat={chat} />
|
||||
))}
|
||||
</div>
|
||||
<MiniFooter />
|
||||
<SidebarActions />
|
||||
</div>
|
||||
</motion.div>
|
||||
<button
|
||||
@ -115,6 +115,8 @@ export const ChatsList = (): JSX.Element => {
|
||||
}}
|
||||
className="absolute left-full top-16 text-3xl bg-black dark:bg-white text-white dark:text-black rounded-r-full p-3 pl-1"
|
||||
data-testid="chats-list-toggle"
|
||||
title="Toggle Chats List"
|
||||
type="button"
|
||||
>
|
||||
<motion.div
|
||||
whileTap={{ scale: 0.9 }}
|
||||
|
@ -3,8 +3,7 @@ import { MdCheck } from "react-icons/md";
|
||||
|
||||
import Popover from "@/lib/components/ui/Popover";
|
||||
|
||||
import { useLanguageHook } from "../LanguageDropDown/hooks/useLanguageHook";
|
||||
|
||||
import { useLanguageHook } from "./hooks/useLanguageHook";
|
||||
|
||||
export const LanguageDropDown = (): JSX.Element => {
|
||||
const { allLanguages, currentLanguage, change } = useLanguageHook();
|
@ -1,36 +1,21 @@
|
||||
/* eslint-disable */
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { DarkModeToggle } from "@/app/user/components/DarkModeToggle";
|
||||
import { LanguageDropDown } from "@/app/user/components/LanguageDropDown";
|
||||
import Spinner from "@/lib/components/ui/Spinner";
|
||||
import { UserStats } from "@/lib/types/User";
|
||||
|
||||
import { USER_DATA_KEY } from "@/lib/api/user/config";
|
||||
import { useUserApi } from "@/lib/api/user/useUserApi";
|
||||
import { DarkModeToggle } from "@/lib/components/NavBar/components/NavItems/components/DarkModeToggle";
|
||||
import { LanguageDropDown } from "@/lib/components/NavBar/components/NavItems/components/LanguageDropDown";
|
||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||
import { useUserData } from "@/lib/hooks/useUserData";
|
||||
import { redirectToLogin } from "@/lib/router/redirectToLogin";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { UserStatistics } from "./components/UserStatistics";
|
||||
|
||||
const UserPage = (): JSX.Element => {
|
||||
const [userStats, setUserStats] = useState<UserStats>();
|
||||
const { session } = useSupabase();
|
||||
const { t } = useTranslation(["translation", "user"]);
|
||||
const { getUser } = useUserApi();
|
||||
|
||||
const { data: userData } = useQuery({
|
||||
queryKey: [USER_DATA_KEY],
|
||||
queryFn: getUser,
|
||||
});
|
||||
const { userData: userStats } = useUserData();
|
||||
|
||||
useEffect(() => {
|
||||
if (userData !== undefined) {
|
||||
setUserStats(userData);
|
||||
}
|
||||
}, [userData]);
|
||||
if (session === null) {
|
||||
redirectToLogin();
|
||||
}
|
||||
|
@ -1,22 +0,0 @@
|
||||
import Link from "next/link";
|
||||
import { FaBrain } from "react-icons/fa";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
|
||||
export const BrainManagementButton = (): JSX.Element => {
|
||||
const { currentBrainId } = useBrainContext();
|
||||
|
||||
return (
|
||||
<Link href={`/brains-management/${currentBrainId ?? ""}`}>
|
||||
<Button
|
||||
variant={"tertiary"}
|
||||
className="focus:outline-none text-2xl"
|
||||
aria-label="Settings"
|
||||
data-testid="brain-management-button"
|
||||
>
|
||||
<FaBrain className="w-6 h-6" />
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
};
|
@ -1,13 +1,10 @@
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
import { Dispatch, HTMLAttributes, SetStateAction } from "react";
|
||||
import { MdPerson } from "react-icons/md";
|
||||
|
||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { AuthButtons } from "./components/AuthButtons";
|
||||
import { BrainManagementButton } from "./components/BrainManagementButton";
|
||||
import { NavLink } from "./components/NavLink";
|
||||
|
||||
interface NavItemsProps extends HTMLAttributes<HTMLUListElement> {
|
||||
@ -41,14 +38,6 @@ export const NavItems = ({
|
||||
</>
|
||||
)}
|
||||
<div className="flex sm:flex-1 sm:justify-end flex-row items-center justify-center sm:flex-row gap-5 sm:gap-2">
|
||||
{isUserLoggedIn && (
|
||||
<>
|
||||
<BrainManagementButton />
|
||||
<Link aria-label="account" className="" href={"/user"}>
|
||||
<MdPerson className="text-2xl" />
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
{!isUserLoggedIn && <AuthButtons />}
|
||||
</div>
|
||||
</ul>
|
||||
|
@ -1,16 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { Header } from "./components/Header";
|
||||
import { Logo } from "./components/Logo";
|
||||
import { MobileMenu } from "./components/MobileMenu";
|
||||
import { NavItems } from "./components/NavItems";
|
||||
|
||||
export const NavBar = (): JSX.Element => {
|
||||
const path = usePathname();
|
||||
|
||||
return (
|
||||
<Header>
|
||||
<Logo />
|
||||
<NavItems className="hidden sm:flex" />
|
||||
<MobileMenu />
|
||||
</Header>
|
||||
<>
|
||||
{path === null || path.startsWith("/chat") ? (
|
||||
<></>
|
||||
) : (
|
||||
<Header>
|
||||
<Logo />
|
||||
<NavItems className="hidden sm:flex" />
|
||||
<MobileMenu />
|
||||
</Header>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user