({
+ apiEndpoint: 'https://picsum.photos/300/160',
+ timeout: 5001,
+ });
+
+ if (!response?.url) {
+ throw new Error('Error fetching the image, no response url');
}
+
setImageUrl(response.url);
updateClientCtx({ fetchCount: fetchCount + 1 });
setAlertBarProps({
@@ -36,18 +53,22 @@ const DisplayRandomPicture = () => {
severity: 'info',
});
} catch (error) {
- setError('Error fetching the image');
+ const errorMsg =
+ error instanceof Error ? error.message : 'Error fetching the image';
+
+ setError(errorMsg);
setAlertBarProps({
- message: 'Error fetching the image',
+ message: errorMsg,
severity: 'error',
});
+ setLoading(false);
} finally {
setLoading(false);
}
};
useEffect(() => {
- if (renderCountRef.current === 0) {
+ if (renderCountRef.current === 0 && !loading) {
fetchRandomPicture();
}
renderCountRef.current += 1;
@@ -59,6 +80,7 @@ const DisplayRandomPicture = () => {
justifyContent='center'
alignItems='center'
spacing={2}
+ sx={{ position: 'relative', width: '300px', margin: '0 auto' }}
>
{error && {error}
}
{imageUrl && (
@@ -71,7 +93,7 @@ const DisplayRandomPicture = () => {
)}
{loading && Loading...} Component Render Count:{' '}
- {renderCountRef.current}
+ {renderCountRef.current + 1}
{
Get Another Picture
+ {imageUrl && (
+
+
+
+
+
+ )}
{renderAlertBar()}
);
};
+const spin = keyframes`
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+`;
+const StyledRefreshButton = styled.div<{ loading?: boolean }>`
+ position: absolute;
+ right: 0;
+ top: 0;
+ margin: 0.5rem !important;
+ pointer-events: ${({ loading }) => (loading ? 'none' : 'auto')};
+ opacity: ${({ loading }) => (loading ? '0.6' : '1')};
+ cursor: ${({ loading }) => (loading ? 'not-allowed' : 'pointer')};
+ svg {
+ width: 20px;
+ height: 20px;
+ animation: ${({ loading }) =>
+ loading
+ ? css`
+ ${spin} 2s linear infinite
+ `
+ : 'none'};
+ }
+ :hover {
+ svg {
+ path {
+ fill: ${purple[500]};
+ }
+ }
+ .MuiAvatar-circular {
+ background-color: ${purple[50]};
+ }
+ }
+`;
+
export default DisplayRandomPicture;
diff --git a/src/utils/shared/get-api-response.ts b/src/utils/shared/get-api-response.ts
index c4cb92d..45e70e4 100644
--- a/src/utils/shared/get-api-response.ts
+++ b/src/utils/shared/get-api-response.ts
@@ -1,21 +1,40 @@
import { IS_PROD } from '@/constants';
import { consoleLog } from '@/utils/shared/console-log';
+/**
+ * Makes an API request and returns the response data.
+ *
+ * @param apiEndpoint - The API endpoint URL.
+ * @param requestData - The request data to be sent in the request body.
+ * @param method - The HTTP method for the request (default: 'GET').
+ * @param revalidate - The time in seconds to cache the data (default: 3600 seconds in production, 120 seconds otherwise).
+ * @param headers - The headers to be included in the request.
+ * @param timeout - The timeout in milliseconds for the request (default: 100000 = 100 seconds).
+ * @returns The response data from the API.
+ * @throws An error if the API request fails or times out.
+ */
export const getApiResponse = async ({
apiEndpoint,
requestData,
method = 'GET',
revalidate = IS_PROD ? 3600 : 120, // cache data in seconds
headers,
+ timeout = 100000, // 100 seconds
}: {
apiEndpoint: string;
requestData?: BodyInit;
method?: 'POST' | 'GET' | 'PUT' | 'DELETE';
revalidate?: number;
headers?: HeadersInit;
+ timeout?: number;
}) => {
try {
const startTime = Date.now();
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
+
const response = await fetch(apiEndpoint, {
method,
body: requestData,
@@ -23,6 +42,7 @@ export const getApiResponse = async ({
next: {
revalidate,
},
+ signal,
});
if (!response.ok) {
consoleLog('🚀 Debug getApiResponse requestData:', requestData);
@@ -38,9 +58,18 @@ export const getApiResponse = async ({
duration > 2000 ? '💔' : '-'
} ${apiEndpoint}`
);
-
+ clearTimeout(timeoutId);
+ // if is not valid JSON, return response
+ if (!response.headers.get('content-type')?.includes('application/json')) {
+ return response as T;
+ }
return (await response.json()) as T;
} catch (error) {
+ if (error instanceof Error && error.name === 'AbortError') {
+ throw new Error(
+ 'Fetch request timed out: ' + (timeout / 1000).toFixed(1) + ' s'
+ );
+ }
consoleLog('getApiResponse error:', error);
}