chore: simplify page loading logic
This commit is contained in:
parent
c5d9770fd1
commit
81022123a1
8 changed files with 47 additions and 122 deletions
|
|
@ -1,55 +1,49 @@
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface Props {
|
interface SkeletonProps {
|
||||||
showCreator?: boolean;
|
showCreator?: boolean;
|
||||||
count?: number;
|
count?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memo card skeleton component for list loading states
|
const skeletonBase = "bg-muted/70 rounded animate-pulse";
|
||||||
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">
|
const MemoCardSkeleton = ({ showCreator, index }: { showCreator?: boolean; index: number }) => (
|
||||||
{/* Header section */}
|
<div className="relative flex flex-col bg-card w-full px-4 py-3 mb-2 gap-2 rounded-lg border border-border">
|
||||||
<div className="w-full flex flex-row justify-between items-center gap-2">
|
<div className="w-full flex justify-between items-center gap-2">
|
||||||
<div className="w-auto max-w-[calc(100%-8rem)] grow flex flex-row justify-start items-center">
|
<div className="grow flex items-center max-w-[calc(100%-8rem)]">
|
||||||
{showCreator ? (
|
{showCreator ? (
|
||||||
<div className="w-full flex flex-row justify-start items-center gap-2">
|
<div className="w-full flex items-center gap-2">
|
||||||
<div className="w-8 h-8 rounded-full bg-muted shrink-0" />
|
<div className={cn("w-8 h-8 rounded-full shrink-0", skeletonBase)} />
|
||||||
<div className="w-full flex flex-col justify-center items-start gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="h-4 w-24 bg-muted rounded" />
|
<div className={cn("h-4 w-24", skeletonBase)} />
|
||||||
<div className="h-3 w-16 bg-muted rounded" />
|
<div className={cn("h-3 w-16", skeletonBase)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-4 w-32 bg-muted rounded" />
|
<div className={cn("h-4 w-32", skeletonBase)} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Action buttons skeleton */}
|
<div className="flex gap-2">
|
||||||
<div className="flex flex-row gap-2">
|
<div className={cn("w-4 h-4", skeletonBase)} />
|
||||||
<div className="w-4 h-4 bg-muted rounded" />
|
<div className={cn("w-4 h-4", skeletonBase)} />
|
||||||
<div className="w-4 h-4 bg-muted rounded" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
{/* Content section */}
|
<div className={cn("h-4", skeletonBase, index % 3 === 0 ? "w-full" : index % 3 === 1 ? "w-4/5" : "w-5/6")} />
|
||||||
<div className="w-full flex flex-col gap-2">
|
<div className={cn("h-4", skeletonBase, index % 2 === 0 ? "w-3/4" : "w-4/5")} />
|
||||||
<div className="space-y-2">
|
{index % 2 === 0 && <div className={cn("h-4 w-2/3", skeletonBase)} />}
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Skeleton loading state for memo lists.
|
* Memo list loading skeleton - shows card structure while loading.
|
||||||
* Use this for initial memo list loading and pagination.
|
* Only use for memo lists in PagedMemoList component.
|
||||||
* For generic page/route loading, use Spinner instead.
|
|
||||||
*/
|
*/
|
||||||
const Skeleton = ({ showCreator = false, count = 4 }: Props) => (
|
const Skeleton = ({ showCreator = false, count = 4 }: SkeletonProps) => (
|
||||||
<div className="w-full max-w-2xl mx-auto">
|
<div className="w-full max-w-2xl mx-auto">
|
||||||
{Array.from({ length: count }).map((_, index) => (
|
{Array.from({ length: count }, (_, i) => (
|
||||||
<MemoCardSkeleton key={index} showCreator={showCreator} index={index} />
|
<MemoCardSkeleton key={i} showCreator={showCreator} index={i} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -8,7 +8,6 @@ import { MapContainer, Marker, Popup, useMap } from "react-leaflet";
|
||||||
import MarkerClusterGroup from "react-leaflet-cluster";
|
import MarkerClusterGroup from "react-leaflet-cluster";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { defaultMarkerIcon, ThemedTileLayer } from "@/components/map/map-utils";
|
import { defaultMarkerIcon, ThemedTileLayer } from "@/components/map/map-utils";
|
||||||
import Spinner from "@/components/Spinner";
|
|
||||||
import { useInfiniteMemos } from "@/hooks/useMemoQueries";
|
import { useInfiniteMemos } from "@/hooks/useMemoQueries";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { State } from "@/types/proto/api/v1/common_pb";
|
import { State } from "@/types/proto/api/v1/common_pb";
|
||||||
|
|
@ -64,13 +63,7 @@ const UserMemoMap = ({ creator, className }: Props) => {
|
||||||
|
|
||||||
const memosWithLocation = useMemo(() => data?.pages.flatMap((page) => page.memos).filter((memo) => memo.location) || [], [data]);
|
const memosWithLocation = useMemo(() => data?.pages.flatMap((page) => page.memos).filter((memo) => memo.location) || [], [data]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) return null;
|
||||||
return (
|
|
||||||
<div className="w-full h-[380px] flex items-center justify-center rounded-xl border border-border bg-muted/30">
|
|
||||||
<Spinner className="w-8 h-8" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultCenter = { lat: 48.8566, lng: 2.3522 };
|
const defaultCenter = { lat: 48.8566, lng: 2.3522 };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { Suspense, useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { Outlet, useLocation, useSearchParams } from "react-router-dom";
|
import { Outlet, useLocation, useSearchParams } from "react-router-dom";
|
||||||
import usePrevious from "react-use/lib/usePrevious";
|
import usePrevious from "react-use/lib/usePrevious";
|
||||||
import Navigation from "@/components/Navigation";
|
import Navigation from "@/components/Navigation";
|
||||||
import Spinner from "@/components/Spinner";
|
|
||||||
import { useInstance } from "@/contexts/InstanceContext";
|
import { useInstance } from "@/contexts/InstanceContext";
|
||||||
import { useMemoFilterContext } from "@/contexts/MemoFilterContext";
|
import { useMemoFilterContext } from "@/contexts/MemoFilterContext";
|
||||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
|
|
@ -47,15 +46,7 @@ const RootLayout = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<main className="w-full h-auto grow shrink flex flex-col justify-start items-center">
|
<main className="w-full h-auto grow shrink flex flex-col justify-start items-center">
|
||||||
<Suspense
|
<Outlet />
|
||||||
fallback={
|
|
||||||
<div className="w-full h-64 flex items-center justify-center">
|
|
||||||
<Spinner size="lg" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Outlet />
|
|
||||||
</Suspense>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import { RouterProvider } from "react-router-dom";
|
||||||
import "./i18n";
|
import "./i18n";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { ErrorBoundary } from "@/components/ErrorBoundary";
|
import { ErrorBoundary } from "@/components/ErrorBoundary";
|
||||||
import Spinner from "@/components/Spinner";
|
|
||||||
import { AuthProvider, useAuth } from "@/contexts/AuthContext";
|
import { AuthProvider, useAuth } from "@/contexts/AuthContext";
|
||||||
import { InstanceProvider, useInstance } from "@/contexts/InstanceContext";
|
import { InstanceProvider, useInstance } from "@/contexts/InstanceContext";
|
||||||
import { ViewProvider } from "@/contexts/ViewContext";
|
import { ViewProvider } from "@/contexts/ViewContext";
|
||||||
|
|
@ -41,11 +40,7 @@ function AppInitializer({ children }: { children: React.ReactNode }) {
|
||||||
}, [initAuth, initInstance]);
|
}, [initAuth, initInstance]);
|
||||||
|
|
||||||
if (!authInitialized || !instanceInitialized) {
|
if (!authInitialized || !instanceInitialized) {
|
||||||
return (
|
return null;
|
||||||
<div className="w-full h-screen flex items-center justify-center">
|
|
||||||
<Spinner size="lg" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { timestampDate } from "@bufbuild/protobuf/wkt";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { setAccessToken } from "@/auth-state";
|
import { setAccessToken } from "@/auth-state";
|
||||||
import Spinner from "@/components/Spinner";
|
|
||||||
import { authServiceClient } from "@/connect";
|
import { authServiceClient } from "@/connect";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { absolutifyLink } from "@/helpers/utils";
|
import { absolutifyLink } from "@/helpers/utils";
|
||||||
|
|
@ -110,13 +109,11 @@ const AuthCallback = () => {
|
||||||
})();
|
})();
|
||||||
}, [searchParams, navigateTo]);
|
}, [searchParams, navigateTo]);
|
||||||
|
|
||||||
|
if (state.loading) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 py-24 w-full h-full flex justify-center items-center">
|
<div className="p-4 py-24 w-full h-full flex justify-center items-center">
|
||||||
{state.loading ? (
|
<div className="max-w-lg font-mono whitespace-pre-wrap opacity-80">{state.errorMessage}</div>
|
||||||
<Spinner size="lg" />
|
|
||||||
) : (
|
|
||||||
<div className="max-w-lg font-mono whitespace-pre-wrap opacity-80">{state.errorMessage}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
import { Navigate, useParams } from "react-router-dom";
|
|
||||||
|
|
||||||
const MemoDetailRedirect = () => {
|
|
||||||
const { uid } = useParams();
|
|
||||||
return <Navigate to={`/memos/${uid}`} replace />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MemoDetailRedirect;
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import type { ComponentType } from "react";
|
import { lazy } from "react";
|
||||||
import { lazy, Suspense } from "react";
|
|
||||||
import { createBrowserRouter } from "react-router-dom";
|
import { createBrowserRouter } from "react-router-dom";
|
||||||
import App from "@/App";
|
import App from "@/App";
|
||||||
import Spinner from "@/components/Spinner";
|
|
||||||
import MainLayout from "@/layouts/MainLayout";
|
import MainLayout from "@/layouts/MainLayout";
|
||||||
import RootLayout from "@/layouts/RootLayout";
|
import RootLayout from "@/layouts/RootLayout";
|
||||||
import Home from "@/pages/Home";
|
import Home from "@/pages/Home";
|
||||||
|
|
@ -20,7 +18,6 @@ const Setting = lazy(() => import("@/pages/Setting"));
|
||||||
const SignIn = lazy(() => import("@/pages/SignIn"));
|
const SignIn = lazy(() => import("@/pages/SignIn"));
|
||||||
const SignUp = lazy(() => import("@/pages/SignUp"));
|
const SignUp = lazy(() => import("@/pages/SignUp"));
|
||||||
const UserProfile = lazy(() => import("@/pages/UserProfile"));
|
const UserProfile = lazy(() => import("@/pages/UserProfile"));
|
||||||
const MemoDetailRedirect = lazy(() => import("./MemoDetailRedirect"));
|
|
||||||
|
|
||||||
import { ROUTES } from "./routes";
|
import { ROUTES } from "./routes";
|
||||||
|
|
||||||
|
|
@ -28,19 +25,6 @@ import { ROUTES } from "./routes";
|
||||||
export const Routes = ROUTES;
|
export const Routes = ROUTES;
|
||||||
export { 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([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
|
|
@ -49,10 +33,10 @@ const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: Routes.AUTH,
|
path: Routes.AUTH,
|
||||||
children: [
|
children: [
|
||||||
{ path: "", element: <LazyRoute component={SignIn} /> },
|
{ path: "", element: <SignIn /> },
|
||||||
{ path: "admin", element: <LazyRoute component={AdminSignIn} /> },
|
{ path: "admin", element: <AdminSignIn /> },
|
||||||
{ path: "signup", element: <LazyRoute component={SignUp} /> },
|
{ path: "signup", element: <SignUp /> },
|
||||||
{ path: "callback", element: <LazyRoute component={AuthCallback} /> },
|
{ path: "callback", element: <AuthCallback /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -63,20 +47,18 @@ const router = createBrowserRouter([
|
||||||
element: <MainLayout />,
|
element: <MainLayout />,
|
||||||
children: [
|
children: [
|
||||||
{ path: "", element: <Home /> },
|
{ path: "", element: <Home /> },
|
||||||
{ path: Routes.EXPLORE, element: <LazyRoute component={Explore} /> },
|
{ path: Routes.EXPLORE, element: <Explore /> },
|
||||||
{ path: Routes.ARCHIVED, element: <LazyRoute component={Archived} /> },
|
{ path: Routes.ARCHIVED, element: <Archived /> },
|
||||||
{ path: "u/:username", element: <LazyRoute component={UserProfile} /> },
|
{ path: "u/:username", element: <UserProfile /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ path: Routes.ATTACHMENTS, element: <LazyRoute component={Attachments} /> },
|
{ path: Routes.ATTACHMENTS, element: <Attachments /> },
|
||||||
{ path: Routes.INBOX, element: <LazyRoute component={Inboxes} /> },
|
{ path: Routes.INBOX, element: <Inboxes /> },
|
||||||
{ path: Routes.SETTING, element: <LazyRoute component={Setting} /> },
|
{ path: Routes.SETTING, element: <Setting /> },
|
||||||
{ path: "memos/:uid", element: <LazyRoute component={MemoDetail} /> },
|
{ path: "memos/:uid", element: <MemoDetail /> },
|
||||||
// Redirect old path to new path
|
{ path: "403", element: <PermissionDenied /> },
|
||||||
{ path: "m/:uid", element: <LazyRoute component={MemoDetailRedirect} /> },
|
{ path: "404", element: <NotFound /> },
|
||||||
{ path: "403", element: <LazyRoute component={PermissionDenied} /> },
|
{ path: "*", element: <NotFound /> },
|
||||||
{ path: "404", element: <LazyRoute component={NotFound} /> },
|
|
||||||
{ path: "*", element: <LazyRoute component={NotFound} /> },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue