Skip to content

Commit

Permalink
feat: groupchats pagination (#246)
Browse files Browse the repository at this point in the history
  • Loading branch information
geisterfurz007 authored Dec 3, 2023
1 parent b1daf02 commit 3dcba4a
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 52 deletions.
62 changes: 14 additions & 48 deletions packages/web/src/app/groupchats/components/group-chat-search.tsx
Original file line number Diff line number Diff line change
@@ -1,70 +1,34 @@
"use client";

import { FC, Fragment, useEffect, useState } from "react";
import { FC, Fragment, useState } from "react";
import { GroupChatPlatform, GroupChatResult } from "ui/groupchats";
import { GroupChatSearchBar } from "ui/groupchats/client";
import { SearchClient } from "typesense";
import {
ExclamationTriangleIcon,
InformationCircleIcon,
} from "@heroicons/react/20/solid";
import Link from "next/link";
import { navigateToLogin } from "../../../context/user/navigate-to-login";
import { useTypesense } from "../../../context/typesense";

type GroupchatResult = {
id: string;
name: string;
keywords: string[];
url: string;
description: string;
platform: GroupChatPlatform;
promoted: number;
};

const fetchResults = async (
queryText: string,
platforms: GroupChatPlatform[],
searchClient: SearchClient,
): Promise<GroupchatResult[]> => {
const filterBy =
platforms.length === 0 ? "" : `platform:[${platforms.join(",")}]`;

const { hits } = await searchClient
.collections<GroupchatResult>("groupchats")
.documents()
.search(
{
q: queryText,
query_by: "name,keywords,description",
filter_by: filterBy,
sort_by: "promoted:desc",
},
{},
);

return (
hits
?.map((h) => h.document)
.filter((x): x is GroupchatResult => "name" in x) ?? []
);
};
import { useGroupchatSearch } from "./use-groupchat-search";

export const GroupChatSearch: FC<{ isLoggedIn: boolean }> = ({
isLoggedIn,
}) => {
const [results, setResults] = useState<GroupchatResult[]>([]);
const { client } = useTypesense();
const [search, setSearch] = useState<{
query: string;
platforms: GroupChatPlatform[];
}>({ query: "", platforms: [] });

useEffect(() => {
fetchResults("", [], client).then(setResults);
}, [client]);
const { loading, groupchats } = useGroupchatSearch(
search.query,
search.platforms,
);

return (
<div className={"flex flex-col gap-4 max-w-4xl mx-auto"}>
<GroupChatSearchBar
onSearchChange={({ query, platforms }) =>
fetchResults(query, platforms, client).then(setResults)
setSearch({ query, platforms })
}
/>

Expand Down Expand Up @@ -104,12 +68,14 @@ export const GroupChatSearch: FC<{ isLoggedIn: boolean }> = ({
)}
<hr />

{results.map((r) => (
{groupchats.map((r) => (
<Fragment key={r.id}>
<GroupChatResult {...r} />
<div className={"h-px mx-4 bg-gray-100 min-w-max last:hidden"} />
</Fragment>
))}

{loading && "Loading..."}
</div>
</div>
);
Expand Down
129 changes: 129 additions & 0 deletions packages/web/src/app/groupchats/components/use-groupchat-search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { useState, useEffect, useCallback } from "react";
import { GroupChatPlatform } from "../../../ui/groupchats";
import { SearchClient } from "typesense";
import { useTypesense } from "../../../context/typesense";

export type GroupchatResult = {
id: string;
name: string;
keywords: string[];
url: string;
description: string;
platform: GroupChatPlatform;
promoted: number;
};

const pageSize = 15;

const fetchResults = async (
queryText: string,
platforms: GroupChatPlatform[],
searchClient: SearchClient,
page = 1,
): Promise<[GroupchatResult[], hasNext: boolean]> => {
const filterBy =
platforms.length === 0 ? "" : `platform:[${platforms.join(",")}]`;

const {
hits,
found,
page: returnedPage,
} = await searchClient
.collections<GroupchatResult>("groupchats")
.documents()
.search(
{
q: queryText,
query_by: "name,keywords,description",
filter_by: filterBy,
sort_by: "promoted:desc",
per_page: pageSize,
page,
},
{},
);

const results =
hits
?.map((h) => h.document)
.filter((x): x is GroupchatResult => "name" in x) ?? [];

const hasNext = returnedPage * pageSize < found;

return [results, hasNext];
};

export type UseGroupchatSearchReturn = {
groupchats: GroupchatResult[];
loading: boolean;
};

export const useGroupchatSearch = (
queryText: string,
platforms: GroupChatPlatform[],
): UseGroupchatSearchReturn => {
const { client } = useTypesense();

const [nextPage, setNextPage] = useState(1);
const [hasNextPage, setHasNextPage] = useState(true);
const [loading, setLoading] = useState(false);
const [chats, setChats] = useState<GroupchatResult[]>([]);

const fetchMore = useCallback(
async (mode: "append" | "replace") => {
if (loading || !hasNextPage) return;

setLoading(true);
const [newChats, hasNext] = await fetchResults(
queryText,
platforms,
client,
nextPage,
);

setChats((chats) =>
mode === "replace" ? newChats : [...chats, ...newChats],
);
setHasNextPage(hasNext);
if (hasNext) setNextPage(nextPage + 1);
setLoading(false);
},
[nextPage, hasNextPage, loading, queryText, platforms, client],
);

useEffect(() => {
setNextPage(1);
void fetchMore("replace");
}, [queryText, platforms, client]);

useEffect(() => {
if (loading) return;

let tripped = false;

const scrollListener = () => {
const scrollTarget = document.scrollingElement as HTMLHtmlElement | null;
if (!scrollTarget || tripped) return;

const footerHeight = document.querySelector("footer")?.clientHeight ?? 0;
const loadingOffset = footerHeight + 150;

const { scrollTop, scrollHeight, clientHeight } = scrollTarget;

if (scrollTop + clientHeight + loadingOffset >= scrollHeight) {
console.log(`Scroll triggered for nextPage ${nextPage}`);
tripped = true;
void fetchMore("append");
}
};

document.addEventListener("scroll", scrollListener);

return () => document.removeEventListener("scroll", scrollListener);
}, [fetchMore, loading, nextPage]);

return {
loading,
groupchats: chats,
};
};
4 changes: 2 additions & 2 deletions packages/web/src/ui/common/footer/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ export type FooterProps = { links: FooterLinkDefinition[] };

export const Footer: FC<FooterProps> = ({ links }) => {
return (
<div className="flex flex-col bg-white py-4 items-center gap-2">
<footer className="flex flex-col bg-white py-4 items-center gap-2">
<Logo />
<FooterLinkRow links={links} />
<AffiliationExclusion />
<Copyright />
</div>
</footer>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ export type SearchInputProps = {
onChange: (newValue: string) => void;
};

// TODO pagination

export const SearchInput: FC<SearchInputProps> = ({ onChange }) => {
const inputRef = useRef<HTMLInputElement>(null);
const [search, setSearch] = useState("");
Expand Down

0 comments on commit 3dcba4a

Please sign in to comment.