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 { LazyImage } from "./LazyImage.tsx";
|
||||||
import StyledLink from "./StyledLink.tsx";
|
import StyledLink from "./StyledLink.tsx";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Skeleton } from "../ui/skeleton.tsx";
|
||||||
|
|
||||||
function clipTagsWhenOverflow(tags: string[], limit: number) {
|
function clipTagsWhenOverflow(tags: string[], limit: number) {
|
||||||
let l = 0;
|
let l = 0;
|
||||||
@ -87,4 +88,27 @@ function GalleryCardImpl({
|
|||||||
</Card>;
|
</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);
|
export const GalleryCard = React.memo(GalleryCardImpl);
|
@ -63,7 +63,7 @@ export function NavList() {
|
|||||||
const loginInfo = useLogin();
|
const loginInfo = useLogin();
|
||||||
const navItems = useNavItems();
|
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">
|
<nav className="flex flex-col items-center gap-4 px-2 sm:py-5 flex-1">
|
||||||
{navItems && <>{navItems} <Separator/> </>}
|
{navItems && <>{navItems} <Separator/> </>}
|
||||||
<NavItem icon={<SearchIcon className="h-5 w-5" />} to="/search" name="Search" />
|
<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 { useGalleryDoc, useGalleryDocSimilar, useRehashDoc } from "../hook/useGalleryDoc.ts";
|
||||||
import TagBadge from "@/components/gallery/TagBadge";
|
import TagBadge from "@/components/gallery/TagBadge";
|
||||||
import StyledLink from "@/components/gallery/StyledLink";
|
import StyledLink from "@/components/gallery/StyledLink";
|
||||||
import { Link } from "wouter";
|
import { Link, useLocation } from "wouter";
|
||||||
import { classifyTags } from "../lib/classifyTags.tsx";
|
import { classifyTags } from "../lib/classifyTags.tsx";
|
||||||
import { DescTagItem, DescItem } from "../components/gallery/DescItem.tsx";
|
import { DescTagItem, DescItem } from "../components/gallery/DescItem.tsx";
|
||||||
import { Button } from "@/components/ui/button.tsx";
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
import { GalleryCard } from "@/components/gallery/GalleryCard.tsx";
|
import { GalleryCard } from "@/components/gallery/GalleryCard.tsx";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
export interface ContentInfoPageProps {
|
export interface ContentInfoPageProps {
|
||||||
params: {
|
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) {
|
export function ContentInfoPage({ params }: ContentInfoPageProps) {
|
||||||
const { data, error, isLoading } = useGalleryDoc(params.id);
|
const { data, error, isLoading } = useGalleryDoc(params.id);
|
||||||
const rehashDoc = useRehashDoc();
|
const rehashDoc = useRehashDoc();
|
||||||
|
|
||||||
if (isLoading) {
|
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) {
|
if (error) {
|
||||||
@ -36,7 +58,7 @@ export function ContentInfoPage({ params }: ContentInfoPageProps) {
|
|||||||
const contentLocation = `/doc/${params.id}/reader`;
|
const contentLocation = `/doc/${params.id}/reader`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<Wrapper>
|
||||||
<Link to={contentLocation}>
|
<Link to={contentLocation}>
|
||||||
<div className="m-auto h-[400px] mb-4 flex justify-center items-center flex-none bg-[#272733]
|
<div className="m-auto h-[400px] mb-4 flex justify-center items-center flex-none bg-[#272733]
|
||||||
rounded-xl shadow-lg overflow-hidden">
|
rounded-xl shadow-lg overflow-hidden">
|
||||||
@ -85,7 +107,7 @@ export function ContentInfoPage({ params }: ContentInfoPageProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<SimilarContentCard id={params.id} />
|
<SimilarContentCard id={params.id} />
|
||||||
</div>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +115,7 @@ function SimilarContentCard({
|
|||||||
id,
|
id,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
}){
|
}) {
|
||||||
const { data, error, isLoading } = useGalleryDocSimilar(id);
|
const { data, error, isLoading } = useGalleryDocSimilar(id);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useLocation, useSearch } from "wouter";
|
import { useLocation, useSearch } from "wouter";
|
||||||
import { Button } from "@/components/ui/button.tsx";
|
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 TagBadge from "@/components/gallery/TagBadge.tsx";
|
||||||
import { useSearchGalleryInfinite } from "../hook/useSearchGallery.ts";
|
import { useSearchGalleryInfinite } from "../hook/useSearchGallery.ts";
|
||||||
import { Spinner } from "../components/Spinner.tsx";
|
import { Spinner } from "../components/Spinner.tsx";
|
||||||
@ -35,8 +35,6 @@ export default function Gallery() {
|
|||||||
overscan: 1,
|
overscan: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const virtualItems = virtualizer.getVirtualItems();
|
const virtualItems = virtualizer.getVirtualItems();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const lastItems = virtualItems.slice(-1);
|
const lastItems = virtualItems.slice(-1);
|
||||||
@ -53,30 +51,19 @@ export default function Gallery() {
|
|||||||
virtualizer.measure();
|
virtualizer.measure();
|
||||||
}, [virtualizer, data]);
|
}, [virtualizer, data]);
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <div className="p-4">Loading...</div>
|
const renderContent = () => {
|
||||||
}
|
|
||||||
if (error) {
|
|
||||||
return <div className="p-4">Error: {String(error)}</div>
|
|
||||||
}
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return <div className="p-4">No data</div>
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const isLoadingMore = data && size > 0 && (data[size - 1] === undefined);
|
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}>
|
const NoResult = data.reduce((acc, x) => acc + x.data.length, 0) === 0;
|
||||||
<Search />
|
|
||||||
{(word || tags) &&
|
if (NoResult) {
|
||||||
<div className="bg-primary rounded-full p-1 sticky top-0 shadow-md z-10">
|
return <div className="p-4 text-3xl">No results</div>
|
||||||
{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>
|
|
||||||
}
|
}
|
||||||
{data?.length === 0 && <div className="p-4 text-3xl">No results</div>}
|
else {
|
||||||
<div className="w-full relative"
|
return <div className="w-full relative overflow-auto h-full"
|
||||||
style={{ height: virtualizer.getTotalSize() }}>
|
style={{ height: virtualizer.getTotalSize() }}>
|
||||||
{// TODO: date based grouping
|
{// TODO: date based grouping
|
||||||
virtualItems.map((item) => {
|
virtualItems.map((item) => {
|
||||||
@ -110,6 +97,26 @@ export default function Gallery() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ function ComicViewer({
|
|||||||
}, [curPage, doc.id, totalPage]);
|
}, [curPage, doc.id, totalPage]);
|
||||||
|
|
||||||
return (
|
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)} />
|
<div className="absolute left-0 w-1/2 h-full z-10" onMouseDown={() => PageDown(1)} />
|
||||||
<img
|
<img
|
||||||
ref={currentImageRef}
|
ref={currentImageRef}
|
||||||
|
Loading…
Reference in New Issue
Block a user