Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
andresgnlez committed Oct 10, 2024
1 parent ee691c0 commit 71f970e
Show file tree
Hide file tree
Showing 10 changed files with 389 additions and 6 deletions.
2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/typeorm": "^10.0.2",
"@ts-rest/nest": "^3.51.0",
"@ts-rest/nest": "3.45.2",
"@types/multer": "1.4.12",
"bcrypt": "catalog:",
"class-transformer": "catalog:",
Expand Down
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@types/node": "catalog:",
"@types/react": "^18",
"@types/react-dom": "^18",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"eslint": "^8",
"eslint-config-next": "14.2.8",
"eslint-plugin-prettier": "5.2.1",
Expand Down
158 changes: 158 additions & 0 deletions client/src/containers/profile/account-details/form/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"use client";

import { FC, KeyboardEvent, useCallback, useRef, useState } from "react";

import { useForm } from "react-hook-form";

import { zodResolver } from "@hookform/resolvers/zod";
import { useQueryClient } from "@tanstack/react-query";
import { SquarePenIcon, XIcon } from "lucide-react";
import { useSession } from "next-auth/react";
import { z } from "zod";

import { client } from "@/lib/query-client";
import { queryKeys } from "@/lib/query-keys";

import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useToast } from "@/components/ui/toast/use-toast";

import { accountDetailsSchema } from "./schema";

const AccountDetailsForm: FC = () => {
const queryClient = useQueryClient();
const { data: session, update: updateSession } = useSession();
const [isEditing, setEditing] = useState(false);
const formRef = useRef<HTMLFormElement>(null);
const { toast } = useToast();

const { data: user } = client.user.findMe.useQuery(
queryKeys.users.me(session?.user?.id as string).queryKey,
{
extraHeaders: {
authorization: `Bearer ${session?.accessToken}`,
},
query: {},
},
{
select: (res) => res.body.data,
},
);

const form = useForm<z.infer<typeof accountDetailsSchema>>({
resolver: zodResolver(accountDetailsSchema),
defaultValues: {
name: user?.name,
email: user?.email,
},
mode: "onSubmit",
});

const onSubmit = useCallback(
async (data: FormData) => {
const formData = Object.fromEntries(data);
const parsed = accountDetailsSchema.safeParse(formData);

if (parsed.success) {
const response = await client.user.updateUser.mutation({
params: {
id: session?.user?.id as string,
},
body: {
email: parsed.data.email,
},
extraHeaders: {
authorization: `Bearer ${session?.accessToken}`,
},
});

if (response.status === 200) {
await updateSession(response.body);

await queryClient.invalidateQueries({
queryKey: queryKeys.users.me(session?.user?.id as string).queryKey,
});

toast({
description: "Your email has been updated successfully.",
});
}
}
},
[queryClient, session, updateSession, toast],
);

const handleEnterKey = useCallback(
(evt: KeyboardEvent) => {
if (evt.code === "Enter" && isEditing && form.formState.isValid) {
form.handleSubmit(async () => {
await onSubmit(new FormData(formRef.current!));
})();
}
},
[isEditing, form, onSubmit],
);

return (
<Form {...form}>
<form
ref={formRef}
className="w-full space-y-4"
onSubmit={(evt) => {
form.handleSubmit(async () => {
await onSubmit(new FormData(formRef.current!));
})(evt);
}}
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<div className="relative flex items-center">
<Input
type="email"
autoComplete={field.name}
readOnly={!isEditing}
onKeyDown={handleEnterKey}
{...field}
onBlur={() => {
field.onBlur();
setEditing(false);
}}
/>
<button
type="button"
onClick={() => {
setEditing((prev) => !prev);
form.setFocus("email");
}}
className="absolute right-20 text-muted-foreground"
>
{isEditing ? (
<XIcon className="h-4 w-4" />
) : (
<SquarePenIcon className="h-4 w-4" />
)}
</button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
);
};

export default AccountDetailsForm;
9 changes: 9 additions & 0 deletions client/src/containers/profile/account-details/form/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { z } from "zod";

