-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
1,088 additions
and
788 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 74 additions & 0 deletions
74
frontend/app/(authenticated)/user/profile/category-form.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
"use client"; | ||
|
||
import { useState } from "react"; | ||
import { useQuery } from "@tanstack/react-query"; | ||
import clsx from "clsx"; | ||
|
||
import { Button } from "@/components/ui/button"; | ||
import { LoadingSpinner } from "@/components/ui/loading-spinner"; | ||
import { getCategories } from "@/queries/category"; | ||
import { useUpdateProfile } from "@/queries/user"; | ||
import { getCategoryFor } from "@/types/categories"; | ||
|
||
interface Props { | ||
initialCategoryIds: number[]; | ||
} | ||
|
||
export default function CategoryForm({ initialCategoryIds }: Props) { | ||
const { data: categories, isLoading } = useQuery(getCategories()); | ||
const [categoryIds, setCategoryIds] = useState<number[]>(initialCategoryIds); | ||
|
||
const toggleCategory = (id: number) => { | ||
if (!categoryIds.includes(id)) { | ||
setCategoryIds([...categoryIds, id]); | ||
} else { | ||
setCategoryIds(categoryIds.filter((item) => item !== id)); | ||
} | ||
}; | ||
|
||
const updateProfileMutation = useUpdateProfile(); | ||
|
||
const handleSubmit = () => { | ||
updateProfileMutation.mutate({ categoryIds }); | ||
}; | ||
|
||
if (isLoading) { | ||
return ( | ||
<div className="flex justify-center items-center w-full"> | ||
<LoadingSpinner className="w-24 h-24" /> | ||
</div> | ||
); | ||
} | ||
|
||
return ( | ||
<div className="flex flex-col w-auto mx-4 md:mx-16 xl:mx-56 pb-4"> | ||
<div className=""> | ||
<h2 className="text-2xl 2xl:text-3xl font-bold">Categories</h2> | ||
<p className="text-sm text-muted-foreground mt-2"> | ||
Select the General Paper categories you are interested in. | ||
</p> | ||
<div className="flex flex-wrap gap-4 justify-center max-w-2xl"> | ||
{categories!.map((category) => { | ||
const isActive = categoryIds.includes(category.id); | ||
return ( | ||
<div | ||
className={clsx( | ||
"font-medium p-3 px-4 rounded-3xl cursor-pointer shadow-md mt-4", | ||
{ | ||
"bg-emerald-600 text-white": isActive, | ||
"bg-slate-100": !isActive, | ||
}, | ||
)} | ||
key={category.id} | ||
onClick={() => toggleCategory(category.id)} | ||
> | ||
{getCategoryFor(category.name)} | ||
</div> | ||
); | ||
})} | ||
</div> | ||
<Button onClick={handleSubmit}>Save</Button> | ||
</div> | ||
</div> | ||
); | ||
} |
179 changes: 179 additions & 0 deletions
179
frontend/app/(authenticated)/user/profile/change-password-form.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
import { useState } from "react"; | ||
import { SubmitHandler, useForm } from "react-hook-form"; | ||
import { zodResolver } from "@hookform/resolvers/zod"; | ||
import { Check, CircleAlert } from "lucide-react"; | ||
import { z } from "zod"; | ||
|
||
import { Alert, AlertDescription } from "@/components/ui/alert"; | ||
import { Box } from "@/components/ui/box"; | ||
import { Button } from "@/components/ui/button"; | ||
import { Checkbox } from "@/components/ui/checkbox"; | ||
import { | ||
Form, | ||
FormControl, | ||
FormField, | ||
FormItem, | ||
FormLabel, | ||
FormMessage, | ||
} from "@/components/ui/form"; | ||
import { Input } from "@/components/ui/input"; | ||
import { useChangePassword } from "@/queries/user"; | ||
|
||
const changePasswordCompleteFormSchema = z | ||
.object({ | ||
password: z.string(), | ||
confirmPassword: z.string(), | ||
oldPassword: z.string(), | ||
}) | ||
.refine(({ password, confirmPassword }) => password === confirmPassword, { | ||
message: "Passwords must match", | ||
path: ["confirmPassword"], | ||
}); | ||
|
||
type ChangePasswordRequestForm = z.infer< | ||
typeof changePasswordCompleteFormSchema | ||
>; | ||
|
||
const changePasswordFormDefault = { | ||
password: "", | ||
confirmPassword: "", | ||
oldPassword: "", | ||
}; | ||
|
||
export default function ChangePasswordForm() { | ||
const [isError, setIsError] = useState<boolean>(false); | ||
const [success, setSuccess] = useState<boolean>(false); | ||
const [isPasswordVisible, setIsPasswordVisible] = useState<boolean>(false); | ||
|
||
const form = useForm<ChangePasswordRequestForm>({ | ||
resolver: zodResolver(changePasswordCompleteFormSchema), | ||
defaultValues: changePasswordFormDefault, | ||
}); | ||
const changePasswordMutation = useChangePassword(); | ||
|
||
const onSubmit: SubmitHandler<ChangePasswordRequestForm> = (data) => { | ||
changePasswordMutation.mutate(data, { | ||
onSuccess: async (data) => { | ||
if (data.error) { | ||
setIsError(true); | ||
setSuccess(false); | ||
} else { | ||
setIsError(false); | ||
setSuccess(true); | ||
} | ||
}, | ||
}); | ||
}; | ||
|
||
return ( | ||
<div className="flex flex-col w-auto mx-4 md:mx-16 xl:mx-56 py-8"> | ||
<div className=""> | ||
<h2 className="text-2xl 2xl:text-3xl font-bold">Change password</h2> | ||
<Box className="space-y-6 mt-4"> | ||
{isError && ( | ||
<Alert variant="destructive"> | ||
<CircleAlert className="h-5 w-5" /> | ||
<AlertDescription>Wrong password. Try again?</AlertDescription> | ||
</Alert> | ||
)} | ||
{success && ( | ||
<Alert variant="teal"> | ||
<Check className="h-5 w-5" /> | ||
<AlertDescription> | ||
Successfully changed password. | ||
</AlertDescription> | ||
</Alert> | ||
)} | ||
<Form {...form}> | ||
<form className="space-y-4" onSubmit={form.handleSubmit(onSubmit)}> | ||
<div className="space-y-4"> | ||
<Box className="flex flex-col space-y-2.5"> | ||
<FormField | ||
control={form.control} | ||
name={"oldPassword"} | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormLabel className="!text-current"> | ||
Old password | ||
</FormLabel> | ||
<FormControl> | ||
<Input | ||
{...field} | ||
type={isPasswordVisible ? "text" : "password"} | ||
/> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
</Box> | ||
</div> | ||
<div className="space-y-4"> | ||
<Box className="flex flex-col space-y-2.5"> | ||
<FormField | ||
control={form.control} | ||
name={"password"} | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormLabel className="!text-current"> | ||
New Password | ||
</FormLabel> | ||
<FormControl> | ||
<Input | ||
{...field} | ||
type={isPasswordVisible ? "text" : "password"} | ||
/> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
</Box> | ||
</div> | ||
<div className="space-y-4"> | ||
<Box className="flex flex-col space-y-2.5"> | ||
<FormField | ||
control={form.control} | ||
name={"confirmPassword"} | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormLabel className="!text-current"> | ||
Confirm Password | ||
</FormLabel> | ||
<FormControl> | ||
<Input | ||
{...field} | ||
type={isPasswordVisible ? "text" : "password"} | ||
/> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
</Box> | ||
</div> | ||
<div className="flex items-center gap-x-2"> | ||
<Checkbox | ||
checked={isPasswordVisible} | ||
id="password-visibility" | ||
onCheckedChange={(checkedState) => | ||
setIsPasswordVisible( | ||
checkedState === "indeterminate" ? false : checkedState, | ||
) | ||
} | ||
/> | ||
<label | ||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" | ||
htmlFor="password-visibility" | ||
> | ||
Show password | ||
</label> | ||
</div> | ||
<Button type="submit">Submit</Button> | ||
</form> | ||
</Form> | ||
</Box> | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
"use client"; | ||
|
||
import { useUserStore } from "@/store/user/user-store-provider"; | ||
|
||
import CategoryForm from "./category-form"; | ||
import ChangePasswordForm from "./change-password-form"; | ||
|
||
export default function Profile() { | ||
const user = useUserStore((store) => store.user); | ||
|
||
return ( | ||
user && ( | ||
<div className="flex flex-col w-full py-8"> | ||
<div className="flex flex-col mb-8 gap-y-2 mx-8 md:mx-16 xl:mx-56 pt-8"> | ||
<h1 className="text-3xl 2xl:text-4xl font-bold">Settings</h1> | ||
</div> | ||
|
||
<div className="flex flex-col gap-8"> | ||
<CategoryForm | ||
initialCategoryIds={user.categories.map((category) => category.id)} | ||
/> | ||
|
||
<ChangePasswordForm /> | ||
</div> | ||
</div> | ||
) | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,15 @@ | ||
export { createClient } from "./core/"; | ||
export { createClient } from './core/'; | ||
export type { | ||
Client, | ||
Config, | ||
Options, | ||
RequestOptions, | ||
RequestOptionsBase, | ||
RequestResult, | ||
} from "./core/types"; | ||
} from './core/types'; | ||
export { | ||
createConfig, | ||
formDataBodySerializer, | ||
jsonBodySerializer, | ||
urlSearchParamsBodySerializer, | ||
} from "./core/utils"; | ||
} from './core/utils'; |
Oops, something went wrong.