Skip to content

Commit c147efe

Browse files
committed
brrr
1 parent 3789970 commit c147efe

8 files changed

Lines changed: 158 additions & 78 deletions

File tree

frontend/__tests__/components/common/AsyncContent.spec.tsx

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,39 @@ describe("AsyncContent", () => {
159159
retry: 0,
160160
}));
161161

162+
if (options?.alwaysShowContent) {
163+
return (
164+
<AsyncContent
165+
query={myQuery}
166+
{...(options as Props<string>)}
167+
alwaysShowContent
168+
>
169+
{(data) => (
170+
<>
171+
static content
172+
<Show
173+
when={data() !== undefined}
174+
fallback={<div>no data</div>}
175+
>
176+
<div data-testid="content">{data()}</div>
177+
</Show>
178+
</>
179+
)}
180+
</AsyncContent>
181+
);
182+
}
183+
162184
return (
163-
<AsyncContent query={myQuery} {...(options as Props<string>)}>
164-
{(data: string | undefined) => (
185+
<AsyncContent
186+
query={myQuery}
187+
{...(options as Props<string>)}
188+
alwaysShowContent={false}
189+
>
190+
{(data) => (
165191
<>
166192
static content
167-
<Show when={data !== undefined} fallback={<div>no data</div>}>
168-
<div data-testid="content">{data}</div>
193+
<Show when={data() !== undefined} fallback={<div>no data</div>}>
194+
<div data-testid="content">{data()}</div>
169195
</Show>
170196
</>
171197
)}
@@ -347,24 +373,49 @@ describe("AsyncContent", () => {
347373
}));
348374

349375
type Q = { first: string | undefined; second: string | undefined };
376+
377+
if (options?.alwaysShowContent) {
378+
return (
379+
<AsyncContent
380+
queries={{ first: firstQuery, second: secondQuery }}
381+
{...(options as Props<Q>)}
382+
alwaysShowContent
383+
>
384+
{(results) => (
385+
<>
386+
<Show
387+
when={
388+
results()?.first !== undefined &&
389+
results()?.second !== undefined
390+
}
391+
fallback={<div>no data</div>}
392+
>
393+
<div data-testid="first">{results()?.first}</div>
394+
<div data-testid="second">{results()?.second}</div>
395+
</Show>
396+
</>
397+
)}
398+
</AsyncContent>
399+
);
400+
}
401+
350402
return (
351403
<AsyncContent
352404
queries={{ first: firstQuery, second: secondQuery }}
353405
{...(options as Props<Q>)}
406+
alwaysShowContent={false}
354407
>
355-
{(results: {
356-
first: string | undefined;
357-
second: string | undefined;
358-
}) => (
408+
{(results) => (
359409
<>
360410
<Show
361411
when={
362-
results.first !== undefined && results.second !== undefined
412+
results().first !== undefined &&
413+
results().second !== undefined
363414
}
364415
fallback={<div>no data</div>}
365416
>
366-
<div data-testid="first">{results.first}</div>
367-
<div data-testid="second">{results.second}</div>
417+
<div data-testid="first">{results().first}</div>
418+
<div data-testid="second">{results().second}</div>
368419
</Show>
369420
</>
370421
)}

frontend/src/ts/components/common/AsyncContent.tsx

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,15 @@ type SingleCollectionProps<T> = {
5656

5757
type DeferredChildren<T extends QueryMapping> = {
5858
alwaysShowContent?: false;
59-
children: (data: { [K in keyof T]: T[K] }) => JSXElement;
59+
children: (data: Accessor<{ [K in keyof T]: T[K] }>) => JSXElement;
6060
};
6161

6262
type EagerChildren<T extends QueryMapping> = {
6363
alwaysShowContent: true;
6464
showLoader?: true;
65-
children: (data: { [K in keyof T]: T[K] | undefined }) => JSXElement;
65+
children: (
66+
data: Accessor<{ [K in keyof T]: T[K] } | undefined>,
67+
) => JSXElement;
6668
};
6769

6870
export type Props<T extends QueryMapping> = BaseProps &
@@ -136,6 +138,18 @@ export default function AsyncContent<T extends QueryMapping>(
136138
.find((s) => s.isError())
137139
?.error?.();
138140

141+
// Keep the last resolved value so deferred children stay mounted during
142+
// transient loading states (e.g. navigating away and back).
143+
const lastResolvedValue = createMemo<T | undefined>((prev) => {
144+
const current = value();
145+
return allResolved(current) ? current : prev;
146+
});
147+
148+
const hasResolved = createMemo<boolean>(
149+
(prev) => prev || lastResolvedValue() !== undefined,
150+
false,
151+
);
152+
139153
const loader = (): JSXElement =>
140154
props.loader ?? <LoadingCircle class="p-4 text-center text-2xl" />;
141155

@@ -144,24 +158,31 @@ export default function AsyncContent<T extends QueryMapping>(
144158
<div class={props.errorClass}>{handleError(err)}</div>
145159
);
146160

161+
// Only show loader on initial load, not on refetches
162+
const showLoader = (): boolean =>
163+
isLoading() &&
164+
!props.alwaysShowContent &&
165+
lastResolvedValue() === undefined;
166+
147167
return (
148168
<ErrorBoundary fallback={props.ignoreError ? undefined : errorText}>
149169
<Switch
150170
fallback={
151171
<>
152-
<Show when={isLoading() && !props.alwaysShowContent}>
153-
{loader()}
154-
</Show>
155-
172+
<Show when={showLoader()}>{loader()}</Show>
156173
<Show
157174
when={props.alwaysShowContent === true}
158175
fallback={
159-
<Show when={allResolved(value())}>
160-
{props.children(value())}
176+
<Show when={hasResolved()}>
177+
{(_) =>
178+
props.children(
179+
lastResolvedValue as Accessor<{ [K in keyof T]: T[K] }>,
180+
)
181+
}
161182
</Show>
162183
}
163184
>
164-
{props.children(value())}
185+
{props.children(value)}
165186
</Show>
166187
</>
167188
}
@@ -170,7 +191,7 @@ export default function AsyncContent<T extends QueryMapping>(
170191
{errorText(firstError())}
171192
</Match>
172193

173-
<Match when={isLoading() && !props.alwaysShowContent}>{loader()}</Match>
194+
<Match when={showLoader()}>{loader()}</Match>
174195
</Switch>
175196
</ErrorBoundary>
176197
);

frontend/src/ts/components/modals/VersionHistoryModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function VersionHistoryModal(): JSXElement {
4040
{(data) => (
4141
<>
4242
<div class="releases">
43-
<For each={data.pages.flatMap((it) => it.releases)}>
43+
<For each={data().pages.flatMap((it) => it.releases)}>
4444
{(release) => <ReleaseItem {...release} />}
4545
</For>
4646
</div>

frontend/src/ts/components/pages/AboutPage.tsx

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -67,27 +67,29 @@ export function AboutPage(): JSXElement {
6767
query={typingStats}
6868
errorMessage="Failed to get global typing stats"
6969
>
70-
{(data) => (
71-
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
72-
<For
73-
each={
74-
[
75-
["total tests started", data?.testsStarted],
76-
["total typing time", data?.timeTyping],
77-
["total tests completed", data?.testsCompleted],
78-
] as const
79-
}
80-
>
81-
{([title, data]) => (
82-
<div class="text-center">
83-
<div class="text-sub">{title}</div>
84-
<div class="text-5xl">{data?.text ?? "-"}</div>
85-
<div class="text-xl">{data?.subText ?? "-"}</div>
86-
</div>
87-
)}
88-
</For>
89-
</div>
90-
)}
70+
{(data) => {
71+
return (
72+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
73+
<For
74+
each={
75+
[
76+
["total tests started", () => data()?.testsStarted],
77+
["total typing time", () => data()?.timeTyping],
78+
["total tests completed", () => data()?.testsCompleted],
79+
] as const
80+
}
81+
>
82+
{([title, stat]) => (
83+
<div class="text-center">
84+
<div class="text-sub">{title}</div>
85+
<div class="text-5xl">{stat()?.text ?? "-"}</div>
86+
<div class="text-xl">{stat()?.subText ?? "-"}</div>
87+
</div>
88+
)}
89+
</For>
90+
</div>
91+
);
92+
}}
9193
</AsyncContent>
9294
</section>
9395
<section class="h-48 w-full">
@@ -101,12 +103,12 @@ export function AboutPage(): JSXElement {
101103
<ChartJs
102104
type="bar"
103105
data={{
104-
labels: data?.labels ?? [],
106+
labels: data()?.labels ?? [],
105107
datasets: [
106108
{
107109
yAxisID: "count",
108110
label: "Users",
109-
data: data?.data ?? [],
111+
data: data()?.data ?? [],
110112
minBarLength: 2,
111113
backgroundColor: getTheme().main,
112114
borderColor: getTheme().main,
@@ -169,7 +171,7 @@ export function AboutPage(): JSXElement {
169171
/>
170172
<div class="text-right text-xs text-sub">
171173
distribution of time 60 leaderboard results (wpm) <br />
172-
{numberOfHistogramRecords(data?.data)} total results
174+
{numberOfHistogramRecords(data()?.data)} total results
173175
</div>
174176
</>
175177
)}
@@ -413,7 +415,7 @@ export function AboutPage(): JSXElement {
413415
"grid-template-columns": "repeat(auto-fill, minmax(13em, 1fr))",
414416
}}
415417
>
416-
<For each={data}>{(name) => <div>{name}</div>}</For>
418+
<For each={data()}>{(name) => <div>{name}</div>}</For>
417419
</div>
418420
)}
419421
</AsyncContent>
@@ -436,7 +438,7 @@ export function AboutPage(): JSXElement {
436438
"grid-template-columns": "repeat(auto-fill, minmax(13em, 1fr))",
437439
}}
438440
>
439-
<For each={data}>{(name) => <div>{name}</div>}</For>
441+
<For each={data()}>{(name) => <div>{name}</div>}</For>
440442
</div>
441443
)}
442444
</AsyncContent>

frontend/src/ts/components/pages/leaderboard/LeaderboardPage.tsx

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,8 @@ export function LeaderboardPage(): JSXElement {
175175
<Sidebar
176176
selection={getSelection}
177177
onSelect={onSelectionChange}
178-
validModeRules={config.dailyLeaderboards.validModeRules ?? []}
179-
connectionsEnabled={config.connections.enabled}
178+
validModeRules={config().dailyLeaderboards.validModeRules ?? []}
179+
connectionsEnabled={config().connections.enabled}
180180
/>
181181
)}
182182
</AsyncContent>
@@ -203,27 +203,33 @@ export function LeaderboardPage(): JSXElement {
203203
alwaysShowContent
204204
errorClass="rounded bg-sub-alt p-4"
205205
>
206-
{({ data, rank, config }) => (
207-
<UserRank
208-
type={getSelection().type === "weekly" ? "xp" : "speed"}
209-
data={rank}
210-
friendsOnly={getSelection().friendsOnly}
211-
total={data?.count}
212-
minWpm={
213-
data && "minWpm" in data
214-
? (data.minWpm as number)
215-
: undefined
216-
}
217-
memoryDifference={getLbMemoryDifference(
218-
getSelection(),
219-
rank?.rank,
220-
)}
221-
isLbOptOut={getSnapshot()?.lbOptOut ?? false}
222-
isBanned={getSnapshot()?.banned ?? false}
223-
minTimeTyping={config?.leaderboards.minTimeTyping ?? 0}
224-
userTimeTyping={getSnapshot()?.typingStats.timeTyping ?? 0}
225-
/>
226-
)}
206+
{(queries) => {
207+
const data = () => queries()?.data;
208+
const rank = () => queries()?.rank;
209+
const config = () => queries()?.config;
210+
return (
211+
<UserRank
212+
type={getSelection().type === "weekly" ? "xp" : "speed"}
213+
data={rank()}
214+
friendsOnly={getSelection().friendsOnly}
215+
total={data()?.count}
216+
minWpm={(() => {
217+
const d = data();
218+
return d && "minWpm" in d
219+
? (d.minWpm as number)
220+
: undefined;
221+
})()}
222+
memoryDifference={getLbMemoryDifference(
223+
getSelection(),
224+
rank()?.rank,
225+
)}
226+
isLbOptOut={getSnapshot()?.lbOptOut ?? false}
227+
isBanned={getSnapshot()?.banned ?? false}
228+
minTimeTyping={config()?.leaderboards.minTimeTyping ?? 0}
229+
userTimeTyping={getSnapshot()?.typingStats.timeTyping ?? 0}
230+
/>
231+
);
232+
}}
227233
</AsyncContent>
228234
</Show>
229235

@@ -249,7 +255,7 @@ export function LeaderboardPage(): JSXElement {
249255
dataQuery.isFetching ||
250256
dataQuery.isRefetching
251257
}
252-
lastPage={Math.ceil((data?.count ?? 0) / pageSize)}
258+
lastPage={Math.ceil((data()?.count ?? 0) / pageSize)}
253259
userPage={userPage()}
254260
currentPage={getPage()}
255261
onPageChange={setPage}
@@ -261,7 +267,7 @@ export function LeaderboardPage(): JSXElement {
261267
<div>
262268
<Table
263269
type={getSelection().type === "weekly" ? "xp" : "speed"}
264-
entries={data?.entries ?? []}
270+
entries={data()?.entries ?? []}
265271
friendsOnly={getSelection().friendsOnly}
266272
scrollToUser={scrollToUser}
267273
onScrolledToUser={() => setScrollToUser(false)}
@@ -270,7 +276,7 @@ export function LeaderboardPage(): JSXElement {
270276

271277
<div class="mt-4 grid grid-cols-1 items-center justify-between text-sm sm:text-base">
272278
<Navigation
273-
lastPage={Math.ceil((data?.count ?? 0) / pageSize)}
279+
lastPage={Math.ceil((data()?.count ?? 0) / pageSize)}
274280
currentPage={getPage()}
275281
onPageChange={setPage}
276282
onScrollToUser={setScrollToUser}

frontend/src/ts/components/pages/profile/ProfilePage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function ProfilePage(): JSXElement {
2121
<Show when={isOpen}>
2222
<div class="flex h-full items-center justify-center text-lg">
2323
<AsyncContent query={profileQuery} ignoreError={true}>
24-
{(profile) => <UserProfile profile={profile} />}
24+
{(profile) => <UserProfile profile={profile()} />}
2525
</AsyncContent>
2626
<Show when={profileQuery.isError}>
2727
<div class="flex items-baseline gap-2 text-error">

frontend/src/ts/components/popups/alerts/Inbox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export function Inbox(): JSXElement {
149149
</Show>
150150

151151
<For
152-
each={inbox}
152+
each={inbox()}
153153
fallback={<div class="place-self-center">Nothing to show</div>}
154154
>
155155
{(entry) => <Entry entry={entry} mutate={mutate} />}

0 commit comments

Comments
 (0)