Skip to content

Commit 115d1ba

Browse files
committed
refactor: replace MemoSkeleton with a new Skeleton component for improved loading states
1 parent 792d58b commit 115d1ba

File tree

10 files changed

+132
-112
lines changed

10 files changed

+132
-112
lines changed

web/src/components/MemoEditor/index.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,6 @@ const MemoEditorImpl: React.FC<MemoEditorProps> = ({
105105

106106
// Notify parent component of successful save
107107
onConfirm?.(result.memoName);
108-
109-
toast.success("Saved successfully");
110108
} catch (error) {
111109
handleError(error, toast.error, {
112110
context: "Failed to save memo",

web/src/components/MemoSkeleton.tsx

Lines changed: 0 additions & 56 deletions
This file was deleted.

web/src/components/PagedMemoList/PagedMemoList.tsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type { MemoRenderContext } from "../MasonryView";
1515
import MasonryView from "../MasonryView";
1616
import MemoEditor from "../MemoEditor";
1717
import MemoFilters from "../MemoFilters";
18-
import MemoSkeleton from "../MemoSkeleton";
18+
import Skeleton from "../Skeleton";
1919

2020
interface Props {
2121
renderer: (memo: Memo, context?: MemoRenderContext) => JSX.Element;
@@ -143,9 +143,7 @@ const PagedMemoList = (props: Props) => {
143143
<div className="flex flex-col justify-start items-start w-full max-w-full">
144144
{/* Show skeleton loader during initial load */}
145145
{isLoading ? (
146-
<div className="w-full flex flex-col justify-start items-center">
147-
<MemoSkeleton showCreator={props.showCreator} count={4} />
148-
</div>
146+
<Skeleton type="memo" showCreator={props.showCreator} count={4} />
149147
) : (
150148
<>
151149
<MasonryView
@@ -162,12 +160,8 @@ const PagedMemoList = (props: Props) => {
162160
listMode={layout === "LIST"}
163161
/>
164162

165-
{/* Loading indicator for pagination */}
166-
{isFetchingNextPage && (
167-
<div className="w-full flex flex-row justify-center items-center my-4">
168-
<LoaderIcon className="animate-spin text-muted-foreground" />
169-
</div>
170-
)}
163+
{/* Loading indicator for pagination - use skeleton for content consistency */}
164+
{isFetchingNextPage && <Skeleton type="pagination" showCreator={props.showCreator} count={2} />}
171165

172166
{/* Empty state or back-to-top button */}
173167
{!isFetchingNextPage && (

web/src/components/Skeleton.tsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { cn } from "@/lib/utils";
2+
3+
interface Props {
4+
type?: "route" | "memo" | "pagination";
5+
showCreator?: boolean;
6+
count?: number;
7+
showEditor?: boolean;
8+
}
9+
10+
// Memo card skeleton component
11+
const MemoCardSkeleton = ({ showCreator = false, index = 0 }: { showCreator?: boolean; index?: number }) => (
12+
<div className="relative flex flex-col justify-start items-start bg-card w-full px-4 py-3 mb-2 gap-2 rounded-lg border border-border animate-pulse">
13+
{/* Header section */}
14+
<div className="w-full flex flex-row justify-between items-center gap-2">
15+
<div className="w-auto max-w-[calc(100%-8rem)] grow flex flex-row justify-start items-center">
16+
{showCreator ? (
17+
<div className="w-full flex flex-row justify-start items-center gap-2">
18+
<div className="w-8 h-8 rounded-full bg-muted shrink-0" />
19+
<div className="w-full flex flex-col justify-center items-start gap-1">
20+
<div className="h-4 w-24 bg-muted rounded" />
21+
<div className="h-3 w-16 bg-muted rounded" />
22+
</div>
23+
</div>
24+
) : (
25+
<div className="h-4 w-32 bg-muted rounded" />
26+
)}
27+
</div>
28+
{/* Action buttons skeleton */}
29+
<div className="flex flex-row gap-2">
30+
<div className="w-4 h-4 bg-muted rounded" />
31+
<div className="w-4 h-4 bg-muted rounded" />
32+
</div>
33+
</div>
34+
35+
{/* Content section */}
36+
<div className="w-full flex flex-col gap-2">
37+
<div className="space-y-2">
38+
<div className={cn("h-4 bg-muted rounded", index % 3 === 0 ? "w-full" : index % 3 === 1 ? "w-4/5" : "w-5/6")} />
39+
<div className={cn("h-4 bg-muted rounded", index % 2 === 0 ? "w-3/4" : "w-4/5")} />
40+
{index % 2 === 0 && <div className="h-4 w-2/3 bg-muted rounded" />}
41+
</div>
42+
</div>
43+
</div>
44+
);
45+
46+
const Skeleton = ({ type = "route", showCreator = false, count = 4, showEditor = true }: Props) => {
47+
// Pagination type: simpler, just memos
48+
if (type === "pagination") {
49+
return (
50+
<div className="w-full flex flex-col justify-center items-center my-4">
51+
<div className="w-full max-w-2xl mx-auto">
52+
{Array.from({ length: count }).map((_, index) => (
53+
<MemoCardSkeleton key={index} showCreator={showCreator} index={index} />
54+
))}
55+
</div>
56+
</div>
57+
);
58+
}
59+
60+
// Route or memo type: with optional wrapper
61+
return (
62+
<div className="w-full max-w-full px-4 py-6">
63+
<div className="w-full max-w-2xl mx-auto">
64+
{/* Editor skeleton - only for route type */}
65+
{type === "route" && showEditor && (
66+
<div className="relative flex flex-col justify-start items-start bg-card w-full px-4 py-3 mb-4 gap-2 rounded-lg border border-border animate-pulse">
67+
<div className="w-full h-12 bg-muted rounded" />
68+
</div>
69+
)}
70+
71+
{/* Memo skeletons */}
72+
{Array.from({ length: count }).map((_, index) => (
73+
<MemoCardSkeleton key={index} showCreator={showCreator} index={index} />
74+
))}
75+
</div>
76+
</div>
77+
);
78+
};
79+
80+
export default Skeleton;

web/src/components/Spinner.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { LoaderIcon } from "lucide-react";
2+
import { cn } from "@/lib/utils";
3+
4+
interface Props {
5+
className?: string;
6+
size?: "sm" | "md" | "lg";
7+
}
8+
9+
const Spinner = ({ className, size = "md" }: Props) => {
10+
const sizeClasses = {
11+
sm: "w-4 h-4",
12+
md: "w-6 h-6",
13+
lg: "w-8 h-8",
14+
};
15+
16+
return <LoaderIcon className={cn("animate-spin", sizeClasses[size], className)} />;
17+
};
18+
19+
export default Spinner;

web/src/layouts/RootLayout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import { Suspense, useEffect, useMemo } from "react";
22
import { Outlet, useLocation, useSearchParams } from "react-router-dom";
33
import usePrevious from "react-use/lib/usePrevious";
44
import Navigation from "@/components/Navigation";
5+
import Skeleton from "@/components/Skeleton";
56
import { useInstance } from "@/contexts/InstanceContext";
67
import { useMemoFilterContext } from "@/contexts/MemoFilterContext";
78
import useCurrentUser from "@/hooks/useCurrentUser";
89
import useMediaQuery from "@/hooks/useMediaQuery";
910
import { cn } from "@/lib/utils";
10-
import Loading from "@/pages/Loading";
1111
import { redirectOnAuthFailure } from "@/utils/auth-redirect";
1212

1313
const RootLayout = () => {
@@ -47,7 +47,7 @@ const RootLayout = () => {
4747
</div>
4848
)}
4949
<main className="w-full h-auto grow shrink flex flex-col justify-start items-center">
50-
<Suspense fallback={<Loading />}>
50+
<Suspense fallback={<Skeleton type="route" />}>
5151
<Outlet />
5252
</Suspense>
5353
</main>

web/src/main.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import "@github/relative-time-element";
22
import { QueryClientProvider } from "@tanstack/react-query";
33
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
4-
import React, { useEffect, useState } from "react";
4+
import React, { useEffect, useRef } from "react";
55
import { createRoot } from "react-dom/client";
66
import { Toaster } from "react-hot-toast";
77
import { RouterProvider } from "react-router-dom";
@@ -12,7 +12,6 @@ import { AuthProvider, useAuth } from "@/contexts/AuthContext";
1212
import { InstanceProvider, useInstance } from "@/contexts/InstanceContext";
1313
import { ViewProvider } from "@/contexts/ViewContext";
1414
import { queryClient } from "@/lib/query-client";
15-
import Loading from "@/pages/Loading";
1615
import router from "./router";
1716
import { applyLocaleEarly } from "./utils/i18n";
1817
import { applyThemeEarly } from "./utils/theme";
@@ -26,22 +25,21 @@ applyLocaleEarly();
2625
function AppInitializer({ children }: { children: React.ReactNode }) {
2726
const { isInitialized: authInitialized, initialize: initAuth } = useAuth();
2827
const { isInitialized: instanceInitialized, initialize: initInstance } = useInstance();
29-
const [initStarted, setInitStarted] = useState(false);
28+
const initStartedRef = useRef(false);
3029

31-
// Initialize on mount
30+
// Initialize on mount - run in parallel for better performance
3231
useEffect(() => {
33-
if (initStarted) return;
34-
setInitStarted(true);
32+
if (initStartedRef.current) return;
33+
initStartedRef.current = true;
3534

3635
const init = async () => {
37-
await initInstance();
38-
await initAuth();
36+
await Promise.all([initInstance(), initAuth()]);
3937
};
4038
init();
41-
}, [initAuth, initInstance, initStarted]);
39+
}, [initAuth, initInstance]);
4240

4341
if (!authInitialized || !instanceInitialized) {
44-
return <Loading />;
42+
return undefined;
4543
}
4644

4745
return <>{children}</>;

web/src/pages/AuthCallback.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { timestampDate } from "@bufbuild/protobuf/wkt";
2-
import { LoaderIcon } from "lucide-react";
32
import { useEffect, useState } from "react";
43
import { useSearchParams } from "react-router-dom";
54
import { setAccessToken } from "@/auth-state";
5+
import Spinner from "@/components/Spinner";
66
import { authServiceClient } from "@/connect";
77
import { useAuth } from "@/contexts/AuthContext";
88
import { absolutifyLink } from "@/helpers/utils";
@@ -113,7 +113,7 @@ const AuthCallback = () => {
113113
return (
114114
<div className="p-4 py-24 w-full h-full flex justify-center items-center">
115115
{state.loading ? (
116-
<LoaderIcon className="animate-spin text-foreground" />
116+
<Spinner size="lg" />
117117
) : (
118118
<div className="max-w-lg font-mono whitespace-pre-wrap opacity-80">{state.errorMessage}</div>
119119
)}

web/src/pages/Loading.tsx

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)