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:
Matthieu Jacq 2023-09-26 18:26:19 +02:00 committed by GitHub
parent 479f6af8ec
commit da6d5b698d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 118 additions and 105 deletions

View File

@ -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}
/>
);

View File

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

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

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

View File

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

View File

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

View File

@ -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();
}

View File

@ -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>
);
};

View File

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

View File

@ -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>
)}
</>
);
};