Refactor GalleryCard to include a skeleton loading state

This commit is contained in:
monoid 2024-10-18 03:23:36 +09:00
parent e06787fb3f
commit a99b62a229
5 changed files with 114 additions and 61 deletions

View File

@ -5,6 +5,7 @@ import { Fragment, useLayoutEffect, useRef, useState } from "react";
import { LazyImage } from "./LazyImage.tsx";
import StyledLink from "./StyledLink.tsx";
import React from "react";
import { Skeleton } from "../ui/skeleton.tsx";
function clipTagsWhenOverflow(tags: string[], limit: number) {
let l = 0;
@ -87,4 +88,27 @@ function GalleryCardImpl({
</Card>;
}
export function GalleryCardSkeleton({
tagCount = 20
}: {
tagCount?: number;
}) {
return <Card className="flex h-[200px]">
<Skeleton className="rounded-xl overflow-hidden h-[200px] w-[142px] flex-none" />
<div className="flex-1 flex flex-col">
<CardHeader className="flex-none">
<Skeleton className="line-clamp-2 w-1/2 h-4" />
<Skeleton className="w-1/4 h-3" />
</CardHeader>
<CardContent className="flex-1 overflow-hidden">
<ul className="flex flex-wrap gap-2 items-baseline content-start">
{Array.from({ length: tagCount }).map((_, i) => <Skeleton key={i}
style={{ width: `${Math.random() * 100 + 50}px` }}
className="h-4" />)}
</ul>
</CardContent>
</div>
</Card>
}
export const GalleryCard = React.memo(GalleryCardImpl);

View File

@ -63,7 +63,7 @@ export function NavList() {
const loginInfo = useLogin();
const navItems = useNavItems();
return <aside className="h-dvh flex flex-col fixed">
return <aside className="h-dvh flex flex-col">
<nav className="flex flex-col items-center gap-4 px-2 sm:py-5 flex-1">
{navItems && <>{navItems} <Separator/> </>}
<NavItem icon={<SearchIcon className="h-5 w-5" />} to="/search" name="Search" />

View File

@ -2,11 +2,12 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { useGalleryDoc, useGalleryDocSimilar, useRehashDoc } from "../hook/useGalleryDoc.ts";
import TagBadge from "@/components/gallery/TagBadge";
import StyledLink from "@/components/gallery/StyledLink";
import { Link } from "wouter";
import { Link, useLocation } from "wouter";
import { classifyTags } from "../lib/classifyTags.tsx";
import { DescTagItem, DescItem } from "../components/gallery/DescItem.tsx";
import { Button } from "@/components/ui/button.tsx";
import { GalleryCard } from "@/components/gallery/GalleryCard.tsx";
import { useEffect, useRef } from "react";
export interface ContentInfoPageProps {
params: {
@ -14,12 +15,33 @@ export interface ContentInfoPageProps {
};
}
function Wrapper({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
const [pathname] = useLocation();
useEffect(() => {
if (ref.current) {
ref.current.scrollTo({
top: 0,
left: 0,
});
}
}, [pathname]);
return <div className="p-4 overflow-auto h-dvh" ref={ref}>
{children}
</div>;
}
export function ContentInfoPage({ params }: ContentInfoPageProps) {
const { data, error, isLoading } = useGalleryDoc(params.id);
const rehashDoc = useRehashDoc();
if (isLoading) {
return <div className="p-4">Loading...</div>
return <div className="p-4 flex items-center justify-center h-full">
<span className="animate-pulse text-4xl">
Loading...
</span>
</div>
}
if (error) {
@ -36,7 +58,7 @@ export function ContentInfoPage({ params }: ContentInfoPageProps) {
const contentLocation = `/doc/${params.id}/reader`;
return (
<div className="p-4">
<Wrapper>
<Link to={contentLocation}>
<div className="m-auto h-[400px] mb-4 flex justify-center items-center flex-none bg-[#272733]
rounded-xl shadow-lg overflow-hidden">
@ -48,10 +70,10 @@ export function ContentInfoPage({ params }: ContentInfoPageProps) {
</Link>
<Card className="flex-1 relative">
<div className="absolute top-0 right-0 p-2">
<Button variant="ghost" onClick={async () => {
<Button variant="ghost" onClick={async () => {
// Rehash
await rehashDoc(params.id);
}}>Rehash</Button>
}}>Rehash</Button>
</div>
<CardHeader>
<CardTitle>
@ -85,7 +107,7 @@ export function ContentInfoPage({ params }: ContentInfoPageProps) {
</CardContent>
</Card>
<SimilarContentCard id={params.id} />
</div>
</Wrapper>
);
}
@ -93,7 +115,7 @@ function SimilarContentCard({
id,
}: {
id: string;
}){
}) {
const { data, error, isLoading } = useGalleryDocSimilar(id);
if (isLoading) {
@ -112,9 +134,9 @@ function SimilarContentCard({
<div className="space-y-4 mt-4 mx-2">
<h2 className="text-2xl font-bold">Contents with Similar Tags</h2>
<div className="grid gap-4 grid-cols-[repeat(auto_fill_300px)]">
{data.map((doc) => (
<GalleryCard key={doc.id} doc={doc} />
))}
{data.map((doc) => (
<GalleryCard key={doc.id} doc={doc} />
))}
</div>
</div>
)

View File

@ -1,6 +1,6 @@
import { useLocation, useSearch } from "wouter";
import { Button } from "@/components/ui/button.tsx";
import { GalleryCard } from "@/components/gallery/GalleryCard.tsx";
import { GalleryCard, GalleryCardSkeleton } from "@/components/gallery/GalleryCard.tsx";
import TagBadge from "@/components/gallery/TagBadge.tsx";
import { useSearchGalleryInfinite } from "../hook/useSearchGallery.ts";
import { Spinner } from "../components/Spinner.tsx";
@ -35,8 +35,6 @@ export default function Gallery() {
overscan: 1,
});
const virtualItems = virtualizer.getVirtualItems();
useEffect(() => {
const lastItems = virtualItems.slice(-1);
@ -53,19 +51,56 @@ export default function Gallery() {
virtualizer.measure();
}, [virtualizer, data]);
if (isLoading) {
return <div className="p-4">Loading...</div>
}
if (error) {
return <div className="p-4">Error: {String(error)}</div>
}
if (!data) {
return <div className="p-4">No data</div>
const renderContent = () => {
if (!data) {
return null;
}
const isLoadingMore = data && size > 0 && (data[size - 1] === undefined);
const NoResult = data.reduce((acc, x) => acc + x.data.length, 0) === 0;
if (NoResult) {
return <div className="p-4 text-3xl">No results</div>
}
else {
return <div className="w-full relative overflow-auto h-full"
style={{ height: virtualizer.getTotalSize() }}>
{// TODO: date based grouping
virtualItems.map((item) => {
const isLoaderRow = item.index === size - 1 && isLoadingMore;
if (isLoaderRow) {
return <div key={item.index} className="w-full flex justify-center top-0 left-0 absolute"
style={{
height: `${item.size}px`,
transform: `translateY(${item.start}px)`
}}>
<Spinner />
</div>;
}
const docs = data[item.index];
if (!docs) return null;
return <div className="w-full grid gap-2 content-start top-0 left-0 absolute mb-2" key={item.index}
style={{
height: `${item.size}px`,
transform: `translateY(${item.start}px)`
}}>
{docs.startCursor && <div>
<h3 className="text-3xl">Start with {docs.startCursor}</h3>
<Separator />
</div>}
{docs?.data?.map((x) => {
return (
<GalleryCard doc={x} key={x.id} />
);
})}
</div>
})
}
</div>
}
}
const isLoadingMore = data && size > 0 && (data[size - 1] === undefined);
return (<div className="p-4 grid gap-2 overflow-auto h-dvh items-start content-start" ref={parentRef}>
return (<div className="p-4 grid gap-2 h-dvh overflow-auto items-start content-start" ref={parentRef}>
<Search />
{(word || tags) &&
<div className="bg-primary rounded-full p-1 sticky top-0 shadow-md z-10">
@ -75,41 +110,13 @@ export default function Gallery() {
</ul></span>}
</div>
}
{data?.length === 0 && <div className="p-4 text-3xl">No results</div>}
<div className="w-full relative"
style={{ height: virtualizer.getTotalSize() }}>
{// TODO: date based grouping
virtualItems.map((item) => {
const isLoaderRow = item.index === size - 1 && isLoadingMore;
if (isLoaderRow) {
return <div key={item.index} className="w-full flex justify-center top-0 left-0 absolute"
style={{
height: `${item.size}px`,
transform: `translateY(${item.start}px)`
}}>
<Spinner />
</div>;
}
const docs = data[item.index];
if (!docs) return null;
return <div className="w-full grid gap-2 content-start top-0 left-0 absolute mb-2" key={item.index}
style={{
height: `${item.size}px`,
transform: `translateY(${item.start}px)`
}}>
{docs.startCursor && <div>
<h3 className="text-3xl">Start with {docs.startCursor}</h3>
<Separator />
</div>}
{docs?.data?.map((x) => {
return (
<GalleryCard doc={x} key={x.id} />
);
})}
</div>
})
}
</div>
{error && <div className="p-4">Error: {String(error)}</div>}
{isLoading && <>
<GalleryCardSkeleton />
<GalleryCardSkeleton />
<GalleryCardSkeleton />
</>}
{renderContent()}
</div>
);
}

View File

@ -76,7 +76,7 @@ function ComicViewer({
}, [curPage, doc.id, totalPage]);
return (
<div className="overflow-hidden w-full h-full relative">
<div className="overflow-hidden w-full h-dvh relative">
<div className="absolute left-0 w-1/2 h-full z-10" onMouseDown={() => PageDown(1)} />
<img
ref={currentImageRef}