From 08eaa19071efc9662436341daaf9154da304022f Mon Sep 17 00:00:00 2001 From: FrancoAguzzi Date: Tue, 24 Dec 2024 15:45:03 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20[SC-25978]=20=F0=9F=92=84=20Rebranded?= =?UTF-8?q?=20namegraph.dev=20apps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/namegraph.dev/app/collections/page.tsx | 265 +++++++++++++ .../app/explore-collections/page.tsx | 332 ---------------- apps/namegraph.dev/app/globals.css | 16 + apps/namegraph.dev/app/page.tsx | 32 +- .../app/{ideate => your-catalog}/page.tsx | 7 +- .../suggestion-category.tsx | 21 +- .../components/mini-apps/ideate/catalog.tsx | 5 +- apps/namegraph.dev/components/ui/card.tsx | 76 ++++ apps/namegraph.dev/components/ui/chart.tsx | 365 ++++++++++++++++++ apps/namegraph.dev/lib/utils.ts | 38 ++ 10 files changed, 782 insertions(+), 375 deletions(-) create mode 100644 apps/namegraph.dev/app/collections/page.tsx delete mode 100644 apps/namegraph.dev/app/explore-collections/page.tsx rename apps/namegraph.dev/app/{ideate => your-catalog}/page.tsx (90%) create mode 100644 apps/namegraph.dev/components/ui/card.tsx create mode 100644 apps/namegraph.dev/components/ui/chart.tsx diff --git a/apps/namegraph.dev/app/collections/page.tsx b/apps/namegraph.dev/app/collections/page.tsx new file mode 100644 index 000000000..e7424cbe8 --- /dev/null +++ b/apps/namegraph.dev/app/collections/page.tsx @@ -0,0 +1,265 @@ +"use client"; + +import { + NameGraphCollection, + NameGraphFindCollectionsResponse, +} from "@namehash/namegraph-sdk/utils"; +import { findCollectionsByString, getRandomColor } from "@/lib/utils"; +import { + ChartContainer, + ChartTooltip, + ChartTooltipContent, + type ChartConfig, +} from "@/components/ui/chart"; +import { DebounceInput } from "react-debounce-input"; +import { useEffect, useState } from "react"; +import { Label, Pie, PieChart } from "recharts"; +import { NumberedListIcon } from "@heroicons/react/24/solid"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +const chartConfig = { + number_of_names: { + label: "Number of names", + icon: NumberedListIcon, + }, +} satisfies ChartConfig; + +interface NameGraphCollectionInPieChart extends NameGraphCollection { + fill: string; +} + +export default function ExploreCollectionsPage() { + /** + * findCollectionsRes state: + * + * undefined is set when component never tried querying collections + * null is set when component tried querying collections but failed + * NameGraphFindCollectionsResponse is set when collections were successfully queried + */ + const [findCollectionsRes, setFindCollectionsRes] = useState< + undefined | null | NameGraphFindCollectionsResponse + >(undefined); + + useEffect(() => { + if (findCollectionsRes) { + let groupedCollections: NameGraphCollection[] = []; + + const otherCollections = findCollectionsRes.other_collections; + const relatedCollections = findCollectionsRes.related_collections; + + /** + * Unites "other" and "related" collections, from NameGraph SDK + */ + groupedCollections = [...otherCollections, ...relatedCollections]; + + /** + * Adds a random color for each collection for Pie Chart later displaying + */ + let coloredCollections: NameGraphCollectionInPieChart[] = []; + groupedCollections.forEach((_, idx) => { + const randomColor = getRandomColor(); + + coloredCollections.push({ + ...groupedCollections[idx], + fill: randomColor, + }); + }); + + setCollections(coloredCollections); + setLoadingCollections(false); + } else { + setCollections(undefined); + } + }, [findCollectionsRes]); + + /** + * collections state: + * + * undefined is set when component never tried querying collections + * null is set when no collections was retrieved from NameGraph SDK for debouncedValue + * NameGraphCollectionInPieChart[] is set when collections that were retrieved were grouped and state was set + */ + const [collections, setCollections] = useState< + undefined | null | NameGraphCollectionInPieChart[] + >(undefined); + + const [loadingCollections, setLoadingCollections] = useState(true); + + const [debouncedValue, setDebouncedValue] = useState(""); + + useEffect(() => { + if (debouncedValue) { + let query = debouncedValue; + if (debouncedValue.includes(".")) { + query = debouncedValue.split(".")[0]; + } + + setFindCollectionsRes(undefined); + setLoadingCollections(true); + findCollectionsByString(query) + .then((res) => setFindCollectionsRes(res)) + .catch(() => setFindCollectionsRes(null)); + } else { + setLoadingCollections(false); + } + }, [debouncedValue]); + + return ( +
+
+
+
+
+

+ 🔎 Discover the power of +400.000 name ideas 🌐 +

+

+ ✨ Do it with a single SDK call 📡 +

+
+

+ Imagine your are a user looking for a great profile name: +

+
+ {/** Query input ⬇️ */} + setDebouncedValue(e.target.value)} + className="w-full bg-gray-100 border border-gray-300 rounded-md p-3 px-4 placeholder:font-medium text-center" + /> + + {/** Collections query status icons ⬇️ */} +
+ {loadingCollections ? ( +
+ + Loading... +
+ ) : debouncedValue ? ( + + ) : null} +
+
+
+
+ {collections?.length ? ( + <> + + + + {collections.length} name collections queried + + + for{" "} + + {debouncedValue.includes(".") + ? debouncedValue.split(".")[0] + : debouncedValue} + + search + + + + {/** Chart with query results ⬇️ */} + + + } + /> + + + + + + + + ) : null} +
+
+
+
+ ); +} diff --git a/apps/namegraph.dev/app/explore-collections/page.tsx b/apps/namegraph.dev/app/explore-collections/page.tsx deleted file mode 100644 index 5ad38644a..000000000 --- a/apps/namegraph.dev/app/explore-collections/page.tsx +++ /dev/null @@ -1,332 +0,0 @@ -"use client"; - -import { - getCategoryID, - QuickJumpsByCategory, -} from "@/components/mini-apps/explore-collections/quick-jumps-by-category"; -import { SuggestionCategory } from "@/components/mini-apps/explore-collections/suggestion-category"; -import { - NameGraphFetchTopCollectionMembersResponse, - NameGraphGroupedByCategoryResponse, -} from "@namehash/namegraph-sdk/utils"; -import { getCollectionsForQuery } from "@/lib/utils"; -import { DebounceInput } from "react-debounce-input"; -import { useEffect, useState } from "react"; -import lodash, { debounce } from "lodash"; - -const SUGGESTION_CATEGORY_CLASSNAME = "suggestionCategory"; - -export default function ExploreCollectionsPage() { - /** - * nameIdeas state: - * - * undefined is set when component never tried querying name ideas - * null is set when component tried querying name ideas but failed - * NameGraphGroupedByCategoryResponse is set when name ideas were successfully queried - */ - const [nameIdeas, setNameIdeas] = useState< - undefined | null | NameGraphGroupedByCategoryResponse - >(undefined); - - const [nameIdeasLoading, setNameIdeasLoading] = useState(true); - - const [debouncedValue, setDebouncedValue] = useState(""); - - useEffect(() => { - if (debouncedValue) { - let query = debouncedValue; - if (debouncedValue.includes(".")) { - query = debouncedValue.split(".")[0]; - } - - setNameIdeas(undefined); - setNameIdeasLoading(true); - getCollectionsForQuery(query) - .then((res) => setNameIdeas(res)) - .catch(() => setNameIdeas(null)) - .finally(() => setNameIdeasLoading(false)); - } else { - setNameIdeasLoading(false); - } - }, [debouncedValue]); - - const [activeCategoryID, setActiveCategoryID] = useState(""); - - useEffect(() => { - if (nameIdeas?.categories.length && !activeCategoryID) { - setActiveCategoryID(getCategoryID(nameIdeas.categories[0])); - } - }, [nameIdeas?.categories]); - - const setFirstQuickJumpPillAsActive = () => { - const firstCollectionPill = document.querySelector(".collectionPill"); - const categoryID = firstCollectionPill?.getAttribute( - "data-navigation-item", - ); - - if (categoryID) { - setActiveCategoryID(categoryID); - } - }; - - const setActiveQuickJumpPill = () => { - const scrollableContainer = document.getElementById("scrollable-elm"); - - if (scrollableContainer) { - console.log(scrollableContainer.getBoundingClientRect()); - - // if the container was not yet scrolled - if ( - scrollableContainer.scrollTop < - scrollableContainer?.getBoundingClientRect().y || - !scrollableContainer.scrollTop - ) { - setFirstQuickJumpPillAsActive(); - return; - } - - const containerScrollTopPosition = - scrollableContainer?.getBoundingClientRect().top + - scrollableContainer?.scrollTop; - const containerScrollBottomPosition = - scrollableContainer?.getBoundingClientRect().bottom + - scrollableContainer?.scrollTop; - - const categories = document.querySelectorAll( - `.${SUGGESTION_CATEGORY_CLASSNAME}`, - ); - - categories.forEach((category, idx) => { - if (idx === 0) { - const firstCategoryBottom = category.getAttribute( - "data-category-bottom", - ); - const secondCategoryTop = - categories[idx + 1].getAttribute("data-category-top"); - - if ( - containerScrollTopPosition <= Number(firstCategoryBottom) || - containerScrollBottomPosition < Number(secondCategoryTop) - ) { - setActiveCategoryID(category.id); - } - } else { - const currentCategoryTop = category.getAttribute("data-category-top"); - const currentCategoryBottom = category.getAttribute( - "data-category-bottom", - ); - - if ( - containerScrollTopPosition <= Number(currentCategoryBottom) && - containerScrollTopPosition >= Number(currentCategoryTop) - ) { - setActiveCategoryID(category.id); - } - } - }); - - // if the container was scrolled to its bottom - if ( - scrollableContainer.scrollHeight && - scrollableContainer.scrollTop && - scrollableContainer.clientHeight + scrollableContainer.scrollTop >= - scrollableContainer.scrollHeight - ) { - const lastCategory = categories[categories.length - 1]; - - if (lastCategory) { - setActiveCategoryID(lastCategory.id); - } - } - } - }; - const ACTIVE_QUICK_JUMP_PILL_CLASSNAME = "activeQuickJumpPill"; - const clearActiveQuickJumpPills = () => { - const activeCollectionPills = document.querySelectorAll( - `.${ACTIVE_QUICK_JUMP_PILL_CLASSNAME}`, - ); - activeCollectionPills.forEach((pill) => { - pill.classList.remove(ACTIVE_QUICK_JUMP_PILL_CLASSNAME); - }); - }; - - useEffect(() => { - const wrapper = document.getElementById("scrollable-elm"); - - if (wrapper) { - wrapper.addEventListener( - "scroll", - lodash.debounce(setActiveQuickJumpPill, 100), - ); - wrapper.addEventListener( - "resize", - lodash.debounce(setActiveQuickJumpPill, 100), - ); - } - - return () => { - if (wrapper) { - wrapper.removeEventListener( - "scroll", - lodash.debounce(setActiveQuickJumpPill, 100), - ); - wrapper.removeEventListener( - "resize", - lodash.debounce(setActiveQuickJumpPill, 100), - ); - } - }; - }, []); - - useEffect(() => { - if (nameIdeas) { - setActiveQuickJumpPill(); - } - }, [nameIdeas]); - - useEffect(() => { - if (activeCategoryID) { - clearActiveQuickJumpPills(); - - const collectionPill = document.querySelector( - '[data-collection-pill="' + activeCategoryID + '"]', - ); - collectionPill?.classList.add(ACTIVE_QUICK_JUMP_PILL_CLASSNAME); - } - }, [activeCategoryID]); - - return ( -
-
-
-
-
-

- 🔎 Search for a name and see name ideas ⬇️ -

-
-
- setDebouncedValue(e.target.value)} - className="w-full bg-gray-100 border border-gray-300 rounded-md p-3 px-4" - /> - {nameIdeasLoading ? ( -
- - Loading... -
- ) : debouncedValue ? ( - - ) : null} -
-

- Use NameGraph SDK to generate multiple name ideas suggestions for - a single search. This works just as typing something like{" "} - {"'Batman'"} and getting back multiple name suggestions for - different categories related to Batman just as{" "} - {"'Batman Creators'"}, {"'Animated Batman Films'"},{" "} - {"'Batman Supporting Characters'"} and much more! -

-
-
-
-
- {/* - NameIdeas component uses two states for loading - state management: reqLoading and allSuggestionsLoading. - - reqLoading is used to manage the loading state of the - entire NameIdeas component, while allSuggestionsLoading - is used to manage the loading state of the QuickJumpsByCategory. - - QuickJumpsByCategory loading directly affects reqLoading state. - It needs to be displayed as soon as we have the categories - available, so we can calculate the need of navigation arrows - displaying. This is why we use allSuggestionsLoading state - to manage the loading state of QuickJumpsByCategory. - - Once QuickJumpsByCategory is ready (which means, once it - knows the suggestedCategories and it knows wether to - show navigation arrows or not) we update reqLoading, - syncing the loading state of the entire NameIdeas. - */} - {debouncedValue && (nameIdeasLoading || nameIdeas) ? ( - <> -
- -
- - ) : null} -
-
- {nameIdeas - ? nameIdeas.categories.map( - ( - category: NameGraphFetchTopCollectionMembersResponse, - idx: number, - ) => { - return ( -
- -
- ); - }, - ) - : null} -
-
-
-
-
-
-
-
- ); -} diff --git a/apps/namegraph.dev/app/globals.css b/apps/namegraph.dev/app/globals.css index 7c284fc2c..68e86dac0 100644 --- a/apps/namegraph.dev/app/globals.css +++ b/apps/namegraph.dev/app/globals.css @@ -25,6 +25,22 @@ @layer base { :root { --radius: 0.5rem; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + } + + .dark { + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } + + :root { } } diff --git a/apps/namegraph.dev/app/page.tsx b/apps/namegraph.dev/app/page.tsx index 37abec0eb..7db4032b9 100644 --- a/apps/namegraph.dev/app/page.tsx +++ b/apps/namegraph.dev/app/page.tsx @@ -5,32 +5,30 @@ import Link from "next/link"; export default function HomePage() { return (
-
+

- Welcome to NameGraph mini-apps + Acknowledge the power of NameGraph SDK

- Here you will be able to access different examples of how you can make - usage of NameGraph SDK + 📔 NameGraph makes it possible for your application to be connected to + hundreds of thousands of name ideas. +
+ 🔮 Boost the ideas of your users for profile naming or anything - make + it with a single SDK method call.

-
    +
      +

      Choose between

    • -

      ➡️

      - - Ideate +

      📊

      + + Using your own name ideas catalog
    • -

      ➡️

      - - Explore collections - -
    • -
    • -

      ➡️

      - - Randomize +

      📈

      + + Using NameGraph collections
    diff --git a/apps/namegraph.dev/app/ideate/page.tsx b/apps/namegraph.dev/app/your-catalog/page.tsx similarity index 90% rename from apps/namegraph.dev/app/ideate/page.tsx rename to apps/namegraph.dev/app/your-catalog/page.tsx index 2d5a20272..29fe5665c 100644 --- a/apps/namegraph.dev/app/ideate/page.tsx +++ b/apps/namegraph.dev/app/your-catalog/page.tsx @@ -37,12 +37,11 @@ export default function IdeatePage() {

    - Check out what's hot + Ideate over your own name ideas catalog:

    - Check out some suggestions from the community. -
    Edit the catalog in the right to constraint the results - sampled. + Edit the catalog in the right to constraint the results your users + will receive.

    diff --git a/apps/namegraph.dev/components/mini-apps/explore-collections/suggestion-category.tsx b/apps/namegraph.dev/components/mini-apps/explore-collections/suggestion-category.tsx index edef796af..06471019d 100644 --- a/apps/namegraph.dev/components/mini-apps/explore-collections/suggestion-category.tsx +++ b/apps/namegraph.dev/components/mini-apps/explore-collections/suggestion-category.tsx @@ -10,6 +10,7 @@ import { import { RecursiveRelatedCollectionPills } from "./recursive-related-collection-pills"; import { SuggestionCategoryHeader } from "./suggestion-category-header"; import Skeleton from "@/components/skeleton"; +import { getRandomColor } from "@/lib/utils"; interface SuggestionCategoryProps { category: NameGraphFetchTopCollectionMembersResponse; @@ -117,28 +118,10 @@ export const SuggestionCategory = ({ category }: SuggestionCategoryProps) => { }; }, []); - const customizedPillsColors = [ - "#E7DBF7", - "#1FA3C7", - "#FE097C", - "#FFBE00", - "#DB3D58", - "#01C69A", - "#8464CA", - "#E84233", - "#F5851E", - "#CBECEC", - "#FDE2CB", - "#F0C3F3", - ]; - const getRandomCustomizedPill = () => { const defaultClasses = "rounded-xl px-2.5 py-1 bg-opacity-70"; - const randomColor = - customizedPillsColors[ - Math.floor(Math.random() * customizedPillsColors.length) - ]; + const randomColor = getRandomColor(); return `bg-[${randomColor}] ${defaultClasses}`; }; diff --git a/apps/namegraph.dev/components/mini-apps/ideate/catalog.tsx b/apps/namegraph.dev/components/mini-apps/ideate/catalog.tsx index 909cee84f..aae76f9cd 100644 --- a/apps/namegraph.dev/components/mini-apps/ideate/catalog.tsx +++ b/apps/namegraph.dev/components/mini-apps/ideate/catalog.tsx @@ -37,7 +37,7 @@ export function Catalog({ onJsonChange }: CatalogProps) { return (
    -

    Writer's Block Catalog

    +

    A custom catalog

    📖 By modifying the below text you
    - can customize the catalog of name -
    collections you want to ideate around 🌏 + can customize the catalog you want to ideate around
    diff --git a/apps/namegraph.dev/components/ui/card.tsx b/apps/namegraph.dev/components/ui/card.tsx new file mode 100644 index 000000000..be021b080 --- /dev/null +++ b/apps/namegraph.dev/components/ui/card.tsx @@ -0,0 +1,76 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/apps/namegraph.dev/components/ui/chart.tsx b/apps/namegraph.dev/components/ui/chart.tsx new file mode 100644 index 000000000..5b6bae8c1 --- /dev/null +++ b/apps/namegraph.dev/components/ui/chart.tsx @@ -0,0 +1,365 @@ +"use client"; + +import * as React from "react"; +import * as RechartsPrimitive from "recharts"; + +import { cn } from "@/lib/utils"; + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const; + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode; + icon?: React.ComponentType; + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ); +}; + +type ChartContextProps = { + config: ChartConfig; +}; + +const ChartContext = React.createContext(null); + +function useChart() { + const context = React.useContext(ChartContext); + + if (!context) { + throw new Error("useChart must be used within a "); + } + + return context; +} + +const ChartContainer = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + config: ChartConfig; + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"]; + } +>(({ id, className, children, config, ...props }, ref) => { + const uniqueId = React.useId(); + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; + + return ( + +
    + + + {children} + +
    +
    + ); +}); +ChartContainer.displayName = "Chart"; + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([, config]) => config.theme || config.color, + ); + + if (!colorConfig.length) { + return null; + } + + return ( +