mirror of https://github.com/usememos/memos.git
refactor: standardize loading indicators by using Spinner for route fallbacks and specializing Skeleton for memo lists
This commit is contained in:
parent
61e94e8b08
commit
d55af9b527
|
|
@ -1,7 +1,7 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getInitialTheme, loadTheme } from "@/utils/theme";
|
||||
import { loadLocale } from "@/utils/i18n";
|
||||
import { getInitialTheme, loadTheme } from "@/utils/theme";
|
||||
import LocaleSelect from "./LocaleSelect";
|
||||
import ThemeSelect from "./ThemeSelect";
|
||||
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ const PagedMemoList = (props: Props) => {
|
|||
<div className="flex flex-col justify-start items-start w-full max-w-full">
|
||||
{/* Show skeleton loader during initial load */}
|
||||
{isLoading ? (
|
||||
<Skeleton type="memo" showCreator={props.showCreator} count={4} />
|
||||
<Skeleton showCreator={props.showCreator} count={4} />
|
||||
) : (
|
||||
<>
|
||||
<MasonryView
|
||||
|
|
@ -166,8 +166,8 @@ const PagedMemoList = (props: Props) => {
|
|||
listMode={layout === "LIST"}
|
||||
/>
|
||||
|
||||
{/* Loading indicator for pagination - use skeleton for content consistency */}
|
||||
{isFetchingNextPage && <Skeleton type="pagination" showCreator={props.showCreator} count={2} />}
|
||||
{/* Loading indicator for pagination */}
|
||||
{isFetchingNextPage && <Skeleton showCreator={props.showCreator} count={2} />}
|
||||
|
||||
{/* Empty state or back-to-top button */}
|
||||
{!isFetchingNextPage && (
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
type?: "route" | "memo" | "pagination";
|
||||
showCreator?: boolean;
|
||||
count?: number;
|
||||
showEditor?: boolean;
|
||||
}
|
||||
|
||||
// Memo card skeleton component
|
||||
// Memo card skeleton component for list loading states
|
||||
const MemoCardSkeleton = ({ showCreator = false, index = 0 }: { showCreator?: boolean; index?: number }) => (
|
||||
<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">
|
||||
{/* Header section */}
|
||||
|
|
@ -43,36 +41,17 @@ const MemoCardSkeleton = ({ showCreator = false, index = 0 }: { showCreator?: bo
|
|||
</div>
|
||||
);
|
||||
|
||||
const Skeleton = ({ type = "route", showCreator = false, count = 4, showEditor = true }: Props) => {
|
||||
// Pagination type: simpler, just memos
|
||||
if (type === "pagination") {
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-center items-center my-4">
|
||||
<div className="w-full max-w-2xl mx-auto">
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<MemoCardSkeleton key={index} showCreator={showCreator} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Route or memo type: with optional wrapper
|
||||
return (
|
||||
<div className="w-full max-w-2xl mx-auto">
|
||||
{/* Editor skeleton - only for route type */}
|
||||
{type === "route" && showEditor && (
|
||||
<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">
|
||||
<div className="w-full h-12 bg-muted rounded" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Memo skeletons */}
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<MemoCardSkeleton key={index} showCreator={showCreator} index={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
/**
|
||||
* Skeleton loading state for memo lists.
|
||||
* Use this for initial memo list loading and pagination.
|
||||
* For generic page/route loading, use Spinner instead.
|
||||
*/
|
||||
const Skeleton = ({ showCreator = false, count = 4 }: Props) => (
|
||||
<div className="w-full max-w-2xl mx-auto">
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<MemoCardSkeleton key={index} showCreator={showCreator} index={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Skeleton;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Suspense, useEffect, useMemo } from "react";
|
|||
import { Outlet, useLocation, useSearchParams } from "react-router-dom";
|
||||
import usePrevious from "react-use/lib/usePrevious";
|
||||
import Navigation from "@/components/Navigation";
|
||||
import Skeleton from "@/components/Skeleton";
|
||||
import Spinner from "@/components/Spinner";
|
||||
import { useInstance } from "@/contexts/InstanceContext";
|
||||
import { useMemoFilterContext } from "@/contexts/MemoFilterContext";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
|
|
@ -47,7 +47,13 @@ const RootLayout = () => {
|
|||
</div>
|
||||
)}
|
||||
<main className="w-full h-auto grow shrink flex flex-col justify-start items-center">
|
||||
<Suspense fallback={<Skeleton type="route" />}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="w-full h-64 flex items-center justify-center">
|
||||
<Spinner size="lg" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { RouterProvider } from "react-router-dom";
|
|||
import "./i18n";
|
||||
import "./index.css";
|
||||
import { ErrorBoundary } from "@/components/ErrorBoundary";
|
||||
import Spinner from "@/components/Spinner";
|
||||
import { AuthProvider, useAuth } from "@/contexts/AuthContext";
|
||||
import { InstanceProvider, useInstance } from "@/contexts/InstanceContext";
|
||||
import { ViewProvider } from "@/contexts/ViewContext";
|
||||
|
|
@ -39,7 +40,11 @@ function AppInitializer({ children }: { children: React.ReactNode }) {
|
|||
}, [initAuth, initInstance]);
|
||||
|
||||
if (!authInitialized || !instanceInitialized) {
|
||||
return undefined;
|
||||
return (
|
||||
<div className="w-full h-screen flex items-center justify-center">
|
||||
<Spinner size="lg" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import type { ComponentType } from "react";
|
||||
import { lazy, Suspense } from "react";
|
||||
import { createBrowserRouter } from "react-router-dom";
|
||||
import App from "@/App";
|
||||
import Skeleton from "@/components/Skeleton";
|
||||
import Spinner from "@/components/Spinner";
|
||||
import MainLayout from "@/layouts/MainLayout";
|
||||
import RootLayout from "@/layouts/RootLayout";
|
||||
import Home from "@/pages/Home";
|
||||
|
|
@ -27,6 +28,19 @@ import { ROUTES } from "./routes";
|
|||
export const Routes = ROUTES;
|
||||
export { ROUTES };
|
||||
|
||||
// Helper component to reduce Suspense boilerplate for lazy routes
|
||||
const LazyRoute = ({ component: Component }: { component: ComponentType }) => (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="w-full h-64 flex items-center justify-center">
|
||||
<Spinner size="lg" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Component />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
|
|
@ -35,38 +49,10 @@ const router = createBrowserRouter([
|
|||
{
|
||||
path: Routes.AUTH,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
element: (
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<SignIn />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "admin",
|
||||
element: (
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<AdminSignIn />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "signup",
|
||||
element: (
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<SignUp />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "callback",
|
||||
element: (
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<AuthCallback />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{ path: "", element: <LazyRoute component={SignIn} /> },
|
||||
{ path: "admin", element: <LazyRoute component={AdminSignIn} /> },
|
||||
{ path: "signup", element: <LazyRoute component={SignUp} /> },
|
||||
{ path: "callback", element: <LazyRoute component={AuthCallback} /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -76,101 +62,21 @@ const router = createBrowserRouter([
|
|||
{
|
||||
element: <MainLayout />,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
element: <Home />,
|
||||
},
|
||||
{
|
||||
path: Routes.EXPLORE,
|
||||
element: (
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<Explore />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: Routes.ARCHIVED,
|
||||
element: (
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<Archived />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "u/:username",
|
||||
element: (
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<UserProfile />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{ path: "", element: <Home /> },
|
||||
{ path: Routes.EXPLORE, element: <LazyRoute component={Explore} /> },
|
||||
{ path: Routes.ARCHIVED, element: <LazyRoute component={Archived} /> },
|
||||
{ path: "u/:username", element: <LazyRoute component={UserProfile} /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: Routes.ATTACHMENTS,
|
||||
element: (
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<Attachments />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: Routes.INBOX,
|
||||
element: (
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<Inboxes />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: Routes.SETTING,
|
||||
element: (
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<Setting />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "memos/:uid",
|
||||
element: (
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<MemoDetail />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
// Redirect old path to new path.
|
||||
{
|
||||
path: "m/:uid",
|
||||
element: (
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<MemoDetailRedirect />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "403",
|
||||
element: (
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<PermissionDenied />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "404",
|
||||
element: (
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<NotFound />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "*",
|
||||
element: (
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<NotFound />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{ path: Routes.ATTACHMENTS, element: <LazyRoute component={Attachments} /> },
|
||||
{ path: Routes.INBOX, element: <LazyRoute component={Inboxes} /> },
|
||||
{ path: Routes.SETTING, element: <LazyRoute component={Setting} /> },
|
||||
{ path: "memos/:uid", element: <LazyRoute component={MemoDetail} /> },
|
||||
// Redirect old path to new path
|
||||
{ path: "m/:uid", element: <LazyRoute component={MemoDetailRedirect} /> },
|
||||
{ path: "403", element: <LazyRoute component={PermissionDenied} /> },
|
||||
{ path: "404", element: <LazyRoute component={NotFound} /> },
|
||||
{ path: "*", element: <LazyRoute component={NotFound} /> },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in New Issue