Skip to content

Commit b81abe0

Browse files
authored
refactor(frontend): consolidate Home card options into a single state object (#344)
`HomeScreen` held ~16 separate `useState` hooks for card options and threaded them through 34 props into `CustomizeStage`. This consolidates them into a single typed `CardOptions` state object (new `pages/Home/cardOptions.ts`), updated via a keyed setter. ### Changes - `CustomizeStage` props go from 34 to 5; `buildCardUrl` takes `userId`, `selectedCard` and `options`. - Stage descriptions move into `STAGE_LABELS`, replacing a positional string array in the JSX. - The three GitHub-URL paste handlers in `CustomizeStage` share one `pastedPathTail` helper. - A failed OAuth `authenticate` call left the page stuck on the loading spinner; the exchange now runs in `try/catch/finally` and the redirect is parsed with `URLSearchParams` instead of substring matching.
1 parent b0100ca commit b81abe0

6 files changed

Lines changed: 267 additions & 335 deletions

File tree

apps/frontend/src/models/Stage.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,34 @@ export const STAGE_LABELS = [
22
{
33
title: "Login",
44
shortTitle: "Login",
5+
description: "",
56
},
67
{
78
title: "Select a Card",
89
shortTitle: "Select Card",
10+
description: "You will be able to customize your card in future steps.",
911
},
1012
{
1113
title: "Modify Card Parameters",
1214
shortTitle: "Modify Parameters",
15+
description: "",
1316
},
1417
{
1518
title: "Choose a Theme",
1619
shortTitle: "Select Theme",
20+
description: "",
1721
},
1822
{
1923
title: "Display your Card",
2024
shortTitle: "Display Card",
25+
description:
26+
"Display the finished card on GitHub, Twitter/X, LinkedIn, or anywhere else!",
2127
},
22-
] as const satisfies Array<{ title: string; shortTitle: string }>;
28+
] as const satisfies Array<{
29+
title: string;
30+
shortTitle: string;
31+
description: string;
32+
}>;
2333

