use zustand to store management
This commit is contained in:
parent
b4eb2dc1ba
commit
22265fc2d2
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
<IconLogout className="size-4" />
|
||||
<span>Log out</span>
|
||||
{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,68 +145,81 @@ export function NavUser({ user }: { user: IUserSchema | null }) {
|
|||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={userAvatar || ""} alt={username} />
|
||||
<AvatarFallback className="rounded-lg">
|
||||
{getInitials()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">{username}</span>
|
||||
<span className="truncate text-xs">{userEmail}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto size-4" />
|
||||
{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">
|
||||
{getInitials()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">{username}</span>
|
||||
<span className="truncate text-xs">{userEmail}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto size-4" />
|
||||
</>
|
||||
)}
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={userAvatar || ""} alt={username} />
|
||||
<AvatarFallback className="rounded-lg">
|
||||
{getInitials()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">
|
||||
{username}
|
||||
</span>
|
||||
<span className="truncate text-xs">{userEmail}</span>
|
||||
{!isPending && (
|
||||
<DropdownMenuContent
|
||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={userAvatar || ""} alt={username} />
|
||||
<AvatarFallback className="rounded-lg">
|
||||
{getInitials()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">
|
||||
{username}
|
||||
</span>
|
||||
<span className="truncate text-xs">{userEmail}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem className="space-x-2">
|
||||
<IconSparkles className="size-4" />
|
||||
<span>Upgrade to Pro</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<SettingsDialog
|
||||
user={user}
|
||||
trigger={
|
||||
<DropdownMenuItem
|
||||
className="space-x-2"
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<IconSettings className="size-4" />
|
||||
<span>Settings</span>
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
/>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<LogoutButton handleSignOut={handleSignOut} isPending={isPending} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem className="space-x-2">
|
||||
<IconSparkles className="size-4" />
|
||||
<span>Upgrade to Pro</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<SettingsDialog
|
||||
trigger={
|
||||
<DropdownMenuItem
|
||||
className="space-x-2"
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<IconSettings className="size-4" />
|
||||
<span>Settings</span>
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
/>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<LogoutButton handleSignOut={handleSignOut} isSignOutPending={isSignOutPending} />
|
||||
</DropdownMenuContent>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
|
|
|
@ -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 || "";
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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">
|
||||
<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>
|
||||
{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">
|
||||
{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>
|
||||
|
|
|
@ -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 || '';
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 }),
|
||||
}));
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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({
|
||||
|
|
Loading…
Reference in New Issue