Refactor GalleryCard to include a skeleton loading state
This commit is contained in:
parent
e06787fb3f
commit
a99b62a229
@ -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);
|
@ -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" />
|
||||
|
@ -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">
|
||||
@ -85,7 +107,7 @@ export function ContentInfoPage({ params }: ContentInfoPageProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
<SimilarContentCard id={params.id} />
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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,30 +51,19 @@ 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>
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
if (!data) {
|
||||
return <div className="p-4">No data</div>
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
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}>
|
||||
<Search />
|
||||
{(word || tags) &&
|
||||
<div className="bg-primary rounded-full p-1 sticky top-0 shadow-md z-10">
|
||||
{word && <span className="text-primary-foreground ml-4">Search: {word}</span>}
|
||||
{tags && <span className="text-primary-foreground ml-4">Tags: <ul className="inline-flex gap-1">{
|
||||
tags.map(x => <TagBadge tagname={x} key={x} />)}
|
||||
</ul></span>}
|
||||
</div>
|
||||
const NoResult = data.reduce((acc, x) => acc + x.data.length, 0) === 0;
|
||||
|
||||
if (NoResult) {
|
||||
return <div className="p-4 text-3xl">No results</div>
|
||||
}
|
||||
{data?.length === 0 && <div className="p-4 text-3xl">No results</div>}
|
||||
<div className="w-full relative"
|
||||
else {
|
||||
return <div className="w-full relative overflow-auto h-full"
|
||||
style={{ height: virtualizer.getTotalSize() }}>
|
||||
{// TODO: date based grouping
|
||||
virtualItems.map((item) => {
|
||||
@ -110,6 +97,26 @@ export default function Gallery() {
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
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">
|
||||
{word && <span className="text-primary-foreground ml-4">Search: {word}</span>}
|
||||
{tags && <span className="text-primary-foreground ml-4">Tags: <ul className="inline-flex gap-1">{
|
||||
tags.map(x => <TagBadge tagname={x} key={x} />)}
|
||||
</ul></span>}
|
||||
</div>
|
||||
}
|
||||
{error && <div className="p-4">Error: {String(error)}</div>}
|
||||
{isLoading && <>
|
||||
<GalleryCardSkeleton />
|
||||
<GalleryCardSkeleton />
|
||||
<GalleryCardSkeleton />
|
||||
</>}
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user