export const accountDetailsSchema = z.object({
email: z
.string({ message: "Email is required" })
.min(1, "Email is required")
.email("Invalid email"),
name: z.string({ message: "Name is required" }).min(1, "Name is required"),
});
9 changes: 9 additions & 0 deletions client/src/containers/profile/account-details/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { FC } from "react";

import AccountDetailsForm from "./form";

const AccountDetails: FC = () => {
return <AccountDetailsForm />;
};

export default AccountDetails;
32 changes: 28 additions & 4 deletions client/src/containers/profile/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,40 @@

import { signOut, useSession } from "next-auth/react";

import { client } from "@/lib/query-client";
import { queryKeys } from "@/lib/query-keys";

import AccountDetailsForm from "@/containers/profile/account-details/form";
import UpdatePassword from "@/containers/profile/update-password";

import { Button } from "@/components/ui/button";

export default function Profile() {
const { data: session } = useSession();

const { data } = client.user.findMe.useQuery(
queryKeys.users.me(
session?.accessToken as NonNullable<typeof session>["accessToken"],
).queryKey,
{
extraHeaders: {
authorization: `Bearer ${session?.accessToken}`,
},
query: {},
},
{
select: (res) => res.body.data,
},
);

console.log(data);

return (
<div>
<p>Welcome {session?.user?.name}</p>
<p>Email: {session?.user?.email}</p>
<p>role: {session?.user?.role}</p>
<div className="container my-10">
<div className="grid grid-cols-2 gap-4">
<AccountDetailsForm />
<UpdatePassword />
</div>

<Button
variant="link"
Expand Down
155 changes: 155 additions & 0 deletions client/src/containers/profile/update-password/form/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"use client";

import { FC, useCallback, useRef } from "react";

import { useForm } from "react-hook-form";

import { zodResolver } from "@hookform/resolvers/zod";
import { useSession } from "next-auth/react";
import { z } from "zod";

import { client } from "@/lib/query-client";

import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
FormDescription,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useToast } from "@/components/ui/toast/use-toast";

import { changePasswordSchema } from "./schema";

const UpdatePassword: FC = () => {
const formRef = useRef<HTMLFormElement>(null);
const { data: session } = useSession();
const { toast } = useToast();

const form = useForm<z.infer<typeof changePasswordSchema>>({
resolver: zodResolver(changePasswordSchema),
defaultValues: {
password: "",
newPassword: "",
},
mode: "onSubmit",
});

const onSubmit = useCallback(
async (data: FormData) => {
const formData = Object.fromEntries(data);
const parsed = changePasswordSchema.safeParse(formData);
if (parsed.success) {
try {
const response = await client.user.updatePassword.mutation({
body: {
currentPassword: parsed.data.password,
newPassword: parsed.data.newPassword,
},
extraHeaders: {
authorization: `Bearer ${session?.accessToken}`,
},
});

if (response.status === 200) {
toast({
description: "Your password has been updated successfully.",
});
}

if (response.status === 400 || response.status === 401) {
toast({
variant: "destructive",
description: response.body.errors?.[0].title,
});
}
} catch (e) {
toast({
variant: "destructive",
description: "Something went wrong updating the password",
});
}
}
},
[session, toast],
);

return (
<Form {...form}>
<form
ref={formRef}
className="w-full space-y-4"
onSubmit={(evt) => {
evt.preventDefault();
form.handleSubmit(async () => {
await onSubmit(new FormData(formRef.current!));
})(evt);
}}
>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<div className="relative flex items-center">
<Input
placeholder="Type your current password"
type="password"
autoComplete="current-password"
{...field}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="newPassword"
render={({ field, fieldState }) => (
<FormItem>
<FormLabel>New password</FormLabel>
<FormControl>
<div className="relative flex items-center">
<Input
placeholder="Create new password"
type="password"
autoComplete="new-password"
{...field}
/>
</div>
</FormControl>
{!fieldState.invalid && (
<FormDescription>
Password must contain at least 8 characters.
</FormDescription>
)}
<FormMessage />
</FormItem>
)}
/>

<div className="!mt-10 px-8">
<Button
variant="secondary"
type="submit"
className="w-full"
disabled={!form.formState.isValid}
>
Apply
</Button>
</div>
</form>
</Form>
);
};

export default UpdatePassword;
Loading

0 comments on commit 71f970e

Please sign in to comment.