diff --git a/package-lock.json b/package-lock.json index f656725..e29389d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1361,32 +1361,6 @@ "url": "https://opencollective.com/mui-org" } }, - "node_modules/@mui/icons-material": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.1.tgz", - "integrity": "sha512-upzCtG6awpL6noEZlJ5Z01khZ9VnLNLaj7tb6iPbN6G97eYfUTs8e9OyPKy3rEms3VQWmVBfri7jzeaRxdFIzA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@mui/material": "^7.3.1", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@mui/material": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.1.tgz", diff --git a/src/features/profile/api/settingsApi.ts b/src/features/profile/api/settingsApi.ts new file mode 100644 index 0000000..0f56237 --- /dev/null +++ b/src/features/profile/api/settingsApi.ts @@ -0,0 +1,122 @@ +import axios, { isAxiosError } from 'axios'; +import apiClient from '@/lib/apiClient'; + +import { + type GetProfileApiResponse, + type SaveProfileApiResponse, + type TokenApiResponse, + type PhoneNumberApiResponse, + type ConfirmPhoneNumberApiResponse, +} from '../types/settingsApiType'; +import { type InfoRowData } from '../types/settingsType'; + +const storedToken = () => localStorage.getItem('authToken'); + +export async function fetchProfile(): Promise<{ data: GetProfileApiResponse }> { + const res = await apiClient.post( + '/Profile/GetProfile', + {}, + { headers: { Authorization: `Bearer ${storedToken()}` } }, + ); + return { data: res.data }; +} + +export async function saveProfile(payload?: { + data: InfoRowData; + imageUrl: string | null; +}): Promise<{ data: SaveProfileApiResponse }> { + if (!payload) { + throw new Error('Payload for saving profile is missing.'); + } + + const res = await apiClient.post( + '/Profile/SaveProfilePersonalInforamtion', + payload, + { headers: { Authorization: `Bearer ${storedToken()}` } }, + ); + return { data: res.data }; +} + +export async function getToken(): Promise<{ + data: { success: boolean; message?: string }; +}> { + const apiUrl = 'https://accounts.business-harmony.com'; + const tokenEndpoint = `${apiUrl}/connect/token`; + + try { + const body = new URLSearchParams(); + body.set('grant_type', 'password'); + body.set('username', 'zareian.1381@gmail.com'); + body.set('password', '123@Qweasd'); + body.set('client_id', 'harmony_identity'); + body.set('scope', 'openid harmony_identity profile offline_access'); + + const response = await axios.post( + tokenEndpoint, + body.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }, + ); + + if (response.data?.access_token) { + localStorage.setItem('authToken', response.data.access_token); + return { data: { success: true } }; + } else { + throw new Error('No access token in response'); + } + } catch (error: unknown) { + let message = 'Failed to get token'; + if (isAxiosError(error) && error.response) { + message = `Request failed with status ${error.response.status}`; + } else if (error instanceof Error) { + message = error.message; + } + throw new Error(message); + } +} + +export async function sendVerificationCode(payload?: { + phoneNumber: string; +}): Promise<{ data: PhoneNumberApiResponse }> { + if (!payload) { + throw new Error('Payload for sending verification code is missing.'); + } + const res = await apiClient.post( + '/Profile/SendVerfiyPhoneNumberCode', + payload, + { headers: { Authorization: `Bearer ${storedToken()}` } }, + ); + return { data: res.data }; +} + +export async function confirmPhoneNumberCode(payload?: { + phoneNumber: string; + verifyCode: string; +}): Promise<{ data: ConfirmPhoneNumberApiResponse }> { + if (!payload) { + throw new Error('Payload for confirming phone number is missing.'); + } + const res = await apiClient.post( + '/Profile/ConfirmPhoneNumberChangeCode', + payload, + { headers: { Authorization: `Bearer ${storedToken()}` } }, + ); + return { data: res.data }; +} + +export async function changePhoneNumber(payload?: { + phoneNumber: string; +}): Promise<{ data: PhoneNumberApiResponse }> { + if (!payload) { + throw new Error('Payload for changing phone number is missing.'); + } + const res = await apiClient.post( + '/Profile/ChangePhoneNumber', + payload, + { headers: { Authorization: `Bearer ${storedToken()}` } }, + ); + return { data: res.data }; +} diff --git a/src/features/profile/components/userInformation/PersonalInformation.tsx b/src/features/profile/components/userInformation/PersonalInformation.tsx index b97cdaa..de67fd0 100644 --- a/src/features/profile/components/userInformation/PersonalInformation.tsx +++ b/src/features/profile/components/userInformation/PersonalInformation.tsx @@ -6,29 +6,9 @@ import { ProfileImage } from './personalInformation/ProfileImage'; import { InfoRowDisplay } from './personalInformation/InfoRowDisplay'; import { InfoRowEdit } from './personalInformation/InfoRowEdit'; import { PageWrapper } from '../PageWrapper'; -import { Gender, type InfoRowData } from '../../types'; -import axios, { isAxiosError } from 'axios'; -import apiClient from '@/lib/apiClient'; - -interface GetProfileApiResponse { - success: boolean; - message?: string; - firstName?: string; - lastName?: string; - nationalCode?: string; - gender?: Gender; - countryCode?: string; - profileImageUrl?: string; -} - -interface SaveProfileApiResponse { - success: boolean; - message?: string; -} - -interface TokenApiResponse { - access_token: string; -} +import { useManualApi } from '@/hooks/useApi'; +import { Gender, type InfoRowData } from '../../types/settingsType'; +import { fetchProfile, saveProfile, getToken } from '../../api/settingsApi'; export function PersonalInformation() { const { t } = useTranslation('setting'); @@ -42,71 +22,60 @@ export function PersonalInformation() { country: '', }); const [originalData, setOriginalData] = useState(null); - // const [token, setToken] = useState(null); - const [tokenError, setTokenError] = useState(null); - const [saveError, setSaveError] = useState(null); - const storedToken = localStorage.getItem('authToken'); - const [isLoading, setIsLoading] = useState(true); - const [fetchError, setFetchError] = useState(null); + const { + data: profileData, + loading: isLoadingProfile, + error: fetchProfileError, + execute: executeFetchProfile, + } = useManualApi(fetchProfile); + + const { + data: saveData, + loading: isSavingProfile, + error: saveProfileError, + execute: executeSaveProfile, + } = useManualApi(saveProfile); + + const { + data: tokenData, + loading: isGettingToken, + error: tokenError, + execute: executeGetToken, + } = useManualApi(getToken); useEffect(() => { - const fetchProfile = async () => { - setIsLoading(true); - setFetchError(null); - try { - const res = await apiClient.post( - '/Profile/GetProfile', - {}, - { - headers: { - Authorization: `Bearer ${storedToken}`, - }, - }, - ); - if (res.data?.success) { - const profile = res.data; - const fetchedData = { - firstName: profile.firstName ?? '', - lastName: profile.lastName ?? '', - nationalCode: profile.nationalCode ?? '', - gender: Object.values(Gender).includes(profile.gender as Gender) - ? (profile.gender as Gender) - : Gender.None, - country: profile.countryCode ?? '', - }; - setData(fetchedData); - setOriginalData(fetchedData); - setUploadedImageUrl(profile.profileImageUrl || null); - } else { - throw new Error(res.data.message || t('settingForm.failRetrieve')); - } - } catch (error: unknown) { - let message = t('settingForm.errorFetch'); - if (error instanceof Error) { - message = error.message; - } - setFetchError(message); - } finally { - setIsLoading(false); - } - }; + executeFetchProfile(); + }, []); - if (storedToken) { - fetchProfile(); - } else { - setIsLoading(false); - setFetchError(t('settingForm.notLoggedIn')); + useEffect(() => { + if (profileData?.success) { + const fetchedData = { + firstName: profileData.firstName ?? '', + lastName: profileData.lastName ?? '', + nationalCode: profileData.nationalCode ?? '', + gender: Object.values(Gender).includes(profileData.gender as Gender) + ? (profileData.gender as Gender) + : Gender.None, + country: profileData.countryCode ?? '', + }; + setData(fetchedData); + setOriginalData(fetchedData); + setUploadedImageUrl(profileData.profileImageUrl || null); } - }, [storedToken, t]); + }, [profileData]); - const initials = `${data?.firstName?.trim()[0] || ''}‌${ - data?.lastName?.trim()[0] || '' - }`; + useEffect(() => { + if (saveData?.success) { + setIsEditing(false); + setOriginalData(data); + } + }, [saveData, data]); + + const initials = `${data?.firstName?.trim()[0] || ''}‌${data?.lastName?.trim()[0] || ''}`; const handleEditClick = () => { setIsEditing(true); - setSaveError(null); setOriginalData(data); }; @@ -115,85 +84,21 @@ export function PersonalInformation() { if (originalData) { setData(originalData); } - setSaveError(null); }; const handleSaveClick = async () => { if (!data) return; - setSaveError(null); - try { - const formData = new FormData(); - formData.append('FirstName', data.firstName || ''); - formData.append('LastName', data.lastName || ''); - formData.append('NationalCode', data.nationalCode || ''); - formData.append('Gender', String(data.gender ?? Gender.None)); - formData.append('CountryCode', data.country || ''); - - if (uploadedImageUrl && uploadedImageUrl.startsWith('data:')) { - const response = await fetch(uploadedImageUrl); - const blob = await response.blob(); - formData.append('Image', blob, 'profile.jpg'); - } - - const res = await apiClient.post( - 'Profile/SaveProfilePersonalInforamtion', - formData, - { - headers: { - Authorization: `Bearer ${storedToken}`, - }, - }, - ); - - if (res.data.success) { - setIsEditing(false); - setOriginalData(data); - } else { - throw new Error(res.data.message || t('settingForm.unknownError')); - } - } catch (error: unknown) { - let message = t('settingForm.checkConnection'); - if (error instanceof Error) { - message = error.message; - } - setSaveError(message); - } + executeSaveProfile({ data, imageUrl: uploadedImageUrl }); }; - const apiUrl = 'https://accounts.business-harmony.com'; - const tokenEndpoint = `${apiUrl}/connect/token`; - const getToken = async () => { - setTokenError(null); - try { - const body = new URLSearchParams(); - body.set('grant_type', 'password'); - body.set('username', 'zareian.1381@gmail.com'); - body.set('password', '123@Qweasd'); - body.set('client_id', 'harmony_identity'); - body.set('scope', 'openid harmony_identity profile offline_access'); - const response = await axios.post( - tokenEndpoint, - body.toString(), - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }, - ); - if (response.data?.access_token) { - localStorage.setItem('authToken', response.data.access_token); - } else { - throw new Error('No access token in response'); - } - } catch (error: unknown) { - let message = 'Failed to get token'; - if (isAxiosError(error) && error.response) { - message = `Request failed with status ${error.response.status}`; - } else if (error instanceof Error) { - message = error.message; - } - setTokenError(message); - } + const handleGetTokenClick = async () => { + executeGetToken(); + }; + + const getErrorMessage = (error: unknown): string | null => { + if (!error) return null; + if (error instanceof Error) return error.message; + return String(error); }; return ( @@ -222,12 +127,12 @@ export function PersonalInformation() { {isEditing ? ( <> @@ -240,6 +145,7 @@ export function PersonalInformation() { textTransform: 'none', width: { xs: '100%', sm: 'auto' }, }} + disabled={isSavingProfile} > {t('settingForm.rejectButton')} @@ -251,8 +157,13 @@ export function PersonalInformation() { textTransform: 'none', width: { xs: '100%', sm: 'auto' }, }} + disabled={isSavingProfile} > - {t('settingForm.saveButton')} + {isSavingProfile ? ( + + ) : ( + t('settingForm.saveButton') + )} ) : ( @@ -260,27 +171,33 @@ export function PersonalInformation() { onClick={handleEditClick} size="large" variant="outlined" - sx={{ - borderRadius: 1, - }} - disabled={isLoading} + sx={{ borderRadius: 1 }} + disabled={isLoadingProfile} > {t('settingForm.editButton')} )} - {saveError && ( + {getErrorMessage(saveProfileError) && ( - {saveError} + {getErrorMessage(saveProfileError)} + + )} + {tokenData?.success && ( + + Token fetched and stored successfully! )} } > - {isLoading ? ( + {isLoadingProfile ? ( - ) : fetchError ? ( + ) : fetchProfileError ? ( - {fetchError} + + {getErrorMessage(fetchProfileError) || + t('settingForm.errorFetch')} + ) : ( <> - {tokenError && ( - Error: {tokenError} + {getErrorMessage(tokenError) && ( + + Error: {getErrorMessage(tokenError)} + )} ( 'default', ); - const [isVerifying, setIsVerifying] = useState(false); const [isVerified, setIsVerified] = useState(false); - const [phones, setPhone] = useState([]); + const [phones, setPhones] = useState([]); const [countryCode, setCountryCode] = useState('+98'); + const [formError, setFormError] = useState(); + const [touched, setTouched] = useState(false); + const textFieldRef = useRef(null); const inputRef = useRef(null); - const [error, setError] = useState(); - const [touched, setTouched] = useState(false); - const inputError: boolean = touched && !!error; - const token = localStorage.getItem('authToken'); - const [isLoading, setIsLoading] = useState(true); - const [fetchError, setFetchError] = useState(null); + const { + data: profileData, + loading: isLoading, + error: fetchError, + execute: executeFetchProfile, + } = useManualApi(fetchProfile); + + const { + data: sendCodeData, + loading: isSendingCode, + error: sendCodeError, + execute: executeSendCode, + } = useManualApi(sendVerificationCode); + + const { + data: confirmData, + loading: isVerifying, + error: confirmError, + execute: executeConfirmCode, + } = useManualApi(confirmPhoneNumberCode); + + const { + data: changePhoneData, + loading: isChangingPhone, + error: changePhoneError, + execute: executeChangePhone, + } = useManualApi(changePhoneNumber); useEffect(() => { - const fetchPhoneNumber = async () => { - setIsLoading(true); - setFetchError(null); - if (!token) { - setIsLoading(false); - setFetchError(t('settingForm.notLoggedIn')); - return; - } - try { - const res = await apiClient.post( - '/Profile/GetProfile', - {}, - { headers: { Authorization: `Bearer ${token}` } }, - ); + if (!isEditing) { + executeFetchProfile(); + } + }, [isEditing]); - if (res.data.success && res.data.phoneNumber) { - setPhone([ - { - phone: res.data.phoneNumber, - time: '', - withCode: res.data.phoneNumber, - }, - ]); - } else if (!res.data.success) { - throw new Error( - res.data.message || t('settingForm.failFetchPhoneNumber'), - ); - } - } catch (err: unknown) { - let message = t('settingForm.errorFetchPhoneNumber'); - if (err instanceof Error) { - message = err.message; - } - setFetchError(message); - } finally { - setIsLoading(false); - } - }; + useEffect(() => { + if (profileData?.success && profileData.phoneNumber) { + setPhones([ + { + phone: profileData.phoneNumber, + time: '', + withCode: profileData.phoneNumber, + }, + ]); + } + }, [profileData]); - fetchPhoneNumber(); - }, [token, t]); + useEffect(() => { + if (sendCodeData?.success) { + setButtonState('counting'); + setIsVerified(false); + } + }, [sendCodeData]); + + useEffect(() => { + if (confirmData?.success && confirmData.confirm) { + setIsVerified(true); + setShowToast(true); + const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, ''); + executeChangePhone({ phoneNumber: fullPhoneNumber }); + } + }, [confirmData, countryCode, phoneNumber, executeChangePhone]); + + useEffect(() => { + if (changePhoneData?.success) { + const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, ''); + setPhones([ + { + phone: phoneNumber, + time: t('settingForm.justNow'), + withCode: fullPhoneNumber, + }, + ]); + setIsEditing(false); + } + }, [changePhoneData, countryCode, phoneNumber, t]); + + const apiError = useMemo( + () => sendCodeError || confirmError || changePhoneError, + [sendCodeError, confirmError, changePhoneError], + ); + + const getErrorMessage = (error: unknown): string | undefined => { + if (!error) return undefined; + if (error instanceof Error) return error.message; + return String(error); + }; const isPhoneValid = (code: string, phone: string) => { const phoneNum = parsePhoneNumberFromString(code + phone); - return phoneNum && phoneNum.isValid(); + return phoneNum?.isValid(); }; const handleBlur = () => { setTouched(true); if (!phoneNumber) { - setError(t('settingForm.thisFieldIsRequired')); + setFormError(t('settingForm.thisFieldIsRequired')); return; } if (!isPhoneValid(countryCode, phoneNumber)) { - setError(t('settingForm.phoneNumberIsInvalid')); + setFormError(t('settingForm.phoneNumberIsInvalid')); } else { - setError(undefined); + setFormError(undefined); } }; const toggleEdit = () => { setIsEditing((prev) => { - const enteringEditMode = !prev; - if (enteringEditMode) { + if (!prev) { setPhoneNumber(''); setVerificationCode(''); setIsVerified(false); setButtonState('default'); setShowToast(false); - setError(undefined); + setFormError(undefined); setTouched(false); } - return enteringEditMode; + return !prev; }); }; - const handleSendCode = async () => { - if (!phoneNumber) return; - if (!isPhoneValid(countryCode, phoneNumber)) { - setError(t('settingForm.phoneNumberIsInvalid')); - return; - } - setError(undefined); + const handleSendCode = () => { + handleBlur(); + if (formError || !isPhoneValid(countryCode, phoneNumber)) return; - try { - const res = await apiClient.post( - '/Profile/SendVerfiyPhoneNumberCode', - { - phoneNumber: countryCode + phoneNumber.replace(/^0/, ''), - }, - { headers: { Authorization: `Bearer ${token}` } }, - ); - if (res.data.success) { - setButtonState('counting'); - setIsVerified(false); - } else { - setError(res.data.message || t('settingForm.sendCodeFailed')); - } - } catch (error: unknown) { - setError(t('settingForm.sendCodeFailed')); - } + executeSendCode({ + phoneNumber: countryCode + phoneNumber.replace(/^0/, ''), + }); }; - const handleVerifyCode = async () => { + const handleVerifyCode = () => { if (!verificationCode) { - setError(t('settingForm.verificationCodeRequired')); + setFormError(t('settingForm.verificationCodeRequired')); return; } - setIsVerifying(true); - setError(undefined); - - try { - const res = await apiClient.post( - '/Profile/ConfirmPhoneNumberChangeCode', - { - phoneNumber: countryCode + phoneNumber.replace(/^0/, ''), - verifyCode: verificationCode, - }, - { headers: { Authorization: `Bearer ${token}` } }, - ); - - if (res.data.success && res.data.confirm) { - setIsVerified(true); - setShowToast(true); - await handleChangePhoneNumber(); - } else { - setError(res.data.message || t('settingForm.verifyCodeFailed')); - setIsVerified(false); - } - } catch (error: unknown) { - setError(t('settingForm.verifyCodeFailed')); - setIsVerified(false); - } finally { - setIsVerifying(false); - } + setFormError(undefined); + executeConfirmCode({ + phoneNumber: countryCode + phoneNumber.replace(/^0/, ''), + verifyCode: verificationCode, + }); }; - const handleChangePhoneNumber = async () => { - try { - const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, ''); - const res = await apiClient.post( - '/Profile/ChangePhoneNumber', - { - phoneNumber: fullPhoneNumber, - }, - { headers: { Authorization: `Bearer ${token}` } }, - ); - - if (res.data.success) { - setPhone([ - { - phone: phoneNumber, - time: t('settingForm.justNow'), - withCode: fullPhoneNumber, - }, - ]); - setIsEditing(false); - } else { - setError(res.data.message || t('settingForm.changePhoneFailed')); - } - } catch (error: unknown) { - setError(t('settingForm.changePhoneFailed')); - } - }; + const combinedError = formError || getErrorMessage(apiError); + const inputError: boolean = touched && !!combinedError; return ( @@ -248,7 +212,10 @@ export function PhoneNumber() { ) : fetchError ? ( - {fetchError} + + {getErrorMessage(fetchError) || + t('settingForm.errorFetchPhoneNumber')} + ) : isEditing ? ( } diff --git a/src/features/profile/components/userInformation/personalInformation/InfoRowDisplay.tsx b/src/features/profile/components/userInformation/personalInformation/InfoRowDisplay.tsx index 6979ed2..01ef683 100644 --- a/src/features/profile/components/userInformation/personalInformation/InfoRowDisplay.tsx +++ b/src/features/profile/components/userInformation/personalInformation/InfoRowDisplay.tsx @@ -2,7 +2,7 @@ import { Box, Typography, Avatar } from '@mui/material'; import { useTranslation } from 'react-i18next'; import { DisplayField } from './DisplayField'; import { CountryFlag } from '@/components/CountryFlag'; -import { Gender } from '@/features/profile/types'; +import { Gender } from '@/features/profile/types/settingsType'; interface InfoRowData { firstName: string; diff --git a/src/features/profile/components/userInformation/personalInformation/InfoRowEdit.tsx b/src/features/profile/components/userInformation/personalInformation/InfoRowEdit.tsx index 38d1622..7ee018c 100644 --- a/src/features/profile/components/userInformation/personalInformation/InfoRowEdit.tsx +++ b/src/features/profile/components/userInformation/personalInformation/InfoRowEdit.tsx @@ -10,8 +10,8 @@ import { import { useTranslation } from 'react-i18next'; import { countries } from '@/features/profile/data/countries'; import { CountryFlag } from '@/components/CountryFlag'; -import { Gender } from '@/features/profile/types'; -import { type InfoRowData } from '@/features/profile/types'; +import { Gender } from '@/features/profile/types/settingsType'; +import { type InfoRowData } from '@/features/profile/types/settingsType'; interface InfoRowEditProps { data: InfoRowData; diff --git a/src/features/profile/types/settingsApiType.ts b/src/features/profile/types/settingsApiType.ts new file mode 100644 index 0000000..02ac5da --- /dev/null +++ b/src/features/profile/types/settingsApiType.ts @@ -0,0 +1,35 @@ +import { Gender } from './settingsType'; + +/* Profile API Types */ +export interface GetProfileApiResponse { + success: boolean; + message?: string; + firstName?: string; + lastName?: string; + nationalCode?: string; + gender?: Gender; + countryCode?: string; + profileImageUrl?: string; + phoneNumber?: string; // ✅ ADDED +} + +export interface SaveProfileApiResponse { + success: boolean; + message?: string; + phoneNumber?: string; + email?: string; +} + +export interface TokenApiResponse { + access_token: string; +} + +/* Phone Number API Types */ +export interface PhoneNumberApiResponse { + success: boolean; + message?: string; +} + +export interface ConfirmPhoneNumberApiResponse extends PhoneNumberApiResponse { + confirm?: boolean; +} diff --git a/src/features/profile/types.ts b/src/features/profile/types/settingsType.ts similarity index 86% rename from src/features/profile/types.ts rename to src/features/profile/types/settingsType.ts index f106063..96c9964 100644 --- a/src/features/profile/types.ts +++ b/src/features/profile/types/settingsType.ts @@ -1,6 +1,6 @@ export enum Gender { - Male = 1, - Female = 2, + Male = 2, + Female = 1, None = 0, } diff --git a/src/hooks/useApi.ts b/src/hooks/useApi.ts index 86fe5bb..3f17789 100644 --- a/src/hooks/useApi.ts +++ b/src/hooks/useApi.ts @@ -1,26 +1,24 @@ -import { useState, useEffect } from 'react'; +import { useState } from 'react'; -type ApiFunction = () => Promise<{ data: T }>; +type ApiFunction = (params?: P) => Promise<{ data: T }>; -export function useApi(apiFunction: ApiFunction) { +export function useManualApi(apiFunction: ApiFunction) { const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - useEffect(() => { - const fetchData = async () => { - try { - const response = await apiFunction(); - setData(response.data); - } catch (err) { - setError(err); - } finally { - setLoading(false); - } - }; + const execute = async (params?: P) => { + setLoading(true); + setError(null); + try { + const response = await apiFunction(params); + setData(response.data); + } catch (err) { + setError(err); + } finally { + setLoading(false); + } + }; - fetchData(); - }, [apiFunction]); - - return { data, loading, error }; + return { data, loading, error, execute }; }