Skip to content

Commit e7ac4a9

Browse files
committed
feat: add a RefreshButton with spiner while loading
1 parent 8c51fb6 commit e7ac4a9

File tree

2 files changed

+73
-34
lines changed

2 files changed

+73
-34
lines changed

src/app/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ const loadDataFromApi = async (slug?: string) => {
1414
getApiResponse<NpmData>({
1515
apiEndpoint: 'https://registry.npmjs.org/react/latest',
1616
revalidate: 60 * 60 * 24, // 24 hours cache
17+
timeout: 5000, // 5 seconds
1718
}),
1819
getApiResponse<NpmData>({
1920
apiEndpoint: 'https://registry.npmjs.org/next/latest',
2021
revalidate: 0, // no cache
22+
timeout: 5000, // 5 seconds
2123
}),
2224
]);
2325

src/components/shared/DisplayRandomPicture.tsx

Lines changed: 71 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
'use client';
44
import styled from '@emotion/styled';
55
import { Autorenew, Send } from '@mui/icons-material';
6+
import { css, keyframes } from '@mui/material';
67
import Avatar from '@mui/material/Avatar';
78
import Button from '@mui/material/Button';
89
import { purple } from '@mui/material/colors';
@@ -14,64 +15,60 @@ import { useClientContext } from '@/hooks/useClientContext';
1415

1516
import SubmitButton from '@/components/shared/SubmitButton';
1617

17-
const StyledRefreshButton = styled.div`
18-
position: absolute;
19-
right: 0;
20-
top: 0;
21-
margin: 0.5rem;
22-
cursor: pointer;
23-
svg {
24-
width: 20px;
25-
height: 20px;
26-
}
27-
:hover {
28-
svg {
29-
path {
30-
fill: ${purple[500]};
31-
}
32-
}
33-
.MuiAvatar-circular {
34-
background-color: ${purple[50]};
35-
}
36-
}
37-
`;
18+
import { getApiResponse } from '@/utils/shared/get-api-response';
3819

3920
const DisplayRandomPicture = () => {
4021
const [imageUrl, setImageUrl] = useState('');
41-
const [loading, setLoading] = useState(true);
22+
const [loading, setLoading] = useState(false);
4223
const [error, setError] = useState('');
4324
const { fetchCount, updateClientCtx } = useClientContext();
4425
const { setAlertBarProps, renderAlertBar } = useAlertBar();
4526
const renderCountRef = React.useRef(0);
4627

4728
const fetchRandomPicture = async () => {
29+
if (loading) {
30+
setAlertBarProps({
31+
message: 'Please wait for the current fetch to complete',
32+
severity: 'warning',
33+
});
34+
return;
35+
}
4836
setLoading(true);
4937
setError('');
5038

5139
try {
52-
const response = await fetch('https://picsum.photos/300/150');
53-
if (!response.ok) {
54-
throw new Error('Error fetching the image');
40+
const response = await getApiResponse<Response & { url: string }>({
41+
apiEndpoint: 'https://picsum.photos/300/160',
42+
timeout: 5001,
43+
});
44+
45+
if (!response?.url) {
46+
throw new Error('Error fetching the image, no response url');
5547
}
48+
5649
setImageUrl(response.url);
5750
updateClientCtx({ fetchCount: fetchCount + 1 });
5851
setAlertBarProps({
5952
message: 'A random picture fetched successfully',
6053
severity: 'info',
6154
});
6255
} catch (error) {
63-
setError('Error fetching the image');
56+
const errorMsg =
57+
error instanceof Error ? error.message : 'Error fetching the image';
58+
59+
setError(errorMsg);
6460
setAlertBarProps({
65-
message: 'Error fetching the image',
61+
message: errorMsg,
6662
severity: 'error',
6763
});
64+
setLoading(false);
6865
} finally {
6966
setLoading(false);
7067
}
7168
};
7269

7370
useEffect(() => {
74-
if (renderCountRef.current === 0) {
71+
if (renderCountRef.current === 0 && !loading) {
7572
fetchRandomPicture();
7673
}
7774
renderCountRef.current += 1;
@@ -96,7 +93,7 @@ const DisplayRandomPicture = () => {
9693
)}
9794
<div>
9895
{loading && <span>Loading...</span>} Component Render Count:{' '}
99-
{renderCountRef.current}
96+
{renderCountRef.current + 1}
10097
</div>
10198

10299
<SubmitButton
@@ -113,14 +110,54 @@ const DisplayRandomPicture = () => {
113110
Get Another Picture
114111
</Button>
115112
</SubmitButton>
116-
<StyledRefreshButton onClick={fetchRandomPicture}>
117-
<Avatar sx={{ width: 24, height: 24 }}>
118-
<Autorenew />
119-
</Avatar>
120-
</StyledRefreshButton>
113+
{imageUrl && (
114+
<StyledRefreshButton onClick={fetchRandomPicture} loading={loading}>
115+
<Avatar sx={{ width: 24, height: 24 }}>
116+
<Autorenew />
117+
</Avatar>
118+
</StyledRefreshButton>
119+
)}
121120
{renderAlertBar()}
122121
</Stack>
123122
);
124123
};
125124

125+
const spin = keyframes`
126+
from {
127+
transform: rotate(0deg);
128+
}
129+
to {
130+
transform: rotate(360deg);
131+
}
132+
`;
133+
const StyledRefreshButton = styled.div<{ loading?: boolean }>`
134+
position: absolute;
135+
right: 0;
136+
top: 0;
137+
margin: 0.5rem !important;
138+
pointer-events: ${({ loading }) => (loading ? 'none' : 'auto')};
139+
opacity: ${({ loading }) => (loading ? '0.6' : '1')};
140+
cursor: ${({ loading }) => (loading ? 'not-allowed' : 'pointer')};
141+
svg {
142+
width: 20px;
143+
height: 20px;
144+
animation: ${({ loading }) =>
145+
loading
146+
? css`
147+
${spin} 2s linear infinite
148+
`
149+
: 'none'};
150+
}
151+
:hover {
152+
svg {
153+
path {
154+
fill: ${purple[500]};
155+
}
156+
}
157+
.MuiAvatar-circular {
158+
background-color: ${purple[50]};
159+
}
160+
}
161+
`;
162+
126163
export default DisplayRandomPicture;

0 commit comments

Comments
 (0)