diff --git a/_data/pages/dapps.yml b/_data/pages/dapps.yml index 6e40f2d168..0f5d47532c 100644 --- a/_data/pages/dapps.yml +++ b/_data/pages/dapps.yml @@ -8,28 +8,10 @@ breadcrumbs: false page_last_updated: true blocks: - type: hero + variant: block_explorers + title: Hundreds of dApps, Services & Wallets + description: Immerse yourself in the Starknet Ecosystem by discovering projects, jobs, metrics and learning resources. + - type: dapps variant: dapps title: dApps - description: Discover apps in the Starknet ecosystem across NFTs, Gaming, Defi, - DAOs and more. - - type: card_list - card_list_items: - - title: Starknet-ecosystem - description: Immerse yourself in the Starknet Ecosystem by discovering projects, - jobs, metrics and learning resources. - website_url: https://www.starknet-ecosystem.com - twitter: https://twitter.com/StarkNetEco - link_url: https://www.starknet-ecosystem.com - image: /assets/sn-ecosystem.jpg - - title: Dappland - description: Discover and rate the best apps on Starknet. - link_url: https://www.dappland.com - website_url: https://www.dappland.com - twitter: https://twitter.com/argentHQ - image: /assets/dappland.jpg - - website_url: https://www.ethereum-ecosystem.com/blockchains/starknet - title: Ethereum Ecosystem - description: Discover the best of Ethereum and its layer 2s. All in one place. - image: /assets/logo.webp - twitter: https://twitter.com/web3xplore - randomize: true + description: "" \ No newline at end of file diff --git a/workspaces/cms-config/src/blocks.ts b/workspaces/cms-config/src/blocks.ts index 19a2832dcc..e47ff8cc24 100644 --- a/workspaces/cms-config/src/blocks.ts +++ b/workspaces/cms-config/src/blocks.ts @@ -335,6 +335,17 @@ export const blocks = [ }, ], }, + { + name: "dapps", + label: "Dapps", + widget: "object", + fields: [ + { + name: "type", + widget: "hidden", + }, + ], + }, { name: "community_events", label: "Community events block", diff --git a/workspaces/cms-data/src/pages.ts b/workspaces/cms-data/src/pages.ts index 9392ad7da6..9c80214cef 100644 --- a/workspaces/cms-data/src/pages.ts +++ b/workspaces/cms-data/src/pages.ts @@ -6,6 +6,7 @@ import { getShuffledArray, } from "@starknet-io/cms-utils/src/index"; import type { Meta } from "@starknet-io/cms-utils/src/index"; +import { Project } from "./starknet-db-projects-dapps"; export interface MarkdownBlock { readonly type: "markdown"; @@ -256,13 +257,17 @@ export interface HeadingContainerBlock { readonly heading_variant: HeadingVariant; readonly blocks: readonly Block[]; } - +export interface DappsBlock { + readonly type: "dapps"; + readonly blocks?: readonly Project[]; +} export type TopLevelBlock = | Block | FlexLayoutBlock | GroupBlock | Container - | HeadingContainerBlock; + | HeadingContainerBlock + | DappsBlock; export interface Page extends Meta { readonly id: string; diff --git a/workspaces/cms-data/src/starknet-db-projects-dapps.ts b/workspaces/cms-data/src/starknet-db-projects-dapps.ts new file mode 100644 index 0000000000..a70ee20065 --- /dev/null +++ b/workspaces/cms-data/src/starknet-db-projects-dapps.ts @@ -0,0 +1,52 @@ +import { getJSON } from "@starknet-io/cms-utils/src/index"; +export interface Project { + readonly id: string; + readonly name: string; + readonly shortName: string; + readonly description: string; + readonly image: string; + readonly network: { + readonly website?: string; + readonly github?: string; + readonly twitter?: string; + readonly twitterImage?: string; + readonly twitterBanner?: string; + readonly medium?: string; + readonly discord?: string; + readonly telegram?: string; + }; + readonly tags: string[]; + readonly socialMetrics: { + readonly twitterFollower: number; + readonly twitterCount: number; + readonly tweetWithStarknet: number; + readonly socialActivity: number; + readonly date: number; + }; + readonly isLive: boolean; + readonly isHidden: boolean; + readonly isTestnetLive: boolean; +} +export interface TagObject { + label: string; + slug: string; +} +export interface DappsProps { + readonly list: Project[]; + readonly categories: TagObject[]; +} +export async function getStarknetDappsDbProjects( + context: EventContext<{}, any, Record> +): Promise { + try { + const sections = await getJSON( + "data/starknet-db-projects-dapps/starknet-db-projects-dapps", + context + ); + return sections; + } catch (cause) { + throw new Error("getStarknetDappsDbProjects failed!", { + cause, + }); + } +} diff --git a/workspaces/cms-scripts/src/index.ts b/workspaces/cms-scripts/src/index.ts index 841676b87e..585dd19e61 100644 --- a/workspaces/cms-scripts/src/index.ts +++ b/workspaces/cms-scripts/src/index.ts @@ -1,8 +1,9 @@ import fs from "fs/promises"; import * as path from "path"; - +import { ApiResponse } from "./types"; +import { Project } from "../../cms-data/src/starknet-db-projects-dapps"; process.chdir(path.resolve(__dirname, "../../..")); - +import slugify from "slugify"; import { write, yaml } from "./utils"; import { locales } from "@starknet-io/cms-data/src/i18n/config"; import { MainMenu } from "./main-menu"; @@ -22,7 +23,7 @@ import { updateBlocks, } from "./data"; import { translateFile } from "./crowdin"; - +import fetch from "node-fetch"; const createRoadmapDetails = async () => { await fs.mkdir(`public/data/roadmap-details`, { recursive: true }); for (const locale of locales) { @@ -301,7 +302,70 @@ await write( `public/data/featured-sections/featured-sections.json`, featuredSections ); +const fetchProjects = async () => { + try { + return await fetch("https://api.starknet-db.com/projects?size=10000").then( + (response) => response.json() as unknown as ApiResponse + ); + } catch (error) { + console.error("Error fetching projects from api.starknet-db:", error); + throw error; + } +}; +const dAppsData: ApiResponse = await fetchProjects(); +export interface TagObject { + label: string; + slug: string; +} +const blackListTags = [ + "all", + "governance", + "pfp", + "green finance", + "cairo", + "formal-verification", + "dex, wallet, multi-chain, cross-chain, okx, bridge, blockchain", + "staking", + "access node", + "data", + "starkware", +]; + +const slugifyTags = (objects: Project[]): Project[] => { + return objects.map((obj) => { + const newObj: Project = { + ...obj, + tags: obj.tags.map((tag) => slugify(tag, "_")), + }; + return newObj; + }); +}; + +const extractTags = (projects: Project[]): TagObject[] => { + const tagsSet = new Set(); + projects.forEach((project) => { + project.tags.forEach((tag: string) => tagsSet.add(tag.toLowerCase())); + }); + // const tagArray = Array.from(tagsSet); + const filteredArray = Array.from(tagsSet).filter( + (item) => !blackListTags.includes(item) + ); + + return filteredArray.map((tag) => ({ + label: tag, + slug: slugify(tag, "_"), + })); +}; +const slugifyDApps = slugifyTags(dAppsData.content); +const categories = extractTags(dAppsData.content).sort((a, b) => + a.label > b.label ? 1 : b.label > a.label ? -1 : 0 +); +await fs.mkdir("public/data/starknet-db-projects-dapps", { recursive: true }); +await write( + `public/data/starknet-db-projects-dapps/starknet-db-projects-dapps.json`, + { list: slugifyDApps, categories } +); await createRoadmapDetails(); await createAnnouncementDetails(); await createSharedData(); diff --git a/workspaces/cms-scripts/src/types.ts b/workspaces/cms-scripts/src/types.ts new file mode 100644 index 0000000000..5387b758ef --- /dev/null +++ b/workspaces/cms-scripts/src/types.ts @@ -0,0 +1,30 @@ +import { Project } from "../../cms-data/src/starknet-db-projects-dapps"; + +export type ApiResponse = { + content: Project[]; + pageable: { + sort: { + empty: boolean; + sorted: boolean; + unsorted: boolean; + }; + offset: number; + pageSize: number; + pageNumber: number; + paged: boolean; + unpaged: boolean; + }; + last: boolean; + totalElements: number; + totalPages: number; + size: number; + number: number; + sort: { + empty: boolean; + sorted: boolean; + unsorted: boolean; + }; + first: boolean; + numberOfElements: number; + empty: boolean; +}; diff --git a/workspaces/website/package.json b/workspaces/website/package.json index 94a1854c06..89d37abafe 100644 --- a/workspaces/website/package.json +++ b/workspaces/website/package.json @@ -24,6 +24,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-streaming": "^0.3.10", + "slugify": "^1.6.6", "vite": "^4.3.9", "vite-node": "0.30.1", "vite-plugin-ssr": "^0.4.131", diff --git a/workspaces/website/src/blocks/Block.tsx b/workspaces/website/src/blocks/Block.tsx index 5a795449b1..197ec1b03b 100644 --- a/workspaces/website/src/blocks/Block.tsx +++ b/workspaces/website/src/blocks/Block.tsx @@ -22,6 +22,8 @@ import VideoSectionBlock from "./VideoSectionBlock"; import { NewsletterCard } from "@ui/Card/NewsletterCard"; import { YoutubePlayer } from "@ui/YoutubePlayer/YoutubePlayer"; import NavbarStickyBanner from "../pages/(components)/NavbarStickyBanner/NavbarStickyBanner"; +import DappsPage from "src/pages/starknet-db-projects-dapps/(components)/DappsPage"; +import { getStarknetDappsDbProjects } from "@starknet-io/cms-data/src/starknet-db-projects-dapps"; import DisplayCardItems from "./DisplayCardItems"; export enum BlockPlacements { @@ -48,6 +50,7 @@ export function Block({ }: Props): JSX.Element | null { switch (placement) { case BlockPlacements.DEFAULT: + const pageContext = usePageContext(); switch (block.type) { case "basic_card": return ; @@ -172,9 +175,13 @@ export function Block({ darkTextColor={block.darkTextColor} /> ); + case "dapps": + const data = useAsync(["getBlockExplorers", locale], () => + getStarknetDappsDbProjects(pageContext.context) + ); + return ; case "home_hero": - const pageContext = usePageContext(); const homeSEO = useAsync(["getBlockExplorers", locale], () => getHomeSEO(locale, pageContext.context) ); diff --git a/workspaces/website/src/hooks/useQueryString.ts b/workspaces/website/src/hooks/useQueryString.ts new file mode 100644 index 0000000000..446c9af0a4 --- /dev/null +++ b/workspaces/website/src/hooks/useQueryString.ts @@ -0,0 +1,22 @@ +import { useEffect, useState } from "react"; + +export interface QueryParams { + [key: string]: string; +} + +const useQueryString = () => { + const [queryString, setQueryString] = useState({}); + + const url = new URL(window.location.href); + useEffect(() => { + const queryString = url.search; + const searchParams = new URLSearchParams(queryString); + + const paramsObject = Object.fromEntries(searchParams.entries()); + setQueryString(paramsObject); + }, [url.pathname]); + + return queryString; +}; + +export default useQueryString; diff --git a/workspaces/website/src/pages/starknet-db-projects-dapps/(components)/DappsPage.tsx b/workspaces/website/src/pages/starknet-db-projects-dapps/(components)/DappsPage.tsx new file mode 100644 index 0000000000..3c27017596 --- /dev/null +++ b/workspaces/website/src/pages/starknet-db-projects-dapps/(components)/DappsPage.tsx @@ -0,0 +1,223 @@ +import { + Box, + Heading, + Image, + Stack, + List, + ListItem, + HStack, + Circle, + Text, +} from "@chakra-ui/react"; +import { + DappsProps, + TagObject, +} from "@starknet-io/cms-data/src/starknet-db-projects-dapps"; +import { Button } from "@ui/Button"; +import { useEffect, useMemo, useState } from "react"; +import { Input } from "@chakra-ui/react"; +import { navigate } from "vite-plugin-ssr/client/router"; +import useQueryString from "src/hooks/useQueryString"; +import { IoSearchOutline } from "react-icons/io5"; + +enum SORT_BY { + ALL = "All", + MAINNET = "Mainnet", + TESTNET = "Testnet", +} + +const DappsPage = ({ list, categories }: DappsProps) => { + const [searchValue, setSearchValue] = useState(); + const [sortBy, setSortBy] = useState(null); + const [selectedCategory, setSelectedCategory] = useState< + TagObject | undefined + >(); + + const queryParams = useQueryString(); + + useEffect(() => { + const { category, sortBy, searchValue } = queryParams; + const fullCategory = categories.find((item) => item.slug === category); + setSelectedCategory(fullCategory); + setSortBy(sortBy); + setSearchValue(searchValue || ""); + }, [queryParams]); + + const url = new URL(window.location.href); + useEffect(() => { + if (!selectedCategory?.slug && !sortBy && !searchValue) { + navigate(url.pathname); + return; + } + + const queryParams = new URLSearchParams(); + selectedCategory && queryParams.set("category", selectedCategory.slug); + sortBy && queryParams.set("sortBy", sortBy); + searchValue && queryParams.set("searchValue", searchValue); + + navigate(`${url.pathname}?${queryParams.toString()}`, { + keepScrollPosition: true, + }); + }, [searchValue, sortBy, selectedCategory, url.pathname]); + const projects = useMemo(() => { + const byCategory = + selectedCategory !== undefined + ? list.filter((item) => item.tags.includes(selectedCategory.slug)) + : list; + if (!searchValue) return byCategory; + return byCategory.filter((item) => + item.name.toLocaleLowerCase().includes(searchValue.toLocaleLowerCase()) + ); + }, [selectedCategory, searchValue]); + + const projectsSort = useMemo(() => { + if (sortBy === SORT_BY.MAINNET) + return projects.filter((project) => project.isLive); + if (sortBy === SORT_BY.TESTNET) + return projects.filter((project) => project.isTestnetLive); + else return projects; + }, [sortBy, projects]); + + const handleChangeCategory = (category: string | undefined) => { + const fullCategory = categories.find((item) => item.slug === category); + setSelectedCategory(fullCategory); + }; + + return ( + <> + <> + + + + Categories + + + {selectedCategory === undefined && ( + + )} + + + {categories.map((category) => { + return ( + + {selectedCategory === category && ( + + )} + + + ); + })} + + + + + + {Object.values(SORT_BY).map((sortByItem) => ( + + ))} + + + + + setSearchValue(e.target.value)} + /> + + + + {projectsSort.map((item) => { + return ( + + + + + + {item.name} + + {item.description} + + ); + })} + + + + + + ); +}; + +export default DappsPage; diff --git a/workspaces/website/src/pages/starknet-db-projects-dapps/index.page.server.tsx b/workspaces/website/src/pages/starknet-db-projects-dapps/index.page.server.tsx new file mode 100644 index 0000000000..0bd3cdeea8 --- /dev/null +++ b/workspaces/website/src/pages/starknet-db-projects-dapps/index.page.server.tsx @@ -0,0 +1,16 @@ +import { PageContextServer } from "src/renderer/types"; +import { getDefaultPageContext } from "src/renderer/helpers"; +import { getStarknetDappsDbProjects } from "@starknet-io/cms-data/src/starknet-db-projects-dapps"; +export async function onBeforeRender(pageContext: PageContextServer) { + const defaultPageContext = await getDefaultPageContext(pageContext); + const starknetDappsDbProjects = await getStarknetDappsDbProjects( + pageContext.context + ); + + return { + pageContext: { + ...defaultPageContext, + starknetDappsDbProjects, + }, + }; +} diff --git a/workspaces/website/src/pages/starknet-db-projects-dapps/index.page.tsx b/workspaces/website/src/pages/starknet-db-projects-dapps/index.page.tsx new file mode 100644 index 0000000000..b8312dd64a --- /dev/null +++ b/workspaces/website/src/pages/starknet-db-projects-dapps/index.page.tsx @@ -0,0 +1 @@ +export { default as Page } from "./(components)/DappsPage"; diff --git a/yarn.lock b/yarn.lock index 05bdd13ca6..858e6fa4bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4750,6 +4750,7 @@ __metadata: react: ^18.2.0 react-dom: ^18.2.0 react-streaming: ^0.3.10 + slugify: ^1.6.6 vite: ^4.3.9 vite-node: 0.30.1 vite-plugin-ssr: ^0.4.131 @@ -17710,6 +17711,13 @@ __metadata: languageName: node linkType: hard +"slugify@npm:^1.6.6": + version: 1.6.6 + resolution: "slugify@npm:1.6.6" + checksum: 04773c2d3b7aea8d2a61fa47cc7e5d29ce04e1a96cbaec409da57139df906acb3a449fac30b167d203212c806e73690abd4ff94fbad0a9a7b7ea109a2a638ae9 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0"