Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add explorer #1555

Merged
merged 34 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f423957
chore: add explorer scaffolding
dominik-stumpf Nov 20, 2024
b5a8014
feat: add infinite scroll
dominik-stumpf Nov 21, 2024
f5ef465
feat: trigger fetch using observer
dominik-stumpf Nov 21, 2024
77f3e0d
feat: add search
dominik-stumpf Nov 21, 2024
8f90f27
feat: sync search using atoms
dominik-stumpf Nov 21, 2024
bc53faf
fix: prevent rerender by router
dominik-stumpf Nov 21, 2024
83f3260
fix: prevent render loop
dominik-stumpf Nov 21, 2024
42fb878
chore: separate useEffect calls
dominik-stumpf Nov 21, 2024
42785f7
feat: add ssr using tanstack query
dominik-stumpf Nov 21, 2024
23ed0ab
refactor: move out consts and fetchers
dominik-stumpf Nov 21, 2024
cb186a0
chore: align to review suggestions
dominik-stumpf Nov 21, 2024
b160182
chore: dont revalidate search
dominik-stumpf Nov 21, 2024
37563be
chore: replace guild card skeleton
dominik-stumpf Nov 21, 2024
e4a8099
refactor: move out guild card
dominik-stumpf Nov 21, 2024
0d57765
chore: move v2 search
dominik-stumpf Nov 21, 2024
d41eff8
chore: add header prereqs
dominik-stumpf Nov 21, 2024
30cfe33
chore: remove infinite scroll button
dominik-stumpf Nov 21, 2024
1693802
chore: add Toggle and ToggleGroup
dominik-stumpf Nov 21, 2024
2b19a9a
chore: hook sticky header together
dominik-stumpf Nov 21, 2024
8d5830f
feat: add sign in button to explorer
dominik-stumpf Nov 21, 2024
9bed877
fix: adjust navigation alignment
dominik-stumpf Nov 21, 2024
41e3959
fix: make dialog overlay properly
dominik-stumpf Nov 22, 2024
dc7e685
fix: make call to action responsive
dominik-stumpf Nov 22, 2024
afdeea0
chore: resolve merge conflict
dominik-stumpf Nov 22, 2024
398c40d
fix(getGuildSearch): throw on error
BrickheadJohnny Nov 22, 2024
22c187b
fix(InfiniteScrollGuilds): properly update the page param
BrickheadJohnny Nov 22, 2024
a9dea07
fix(GuildCard): don't show legacy guild logos
BrickheadJohnny Nov 22, 2024
ade648c
fix(StickyNavbar): use native scroll
BrickheadJohnny Nov 22, 2024
aec4645
fix(InfiniteScrollGuilds): revert latest change
BrickheadJohnny Nov 22, 2024
05dd1a0
fix(StickyNavbar): remove console.log
BrickheadJohnny Nov 22, 2024
b7164c2
fix: rename `_components` to `components`
BrickheadJohnny Nov 22, 2024
1f14a37
fix(Toggle): colors and variants
BrickheadJohnny Nov 22, 2024
4a9b333
fix: sign in buttom styling
BrickheadJohnny Nov 22, 2024
35dae18
final cleanup
BrickheadJohnny Nov 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions guild.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// dumping here types util it comes down properly

export {}

