Skip to content

Commit

Permalink
Notrab/sc 26233/priority refinements for nameai app launch (#582)
Browse files Browse the repository at this point in the history
* bug fixes and ux improvements

* scrolling quicksearch

* more ux improvements

* fix footer link to nhl

* open in new tab

* changeset
  • Loading branch information
notrab authored Feb 2, 2025
1 parent 9a3d509 commit 57b15ac
Show file tree
Hide file tree
Showing 14 changed files with 320 additions and 56 deletions.
5 changes: 5 additions & 0 deletions .changeset/fifty-brooms-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@namehash/namekit-react": minor
---

add loading state to buttons
6 changes: 6 additions & 0 deletions apps/nameai.dev/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer utilities {
.pause-on-hover {
animation-play-state: paused;
}
}
3 changes: 2 additions & 1 deletion apps/nameai.dev/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,8 @@ export default function RootLayout({
</span>
<a
className="cursor-pointer text-black underline decoration-current underline-offset-[4px] transition-all duration-200 hover:underline-offset-[2px]"
href="/"
href="https://namehashlabs.org"
target="_blank"
>
NameHash Labs
</a>
Expand Down
126 changes: 126 additions & 0 deletions apps/nameai.dev/app/quicksearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"use client";

import React, { useEffect, useRef, useState } from "react";
import { Button, IconButton } from "@namehash/namekit-react";
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/solid";

interface QuickSearchProps {
handleClick: (example: string) => void;
}

export function QuickSearch({ handleClick }: QuickSearchProps) {
const sliderRef = useRef<HTMLDivElement>(null);
const [isAtStart, setIsAtStart] = useState<boolean>(true);
const [isAtEnd, setIsAtEnd] = useState<boolean>(false);

const examples = [
"namechain",
"ensserviceprovider",
"makotoinoue",
"myfamilyandi❤️all12months",
"spencecoin",
"chainlinkgod",
"ceresstation",
"garypalmerjr",
"spikewatanabe",
"fireeyesdao",
"ethlimo",
"ensecosystemworkinggroup",
"adamscochran",
"proofofattendanceprotocol",
];

useEffect(() => {
const checkScrollPosition = () => {
if (sliderRef.current) {
const { scrollLeft, scrollWidth, clientWidth } = sliderRef.current;
setIsAtStart(scrollLeft <= 60);
setIsAtEnd(scrollLeft + clientWidth >= scrollWidth);
}
};

const slider = sliderRef.current;
slider?.addEventListener("scroll", checkScrollPosition);

checkScrollPosition();

return () => {
slider?.removeEventListener("scroll", checkScrollPosition);
};
}, []);

const slideLeft = () => {
if (sliderRef.current) {
sliderRef.current.scrollLeft -= 350;
}
};

const slideRight = () => {
if (sliderRef.current) {
sliderRef.current.scrollLeft += 350;
}
};

return (
<div className="mb-10">
<p className="text-sm mb-3 text-center text-gray-500">
or try one of the example labels below
</p>

<div className="relative max-w-lg mx-auto h-9 w-full">
<div className="absolute top-0 z-20 inset-x-0 w-full h-full pointer-events-none shadow-[inset_45px_0_25px_-20px_rgba(249,250,251,0.97),inset_-45px_0_25px_-20px_rgba(249,250,251,0.97)]"></div>

<div
className="relative z-10 overflow-x-scroll scrollbar-hide whitespace-nowrap scroll-smooth w-full h-full"
ref={sliderRef}
>
{/* <div className="inline-flex items-center space-x-1 absolute left-0 w-full pl-6 pr-10 z-50 top-4">
{!isAtStart && (
<div className="md:hidden fixed left-0 z-50">
<IconButton onClick={slideLeft} variant="ghost" size="small">
<ChevronLeftIcon className="fill-current text-black w-5 h-5" />
</IconButton>
</div>
)}
{!isAtEnd && (
<div className="md:hidden fixed right-0 z-50">
<IconButton onClick={slideRight} variant="ghost" size="small">
<ChevronRightIcon className="fill-current text-black w-5 h-5" />
</IconButton>
</div>
)}
</div> */}
<div className="w-[200%] group flex flex-nowrap justify-center items-center space-x-1 animate-carousel hover:pause-on-hover">
{examples.map((example, index) => (
// @ts-expect-error
<Button
key={index}
variant="secondary"
size="small"
onClick={() => handleClick(example)}
className="!py-1 !px-2 text-sm"
>
{example}
</Button>
))}
{examples.map((example, index) => (
// @ts-expect-error
<Button
key={index}
variant="secondary"
size="small"
onClick={() => handleClick(example)}
className="!py-1 !px-2 text-sm"
>
{example}
</Button>
))}
<div className="w-5 flex-shrink-0 relative">
<span className="sr-only"></span>
</div>
</div>
</div>
</div>
</div>
);
}
72 changes: 43 additions & 29 deletions apps/nameai.dev/app/skeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,60 +8,74 @@ export function Skeleton({ label }: SkeletonProps) {
<div>
<h3 className="text-lg font-semibold mb-2">Label For Analysis</h3>

<p className="bg-white flex items-center p-3 border border-gray-300 rounded mb-3 shadow-sm ens-webfont">
<p className="bg-white flex items-center px-3 py-1.5 border border-gray-300 rounded mb-3 shadow-sm ens-webfont h-12">
{label}
</p>
</div>

<div>
<h3 className="text-lg font-semibold mb-2">Top Tokenization</h3>
<div className="bg-white flex items-center p-3 h-12 border border-gray-300 rounded mb-3 shadow-sm">
<div className="flex justify-between items-center mb-3">
<h3 className="text-lg font-semibold">Recommended Tokenizations</h3>
<ProbabilityHeader />
</div>
<div className="bg-white flex items-center px-3 py-1.5 border border-gray-300 rounded mb-3 shadow-sm h-12">
<div className="animate-pulse flex space-between items-center w-full">
<div className="flex space-x-1.5">
<div className="h-5 bg-gray-200 rounded w-10"></div>
<div className="h-5 bg-gray-200 rounded w-12"></div>
<div className="h-5 bg-gray-200 rounded w-6"></div>
<div className="flex space-x-2">
<div className="bg-gray-200 rounded px-3 py-1 text-sm border font-semibold h-[28px] w-10"></div>
<div className="bg-gray-200 rounded px-3 py-1 text-sm border font-semibold h-[28px] w-16"></div>
<div className="bg-gray-200 rounded px-3 py-1 text-sm border font-semibold h-[28px] w-8"></div>
</div>
<div className="w-32 ml-auto h-5 bg-gray-200 rounded"></div>
<div className="w-24 ml-auto h-2 bg-gray-200 rounded"></div>
</div>
</div>
</div>

<div>
<h3 className="text-lg font-semibold mb-2">
Alternative Tokenizations
</h3>
<div className="bg-white flex items-center p-3 border border-gray-300 rounded mb-3 shadow-sm">
<div className="flex justify-between items-center mb-3">
<h3 className="text-lg font-semibold">Discovered Tokenizations</h3>
<ProbabilityHeader />
</div>
<div className="bg-white flex items-center px-3 py-1.5 border border-gray-300 rounded mb-3 shadow-sm h-12">
<div className="animate-pulse flex space-between items-center w-full">
<div className="flex space-x-1.5">
<div className="h-5 bg-gray-200 rounded w-10"></div>
<div className="h-5 bg-gray-200 rounded w-12"></div>
<div className="h-5 bg-gray-200 rounded w-6"></div>
<div className="flex space-x-2">
<div className="bg-gray-200 rounded px-3 py-1 text-sm border font-semibold h-[28px] w-8"></div>
<div className="bg-gray-200 rounded px-3 py-1 text-sm border font-semibold h-[28px] w-16"></div>
<div className="bg-gray-200 rounded px-3 py-1 text-sm border font-semibold h-[28px] w-12"></div>
<div className="bg-gray-200 rounded px-3 py-1 text-sm border font-semibold h-[28px] w-8"></div>
</div>
<div className="w-32 ml-auto h-5 bg-gray-200 rounded"></div>
<div className="w-24 ml-auto h-2 bg-gray-200 rounded"></div>
</div>
</div>
<div className="bg-white flex items-center p-3 border border-gray-300 rounded mb-3 shadow-sm">
<div className="bg-white flex items-center px-3 py-1.5 border border-gray-300 rounded mb-3 shadow-sm h-12">
<div className="animate-pulse flex space-between items-center w-full">
<div className="flex space-x-1.5">
<div className="h-5 bg-gray-200 rounded w-10"></div>
<div className="h-5 bg-gray-200 rounded w-12"></div>
<div className="h-5 bg-gray-200 rounded w-6"></div>
<div className="flex space-x-2">
<div className="bg-gray-200 rounded px-3 py-1 text-sm border font-semibold h-[28px] w-10"></div>
<div className="bg-gray-200 rounded px-3 py-1 text-sm border font-semibold h-[28px] w-16"></div>
<div className="bg-gray-200 rounded px-3 py-1 text-sm border font-semibold h-[28px] w-20"></div>
<div className="bg-gray-200 rounded px-3 py-1 text-sm border font-semibold h-[28px] w-8"></div>
</div>
<div className="w-32 ml-auto h-5 bg-gray-200 rounded"></div>
<div className="w-24 ml-auto h-2 bg-gray-200 rounded"></div>
</div>
</div>
<div className="bg-white flex items-center p-3 border border-gray-300 rounded mb-3 shadow-sm">
<div className="bg-white flex items-center px-3 py-1.5 border border-gray-300 rounded mb-3 shadow-sm h-12">
<div className="animate-pulse flex space-between items-center w-full">
<div className="flex space-x-1.5">
<div className="h-5 bg-gray-200 rounded w-10"></div>
<div className="h-5 bg-gray-200 rounded w-12"></div>
<div className="h-5 bg-gray-200 rounded w-6"></div>
<div className="flex space-x-2">
<div className="bg-gray-200 rounded px-3 py-1 text-sm border font-semibold h-[28px] w-8"></div>
<div className="bg-gray-200 rounded px-3 py-1 text-sm border font-semibold h-[28px] w-20"></div>
<div className="bg-gray-200 rounded px-3 py-1 text-sm border font-semibold h-[28px] w-16"></div>
</div>
<div className="w-32 ml-auto h-5 bg-gray-200 rounded"></div>
<div className="w-24 ml-auto h-2 bg-gray-200 rounded"></div>
</div>
</div>
</div>
</div>
);
}

export function ProbabilityHeader() {
return (
<span className="text-gray-500 text-xs uppercase select-none">
Probability
</span>
);
}
3 changes: 1 addition & 2 deletions apps/nameai.dev/app/sort/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,12 @@ export function Form({
<Input
type="text"
name="label"
placeholder="Add a label"
placeholder="Enter a label"
className="ens-webfont flex-1"
required
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
autoComplete="off"
data-1p-ignore
error={clientError ?? ""}
/>
<Button type="submit" disabled={isSubmitDisabled} className="!py-1.5">
Expand Down
4 changes: 2 additions & 2 deletions apps/nameai.dev/app/sort/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import type { LabelItem } from "./page";

export function SortedList({ labels }: { labels: LabelItem[] }) {
return (
<ul className="space-y-2">
<ul className="space-y-3">
{labels?.map((item) => (
<li
key={item.label}
className="flex items-center justify-between bg-white p-3 rounded shadow"
className="flex items-center justify-between bg-white p-3 border border-gray-300 rounded shadow-sm"
>
<span className="font-bold">{item.label}</span>
<div className="flex items-center gap-2">
Expand Down
46 changes: 38 additions & 8 deletions apps/nameai.dev/app/tokenization/form.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"use client";

import { useActionState, useState, useEffect } from "react";
import { useActionState, useState, useEffect, useRef } from "react";
import { Input, Button } from "@namehash/namekit-react";
import { ens_normalize } from "@adraffy/ens-normalize";
import { toast } from "sonner";

import { analyzeLabel } from "./actions";
import { Results } from "./results";
import { Skeleton } from "../skeleton";
import { QuickSearch } from "../quicksearch";

type ActionState = {
error?: string;
Expand All @@ -29,18 +31,31 @@ export function Form({ initialValue }: { initialValue?: string }) {
analyzeLabel,
initialState,
);
const [normalizedLabel, setNormalizedLabel] = useState("");
const formRef = useRef<HTMLFormElement>(null);

const handleExampleClick = (example: string) => {
setInputValue(example);
setTimeout(() => {
if (formRef.current) {
formRef.current.requestSubmit();
}
}, 0);
};

useEffect(() => {
if (inputValue) {
let error = null;
let normalized = "";

if (inputValue.includes(" ")) {
error = "Remove spaces from the label";
} else if (inputValue.includes(".")) {
error = "Enter only 1 label (no '.')";
} else {
try {
ens_normalize(inputValue);
normalized = ens_normalize(inputValue);
setNormalizedLabel(normalized);
setClientError(null);
} catch (error) {
error = "Enter a valid ENS label value";
Expand All @@ -50,35 +65,50 @@ export function Form({ initialValue }: { initialValue?: string }) {
setClientError(error);
} else {
setClientError(null);
setNormalizedLabel("");
}
}, [inputValue]);

useEffect(() => {
if (state.error) {
toast.error(state.error);
}
}, [state.error]);

const isSubmitDisabled =
isPending || !!clientError || inputValue.trim() === "";

return (
<>
<form action={formAction} className="mb-6">
<form ref={formRef} action={formAction} className="mb-6">
<div className="flex items-start justify-center space-x-2 flex-1">
<Input
type="text"
name="label"
placeholder="Add a label"
placeholder="Enter a label"
className="ens-webfont flex-1"
required
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
autoComplete="off"
data-1p-ignore
error={clientError ?? ""}
/>
<Button type="submit" disabled={isSubmitDisabled} className="!py-1.5">
{isPending ? "Analyzing..." : "Analyze"}
<Button
type="submit"
// disabled={isSubmitDisabled}
className="!py-1.5"
loading={isPending}
>
Analyze
</Button>
</div>
</form>

{isPending && <Skeleton label={inputValue} />}
<div className="mb-8">
<QuickSearch handleClick={handleExampleClick} />
</div>

{isPending && <Skeleton label={normalizedLabel} />}

{!isPending && state.success && state.label && state.analysis && (
// @ts-expect-error
Expand Down
Loading

0 comments on commit 57b15ac

Please sign in to comment.