-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: user management view #9
Changes from 4 commits
1d1ece2
ec4c044
650b50a
b09d11c
bc5aec5
76bcd8f
ddea850
586bd21
b57e833
8348d2e
d9c8061
72d024f
72e1008
68ecab2
4f3dd25
1566cfe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { useMutation } from "@tanstack/react-query"; | ||
import { AxiosError } from "axios"; | ||
import { toast } from "sonner"; | ||
import { useAuthStore } from "~/modules/Auth/authStore"; | ||
import { ApiClient } from "../api-client"; | ||
import { ChangePasswordBody } from "../generated-api"; | ||
|
||
type ChangePasswordOptions = { | ||
data: ChangePasswordBody; | ||
}; | ||
|
||
export function useChangePassword() { | ||
const { currentUser } = useAuthStore(); | ||
if (!currentUser) { | ||
throw new Error("User is not logged in"); | ||
} | ||
|
||
return useMutation({ | ||
mutationFn: async (options: ChangePasswordOptions) => { | ||
const response = await ApiClient.users.usersControllerChangePassword( | ||
currentUser.id, | ||
options.data | ||
); | ||
|
||
return response.data; | ||
}, | ||
onSuccess: () => { | ||
toast.success("Password updated successfully"); | ||
}, | ||
onError: (error) => { | ||
if (error instanceof AxiosError) { | ||
return toast.error(error.response?.data.message); | ||
} | ||
toast.error(error.message); | ||
}, | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,14 +10,17 @@ type LoginUserOptions = { | |
}; | ||
|
||
export function useLoginUser() { | ||
const { setLoggedIn } = useAuthStore(); | ||
const { setLoggedIn, setCurrentUser } = useAuthStore(); | ||
return useMutation({ | ||
mutationFn: async (options: LoginUserOptions) => { | ||
const response = await ApiClient.auth.authControllerLogin(options.data); | ||
|
||
if (!response) throw new Error("Invalid username or password"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @k1eu any ideas on how to move to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it should actually go to onError if you throw or the ApiCLient throws here 🤔 |
||
|
||
return response.data; | ||
}, | ||
onSuccess: () => { | ||
onSuccess: (data) => { | ||
setCurrentUser(data.data); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd add a /current-user endpoint and treat useCurrentUser() - as react query hook for that with longer revalidate time, then in the |
||
setLoggedIn(true); | ||
}, | ||
onError: (error) => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { useMutation } from "@tanstack/react-query"; | ||
import { AxiosError } from "axios"; | ||
import { toast } from "sonner"; | ||
import { useAuthStore } from "~/modules/Auth/authStore"; | ||
import { ApiClient } from "../api-client"; | ||
import { UpdateUserBody } from "../generated-api"; | ||
|
||
type UpdateUserOptions = { | ||
data: UpdateUserBody; | ||
}; | ||
|
||
export function useUpdateUser() { | ||
const { setCurrentUser, currentUser } = useAuthStore(); | ||
if (!currentUser) { | ||
throw new Error("User is not logged in"); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd remove that, user shouldn't be able to get into that action ui wise, and if SOMEHOW he gets there - he should be blocked on BE roles |
||
|
||
return useMutation({ | ||
mutationFn: async (options: UpdateUserOptions) => { | ||
const response = await ApiClient.users.usersControllerUpdateUser( | ||
currentUser.id, | ||
options.data | ||
); | ||
|
||
return response.data; | ||
}, | ||
onSuccess: (data) => { | ||
setCurrentUser(data.data); | ||
toast.success("User updated successfully"); | ||
}, | ||
onError: (error) => { | ||
if (error instanceof AxiosError) { | ||
return toast.error(error.response?.data.message); | ||
} | ||
toast.error(error.message); | ||
}, | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { Link, Outlet, useLocation } from "@remix-run/react"; | ||
import { replace } from "lodash-es"; | ||
import { cn } from "~/lib/utils"; | ||
|
||
export default function SettingsLayout() { | ||
const { hash } = useLocation(); | ||
const currentTab = replace(hash, "#", "") || "user-info"; | ||
|
||
const navigationItems = [ | ||
{ | ||
id: "user-info", | ||
title: "User Info", | ||
}, | ||
{ | ||
id: "change-password", | ||
title: "Change Password", | ||
}, | ||
]; | ||
|
||
return ( | ||
<div className="mx-auto grid w-full max-w-6xl items-start gap-6 md:grid-cols-[180px_1fr] lg:grid-cols-[250px_1fr] p-4"> | ||
<nav className="grid gap-4 text-sm text-muted-foreground"> | ||
<h1 className="text-3xl font-semibold text-primary">Settings</h1> | ||
{navigationItems.map((item) => ( | ||
<Link | ||
key={item.id} | ||
to={`#${item.id}`} | ||
className={cn({ | ||
"text-primary": currentTab === item.id, | ||
})} | ||
> | ||
{item.title} | ||
</Link> | ||
))} | ||
</nav> | ||
<Outlet /> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import ChangePasswordForm from "./forms/ChangePasswordForm"; | ||
import UserForm from "./forms/UserForm"; | ||
|
||
export default function SettingsPage() { | ||
return ( | ||
<div className="grid gap-6"> | ||
<UserForm /> | ||
<ChangePasswordForm /> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { zodResolver } from "@hookform/resolvers/zod"; | ||
import { useForm } from "react-hook-form"; | ||
import { z } from "zod"; | ||
import { ChangePasswordBody } from "~/api/generated-api"; | ||
import { Button } from "~/components/ui/button"; | ||
import { | ||
Card, | ||
CardHeader, | ||
CardTitle, | ||
CardDescription, | ||
CardContent, | ||
CardFooter, | ||
} from "~/components/ui/card"; | ||
import { Input } from "~/components/ui/input"; | ||
import { Label } from "~/components/ui/label"; | ||
import { useChangePassword } from "~/api/mutations/useChangePassword"; | ||
import { cn } from "~/lib/utils"; | ||
|
||
const passwordSchema = z.object({ | ||
oldPassword: z.string().min(8), | ||
newPassword: z.string().min(8), | ||
}); | ||
|
||
export default function ChangePasswordForm() { | ||
const { mutate: changePassword } = useChangePassword(); | ||
const { | ||
register, | ||
handleSubmit, | ||
formState: { errors }, | ||
} = useForm<ChangePasswordBody>({ resolver: zodResolver(passwordSchema) }); | ||
|
||
const onSubmit = (data: ChangePasswordBody) => { | ||
changePassword({ data }); | ||
}; | ||
|
||
return ( | ||
<Card id="user-info"> | ||
<form onSubmit={handleSubmit(onSubmit)}> | ||
<CardHeader> | ||
<CardTitle>Change password</CardTitle> | ||
<CardDescription>Update your password here.</CardDescription> | ||
</CardHeader> | ||
<CardContent className="space-y-2"> | ||
<Label htmlFor="oldPassword">Old Password</Label> | ||
<Input | ||
id="oldPassword" | ||
className={cn({ "border-red-500": errors.oldPassword })} | ||
{...register("oldPassword", { | ||
required: "Old password is required", | ||
})} | ||
/> | ||
{errors.oldPassword && ( | ||
<p className="text-red-500 text-sm mt-1"> | ||
{errors.oldPassword.message} | ||
</p> | ||
)} | ||
<Label htmlFor="newPassword">New Password</Label> | ||
<Input | ||
id="newPassword" | ||
className={cn({ "border-red-500": errors.newPassword })} | ||
{...register("newPassword", { | ||
required: "New password is required", | ||
})} | ||
/> | ||
{errors.newPassword && ( | ||
<p className="text-red-500 text-sm mt-1"> | ||
{errors.newPassword.message} | ||
</p> | ||
)} | ||
</CardContent> | ||
<CardFooter className="border-t px-6 py-4"> | ||
<Button type="submit">Save</Button> | ||
</CardFooter> | ||
</form> | ||
</Card> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { zodResolver } from "@hookform/resolvers/zod"; | ||
import { useForm } from "react-hook-form"; | ||
import { z } from "zod"; | ||
import { UpdateUserBody } from "~/api/generated-api"; | ||
import { useUpdateUser } from "~/api/mutations/useUpdateUser"; | ||
import { Button } from "~/components/ui/button"; | ||
import { | ||
Card, | ||
CardHeader, | ||
CardTitle, | ||
CardDescription, | ||
CardContent, | ||
CardFooter, | ||
} from "~/components/ui/card"; | ||
import { Input } from "~/components/ui/input"; | ||
import { Label } from "~/components/ui/label"; | ||
import { cn } from "~/lib/utils"; | ||
import { useAuthStore } from "~/modules/Auth/authStore"; | ||
|
||
const updateUserSchema = z.object({ | ||
email: z.string().email(), | ||
}); | ||
|
||
export default function UserForm() { | ||
const { mutate: updateUser } = useUpdateUser(); | ||
const currentUser = useAuthStore.getState().currentUser; | ||
|
||
const { | ||
register, | ||
handleSubmit, | ||
formState: { errors }, | ||
} = useForm<UpdateUserBody>({ resolver: zodResolver(updateUserSchema) }); | ||
|
||
const onSubmit = (data: UpdateUserBody) => { | ||
updateUser({ data }); | ||
}; | ||
|
||
return ( | ||
<Card id="change-password"> | ||
<form onSubmit={handleSubmit(onSubmit)}> | ||
<CardHeader> | ||
<CardTitle>Change user information</CardTitle> | ||
<CardDescription>Update your user information here.</CardDescription> | ||
</CardHeader> | ||
<CardContent className="space-y-2"> | ||
<Label htmlFor="email">Email</Label> | ||
<Input | ||
id="email" | ||
placeholder="user@email.com" | ||
defaultValue={currentUser?.email} | ||
className={cn({ "border-red-500": errors.email })} | ||
{...register("email", { required: "Email is required" })} | ||
/> | ||
{errors.email && ( | ||
<p className="text-red-500 text-sm mt-1">{errors.email.message}</p> | ||
)} | ||
</CardContent> | ||
<CardFooter className="border-t px-6 py-4"> | ||
<Button>Save</Button> | ||
</CardFooter> | ||
</form> | ||
</Card> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd remove this, if user is not logged in backend will return 401