declare global {
type Guild = {
name: string;
id: string;
urlName: string;
createdAt: number;
updatedAt: number;
description: string;
imageUrl: string;
backgroundImageUrl: string;
visibility: Record<string, string>;
settings: Record<string, string>;
searchTags: string[];
categoryTags: string[];
socialLinks: Record<string, string>;
owner: string;
};

type PaginatedResponse<Item extends any> = {
page: number;
pageSize: number;
sortBy: string;
reverse: boolean;
searchQuery: string;
query: string;
items: Item[];
total: number;
};
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dependencies": {
"@hookform/resolvers": "^3.9.1",
"@phosphor-icons/react": "^2.1.7",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
Expand All @@ -26,6 +27,7 @@
"@radix-ui/react-switch": "^1.1.1",
"@t3-oss/env-nextjs": "^0.11.1",
"@tanstack/react-query": "^5.60.2",
"@tanstack/react-query-devtools": "^5.61.0",
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
Expand Down
3 changes: 3 additions & 0 deletions src/app/explorer/atoms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { atom } from "jotai";

export const searchAtom = atom<string | undefined>(undefined);
32 changes: 0 additions & 32 deletions src/app/explorer/components/GuildCard.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +0,0 @@
import { Badge } from "@/components/ui/Badge";
import { Card } from "@/components/ui/Card";
import { ImageSquare, Users } from "@phosphor-icons/react/dist/ssr";
import Link from "next/link";

export const GuildCard = () => {
return (
<Link href="#" className="rounded-2xl outline-none focus-visible:ring-4">
<Card className="relative grid grid-cols-[theme(space.12)_1fr] grid-rows-2 items-center gap-x-4 gap-y-0.5 overflow-hidden rounded-2xl px-6 py-7 shadow-md before:absolute before:inset-0 before:bg-black/5 before:opacity-0 before:transition-opacity before:duration-200 before:content-[''] hover:before:opacity-55 active:before:opacity-85 dark:before:bg-white/5">
<div className="row-span-2 grid size-12 place-items-center rounded-full bg-image text-white">
<ImageSquare weight="duotone" className="size-6" />
</div>

<h3 className="line-clamp-1 font-black font-display text-lg">
Sample guild
</h3>
<div className="flex flex-wrap gap-1">
<Badge>
<Users className="size-4" />
<span>
{new Intl.NumberFormat("en", {
notation: "compact",
}).format(125244)}
</span>
</Badge>

<Badge>5 groups</Badge>
</div>
</Card>
</Link>
);
};
82 changes: 82 additions & 0 deletions src/app/explorer/components/InfiniteScrollGuilds.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"use client";

import { GuildCard, GuildCardSkeleton } from "@/components/GuildCard";
import { Button } from "@/components/ui/Button";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useIntersection } from "foxact/use-intersection";
import { useAtomValue } from "jotai";
import { useCallback, useEffect } from "react";
import { searchAtom } from "../atoms";
import { PAGE_SIZE } from "../constants";
import { getGuildSearch } from "../fetchers";

export const InfiniteScrollGuilds = () => {
const search = useAtomValue(searchAtom);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
useInfiniteQuery({
queryKey: ["guilds", search || ""],
queryFn: getGuildSearch(search),
initialPageParam: 1,
staleTime: Number.POSITIVE_INFINITY,
enabled: search !== undefined,
getNextPageParam: (lastPage) =>
lastPage.total / lastPage.pageSize <= lastPage.page
? undefined
: lastPage.page + 1,
});

const [setIntersection, isIntersected, resetIsIntersected] = useIntersection({
rootMargin: "700px",
});

useEffect(() => {
if (!isFetchingNextPage) {
resetIsIntersected();
}
}, [resetIsIntersected, isFetchingNextPage]);

useEffect(() => {
if (isFetchingNextPage) return;
if (isIntersected && hasNextPage) {
fetchNextPage();
}
}, [isIntersected, hasNextPage, isFetchingNextPage, fetchNextPage]);

const guilds = data?.pages.flatMap((page) => page.items) || [];

return (
<section className="grid gap-2">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{isLoading
? Array.from({ length: PAGE_SIZE }, (_, i) => (
<GuildCardSkeleton key={i} />

Check notice on line 52 in src/app/explorer/components/InfiniteScrollGuilds.tsx

View workflow job for this annotation

GitHub Actions / quality-assurance

lint/suspicious/noArrayIndexKey

Avoid using the index of an array as key property in an element.
))
: guilds.map((guild) => <GuildCard key={guild.id} guild={guild} />)}
</div>
<div
ref={useCallback(
(element: HTMLDivElement | null) => {
setIntersection(element);
},
[setIntersection],
)}
aria-hidden
/>
{guilds.length === 0 &&
!isLoading &&
search &&
`No results for "${search}"`}
<Button
className="mt-8"
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? "Loading more..."
: hasNextPage
? "Load More"
: "No More Data"}
</Button>
</section>
);
};
43 changes: 43 additions & 0 deletions src/app/explorer/components/Search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use client";

import { Input } from "@/components/ui/Input";
import { useDebouncedValue } from "foxact/use-debounced-value";
import { useSetAtom } from "jotai";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { searchAtom } from "../atoms";

export const Search = () => {
const searchParams = useSearchParams();
const pathname = usePathname();
const [value, setValue] = useState(
searchParams?.get("search")?.toString() || "",
);

const debouncedValue = useDebouncedValue(value, 200);
const setSearch = useSetAtom(searchAtom);

useEffect(() => {

Check warning on line 20 in src/app/explorer/components/Search.tsx

View workflow job for this annotation

GitHub Actions / quality-assurance

lint/correctness/useExhaustiveDependencies

This hook does not specify all of its dependencies: setSearch
setSearch(debouncedValue);
}, [debouncedValue]);

useEffect(() => {

Check warning on line 24 in src/app/explorer/components/Search.tsx

View workflow job for this annotation

GitHub Actions / quality-assurance

lint/correctness/useExhaustiveDependencies

This hook does not specify all of its dependencies: pathname
const newSearchParams = new URLSearchParams(
Object.entries({ search: value }).filter(([_, value]) => value),
);
window.history.replaceState(
null,
"",
`${pathname}?${newSearchParams.toString()}`,
);
}, [value]);

return (
<Input
placeholder="Search guild.xyz"
size="lg"
value={value}
onChange={(e) => setValue(e.currentTarget.value)}
/>
);
};
1 change: 1 addition & 0 deletions src/app/explorer/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const PAGE_SIZE = 24;
12 changes: 12 additions & 0 deletions src/app/explorer/fetchers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { env } from "@/lib/env";
import { PAGE_SIZE } from "./constants";

