fixed bug scroll area pada setting components

This commit is contained in:
vergiLgood1 2025-04-11 15:34:34 +07:00
parent 258205ef49
commit 1f8a6b18df
7 changed files with 95 additions and 200 deletions

View File

@ -61,14 +61,8 @@ const ImportData = () => {
}, []); }, []);
return ( return (
<ScrollArea <div className="space-y-14 w-full max-w-4xl mx-auto">
className={`h-[${scrollAreaHeight}] w-full`} <div className="space-y-14 p-8 max-w-4xl mx-auto">
scrollable={isScrollable}
>
<div
ref={contentRef}
className="min-h-screen p-8 max-w-4xl mx-auto space-y-8"
>
<div className="space-y-4 mb-4"> <div className="space-y-4 mb-4">
<h1 className="font-medium">Import data</h1> <h1 className="font-medium">Import data</h1>
<Separator /> <Separator />
@ -134,7 +128,7 @@ const ImportData = () => {
</div> </div>
</div> */} </div> */}
</div> </div>
</ScrollArea> </div>
); );
}; };

View File

@ -93,14 +93,8 @@ export default function NotificationsSetting() {
}; };
return ( return (
<ScrollArea <div className="space-y-14 w-full max-w-4xl mx-auto">
className="h-[calc(100vh-140px)] w-full" <div className="space-y-14 p-8 max-w-4xl mx-auto">
scrollable={isScrollable}
>
<div
ref={contentRef}
className="min-h-screen p-8 max-w-4xl mx-auto space-y-14"
>
<div> <div>
<div className="space-y-4 mb-4"> <div className="space-y-4 mb-4">
<h1 className="font-medium">Notifications</h1> <h1 className="font-medium">Notifications</h1>
@ -174,6 +168,6 @@ export default function NotificationsSetting() {
</div> </div>
</div> </div>
</div> </div>
</ScrollArea> </div>
); );
} }

View File

@ -241,8 +241,8 @@ export default function PreferencesSettings() {
} }
return ( return (
<ScrollArea className="h-[calc(100vh-140px)] w-full"> <div className="space-y-14 w-full max-w-4xl mx-auto">
<div className="min-h-screen p-8 max-w-4xl mx-auto space-y-14"> <div className="space-y-14 p-8 max-w-4xl mx-auto">
<div> <div>
<div className="space-y-4 mb-4"> <div className="space-y-4 mb-4">
<h1 className="font-medium">Preferences</h1> <h1 className="font-medium">Preferences</h1>
@ -428,6 +428,6 @@ export default function PreferencesSettings() {
</div> </div>
</div> </div>
</div> </div>
</ScrollArea> </div>
); );
} }

View File

