@@ -345,7 +344,7 @@ export function AppsContent({ newLayout, currentCategory }: AppsContentProps) {
{/* Apps carousel & table */}
- {!newLayout && !search && (
+ {!search && (
setPage(page + 1)}
hasMore={page < Math.floor(filteredApps.length / 10)}
>
- {newLayout ? (
- setSearch("")}
- hasActiveFilters={hasActiveFilters(filters)}
- resetFilters={() => {
- updateFilters({
- network: "Mainnet",
- categories: [],
- tags: [],
- });
- }}
- />
- }
- network={network}
- />
- ) : (
- setSearch("")}
- hasActiveFilters={hasActiveFilters(filters)}
- resetFilters={() => {
- updateFilters({
- network: "Mainnet",
- categories: [],
- tags: [],
- });
- }}
- />
- }
- network={network}
- />
- )}
+ setSearch("")}
+ hasActiveFilters={hasActiveFilters(filters)}
+ resetFilters={() => {
+ updateFilters({
+ network: "Mainnet",
+ categories: [],
+ tags: [],
+ });
+ }}
+ />
+ }
+ network={network}
+ />
diff --git a/src/app/[locale]/(dashboard)/dashboard/_components/AppsTable.tsx b/src/app/[locale]/(dashboard)/dashboard/_components/AppsTable.tsx
index 2d30a0f..f645f2e 100644
--- a/src/app/[locale]/(dashboard)/dashboard/_components/AppsTable.tsx
+++ b/src/app/[locale]/(dashboard)/dashboard/_components/AppsTable.tsx
@@ -1,14 +1,8 @@
-import React, {
- PropsWithChildren,
- useCallback,
- useEffect,
- useRef,
- useState,
-} from "react";
+import React, { useCallback, useState } from "react";
import Image from "next/image";
import { NoisyContainer } from "@/components/Noisy";
-import { classNames } from "@/util/classes";
+import { ScrollWithGradient } from "@/components/ScrollWithGradient";
import { AppLinks } from "./AppLinks";
import { InkApp, InkAppNetwork, mainUrl } from "./InkApp";
@@ -108,69 +102,3 @@ export const AppsTable: React.FC<{
);
};
-
-const useScrollTracking = ({
- scrollRef,
- onPercent,
-}: {
- scrollRef: React.RefObject
;
- onPercent: (pct: number, px: number) => void;
-}) => {
- const [firstLoadDone, setFirstLoadDone] = useState(false);
- const handleScroll = useCallback(() => {
- if (!scrollRef.current) return;
- const scrollElement = scrollRef.current;
- const totalOverflow = scrollElement.scrollWidth - scrollElement.clientWidth;
- const scrollPosition = scrollElement.scrollLeft;
- onPercent((scrollPosition / totalOverflow) * 100, scrollPosition);
- }, [scrollRef, onPercent]);
-
- useEffect(() => {
- if (!scrollRef.current) return;
- const scrollElement = scrollRef.current;
- window.addEventListener("resize", handleScroll);
- scrollElement.addEventListener("scroll", handleScroll);
- return () => {
- scrollElement.removeEventListener("scroll", handleScroll);
- window.removeEventListener("resize", handleScroll);
- };
- }, [scrollRef, handleScroll]);
- useEffect(() => {
- if (!scrollRef.current) return;
- if (firstLoadDone) return;
- handleScroll();
- setFirstLoadDone(true);
- }, [scrollRef, handleScroll, firstLoadDone]);
-};
-
-const ScrollWithGradient: React.FC<
- PropsWithChildren & {
- className?: string;
- onScroll?: (pct: number, px: number) => void;
- }
-> = ({ className, children, onScroll: onPercent }) => {
- const scrollRef = useRef(null);
- useScrollTracking({
- scrollRef,
- onPercent: (pct, px) => {
- if (!scrollRef.current) return;
- onPercent?.(pct, px);
- scrollRef.current.style.setProperty(
- "--tw-gradient-from-position",
- `${Math.max(Math.min(pct + 60, 100), 80)}%`
- );
- },
- });
-
- return (
-
- {children}
-
- );
-};
diff --git a/src/app/[locale]/_components/AboutContent/AboutContent.tsx b/src/app/[locale]/_components/AboutContent/AboutContent.tsx
index f4c78c8..732e7fd 100644
--- a/src/app/[locale]/_components/AboutContent/AboutContent.tsx
+++ b/src/app/[locale]/_components/AboutContent/AboutContent.tsx
@@ -127,8 +127,8 @@ export const AboutContent = () => {
diff --git a/src/app/[locale]/_components/ContactContent/ContactContent.tsx b/src/app/[locale]/_components/ContactContent/ContactContent.tsx
index 695e17b..82c8487 100644
--- a/src/app/[locale]/_components/ContactContent/ContactContent.tsx
+++ b/src/app/[locale]/_components/ContactContent/ContactContent.tsx
@@ -29,8 +29,8 @@ export const ContactContent: React.FC = ({}) => {
diff --git a/src/app/[locale]/_components/DeveloperContent/DeveloperContent.tsx b/src/app/[locale]/_components/DeveloperContent/DeveloperContent.tsx
index e78d917..aa25e59 100644
--- a/src/app/[locale]/_components/DeveloperContent/DeveloperContent.tsx
+++ b/src/app/[locale]/_components/DeveloperContent/DeveloperContent.tsx
@@ -233,8 +233,8 @@ export const DeveloperContent = () => {
diff --git a/src/app/[locale]/_components/MainCallToActionButton.tsx b/src/app/[locale]/_components/MainCallToActionButton.tsx
index aa10b52..0f46c6c 100644
--- a/src/app/[locale]/_components/MainCallToActionButton.tsx
+++ b/src/app/[locale]/_components/MainCallToActionButton.tsx
@@ -2,20 +2,19 @@
import React from "react";
import { Button, ButtonProps } from "@inkonchain/ink-kit";
-import { ArrowOnHover } from "@/components/ArrowOnHover";
import { ButtonLink as LegacyButtonLink } from "@/components/Button/ButtonLink";
+import { AppsIcon } from "@/components/icons/Apps";
import { BridgeIcon } from "@/components/icons/Bridge";
-import { DiscordIcon } from "@/components/icons/Discord";
import { OnlyWithFeatureFlag } from "@/components/OnlyWithFeatureFlag";
import { useRouterQuery } from "@/hooks/useRouterQuery";
-import { EXTERNAL_LINKS, Link } from "@/routing";
+import { Link } from "@/routing";
import { classNames } from "@/util/classes";
export interface MainCallToActionButtonProps {
variant?: ButtonProps["variant"] | "spotlight";
copy: {
- ctaLabel: string;
- discordCtaLabel: string;
+ bridgeNow: string;
+ exploreApps: string;
};
/** For some reason, the only button that should have a larger width is the main "hero" call to action button */
isMainCallToAction?: boolean;
@@ -26,42 +25,40 @@ export const MainCallToActionButton: React.FC
= (
) => {
return (
}
+ flag="newNav"
+ otherwise={}
>
-
+
);
};
-const DiscordMainCallToActionButton: React.FC = ({
- variant = "primary",
- copy,
- isMainCallToAction = false,
-}) => {
+const BridgeNowMainCallToActionButton: React.FC<
+ MainCallToActionButtonProps
+> = ({ variant = "primary", copy, isMainCallToAction = false }) => {
+ const query = useRouterQuery();
if (variant === "spotlight") {
/** TODO: Remove this if the button component is updated to have this variant */
return (
+
}
>
-
- {copy.discordCtaLabel}
-
+
+ {copy.bridgeNow}
);
@@ -79,7 +76,7 @@ const DiscordMainCallToActionButton: React.FC = ({
}
)}
iconLeft={
- = ({
}
>
- {copy.discordCtaLabel}
-
+ {copy.bridgeNow}
);
};
-const BridgeNowMainCallToActionButton: React.FC<
+const ExploreAppsMainCallToActionButton: React.FC<
MainCallToActionButtonProps
> = ({ variant = "primary", copy, isMainCallToAction = false }) => {
const query = useRouterQuery();
@@ -112,10 +109,9 @@ const BridgeNowMainCallToActionButton: React.FC<
return (
+
}
>
- {copy.ctaLabel}
+ {copy.exploreApps}
);
@@ -146,7 +142,7 @@ const BridgeNowMainCallToActionButton: React.FC<
}
)}
iconLeft={
-
- {copy.ctaLabel}
+ {copy.exploreApps}
diff --git a/src/app/[locale]/_components/MainContent/MainContent.tsx b/src/app/[locale]/_components/MainContent/MainContent.tsx
index 3a8a367..3875a5a 100644
--- a/src/app/[locale]/_components/MainContent/MainContent.tsx
+++ b/src/app/[locale]/_components/MainContent/MainContent.tsx
@@ -38,37 +38,46 @@ export const MainContent: React.FC<{
/>
-
-
- }
- size="lg"
- asChild
- >
-
- {t("exploreApps:cta")}
-
-
+
+
+
+ }
+ size="lg"
+ asChild
+ >
+
+ {t("exploreApps:cta")}
+
+
+ >
+ }
+ >
+
-
}
- >
-
-
+
diff --git a/src/app/[locale]/new/_components/SideNav.tsx b/src/app/[locale]/new/_components/SideNav.tsx
new file mode 100644
index 0000000..d443994
--- /dev/null
+++ b/src/app/[locale]/new/_components/SideNav.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import { InkLayoutSideNav } from "@inkonchain/ink-kit";
+
+import { AppsIcon } from "@/components/icons/Apps";
+import { useRouterQuery } from "@/hooks/useRouterQuery";
+import { Link, usePathname } from "@/routing";
+
+export const SideNav = () => {
+ const path = usePathname();
+ const query = useRouterQuery();
+
+ if (path === "/new") {
+ return false;
+ }
+
+ return (
+
Apps
+ ),
+ href: "/new/dashboard",
+ icon: ,
+ },
+ ]}
+ />
+ );
+};
diff --git a/src/app/[locale]/new/dashboard/[category]/page.tsx b/src/app/[locale]/new/dashboard/[category]/page.tsx
index a176268..57d2897 100644
--- a/src/app/[locale]/new/dashboard/[category]/page.tsx
+++ b/src/app/[locale]/new/dashboard/[category]/page.tsx
@@ -5,7 +5,7 @@ import { AppSubmissionModalProvider } from "@/components/AppSubmissionModal/AppS
import { JsonLd } from "@/components/JsonLd";
import { PageView } from "@/components/PageView";
-import { AppsContent } from "../../../(dashboard)/dashboard/_components/AppsContent";
+import { AppsContent } from "../_components/AppsContent";
export const metadata: Metadata = {
title: "Ink Apps - Discover DeFi Applications on the Superchain",
@@ -31,7 +31,7 @@ export default async function AppsPage({
}}
/>
-
+
>
diff --git a/src/app/[locale]/new/dashboard/_components/AppCard.tsx b/src/app/[locale]/new/dashboard/_components/AppCard.tsx
new file mode 100644
index 0000000..66e0590
--- /dev/null
+++ b/src/app/[locale]/new/dashboard/_components/AppCard.tsx
@@ -0,0 +1,113 @@
+import { useState } from "react";
+import Image from "next/image";
+
+import { FeaturedAppPill } from "@/app/[locale]/(dashboard)/dashboard/_components/FeaturedAppPill";
+import { ParallaxedHoverImage } from "@/components/ParallaxedHoverImage";
+import featuredApps from "@/generated/featured-apps.json";
+import { classNames } from "@/util/classes";
+
+import { AppLinks } from "../../../(dashboard)/dashboard/_components/AppLinks";
+
+import { InkApp, InkAppNetwork, mainUrl } from "./InkApp";
+
+function matchAppImageFileName(name: string): string {
+ // No whitespace, colons seems to be replaced with underscores.
+ // Check the exported Figma file and adjust to match them if necessary.
+ return name.replaceAll(/:/g, "_").replaceAll(/ /g, "");
+}
+
+export function AppCard({
+ app,
+ network,
+}: {
+ app: InkApp;
+ network: InkAppNetwork;
+}) {
+ const [originalClick, setOriginalClick] = useState<{
+ x: number;
+ y: number;
+ } | null>(null);
+
+ const featuredAppSpecificImage = featuredApps.find(
+ (f) => f.name === app.name || f.name === matchAppImageFileName(app.name)
+ );
+ return (
+
+
+
+
+
+
+
{app.name}
+
+ {app.description}
+
+
+
+
+ );
+}
diff --git a/src/app/[locale]/new/dashboard/_components/AppLinks.tsx b/src/app/[locale]/new/dashboard/_components/AppLinks.tsx
new file mode 100644
index 0000000..35f63a9
--- /dev/null
+++ b/src/app/[locale]/new/dashboard/_components/AppLinks.tsx
@@ -0,0 +1 @@
+export * from "../../../(dashboard)/dashboard/_components/AppLinks";
diff --git a/src/app/[locale]/new/dashboard/_components/AppMainnetToggle.tsx b/src/app/[locale]/new/dashboard/_components/AppMainnetToggle.tsx
new file mode 100644
index 0000000..849befa
--- /dev/null
+++ b/src/app/[locale]/new/dashboard/_components/AppMainnetToggle.tsx
@@ -0,0 +1,51 @@
+"use client";
+
+import {
+ Listbox,
+ ListboxButton,
+ ListboxOption,
+ ListboxOptions,
+} from "@inkonchain/ink-kit";
+
+import { classNames } from "@/util/classes";
+
+import { InkAppNetwork } from "./InkApp";
+
+interface AppMainnetToggleProps {
+ value: InkAppNetwork;
+ onChange: (value: InkAppNetwork) => void;
+}
+
+const items = [
+ { label: "All networks", value: "Both" },
+ { label: "Mainnet", value: "Mainnet" },
+ { label: "Testnet", value: "Testnet" },
+] satisfies { label: string; value: InkAppNetwork }[];
+
+export function AppMainnetToggle({ value, onChange }: AppMainnetToggleProps) {
+ const selectedItem = items.find((item) => item.value === value) || items[0];
+
+ return (
+ onChange(option.value)}>
+
+
+ {selectedItem.label}
+
+
+
+ {items.map((item) => (
+
+ {item.label}
+
+ ))}
+
+
+ );
+}
diff --git a/src/app/[locale]/new/dashboard/_components/AppsCategoryFilter.tsx b/src/app/[locale]/new/dashboard/_components/AppsCategoryFilter.tsx
new file mode 100644
index 0000000..526751f
--- /dev/null
+++ b/src/app/[locale]/new/dashboard/_components/AppsCategoryFilter.tsx
@@ -0,0 +1,61 @@
+import { useMemo } from "react";
+import { SegmentedControl } from "@inkonchain/ink-kit";
+
+import { ScrollWithGradient } from "@/components/ScrollWithGradient";
+import { useRouterQuery } from "@/hooks/useRouterQuery";
+import { Link } from "@/routing";
+
+import { appCategories } from "./categories";
+
+interface AppsCategoryFilterProps {
+ selected: string | undefined;
+ setSelected: (value: string | undefined) => void;
+}
+
+const ALL_CATEGORY = "all";
+
+export const AppsCategoryFilter = ({
+ selected,
+ setSelected,
+}: AppsCategoryFilterProps) => {
+ const query = useRouterQuery();
+ const options = useMemo(
+ () =>
+ appCategories.map((item) => ({
+ value: item.value || ALL_CATEGORY,
+ selectedByDefault:
+ selected === undefined
+ ? item.value === null
+ : selected === item.value,
+ asChild: true,
+ children: (
+ {
+ e.preventDefault();
+ }}
+ >
+ {item.label}
+
+ ),
+ })),
+ [selected, query]
+ );
+ return (
+
+
+ setSelected(option.value === ALL_CATEGORY ? "" : option.value)
+ }
+ />
+
+ );
+};
diff --git a/src/app/[locale]/new/dashboard/_components/AppsContent.tsx b/src/app/[locale]/new/dashboard/_components/AppsContent.tsx
new file mode 100644
index 0000000..3ff5147
--- /dev/null
+++ b/src/app/[locale]/new/dashboard/_components/AppsContent.tsx
@@ -0,0 +1,257 @@
+"use client";
+
+import { useCallback, useMemo, useState } from "react";
+import { Button } from "@inkonchain/ink-kit";
+import { useSearchParams } from "next/navigation";
+
+import { InfiniteScrollContainer } from "@/components/InfiniteScrollContainer";
+import { useRouter } from "@/routing";
+
+import { AppsCategoryFilter } from "./AppsCategoryFilter";
+import { AppsGrid } from "./AppsGrid";
+import { AppsTagsFilter } from "./AppsTagsFilter";
+import {
+ InkAppFilters,
+ InkAppNetwork,
+ inkApps,
+ inkFeaturedApps,
+} from "./InkApp";
+
+interface AppsContentProps {
+ currentCategory?: string;
+}
+
+export function AppsContent({ currentCategory }: AppsContentProps) {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const network = getNetwork(searchParams.get("network"));
+ const category = currentCategory || searchParams.get("category");
+ const tags = searchParams.get("tags");
+
+ const [page, setPage] = useState(0);
+ const [search, setSearch] = useState("");
+ const [filters, setFilters] = useState>({
+ categories: category ? category.split(",") : [],
+ tags: tags ? tags.split(",") : [],
+ network: network ? network : "Mainnet",
+ });
+
+ const filteredAppsWithoutSearchTerms = useMemo(
+ () =>
+ inkApps.filter((app) => {
+ if (
+ filters.network === "Mainnet" &&
+ app.network !== "Mainnet" &&
+ app.network !== "Both"
+ )
+ return false;
+ if (
+ filters.network === "Testnet" &&
+ app.network !== "Testnet" &&
+ app.network !== "Both"
+ )
+ return false;
+
+ if (
+ filters.categories.length > 0 &&
+ !app.category.some((category) =>
+ filters.categories.includes(category.toLowerCase())
+ )
+ )
+ return false;
+
+ if (
+ filters.tags.length > 0 &&
+ !app.tags.some((tag) => filters.tags.includes(tag))
+ )
+ return false;
+
+ return true;
+ }),
+ [filters]
+ );
+
+ const filteredApps = useMemo(
+ () =>
+ filteredAppsWithoutSearchTerms.filter((app) => {
+ const searchTerm = search.toLowerCase();
+ if (
+ searchTerm &&
+ !app.name.toLowerCase().includes(searchTerm) &&
+ !app.description.toLowerCase().includes(searchTerm) &&
+ !app.category.some((category) =>
+ category.toLowerCase().includes(searchTerm)
+ ) &&
+ !app.tags.some((tag) => tag.toLowerCase().includes(searchTerm))
+ )
+ return false;
+ return true;
+ }),
+ [filteredAppsWithoutSearchTerms, search]
+ );
+
+ const updateSearchParams = useCallback(
+ (newParams: Record) => {
+ const { network, category, tags, ...params } = Object.fromEntries(
+ searchParams.entries()
+ );
+ const { category: newCategory, ...newParamsToUpdate } = newParams;
+ const queryParams = new URLSearchParams({
+ ...params,
+ ...newParamsToUpdate,
+ });
+ // Doing this manually is *insanely* faster than using router.replace for some reason.
+ window.history.pushState(
+ "",
+ "",
+ `/new/dashboard${newCategory ? `/${newCategory}` : ""}?${queryParams}`
+ );
+ },
+ [searchParams]
+ );
+ const updateFilters = useCallback(
+ (newFilters: Partial) => {
+ setFilters((prevFilters) => {
+ const mergedFilters = { ...prevFilters, ...newFilters };
+
+ updateSearchParams({
+ ...(mergedFilters.network && mergedFilters.network !== "Mainnet"
+ ? { network: mergedFilters.network }
+ : {}),
+ ...(mergedFilters.tags && mergedFilters.tags.length > 0
+ ? { tags: mergedFilters.tags.join(",") }
+ : {}),
+ ...(mergedFilters.categories && mergedFilters.categories.length > 0
+ ? { category: mergedFilters.categories[0] }
+ : {}),
+ });
+
+ setPage(0);
+ return mergedFilters;
+ });
+ },
+ [updateSearchParams]
+ );
+
+ const hasActiveFilters = (filters: InkAppFilters): boolean => {
+ return (
+ !!search ||
+ (filters.network && filters.network !== "Mainnet") ||
+ (filters.categories && filters.categories.length > 0) ||
+ (filters.tags && filters.tags.length > 0)
+ );
+ };
+
+ const appsToDisplay = useMemo(
+ () => filteredApps.slice(0, (page + 1) * 10),
+ [filteredApps, page]
+ );
+
+ return (
+ <>
+
+
+
{
+ updateFilters({
+ categories: value ? [value] : [],
+ });
+ }}
+ />
+
+
{
+ updateFilters({
+ tags: value,
+ });
+ }}
+ />
+
+
+
+ {/* Main flexbox */}
+
+
+
setPage(page + 1)}
+ hasMore={page < Math.floor(filteredApps.length / 10)}
+ >
+ setSearch("")}
+ hasActiveFilters={hasActiveFilters(filters)}
+ resetFilters={() => {
+ updateFilters({
+ network: "Mainnet",
+ categories: [],
+ tags: [],
+ });
+ }}
+ />
+ }
+ network={network}
+ />
+
+
+
+
+ >
+ );
+}
+
+function getNetwork(networkSearchParam: string | null): InkAppNetwork {
+ if (networkSearchParam === "Both" || networkSearchParam === "Testnet") {
+ return networkSearchParam;
+ }
+
+ return "Mainnet";
+}
+
+function NoAppsFound({
+ hasSearch,
+ hasActiveFilters,
+ resetFilters,
+ resetSearch,
+}: {
+ hasSearch: boolean;
+ hasActiveFilters: boolean;
+ resetFilters: () => void;
+ resetSearch: () => void;
+}) {
+ return (
+
+
+
+ No matches found
+
+
+ {`Please change your keywords${
+ hasActiveFilters ? " or reset your filters" : ""
+ } and try again`}
+
+
+ {(hasActiveFilters || hasSearch) && (
+
+ )}
+
+ );
+}
diff --git a/src/app/[locale]/(dashboard)/dashboard/_components/AppsGrid.tsx b/src/app/[locale]/new/dashboard/_components/AppsGrid.tsx
similarity index 88%
rename from src/app/[locale]/(dashboard)/dashboard/_components/AppsGrid.tsx
rename to src/app/[locale]/new/dashboard/_components/AppsGrid.tsx
index 790d055..497b57f 100644
--- a/src/app/[locale]/(dashboard)/dashboard/_components/AppsGrid.tsx
+++ b/src/app/[locale]/new/dashboard/_components/AppsGrid.tsx
@@ -21,7 +21,7 @@ export const AppsGrid: React.FC<{
{noAppsFound}
) : (
-
+
{featuredApps.map((app) => (
))}
@@ -45,7 +45,7 @@ function AppCard({
}) {
const t = useTranslations("dashboard");
return (
-
+
{featured && (
-
@@ -66,7 +66,9 @@ function AppCard({
{app.name}
-
{app.description}
+
+ {app.description}
+
diff --git a/src/app/[locale]/new/dashboard/_components/AppsTagsFilter.tsx b/src/app/[locale]/new/dashboard/_components/AppsTagsFilter.tsx
new file mode 100644
index 0000000..7f56b77
--- /dev/null
+++ b/src/app/[locale]/new/dashboard/_components/AppsTagsFilter.tsx
@@ -0,0 +1,102 @@
+import React, { useMemo } from "react";
+import {
+ Checkbox,
+ Listbox,
+ ListboxButton,
+ ListboxOption,
+ ListboxOptions,
+} from "@inkonchain/ink-kit";
+
+import { inkTags } from "./InkApp";
+
+interface AppsTagsFilterProps {
+ selected: string[] | undefined;
+ setSelected: (value: string[] | undefined) => void;
+}
+
+function capitalizeFirstLetter(value: string) {
+ return value.charAt(0).toUpperCase() + value.slice(1);
+}
+
+const tags = inkTags
+ .map((tag) => ({
+ value: tag,
+ label: capitalizeFirstLetter(tag),
+ }))
+ .sort((a, b) => a.label.localeCompare(b.label));
+
+export const AppsTagsFilter: React.FC
= ({
+ selected,
+ setSelected,
+}) => {
+ const selectedTags = useMemo(
+ () => tags.filter((tag) => selected?.includes(tag.value)),
+ [selected]
+ );
+
+ const toggleAll = React.useCallback(
+ (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (selectedTags.length > 0) {
+ setSelected([]);
+ } else {
+ setSelected(tags.map((t) => t.value));
+ }
+ },
+ [selectedTags, setSelected]
+ );
+
+ return (
+
+
+
+
+ {selectedTags.length === 0 ? (
+ All Tags
+ ) : (
+ Tags
+ )}
+
+ {selectedTags.length > 0 && (
+
+ {selectedTags.length}
+
+ )}
+
+
+
+ 0 && selectedTags.length < tags.length
+ ? true
+ : undefined
+ }
+ />
+ }
+ iconRight={<>>}
+ >
+ Select all
+
+ {tags.map((tag) => (
+ >}>
+
+ {tag.label}
+
+
+ ))}
+
+
+ );
+};
diff --git a/src/app/[locale]/new/dashboard/_components/InkApp.ts b/src/app/[locale]/new/dashboard/_components/InkApp.ts
new file mode 100644
index 0000000..741990f
--- /dev/null
+++ b/src/app/[locale]/new/dashboard/_components/InkApp.ts
@@ -0,0 +1 @@
+export * from "../../../(dashboard)/dashboard/_components/InkApp";
diff --git a/src/app/[locale]/new/dashboard/_components/RoutedLayout.tsx b/src/app/[locale]/new/dashboard/_components/RoutedLayout.tsx
new file mode 100644
index 0000000..d3ef0f0
--- /dev/null
+++ b/src/app/[locale]/new/dashboard/_components/RoutedLayout.tsx
@@ -0,0 +1,29 @@
+"use client";
+
+import { Suspense } from "react";
+import { ConnectWallet, InkLayout } from "@inkonchain/ink-kit";
+import { usePathname } from "next/navigation";
+
+import { InkLogo, InkLogoImage } from "../../_components/InkLogo";
+import { MobileNav } from "../../_components/MobileNav";
+import { SideNav } from "../../_components/SideNav";
+import { TopNav } from "../../_components/TopNav";
+
+export function RoutedLayout({ children }: { children: React.ReactNode }) {
+ const path = usePathname();
+ return (
+ }>
+
+
+ }
+ headerContent={}
+ topNavigation={path === "/new" ? : undefined}
+ mobileNavigation={MobileNav}
+ sideNavigation={path === "/new" ? undefined : }
+ >
+ {children}
+
+ );
+}
diff --git a/src/app/[locale]/new/dashboard/_components/TableRowPill.tsx b/src/app/[locale]/new/dashboard/_components/TableRowPill.tsx
new file mode 100644
index 0000000..5c6607f
--- /dev/null
+++ b/src/app/[locale]/new/dashboard/_components/TableRowPill.tsx
@@ -0,0 +1 @@
+export * from "../../../(dashboard)/dashboard/_components/TableRowPill";
diff --git a/src/app/[locale]/new/dashboard/_components/categories.tsx b/src/app/[locale]/new/dashboard/_components/categories.tsx
new file mode 100644
index 0000000..fbd917a
--- /dev/null
+++ b/src/app/[locale]/new/dashboard/_components/categories.tsx
@@ -0,0 +1,52 @@
+import { InkIcon } from "@inkonchain/ink-kit";
+
+import { AppsIcon } from "@/components/icons/Apps";
+import { BankIcon } from "@/components/icons/Bank";
+import { BlocksIcon } from "@/components/icons/Blocks";
+import { BridgeIcon } from "@/components/icons/Bridge";
+import { GlobeIcon } from "@/components/icons/Globe";
+import { UsersIcon } from "@/components/icons/Users";
+
+export const appCategories = [
+ {
+ value: null,
+ label: "All categories",
+ icon: ,
+ },
+ {
+ value: "bridge",
+ label: "Bridge",
+ icon: (
+
+ ),
+ },
+ {
+ value: "defi",
+ label: "DeFi",
+ icon: ,
+ },
+ {
+ value: "explorers",
+ label: "Explorers",
+ icon: ,
+ },
+ {
+ value: "infra",
+ label: "Infrastructure",
+ icon: (
+
+ ),
+ },
+ {
+ value: "on-ramps",
+ label: "On-ramps",
+ icon: (
+
+ ),
+ },
+ {
+ value: "social",
+ label: "Social",
+ icon: ,
+ },
+] as const;
diff --git a/src/app/[locale]/new/dashboard/page.tsx b/src/app/[locale]/new/dashboard/page.tsx
index a9520d4..ec98660 100644
--- a/src/app/[locale]/new/dashboard/page.tsx
+++ b/src/app/[locale]/new/dashboard/page.tsx
@@ -5,7 +5,7 @@ import { AppSubmissionModalProvider } from "@/components/AppSubmissionModal/AppS
import { JsonLd } from "@/components/JsonLd";
import { PageView } from "@/components/PageView";
-import { AppsContent } from "../../(dashboard)/dashboard/_components/AppsContent";
+import { AppsContent } from "./_components/AppsContent";
export const metadata: Metadata = {
title: "Ink Apps - Discover DeFi Applications on the Superchain",
@@ -26,7 +26,7 @@ export default function AppsPage() {
}}
/>
-
+
>
diff --git a/src/app/[locale]/new/layout.tsx b/src/app/[locale]/new/layout.tsx
index 73eedbb..fe109d7 100644
--- a/src/app/[locale]/new/layout.tsx
+++ b/src/app/[locale]/new/layout.tsx
@@ -1,14 +1,11 @@
-import { Suspense } from "react";
-import { ConnectWallet, InkLayout } from "@inkonchain/ink-kit";
+import React from "react";
import { Footer } from "@/components/Footer";
import { OnlyWithFeatureFlag } from "@/components/OnlyWithFeatureFlag";
import { routing } from "@/routing";
-import { InkLogo, InkLogoImage } from "./_components/InkLogo";
import { MainPageBackground } from "./_components/MainPageBackground";
-import { MobileNav } from "./_components/MobileNav";
-import { TopNav } from "./_components/TopNav";
+import { RoutedLayout } from "./dashboard/_components/RoutedLayout";
export async function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
@@ -21,23 +18,14 @@ export default async function InfoLayout({
}) {
return (
- }>
-
-
- }
- headerContent={}
- topNavigation={}
- mobileNavigation={MobileNav}
- >
+
-
+
);
diff --git a/src/components/ScrollWithGradient.tsx b/src/components/ScrollWithGradient.tsx
new file mode 100644
index 0000000..76b7cfc
--- /dev/null
+++ b/src/components/ScrollWithGradient.tsx
@@ -0,0 +1,75 @@
+import {
+ PropsWithChildren,
+ useCallback,
+ useEffect,
+ useRef,
+ useState,
+} from "react";
+
+import { classNames } from "@/util/classes";
+
+const useScrollTracking = ({
+ scrollRef,
+ onPercent,
+}: {
+ scrollRef: React.RefObject;
+ onPercent: (pct: number, px: number) => void;
+}) => {
+ const [firstLoadDone, setFirstLoadDone] = useState(false);
+ const handleScroll = useCallback(() => {
+ if (!scrollRef.current) return;
+ const scrollElement = scrollRef.current;
+ const totalOverflow = scrollElement.scrollWidth - scrollElement.clientWidth;
+ const scrollPosition = scrollElement.scrollLeft;
+ onPercent((scrollPosition / totalOverflow) * 100, scrollPosition);
+ }, [scrollRef, onPercent]);
+
+ useEffect(() => {
+ if (!scrollRef.current) return;
+ const scrollElement = scrollRef.current;
+ window.addEventListener("resize", handleScroll);
+ scrollElement.addEventListener("scroll", handleScroll);
+ return () => {
+ scrollElement.removeEventListener("scroll", handleScroll);
+ window.removeEventListener("resize", handleScroll);
+ };
+ }, [scrollRef, handleScroll]);
+ useEffect(() => {
+ if (!scrollRef.current) return;
+ if (firstLoadDone) return;
+ handleScroll();
+ setFirstLoadDone(true);
+ }, [scrollRef, handleScroll, firstLoadDone]);
+};
+
+export const ScrollWithGradient: React.FC<
+ PropsWithChildren & {
+ className?: string;
+ onScroll?: (pct: number, px: number) => void;
+ }
+> = ({ className, children, onScroll: onPercent }) => {
+ const scrollRef = useRef(null);
+ useScrollTracking({
+ scrollRef,
+ onPercent: (pct, px) => {
+ if (!scrollRef.current) return;
+ onPercent?.(pct, px);
+ scrollRef.current.style.setProperty(
+ "--tw-gradient-from-position",
+ `${Math.max(Math.min(pct + 60, 100), 80)}%`
+ );
+ },
+ });
+
+ return (
+
+ {children}
+
+ );
+};