export const getGuildSearch =
(search = "") =>
async ({ pageParam }: { pageParam: number }) => {
return (
await fetch(
`${env.NEXT_PUBLIC_API}/guild/search?page=${pageParam}&pageSize=${PAGE_SIZE}&search=${search}`,
)
).json() as Promise<PaginatedResponse<Guild>>;
};
119 changes: 113 additions & 6 deletions src/app/explorer/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,118 @@
import { GuildCard } from "./components/GuildCard";
import { AuthBoundary } from "@/components/AuthBoundary";
import { GuildCardSkeleton } from "@/components/GuildCard";
import { GuildCard } from "@/components/GuildCard";
import { Button } from "@/components/ui/Button";
import { env } from "@/lib/env";
import { Plus, SignIn } from "@phosphor-icons/react/dist/ssr";
import {
HydrationBoundary,
QueryClient,
dehydrate,
} from "@tanstack/react-query";
import { Suspense } from "react";
import { InfiniteScrollGuilds } from "./components/InfiniteScrollGuilds";
import { Search } from "./components/Search";
import { getGuildSearch } from "./fetchers";

const getAssociatedGuilds = async () => {
const request = `${env.NEXT_PUBLIC_API}/guild/search?page=1&pageSize=24&sortBy=name&reverse=false&search=`;
const guilds = (await (
await fetch(request)
).json()) as PaginatedResponse<Guild>;

return guilds;
};

export default async function Explorer() {
const queryClient = new QueryClient();
await queryClient.prefetchInfiniteQuery({
queryKey: ["guilds", ""],
initialPageParam: 1,
queryFn: getGuildSearch(""),
});

const Explorer = () => {
return (
<main className="container mx-auto grid max-w-screen-lg gap-4 px-4 py-8 sm:grid-cols-2 lg:grid-cols-3">
<GuildCard />
<main className="container mx-auto grid max-w-screen-lg gap-8 px-4 py-8">
<section className="pt-6 pb-8">
<h1 className="font-black text-5xl">Guildhall</h1>
</section>

<YourGuildsSection />

<section>
<Search />
</section>

<HydrationBoundary state={dehydrate(queryClient)}>
<InfiniteScrollGuilds />
</HydrationBoundary>
</main>
);
};
}

async function YourGuildsSection() {
return (
<section className="grid gap-2">
<h2 className="font-bold text-lg">Your guilds</h2>

<AuthBoundary
fallback={
<div className="flex items-center gap-4 rounded-2xl bg-card px-5 py-6">
<img src="/icons/robot.svg" alt="Guild Robot" className="size-8" />

export default Explorer;
<p className="font-semibold">
Sign in to view your guilds or create new ones
</p>

<Button
colorScheme="primary"
leftIcon={<SignIn weight="bold" />}
className="ml-auto h-10"
>
Sign in
</Button>
</div>
}
>
<Suspense fallback={<YourGuildsSkeleton />}>
<YourGuilds />
</Suspense>
</AuthBoundary>
</section>
);
}

async function YourGuilds() {
const { items: myGuilds } = await getAssociatedGuilds();

return myGuilds && myGuilds.length > 0 ? (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{myGuilds.map((guild) => (
<GuildCard key={guild.id} guild={guild} />
))}
</div>
) : (
<div className="flex items-center gap-4 rounded-2xl bg-card px-5 py-6">
<img src="/icons/robot.svg" alt="Guild Robot" className="size-8" />

<p className="font-semibold">
You&apos;re not a member of any guilds yet. Explore and join some below,
or create your own!
</p>

<Button leftIcon={<Plus weight="bold" />} className="ml-auto h-10">
Create guild
</Button>
</div>
);
}

function YourGuildsSkeleton() {
return (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{[...Array(3)].map((_, i) => (
<GuildCardSkeleton key={i} />

Check notice on line 114 in src/app/explorer/page.tsx

View workflow job for this annotation

GitHub Actions / quality-assurance

lint/suspicious/noArrayIndexKey

Avoid using the index of an array as key property in an element.
))}
</div>
);
}
Loading
Loading