2434
export type StageIndex = {
2535
[K in keyof typeof STAGE_LABELS]: K extends `${infer N extends number}`

apps/frontend/src/pages/Home/Home.tsx

Lines changed: 52 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
1-
import { useEffect, useMemo, useRef, useState } from "react";
1+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
22
import type { JSX } from "react";
33
import { useDispatch } from "react-redux";
44
import { BounceLoader } from "react-spinners";
55
import { v4 as uuidv4 } from "uuid";
66

77
import { authenticate } from "../../api/user";
88
import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../../components/Home/LanguagesLayoutSection";
9-
import { DEFAULT_OPTION as STATS_DEFAULT_RANK } from "../../components/Home/StatsRankSection";
109
import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../../components/Home/WakatimeLayoutSection";
11-
import {
12-
DEMO_GIST,
13-
DEMO_REPO,
14-
DEMO_USER,
15-
DEMO_WAKATIME_USER,
16-
HOST,
17-
} from "../../constants";
10+
import { DEMO_USER } from "../../constants";
1811
import { CardType } from "../../models/CardType";
1912
import { STAGE_LABELS } from "../../models/Stage";
2013
import type { StageIndex } from "../../models/Stage";
@@ -27,6 +20,8 @@ import {
2720
import { login } from "../../redux/slices/user";
2821

2922
import { buildCardUrl } from "./buildCardUrl";
23+
import { getDefaultCardOptions } from "./cardOptions";
24+
import type { CardOptions } from "./cardOptions";
3025
import { CustomizeStage } from "./stages/Customize";
3126
import { DisplayStage } from "./stages/Display";
3227
import { LoginStage } from "./stages/Login/Login";
@@ -48,57 +43,44 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
4843

4944
const dispatch = useDispatch();
5045

51-
// for stage two
52-
const [selectedUserId, setSelectedUserId] = useState(userId);
53-
const [repo, setRepo] = useState(DEMO_REPO);
54-
const [gist, setGist] = useState(DEMO_GIST);
55-
const [wakatimeUser, setWakatimeUser] = useState(DEMO_WAKATIME_USER);
56-
5746
const [selectedCard, setSelectedCard] = useState<CardType>(CardType.STATS);
5847

48+
// for stages two and three
49+
const [cardOptions, setCardOptions] = useState(() =>
50+
getDefaultCardOptions(userId),
51+
);
52+
53+
const setCardOption = useCallback<
54+
<K extends keyof CardOptions>(key: K, value: CardOptions[K]) => void
55+
>((key, value) => {
56+
setCardOptions((prev) => ({ ...prev, [key]: value }));
57+
}, []);
58+
5959
// Reset the selected user to the resolved account id when it changes,
6060
// adjusting state during render rather than in an effect:
6161
// https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
6262
const [prevUserId, setPrevUserId] = useState(userId);
6363
if (userId !== prevUserId) {
6464
setPrevUserId(userId);
65-
setSelectedUserId(userId);
65+
setCardOptions((prev) => ({ ...prev, selectedUserId: userId }));
6666
}
6767

68-
// for stage three
69-
const [selectedStatsRank, setSelectedStatsRank] =
70-
useState(STATS_DEFAULT_RANK);
71-
const [selectedLanguagesLayout, setSelectedLanguagesLayout] = useState(
72-
LANGUAGES_DEFAULT_LAYOUT,
73-
);
74-
const [selectedWakatimeLayout, setSelectedWakatimeLayout] = useState(
75-
WAKATIME_DEFAULT_LAYOUT,
76-
);
77-
78-
const [showTitle, setShowTitle] = useState(true);
79-
const [showOwner, setShowOwner] = useState(false);
80-
const [descriptionLines, setDescriptionLines] = useState<
81-
number | undefined
82-
>();
83-
const [customTitle, setCustomTitle] = useState("");
84-
const [langsCount, setLangsCount] = useState<number | undefined>();
85-
const [hideValues, setHideValues] = useState(false);
86-
const [showAllStats, setShowAllStats] = useState(false);
87-
const [showIcons, setShowIcons] = useState(false);
88-
const [includeAllCommits, setIncludeAllCommits] = useState(true);
89-
const [enableAnimations, setEnableAnimations] = useState(true);
90-
const [usePercent, setUsePercent] = useState(false);
91-
9268
const { isDark } = useTheme();
9369
const [theme, setTheme] = useState(isDark ? "dark" : "default");
9470

9571
const handleCardTypeChange = (cardType: CardType) => {
9672
if (cardType === CardType.TOP_LANGS) {
97-
setLangsCount(4);
98-
setSelectedWakatimeLayout(WAKATIME_DEFAULT_LAYOUT);
73+
setCardOptions((prev) => ({
74+
...prev,
75+
langsCount: 4,
76+
selectedWakatimeLayout: WAKATIME_DEFAULT_LAYOUT,
77+
}));
9978
} else if (cardType === CardType.WAKATIME) {
100-
setLangsCount(6);
101-
setSelectedLanguagesLayout(LANGUAGES_DEFAULT_LAYOUT);
79+
setCardOptions((prev) => ({
80+
...prev,
81+
langsCount: 6,
82+
selectedLanguagesLayout: LANGUAGES_DEFAULT_LAYOUT,
83+
}));
10284
}
10385

10486
if (theme === "default" || theme === "default_repocard") {
@@ -119,51 +101,8 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
119101
// string), but memoizing keeps it safe to pass as a prop if any of them are
120102
// ever wrapped in React.memo.
121103
const cardBuilder = useMemo(
122-
() =>
123-
buildCardUrl({
124-
userId,
125-
selectedCard,
126-
selectedUserId,
127-
repo,
128-
gist,
129-
wakatimeUser,
130-
selectedStatsRank,
131-
selectedLanguagesLayout,
132-
selectedWakatimeLayout,
133-
showTitle,
134-
showOwner,
135-
descriptionLines,
136-
customTitle,
137-
langsCount,
138-
hideValues,
139-
showAllStats,
140-
showIcons,
141-
includeAllCommits,
142-
enableAnimations,
143-
usePercent,
144-
}),
145-
[
146-
userId,
147-
selectedCard,
148-
selectedUserId,
149-
repo,
150-
gist,
151-
wakatimeUser,
152-
selectedStatsRank,
153-
selectedLanguagesLayout,
154-
selectedWakatimeLayout,
155-
showTitle,
156-
showOwner,
157-
descriptionLines,
158-
customTitle,
159-
langsCount,
160-
hideValues,
161-
showAllStats,
162-
showIcons,
163-
includeAllCommits,
164-
enableAnimations,
165-
usePercent,
166-
],
104+
() => buildCardUrl(userId, selectedCard, cardOptions),
105+
[userId, selectedCard, cardOptions],
167106
);
168107

169108
// Preview builder for the customize stage, dark-themed to match the surroundings.
@@ -187,10 +126,10 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
187126
const cardDescriptor = useCardDescriptor({
188127
selectedCard,
189128
themeBuilder,
190-
repo,
129+
repo: cardOptions.repo,
191130
userId,
192-
wakatimeUser,
193-
gist,
131+
wakatimeUser: cardOptions.wakatimeUser,
132+
gist: cardOptions.gist,
194133
});
195134

196135
const contentSectionRef = useRef<HTMLDivElement | null>(null);
@@ -208,31 +147,31 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
208147
}, [stage]);
209148

210149
useEffect(() => {
211-
async function redirectCode() {
212-
// After requesting GitHub access, GitHub redirects back to your app with a code parameter
213-
const url = window.location.href;
150+
// After requesting GitHub access, GitHub redirects back to the app with a
151+
// code parameter appended to the redirect URI (which carries mode=private|public).
152+
const url = new URL(window.location.href);
153+
const code = url.searchParams.get("code");
154+
if (code === null) {
155+
return;
156+
}
214157

215-
// If GitHub API returns the code parameter
216-
if (url.includes("code=")) {
217-
const tempPrivateAccess = url.includes("private");
218-
const newUrl = url.split("code=", 2) as [string, string];
219-
const redirect = `${url.split(HOST)[0] as string}${HOST}/frontend`;
220-
window.history.pushState({}, "", redirect);
221-
setIsLoading(true);
222-
const userKey = uuidv4();
223-
const newUserId = await authenticate(
224-
newUrl[1],
225-
tempPrivateAccess,
226-
userKey,
227-
);
158+
const tempPrivateAccess = url.searchParams.get("mode") === "private";
159+
window.history.pushState({}, "", `${url.origin}/frontend`);
228160

161+
async function exchangeCode(code: string) {
162+
setIsLoading(true);
163+
try {
164+
const userKey = uuidv4();
165+
const newUserId = await authenticate(code, tempPrivateAccess, userKey);
229166
dispatch(login({ userId: newUserId, userKey }));
230-
167+
} catch (error) {
168+
console.error(error);
169+
} finally {
231170
setIsLoading(false);
232171
}
233172
}
234173

235-
void redirectCode();
174+
void exchangeCode(code);
236175
}, [dispatch]);
237176

238177
if (isLoading) {
@@ -283,13 +222,7 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
283222
</p>
284223
</div>
285224
) : (
286-
[
287-
"",
288-
"You will be able to customize your card in future steps.",
289-
"",
290-
"",
291-
"Display the finished card on GitHub, Twitter/X, LinkedIn, or anywhere else!",
292-
][stage]
225+
STAGE_LABELS[stage].description
293226
)}
294227
</div>
295228
</div>
@@ -309,42 +242,8 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
309242
{stage === 2 && (
310243
<CustomizeStage
311244
selectedCard={selectedCard}
312-
selectedStatsRank={selectedStatsRank}
313-
setSelectedStatsRank={setSelectedStatsRank}
314-
selectedLanguagesLayout={selectedLanguagesLayout}
315-
setSelectedLanguagesLayout={setSelectedLanguagesLayout}
316-
selectedWakatimeLayout={selectedWakatimeLayout}
317-
setSelectedWakatimeLayout={setSelectedWakatimeLayout}
318-
showTitle={showTitle}
319-
setShowTitle={setShowTitle}
320-
showOwner={showOwner}
321-
setShowOwner={setShowOwner}
322-
descriptionLines={descriptionLines}
323-
setDescriptionLines={setDescriptionLines}
324-
customTitle={customTitle}
325-
setCustomTitle={setCustomTitle}
326-
langsCount={langsCount}
327-
setLangsCount={setLangsCount}
328-
hideValues={hideValues}
329-
setHideValues={setHideValues}
330-
showAllStats={showAllStats}
331-
setShowAllStats={setShowAllStats}
332-
showIcons={showIcons}
333-
setShowIcons={setShowIcons}
334-
includeAllCommits={includeAllCommits}
335-
setIncludeAllCommits={setIncludeAllCommits}
336-
enableAnimations={enableAnimations}
337-
setEnableAnimations={setEnableAnimations}
338-
selectedUserId={selectedUserId}
339-
setSelectedUserId={setSelectedUserId}
340-
repo={repo}
341-
setRepo={setRepo}
342-
gist={gist}
343-
setGist={setGist}
344-
wakatimeUser={wakatimeUser}
345-
setWakatimeUser={setWakatimeUser}
346-
usePercent={usePercent}
347-
setUsePercent={setUsePercent}
245+
options={cardOptions}
246+
onOptionChange={setCardOption}
348247
card={customizeCardBuilder}
349248
setStage={setStage}
350249
/>

0 commit comments

Comments
 (0)