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";
import * as React from "react";
import { useEffect } from "react";
import { NavMain } from "@/app/(pages)/(admin)/_components/navigations/nav-main";
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 { TeamSwitcher } from "../../../_components/team-switcher";
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>) {
const { data: user, isPending, error } = useGetCurrentUserQuery()
// React.useEffect(() => {
// 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);
// }
// }
const { setUser, setIsPending } = useUserStore();
// fetchUser();
// }, []);
// Set pending state
useEffect(() => {
setIsPending(isPending);
}, [isPending, setIsPending]);
useEffect(() => {
if (user) {
setUser(user);
}
}, [user, setUser]);
return (
<Sidebar collapsible="icon" {...props}>
@ -49,7 +48,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<NavReports reports={navData.reports} />
</SidebarContent>
<SidebarFooter>
<NavUser user={user ?? null} />
<NavUser />
</SidebarFooter>
<SidebarRail />
</Sidebar>

View File

@ -29,8 +29,18 @@ import type { IUserSchema } from "@/src/entities/models/users/users.model";
import { SettingsDialog } from "../settings/setting-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 { 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 [isDialogOpen, setIsDialogOpen] = useState(false);
@ -59,9 +69,9 @@ export function NavUser({ user }: { user: IUserSchema | null }) {
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);
return (
@ -72,11 +82,20 @@ export function NavUser({ user }: { user: IUserSchema | null }) {
e.preventDefault();
setOpen(true); // Buka dialog saat diklik
}}
disabled={isPending}
disabled={isSignOutPending}
className="space-x-2"
>
{isSignOutPending ? (
<>
<Loader2 className="size-4 animate-spin" />
<span>Logging out...</span>
</>
) : (
<>
<IconLogout className="size-4" />
<span>Log out</span>
</>
)}
</DropdownMenuItem>
{/* Alert Dialog */}
@ -94,14 +113,14 @@ export function NavUser({ user }: { user: IUserSchema | null }) {
onClick={() => {
handleSignOut();
if (!isPending) {
if (!isSignOutPending) {
setOpen(false);
}
}}
className="btn btn-primary"
disabled={isPending}
disabled={isSignOutPending}
>
{isPending ? (
{isSignOutPending ? (
<>
<Loader2 className="size-4" />
<span>Logging You Out...</span>
@ -126,6 +145,16 @@ export function NavUser({ user }: { user: IUserSchema | null }) {
size="lg"
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">
<AvatarImage src={userAvatar || ""} alt={username} />
<AvatarFallback className="rounded-lg">
@ -137,8 +166,11 @@ export function NavUser({ user }: { user: IUserSchema | null }) {
<span className="truncate text-xs">{userEmail}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</>
)}
</SidebarMenuButton>
</DropdownMenuTrigger>
{!isPending && (
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
@ -171,7 +203,6 @@ export function NavUser({ user }: { user: IUserSchema | null }) {
<DropdownMenuSeparator />
<DropdownMenuGroup>
<SettingsDialog
user={user}
trigger={
<DropdownMenuItem
className="space-x-2"
@ -186,8 +217,9 @@ export function NavUser({ user }: { user: IUserSchema | null }) {
/>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<LogoutButton handleSignOut={handleSignOut} isPending={isPending} />
<LogoutButton handleSignOut={handleSignOut} isSignOutPending={isSignOutPending} />
</DropdownMenuContent>
)}
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>

View File

@ -46,65 +46,6 @@ interface 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 username = user?.profile?.username || "";

View File

@ -1,11 +1,12 @@
"use client";
import type { User } from "@/src/entities/models/users/users.model";
import { Button } from "@/app/_components/ui/button";
import { Separator } from "@/app/_components/ui/separator";
import { IUserSchema } from "@/src/entities/models/users/users.model";
interface SecuritySettingsProps {
user: User | null;
user: IUserSchema | null;
}
export function SecuritySettings({ user }: SecuritySettingsProps) {

View File

@ -36,9 +36,9 @@ 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/_utils/zustand/stores/user";
interface SettingsDialogProps {
user: IUserSchema | null;
trigger: React.ReactNode;
defaultTab?: string;
open?: boolean;
@ -58,12 +58,14 @@ interface SettingsSection {
}
export function SettingsDialog({
user,
trigger,
defaultTab = "account",
open,
onOpenChange,
}: SettingsDialogProps) {
const { user, isPending } = useUserStore();
const [selectedTab, setSelectedTab] = useState(defaultTab);
// Get user display name
@ -130,13 +132,23 @@ export function SettingsDialog({
<ScrollArea className="h-[600px]">
<div className="p-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">
<AvatarImage src={userAvatar} alt={displayName} />
<AvatarFallback>
{displayName[0].toUpperCase()}
</AvatarFallback>
</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>
{sections.map((section, index) => (
<div key={section.title} className="py-2">
@ -173,7 +185,13 @@ export function SettingsDialog({
{/* Content */}
<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>
</DialogContent>

View File

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

View File

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

View File

@ -29,6 +29,7 @@ export function SignInWithPasswordForm({
// Get the current active handler based on state
const activeHandler = isSignInWithPassword ? passwordHandler : passwordlessHandler;
// Toggle password form field
const togglePasswordField = () => {
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",
"sonner": "^2.0.1",
"vaul": "^1.1.2",
"zod": "^3.24.2"
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
"devDependencies": {
"@tanstack/eslint-plugin-query": "^5.67.2",
@ -10787,6 +10788,35 @@
"funding": {
"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",
"sonner": "^2.0.1",
"vaul": "^1.1.2",
"zod": "^3.24.2"
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
"devDependencies": {
"@tanstack/eslint-plugin-query": "^5.67.2",

View File

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