feat(settings): implement user profile page and notification handler
This commit introduces a new user settings page and enhances push notification functionality. The new settings page, located at `/pages/settings`, allows users to view and update their personal profile information. It fetches the current user's data and provides a form to edit fields such as name, email, phone number, position, and role. The form is built using `react-hook-form` for state management and validation, and it leverages custom hooks (`useProfileQuery`, `useUpdateProfileQuery`) to interact with the API. Additionally, a `notificationclick` event listener has been added to the Firebase messaging service worker. This handler improves the user experience for push notifications by focusing on an existing tab with the relevant URL or opening a new one if none exists when a notification is clicked.
This commit is contained in:
@@ -14,3 +14,28 @@ firebase.initializeApp({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const messaging = firebase.messaging();
|
const messaging = firebase.messaging();
|
||||||
|
|
||||||
|
self.addEventListener("notificationclick", (event) => {
|
||||||
|
event.notification.close();
|
||||||
|
|
||||||
|
const urlToOpen = event.notification.data.url || "/";
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
clients
|
||||||
|
.matchAll({
|
||||||
|
type: "window",
|
||||||
|
includeUncontrolled: true,
|
||||||
|
})
|
||||||
|
.then((clientList) => {
|
||||||
|
for (const client of clientList) {
|
||||||
|
if (client.url === urlToOpen && "focus" in client) {
|
||||||
|
return client.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clients.openWindow) {
|
||||||
|
return clients.openWindow(urlToOpen);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import InputGroup from "@/components/FormElements/InputGroup";
|
|
||||||
import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area";
|
|
||||||
import { Select } from "@/components/FormElements/select";
|
|
||||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||||
|
|
||||||
export function ContactForm() {
|
export function ContactForm() {
|
||||||
|
|||||||
+160
-18
@@ -1,23 +1,165 @@
|
|||||||
import type { Metadata } from "next";
|
"use client";
|
||||||
import { PersonalInfoForm } from "./_components/personal-info";
|
import Loading from "@/app/loading";
|
||||||
import { UploadPhotoForm } from "./_components/upload-photo";
|
import InputGroup from "@/components/FormElements/InputGroup";
|
||||||
|
import { Select } from "@/components/FormElements/select";
|
||||||
export const metadata: Metadata = {
|
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||||
title: "Settings Page",
|
import FormFooter from "@/components/ui/FormFooter";
|
||||||
};
|
import { AdminRoles } from "@/constants/enums";
|
||||||
|
import { REGEX } from "@/constants/regex";
|
||||||
|
import { FormErrorMessages } from "@/constants/Texts";
|
||||||
|
import { useProfileQuery, useUpdateProfileQuery } from "@/hooks/queries";
|
||||||
|
import { TUpdateProfileCredentials, UpdateProfileCredentials } from "@/lib/api";
|
||||||
|
import { getEnumAsArray } from "@/utils/shared";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { ZodSafeParseResult } from "zod";
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
|
const { data, isLoading } = useProfileQuery();
|
||||||
|
const { mutate } = useUpdateProfileQuery();
|
||||||
|
const { register, reset, handleSubmit } = useForm<TUpdateProfileCredentials>({
|
||||||
|
mode: "onChange",
|
||||||
|
defaultValues: data,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
reset(data);
|
||||||
|
}
|
||||||
|
}, [data, reset]);
|
||||||
|
const updateUser = (data: TUpdateProfileCredentials) => {
|
||||||
|
const validation: ZodSafeParseResult<TUpdateProfileCredentials> =
|
||||||
|
UpdateProfileCredentials.safeParse(data);
|
||||||
|
if (validation.success) {
|
||||||
|
mutate({ credentials: validation.data });
|
||||||
|
} else {
|
||||||
|
console.error(validation.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (isLoading) return <Loading />;
|
||||||
return (
|
return (
|
||||||
<div>Settings</div>
|
<ShowcaseSection title="Update Profile">
|
||||||
// <div className="mx-auto w-full max-w-[1080px]">
|
<form
|
||||||
// <div className="grid grid-cols-5 gap-8">
|
className="grid grid-cols-2 gap-2"
|
||||||
// <div className="col-span-5 xl:col-span-3">
|
onSubmit={handleSubmit(updateUser)}
|
||||||
// <PersonalInfoForm />
|
>
|
||||||
// </div>
|
<InputGroup
|
||||||
// <div className="col-span-5 xl:col-span-2">
|
{...register("full_name", {
|
||||||
// <UploadPhotoForm />
|
minLength: {
|
||||||
// </div>
|
value: 2,
|
||||||
// </div>
|
message: FormErrorMessages.minLength(2),
|
||||||
// </div>
|
},
|
||||||
|
maxLength: {
|
||||||
|
value: 100,
|
||||||
|
message: FormErrorMessages.maxLength(100),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
label="Full name"
|
||||||
|
name="full_name"
|
||||||
|
placeholder="Full name"
|
||||||
|
type="text"
|
||||||
|
className="col-span-1"
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
{...register("username", {
|
||||||
|
minLength: {
|
||||||
|
value: 3,
|
||||||
|
message: FormErrorMessages.minLength(3),
|
||||||
|
},
|
||||||
|
maxLength: {
|
||||||
|
value: 30,
|
||||||
|
message: FormErrorMessages.maxLength(30),
|
||||||
|
},
|
||||||
|
pattern: {
|
||||||
|
value: REGEX.USERNAME,
|
||||||
|
message: FormErrorMessages.invalidPattern,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
label="Username"
|
||||||
|
className="col-span-1"
|
||||||
|
name="username"
|
||||||
|
placeholder="Username"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
{...register("email", {
|
||||||
|
pattern: {
|
||||||
|
value: REGEX.EMAIL,
|
||||||
|
message: FormErrorMessages.invalidEmail,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
label="Email"
|
||||||
|
className="col-span-1"
|
||||||
|
name="email"
|
||||||
|
placeholder="Email"
|
||||||
|
type="email"
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
{...register("phone", {
|
||||||
|
minLength: {
|
||||||
|
value: 10,
|
||||||
|
message: FormErrorMessages.minLength(10),
|
||||||
|
},
|
||||||
|
maxLength: {
|
||||||
|
value: 20,
|
||||||
|
message: FormErrorMessages.maxLength(20),
|
||||||
|
},
|
||||||
|
pattern: {
|
||||||
|
value: REGEX.PHONE,
|
||||||
|
message: FormErrorMessages.invalidPattern,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
label="Phone"
|
||||||
|
className="col-span-1"
|
||||||
|
name="phone"
|
||||||
|
placeholder="Phone"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
{...register("department", {
|
||||||
|
minLength: {
|
||||||
|
value: 2,
|
||||||
|
message: FormErrorMessages.minLength(2),
|
||||||
|
},
|
||||||
|
maxLength: {
|
||||||
|
value: 100,
|
||||||
|
message: FormErrorMessages.maxLength(100),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
label="Department"
|
||||||
|
className="col-span-1"
|
||||||
|
name="department"
|
||||||
|
placeholder="Department"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
{...register("position", {
|
||||||
|
minLength: {
|
||||||
|
value: 2,
|
||||||
|
message: FormErrorMessages.minLength(2),
|
||||||
|
},
|
||||||
|
maxLength: {
|
||||||
|
value: 100,
|
||||||
|
message: FormErrorMessages.maxLength(100),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
label="Position"
|
||||||
|
className="col-span-1"
|
||||||
|
name="position"
|
||||||
|
placeholder="position"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Role"
|
||||||
|
{...register("role", {
|
||||||
|
required: { message: FormErrorMessages.required, value: true },
|
||||||
|
})}
|
||||||
|
name="role"
|
||||||
|
items={getEnumAsArray(AdminRoles)}
|
||||||
|
placeholder="Select Role"
|
||||||
|
/>
|
||||||
|
<FormFooter />
|
||||||
|
</form>
|
||||||
|
</ShowcaseSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ export default function SigninWithPassword() {
|
|||||||
const [passwordFieldInputType, setPasswordFieldInputType] = useState<
|
const [passwordFieldInputType, setPasswordFieldInputType] = useState<
|
||||||
"password" | "text"
|
"password" | "text"
|
||||||
>("password");
|
>("password");
|
||||||
const { handleSubmit, control, reset, register } =
|
const { handleSubmit, reset, register } = useForm<ILoginCredentials>();
|
||||||
useForm<ILoginCredentials>();
|
|
||||||
const { mutate } = useLoginQuery(reset);
|
const { mutate } = useLoginQuery(reset);
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<ILoginCredentials> = (data) => {
|
const onSubmit: SubmitHandler<ILoginCredentials> = (data) => {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const InputGroup = <T extends FieldValues>({
|
|||||||
<div className={className}>
|
<div className={className}>
|
||||||
<label
|
<label
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
className="text-body-sm font-medium text-dark dark:text-white"
|
className="text-body-sm font-medium capitalize text-dark dark:text-white"
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
{required && <span className="ml-1 select-none text-error">*</span>}
|
{required && <span className="ml-1 select-none text-error">*</span>}
|
||||||
@@ -93,7 +93,7 @@ const InputGroup = <T extends FieldValues>({
|
|||||||
defaultValue: props.defaultValue,
|
defaultValue: props.defaultValue,
|
||||||
})}
|
})}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full rounded-lg border-[1.5px] border-stroke bg-transparent outline-none transition focus:border-primary disabled:cursor-default disabled:bg-gray-2 data-[active=true]:border-primary dark:border-dark-3 dark:bg-dark-2 dark:focus:border-primary dark:disabled:bg-dark dark:data-[active=true]:border-primary",
|
"w-full rounded-lg border-[1.5px] border-stroke bg-transparent outline-none transition placeholder:capitalize focus:border-primary disabled:cursor-default disabled:bg-gray-2 data-[active=true]:border-primary dark:border-dark-3 dark:bg-dark-2 dark:focus:border-primary dark:disabled:bg-dark dark:data-[active=true]:border-primary",
|
||||||
// Add error styles conditionally
|
// Add error styles conditionally
|
||||||
error && "border-red focus:border-red dark:border-red",
|
error && "border-red focus:border-red dark:border-red",
|
||||||
type === "file"
|
type === "file"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { ChevronUpIcon } from "@/assets/icons";
|
import { ChevronUpIcon } from "@/assets/icons";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { ILabelValue } from "@/types/shared";
|
||||||
import { useId, useState, type ChangeEvent } from "react";
|
import { useId, useState, type ChangeEvent } from "react";
|
||||||
import {
|
import {
|
||||||
type FieldErrors,
|
type FieldErrors,
|
||||||
@@ -14,7 +15,7 @@ import {
|
|||||||
type PropsType<T extends FieldValues> = {
|
type PropsType<T extends FieldValues> = {
|
||||||
name: Path<T>;
|
name: Path<T>;
|
||||||
label: string;
|
label: string;
|
||||||
items: { value: string; label: string }[];
|
items: ILabelValue[];
|
||||||
prefixIcon?: React.ReactNode;
|
prefixIcon?: React.ReactNode;
|
||||||
onChange?: (e: ChangeEvent<HTMLSelectElement>) => void;
|
onChange?: (e: ChangeEvent<HTMLSelectElement>) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ const NotificationHistoryTable = () => {
|
|||||||
setParams((currentParams) => ({
|
setParams((currentParams) => ({
|
||||||
...currentParams,
|
...currentParams,
|
||||||
offset: e.selected * (metadata?.limit ?? 10),
|
offset: e.selected * (metadata?.limit ?? 10),
|
||||||
limit: metadata?.limit ?? 1000,
|
limit: metadata?.limit ?? 10,
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import InputGroup from "@/components/FormElements/InputGroup";
|
import InputGroup from "@/components/FormElements/InputGroup";
|
||||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||||
import FormFooter from "@/components/ui/FormFooter";
|
import FormFooter from "@/components/ui/FormFooter";
|
||||||
|
import { REGEX } from "@/constants/regex";
|
||||||
import { FormErrorMessages } from "@/constants/Texts";
|
import { FormErrorMessages } from "@/constants/Texts";
|
||||||
import { ICreateAdminCredentials } from "@/lib/api";
|
import { ICreateAdminCredentials } from "@/lib/api";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
@@ -12,7 +13,7 @@ interface IProps {
|
|||||||
id?: string;
|
id?: string;
|
||||||
}
|
}
|
||||||
const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
|
const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
|
||||||
const { errors, handleSubmit, register, router, onSubmit } =
|
const { errors, handleSubmit, register, onSubmit } =
|
||||||
useCreateAdminPresenter({
|
useCreateAdminPresenter({
|
||||||
editMode,
|
editMode,
|
||||||
defaultValues,
|
defaultValues,
|
||||||
@@ -162,6 +163,10 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
|
|||||||
value: 20,
|
value: 20,
|
||||||
message: FormErrorMessages.maxLength(20),
|
message: FormErrorMessages.maxLength(20),
|
||||||
},
|
},
|
||||||
|
pattern:{
|
||||||
|
value:REGEX.PHONE,
|
||||||
|
message:FormErrorMessages.invalidPattern
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
name="phone"
|
name="phone"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { REGEX } from "@/constants/regex";
|
|||||||
import { FormErrorMessages } from "@/constants/Texts";
|
import { FormErrorMessages } from "@/constants/Texts";
|
||||||
import { CreateCustomerCredentials } from "@/lib/api";
|
import { CreateCustomerCredentials } from "@/lib/api";
|
||||||
import useCreateCustomerPresenter from "./useCreateCustomerPresenter";
|
import useCreateCustomerPresenter from "./useCreateCustomerPresenter";
|
||||||
|
import { getEnumAsArray } from "@/utils/shared";
|
||||||
|
import { AdminRoles } from "@/constants/enums";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
editMode?: boolean;
|
editMode?: boolean;
|
||||||
@@ -97,16 +99,7 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => {
|
|||||||
required: { message: FormErrorMessages.required, value: true },
|
required: { message: FormErrorMessages.required, value: true },
|
||||||
})}
|
})}
|
||||||
name="role"
|
name="role"
|
||||||
items={[
|
items={getEnumAsArray(AdminRoles)}
|
||||||
{
|
|
||||||
label: "Admin",
|
|
||||||
value: "admin",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Analyst",
|
|
||||||
value: "analyst",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
placeholder="Select Role"
|
placeholder="Select Role"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,3 +35,7 @@ export enum NotificationTypes {
|
|||||||
ALERT = "alert",
|
ALERT = "alert",
|
||||||
REJECT = "reject",
|
REJECT = "reject",
|
||||||
}
|
}
|
||||||
|
export enum AdminRoles {
|
||||||
|
ADMIN = "admin",
|
||||||
|
ANALYST = "analyst",
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ export const REGEX = {
|
|||||||
PHONE: /^[+\d]+$/,
|
PHONE: /^[+\d]+$/,
|
||||||
USERNAME: /^[a-zA-Z0-9](?:[a-zA-Z0-9._]*[a-zA-Z0-9])?$/,
|
USERNAME: /^[a-zA-Z0-9](?:[a-zA-Z0-9._]*[a-zA-Z0-9])?$/,
|
||||||
URL: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/,
|
URL: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/,
|
||||||
|
EMAIL: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import React, {
|
import React, {
|
||||||
createContext,
|
createContext,
|
||||||
|
Dispatch,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
|
SetStateAction,
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -13,18 +15,19 @@ import React, {
|
|||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ILoginResponse, UserSchema } from "../lib/api/types";
|
import { ILoginResponse, IUser, UserSchema } from "../lib/api/types";
|
||||||
import { COOKIE_KEYS } from "../lib/shared/cookies";
|
import { COOKIE_KEYS } from "../lib/shared/cookies";
|
||||||
|
|
||||||
type User = z.infer<typeof UserSchema>;
|
|
||||||
|
|
||||||
interface UserContextType {
|
interface UserContextType {
|
||||||
user: User | null;
|
user: IUser | null;
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
setAuthState: (data: ILoginResponse) => void;
|
setAuthState: (data: ILoginResponse) => void;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
|
setUser:Dispatch<SetStateAction<IUser|null>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserContext = createContext<UserContextType | undefined>(undefined);
|
const UserContext = createContext<UserContextType | undefined>(undefined);
|
||||||
@@ -34,7 +37,7 @@ interface UserProviderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UserProvider: React.FC<UserProviderProps> = ({ children }) => {
|
export const UserProvider: React.FC<UserProviderProps> = ({ children }) => {
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<IUser | null>(null);
|
||||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -88,8 +91,9 @@ export const UserProvider: React.FC<UserProviderProps> = ({ children }) => {
|
|||||||
isLoading,
|
isLoading,
|
||||||
setAuthState,
|
setAuthState,
|
||||||
logout,
|
logout,
|
||||||
|
setUser,
|
||||||
}),
|
}),
|
||||||
[user, accessToken, isLoading, setAuthState, logout],
|
[user, accessToken, isLoading, setAuthState, logout, setUser],
|
||||||
);
|
);
|
||||||
|
|
||||||
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
|
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { API_ENDPOINTS, profileService } from "@/lib/api";
|
import { useUser } from "@/contexts";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { API_ENDPOINTS, IUser, profileService } from "@/lib/api";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
export const useProfileQuery = () => {
|
export const useProfileQuery = () => {
|
||||||
const queryKey = useMemo(
|
const queryKey = useMemo(
|
||||||
() => [API_ENDPOINTS.PROFILE.READ, "CURRENT_USER_PROFILE"],
|
() => [API_ENDPOINTS.PROFILE.READ, "CURRENT_USER_PROFILE"],
|
||||||
@@ -12,3 +13,22 @@ export const useProfileQuery = () => {
|
|||||||
queryFn: () => profileService.getProfile(),
|
queryFn: () => profileService.getProfile(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
export const useUpdateProfileQuery = () => {
|
||||||
|
const { setUser } = useUser();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const mutationKey = useMemo(
|
||||||
|
() => [API_ENDPOINTS.PROFILE.UPDATE, "UPDATE_USER_PROFILE"],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
return useMutation({
|
||||||
|
mutationKey,
|
||||||
|
mutationFn: profileService.updateProfile,
|
||||||
|
onSuccess: (response: IUser) => {
|
||||||
|
toast.success("Profile updated successfully");
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [API_ENDPOINTS.PROFILE.READ, "CURRENT_USER_PROFILE"],
|
||||||
|
});
|
||||||
|
setUser(response);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const API_ENDPOINTS = {
|
|||||||
},
|
},
|
||||||
PROFILE: {
|
PROFILE: {
|
||||||
READ: "profile",
|
READ: "profile",
|
||||||
|
UPDATE: "profile",
|
||||||
},
|
},
|
||||||
COMPANIES: {
|
COMPANIES: {
|
||||||
READ_ALL: "companies",
|
READ_ALL: "companies",
|
||||||
|
|||||||
@@ -1,15 +1,46 @@
|
|||||||
|
import { parseWithSchema } from "@/utils/shared";
|
||||||
import api from "../axios";
|
import api from "../axios";
|
||||||
|
|
||||||
import { API_ENDPOINTS } from "../endpoints";
|
import { API_ENDPOINTS } from "../endpoints";
|
||||||
import { CurrentUserProfileSchema } from "../types";
|
import {
|
||||||
|
CurrentUserProfileSchema,
|
||||||
|
IUser,
|
||||||
|
TCurrentUser,
|
||||||
|
TUpdateProfileCredentials,
|
||||||
|
UserSchema,
|
||||||
|
} from "../types";
|
||||||
export const profileService = {
|
export const profileService = {
|
||||||
getProfile: async () => {
|
getProfile: async () => {
|
||||||
try {
|
try {
|
||||||
const response = (await api.get(API_ENDPOINTS.PROFILE.READ)).data;
|
const response = (await api.get(API_ENDPOINTS.PROFILE.READ)).data;
|
||||||
|
const userData = parseWithSchema(
|
||||||
return CurrentUserProfileSchema.parse(response.data);
|
CurrentUserProfileSchema,
|
||||||
|
response.data,
|
||||||
|
"Profile Service: Get Profile",
|
||||||
|
);
|
||||||
|
return userData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("ERROR caught in Profile Service: Get Profile", error);
|
console.error("ERROR caught in Profile Service: Get Profile", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updateProfile: async ({
|
||||||
|
credentials,
|
||||||
|
}: {
|
||||||
|
credentials: TUpdateProfileCredentials;
|
||||||
|
}): Promise<IUser> => {
|
||||||
|
try {
|
||||||
|
const response = (
|
||||||
|
await api.put(API_ENDPOINTS.PROFILE.UPDATE, credentials)
|
||||||
|
).data;
|
||||||
|
return parseWithSchema(
|
||||||
|
UserSchema,
|
||||||
|
response.data,
|
||||||
|
"Profile Service: Update Profile",
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("ERROR caught in Profile Service: Update Profile", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,4 +18,18 @@ export const CurrentUserProfileSchema = z.object({
|
|||||||
updated_by: z.string().optional(),
|
updated_by: z.string().optional(),
|
||||||
username: z.string().optional(),
|
username: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const UpdateProfileCredentials = z.object({
|
||||||
|
department: z.string(),
|
||||||
|
email: z.string(),
|
||||||
|
full_name: z.string(),
|
||||||
|
phone: z.string(),
|
||||||
|
position: z.string(),
|
||||||
|
profile_image: z.string(),
|
||||||
|
role: z.string(),
|
||||||
|
username: z.string(),
|
||||||
|
});
|
||||||
|
export type TUpdateProfileCredentials = z.infer<
|
||||||
|
typeof UpdateProfileCredentials
|
||||||
|
>;
|
||||||
export type TCurrentUser = z.infer<typeof CurrentUserProfileSchema>;
|
export type TCurrentUser = z.infer<typeof CurrentUserProfileSchema>;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ILabelValue } from "@/types/shared";
|
import { ILabelValue } from "@/types/shared";
|
||||||
import * as moment from "moment";
|
import * as moment from "moment";
|
||||||
|
import { z } from "zod";
|
||||||
export const unixToDate = ({
|
export const unixToDate = ({
|
||||||
hasTime = false,
|
hasTime = false,
|
||||||
unix,
|
unix,
|
||||||
@@ -82,3 +83,18 @@ export const truncateString = (
|
|||||||
}
|
}
|
||||||
return str.slice(0, startLength) + "..." + str.slice(-endLength);
|
return str.slice(0, startLength) + "..." + str.slice(-endLength);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function parseWithSchema<T>(
|
||||||
|
schema: z.ZodSchema<T>,
|
||||||
|
data: unknown,
|
||||||
|
errorContext: string,
|
||||||
|
): T {
|
||||||
|
const parsedData = schema.safeParse(data);
|
||||||
|
|
||||||
|
if (parsedData.success) {
|
||||||
|
return parsedData.data;
|
||||||
|
} else {
|
||||||
|
console.error(`ERROR caught in ${errorContext}`);
|
||||||
|
throw new Error(`ERROR caught in ${errorContext}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user