feat: add loading states to home page
This commit is contained in:
parent
ee47a05f9f
commit
db94d2480d
16
src/components/ui/spinner.tsx
Normal file
16
src/components/ui/spinner.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Loader2Icon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
||||||
|
return (
|
||||||
|
<Loader2Icon
|
||||||
|
role="status"
|
||||||
|
aria-label="Loading"
|
||||||
|
className={cn("size-4 animate-spin", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Spinner }
|
||||||
40
src/features/home/components/MangaLoadingState.tsx
Normal file
40
src/features/home/components/MangaLoadingState.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import { useRef } from "react";
|
||||||
|
|
||||||
|
const CHEEKY_MESSAGES = [
|
||||||
|
"Sharpening katanas and downloading manga...",
|
||||||
|
"Searching for the One Piece... and your titles.",
|
||||||
|
"Summoning the Great Sage for faster loading...",
|
||||||
|
"Powering up to Super Saiyan level... please wait.",
|
||||||
|
"Collecting all seven Dragon Balls to fetch data...",
|
||||||
|
"Even Saitama takes a second to load... sometimes.",
|
||||||
|
"Naruto is training, wait for the results...",
|
||||||
|
"Loading... because we don't have a Death Note for bugs.",
|
||||||
|
"Entering the Hidden Leaf Village... of data.",
|
||||||
|
"Waiting for the next chapter... and your results.",
|
||||||
|
"Training in the Hyperbolic Time Chamber for better speed...",
|
||||||
|
"Collecting chakra for the ultimate data retrieval...",
|
||||||
|
"Waiting for the next hiatus to end... oh wait, just loading.",
|
||||||
|
"Reading the manga faster than you can... hold on.",
|
||||||
|
"Asking the Shinigami for the right data...",
|
||||||
|
"Is this a Jojo reference? No, it's just loading.",
|
||||||
|
"Surpassing our limits... Right here! Right now!",
|
||||||
|
"Hunting for the rarest manga volumes in the digital world...",
|
||||||
|
"Dodging spoilers while fetching your manga...",
|
||||||
|
"Preparing the transmutation circle for your results...",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MangaLoadingState = () => {
|
||||||
|
const loadingMessage = useRef(
|
||||||
|
CHEEKY_MESSAGES[Math.floor(Math.random() * CHEEKY_MESSAGES.length)],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center gap-4">
|
||||||
|
<Spinner className="size-12 text-primary" />
|
||||||
|
<p className="animate-pulse text-muted-foreground font-medium text-center px-4">
|
||||||
|
{loadingMessage.current}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -3,12 +3,14 @@ import { useEffect, useRef } from "react";
|
|||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import { useGetMangas } from "@/api/generated/catalog/catalog.ts";
|
import { useGetMangas } from "@/api/generated/catalog/catalog.ts";
|
||||||
import { AuthHeader } from "@/components/AuthHeader.tsx";
|
import { AuthHeader } from "@/components/AuthHeader.tsx";
|
||||||
|
import { Spinner } from "@/components/ui/spinner.tsx";
|
||||||
import { Pagination } from "@/components/Pagination.tsx";
|
import { Pagination } from "@/components/Pagination.tsx";
|
||||||
import { ThemeToggle } from "@/components/ThemeToggle.tsx";
|
import { ThemeToggle } from "@/components/ThemeToggle.tsx";
|
||||||
import { Input } from "@/components/ui/input.tsx";
|
import { Input } from "@/components/ui/input.tsx";
|
||||||
import { useUIState } from "@/contexts/UIStateContext.tsx";
|
import { useUIState } from "@/contexts/UIStateContext.tsx";
|
||||||
import { FilterSidebar } from "@/features/home/components/FilterSidebar.tsx";
|
import { FilterSidebar } from "@/features/home/components/FilterSidebar.tsx";
|
||||||
import { MangaGrid } from "@/features/home/components/MangaGrid.tsx";
|
import { MangaGrid } from "@/features/home/components/MangaGrid.tsx";
|
||||||
|
import { MangaLoadingState } from "@/features/home/components/MangaLoadingState.tsx";
|
||||||
import { SortDropdown } from "@/features/home/components/SortDropdown.tsx";
|
import { SortDropdown } from "@/features/home/components/SortDropdown.tsx";
|
||||||
import { useDynamicPageSize } from "@/hooks/useDynamicPageSize.ts";
|
import { useDynamicPageSize } from "@/hooks/useDynamicPageSize.ts";
|
||||||
|
|
||||||
@ -37,7 +39,12 @@ const Home = () => {
|
|||||||
|
|
||||||
const [debouncedSearchText] = useDebounce(searchText, 500);
|
const [debouncedSearchText] = useDebounce(searchText, 500);
|
||||||
|
|
||||||
const { data: mangasData, queryKey: mangasQueryKey } = useGetMangas({
|
const {
|
||||||
|
data: mangasData,
|
||||||
|
queryKey: mangasQueryKey,
|
||||||
|
isPending,
|
||||||
|
isFetching,
|
||||||
|
} = useGetMangas({
|
||||||
page: currentPage - 1,
|
page: currentPage - 1,
|
||||||
size: itemsPerPage,
|
size: itemsPerPage,
|
||||||
sort: ["id"],
|
sort: ["id"],
|
||||||
@ -75,7 +82,7 @@ const Home = () => {
|
|||||||
onShowAdultContentChange={setShowAdultContent}
|
onShowAdultContentChange={setShowAdultContent}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1 flex flex-col">
|
||||||
<header className="border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
<header className="border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||||
<div className="px-8 py-6">
|
<div className="px-8 py-6">
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
@ -86,9 +93,14 @@ const Home = () => {
|
|||||||
<h1 className="text-3xl font-bold tracking-tight text-foreground">
|
<h1 className="text-3xl font-bold tracking-tight text-foreground">
|
||||||
MangaMochi
|
MangaMochi
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
<div className="mt-1 flex items-center gap-2">
|
||||||
{mangasData?.data?.totalElements} titles available
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{mangasData?.data?.totalElements ?? 0} titles available
|
||||||
</p>
|
</p>
|
||||||
|
{isFetching && (
|
||||||
|
<Spinner className="size-3 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
@ -114,8 +126,10 @@ const Home = () => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="px-8 py-8">
|
<main className="flex-1 px-8 py-8 flex flex-col">
|
||||||
{mangasData?.data?.content && mangasData.data.content.length > 0 ? (
|
{isPending ? (
|
||||||
|
<MangaLoadingState />
|
||||||
|
) : mangasData?.data?.content && mangasData.data.content.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{mangasData.data?.totalElements && (
|
{mangasData.data?.totalElements && (
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<div className="mb-6 flex items-center justify-between">
|
||||||
@ -149,7 +163,7 @@ const Home = () => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex min-h-[400px] items-center justify-center">
|
<div className="flex-1 flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-lg text-muted-foreground">
|
<p className="text-lg text-muted-foreground">
|
||||||
No manga found matching your filters.
|
No manga found matching your filters.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user