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 23 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;
};
}
6 changes: 5 additions & 1 deletion 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 @@ -24,13 +25,16 @@
"@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@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",
"foxact": "^0.2.41",
"jotai": "^2.10.2",
"jotai": "^2.10.3",
"next": "15.0.3",
"next-themes": "^0.4.3",
"react": "19.0.0-rc-66855b96-20241106",
Expand Down
14 changes: 14 additions & 0 deletions public/images/robot.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions src/app/explorer/_components/CreateGuildLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use client";

import { buttonVariants } from "@/components/ui/Button";
import { Plus } from "@phosphor-icons/react";
import { useAtomValue } from "jotai";
import Link from "next/link";
import { isNavStuckAtom } from "../atoms";

export const CreateGuildLink = () => {
const isNavStuck = useAtomValue(isNavStuckAtom);
return (
<Link
href="/create-guild"
aria-label="Create guild"
prefetch={false}
className={buttonVariants({
variant: "ghost",
size: "sm",
className: [
// Temporarily, until we don't migrate the scrollable Tabs component
"min-h-11 w-11 gap-1.5 px-0 sm:min-h-0 sm:w-auto sm:px-3",
{
"text-white": !isNavStuck,
},
],
})}
>
<Plus />
<span className="hidden sm:inline-block">Create guild</span>
</Link>
);
};
Empty file.
23 changes: 23 additions & 0 deletions src/app/explorer/_components/HeaderBackground.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"use client";

import { cn } from "@/lib/cssUtils";
import { useAtomValue } from "jotai";
import { isNavStuckAtom, isSearchStuckAtom } from "../atoms";

export const HeaderBackground = () => {
const isNavStuck = useAtomValue(isNavStuckAtom);
const isSearchStuck = useAtomValue(isSearchStuckAtom);

return (
<div
className={cn(
"fixed inset-x-0 top-0 z-10 h-0 bg-card shadow-md transition-all duration-200 dark:bg-background",
{
"h-16": isNavStuck,
"h-[calc(theme(space.36)+theme(space.2))] bg-gradient-to-b from-card to-background sm:h-[calc(theme(space.28)-theme(space.2))] dark:from-background dark:to-card-secondary/50":
isSearchStuck,
},
)}
/>
);
};
78 changes: 78 additions & 0 deletions src/app/explorer/_components/InfiniteScrollGuilds.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"use client";

import { GuildCard, GuildCardSkeleton } from "@/components/GuildCard";
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} />
))
: 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 ? (
<p className="mt-6 text-center text-foreground-secondary">
`No results for "${search}"`
</p>
) : (
<p className="mt-6 text-center text-foreground-secondary">
{isFetchingNextPage
? "Loading more..."
: hasNextPage || "No More Data"}
</p>
)}
</section>
);
};
91 changes: 91 additions & 0 deletions src/app/explorer/_components/StickyNavbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"use client";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/ToggleGroup";
import useIsStuck from "@/hooks/useIsStuck";
import useScrollspy from "@/hooks/useScrollSpy";
import { cn } from "@/lib/cssUtils";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { type PropsWithChildren, useEffect } from "react";
import { activeSectionAtom, isNavStuckAtom, isSearchStuckAtom } from "../atoms";
import { ACTIVE_SECTION } from "../constants";

export const smoothScrollTo = (id: string) => {
const target = document.getElementById(id);

if (!target) return;

window.scrollTo({
behavior: "smooth",
top: target.offsetTop,
});
};

