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 ? (
+
+ ) : debouncedValue ? (
+
+
+
+ ) : null}
+
+
+
+
+ {collections?.length ? (
+ <>
+
+
+
+ {collections.length} name collections queried
+
+
+ for{" "}
+
+ {debouncedValue.includes(".")
+ ? debouncedValue.split(".")[0]
+ : debouncedValue}
+
+ search
+
+
+
+ {/** Chart with query results ⬇️ */}
+
+
+ }
+ />
+
+ {
+ if (
+ viewBox &&
+ "cx" in viewBox &&
+ "cy" in viewBox
+ ) {
+ return (
+
+
+ {collections.reduce(
+ (accumulator, currentValue) =>
+ accumulator +
+ currentValue.number_of_names,
+ 0,
+ )}
+
+
+ Name ideas
+
+
+ );
+ }
+ }}
+ />
+
+
+
+
+
+ >
+ ) : 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 ? (
-
- ) : 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}
-
-
-
-
-
-
-
- );
-}
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 (
+