feat: upgrade button in user settings (#1484)

# Description

Epic: #1429
User Story: #1431

- Add an upgrade button in user settings.
- Remove hover links on sidebar buttons (otherwise the link could
partially hide the button)

## Screenshots (if appropriate):

<img width="749" alt="image"
src="https://github.com/StanGirard/quivr/assets/67386567/6265ba2b-8d91-4ee8-abb3-98417ad91076">

<img width="803" alt="image"
src="https://github.com/StanGirard/quivr/assets/67386567/c13ce60b-a54d-44d7-a622-bcb1200ddb81">
This commit is contained in:
Matthieu Jacq 2023-10-25 12:42:53 +02:00 committed by GitHub
parent 7038cddd2f
commit ee7af51c4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 89 additions and 45 deletions

View File

@ -17,7 +17,7 @@ export const BrainsList = (): JSX.Element => {
const { t } = useTranslation(["brain", "chat"]); const { t } = useTranslation(["brain", "chat"]);
return ( return (
<Sidebar showButtons={["user", "upgradeToPlus"]}> <Sidebar showButtons={["upgradeToPlus", "user"]}>
<div className="flex flex-col p-2 gap-2"> <div className="flex flex-col p-2 gap-2">
<Link href="/chat"> <Link href="/chat">
<Button type="button" className="bg-primary text-white py-2 w-full"> <Button type="button" className="bg-primary text-white py-2 w-full">

View File

@ -15,7 +15,7 @@ export const ChatsList = (): JSX.Element => {
const { shouldDisplayWelcomeChat } = useOnboarding(); const { shouldDisplayWelcomeChat } = useOnboarding();
return ( return (
<Sidebar showButtons={["myBrains", "user", "upgradeToPlus"]}> <Sidebar showButtons={["myBrains", "upgradeToPlus", "user"]}>
<div className="flex flex-col flex-1 h-full" data-testid="chats-list"> <div className="flex flex-col flex-1 h-full" data-testid="chats-list">
<div className="pt-2"> <div className="pt-2">
<NewChatButton /> <NewChatButton />

View File

@ -3,9 +3,11 @@
import Link from "next/link"; import Link from "next/link";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { StripePricingModal } from "@/lib/components/Stripe";
import Button from "@/lib/components/ui/Button"; import Button from "@/lib/components/ui/Button";
import Card, { CardBody, CardHeader } from "@/lib/components/ui/Card"; import Card, { CardBody, CardHeader } from "@/lib/components/ui/Card";
import { useSupabase } from "@/lib/context/SupabaseProvider"; import { useSupabase } from "@/lib/context/SupabaseProvider";
import { useUserData } from "@/lib/hooks/useUserData";
import { redirectToLogin } from "@/lib/router/redirectToLogin"; import { redirectToLogin } from "@/lib/router/redirectToLogin";
import { UserStatistics } from "./components"; import { UserStatistics } from "./components";
@ -15,13 +17,20 @@ import ThemeSelect from "./components/ThemeSelect/ThemeSelect";
const UserPage = (): JSX.Element => { const UserPage = (): JSX.Element => {
const { session } = useSupabase(); const { session } = useSupabase();
const { userData } = useUserData();
const is_premium = userData?.is_premium;
if (!session) { if (!session) {
redirectToLogin(); redirectToLogin();
} }
const { user } = session; const { user } = session;
const { t } = useTranslation(["translation", "user", "config"]); const { t } = useTranslation([
"translation",
"user",
"config",
"monetization",
]);
return ( return (
<main className="container lg:w-2/3 mx-auto py-10 px-5"> <main className="container lg:w-2/3 mx-auto py-10 px-5">
@ -32,18 +41,22 @@ const UserPage = (): JSX.Element => {
</h2> </h2>
</CardHeader> </CardHeader>
<CardBody> <CardBody className="flex flex-col items-stretch max-w-max gap-2">
<p className="mb-3"> <div className="flex gap-5 items-center">
<strong>{t("email")}:</strong> <span>{user.email}</span> <p>
</p> <strong>{t("email")}:</strong> <span>{user.email}</span>
</p>
<div className="inline-block">
<Link href={"/logout"}> <Link href={"/logout"}>
<Button className="px-3 py-2" variant="secondary"> <Button className="px-3 py-2" variant="secondary">
{t("logoutButton")} {t("logoutButton")}
</Button> </Button>
</Link> </Link>
</div> </div>
{is_premium === true ? null : (
<StripePricingModal
Trigger={<Button>{t("monetization:upgrade")}</Button>}
/>
)}
</CardBody> </CardBody>
</Card> </Card>

View File

@ -12,12 +12,16 @@ type SidebarFooterProps = {
export const SidebarFooter = ({ export const SidebarFooter = ({
showButtons, showButtons,
}: SidebarFooterProps): JSX.Element => { }: SidebarFooterProps): JSX.Element => {
const buttons = {
myBrains: <BrainManagementButton />,
upgradeToPlus: <UpgradeToPlus />,
user: <UserButton />,
};
return ( return (
<div className="bg-gray-50 dark:bg-gray-900 border-t dark:border-white/10 mt-auto p-2"> <div className="bg-gray-50 dark:bg-gray-900 border-t dark:border-white/10 mt-auto p-2">
<div className="max-w-screen-xl flex justify-center items-center flex-col"> <div className="max-w-screen-xl flex justify-center items-center flex-col">
{showButtons.includes("myBrains") && <BrainManagementButton />} {showButtons.map((button) => buttons[button])}
{showButtons.includes("upgradeToPlus") && <UpgradeToPlus />}
{showButtons.includes("user") && <UserButton />}
</div> </div>
</div> </div>
); );

View File

@ -1,22 +1,20 @@
import Link from "next/link";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FaBrain } from "react-icons/fa"; import { FaBrain } from "react-icons/fa";
import { sidebarLinkStyle } from "@/lib/components/Sidebar/components/SidebarFooter/styles/SidebarLinkStyle";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext"; import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { SidebarFooterButton } from "./SidebarFooterButton";
export const BrainManagementButton = (): JSX.Element => { export const BrainManagementButton = (): JSX.Element => {
const { currentBrainId } = useBrainContext(); const { currentBrainId } = useBrainContext();
const { t } = useTranslation("brain"); const { t } = useTranslation("brain");
return ( return (
<Link <SidebarFooterButton
href={`/brains-management/${currentBrainId ?? ""}`} href={`/brains-management/${currentBrainId ?? ""}`}
className={sidebarLinkStyle} icon={<FaBrain className="w-8 h-8" />}
label={t("myBrains")}
data-testid="brain-management-button" data-testid="brain-management-button"
> />
<FaBrain className="w-8 h-8" />
<span>{t("myBrains")}</span>
</Link>
); );
}; };

View File

@ -0,0 +1,34 @@
import { useRouter } from "next/navigation";
type SidebarFooterButtonProps = {
icon: JSX.Element;
label: string | JSX.Element;
href?: string;
onClick?: () => void;
};
export const SidebarFooterButton = ({
icon,
label,
href,
onClick,
}: SidebarFooterButtonProps): JSX.Element => {
const router = useRouter();
if (href !== undefined) {
onClick = () => {
void router.push(href);
};
}
return (
<button
type="button"
className="w-full rounded-lg px-5 py-2 text-base flex justify-start items-center gap-4 hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-primary focus:outline-none"
onClick={onClick}
>
{icon}
<span className="text-ellipsis overflow-hidden">{label}</span>
</button>
);
};

View File

@ -5,7 +5,7 @@ import { FiUser } from "react-icons/fi";
import { StripePricingModal } from "@/lib/components/Stripe"; import { StripePricingModal } from "@/lib/components/Stripe";
import { useUserData } from "@/lib/hooks/useUserData"; import { useUserData } from "@/lib/hooks/useUserData";
import { sidebarLinkStyle } from "../styles/SidebarLinkStyle"; import { SidebarFooterButton } from "./SidebarFooterButton";
export const UpgradeToPlus = (): JSX.Element => { export const UpgradeToPlus = (): JSX.Element => {
const { userData } = useUserData(); const { userData } = useUserData();
@ -20,15 +20,17 @@ export const UpgradeToPlus = (): JSX.Element => {
return ( return (
<StripePricingModal <StripePricingModal
Trigger={ Trigger={
<button type="button" className={sidebarLinkStyle}> <SidebarFooterButton
<FiUser className="w-8 h-8" /> icon={<FiUser className="w-8 h-8" />}
<span> label={
{t("upgrade")}{" "} <>
<span className="rounded bg-primary/50 py-1 px-3 text-xs"> {t("upgrade")}{" "}
{t("new")} <span className="rounded bg-primary/50 py-1 px-3 text-xs">
</span> {t("new")}
</span> </span>
</button> </>
}
/>
} }
/> />
); );

View File

@ -1,21 +1,18 @@
import Link from "next/link";
import { Avatar } from "@/lib/components/ui/Avatar"; import { Avatar } from "@/lib/components/ui/Avatar";
import { useSupabase } from "@/lib/context/SupabaseProvider"; import { useSupabase } from "@/lib/context/SupabaseProvider";
import { SidebarFooterButton } from "./SidebarFooterButton";
import { useGravatar } from "../../../../../hooks/useGravatar"; import { useGravatar } from "../../../../../hooks/useGravatar";
import { sidebarLinkStyle } from "../styles/SidebarLinkStyle";
export const UserButton = (): JSX.Element => { export const UserButton = (): JSX.Element => {
const { session } = useSupabase(); const { session } = useSupabase();
const { gravatarUrl } = useGravatar(); const { gravatarUrl } = useGravatar();
return ( return (
<Link aria-label="account" className={sidebarLinkStyle} href={"/user"}> <SidebarFooterButton
<Avatar url={gravatarUrl} alt="user-gravatar" /> href={"/user"}
<span className="text-ellipsis overflow-hidden"> icon={<Avatar url={gravatarUrl} />}
{session?.user.email ?? ""} label={session?.user.email ?? ""}
</span> />
</Link>
); );
}; };

View File

@ -1,2 +0,0 @@
export const sidebarLinkStyle =
"w-full rounded-lg px-5 py-2 text-base flex justify-start items-center gap-4 hover:bg-gray-200 dark:hover:bg-gray-800 focus:outline-none";

View File

@ -4,20 +4,18 @@ import { cn } from "@/lib/utils";
type AvatarProps = { type AvatarProps = {
url: string; url: string;
alt: string;
imgClassName?: string; imgClassName?: string;
className?: string; className?: string;
}; };
export const Avatar = ({ export const Avatar = ({
url, url,
alt,
imgClassName, imgClassName,
className, className,
}: AvatarProps): JSX.Element => { }: AvatarProps): JSX.Element => {
return ( return (
<div className={cn("relative w-8 h-8", className)}> <div className={cn("relative w-8 h-8 shrink-0", className)}>
<Image <Image
alt={alt} alt="avatar"
fill={true} fill={true}
sizes="32px" sizes="32px"
src={url} src={url}