Quellcode durchsuchen

fix(staking): implement a bunch of staking app fixes

- Improve layout of initial landing page after connecting wallet
- Show options to reverse sort on mobile
- Fix paginator
- Fix sort by pool utilization
- Clean up mid-screen sizes (less dead empty space)
- Use a red instead of fuchsia for over utilized pools
- Clean up middleware implementation
- Don't apply middleware to static files
Connor Prussin vor 1 Jahr
Ursprung
Commit
57ec96fe94

+ 4 - 4
apps/staking/src/components/Dashboard/index.tsx

@@ -144,10 +144,10 @@ export const Dashboard = ({
         className="group border-neutral-600/50 data-[empty]:my-[5dvh] data-[empty]:border data-[empty]:bg-white/10 data-[empty]:p-4 sm:p-4 data-[empty]:sm:my-0 data-[empty]:sm:border-0 data-[empty]:sm:bg-transparent data-[empty]:sm:p-0"
         {...(tab === TabIds.Empty && { "data-empty": true })}
       >
-        <h1 className="my-4 hidden text-center text-xl/tight font-light group-data-[empty]:block sm:mb-6 sm:text-3xl lg:my-14 lg:text-5xl">
+        <h1 className="my-4 hidden text-center text-xl/tight font-light group-data-[empty]:mb-10 group-data-[empty]:block sm:mb-6 sm:text-3xl group-data-[empty]:sm:mb-6 lg:my-14 lg:text-5xl">
           Choose Your Journey
         </h1>
-        <TabList className="sticky top-header-height z-10 flex flex-row items-stretch justify-center group-data-[empty]:mx-auto group-data-[empty]:max-w-7xl group-data-[empty]:flex-col group-data-[empty]:gap-2 group-data-[empty]:sm:flex-row">
+        <TabList className="sticky top-header-height z-10 flex flex-row items-stretch justify-center group-data-[empty]:mx-auto group-data-[empty]:max-w-7xl group-data-[empty]:flex-col group-data-[empty]:gap-8 group-data-[empty]:sm:flex-row group-data-[empty]:sm:gap-2">
           <Tab id={TabIds.Empty} className="hidden" />
           <Journey
             longText="Oracle Integrity Staking (OIS)"
@@ -241,13 +241,13 @@ const Journey = ({
     )}
     {...props}
   >
-    <div className="grid size-full flex-none basis-0 place-content-center border border-neutral-600/50 bg-pythpurple-800 p-2 text-center font-semibold transition group-hover/tab:bg-pythpurple-600/30 group-selected/tab:border-pythpurple-400/60 group-selected/tab:bg-pythpurple-600/60 group-hover/tab:group-selected/tab:bg-pythpurple-600/60 sm:py-4 sm:text-lg">
+    <div className="grid size-full flex-none basis-0 place-content-center border border-neutral-600/50 bg-pythpurple-800 p-2 text-center font-semibold transition group-data-[empty]:py-8 group-hover/tab:bg-pythpurple-600/30 group-selected/tab:border-pythpurple-400/60 group-selected/tab:bg-pythpurple-600/60 group-hover/tab:group-selected/tab:bg-pythpurple-600/60 sm:py-4 sm:text-lg group-data-[empty]:sm:py-2">
       <span className="hidden group-data-[empty]:inline sm:inline">
         {longText}
       </span>
       <span className="group-data-[empty]:hidden sm:hidden">{shortText}</span>
     </div>
-    <div className="relative hidden max-h-[40dvh] w-4/5 flex-none overflow-hidden opacity-30 transition group-hover/tab:opacity-100 group-data-[empty]:sm:block">
+    <div className="relative hidden w-4/5 flex-none overflow-hidden opacity-30 transition group-hover/tab:opacity-100 group-data-[empty]:sm:block">
       <div className="absolute inset-0 bg-[#E6DAFE] mix-blend-color" />
       <Image src={image} alt="" className="size-full object-cover object-top" />
       <div className="absolute inset-0 top-16 text-center text-xl text-pythpurple-800 md:text-2xl lg:text-3xl">

+ 181 - 107
apps/staking/src/components/OracleIntegrityStaking/index.tsx

@@ -582,12 +582,9 @@ const PublisherList = ({
   const scrollTarget = useRef<HTMLDivElement | null>(null);
   const [search, setSearch] = useState("");
   const [yoursFirst, setYoursFirst] = useState(true);
-  const [sort, setSort] = useState({
-    field: SortField.PoolUtilization,
-    descending: true,
-  });
+  const [sort, setSort] = useState(SortOption.RemainingPoolDescending);
   const filter = useFilter({ sensitivity: "base", usage: "search" });
-  const [currentPage, setPage] = useState(0);
+  const [currentPage, setPage] = useState(1);
   const filteredSortedPublishers = useMemo(
     () =>
       publishers
@@ -607,25 +604,16 @@ const PublisherList = ({
               return 1;
             }
           }
-          const sortResult = doSort(a, b, yieldRate, sort.field);
-          return sort.descending ? sortResult * -1 : sortResult;
+          return doSort(a, b, yieldRate, sort);
         }),
-    [
-      publishers,
-      search,
-      sort.field,
-      sort.descending,
-      filter,
-      yieldRate,
-      yoursFirst,
-    ],
+    [publishers, search, sort, filter, yieldRate, yoursFirst],
   );
 
   const paginatedPublishers = useMemo(
     () =>
       filteredSortedPublishers.slice(
+        (currentPage - 1) * PAGE_SIZE,
         currentPage * PAGE_SIZE,
-        (currentPage + 1) * PAGE_SIZE,
       ),
     [filteredSortedPublishers, currentPage],
   );
@@ -643,7 +631,7 @@ const PublisherList = ({
   const updateSearch = useCallback<typeof setSearch>(
     (newSearch) => {
       setSearch(newSearch);
-      updatePage(0);
+      updatePage(1);
     },
     [setSearch, updatePage],
   );
@@ -651,7 +639,7 @@ const PublisherList = ({
   const updateSort = useCallback<typeof setSort>(
     (newSort) => {
       setSort(newSort);
-      updatePage(0);
+      updatePage(1);
     },
     [setSort, updatePage],
   );
@@ -659,7 +647,7 @@ const PublisherList = ({
   const updateYoursFirst = useCallback<typeof setYoursFirst>(
     (newYoursFirst) => {
       setYoursFirst(newYoursFirst);
-      updatePage(0);
+      updatePage(1);
     },
     [setYoursFirst, updatePage],
   );
@@ -697,20 +685,13 @@ const PublisherList = ({
           </SearchField>
           <Select
             className="flex flex-row items-center gap-2 2xl:hidden"
-            selectedKey={sort.field}
-            onSelectionChange={(field) => {
-              updateSort({
-                field: field as SortField,
-                descending:
-                  field === SortField.NumberOfFeeds ||
-                  field === SortField.APY ||
-                  field === SortField.SelfStake,
-              });
-            }}
+            selectedKey={sort}
+            // @ts-expect-error react-aria coerces everything to Key for some reason...
+            onSelectionChange={updateSort}
           >
             <Label className="whitespace-nowrap opacity-80">Sort by</Label>
             <Button className="group flex flex-row items-center gap-2 text-xs transition">
-              {SORT_FIELD_TO_NAME[sort.field]}
+              {getSortName(sort)}
               <ChevronDownIcon className="size-4 flex-none opacity-60 transition duration-300 group-data-[pressed]:-rotate-180" />
             </Button>
             <Popover
@@ -720,17 +701,23 @@ const PublisherList = ({
               <ListBox
                 className="flex origin-top-right flex-col border border-neutral-400 bg-pythpurple-100 py-2 text-sm text-pythpurple-950 shadow shadow-neutral-400 outline-none"
                 items={[
-                  { id: SortField.PublisherName },
-                  { id: SortField.PoolUtilization },
-                  { id: SortField.APY },
-                  { id: SortField.SelfStake },
-                  { id: SortField.NumberOfFeeds },
-                  { id: SortField.QualityRanking },
-                ]}
+                  SortOption.PublisherNameDescending,
+                  SortOption.PublisherNameAscending,
+                  SortOption.RemainingPoolDescending,
+                  SortOption.RemainingPoolAscending,
+                  SortOption.ApyDescending,
+                  SortOption.ApyAscending,
+                  SortOption.SelfStakeDescending,
+                  SortOption.SelfStakeAscending,
+                  SortOption.NumberOfFeedsDescending,
+                  SortOption.NumberOfFeedsAscending,
+                  SortOption.QualityRankingDescending,
+                  SortOption.QualityRankingAscending,
+                ].map((id) => ({ id }))}
               >
                 {({ id }) => (
                   <ListBoxItem className="flex cursor-pointer items-center gap-2 whitespace-nowrap px-4 py-2 text-left data-[disabled]:cursor-default data-[focused]:bg-pythpurple-800/20 data-[has-submenu]:data-[open]:bg-pythpurple-800/10 data-[has-submenu]:data-[open]:data-[focused]:bg-pythpurple-800/20 focus:outline-none focus-visible:outline-none">
-                    {SORT_FIELD_TO_NAME[id]}
+                    {getSortName(id)}
                   </ListBoxItem>
                 )}
               </ListBox>
@@ -772,7 +759,8 @@ const PublisherList = ({
             <thead className="bg-pythpurple-100/30 font-light">
               <tr>
                 <SortablePublisherTableHeader
-                  field={SortField.PublisherName}
+                  asc={SortOption.PublisherNameAscending}
+                  desc={SortOption.PublisherNameDescending}
                   sort={sort}
                   setSort={updateSort}
                   alignment="left"
@@ -781,21 +769,24 @@ const PublisherList = ({
                   Publisher
                 </SortablePublisherTableHeader>
                 <SortablePublisherTableHeader
-                  field={SortField.SelfStake}
+                  asc={SortOption.SelfStakeAscending}
+                  desc={SortOption.SelfStakeDescending}
                   sort={sort}
                   setSort={updateSort}
                 >
                   {"Publisher's stake"}
                 </SortablePublisherTableHeader>
                 <SortablePublisherTableHeader
-                  field={SortField.PoolUtilization}
+                  asc={SortOption.RemainingPoolAscending}
+                  desc={SortOption.RemainingPoolDescending}
                   sort={sort}
                   setSort={updateSort}
                 >
                   Pool
                 </SortablePublisherTableHeader>
                 <SortablePublisherTableHeader
-                  field={SortField.APY}
+                  asc={SortOption.ApyAscending}
+                  desc={SortOption.ApyDescending}
                   sort={sort}
                   setSort={updateSort}
                 >
@@ -803,14 +794,16 @@ const PublisherList = ({
                 </SortablePublisherTableHeader>
                 <PublisherTableHeader>Historical APY</PublisherTableHeader>
                 <SortablePublisherTableHeader
-                  field={SortField.NumberOfFeeds}
+                  asc={SortOption.NumberOfFeedsAscending}
+                  desc={SortOption.NumberOfFeedsDescending}
                   sort={sort}
                   setSort={updateSort}
                 >
                   Number of feeds
                 </SortablePublisherTableHeader>
                 <SortablePublisherTableHeader
-                  field={SortField.QualityRanking}
+                  asc={SortOption.QualityRankingAscending}
+                  desc={SortOption.QualityRankingDescending}
                   sort={sort}
                   setSort={updateSort}
                 >
@@ -896,6 +889,7 @@ const Paginator = ({ currentPage, numPages, onPageChange }: PaginatorProps) => {
                 onPageChange(page);
               }}
               size="nopad"
+              variant="secondary"
               className="grid size-8 place-content-center"
             >
               {page}
@@ -936,16 +930,19 @@ const doSort = (
   a: PublisherProps["publisher"],
   b: PublisherProps["publisher"],
   yieldRate: bigint,
-  sortField: SortField,
+  sort: SortOption,
 ): number => {
-  switch (sortField) {
-    case SortField.PublisherName: {
-      return (a.name ?? a.publicKey.toBase58()).localeCompare(
+  switch (sort) {
+    case SortOption.PublisherNameAscending:
+    case SortOption.PublisherNameDescending: {
+      const value = (a.name ?? a.publicKey.toBase58()).localeCompare(
         b.name ?? b.publicKey.toBase58(),
       );
+      return sort === SortOption.PublisherNameAscending ? -1 * value : value;
     }
-    case SortField.APY: {
-      return (
+    case SortOption.ApyAscending:
+    case SortOption.ApyDescending: {
+      const value =
         calculateApy({
           isSelf: false,
           selfStake: a.selfStake + a.selfStakeDelta,
@@ -959,20 +956,34 @@ const doSort = (
           poolCapacity: b.poolCapacity,
           poolUtilization: b.poolUtilization + b.poolUtilizationDelta,
           yieldRate,
-        })
-      );
+        });
+      return sort === SortOption.ApyDescending ? -1 * value : value;
     }
-    case SortField.NumberOfFeeds: {
+    case SortOption.NumberOfFeedsAscending: {
       return Number(a.numFeeds - b.numFeeds);
     }
-    case SortField.PoolUtilization: {
-      const value = Number(
-        (a.poolUtilization + a.poolUtilizationDelta) * b.poolCapacity -
-          (b.poolUtilization + b.poolUtilizationDelta) * a.poolCapacity,
-      );
-      return value === 0 ? Number(a.poolCapacity - b.poolCapacity) : value;
+    case SortOption.NumberOfFeedsDescending: {
+      return Number(b.numFeeds - a.numFeeds);
+    }
+    case SortOption.RemainingPoolAscending:
+    case SortOption.RemainingPoolDescending: {
+      if (a.poolCapacity === 0n && b.poolCapacity === 0n) {
+        return 0;
+      } else if (a.poolCapacity === 0n) {
+        return 1;
+      } else if (b.poolCapacity === 0n) {
+        return -1;
+      } else {
+        const remainingPoolA =
+          a.poolCapacity - a.poolUtilization - a.poolUtilizationDelta;
+        const remainingPoolB =
+          b.poolCapacity - b.poolUtilization - b.poolUtilizationDelta;
+        const value = Number(remainingPoolA - remainingPoolB);
+        return sort === SortOption.RemainingPoolDescending ? -1 * value : value;
+      }
     }
-    case SortField.QualityRanking: {
+    case SortOption.QualityRankingDescending:
+    case SortOption.QualityRankingAscending: {
       if (a.qualityRanking === 0 && b.qualityRanking === 0) {
         return 0;
       } else if (a.qualityRanking === 0) {
@@ -980,12 +991,16 @@ const doSort = (
       } else if (b.qualityRanking === 0) {
         return -1;
       } else {
-        return Number(a.qualityRanking - b.qualityRanking);
+        const value = Number(a.qualityRanking - b.qualityRanking);
+        return sort === SortOption.QualityRankingAscending ? -1 * value : value;
       }
     }
-    case SortField.SelfStake: {
+    case SortOption.SelfStakeAscending: {
       return Number(a.selfStake - b.selfStake);
     }
+    case SortOption.SelfStakeDescending: {
+      return Number(b.selfStake - a.selfStake);
+    }
   }
 };
 
@@ -994,14 +1009,16 @@ type SortablePublisherTableHeaderProps = Omit<
   "children"
 > & {
   children: string;
-  field: SortField;
-  sort: { field: SortField; descending: boolean };
-  setSort: Dispatch<SetStateAction<{ field: SortField; descending: boolean }>>;
+  asc: SortOption;
+  desc: SortOption;
+  sort: SortOption;
+  setSort: Dispatch<SetStateAction<SortOption>>;
   alignment?: "left" | "right";
 };
 
 const SortablePublisherTableHeader = ({
-  field,
+  asc,
+  desc,
   sort,
   setSort,
   children,
@@ -1010,11 +1027,8 @@ const SortablePublisherTableHeader = ({
   ...props
 }: SortablePublisherTableHeaderProps) => {
   const updateSort = useCallback(() => {
-    setSort((cur) => ({
-      field,
-      descending: cur.field === field ? !cur.descending : false,
-    }));
-  }, [setSort, field]);
+    setSort((cur) => (cur === desc ? asc : desc));
+  }, [setSort, asc, desc]);
 
   return (
     <th>
@@ -1025,8 +1039,8 @@ const SortablePublisherTableHeader = ({
           className,
         )}
         onPress={updateSort}
-        {...(sort.field === field && { "data-sorted": true })}
-        {...(sort.descending && { "data-descending": true })}
+        {...((sort === asc || sort === desc) && { "data-sorted": true })}
+        {...(sort === desc && { "data-descending": true })}
         data-alignment={alignment ?? "center"}
         {...props}
       >
@@ -1133,13 +1147,13 @@ const Publisher = ({
   );
 
   return compact ? (
-    <div className="border-t border-neutral-600/50 p-4 sm:px-10">
+    <div className="border-t border-neutral-600/50 p-4 sm:px-10 md:pt-8">
       {!isSelf && (
         <div className="flex flex-row items-center justify-between">
           <PublisherName
             className="font-semibold"
-            truncatedClassName="sm:hidden"
-            fullClassName="hidden sm:inline"
+            truncatedClassName="md:hidden"
+            fullClassName="hidden md:inline"
           >
             {publisher}
           </PublisherName>
@@ -1153,24 +1167,40 @@ const Publisher = ({
           />
         </div>
       )}
-      <div className="gap-8 xs:flex xs:flex-row-reverse xs:items-center xs:justify-between">
-        <div className="flex grow flex-col gap-2 xs:items-end">
-          {isSelf && (
-            <StakeToPublisherButton
-              api={api}
-              currentEpoch={currentEpoch}
-              availableToStake={availableToStake}
+      <div
+        className={clsx(
+          "gap-8",
+          isSelf
+            ? "flex flex-row-reverse items-center justify-between"
+            : "xs:flex xs:flex-row-reverse xs:items-center xs:justify-between",
+        )}
+      >
+        {!isSelf && (
+          <div className="flex grow flex-col gap-2 xs:items-end">
+            <UtilizationMeter
               publisher={publisher}
-              yieldRate={yieldRate}
-              isSelf
+              className="mx-auto my-4 w-full grow xs:mx-0 sm:w-auto sm:flex-none"
             />
-          )}
-          <UtilizationMeter
+          </div>
+        )}
+        {isSelf && (
+          <StakeToPublisherButton
+            api={api}
+            currentEpoch={currentEpoch}
+            availableToStake={availableToStake}
             publisher={publisher}
-            className="mx-auto my-4 w-full grow xs:mx-0 sm:w-auto sm:flex-none"
+            yieldRate={yieldRate}
+            isSelf
           />
-        </div>
-        <dl className="flex-none text-xs">
+        )}
+        <dl
+          className={clsx(
+            "flex-none text-xs",
+            isSelf
+              ? "lg:flex lg:flex-row lg:gap-6"
+              : "md:grid md:grid-cols-2 lg:gap-x-10 xl:flex xl:flex-row xl:gap-8",
+          )}
+        >
           {!isSelf && (
             <div className="flex flex-row items-center gap-2">
               <dt className="font-semibold">{"Publisher's Stake:"}</dt>
@@ -1195,6 +1225,12 @@ const Publisher = ({
           </div>
         </dl>
       </div>
+      {isSelf && (
+        <UtilizationMeter
+          publisher={publisher}
+          className="mx-auto my-4 w-full grow xs:mx-0"
+        />
+      )}
       {(warmup !== undefined || staked !== undefined) && (
         <YourPositionsTable
           publisher={publisher}
@@ -1311,7 +1347,7 @@ const UtilizationMeter = ({ publisher, ...props }: UtilizationMeterProps) => {
               }}
               className={clsx(
                 "absolute inset-0 max-w-full",
-                percentage < 100 ? "bg-pythpurple-400" : "bg-fuchsia-900",
+                percentage < 100 ? "bg-pythpurple-400" : "bg-red-800",
               )}
             />
             <div
@@ -1600,23 +1636,61 @@ const hasAnyPositions = ({ positions }: PublisherProps["publisher"]) =>
     positions.cooldown2,
   ].some((value) => value !== undefined && value > 0n);
 
-enum SortField {
-  PublisherName,
-  PoolUtilization,
-  APY,
-  SelfStake,
-  NumberOfFeeds,
-  QualityRanking,
+enum SortOption {
+  PublisherNameDescending,
+  PublisherNameAscending,
+  RemainingPoolDescending,
+  RemainingPoolAscending,
+  ApyDescending,
+  ApyAscending,
+  SelfStakeDescending,
+  SelfStakeAscending,
+  NumberOfFeedsDescending,
+  NumberOfFeedsAscending,
+  QualityRankingDescending,
+  QualityRankingAscending,
 }
 
-const SORT_FIELD_TO_NAME: Record<SortField, string> = {
-  [SortField.PublisherName]: "Publisher Name",
-  [SortField.PoolUtilization]: "Pool Utilization",
-  [SortField.APY]: "Estimated Next APY",
-  [SortField.SelfStake]: "Publisher's Stake",
-  [SortField.NumberOfFeeds]: "Number of Feeds",
-  [SortField.QualityRanking]: "Quality Ranking",
-} as const;
+const getSortName = (sortOption: SortOption) => {
+  switch (sortOption) {
+    case SortOption.PublisherNameDescending: {
+      return "Publisher Name (A-Z)";
+    }
+    case SortOption.PublisherNameAscending: {
+      return "Publisher Name (Z-A)";
+    }
+    case SortOption.RemainingPoolDescending: {
+      return "Most remaining pool";
+    }
+    case SortOption.RemainingPoolAscending: {
+      return "Least remaining pool";
+    }
+    case SortOption.ApyDescending: {
+      return "Highest estimated next APY";
+    }
+    case SortOption.ApyAscending: {
+      return "Lowest estimated next APY";
+    }
+    case SortOption.SelfStakeDescending: {
+      return "Highest publisher's stake";
+    }
+    case SortOption.SelfStakeAscending: {
+      return "Lowest publisher's stake";
+    }
+    case SortOption.NumberOfFeedsDescending: {
+      return "Most feeds";
+    }
+    case SortOption.NumberOfFeedsAscending: {
+      return "Least feeds";
+    }
+    case SortOption.QualityRankingDescending: {
+      return "Best quality ranking";
+    }
+    case SortOption.QualityRankingAscending: {
+      return "Worst quality ranking";
+    }
+  }
+};
 
 class InvalidKeyError extends Error {
   constructor() {

+ 19 - 17
apps/staking/src/middleware.ts

@@ -7,31 +7,31 @@ import {
 } from "./config/isomorphic";
 import { BLOCKED_REGIONS, PROXYCHECK_API_KEY } from "./config/server";
 
+const PROXY_BLOCK_PATH = `/${REGION_BLOCKED_SEGMENT}`;
+const VPN_BLOCK_PATH = `/${VPN_BLOCKED_SEGMENT}`;
+
 const proxyCheckClient = PROXYCHECK_API_KEY
   ? new ProxyCheck({ api_key: PROXYCHECK_API_KEY })
   : undefined;
 
 export const middleware = async (request: NextRequest) => {
   if (isRegionBlocked(request)) {
-    return NextResponse.rewrite(
-      new URL(`/${REGION_BLOCKED_SEGMENT}`, request.url),
-    );
+    return rewrite(request, PROXY_BLOCK_PATH);
   } else if (await isProxyBlocked(request)) {
-    return NextResponse.rewrite(
-      new URL(`/${VPN_BLOCKED_SEGMENT}`, request.url),
-    );
+    return rewrite(request, VPN_BLOCK_PATH);
+  } else if (isBlockedSegment(request)) {
+    return rewrite(request, "/not-found");
   } else {
-    const { pathname } = request.nextUrl;
-    return pathname.startsWith(`/${REGION_BLOCKED_SEGMENT}`) ||
-      pathname.startsWith(`/${VPN_BLOCKED_SEGMENT}`)
-      ? NextResponse.rewrite(new URL("/not-found", request.url))
-      : undefined;
+    return;
   }
 };
 
-const isRegionBlocked = (request: NextRequest) =>
-  request.geo?.country !== undefined &&
-  BLOCKED_REGIONS.includes(request.geo.country.toLowerCase());
+const rewrite = (request: NextRequest, path: string) =>
+  NextResponse.rewrite(new URL(path, request.url));
+
+const isRegionBlocked = ({ geo }: NextRequest) =>
+  geo?.country !== undefined &&
+  BLOCKED_REGIONS.includes(geo.country.toLowerCase());
 
 const isProxyBlocked = async ({ ip }: NextRequest) => {
   if (proxyCheckClient === undefined || ip === undefined) {
@@ -42,8 +42,10 @@ const isProxyBlocked = async ({ ip }: NextRequest) => {
   }
 };
 
+const isBlockedSegment = ({ nextUrl: { pathname } }: NextRequest) =>
+  pathname.startsWith(`/${REGION_BLOCKED_SEGMENT}`) ||
+  pathname.startsWith(`/${VPN_BLOCKED_SEGMENT}`);
+
 export const config = {
-  matcher: [
-    "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
-  ],
+  matcher: [String.raw`/((?!_next/static|_next/image|.*\.).*)`],
 };