@ -1,67 +1,25 @@
"use client"; "use client"
import { Loader2, ImageIcon } from "lucide-react"
import type React from "react"; import { Form, FormControl, FormField, FormItem, FormMessage } from "@/app/_components/ui/form"
import { Input } from "@/app/_components/ui/input"
import { Button } from "@/app/_components/ui/button"
import { Avatar, AvatarFallback, AvatarImage } from "@/app/_components/ui/avatar"
import { Label } from "@/app/_components/ui/label"
import { Separator } from "@/app/_components/ui/separator"
import { Switch } from "@/app/_components/ui/switch"
import { useProfileFormHandlers } from "../../dashboard/user-management/_handlers/use-profile-form"
import { CTexts } from "@/app/_utils/const/texts"
import type { IUserSchema } from "@/src/entities/models/users/users.model"; export function ProfileSettings() {
const { form, fileInputRef, handleFileChange, handleAvatarClick, isPending, user } = useProfileFormHandlers()
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Loader2, ImageIcon } from "lucide-react";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/app/_components/ui/form";
import { Input } from "@/app/_components/ui/input";
import { Button } from "@/app/_components/ui/button";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/app/_components/ui/avatar";
import { Label } from "@/app/_components/ui/label";
import { Separator } from "@/app/_components/ui/separator";
import { Switch } from "@/app/_components/ui/switch";
import { useRef, useState } from "react";
import { ScrollArea } from "@/app/_components/ui/scroll-area";
import {
updateUser,
} from "@/app/(pages)/(admin)/dashboard/user-management/action";
import { useProfileFormHandlers } from "../../dashboard/user-management/_handlers/use-profile-form";
import { CTexts } from "@/app/_utils/const/texts";
const profileFormSchema = z.object({
username: z.string().nullable().optional(),
avatar: z.string().nullable().optional(),
});
type ProfileFormValues = z.infer<typeof profileFormSchema>;
interface ProfileSettingsProps {
user: IUserSchema | null;
}
export function ProfileSettings({ user }: ProfileSettingsProps) {
const email = user?.email || "";
const username = user?.profile?.username || "";
const {
form,
fileInputRef,
handleFileChange,
handleAvatarClick,
isPending,
onSubmit,
} = useProfileFormHandlers({ user });
const email = user?.email || ""
const username = user?.profile?.username || ""
return ( return (
<ScrollArea className="h-[calc(100vh-140px)] w-full "> <div className="space-y-14 w-full max-w-4xl mx-auto">
<div className="space-y-14 min-h-screen p-8 max-w-4xl mx-auto"> <div className="space-y-14 p-8 max-w-4xl mx-auto">
<Form {...form}> <Form {...form}>
<form onSubmit={() => { }} className="space-y-8"> <form onSubmit={() => { }} className="space-y-8">
<div className="space-y-4"> <div className="space-y-4">
@ -70,22 +28,14 @@ export function ProfileSettings({ user }: ProfileSettingsProps) {
<Separator className="" /> <Separator className="" />
</div> </div>
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div <div className="relative cursor-pointer group" onClick={handleAvatarClick}>
className="relative cursor-pointer group"
onClick={handleAvatarClick}
>
<Avatar className="h-16 w-16"> <Avatar className="h-16 w-16">
{isPending ? ( {isPending ? (
<div className="h-full w-full bg-muted animate-pulse rounded-full" /> <div className="h-full w-full bg-muted animate-pulse rounded-full" />
) : ( ) : (
<> <>
<AvatarImage <AvatarImage src={user?.profile?.avatar || ""} alt={username} />
src={user?.profile?.avatar || ""} <AvatarFallback>{username?.[0]?.toUpperCase() || email?.[0]?.toUpperCase()}</AvatarFallback>
alt={username}
/>
<AvatarFallback>
{username?.[0]?.toUpperCase() || email?.[0]?.toUpperCase()}
</AvatarFallback>
</> </>
)} )}
<div className="absolute inset-0 flex items-center justify-center bg-black/50 rounded-full opacity-0 group-hover:opacity-100 transition-opacity"> <div className="absolute inset-0 flex items-center justify-center bg-black/50 rounded-full opacity-0 group-hover:opacity-100 transition-opacity">
@ -166,9 +116,7 @@ export function ProfileSettings({ user }: ProfileSettingsProps) {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<Label>Password</Label> <Label>Password</Label>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">Set a permanent password to login to your account.</p>
Set a permanent password to login to your account.
</p>
</div> </div>
<Button variant="outline" size="sm"> <Button variant="outline" size="sm">
Change password Change password
@ -179,8 +127,7 @@ export function ProfileSettings({ user }: ProfileSettingsProps) {
<div> <div>
<Label>2-step verification</Label> <Label>2-step verification</Label>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Add an additional layer of security to your account during Add an additional layer of security to your account during login.
login.
</p> </p>
</div> </div>
<Button variant="outline" size="sm"> <Button variant="outline" size="sm">
@ -222,8 +169,7 @@ export function ProfileSettings({ user }: ProfileSettingsProps) {
<div> <div>
<Label className="text-destructive">Delete account</Label> <Label className="text-destructive">Delete account</Label>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Permanently delete the account and remove access from all Permanently delete the account and remove access from all workspaces.
workspaces.
</p> </p>
</div> </div>
<Button variant="outline" size="sm" className="text-destructive"> <Button variant="outline" size="sm" className="text-destructive">
@ -233,6 +179,6 @@ export function ProfileSettings({ user }: ProfileSettingsProps) {
</div> </div>
</div> </div>
</div> </div>
</ScrollArea> </div>
); )
} }

View File

@ -4,12 +4,12 @@
import { Button } from "@/app/_components/ui/button"; import { Button } from "@/app/_components/ui/button";
import { Separator } from "@/app/_components/ui/separator"; import { Separator } from "@/app/_components/ui/separator";
import { IUserSchema } from "@/src/entities/models/users/users.model"; import { IUserSchema } from "@/src/entities/models/users/users.model";
import { useGetCurrentUserQuery } from "../../dashboard/user-management/_queries/queries";
interface SecuritySettingsProps { export function SecuritySettings() {
user: IUserSchema | null;
} const { data: user } = useGetCurrentUserQuery();
export function SecuritySettings({ user }: SecuritySettingsProps) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>

View File

@ -1,78 +1,52 @@
"use client"; "use client"
import type React from "react"
import { cn } from "@/app/_lib/utils"; import { cn } from "@/app/_lib/utils"
import { import { Dialog, DialogContent, DialogTrigger } from "@/app/_components/ui/dialog"
Dialog, import { ScrollArea } from "@/app/_components/ui/scroll-area"
DialogContent, import { Separator } from "@/app/_components/ui/separator"
DialogTrigger, import { Avatar, AvatarFallback, AvatarImage } from "@/app/_components/ui/avatar"
} from "@/app/_components/ui/dialog"; import { IconAdjustmentsHorizontal, IconBell, IconFileExport, IconFileImport, IconUser } from "@tabler/icons-react"
import { ScrollArea } from "@/app/_components/ui/scroll-area"; import { ProfileSettings } from "./profile-settings"
import { Separator } from "@/app/_components/ui/separator"; import { DialogTitle } from "@radix-ui/react-dialog"
import { import { useState } from "react"
Avatar,
AvatarFallback,
AvatarImage,
} from "@/app/_components/ui/avatar";
import {
IconAdjustmentsHorizontal,
IconBaselineDensityLarge,
IconBell,
IconFileExport,
IconFileImport,
IconFingerprint,
IconLock,
IconPlugConnected,
IconSettings,
IconUser,
IconUsers,
IconWorld,
} from "@tabler/icons-react";
import { ProfileSettings } from "./profile-settings";
import { DialogTitle } from "@radix-ui/react-dialog";
import { useState } from "react";
import NotificationsSetting from "./notification-settings"; import NotificationsSetting from "./notification-settings"
import PreferencesSettings from "./preference-settings"; import PreferencesSettings from "./preference-settings"
import ImportData from "./import-data"; import ImportData from "./import-data"
import { IUserSchema } from "@/src/entities/models/users/users.model";
import { useUserStore } from "@/app/_lib/zustand/stores/user"; import { useGetCurrentUserQuery } from "../../dashboard/user-management/_queries/queries"
interface SettingsDialogProps { interface SettingsDialogProps {
trigger: React.ReactNode; trigger: React.ReactNode
defaultTab?: string; defaultTab?: string
open?: boolean; open?: boolean
onOpenChange?: (open: boolean) => void; onOpenChange?: (open: boolean) => void
} }
interface SettingsTab { interface SettingsTab {
id: string; id: string
icon: typeof IconUser; icon: typeof IconUser
title: string; title: string
content: React.ReactNode; content: React.ReactNode
} }
interface SettingsSection { interface SettingsSection {
title: string; title: string
tabs: SettingsTab[]; tabs: SettingsTab[]
} }
export function SettingsDialog({ export function SettingsDialog({ trigger, defaultTab = "account", open, onOpenChange }: SettingsDialogProps) {
trigger, const { data: user, isPending } = useGetCurrentUserQuery()
defaultTab = "account",
open,
onOpenChange,
}: SettingsDialogProps) {
const { user, isPending } = useUserStore(); const [selectedTab, setSelectedTab] = useState(defaultTab)
const [selectedTab, setSelectedTab] = useState(defaultTab);
// Get user display name // Get user display name
const preferredName = user?.profile?.username || ""; const preferredName = user?.profile?.username || ""
const userEmail = user?.email || ""; const userEmail = user?.email || ""
const displayName = preferredName || userEmail?.split("@")[0] || "User"; const displayName = preferredName || userEmail?.split("@")[0] || "User"
const userAvatar = user?.profile?.avatar || ""; const userAvatar = user?.profile?.avatar || ""
const sections: SettingsSection[] = [ const sections: SettingsSection[] = [
{ {
@ -82,7 +56,7 @@ export function SettingsDialog({
id: "account", id: "account",
icon: IconUser, icon: IconUser,
title: "My Account", title: "My Account",
content: <ProfileSettings user={user} />, content: <ProfileSettings />,
}, },
{ {
id: "preferences", id: "preferences",
@ -115,17 +89,15 @@ export function SettingsDialog({
}, },
], ],
}, },
]; ]
const currentTab = sections const currentTab = sections.flatMap((section) => section.tabs).find((tab) => tab.id === selectedTab)
.flatMap((section) => section.tabs)
.find((tab) => tab.id === selectedTab);
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogTitle></DialogTitle> <DialogTitle></DialogTitle>
<DialogTrigger asChild>{trigger}</DialogTrigger> <DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogContent className="max-w-[1200px] gap-0 p-0"> <DialogContent className="max-w-[1200px] gap-0 p-0 h-[600px]">
<div className="grid h-[600px] grid-cols-[250px,1fr]"> <div className="grid h-[600px] grid-cols-[250px,1fr]">
{/* Sidebar */} {/* Sidebar */}
<div className="border-r bg-muted/50"> <div className="border-r bg-muted/50">
@ -137,25 +109,17 @@ export function SettingsDialog({
) : ( ) : (
<Avatar className="h-8 w-8"> <Avatar className="h-8 w-8">
<AvatarImage src={userAvatar} alt={displayName} /> <AvatarImage src={userAvatar} alt={displayName} />
<AvatarFallback> <AvatarFallback>{displayName[0].toUpperCase()}</AvatarFallback>
{displayName[0].toUpperCase()}
</AvatarFallback>
</Avatar> </Avatar>
)} )}
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{isPending ? ( {isPending ? <div className="h-4 w-24 rounded bg-muted animate-pulse" /> : displayName}
<div className="h-4 w-24 rounded bg-muted animate-pulse" />
) : (
displayName
)}
</span> </span>
</div> </div>
{sections.map((section, index) => ( {sections.map((section, index) => (
<div key={section.title} className="py-2"> <div key={section.title} className="py-2">
<div className="px-3 py-2"> <div className="px-3 py-2">
<h3 className="text-sm font-medium text-muted-foreground"> <h3 className="text-sm font-medium text-muted-foreground">{section.title}</h3>
{section.title}
</h3>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
{section.tabs.map((tab) => ( {section.tabs.map((tab) => (
@ -166,7 +130,7 @@ export function SettingsDialog({
"flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium", "flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium",
tab.id === selectedTab tab.id === selectedTab
? "bg-accent text-accent-foreground" ? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground" : "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
)} )}
> >
<tab.icon className="h-4 w-4" /> <tab.icon className="h-4 w-4" />
@ -174,9 +138,7 @@ export function SettingsDialog({
</button> </button>
))} ))}
</div> </div>
{index < sections.length - 1 && ( {index < sections.length - 1 && <Separator className="mx-3 my-2" />}
<Separator className="mx-3 my-2" />
)}
</div> </div>
))} ))}
</div> </div>
@ -184,17 +146,17 @@ export function SettingsDialog({
</div> </div>
{/* Content */} {/* Content */}
<div className="flex flex-col"> <div className="flex flex-col h-full">
<div className="flex-1"> {isPending ? (
{isPending ? ( <div className="h-full w-full animate-pulse bg-muted" />
<div className="h-full w-full animate-pulse bg-muted" /> ) : (
) : ( <ScrollArea className="h-[600px]">
currentTab?.content <div className="p-6">{currentTab?.content}</div>
)} </ScrollArea>
</div> )}
</div> </div>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); )
} }

View File

@ -14,6 +14,7 @@ import { toast } from "sonner"
import { CNumbers } from "@/app/_utils/const/numbers" import { CNumbers } from "@/app/_utils/const/numbers"
import { CTexts } from "@/app/_utils/const/texts" import { CTexts } from "@/app/_utils/const/texts"
import { useUserActionsHandler } from "./actions/use-user-actions" import { useUserActionsHandler } from "./actions/use-user-actions"
import { useGetCurrentUserQuery } from "../_queries/queries"
// Profile update form schema // Profile update form schema
const profileFormSchema = z.object({ const profileFormSchema = z.object({
@ -26,14 +27,11 @@ const profileFormSchema = z.object({
type ProfileFormValues = z.infer<typeof profileFormSchema> type ProfileFormValues = z.infer<typeof profileFormSchema>
interface ProfileFormProps { export const useProfileFormHandlers = () => {
user: IUserSchema | null
onSuccess?: () => void
}
export const useProfileFormHandlers = ({ user, onSuccess }: ProfileFormProps) => {
const { invalidateUsers, invalidateCurrentUser, invalidateUser } = useUserActionsHandler() const { invalidateUsers, invalidateCurrentUser, invalidateUser } = useUserActionsHandler()
const { data: user } = useGetCurrentUserQuery()
const { const {
mutateAsync: updateUser, mutateAsync: updateUser,
isPending, isPending,
@ -176,7 +174,7 @@ export const useProfileFormHandlers = ({ user, onSuccess }: ProfileFormProps) =>
invalidateUser(user.id) invalidateUser(user.id)
// Call success callback // Call success callback
onSuccess?.() // onSuccess?.()
} catch (error) { } catch (error) {
console.error("Error updating profile:", error) console.error("Error updating profile:", error)
toast.error("Error updating profile") toast.error("Error updating profile")
@ -191,6 +189,7 @@ export const useProfileFormHandlers = ({ user, onSuccess }: ProfileFormProps) =>
isPending, isPending,
avatarPreview, avatarPreview,
fileInputRef, fileInputRef,
user,
} }
} }