Skip to content

Commit e4e027d

Browse files
committed
update loading state UI to be more descriptive
1 parent 30899d5 commit e4e027d

File tree

8 files changed

+132
-57
lines changed

8 files changed

+132
-57
lines changed

src/components/CalculationView/CalculationView.tsx

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ import { useState } from "react";
22
import clsx from "clsx";
33

44
import { useStorage } from "../../hooks/useStorage";
5-
import { useCurrencyMapData } from "../../hooks/useCurrencyMap";
65

7-
import { Gears } from "../Gears";
86
import { CurrencySelection } from "./CurrencySelection";
97
import { CalculationResults } from "./CalculationResults";
108
import { CurrencyInput } from "./CurrencyInput";
@@ -14,15 +12,6 @@ export function CalculationView() {
1412
const { preferences } = useStorage();
1513
const [selected, setSelected] = useState<CurrencyKey | "">(preferences.starred ?? "");
1614
const [value, setValue] = useState("1");
17-
const currencyMap = useCurrencyMapData();
18-
19-
if (currencyMap == null) {
20-
return (
21-
<div className='flex-1'>
22-
<Gears />
23-
</div>
24-
);
25-
}
2615

2716
return (
2817
<div className={clsx("flex flex-col h-min gap-4", "lg:gap-6 lg:flex-row")}>

src/components/CenterChild.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { PropsWithChildren } from "react";
2+
3+
export const CenterChild = ({ children }: PropsWithChildren) => (
4+
<div className='grid place-items-center w-screen h-screen'>{children}</div>
5+
);

src/components/Gears.tsx

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import "./gears.css";
22
// https://codepen.io/Brian-Montierth/details/PVZRNj
3-
export const Gears = () => (
4-
<div className='gears'>
5-
<div className='gear one'>
6-
<div className='bar'></div>
7-
<div className='bar'></div>
8-
<div className='bar'></div>
9-
</div>
10-
<div className='gear two'>
11-
<div className='bar'></div>
12-
<div className='bar'></div>
13-
<div className='bar'></div>
14-
</div>
15-
<div className='gear three'>
16-
<div className='bar'></div>
17-
<div className='bar'></div>
18-
<div className='bar'></div>
3+
export const Gears = ({ isLoading }: { isLoading?: boolean }) => (
4+
<div className={isLoading ? "loading" : undefined}>
5+
<div className='gears'>
6+
<div className='gear one'>
7+
<div className='bar'></div>
8+
<div className='bar'></div>
9+
<div className='bar'></div>
10+
</div>
11+
<div className='gear two'>
12+
<div className='bar'></div>
13+
<div className='bar'></div>
14+
<div className='bar'></div>
15+
</div>
16+
<div className='gear three'>
17+
<div className='bar'></div>
18+
<div className='bar'></div>
19+
<div className='bar'></div>
20+
</div>
1921
</div>
2022
</div>
2123
);

src/components/shadcn/Alert.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ const alertVariants = cva(
77
{
88
variants: {
99
variant: {
10-
default: "bg-background text-foreground",
11-
destructive: "border-red-900/50 text-red-900 dark:border-red-900 [&>svg]:text-red-900"
10+
default: "bg-black border-primary-dark text-primary-main",
11+
destructive: "border-red-900/50 text-red-900 dark:border-red-900 [&>svg]:text-red-900",
12+
warning: "border-yellow-600 text-yellow-600"
1213
}
1314
},
1415
defaultVariants: {

src/main.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { StrictMode } from "react";
22
import { createRoot } from "react-dom/client";
33

4+
import { setInitialStorageValues } from "./utils/storage.ts";
5+
46
import App from "./App.tsx";
57

8+
setInitialStorageValues();
9+
610
createRoot(document.getElementById("root")!).render(
711
<StrictMode>
812
<App />

src/utils/AuthGuard.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import clsx from "clsx";
21
import { PropsWithChildren, useEffect, useState } from "react";
32
import { getAuth, signInAnonymously } from "firebase/auth";
43
import { getFirestore } from "firebase/firestore";
@@ -31,17 +30,32 @@ export function AuthGuard(props: PropsWithChildren) {
3130
});
3231
}, []);
3332

34-
if (!isSignedIn && !isLoading) {
33+
if (isLoading) {
34+
return <Gears isLoading />;
35+
}
36+
37+
if (error) {
3538
return (
36-
<div className={clsx("flex flex-1 flex-col items-center mt-4", isLoading ? "loading" : undefined)}>
39+
<div className='flex flex-1 flex-col items-center mt-4'>
3740
<Alert variant='destructive' className='w-full max-w-[450px] z-50 bg-black'>
3841
<AlertCircle className='h-4 w-4' />
3942
<AlertTitle>Error</AlertTitle>
4043
<AlertDescription>
4144
<p>There was a problem while connecting to the server. Please try again later.</p>
42-
{error && <p>{error}</p>}
45+
<p>{error}</p>
4346
</AlertDescription>
4447
</Alert>
48+
49+
<Gears />
50+
</div>
51+
);
52+
}
53+
54+
if (!isSignedIn) {
55+
return (
56+
<div className='flex flex-1 flex-col items-center mt-4'>
57+
<p>Unable to sign in. Please try again later.</p>
58+
4559
<Gears />
4660
</div>
4761
);

src/utils/CurrencyMapProvider.tsx

Lines changed: 75 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,92 @@ import { collection, getDocs, limit, orderBy, query } from "firebase/firestore";
44
import { CurrencyMapContext } from "../context/CurrencyMapContext";
55
import { DatabaseContext } from "../context/DatabaseContext";
66
import { setCache } from "./storage";
7+
import { Gears } from "@/components/Gears";
8+
import { Alert, AlertDescription, AlertTitle } from "@/components/shadcn/Alert";
9+
import { AlertCircle, LoaderCircle, RefreshCw } from "lucide-react";
10+
import { CenterChild } from "@/components/CenterChild";
11+
import { Button } from "@/components/shadcn/Button";
712

813
export function CurrencyMapProvider(props: PropsWithChildren) {
914
const db = useContext(DatabaseContext);
1015
const [currencyMap, setCurrencyMap] = useState<RateDefinitions | null>(null);
16+
const [error, setError] = useState<string | null>(null);
17+
const [isLoadingDelayed, setIsLoadingDelayed] = useState(false);
1118

1219
useEffect(() => {
20+
const loadingDelayTimeout = setTimeout(() => {
21+
setIsLoadingDelayed(true);
22+
}, 3000);
23+
1324
const fetchLatestDocument = async () => {
14-
const collectionRef = collection(db, "rates");
25+
try {
26+
const collectionRef = collection(db, "rates");
27+
const q = query(collectionRef, orderBy("meta.createdAt", "desc"), limit(1));
28+
const querySnapshot = await getDocs(q);
1529

16-
const q = query(collectionRef, orderBy("meta.createdAt", "desc"), limit(1)); // Query to fetch the latest document
30+
if (!querySnapshot.empty) {
31+
querySnapshot.forEach((doc) => {
32+
const data = doc.data() as RateDefinitions;
33+
setCurrencyMap(data);
34+
setCache(data);
35+
});
36+
} else {
37+
setError("No exchange rate data found in the database.");
38+
}
39+
} catch (err) {
40+
setError("Could not fetch latest exchange rate data. Please try again after a few minutes.");
41+
} finally {
42+
clearTimeout(loadingDelayTimeout);
43+
}
44+
};
1745

18-
const querySnapshot = await getDocs(q);
46+
fetchLatestDocument();
1947

20-
// FIXME: do I have to loop a .limit(1) data?
21-
querySnapshot.forEach((doc) => {
22-
const data = doc.data() as RateDefinitions;
23-
setCurrencyMap(data);
24-
setCache(data);
25-
});
26-
};
48+
return () => clearTimeout(loadingDelayTimeout);
49+
}, [db]);
50+
51+
if (error) {
52+
return (
53+
<CenterChild>
54+
<Alert variant='destructive' className='w-full max-w-[450px]'>
55+
<AlertCircle className='h-4 w-4' />
56+
<AlertTitle>Error</AlertTitle>
57+
<AlertDescription>{error}</AlertDescription>
58+
</Alert>
59+
</CenterChild>
60+
);
61+
}
62+
63+
if (isLoadingDelayed && !currencyMap) {
64+
return (
65+
<CenterChild>
66+
<div className='flex flex-col gap-6 items-center'>
67+
<Alert variant='default' className='w-full max-w-[450px]'>
68+
<LoaderCircle className='w-4 h-4 animate-spin' />
69+
<AlertTitle>Warning!</AlertTitle>
70+
<AlertDescription>
71+
<p>
72+
The data is still trying to load, but it is taking longer than expected.{" "}
73+
<span className='font-semibold'>It may keep loading indefinitely.</span>
74+
</p>
75+
<p>
76+
If the app does not launch within a few seconds, you can{" "}
77+
<span className='underline'>reload the page</span> or try again later.
78+
</p>
79+
</AlertDescription>
80+
</Alert>
81+
82+
<Button onClick={() => window.location.reload()}>
83+
<RefreshCw className='h-4 w-4' /> Reload Page
84+
</Button>
85+
</div>
86+
</CenterChild>
87+
);
88+
}
2789

28-
fetchLatestDocument()
29-
.then(() => {
30-
// TODO: on fetch success?
31-
})
32-
.catch((error) => {
33-
console.error("Error fetching latest document:", error);
34-
});
35-
}, []);
90+
if (!currencyMap) {
91+
return <Gears isLoading />;
92+
}
3693

3794
return <CurrencyMapContext.Provider value={currencyMap}>{props.children}</CurrencyMapContext.Provider>;
3895
}

src/utils/storage.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const cacheKey = "currencyMapCache";
22
const preferencesKey = "preferences";
3+
34
const initialPreferences: Preference = {
45
pinned: null,
56
starred: null,
@@ -27,11 +28,13 @@ export function getPreferences() {
2728
return JSON.parse(localStorage.getItem(preferencesKey)!) as Preference;
2829
}
2930

30-
if (!localStorage.getItem(preferencesKey)) {
31-
setPreferences(initialPreferences);
32-
} else {
33-
// Even if preferences exist, new additions to here will have to be considered. So, it is better to always write inital values on load, then overwrite with existing values.
34-
const pref = getPreferences();
31+
export function setInitialStorageValues() {
32+
if (!localStorage.getItem(preferencesKey)) {
33+
setPreferences(initialPreferences);
34+
} else {
35+
// Even if preferences exist, new additions to here will have to be considered. So, it is better to always write inital values on load, then overwrite with existing values.
36+
const pref = getPreferences();
3537

36-
setPreferences({ ...initialPreferences, ...pref });
38+
setPreferences({ ...initialPreferences, ...pref });
39+
}
3740
}

0 commit comments

Comments
 (0)