Show Top Progress Bar on navigation initiated using useRouter hook (#4097)
## Problem solved
Short description of the bug fixed or feature added
<!-- start pr-codex -->
---
## PR-Codex overview
This PR updates Next.js navigation usage in the dashboard app to `useDashboardRouter` from `@/lib/DashboardRouter`.
### Detailed summary
- Replaces `useRouter` with `useDashboardRouter` in multiple files
- Adds `DashboardRouterTopProgressBar` component import in layout files
- Introduces `useDashboardRouter` import in various components
- Defines `createStore` function in `@/lib/reactive.ts`
> The following files were skipped due to too many changes: `apps/dashboard/src/@/lib/DashboardRouter.tsx`
> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`
<!-- end pr-codex -->
This commit is contained in:
@@ -80,6 +80,12 @@ module.exports = {
|
||||
message:
|
||||
"Import from `@chakra-ui/react` instead of `@chakra-ui/menu`.",
|
||||
},
|
||||
{
|
||||
name: "next/navigation",
|
||||
importNames: ["useRouter"],
|
||||
message:
|
||||
'Use `import { useDashboardRouter } from "@/lib/DashboardRouter";` instead',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import {
|
||||
Suspense,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
} from "react";
|
||||
import { createStore } from "./reactive";
|
||||
|
||||
// Using useDashboardRouter instead of useRouter gives us a nice progress bar on top of the page when navigating using router.push or router.replace
|
||||
|
||||
// using a store instead of context to avoid triggering re-renders on root component
|
||||
export const LoadingRouteHref = createStore<string | undefined>(undefined);
|
||||
|
||||
export function useDashboardRouter() {
|
||||
const router = useRouter();
|
||||
return useMemo(() => {
|
||||
return {
|
||||
...router,
|
||||
replace(href: string, options?: { scroll?: boolean }) {
|
||||
LoadingRouteHref.setValue(href);
|
||||
router.replace(href, options);
|
||||
},
|
||||
push(href: string, options?: { scroll?: boolean }) {
|
||||
LoadingRouteHref.setValue(href);
|
||||
router.push(href, options);
|
||||
},
|
||||
};
|
||||
}, [router]);
|
||||
}
|
||||
|
||||
function useRouterLoadingStatus() {
|
||||
const loadingHref = useSyncExternalStore(
|
||||
LoadingRouteHref.subscribe,
|
||||
LoadingRouteHref.getValue,
|
||||
LoadingRouteHref.getValue,
|
||||
);
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const searchParamsStr = searchParams?.toString();
|
||||
|
||||
const routerHref = pathname + (searchParamsStr ? `?${searchParamsStr}` : "");
|
||||
const isLoading = loadingHref && loadingHref !== routerHref;
|
||||
|
||||
// reset loading on route load
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
LoadingRouteHref.setValue(undefined);
|
||||
}
|
||||
}, [isLoading]);
|
||||
|
||||
return isLoading;
|
||||
}
|
||||
|
||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// Render this on root
|
||||
function DashboardRouterTopProgressBarInner() {
|
||||
const isLoading = useRouterLoadingStatus();
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const progressStartedRef = useRef(false);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
setProgress(0);
|
||||
progressStartedRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (progressStartedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
let isMounted = true;
|
||||
|
||||
progressStartedRef.current = true;
|
||||
|
||||
async function updateProgressBar(progress: number, delay: number) {
|
||||
if (!isMounted) {
|
||||
if (!isLoading) {
|
||||
setProgress(100);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setProgress(progress);
|
||||
await wait(delay);
|
||||
|
||||
// increase progress by 10% of the remaining progress
|
||||
// increase the delay by 5% to slow down number of updates for slower progress
|
||||
updateProgressBar(progress + (100 - progress) / 10, delay * 1.05);
|
||||
}
|
||||
|
||||
async function startEffect() {
|
||||
// if the loading state remains for atleast 500ms start the progress bar
|
||||
await wait(500);
|
||||
if (isMounted) {
|
||||
updateProgressBar(0, 100);
|
||||
}
|
||||
}
|
||||
|
||||
startEffect();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [isLoading]);
|
||||
|
||||
const width = isLoading ? progress : 100;
|
||||
return (
|
||||
<span
|
||||
className="block fixed top-0 h-[2px] bg-blue-500"
|
||||
style={{
|
||||
opacity: isLoading ? "100" : "0",
|
||||
width: `${width}%`,
|
||||
transition: width === 0 ? "none" : "width 0.2s ease, opacity 0.3s ease",
|
||||
zIndex: "100000000",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// need to wrap with suspense because of useSearchParams usage
|
||||
export function DashboardRouterTopProgressBar() {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<DashboardRouterTopProgressBarInner />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
export type Store<T> = {
|
||||
getValue(): T;
|
||||
setValue(newValue: T): void;
|
||||
subscribe(listener: () => void): () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a reactive value store
|
||||
* @param initialValue - The initial value to store
|
||||
* @example
|
||||
* ```ts
|
||||
* const store = createStore(0);
|
||||
* ```
|
||||
* @returns A store object
|
||||
* @internal
|
||||
*/
|
||||
export function createStore<T>(initialValue: T): Store<T> {
|
||||
type Listener = () => void;
|
||||
const listeners = new Set<Listener>();
|
||||
|
||||
let value = initialValue;
|
||||
|
||||
const notify = () => {
|
||||
for (const listener of listeners) {
|
||||
listener();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getValue() {
|
||||
return value;
|
||||
},
|
||||
setValue(newValue: T) {
|
||||
if (newValue === value) {
|
||||
return;
|
||||
}
|
||||
value = newValue;
|
||||
notify();
|
||||
},
|
||||
subscribe(listener: Listener) {
|
||||
listeners.add(listener);
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { isLoginRequired } from "@/constants/auth";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { EnsureLoginResponse } from "app/api/auth/ensure-login/route";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useRef } from "react";
|
||||
import {
|
||||
useActiveAccount,
|
||||
@@ -20,7 +21,7 @@ export function useLoggedInUser(): {
|
||||
isLoggedIn: boolean;
|
||||
user: { address: string; jwt?: string } | null;
|
||||
} {
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const pathname = usePathname();
|
||||
const connectedAddress = useActiveAccount()?.address;
|
||||
const connectionStatus = useActiveWalletConnectionStatus();
|
||||
|
||||
@@ -10,8 +10,9 @@ import {
|
||||
} from "@/components/ui/popover";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { ChevronDownIcon, Filter, XIcon } from "lucide-react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import type React from "react";
|
||||
import { type PropsWithChildren, useCallback, useMemo } from "react";
|
||||
import { products } from "../../../components/server/products";
|
||||
@@ -73,7 +74,7 @@ type FilterResetButtonProps = {
|
||||
const FilterResetButton: React.FC<
|
||||
PropsWithChildren<FilterResetButtonProps>
|
||||
> = ({ filters, children }) => {
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const mutableSearchParams = useMemo(() => {
|
||||
@@ -124,7 +125,7 @@ type ChainTypeFilterProps = { sectionOnly?: boolean };
|
||||
export const ChainTypeFilter: React.FC<ChainTypeFilterProps> = ({
|
||||
sectionOnly,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -228,7 +229,7 @@ type ChainOptionsFilterProps = { sectionOnly?: boolean };
|
||||
export const ChainOptionsFilter: React.FC<ChainOptionsFilterProps> = ({
|
||||
sectionOnly,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const mutableSearchParams = useMemo(() => {
|
||||
@@ -332,7 +333,7 @@ type ChainServiceFilterProps = { sectionOnly?: boolean };
|
||||
export const ChainServiceFilter: React.FC<ChainServiceFilterProps> = ({
|
||||
sectionOnly,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const mutableSearchParams = useMemo(() => {
|
||||
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { PaginationButtons } from "@/components/pagination-buttons";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { useCallback } from "react";
|
||||
|
||||
type ChainlistPaginationProps = {
|
||||
@@ -15,7 +16,7 @@ export const ChainlistPagination: React.FC<ChainlistPaginationProps> = ({
|
||||
}) => {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
|
||||
const createPageURL = useCallback(
|
||||
(pageNumber: number) => {
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { SearchIcon, XCircleIcon } from "lucide-react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { useRef } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
@@ -15,7 +16,7 @@ function cleanUrl(url: string) {
|
||||
}
|
||||
|
||||
export const SearchInput: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { Grid2X2Icon, ListIcon } from "lucide-react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { useCallback } from "react";
|
||||
|
||||
type hainListViewProps = {
|
||||
@@ -12,7 +13,7 @@ type hainListViewProps = {
|
||||
export const ChainListView: React.FC<hainListViewProps> = ({ activeView }) => {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
|
||||
const createPageURL = useCallback(
|
||||
(view: "grid" | "table") => {
|
||||
|
||||
+2
-2
@@ -15,11 +15,11 @@ import { ImageUpload } from "@/components/ui/image-upload";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { RadioGroup, RadioGroupItemButton } from "@/components/ui/radio-group";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { AccountStatus, useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -49,7 +49,7 @@ export function CreateEcosystemForm() {
|
||||
>();
|
||||
const { data: billingAccountInfo } = useAccount();
|
||||
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
|
||||
@@ -6,10 +6,11 @@ import {
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
|
||||
export function TablePagination() {
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const path = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const page = Number(searchParams?.get("page") || 1);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { usePathname } from "next/navigation";
|
||||
import type { SortBy, TimeRange } from "../../../../lib/search";
|
||||
|
||||
export type SortingHeaderProps = {
|
||||
@@ -13,7 +14,7 @@ export type SortingHeaderProps = {
|
||||
};
|
||||
|
||||
export function SortingHeader(props: SortingHeaderProps) {
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const path = usePathname();
|
||||
const enableSorting = !!props.searchParams;
|
||||
const { timeRange, page, sortBy } = props.searchParams || {};
|
||||
|
||||
+3
-2
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import type { TimeRange } from "lib/search";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
|
||||
export function TimeRangeSwitcher() {
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const path = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const currentSort = searchParams?.get("sortBy") || "transactionCount";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import "@/styles/globals.css";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { DashboardRouterTopProgressBar } from "@/lib/DashboardRouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Metadata } from "next";
|
||||
import PlausibleProvider from "next-plausible";
|
||||
@@ -70,6 +71,7 @@ export default function RootLayout({
|
||||
<PostHogPageView />
|
||||
<AppRouterProviders>{children}</AppRouterProviders>
|
||||
<Toaster richColors />
|
||||
<DashboardRouterTopProgressBar />
|
||||
<NextTopLoader
|
||||
color="hsl(var(--primary))"
|
||||
height={2}
|
||||
|
||||
@@ -18,11 +18,11 @@ import { useContract } from "@thirdweb-dev/react";
|
||||
import type { Abi } from "@thirdweb-dev/sdk";
|
||||
import { SourcesPanel } from "components/contract-components/shared/sources-panel";
|
||||
import { useContractSources } from "contract-ui/hooks/useContractSources";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
import { FiCheckCircle, FiXCircle } from "react-icons/fi";
|
||||
import { toast } from "sonner";
|
||||
import { Badge, Button, Card, Heading } from "tw-components";
|
||||
import { useDashboardRouter } from "../../../@/lib/DashboardRouter";
|
||||
|
||||
interface ContractSourcesPageProps {
|
||||
contractAddress?: string;
|
||||
@@ -257,7 +257,7 @@ function RefreshContractMetadataButton(props: {
|
||||
chainId: number;
|
||||
contractAddress: string;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const contractCacheMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
|
||||
@@ -23,6 +23,7 @@ import { generateBreakpointTypographyCssVars } from "tw-components/utils/typogra
|
||||
import type { ThirdwebNextPage } from "utils/types";
|
||||
import chakraTheme from "../theme";
|
||||
import "@/styles/globals.css";
|
||||
import { DashboardRouterTopProgressBar } from "@/lib/DashboardRouter";
|
||||
|
||||
const inter = interConstructor({
|
||||
subsets: ["latin"],
|
||||
@@ -269,6 +270,8 @@ const ConsoleApp = memo(function ConsoleApp({
|
||||
transitionTimingFunction="ease"
|
||||
/>
|
||||
|
||||
<DashboardRouterTopProgressBar />
|
||||
|
||||
<TailwindTheme>
|
||||
<ChakraProvider theme={chakraThemeWithFonts}>
|
||||
<AnnouncementBanner />
|
||||
|
||||
Reference in New Issue
Block a user