mirror of https://github.com/usememos/memos.git
refactor: replace MemoSkeleton with a new Skeleton component for improved loading states
This commit is contained in:
parent
792d58b74d
commit
115d1bacd7
|
|
@ -105,8 +105,6 @@ const MemoEditorImpl: React.FC<MemoEditorProps> = ({
|
|||
|
||||
// Notify parent component of successful save
|
||||
onConfirm?.(result.memoName);
|
||||
|
||||
toast.success("Saved successfully");
|
||||
} catch (error) {
|
||||
handleError(error, toast.error, {
|
||||
context: "Failed to save memo",
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
showCreator?: boolean;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
const MemoSkeleton = ({ showCreator = false, count = 6 }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="relative flex flex-col justify-start items-start bg-card w-full max-w-2xl mx-auto px-4 py-3 mb-2 gap-2 rounded-lg border border-border animate-pulse"
|
||||
>
|
||||
{/* Header section */}
|
||||
<div className="w-full flex flex-row justify-between items-center gap-2">
|
||||
<div className="w-auto max-w-[calc(100%-8rem)] grow flex flex-row justify-start items-center">
|
||||
{showCreator ? (
|
||||
<div className="w-full flex flex-row justify-start items-center gap-2">
|
||||
{/* Avatar skeleton */}
|
||||
<div className="w-8 h-8 rounded-full bg-muted shrink-0" />
|
||||
<div className="w-full flex flex-col justify-center items-start gap-1">
|
||||
{/* Creator name skeleton */}
|
||||
<div className="h-4 w-24 bg-muted rounded" />
|
||||
{/* Timestamp skeleton */}
|
||||
<div className="h-3 w-16 bg-muted rounded" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-4 w-32 bg-muted rounded" />
|
||||
)}
|
||||
</div>
|
||||
{/* Action buttons skeleton */}
|
||||
<div className="flex flex-row gap-2">
|
||||
<div className="w-4 h-4 bg-muted rounded" />
|
||||
<div className="w-4 h-4 bg-muted rounded" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content section */}
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
{/* Text content skeleton - varied heights for realism */}
|
||||
<div className="space-y-2">
|
||||
<div className={cn("h-4 bg-muted rounded", index % 3 === 0 ? "w-full" : index % 3 === 1 ? "w-4/5" : "w-5/6")} />
|
||||
<div className={cn("h-4 bg-muted rounded", index % 2 === 0 ? "w-3/4" : "w-4/5")} />
|
||||
{index % 2 === 0 && <div className="h-4 w-2/3 bg-muted rounded" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemoSkeleton;
|
||||
|
|
@ -15,7 +15,7 @@ import type { MemoRenderContext } from "../MasonryView";
|
|||
import MasonryView from "../MasonryView";
|
||||
import MemoEditor from "../MemoEditor";
|
||||
import MemoFilters from "../MemoFilters";
|
||||
import MemoSkeleton from "../MemoSkeleton";
|
||||
import Skeleton from "../Skeleton";
|
||||
|
||||
interface Props {
|
||||
renderer: (memo: Memo, context?: MemoRenderContext) => JSX.Element;
|
||||
|
|
@ -143,9 +143,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 ? (
|
||||
<div className="w-full flex flex-col justify-start items-center">
|
||||
<MemoSkeleton showCreator={props.showCreator} count={4} />
|
||||
</div>
|
||||
<Skeleton type="memo" showCreator={props.showCreator} count={4} />
|
||||
) : (
|
||||
<>
|
||||
<MasonryView
|
||||
|
|
@ -162,12 +160,8 @@ const PagedMemoList = (props: Props) => {
|
|||
listMode={layout === "LIST"}
|
||||
/>
|
||||
|
||||
{/* Loading indicator for pagination */}
|
||||
{isFetchingNextPage && (
|
||||
<div className="w-full flex flex-row justify-center items-center my-4">
|
||||
<LoaderIcon className="animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
{/* Loading indicator for pagination - use skeleton for content consistency */}
|
||||
{isFetchingNextPage && <Skeleton type="pagination" showCreator={props.showCreator} count={2} />}
|
||||
|
||||
{/* Empty state or back-to-top button */}
|
||||
{!isFetchingNextPage && (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
type?: "route" | "memo" | "pagination";
|
||||
showCreator?: boolean;
|
||||
count?: number;
|
||||
showEditor?: boolean;
|
||||
}
|
||||
|
||||
// Memo card skeleton component
|
||||
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 */}
|
||||
<div className="w-full flex flex-row justify-between items-center gap-2">
|
||||
<div className="w-auto max-w-[calc(100%-8rem)] grow flex flex-row justify-start items-center">
|
||||
{showCreator ? (
|
||||
<div className="w-full flex flex-row justify-start items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-muted shrink-0" />
|
||||
<div className="w-full flex flex-col justify-center items-start gap-1">
|
||||
<div className="h-4 w-24 bg-muted rounded" />
|
||||
<div className="h-3 w-16 bg-muted rounded" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-4 w-32 bg-muted rounded" />
|
||||
)}
|
||||
</div>
|
||||
{/* Action buttons skeleton */}
|
||||
<div className="flex flex-row gap-2">
|
||||
<div className="w-4 h-4 bg-muted rounded" />
|
||||
<div className="w-4 h-4 bg-muted rounded" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content section */}
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<div className="space-y-2">
|
||||
<div className={cn("h-4 bg-muted rounded", index % 3 === 0 ? "w-full" : index % 3 === 1 ? "w-4/5" : "w-5/6")} />
|
||||
<div className={cn("h-4 bg-muted rounded", index % 2 === 0 ? "w-3/4" : "w-4/5")} />
|
||||
{index % 2 === 0 && <div className="h-4 w-2/3 bg-muted rounded" />}
|
||||
</div>
|
||||
</div>
|
||||
</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-full px-4 py-6">
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Skeleton;
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { LoaderIcon } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
const Spinner = ({ className, size = "md" }: Props) => {
|
||||
const sizeClasses = {
|
||||
sm: "w-4 h-4",
|
||||
md: "w-6 h-6",
|
||||
lg: "w-8 h-8",
|
||||
};
|
||||
|
||||
return <LoaderIcon className={cn("animate-spin", sizeClasses[size], className)} />;
|
||||
};
|
||||
|
||||
export default Spinner;
|
||||
|
|
@ -2,12 +2,12 @@ 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 { useInstance } from "@/contexts/InstanceContext";
|
||||
import { useMemoFilterContext } from "@/contexts/MemoFilterContext";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import useMediaQuery from "@/hooks/useMediaQuery";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Loading from "@/pages/Loading";
|
||||
import { redirectOnAuthFailure } from "@/utils/auth-redirect";
|
||||
|
||||
const RootLayout = () => {
|
||||
|
|
@ -47,7 +47,7 @@ const RootLayout = () => {
|
|||
</div>
|
||||
)}
|
||||
<main className="w-full h-auto grow shrink flex flex-col justify-start items-center">
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Suspense fallback={<Skeleton type="route" />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import "@github/relative-time-element";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
|
|
@ -12,7 +12,6 @@ import { AuthProvider, useAuth } from "@/contexts/AuthContext";
|
|||
import { InstanceProvider, useInstance } from "@/contexts/InstanceContext";
|
||||
import { ViewProvider } from "@/contexts/ViewContext";
|
||||
import { queryClient } from "@/lib/query-client";
|
||||
import Loading from "@/pages/Loading";
|
||||
import router from "./router";
|
||||
import { applyLocaleEarly } from "./utils/i18n";
|
||||
import { applyThemeEarly } from "./utils/theme";
|
||||
|
|
@ -26,22 +25,21 @@ applyLocaleEarly();
|
|||
function AppInitializer({ children }: { children: React.ReactNode }) {
|
||||
const { isInitialized: authInitialized, initialize: initAuth } = useAuth();
|
||||
const { isInitialized: instanceInitialized, initialize: initInstance } = useInstance();
|
||||
const [initStarted, setInitStarted] = useState(false);
|
||||
const initStartedRef = useRef(false);
|
||||
|
||||
// Initialize on mount
|
||||
// Initialize on mount - run in parallel for better performance
|
||||
useEffect(() => {
|
||||
if (initStarted) return;
|
||||
setInitStarted(true);
|
||||
if (initStartedRef.current) return;
|
||||
initStartedRef.current = true;
|
||||
|
||||
const init = async () => {
|
||||
await initInstance();
|
||||
await initAuth();
|
||||
await Promise.all([initInstance(), initAuth()]);
|
||||
};
|
||||
init();
|
||||
}, [initAuth, initInstance, initStarted]);
|
||||
}, [initAuth, initInstance]);
|
||||
|
||||
if (!authInitialized || !instanceInitialized) {
|
||||
return <Loading />;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
||||
import { LoaderIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { setAccessToken } from "@/auth-state";
|
||||
import Spinner from "@/components/Spinner";
|
||||
import { authServiceClient } from "@/connect";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { absolutifyLink } from "@/helpers/utils";
|
||||
|
|
@ -113,7 +113,7 @@ const AuthCallback = () => {
|
|||
return (
|
||||
<div className="p-4 py-24 w-full h-full flex justify-center items-center">
|
||||
{state.loading ? (
|
||||
<LoaderIcon className="animate-spin text-foreground" />
|
||||
<Spinner size="lg" />
|
||||
) : (
|
||||
<div className="max-w-lg font-mono whitespace-pre-wrap opacity-80">{state.errorMessage}</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
import { LoaderIcon } from "lucide-react";
|
||||
|
||||
function Loading() {
|
||||
return (
|
||||
<div className="fixed w-full h-full flex flex-row justify-center items-center">
|
||||
<div className="w-80 max-w-full h-full py-4 flex flex-col justify-center items-center">
|
||||
<LoaderIcon className="animate-spin text-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Loading;
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { lazy, Suspense } from "react";
|
||||
import { createBrowserRouter } from "react-router-dom";
|
||||
import App from "@/App";
|
||||
import Skeleton from "@/components/Skeleton";
|
||||
import MainLayout from "@/layouts/MainLayout";
|
||||
import RootLayout from "@/layouts/RootLayout";
|
||||
import Home from "@/pages/Home";
|
||||
import Loading from "@/pages/Loading";
|
||||
|
||||
const AdminSignIn = lazy(() => import("@/pages/AdminSignIn"));
|
||||
const Archived = lazy(() => import("@/pages/Archived"));
|
||||
|
|
@ -39,7 +39,7 @@ const router = createBrowserRouter([
|
|||
{
|
||||
path: "",
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<SignIn />
|
||||
</Suspense>
|
||||
),
|
||||
|
|
@ -47,7 +47,7 @@ const router = createBrowserRouter([
|
|||
{
|
||||
path: "admin",
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<AdminSignIn />
|
||||
</Suspense>
|
||||
),
|
||||
|
|
@ -55,7 +55,7 @@ const router = createBrowserRouter([
|
|||
{
|
||||
path: "signup",
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<SignUp />
|
||||
</Suspense>
|
||||
),
|
||||
|
|
@ -63,7 +63,7 @@ const router = createBrowserRouter([
|
|||
{
|
||||
path: "callback",
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<AuthCallback />
|
||||
</Suspense>
|
||||
),
|
||||
|
|
@ -84,7 +84,7 @@ const router = createBrowserRouter([
|
|||
{
|
||||
path: Routes.EXPLORE,
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<Explore />
|
||||
</Suspense>
|
||||
),
|
||||
|
|
@ -92,7 +92,7 @@ const router = createBrowserRouter([
|
|||
{
|
||||
path: Routes.ARCHIVED,
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<Archived />
|
||||
</Suspense>
|
||||
),
|
||||
|
|
@ -100,7 +100,7 @@ const router = createBrowserRouter([
|
|||
{
|
||||
path: "u/:username",
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<UserProfile />
|
||||
</Suspense>
|
||||
),
|
||||
|
|
@ -110,7 +110,7 @@ const router = createBrowserRouter([
|
|||
{
|
||||
path: Routes.ATTACHMENTS,
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<Attachments />
|
||||
</Suspense>
|
||||
),
|
||||
|
|
@ -118,7 +118,7 @@ const router = createBrowserRouter([
|
|||
{
|
||||
path: Routes.CALENDAR,
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<Calendar />
|
||||
</Suspense>
|
||||
),
|
||||
|
|
@ -126,7 +126,7 @@ const router = createBrowserRouter([
|
|||
{
|
||||
path: Routes.INBOX,
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<Inboxes />
|
||||
</Suspense>
|
||||
),
|
||||
|
|
@ -134,7 +134,7 @@ const router = createBrowserRouter([
|
|||
{
|
||||
path: Routes.SETTING,
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<Setting />
|
||||
</Suspense>
|
||||
),
|
||||
|
|
@ -142,7 +142,7 @@ const router = createBrowserRouter([
|
|||
{
|
||||
path: "memos/:uid",
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<MemoDetail />
|
||||
</Suspense>
|
||||
),
|
||||
|
|
@ -151,7 +151,7 @@ const router = createBrowserRouter([
|
|||
{
|
||||
path: "m/:uid",
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<MemoDetailRedirect />
|
||||
</Suspense>
|
||||
),
|
||||
|
|
@ -159,7 +159,7 @@ const router = createBrowserRouter([
|
|||
{
|
||||
path: "403",
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<PermissionDenied />
|
||||
</Suspense>
|
||||
),
|
||||
|
|
@ -167,7 +167,7 @@ const router = createBrowserRouter([
|
|||
{
|
||||
path: "404",
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<NotFound />
|
||||
</Suspense>
|
||||
),
|
||||
|
|
@ -175,7 +175,7 @@ const router = createBrowserRouter([
|
|||
{
|
||||
path: "*",
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Suspense fallback={<Skeleton type="route" showEditor={false} />}>
|
||||
<NotFound />
|
||||
</Suspense>
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in New Issue