use zustand to store management

This commit is contained in:
vergiLgood1 2025-04-05 21:15:37 +07:00
parent b4eb2dc1ba
commit 22265fc2d2
12 changed files with 208 additions and 160 deletions

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import * as React from "react"; import { useEffect } from "react";
import { NavMain } from "@/app/(pages)/(admin)/_components/navigations/nav-main"; import { NavMain } from "@/app/(pages)/(admin)/_components/navigations/nav-main";
import { NavReports } from "@/app/(pages)/(admin)/_components/navigations/nav-report"; import { NavReports } from "@/app/(pages)/(admin)/_components/navigations/nav-report";
@ -17,26 +17,25 @@ import { NavPreMain } from "./navigations/nav-pre-main";
import { navData } from "@/prisma/data/nav"; import { navData } from "@/prisma/data/nav";
import { TeamSwitcher } from "../../../_components/team-switcher"; import { TeamSwitcher } from "../../../_components/team-switcher";
import { useGetCurrentUserQuery } from "../dashboard/user-management/_queries/queries"; import { useGetCurrentUserQuery } from "../dashboard/user-management/_queries/queries";
import { useUserStore } from "@/app/_utils/zustand/stores/user";
import { useUserActionsHandler } from "../dashboard/user-management/_handlers/actions/use-user-actions";
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) { export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const { data: user, isPending, error } = useGetCurrentUserQuery() const { data: user, isPending, error } = useGetCurrentUserQuery()
// React.useEffect(() => { const { setUser, setIsPending } = useUserStore();
// async function fetchUser() {
// try {
// setIsLoading(true);
// const userData = await getCurrentUser();
// setUser(userData.data.user);
// } catch (error) {
// console.error("Failed to fetch user:", error);
// } finally {
// setIsLoading(false);
// }
// }
// fetchUser(); // Set pending state
// }, []); useEffect(() => {
setIsPending(isPending);
}, [isPending, setIsPending]);
useEffect(() => {
if (user) {
setUser(user);
}
}, [user, setUser]);
return ( return (
<Sidebar collapsible="icon" {...props}> <Sidebar collapsible="icon" {...props}>
@ -49,7 +48,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<NavReports reports={navData.reports} /> <NavReports reports={navData.reports} />
</SidebarContent> </SidebarContent>
<SidebarFooter> <SidebarFooter>
<NavUser user={user ?? null} /> <NavUser />
</SidebarFooter> </SidebarFooter>
<SidebarRail /> <SidebarRail />
</Sidebar> </Sidebar>

View File

@ -29,8 +29,18 @@ import type { IUserSchema } from "@/src/entities/models/users/users.model";
import { SettingsDialog } from "../settings/setting-dialog"; import { SettingsDialog } from "../settings/setting-dialog";
import { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogCancel, AlertDialogAction } from "@/app/_components/ui/alert-dialog"; import { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogCancel, AlertDialogAction } from "@/app/_components/ui/alert-dialog";
import { useSignOutHandler } from "@/app/(pages)/(auth)/_handlers/use-sign-out"; import { useSignOutHandler } from "@/app/(pages)/(auth)/_handlers/use-sign-out";
import { useGetCurrentUserQuery } from "../../dashboard/user-management/_queries/queries";
import { useUserStore } from "@/app/_utils/zustand/stores/user";
interface NavUserProps {
user: IUserSchema | null;
isPending: boolean;
}
export function NavUser() {
const { user, isPending } = useUserStore()
export function NavUser({ user }: { user: IUserSchema | null }) {
const { isMobile } = useSidebar(); const { isMobile } = useSidebar();
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
@ -59,9 +69,9 @@ export function NavUser({ user }: { user: IUserSchema | null }) {
return "U"; return "U";
}; };
const { handleSignOut, isPending, errors, error } = useSignOutHandler(); const { handleSignOut, isPending: isSignOutPending, errors, error: isSignOutError } = useSignOutHandler();
function LogoutButton({ handleSignOut, isPending }: { handleSignOut: () => void; isPending: boolean }) { function LogoutButton({ handleSignOut, isSignOutPending }: { handleSignOut: () => void; isSignOutPending: boolean }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
@ -72,11 +82,20 @@ export function NavUser({ user }: { user: IUserSchema | null }) {
e.preventDefault(); e.preventDefault();
setOpen(true); // Buka dialog saat diklik setOpen(true); // Buka dialog saat diklik
}} }}
disabled={isPending} disabled={isSignOutPending}
className="space-x-2" className="space-x-2"
> >
{isSignOutPending ? (
<>
<Loader2 className="size-4 animate-spin" />
<span>Logging out...</span>
</>
) : (
<>
<IconLogout className="size-4" /> <IconLogout className="size-4" />
<span>Log out</span> <span>Log out</span>
</>
)}
</DropdownMenuItem> </DropdownMenuItem>
{/* Alert Dialog */} {/* Alert Dialog */}
@ -94,14 +113,14 @@ export function NavUser({ user }: { user: IUserSchema | null }) {
onClick={() => { onClick={() => {
handleSignOut(); handleSignOut();
if (!isPending) { if (!isSignOutPending) {
setOpen(false); setOpen(false);
} }
}} }}
className="btn btn-primary" className="btn btn-primary"
disabled={isPending} disabled={isSignOutPending}
> >
{isPending ? ( {isSignOutPending ? (
<> <>
<Loader2 className="size-4" /> <Loader2 className="size-4" />
<span>Logging You Out...</span> <span>Logging You Out...</span>
@ -126,6 +145,16 @@ export function NavUser({ user }: { user: IUserSchema | null }) {
size="lg" size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
> >
{isPending ? (
<div className="flex items-center space-x-2 animate-pulse">
<div className="h-8 w-8 rounded-lg bg-muted" />
<div className="flex-1 space-y-1">
<div className="h-4 w-24 rounded bg-muted" />
<div className="h-3 w-16 rounded bg-muted" />
</div>
</div>
) : (
<>
<Avatar className="h-8 w-8 rounded-lg"> <Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={userAvatar || ""} alt={username} /> <AvatarImage src={userAvatar || ""} alt={username} />
<AvatarFallback className="rounded-lg"> <AvatarFallback className="rounded-lg">
@ -137,8 +166,11 @@ export function NavUser({ user }: { user: IUserSchema | null }) {
<span className="truncate text-xs">{userEmail}</span> <span className="truncate text-xs">{userEmail}</span>
</div> </div>
<ChevronsUpDown className="ml-auto size-4" /> <ChevronsUpDown className="ml-auto size-4" />
</>
)}
</SidebarMenuButton> </SidebarMenuButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
{!isPending && (
<DropdownMenuContent <DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg" className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"} side={isMobile ? "bottom" : "right"}
@ -171,7 +203,6 @@ export function NavUser({ user }: { user: IUserSchema | null }) {
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuGroup> <DropdownMenuGroup>
<SettingsDialog <SettingsDialog
user={user}
trigger={ trigger={
<DropdownMenuItem <DropdownMenuItem
className="space-x-2" className="space-x-2"
@ -186,8 +217,9 @@ export function NavUser({ user }: { user: IUserSchema | null }) {
/> />
</DropdownMenuGroup> </DropdownMenuGroup>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<LogoutButton handleSignOut={handleSignOut} isPending={isPending} /> <LogoutButton handleSignOut={handleSignOut} isSignOutPending={isSignOutPending} />
</DropdownMenuContent> </DropdownMenuContent>
)}
</DropdownMenu> </DropdownMenu>
</SidebarMenuItem> </SidebarMenuItem>
</SidebarMenu> </SidebarMenu>

View File

@ -46,65 +46,6 @@ interface ProfileSettingsProps {
} }
export function ProfileSettings({ user }: ProfileSettingsProps) { export function ProfileSettings({ user }: ProfileSettingsProps) {
// const [isPending, setIsepisPending] = useState(false);
// const fileInputRef = useRef<HTMLInputElement>(null);
// // Use profile data with fallbacks
// const username = user?.profile?.username || "";
// const email = user?.email || "";
// const userAvatar = user?.profile?.avatar || "";
// const form = useForm<ProfileFormValues>({
// resolver: zodResolver(profileFormSchema),
// defaultValues: {
// username: username || "",
// avatar: userAvatar || "",
// },
// });
// const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
// const file = e.target.files?.[0];
// if (!file || !user?.id || !user?.email) return;
// try {
// setIsepisPending(true);
// // Upload avatar to storage
// // const publicUrl = await uploadAvatar(user.id, user.email, file);
// // console.log("publicUrl", publicUrl);
// // Update the form value
// // form.setValue("avatar", publicUrl);
// } catch (error) {
// console.error("Error uploading avatar:", error);
// } finally {
// setIsepisPending(false);
// }
// };
// const handleAvatarClick = () => {
// fileInputRef.current?.click();
// };
// async function onSubmit(data: ProfileFormValues) {
// try {
// if (!user?.id) return;
// // Update profile in database
// const { error } = await updateUser(user.id, {
// profile: {
// avatar: data.avatar || undefined,
// username: data.username || undefined,
// },
// });
// if (error) throw error;
// } catch (error) {
// console.error("Error updating profile:", error);
// }
const email = user?.email || ""; const email = user?.email || "";
const username = user?.profile?.username || ""; const username = user?.profile?.username || "";

View File

@ -1,11 +1,12 @@
"use client"; "use client";
import type { User } from "@/src/entities/models/users/users.model";
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";
interface SecuritySettingsProps { interface SecuritySettingsProps {
user: User | null; user: IUserSchema | null;
} }
export function SecuritySettings({ user }: SecuritySettingsProps) { export function SecuritySettings({ user }: SecuritySettingsProps) {

View File

@ -36,9 +36,9 @@ 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 { IUserSchema } from "@/src/entities/models/users/users.model";
import { useUserStore } from "@/app/_utils/zustand/stores/user";
interface SettingsDialogProps { interface SettingsDialogProps {
user: IUserSchema | null;
trigger: React.ReactNode; trigger: React.ReactNode;
defaultTab?: string; defaultTab?: string;
open?: boolean; open?: boolean;
@ -58,12 +58,14 @@ interface SettingsSection {
} }
export function SettingsDialog({ export function SettingsDialog({
user,
trigger, trigger,
defaultTab = "account", defaultTab = "account",
open, open,
onOpenChange, onOpenChange,
}: SettingsDialogProps) { }: SettingsDialogProps) {
const { user, isPending } = useUserStore();
const [selectedTab, setSelectedTab] = useState(defaultTab); const [selectedTab, setSelectedTab] = useState(defaultTab);
// Get user display name // Get user display name
@ -130,13 +132,23 @@ export function SettingsDialog({
<ScrollArea className="h-[600px]"> <ScrollArea className="h-[600px]">
<div className="p-2"> <div className="p-2">
<div className="flex items-center gap-2 px-3 py-2"> <div className="flex items-center gap-2 px-3 py-2">
{isPending ? (
<div className="h-8 w-8 rounded-full bg-muted animate-pulse" />
) : (
<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()} {displayName[0].toUpperCase()}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<span className="text-sm font-medium">{displayName}</span> )}
<span className="text-sm font-medium">
{isPending ? (
<div className="h-4 w-24 rounded bg-muted animate-pulse" />
) : (
displayName
)}
</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">
@ -173,7 +185,13 @@ export function SettingsDialog({
{/* Content */} {/* Content */}
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex-1">{currentTab?.content}</div> <div className="flex-1">
{isPending ? (
<div className="h-full w-full animate-pulse bg-muted" />
) : (
currentTab?.content
)}
</div>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>

View File

@ -39,9 +39,8 @@ export const useSignInWithPasswordHandler = () => {
formData.append('email', data.email); formData.append('email', data.email);
formData.append('password', data.password); formData.append('password', data.password);
try { try {
toast.promise( await toast.promise(
signInWithPassword(formData), signInWithPassword(formData),
{ {
loading: 'Signing in...', loading: 'Signing in...',
@ -52,6 +51,7 @@ export const useSignInWithPasswordHandler = () => {
}, },
error: (err) => { error: (err) => {
const errorMessage = err?.message || 'Failed to send email.'; const errorMessage = err?.message || 'Failed to send email.';
setError(errorMessage); setError(errorMessage);
return errorMessage; return errorMessage;
} }
@ -63,6 +63,7 @@ export const useSignInWithPasswordHandler = () => {
} }
}); });
// Extract the validation error message for the email field // Extract the validation error message for the email field
const getFieldErrorMessage = (fieldName: keyof ISignInWithPasswordSchema) => { const getFieldErrorMessage = (fieldName: keyof ISignInWithPasswordSchema) => {
return formErrors[fieldName]?.message || ''; return formErrors[fieldName]?.message || '';

View File

@ -62,8 +62,12 @@ export async function signInWithPassword(formData: FormData) {
const email = formData.get("email")?.toString() const email = formData.get("email")?.toString()
const password = formData.get("password")?.toString() const password = formData.get("password")?.toString()
console.log("woi:", email + " " + password)
const signInWithPasswordController = getInjection("ISignInWithPasswordController") const signInWithPasswordController = getInjection("ISignInWithPasswordController")
return await signInWithPasswordController({ email, password }) await signInWithPasswordController({ email, password })
return { success: true }
} catch (err) { } catch (err) {
if (err instanceof InputParseError) { if (err instanceof InputParseError) {
return { error: err.message } return { error: err.message }

View File

@ -29,6 +29,7 @@ export function SignInWithPasswordForm({
// Get the current active handler based on state // Get the current active handler based on state
const activeHandler = isSignInWithPassword ? passwordHandler : passwordlessHandler; const activeHandler = isSignInWithPassword ? passwordHandler : passwordlessHandler;
// Toggle password form field // Toggle password form field
const togglePasswordField = () => { const togglePasswordField = () => {
setIsSignInWithPassword(!isSignInWithPassword); setIsSignInWithPassword(!isSignInWithPassword);

View File

@ -0,0 +1,18 @@
import { IUserSchema } from "@/src/entities/models/users/users.model";
import { create } from "zustand";
interface UserState {
user: IUserSchema | null;
isPending: boolean;
setUser: (user: IUserSchema | null) => void;
setIsPending: (isPending: boolean) => void;
logout: () => void;
}
export const useUserStore = create<UserState>((set) => ({
user: null,
isPending: false,
setUser: (user) => set({ user }),
setIsPending: (isPending) => set({ isPending }),
logout: () => set({ user: null, isPending: false }),
}));

View File

@ -50,7 +50,8 @@
"resend": "^4.1.2", "resend": "^4.1.2",
"sonner": "^2.0.1", "sonner": "^2.0.1",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"zod": "^3.24.2" "zod": "^3.24.2",
"zustand": "^5.0.3"
}, },
"devDependencies": { "devDependencies": {
"@tanstack/eslint-plugin-query": "^5.67.2", "@tanstack/eslint-plugin-query": "^5.67.2",
@ -10787,6 +10788,35 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
},
"node_modules/zustand": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz",
"integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
} }
} }
} }

View File

@ -55,7 +55,8 @@
"resend": "^4.1.2", "resend": "^4.1.2",
"sonner": "^2.0.1", "sonner": "^2.0.1",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"zod": "^3.24.2" "zod": "^3.24.2",
"zustand": "^5.0.3"
}, },
"devDependencies": { "devDependencies": {
"@tanstack/eslint-plugin-query": "^5.67.2", "@tanstack/eslint-plugin-query": "^5.67.2",

View File

@ -59,6 +59,8 @@ export class AuthenticationService implements IAuthenticationService {
const supabase = await this.supabaseServer const supabase = await this.supabaseServer
const { email, password } = credentials const { email, password } = credentials
const signIn = supabase.auth.signInWithPassword({ email, password }) const signIn = supabase.auth.signInWithPassword({ email, password })
const { data: { session }, error } = await this.instrumentationService.startSpan({ const { data: { session }, error } = await this.instrumentationService.startSpan({