mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-12-14 17:03:29 +03:00
feat: improve ux (#1522)
Issues: https://github.com/StanGirard/quivr/issues/1497 https://github.com/StanGirard/quivr/issues/1495 https://github.com/StanGirard/quivr/issues/1506 1. feat(chatInput): increase upload button size 2. feat(brains-library): add Spinner on loading 3. feat: improve logout ux ![Screenshot 2023-10-30 at 11 48 58](https://github.com/StanGirard/quivr/assets/63923024/fb8e0848-b349-4fbd-a7a5-ff43a73ae364) https://github.com/StanGirard/quivr/assets/63923024/cbd7cd42-e58a-49fb-9867-97f19dde9270 https://github.com/StanGirard/quivr/assets/63923024/a69b6b28-1c19-43e7-a02b-1df215a34a2e
This commit is contained in:
parent
e3a99d1ace
commit
9be4a57979
@ -1,51 +0,0 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import Logout from "../page";
|
||||
|
||||
// mocking related hooks
|
||||
const mockUseLogout = vi.fn(() => ({
|
||||
handleLogout: vi.fn(),
|
||||
isPending: false,
|
||||
}));
|
||||
|
||||
vi.mock("../hooks/useLogout", () => ({
|
||||
useLogout: () => mockUseLogout(),
|
||||
}));
|
||||
|
||||
describe("Logout component", () => {
|
||||
it("should render correctly", () => {
|
||||
const { getByTestId } = render(<Logout />);
|
||||
const logoutPage = getByTestId("logout-page");
|
||||
expect(logoutPage).toBeDefined();
|
||||
});
|
||||
|
||||
it("should call handleLogout 1 time when logout button is clicked", () => {
|
||||
const mockHandleLogout = vi.fn();
|
||||
|
||||
mockUseLogout.mockReturnValue({
|
||||
handleLogout: mockHandleLogout,
|
||||
isPending: false,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(<Logout />);
|
||||
const logoutButton = getByTestId("logout-button");
|
||||
logoutButton.click();
|
||||
|
||||
expect(mockHandleLogout).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should not call handleLogout when isPending is true", () => {
|
||||
const mockHandleLogout = vi.fn();
|
||||
|
||||
mockUseLogout.mockReturnValue({
|
||||
handleLogout: mockHandleLogout,
|
||||
isPending: true,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(<Logout />);
|
||||
const logoutButton = getByTestId("logout-button");
|
||||
logoutButton.click();
|
||||
expect(mockHandleLogout).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
@ -1,50 +0,0 @@
|
||||
/* eslint-disable */
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import Card from "@/lib/components/ui/Card";
|
||||
import PageHeading from "@/lib/components/ui/PageHeading";
|
||||
import { useLogout } from "./hooks/useLogout";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export default function Logout() {
|
||||
|
||||
const {t, i18n} = useTranslation(["translation","logout"]);
|
||||
|
||||
const { handleLogout, isPending } = useLogout();
|
||||
|
||||
function Logout() {
|
||||
return (
|
||||
<main data-testid="logout-page">
|
||||
<section className="w-full min-h-[80vh] h-full outline-none flex flex-col gap-5 items-center justify-center p-6">
|
||||
<PageHeading title={t("title",{ ns: "logout" })} subtitle={t("subtitle",{ ns: "logout" })} />
|
||||
<Card className="max-w-md w-full p-5 sm:p-10 text-center flex flex-col items-center gap-5">
|
||||
<h2 className="text-lg">{t("areYouSure",{ ns: "logout" })}</h2>
|
||||
<div className="flex gap-5 items-center justify-center">
|
||||
<Link href={"/"}>
|
||||
<Button variant={"primary"}>{t("cancel",{ ns: "logout" })}</Button>
|
||||
</Link>
|
||||
<Button
|
||||
isLoading={isPending}
|
||||
variant={"danger"}
|
||||
onClick={() => handleLogout()}
|
||||
data-testid="logout-button"
|
||||
>
|
||||
{t("logoutButton")}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={"Loading..."}>
|
||||
<Logout />
|
||||
</Suspense>
|
||||
)
|
||||
|
||||
}
|
@ -9,7 +9,7 @@ import { PublicBrain } from "@/lib/context/BrainProvider/types";
|
||||
export const useBrainsLibrary = () => {
|
||||
const [searchBarText, setSearchBarText] = useState("");
|
||||
const { getPublicBrains } = useBrainApi();
|
||||
const { data: publicBrains = [] } = useQuery({
|
||||
const { data: publicBrains = [], isLoading } = useQuery({
|
||||
queryKey: [PUBLIC_BRAINS_KEY],
|
||||
queryFn: getPublicBrains,
|
||||
});
|
||||
@ -39,5 +39,6 @@ export const useBrainsLibrary = () => {
|
||||
displayingPublicBrains,
|
||||
searchBarText,
|
||||
setSearchBarText,
|
||||
isLoading,
|
||||
};
|
||||
};
|
||||
|
@ -3,12 +3,13 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Field from "@/lib/components/ui/Field";
|
||||
import Spinner from "@/lib/components/ui/Spinner";
|
||||
|
||||
import { PublicBrainItem } from "./components/PublicBrainItem/PublicBrainItem";
|
||||
import { useBrainsLibrary } from "./hooks/useBrainsLibrary";
|
||||
|
||||
const BrainsLibrary = (): JSX.Element => {
|
||||
const { displayingPublicBrains, searchBarText, setSearchBarText } =
|
||||
const { displayingPublicBrains, searchBarText, setSearchBarText, isLoading } =
|
||||
useBrainsLibrary();
|
||||
const { t } = useTranslation("brain");
|
||||
|
||||
@ -23,6 +24,11 @@ const BrainsLibrary = (): JSX.Element => {
|
||||
placeholder={t("public_brains_search_bar_placeholder")}
|
||||
/>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="flex justify-center items-center flex-1">
|
||||
<Spinner className="text-4xl" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap justify-stretch w-full">
|
||||
{displayingPublicBrains.map((brain) => (
|
||||
|
@ -44,7 +44,7 @@ export const ChatInput = ({
|
||||
onClick={() => setShouldDisplayFeedCard(true)}
|
||||
tooltip={t("add_content_card_button_tooltip")}
|
||||
>
|
||||
<PiPaperclipFill className="text-3xl" />
|
||||
<PiPaperclipFill size={38} />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
51
frontend/app/user/components/LogoutCard/LogoutModal.tsx
Normal file
51
frontend/app/user/components/LogoutCard/LogoutModal.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import { Modal } from "@/lib/components/ui/Modal";
|
||||
|
||||
import { useLogoutModal } from "./hooks/useLogoutModal";
|
||||
|
||||
export const LogoutModal = (): JSX.Element => {
|
||||
const { t } = useTranslation(["translation", "logout"]);
|
||||
const {
|
||||
handleLogout,
|
||||
isLoggingOut,
|
||||
isLogoutModalOpened,
|
||||
setIsLogoutModalOpened,
|
||||
} = useLogoutModal();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
Trigger={
|
||||
<Button className="px-3 py-2" variant="secondary">
|
||||
{t("logoutButton")}
|
||||
</Button>
|
||||
}
|
||||
isOpen={isLogoutModalOpened}
|
||||
setOpen={setIsLogoutModalOpened}
|
||||
CloseTrigger={<div />}
|
||||
>
|
||||
<div className="text-center flex flex-col items-center gap-5">
|
||||
<h2 className="text-lg font-medium mb-5">
|
||||
{t("areYouSure", { ns: "logout" })}
|
||||
</h2>
|
||||
<div className="flex gap-5 items-center justify-center">
|
||||
<Button
|
||||
onClick={() => setIsLogoutModalOpened(false)}
|
||||
variant={"primary"}
|
||||
>
|
||||
{t("cancel", { ns: "logout" })}
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoggingOut}
|
||||
variant={"danger"}
|
||||
onClick={() => void handleLogout()}
|
||||
data-testid="logout-button"
|
||||
>
|
||||
{t("logoutButton")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { useLogout } from "../useLogout";
|
||||
import { useLogoutModal } from "../useLogoutModal";
|
||||
|
||||
const mockSignOut = vi.fn(() => ({ error: null }));
|
||||
|
||||
@ -28,9 +28,9 @@ Object.defineProperty(window, "localStorage", {
|
||||
},
|
||||
});
|
||||
|
||||
describe("useLogout", () => {
|
||||
describe("useLogoutModal", () => {
|
||||
it("should call signOut", async () => {
|
||||
const { result } = renderHook(() => useLogout());
|
||||
const { result } = renderHook(() => useLogoutModal());
|
||||
|
||||
await act(() => result.current.handleLogout());
|
||||
|
@ -7,19 +7,20 @@ import { useToast } from "@/lib/hooks";
|
||||
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useLogout = () => {
|
||||
export const useLogoutModal = () => {
|
||||
const { supabase } = useSupabase();
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||
const [isLogoutModalOpened, setIsLogoutModalOpened] = useState(false);
|
||||
const { track } = useEventTracking();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { t, i18n } = useTranslation(["translation", "logout"]);
|
||||
const { t } = useTranslation(["translation", "logout"]);
|
||||
|
||||
const { publish } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const handleLogout = async () => {
|
||||
setIsPending(true);
|
||||
setIsLoggingOut(true);
|
||||
const { error } = await supabase.auth.signOut();
|
||||
void track("LOGOUT");
|
||||
localStorage.clear();
|
||||
@ -37,11 +38,13 @@ export const useLogout = () => {
|
||||
});
|
||||
router.replace("/");
|
||||
}
|
||||
setIsPending(false);
|
||||
setIsLoggingOut(false);
|
||||
};
|
||||
|
||||
return {
|
||||
handleLogout,
|
||||
isPending,
|
||||
isLoggingOut,
|
||||
isLogoutModalOpened,
|
||||
setIsLogoutModalOpened,
|
||||
};
|
||||
};
|
@ -10,6 +10,7 @@ import { redirectToLogin } from "@/lib/router/redirectToLogin";
|
||||
import { StripePricingOrManageButton, UserStatistics } from "./components";
|
||||
import { ApiKeyConfig } from "./components/ApiKeyConfig";
|
||||
import LanguageSelect from "./components/LanguageDropDown/LanguageSelect";
|
||||
import { LogoutModal } from "./components/LogoutCard/LogoutModal";
|
||||
import ThemeSelect from "./components/ThemeSelect/ThemeSelect";
|
||||
|
||||
const UserPage = (): JSX.Element => {
|
||||
@ -42,11 +43,8 @@ const UserPage = (): JSX.Element => {
|
||||
<p>
|
||||
<strong>{t("email")}:</strong> <span>{user.email}</span>
|
||||
</p>
|
||||
<Link href={"/logout"}>
|
||||
<Button className="px-3 py-2" variant="secondary">
|
||||
{t("logoutButton")}
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<LogoutModal />
|
||||
</div>
|
||||
<StripePricingOrManageButton />
|
||||
</CardBody>
|
||||
|
@ -1,7 +1,12 @@
|
||||
import { FaSpinner } from "react-icons/fa";
|
||||
|
||||
const Spinner = (): JSX.Element => {
|
||||
return <FaSpinner className="animate-spin m-5" />;
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type SpinnerProps = {
|
||||
className?: string;
|
||||
};
|
||||
const Spinner = ({ className }: SpinnerProps): JSX.Element => {
|
||||
return <FaSpinner className={cn("animate-spin m-5", className)} />;
|
||||
};
|
||||
|
||||
export default Spinner;
|
||||
|
@ -2,7 +2,7 @@
|
||||
"title": "Logout",
|
||||
"subtitle": "See you next time",
|
||||
"areYouSure": "Are you sure you want to sign out ?",
|
||||
"cancel": "Go back",
|
||||
"cancel": "Cancel",
|
||||
"error": "Error on logout {{errorMessage}}",
|
||||
"loggedOut": "Logged out successfully"
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"areYouSure": "¿Seguro que quieres cerrar la sesión?",
|
||||
"cancel": "Regresar",
|
||||
"cancel": "Cancelar",
|
||||
"error": "Error al cerrar sesión {{errorMessage}}",
|
||||
"loggedOut": "Sesión finalizada",
|
||||
"subtitle": "Hasta pronto",
|
||||
|
@ -2,7 +2,7 @@
|
||||
"title": "Déconnexion",
|
||||
"subtitle": "À la prochaine",
|
||||
"areYouSure": "Êtes-vous sûr de vouloir vous déconnecter ?",
|
||||
"cancel": "Retour",
|
||||
"cancel": "Annuler",
|
||||
"error": "Erreur lors de la déconnexion {{errorMessage}}",
|
||||
"loggedOut": "Déconnexion réussie"
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
"title": "Sair",
|
||||
"subtitle": "Até a próxima vez",
|
||||
"areYouSure": "Você tem certeza de que deseja sair?",
|
||||
"cancel": "Voltar",
|
||||
"cancel": "Cancelar",
|
||||
"error": "Erro ao fazer logout {{errorMessage}}",
|
||||
"loggedOut": "Saiu com sucesso"
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
"title": "Выход",
|
||||
"subtitle": "Увидимся в следующий раз",
|
||||
"areYouSure": "Вы уверены, что хотите выйти?",
|
||||
"cancel": "Вернуться",
|
||||
"cancel": "Отмена",
|
||||
"error": "Ошибка при выходе: {{errorMessage}}",
|
||||
"loggedOut": "Успешный выход"
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"title": "注销",
|
||||
"subtitle": "下次见",
|
||||
"areYouSure": "是否确实要注销?",
|
||||
"cancel": "返回",
|
||||
"cancel": "取消",
|
||||
"error": "注销时出错 {{errorMessage}}",
|
||||
"loggedOut": "注销成功"
|
||||
}
|
Loading…
Reference in New Issue
Block a user