197 lines
6.8 KiB
TypeScript
197 lines
6.8 KiB
TypeScript
import { commands } from "@/commands.gen";
|
|
import { toLumeEvents } from "@/commons";
|
|
import { Spinner, User } from "@/components";
|
|
import { LumeWindow } from "@/system";
|
|
import { ArrowDown } from "@phosphor-icons/react";
|
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
import { nanoid } from "nanoid";
|
|
import type { NostrEvent } from "nostr-tools";
|
|
import { type RefObject, useCallback, useRef } from "react";
|
|
import { Virtualizer } from "virtua";
|
|
|
|
export const Route = createLazyFileRoute("/columns/_layout/discover-interests")(
|
|
{
|
|
component: Screen,
|
|
},
|
|
);
|
|
|
|
function Screen() {
|
|
const {
|
|
isLoading,
|
|
isError,
|
|
error,
|
|
isFetchingNextPage,
|
|
hasNextPage,
|
|
fetchNextPage,
|
|
data,
|
|
} = useInfiniteQuery({
|
|
queryKey: ["local-interests"],
|
|
initialPageParam: 0,
|
|
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
|
const until = pageParam > 0 ? pageParam.toString() : null;
|
|
const res = await commands.getAllLocalInterests(until);
|
|
|
|
if (res.status === "ok") {
|
|
const data = toLumeEvents(res.data);
|
|
return data;
|
|
} else {
|
|
throw new Error(res.error);
|
|
}
|
|
},
|
|
getNextPageParam: (lastPage) => {
|
|
const lastEvent = lastPage.at(-1);
|
|
|
|
if (lastEvent) {
|
|
return lastEvent.created_at - 1;
|
|
}
|
|
},
|
|
select: (data) =>
|
|
data?.pages
|
|
.flat()
|
|
.filter(
|
|
(item) => item.tags.filter((tag) => tag[0] === "t")?.length > 0,
|
|
),
|
|
refetchOnWindowFocus: false,
|
|
});
|
|
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
const renderItem = useCallback(
|
|
(item: NostrEvent) => {
|
|
const name =
|
|
item.tags.find((tag) => tag[0] === "title")?.[1] || "Unnamed";
|
|
const label =
|
|
item.tags.find((tag) => tag[0] === "label")?.[1] || nanoid();
|
|
|
|
return (
|
|
<div
|
|
key={item.id}
|
|
className="mb-3 group flex flex-col rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800"
|
|
>
|
|
<div className="px-2 pt-2">
|
|
<ScrollArea.Root
|
|
type={"scroll"}
|
|
scrollHideDelay={300}
|
|
className="overflow-hidden size-full"
|
|
>
|
|
<ScrollArea.Viewport className="p-3 h-16 bg-neutral-100 dark:bg-neutral-800 rounded-lg">
|
|
<div className="flex flex-wrap items-center justify-center gap-2">
|
|
{item.tags
|
|
.filter((tag) => tag[0] === "t")
|
|
.map((tag) => (
|
|
<div key={tag[1]} className="text-sm font-medium">
|
|
{tag[1].includes("#") ? tag[1] : `#${tag[1]}`}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ScrollArea.Viewport>
|
|
<ScrollArea.Scrollbar
|
|
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
|
orientation="vertical"
|
|
>
|
|
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
|
|
</ScrollArea.Scrollbar>
|
|
<ScrollArea.Corner className="bg-transparent" />
|
|
</ScrollArea.Root>
|
|
</div>
|
|
<div className="p-3 flex items-center justify-between">
|
|
<div className="inline-flex items-center gap-2">
|
|
<User.Provider pubkey={item.pubkey}>
|
|
<User.Root>
|
|
<User.Avatar className="size-7 rounded-full" />
|
|
</User.Root>
|
|
</User.Provider>
|
|
<h5 className="text-xs font-medium">{name}</h5>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
LumeWindow.openColumn({
|
|
label,
|
|
name,
|
|
url: `/columns/interests/${item.id}`,
|
|
})
|
|
}
|
|
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
|
|
>
|
|
Add
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
[data],
|
|
);
|
|
|
|
return (
|
|
<ScrollArea.Root
|
|
type={"scroll"}
|
|
scrollHideDelay={300}
|
|
className="overflow-hidden size-full"
|
|
>
|
|
<ScrollArea.Viewport ref={ref} className="relative h-full px-3 pb-3">
|
|
<Virtualizer scrollRef={ref as unknown as RefObject<HTMLElement>}>
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center h-20 gap-2">
|
|
<Spinner />
|
|
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
|
Loading event...
|
|
</p>
|
|
</div>
|
|
) : isError ? (
|
|
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/20">
|
|
<p className="text-xs text-center px-4 text-neutral-500 dark:text-neutral-400">
|
|
{error?.message ?? "Error"}
|
|
</p>
|
|
</div>
|
|
) : !data?.length ? (
|
|
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/20">
|
|
<p className="text-xs text-center px-4 text-neutral-500 dark:text-neutral-400">
|
|
Nothing to show yet, you can use Lume more and comeback lack to
|
|
see new events.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
data?.map((item) => renderItem(item))
|
|
)}
|
|
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/20">
|
|
<p className="text-xs text-center px-4 text-neutral-500 dark:text-neutral-400">
|
|
Lume running sync in the background,
|
|
<br />
|
|
the more you use the more event you see.
|
|
</p>
|
|
</div>
|
|
{hasNextPage ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => fetchNextPage()}
|
|
disabled={isFetchingNextPage || isLoading}
|
|
className="h-11 w-full px-3 flex items-center justify-center gap-1.5 bg-neutral-200/50 hover:bg-neutral-200 rounded-full text-sm font-medium text-blue-600 dark:hover:bg-neutral-800 dark:bg-neutral-800/50 dark:text-blue-400"
|
|
>
|
|
{isFetchingNextPage ? (
|
|
<Spinner className="size-4" />
|
|
) : (
|
|
<>
|
|
<ArrowDown className="size-4" />
|
|
Load more
|
|
</>
|
|
)}
|
|
</button>
|
|
) : null}
|
|
</Virtualizer>
|
|
</ScrollArea.Viewport>
|
|
<ScrollArea.Scrollbar
|
|
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
|
orientation="vertical"
|
|
>
|
|
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
|
|
</ScrollArea.Scrollbar>
|
|
<ScrollArea.Corner className="bg-transparent" />
|
|
</ScrollArea.Root>
|
|
);
|
|
}
|