Skip to content

Commit

Permalink
[recnet-web] Subscription setting UI (#350)
Browse files Browse the repository at this point in the history
## Description

<!--- Describe your changes in detail -->
Finish subscription setting UI to select subscription type and channels.

Misc:
- Refactor code (split code) for UserSettingDialog
- Add setting button in User Dropdown 
    
<img width="363" alt="Screenshot 2024-11-07 at 6 19 58 PM"
src="https://github.com/user-attachments/assets/376b45de-9eff-4f23-9e32-d4532f9c62d9">


## Related Issue

<!--- This project only accepts pull requests related to open issues -->
<!--- If suggesting a new feature or change, please discuss it in an
issue first -->
<!--- If fixing a bug, there should be an issue describing it with steps
to reproduce -->
<!--- Please link to the issue here: -->

## Notes

<!-- Other thing to say -->

## Test

<!--- Please describe in detail how you tested your changes locally. -->
Open Setting Dialog and play with the subscription form
- For weekly digest, should show error if trying to unsubscribe all
channels
- After save, verify changes in DB using `prisma:studio` or sql client

## Screenshots (if appropriate):

<!--- Add screenshots of your changes here -->
<img width="652" alt="Screenshot 2024-11-07 at 6 22 20 PM"
src="https://github.com/user-attachments/assets/28628173-f325-4dae-b341-65d3c182e201">

## TODO

- [x] Clear `console.log` or `console.error` for debug usage
- [x] Update the documentation `recnet-docs` if needed
  • Loading branch information
swh00tw authored Nov 8, 2024
2 parents 399bbe2 + b14821d commit 07e3e11
Show file tree
Hide file tree
Showing 13 changed files with 799 additions and 313 deletions.
6 changes: 6 additions & 0 deletions apps/recnet/src/app/Headerbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { useState, useRef, useEffect, useCallback } from "react";

import { useAuth } from "@recnet/recnet-web/app/AuthContext";
import { Avatar } from "@recnet/recnet-web/components/Avatar";
import { useUserSettingDialogContext } from "@recnet/recnet-web/components/setting/UserSettingDialog";
import { UserRole } from "@recnet/recnet-web/constant";
import { logout, useGoogleLogin } from "@recnet/recnet-web/firebase/auth";
import { cn } from "@recnet/recnet-web/utils/cn";
Expand All @@ -29,6 +30,8 @@ export function UserDropdown({ user }: { user: User }) {
await logout();
router.push("/");
};
const { setOpen: setUserSettingDialogOpen } = useUserSettingDialogContext();

return (
<DropdownMenu.Root>
<DropdownMenu.Trigger>
Expand All @@ -43,6 +46,9 @@ export function UserDropdown({ user }: { user: User }) {
<DropdownMenu.Item asChild>
<Link href={`/${user.handle}`}>Profile</Link>
</DropdownMenu.Item>
<DropdownMenu.Item onClick={() => setUserSettingDialogOpen(true)}>
Settings
</DropdownMenu.Item>
{user.role && user.role === UserRole.ADMIN ? (
<DropdownMenu.Item asChild>
<Link href={`/admin`}>Admin Panel</Link>
Expand Down
24 changes: 20 additions & 4 deletions apps/recnet/src/app/[handle]/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@ import { Avatar } from "@recnet/recnet-web/components/Avatar";
import { FollowButton } from "@recnet/recnet-web/components/FollowButton";
import { RecNetLink } from "@recnet/recnet-web/components/Link";
import { Skeleton, SkeletonText } from "@recnet/recnet-web/components/Skeleton";
import { useUserSettingDialogContext } from "@recnet/recnet-web/components/setting/UserSettingDialog";
import { cn } from "@recnet/recnet-web/utils/cn";
import { interleaveWithValue } from "@recnet/recnet-web/utils/interleaveWithValue";

import { UserSettingDialog } from "./UserSettingDialog";

function StatDivider() {
return <div className="w-[1px] bg-gray-6 h-[18px] flex" />;
}
Expand All @@ -28,6 +27,7 @@ export function Profile(props: { handle: string }) {
});
const { user: me } = useAuth();
const isMe = !!me && !!data?.user && me.handle === data.user.handle;
const { setOpen: setUserSettingDialogOpen } = useUserSettingDialogContext();

const userUrl = useMemo(
() => (data?.user?.url ? new URL(data.user.url) : null),
Expand Down Expand Up @@ -184,7 +184,15 @@ export function Profile(props: { handle: string }) {
</Flex>
<Flex className="w-fit hidden md:flex">
{isMe ? (
<UserSettingDialog handle={data.user.handle} />
<Button
className="w-full cursor-pointer"
variant="surface"
onClick={() => {
setUserSettingDialogOpen(true);
}}
>
Settings
</Button>
) : (
<FollowButton user={data.user} />
)}
Expand All @@ -196,7 +204,15 @@ export function Profile(props: { handle: string }) {
<div className="sm:hidden">{userInfo}</div>
<Flex className="w-full md:hidden">
{isMe ? (
<UserSettingDialog handle={data.user.handle} />
<Button
className="w-full cursor-pointer"
variant="surface"
onClick={() => {
setUserSettingDialogOpen(true);
}}
>
Settings
</Button>
) : (
<FollowButton user={data.user} />
)}
Expand Down
21 changes: 12 additions & 9 deletions apps/recnet/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Toaster } from "sonner";
import { Footer } from "@recnet/recnet-web/app/Footer";
import { Headerbar } from "@recnet/recnet-web/app/Headerbar";
import { clientEnv } from "@recnet/recnet-web/clientEnv";
import { UserSettingDialogProvider } from "@recnet/recnet-web/components/setting/UserSettingDialog";
import { getUserServerSide } from "@recnet/recnet-web/utils/getUserServerSide";

import { ApiErrorBoundary } from "./ApiErrorBoundary";
Expand Down Expand Up @@ -43,15 +44,17 @@ export default async function RootLayout({
<HistoryProvider>
<ThemeProvider attribute="class">
<Theme accentColor="blue">
<Headerbar />
<Toaster position="top-right" richColors offset={80} />
<ApiErrorBoundary>
<div className="min-h-[90svh] flex justify-center">
{children}
</div>
</ApiErrorBoundary>
<Footer />
<MobileNavigator />
<UserSettingDialogProvider>
<Headerbar />
<Toaster position="top-right" richColors offset={80} />
<ApiErrorBoundary>
<div className="min-h-[90svh] flex justify-center">
{children}
</div>
</ApiErrorBoundary>
<Footer />
<MobileNavigator />
</UserSettingDialogProvider>
</Theme>
</ThemeProvider>
</HistoryProvider>
Expand Down
127 changes: 127 additions & 0 deletions apps/recnet/src/components/setting/UserSettingDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"use client";

import {
PersonIcon,
Cross1Icon,
EnvelopeClosedIcon,
} from "@radix-ui/react-icons";
import { Dialog, Button, Text } from "@radix-ui/themes";
import { Settings } from "lucide-react";
import React, { useMemo, useState, createContext, useContext } from "react";

import { useAuth } from "@recnet/recnet-web/app/AuthContext";
import { cn } from "@recnet/recnet-web/utils/cn";

import { AccountSetting } from "./account/AccountSetting";
import { ProfileEditForm } from "./profile/ProfileEditForm";
import { SubscriptionSetting } from "./subscription/SubscriptionSetting";

const tabs = {
PROFILE: {
label: "Profile",
icon: <PersonIcon />,
component: ProfileEditForm,
},
ACCOUNT: {
label: "Account",
icon: <Settings className="w-[15px] h-[15px]" />,
component: AccountSetting,
},
SUBSCRIPTION: {
label: "Subscription",
icon: <EnvelopeClosedIcon />,
component: SubscriptionSetting,
},
} as const;
type TabKey = keyof typeof tabs;

const UserSettingDialogContext = createContext<{
open: boolean;
setOpen: (open: boolean) => void;
activeTab: TabKey;
setActiveTab: (tab: TabKey) => void;
} | null>(null);

export function useUserSettingDialogContext() {
const context = useContext(UserSettingDialogContext);
if (!context) {
throw new Error(
"useUserSettingDialog must be used within a UserSettingDialogProvider"
);
}
return context;
}

interface UserSettingDialogProps {
children: React.ReactNode;
}

export function UserSettingDialogProvider(props: UserSettingDialogProps) {
const { children } = props;
const { user } = useAuth();
const [open, setOpen] = useState(false);
const [activeTab, setActiveTab] = useState<TabKey>("PROFILE");

const TabComponent = useMemo(() => tabs[activeTab].component, [activeTab]);

if (!user) {
return children;
}

return (
<UserSettingDialogContext.Provider
value={{
open,
setOpen,
activeTab,
setActiveTab,
}}
>
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Content
maxWidth={{
initial: "480px",
md: "640px",
}}
className="relative"
>
<Dialog.Close className="absolute top-6 right-4 hidden md:block">
<Button variant="soft" color="gray" className="cursor-pointer">
<Cross1Icon />
</Button>
</Dialog.Close>
<div className="flex flex-row gap-x-8 md:p-4">
<div className="w-fit md:w-[25%] flex flex-col gap-y-2">
{Object.entries(tabs).map(([key, { label }]) => (
<div
key={key}
className={cn(
"py-1 px-4 rounded-2 flex gap-x-2 items-center cursor-pointer hover:bg-gray-4",
{
"bg-gray-5": key === activeTab,
}
)}
onClick={() => setActiveTab(key as TabKey)}
>
{tabs[key as TabKey].icon}
<Text
size={{
initial: "1",
md: "2",
}}
>
{label}
</Text>
</div>
))}
</div>
<div className="w-full h-[600px] md:h-[750px] overflow-y-auto">
<TabComponent />
</div>
</div>
</Dialog.Content>
</Dialog.Root>
{children}
</UserSettingDialogContext.Provider>
);
}
49 changes: 49 additions & 0 deletions apps/recnet/src/components/setting/account/AccountSetting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client";

import { Dialog, Button, Text } from "@radix-ui/themes";
import { useRouter } from "next/navigation";

import { trpc } from "@recnet/recnet-web/app/_trpc/client";
import { DoubleConfirmButton } from "@recnet/recnet-web/components/DoubleConfirmButton";
import { logout } from "@recnet/recnet-web/firebase/auth";

export function AccountSetting() {
const deactivateMutation = trpc.deactivate.useMutation();
const router = useRouter();

return (
<div>
<Dialog.Title>Account Setting</Dialog.Title>
<Dialog.Description size="2" mb="4" className="text-gray-11">
Make changes to account settings.
</Dialog.Description>

<Text size="4" color="red" className="block">
Deactivate Account
</Text>
<Text size="1" className="text-gray-11 block">
{
"Your account will be deactivated and you will be logged out. You can reactivate your account by logging in again."
}
{
" While your account is deactivated, your profile will be hidden from other users. You will not receive any weekly digest emails."
}
</Text>
<div className="flex flex-row w-full mt-4">
<DoubleConfirmButton
onConfirm={async () => {
await deactivateMutation.mutateAsync();
await logout();
router.replace("/");
}}
title="Deactivate Account"
description="Are you sure you want to deactivate your account?"
>
<Button color="red" className="bg-red-10 cursor-pointer">
Deactivate Account
</Button>
</DoubleConfirmButton>
</div>
</div>
);
}
Loading

0 comments on commit 07e3e11

Please sign in to comment.