diff --git a/src/app/[locale]/(dashboard)/dashboard/_components/AppsContent.tsx b/src/app/[locale]/(dashboard)/dashboard/_components/AppsContent.tsx index c7e4030..a42b62f 100644 --- a/src/app/[locale]/(dashboard)/dashboard/_components/AppsContent.tsx +++ b/src/app/[locale]/(dashboard)/dashboard/_components/AppsContent.tsx @@ -17,7 +17,6 @@ import { AddYourAppButton } from "./AddYourAppButton"; import { AppCard } from "./AppCard"; import { AppMainnetToggle } from "./AppMainnetToggle"; import { AppsCategoryFilter } from "./AppsCategoryFilter"; -import { AppsGrid } from "./AppsGrid"; import { AppsSideBar } from "./AppsSideBar"; import { AppsTable } from "./AppsTable"; import { AppsTagsFilter } from "./AppsTagsFilter"; @@ -33,11 +32,10 @@ import "react-multi-carousel/lib/styles.css"; import "./AppsContent.scss"; interface AppsContentProps { - newLayout?: boolean; currentCategory?: string; } -export function AppsContent({ newLayout, currentCategory }: AppsContentProps) { +export function AppsContent({ currentCategory }: AppsContentProps) { const router = useRouter(); const searchParams = useSearchParams(); const network = getNetwork(searchParams.get("network")); @@ -186,10 +184,10 @@ export function AppsContent({ newLayout, currentCategory }: AppsContentProps) { return ( <> + {/* Floating section on desktop */}
@@ -213,7 +211,6 @@ export function AppsContent({ newLayout, currentCategory }: AppsContentProps) {
{ updateFilters({ @@ -237,7 +234,6 @@ export function AppsContent({ newLayout, currentCategory }: AppsContentProps) {
{ updateFilters({ @@ -248,7 +244,6 @@ export function AppsContent({ newLayout, currentCategory }: AppsContentProps) {
{ updateFilters({ @@ -265,10 +260,9 @@ export function AppsContent({ newLayout, currentCategory }: AppsContentProps) {
-
+
{ updateFilters({ @@ -289,43 +283,48 @@ export function AppsContent({ newLayout, currentCategory }: AppsContentProps) {
-
-
- -
-
- { - updateFilters({ - network: value, - }); - }} - /> -
-
- { - updateFilters({ - tags: value, - }); - }} - /> +
+
+
+ +
+
+ { + updateFilters({ + network: value, + }); + }} + /> +
+
+ { + updateFilters({ + tags: value, + }); + }} + /> +
@@ -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<{ />
- - - + + + + + + } + > +
- } - > - - +
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 && (
-
+
{t("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} - > +
{children}
-
+
); 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} +
+ ); +};