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

View File

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

View File

@ -241,8 +241,8 @@ export default function PreferencesSettings() {
}
return (
<ScrollArea className="h-[calc(100vh-140px)] w-full">
<div className="min-h-screen p-8 max-w-4xl mx-auto space-y-14">
<div className="space-y-14 w-full max-w-4xl mx-auto">
<div className="space-y-14 p-8 max-w-4xl mx-auto">
<div>
<div className="space-y-4 mb-4">
<h1 className="font-medium">Preferences</h1>
@ -428,6 +428,6 @@ export default function PreferencesSettings() {
</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";
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 });
export function ProfileSettings() {
const { form, fileInputRef, handleFileChange, handleAvatarClick, isPending, user } = useProfileFormHandlers()
const email = user?.email || ""
const username = user?.profile?.username || ""
return (
<ScrollArea className="h-[calc(100vh-140px)] w-full ">
<div className="space-y-14 min-h-screen p-8 max-w-4xl mx-auto">
<div className="space-y-14 w-full max-w-4xl mx-auto">
<div className="space-y-14 p-8 max-w-4xl mx-auto">
<Form {...form}>
<form onSubmit={() => { }} className="space-y-8">
<div className="space-y-4">
@ -70,22 +28,14 @@ export function ProfileSettings({ user }: ProfileSettingsProps) {
<Separator className="" />
</div>
<div className="flex items-start gap-4">
<div
className="relative cursor-pointer group"
onClick={handleAvatarClick}
>
<div className="relative cursor-pointer group" onClick={handleAvatarClick}>
<Avatar className="h-16 w-16">
{isPending ? (
<div className="h-full w-full bg-muted animate-pulse rounded-full" />
) : (
<>
<AvatarImage
src={user?.profile?.avatar || ""}
alt={username}
/>
<AvatarFallback>
{username?.[0]?.toUpperCase() || email?.[0]?.toUpperCase()}
</AvatarFallback>
<AvatarImage src={user?.profile?.avatar || ""} 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">
@ -166,9 +116,7 @@ export function ProfileSettings({ user }: ProfileSettingsProps) {
<div className="flex items-center justify-between">
<div>
<Label>Password</Label>
<p className="text-sm text-muted-foreground">
Set a permanent password to login to your account.
</p>
<p className="text-sm text-muted-foreground">Set a permanent password to login to your account.</p>
</div>
<Button variant="outline" size="sm">
Change password
@ -179,8 +127,7 @@ export function ProfileSettings({ user }: ProfileSettingsProps) {
<div>
<Label>2-step verification</Label>
<p className="text-sm text-muted-foreground">
Add an additional layer of security to your account during
login.
Add an additional layer of security to your account during login.
</p>
</div>
<Button variant="outline" size="sm">
@ -222,8 +169,7 @@ export function ProfileSettings({ user }: ProfileSettingsProps) {
<div>
<Label className="text-destructive">Delete account</Label>
<p className="text-sm text-muted-foreground">
Permanently delete the account and remove access from all
workspaces.
Permanently delete the account and remove access from all workspaces.
</p>
</div>
<Button variant="outline" size="sm" className="text-destructive">
@ -233,6 +179,6 @@ export function ProfileSettings({ user }: ProfileSettingsProps) {
</div>
</div>
</div>
</ScrollArea>
);
</div>
)
}

View File

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

View File

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

View File

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