const Nav = () => {
const isNavStuck = useAtomValue(isNavStuckAtom);
const isSearchStuck = useAtomValue(isSearchStuckAtom);
const [activeSection, setActiveSection] = useAtom(activeSectionAtom);
const spyActiveSection = useScrollspy(Object.values(ACTIVE_SECTION), 100);
useEffect(() => {
if (!spyActiveSection) return;
setActiveSection(
spyActiveSection as (typeof ACTIVE_SECTION)[keyof typeof ACTIVE_SECTION],
);
}, [spyActiveSection, setActiveSection]);

return (
<ToggleGroup
type="single"
className="gap-2"
size={isSearchStuck ? "sm" : "lg"}
variant={isNavStuck ? "secondary" : "mono"}
onValueChange={(value) =>
value &&
setActiveSection(
value as (typeof ACTIVE_SECTION)[keyof typeof ACTIVE_SECTION],
)
}
value={activeSection}
>
<ToggleGroupItem
value={ACTIVE_SECTION.yourGuilds}
className={cn("rounded-xl transition-all", {
"rounded-lg": isSearchStuck,
})}
onClick={() => smoothScrollTo(ACTIVE_SECTION.yourGuilds)}
>
Your guilds
</ToggleGroupItem>
<ToggleGroupItem
value={ACTIVE_SECTION.exploreGuilds}
className={cn("rounded-xl transition-all", {
"rounded-lg": isSearchStuck,
})}
onClick={() => smoothScrollTo(ACTIVE_SECTION.exploreGuilds)}
>
Explore guilds
</ToggleGroupItem>
</ToggleGroup>
);
};

export const StickyNavbar = ({ children }: PropsWithChildren) => {
const setIsNavStuck = useSetAtom(isNavStuckAtom);
const isSearchStuck = useAtomValue(isSearchStuckAtom);
const { ref: navToggleRef } = useIsStuck(setIsNavStuck);

return (
<div
className={cn(
"sticky top-0 z-10 flex h-16 w-full items-center transition-all",
{
"h-12": isSearchStuck,
},
)}
ref={navToggleRef}
>
<div className="relative flex w-full items-center justify-between">
<Nav />
{children}
</div>
</div>
);
};
64 changes: 64 additions & 0 deletions src/app/explorer/_components/StickySearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use client";

import { Input } from "@/components/ui/Input";
import useIsStuck from "@/hooks/useIsStuck";
import { MagnifyingGlass } from "@phosphor-icons/react/dist/ssr";
import { useDebouncedValue } from "foxact/use-debounced-value";
import { useSetAtom } from "jotai";
import { usePathname, useSearchParams } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
import { isSearchStuckAtom, searchAtom } from "../atoms";

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(() => {
setSearch(debouncedValue);
}, [debouncedValue, setSearch]);

useEffect(() => {
const newSearchParams = new URLSearchParams(
Object.entries({ search: value }).filter(([_, value]) => value),
);
window.history.replaceState(
null,
"",
[pathname, newSearchParams.toString()].filter(Boolean).join("?"),
);
}, [value, pathname]);

return (
<div className="relative flex flex-col gap-3 sm:flex-row sm:gap-0">
<Input
className="relative h-12 grow rounded-xl border pr-6 pl-10 text-md"
size="lg"
placeholder="Search verified guilds"
onChange={({ currentTarget }) => setValue(currentTarget.value)}
value={value}
/>
<div className="absolute left-4 flex h-12 items-center justify-center">
<MagnifyingGlass className="text-foreground-secondary" />
</div>
</div>
);
};

export const StickySearch = () => {
const setIsSearchStuck = useSetAtom(isSearchStuckAtom);
const { ref: searchRef } = useIsStuck(setIsSearchStuck);

return (
<div className="sticky top-12 z-10" ref={searchRef}>
<Suspense>
<Search />
</Suspense>
</div>
);
};
9 changes: 9 additions & 0 deletions src/app/explorer/atoms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { atom } from "jotai";
import { ACTIVE_SECTION } from "./constants";

export const searchAtom = atom<string | undefined>(undefined);
export const isNavStuckAtom = atom(false);
export const isSearchStuckAtom = atom(false);
export const activeSectionAtom = atom<
(typeof ACTIVE_SECTION)[keyof typeof ACTIVE_SECTION]
>(ACTIVE_SECTION.yourGuilds);
Loading