From db94d2480d77d2cbac22f3c098fe47becdb5f8b8 Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Sun, 5 Apr 2026 19:45:59 -0300 Subject: [PATCH 1/8] feat: add loading states to home page --- src/components/ui/spinner.tsx | 16 ++++++++ .../home/components/MangaLoadingState.tsx | 40 +++++++++++++++++++ src/pages/Home.tsx | 30 ++++++++++---- 3 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 src/components/ui/spinner.tsx create mode 100644 src/features/home/components/MangaLoadingState.tsx diff --git a/src/components/ui/spinner.tsx b/src/components/ui/spinner.tsx new file mode 100644 index 0000000..a70e713 --- /dev/null +++ b/src/components/ui/spinner.tsx @@ -0,0 +1,16 @@ +import { Loader2Icon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Spinner({ className, ...props }: React.ComponentProps<"svg">) { + return ( + + ) +} + +export { Spinner } diff --git a/src/features/home/components/MangaLoadingState.tsx b/src/features/home/components/MangaLoadingState.tsx new file mode 100644 index 0000000..1167f72 --- /dev/null +++ b/src/features/home/components/MangaLoadingState.tsx @@ -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 ( +
+ +

+ {loadingMessage.current} +

+
+ ); +}; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 97a586e..7bdd2c6 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -3,12 +3,14 @@ import { useEffect, useRef } from "react"; import { useDebounce } from "use-debounce"; import { useGetMangas } from "@/api/generated/catalog/catalog.ts"; import { AuthHeader } from "@/components/AuthHeader.tsx"; +import { Spinner } from "@/components/ui/spinner.tsx"; import { Pagination } from "@/components/Pagination.tsx"; import { ThemeToggle } from "@/components/ThemeToggle.tsx"; import { Input } from "@/components/ui/input.tsx"; import { useUIState } from "@/contexts/UIStateContext.tsx"; import { FilterSidebar } from "@/features/home/components/FilterSidebar.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 { useDynamicPageSize } from "@/hooks/useDynamicPageSize.ts"; @@ -37,7 +39,12 @@ const Home = () => { const [debouncedSearchText] = useDebounce(searchText, 500); - const { data: mangasData, queryKey: mangasQueryKey } = useGetMangas({ + const { + data: mangasData, + queryKey: mangasQueryKey, + isPending, + isFetching, + } = useGetMangas({ page: currentPage - 1, size: itemsPerPage, sort: ["id"], @@ -75,7 +82,7 @@ const Home = () => { onShowAdultContentChange={setShowAdultContent} /> -
+
@@ -86,9 +93,14 @@ const Home = () => {

MangaMochi

-

- {mangasData?.data?.totalElements} titles available -

+
+

+ {mangasData?.data?.totalElements ?? 0} titles available +

+ {isFetching && ( + + )} +
@@ -114,8 +126,10 @@ const Home = () => {
-
- {mangasData?.data?.content && mangasData.data.content.length > 0 ? ( +
+ {isPending ? ( + + ) : mangasData?.data?.content && mangasData.data.content.length > 0 ? ( <> {mangasData.data?.totalElements && (
@@ -149,7 +163,7 @@ const Home = () => { )} ) : ( -
+

No manga found matching your filters. -- 2.49.1 From 0ef91b6f6f5e26e568456b530609971f085aa263 Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Sun, 5 Apr 2026 19:49:33 -0300 Subject: [PATCH 2/8] chore: update gitignore file --- .env.local | 2 -- .gitignore | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 .env.local diff --git a/.env.local b/.env.local deleted file mode 100644 index c418598..0000000 --- a/.env.local +++ /dev/null @@ -1,2 +0,0 @@ -VITE_API_BASE_URL=http://localhost:8080 -VITE_OMV_BASE_URL=http://omv.badger-pirarucu.ts.net:9000/mangamochi-dev \ No newline at end of file diff --git a/.gitignore b/.gitignore index f23ba7c..7dbee46 100644 --- a/.gitignore +++ b/.gitignore @@ -222,7 +222,7 @@ web_modules/ .yarn-integrity # dotenv environment variable files -.env +.env* .env.development.local .env.test.local .env.production.local -- 2.49.1 From 451b45f2d37b3ff2ad612cf22324ba831ffdca53 Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Sun, 5 Apr 2026 20:00:30 -0300 Subject: [PATCH 3/8] feat: add collapsible filter sidebar with loading states and disabled inputs during fetch --- src/contexts/UIStateContext.tsx | 6 ++ .../home/components/FilterSidebar.tsx | 36 +++++++--- src/features/home/components/SortDropdown.tsx | 9 ++- src/pages/Home.tsx | 66 +++++++++++++------ 4 files changed, 86 insertions(+), 31 deletions(-) diff --git a/src/contexts/UIStateContext.tsx b/src/contexts/UIStateContext.tsx index 6aeca64..d60664b 100644 --- a/src/contexts/UIStateContext.tsx +++ b/src/contexts/UIStateContext.tsx @@ -26,6 +26,8 @@ interface UIStateContextType { setSortOption: (sort: SortOption) => void; searchText: string; setSearchText: (text: string) => void; + isSidebarOpen: boolean; + setIsSidebarOpen: (open: boolean) => void; resetFilters: () => void; /* Manga Provider Card State */ @@ -49,6 +51,7 @@ export const UIStateProvider = ({ children }: { children: ReactNode }) => { const [showAdultContent, setShowAdultContent] = useState(false); const [sortOption, setSortOption] = useState("title-asc"); const [searchText, setSearchText] = useState(""); + const [isSidebarOpen, setIsSidebarOpen] = useState(true); /* Manga Provider Card State */ const [expandedProviderIds, setExpandedProviderIds] = useState([]); @@ -104,6 +107,8 @@ export const UIStateProvider = ({ children }: { children: ReactNode }) => { setSortOption, searchText, setSearchText, + isSidebarOpen, + setIsSidebarOpen, resetFilters, expandedProviderIds, toggleProviderId, @@ -119,6 +124,7 @@ export const UIStateProvider = ({ children }: { children: ReactNode }) => { showAdultContent, sortOption, searchText, + isSidebarOpen, resetFilters, expandedProviderIds, toggleProviderId, diff --git a/src/features/home/components/FilterSidebar.tsx b/src/features/home/components/FilterSidebar.tsx index 0fb0aad..919222f 100644 --- a/src/features/home/components/FilterSidebar.tsx +++ b/src/features/home/components/FilterSidebar.tsx @@ -1,4 +1,4 @@ -import { Star, X } from "lucide-react"; +import { PanelLeftClose, Star, X } from "lucide-react"; import { useGetGenres } from "@/api/generated/catalog/catalog.ts"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -18,6 +18,8 @@ interface FilterSidebarProps { onRatingChange: (rating: number) => void; onUserFavoritesChange: (favorites: boolean) => void; onShowAdultContentChange: (showAdult: boolean) => void; + onHide: () => void; + isDisabled?: boolean; } const STATUSES = ["Ongoing", "Completed", "Hiatus"]; @@ -40,6 +42,8 @@ export function FilterSidebar({ onRatingChange, onUserFavoritesChange, onShowAdultContentChange, + onHide, + isDisabled = false, }: FilterSidebarProps) { const { data: genresData, isPending: isPendingGenres } = useGetGenres(); const { isAuthenticated } = useAuth(); @@ -79,14 +83,25 @@ export function FilterSidebar({

{/* Header */}
-

- Filters -

+
+ +

+ Filters +

+
{hasActiveFilters && (
@@ -119,6 +135,7 @@ export function FilterSidebar({
@@ -145,8 +162,8 @@ export function FilterSidebar({ isSelected ? "bg-sidebar-primary text-sidebar-primary-foreground hover:bg-sidebar-primary/90" : "border-sidebar-border text-sidebar-foreground hover:bg-sidebar-accent" - }`} - onClick={() => toggleGenre(genre.id)} + } ${isDisabled ? "opacity-50 pointer-events-none grayscale-[0.5]" : ""}`} + onClick={() => !isDisabled && toggleGenre(genre.id)} > {genre.name} {isSelected && } @@ -174,8 +191,8 @@ export function FilterSidebar({ isSelected ? "bg-sidebar-primary text-sidebar-primary-foreground hover:bg-sidebar-primary/90" : "border-sidebar-border text-sidebar-foreground hover:bg-sidebar-accent" - }`} - onClick={() => toggleStatus(status)} + } ${isDisabled ? "opacity-50 pointer-events-none grayscale-[0.5]" : ""}`} + onClick={() => !isDisabled && toggleStatus(status)} > {status} {isSelected && } @@ -203,7 +220,8 @@ export function FilterSidebar({ isSelected ? "bg-sidebar-accent text-sidebar-accent-foreground" : "text-sidebar-foreground hover:bg-sidebar-accent/50" - }`} + } ${isDisabled ? "opacity-50 pointer-events-none" : ""}`} + disabled={isDisabled} > void; + isDisabled?: boolean; } export const SortDropdown = ({ currentSort, onSortChange, + isDisabled = false, }: SortDropdownProps) => { const getSortLabel = () => { switch (currentSort) { @@ -51,7 +53,12 @@ export const SortDropdown = ({ return ( - diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 7bdd2c6..6f7b526 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,12 +1,13 @@ -import { BookOpen, Search } from "lucide-react"; +import { BookOpen, PanelLeftOpen, Search } from "lucide-react"; import { useEffect, useRef } from "react"; import { useDebounce } from "use-debounce"; import { useGetMangas } from "@/api/generated/catalog/catalog.ts"; import { AuthHeader } from "@/components/AuthHeader.tsx"; -import { Spinner } from "@/components/ui/spinner.tsx"; +import { Button } from "@/components/ui/button.tsx"; import { Pagination } from "@/components/Pagination.tsx"; import { ThemeToggle } from "@/components/ThemeToggle.tsx"; import { Input } from "@/components/ui/input.tsx"; +import { Skeleton } from "@/components/ui/skeleton.tsx"; import { useUIState } from "@/contexts/UIStateContext.tsx"; import { FilterSidebar } from "@/features/home/components/FilterSidebar.tsx"; import { MangaGrid } from "@/features/home/components/MangaGrid.tsx"; @@ -35,6 +36,8 @@ const Home = () => { setSortOption, searchText, setSearchText, + isSidebarOpen, + setIsSidebarOpen, } = useUIState(); const [debouncedSearchText] = useDebounce(searchText, 500); @@ -54,6 +57,7 @@ const Home = () => { userFavorites, score: minRating, }); + const isFiltersDisabled = isFetching; const startSearchRef = useRef(debouncedSearchText); @@ -69,18 +73,22 @@ const Home = () => { return (
- + {isSidebarOpen && ( + setIsSidebarOpen(false)} + isDisabled={isFiltersDisabled} + /> + )}
@@ -93,17 +101,31 @@ const Home = () => {

MangaMochi

-
-

- {mangasData?.data?.totalElements ?? 0} titles available -

- {isFetching && ( - +
+ {isFetching ? ( + + ) : ( + <> +

+ {mangasData?.data?.totalElements ?? 0} titles available +

+ )}
+ {!isSidebarOpen && ( + + )}
@@ -119,7 +141,8 @@ const Home = () => { onChange={(e) => { setSearchText(e.target.value); }} - className="pl-10 bg-card border-border" + disabled={isFiltersDisabled} + className="pl-10 bg-card border-border disabled:opacity-50" />
@@ -144,6 +167,7 @@ const Home = () => {
)} -- 2.49.1 From 7dab8ec21c8f92cc60efa09b40e5343a23593cac Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Sun, 5 Apr 2026 20:33:02 -0300 Subject: [PATCH 4/8] refactor: update MangaCard to use correct DTO types and fix query cache update logic --- src/features/home/components/MangaCard.tsx | 30 +++++++++++++--------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/features/home/components/MangaCard.tsx b/src/features/home/components/MangaCard.tsx index 11747c1..fae4375 100644 --- a/src/features/home/components/MangaCard.tsx +++ b/src/features/home/components/MangaCard.tsx @@ -3,8 +3,8 @@ import { Calendar, Database, Heart, Star } from "lucide-react"; import { useCallback } from "react"; import { Link } from "react-router"; import type { + DefaultResponseDTOPageMangaListDTO, MangaListDTO, - PageMangaListDTO, } from "@/api/generated/api.schemas.ts"; import { useSetFavorite, @@ -18,7 +18,7 @@ import { formatToTwoDigitsDateRange } from "@/utils/dateFormatter.ts"; interface MangaCardProps { manga: MangaListDTO; - queryKey: unknown; + queryKey: readonly unknown[]; } export const MangaCard = ({ manga, queryKey }: MangaCardProps) => { @@ -26,14 +26,20 @@ export const MangaCard = ({ manga, queryKey }: MangaCardProps) => { const queryClient = useQueryClient(); const updateQueryData = useCallback( - (oldData: PageMangaListDTO | undefined, isFavorite: boolean) => ({ + ( + oldData: DefaultResponseDTOPageMangaListDTO | undefined, + isFavorite: boolean, + ) => ({ ...oldData, - content: - oldData?.content?.map((manga) => - manga.id === manga.id ? { ...manga, favorite: isFavorite } : manga, - ) || [], + data: { + ...oldData?.data, + content: + oldData?.data?.content?.map((item) => + item.id === manga.id ? { ...item, favorite: isFavorite } : item, + ) || [], + }, }), - [], + [manga.id], ); const { mutate: mutateFavorite, isPending: isPendingFavorite } = @@ -41,8 +47,8 @@ export const MangaCard = ({ manga, queryKey }: MangaCardProps) => { mutation: { onSuccess: () => queryClient.setQueryData( - [queryKey], - (oldData: PageMangaListDTO | undefined) => + queryKey, + (oldData: DefaultResponseDTOPageMangaListDTO | undefined) => updateQueryData(oldData, true), ), }, @@ -53,8 +59,8 @@ export const MangaCard = ({ manga, queryKey }: MangaCardProps) => { mutation: { onSuccess: () => queryClient.setQueryData( - [queryKey], - (oldData: PageMangaListDTO | undefined) => + queryKey, + (oldData: DefaultResponseDTOPageMangaListDTO | undefined) => updateQueryData(oldData, false), ), }, -- 2.49.1 From 4c73722f9a9fd7d9448520fe55beda6999ff5e58 Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Sun, 5 Apr 2026 20:56:35 -0300 Subject: [PATCH 5/8] feat: add loading skeleton state to Manga page --- src/pages/Manga.tsx | 83 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/src/pages/Manga.tsx b/src/pages/Manga.tsx index ee1028a..dd05fe3 100644 --- a/src/pages/Manga.tsx +++ b/src/pages/Manga.tsx @@ -35,6 +35,7 @@ import { useUIState } from "@/contexts/UIStateContext.tsx"; import { MangaChapter } from "@/features/manga/MangaChapter.tsx"; import { useScrollPersistence } from "@/hooks/useScrollPersistence.ts"; import { formatToTwoDigitsDateRange } from "@/utils/dateFormatter.ts"; +import { Skeleton } from "@/components/ui/skeleton"; const Manga = () => { const { isAuthenticated } = useAuth(); @@ -44,7 +45,7 @@ const Manga = () => { const queryClient = useQueryClient(); - const { data: mangaData, queryKey } = useGetManga(mangaId); + const { data: mangaData, queryKey, isLoading } = useGetManga(mangaId); const { mutate, isPending: fetchPending } = useFetchContentProviderContentList({ @@ -122,7 +123,85 @@ const Manga = () => { [mangaData?.data?.id, mutateUnfollow, mutateFollow], ); - if (!mangaData) { + if (isLoading) { + return ( +
+ {/* Header */} +
+
+
+
+ +

MangaMochi

+
+ +
+
+
+ + {/* Content Shell */} +
+
+
+ {/* Cover Skeleton */} + + + {/* Details Skeleton */} +
+
+
+ +
+ + + +
+
+ +
+ +
+ + + +
+ +
+ {[1, 2, 3, 4].map((i) => ( + + ))} +
+ +
+ {[1, 2, 3, 4, 5].map((i) => ( + + ))} +
+
+
+ +
+ +
+ {[1, 2, 3].map((i) => ( + + ))} +
+
+
+
+
+ ); + } + + if (!mangaData?.data) { return (
-- 2.49.1 From 3db1ce9bdb512ba38d4061a458be810b93a0e0f2 Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Sun, 5 Apr 2026 21:12:12 -0300 Subject: [PATCH 6/8] feat: add loading states to manga actions and images, and improve provider interaction UX --- src/features/home/components/MangaCard.tsx | 14 ++- src/features/home/components/MangaGrid.tsx | 2 +- src/pages/Manga.tsx | 126 ++++++++++++++------- 3 files changed, 96 insertions(+), 46 deletions(-) diff --git a/src/features/home/components/MangaCard.tsx b/src/features/home/components/MangaCard.tsx index fae4375..60af2a6 100644 --- a/src/features/home/components/MangaCard.tsx +++ b/src/features/home/components/MangaCard.tsx @@ -1,6 +1,6 @@ import { useQueryClient } from "@tanstack/react-query"; import { Calendar, Database, Heart, Star } from "lucide-react"; -import { useCallback } from "react"; +import { useCallback, useState } from "react"; import { Link } from "react-router"; import type { DefaultResponseDTOPageMangaListDTO, @@ -13,7 +13,9 @@ import { import { Badge } from "@/components/ui/badge.tsx"; import { Button } from "@/components/ui/button.tsx"; import { Card, CardContent } from "@/components/ui/card.tsx"; +import { Skeleton } from "@/components/ui/skeleton.tsx"; import { useAuth } from "@/contexts/AuthContext.tsx"; +import { cn } from "@/lib/utils.ts"; import { formatToTwoDigitsDateRange } from "@/utils/dateFormatter.ts"; interface MangaCardProps { @@ -24,6 +26,7 @@ interface MangaCardProps { export const MangaCard = ({ manga, queryKey }: MangaCardProps) => { const { isAuthenticated } = useAuth(); const queryClient = useQueryClient(); + const [isImageLoading, setIsImageLoading] = useState(true); const updateQueryData = useCallback( ( @@ -80,6 +83,9 @@ export const MangaCard = ({ manga, queryKey }: MangaCardProps) => {
+ {isImageLoading && ( + + )} { "/placeholder.svg" } alt={manga.title} - className="absolute inset-0 w-full h-full object-cover transition-transform duration-300 group-hover:scale-105" + onLoad={() => setIsImageLoading(false)} + className={cn( + "absolute inset-0 h-full w-full object-cover transition-all duration-300 group-hover:scale-105", + isImageLoading ? "opacity-0" : "opacity-100", + )} />
{ diff --git a/src/pages/Manga.tsx b/src/pages/Manga.tsx index dd05fe3..4b150c0 100644 --- a/src/pages/Manga.tsx +++ b/src/pages/Manga.tsx @@ -14,7 +14,7 @@ import { useCallback } from "react"; import { useNavigate, useParams } from "react-router"; import { toast } from "sonner"; import { useGetManga } from "@/api/generated/catalog/catalog.ts"; -import {useFetchAllContentImages, useFetchContentProviderContentList} from "@/api/generated/ingestion/ingestion.ts"; +import { useFetchAllContentImages, useFetchContentProviderContentList } from "@/api/generated/ingestion/ingestion.ts"; import { useFollowManga, useSetFavorite, @@ -36,6 +36,7 @@ import { MangaChapter } from "@/features/manga/MangaChapter.tsx"; import { useScrollPersistence } from "@/hooks/useScrollPersistence.ts"; import { formatToTwoDigitsDateRange } from "@/utils/dateFormatter.ts"; import { Skeleton } from "@/components/ui/skeleton"; +import { Spinner } from "@/components/ui/spinner"; const Manga = () => { const { isAuthenticated } = useAuth(); @@ -253,8 +254,8 @@ const Manga = () => { src={ (mangaData.data?.coverImageKey && import.meta.env.VITE_OMV_BASE_URL + - "/" + - mangaData.data?.coverImageKey) || + "/" + + mangaData.data?.coverImageKey) || "/placeholder.svg" } alt={mangaData.data?.title ?? ""} @@ -291,7 +292,13 @@ const Manga = () => { }} disabled={isPendingFollowChange} > - {mangaData?.data?.following ? : } + {isPendingFollowChange ? ( + + ) : mangaData?.data?.following ? ( + + ) : ( + + )} )} @@ -364,7 +374,7 @@ const Manga = () => {

Chapters

{mangaData.data?.chapterCount && - mangaData.data?.chapterCount > 0 + mangaData.data?.chapterCount > 0 ? mangaData.data?.chapterCount : "-"}

@@ -428,11 +438,18 @@ const Manga = () => { toggleProvider(provider.id ?? -1)} + onOpenChange={() => { + if (provider.chaptersAvailable > 0) { + toggleProvider(provider.id ?? -1); + } + }} > - - -
+ + 0 ? "cursor-pointer" : "cursor-default"}`} + > +
@@ -445,48 +462,71 @@ const Manga = () => {

- {provider.supportsChapterFetch && provider.active && ( -
- +
+ + + {provider.supportsChapterFetch && provider.active && ( +
+ {provider.chaptersAvailable > 0 && ( + + )}
- )} -
- +
0 + ? "cursor-pointer" + : "invisible" + } + > + - - + }`} + /> +
+
+
Date: Sun, 5 Apr 2026 21:17:17 -0300 Subject: [PATCH 7/8] refactor: relocate MangaLoadingState and implement loading screen in Chapter page --- src/{features/home => }/components/MangaLoadingState.tsx | 0 src/pages/Chapter.tsx | 9 +++++++++ src/pages/Home.tsx | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) rename src/{features/home => }/components/MangaLoadingState.tsx (100%) diff --git a/src/features/home/components/MangaLoadingState.tsx b/src/components/MangaLoadingState.tsx similarity index 100% rename from src/features/home/components/MangaLoadingState.tsx rename to src/components/MangaLoadingState.tsx diff --git a/src/pages/Chapter.tsx b/src/pages/Chapter.tsx index 48173a2..66ca869 100644 --- a/src/pages/Chapter.tsx +++ b/src/pages/Chapter.tsx @@ -6,6 +6,7 @@ import { useMarkContentAsRead } from "@/api/generated/user-interaction/user-inte import { ThemeToggle } from "@/components/ThemeToggle.tsx"; import { Button } from "@/components/ui/button"; import { useReadingTracker } from "@/features/chapter/hooks/useReadingTracker.ts"; +import { MangaLoadingState } from "@/components/MangaLoadingState.tsx"; const Chapter = () => { const { setCurrentChapterPage, getCurrentChapterPage } = useReadingTracker(); @@ -109,6 +110,14 @@ const Chapter = () => { } }, [infiniteScroll]); + if (isLoading) { + return ( +
+ +
+ ); + } + if (!data?.data) { return (
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 6f7b526..c935e49 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -11,7 +11,7 @@ import { Skeleton } from "@/components/ui/skeleton.tsx"; import { useUIState } from "@/contexts/UIStateContext.tsx"; import { FilterSidebar } from "@/features/home/components/FilterSidebar.tsx"; import { MangaGrid } from "@/features/home/components/MangaGrid.tsx"; -import { MangaLoadingState } from "@/features/home/components/MangaLoadingState.tsx"; +import { MangaLoadingState } from "@/components/MangaLoadingState.tsx"; import { SortDropdown } from "@/features/home/components/SortDropdown.tsx"; import { useDynamicPageSize } from "@/hooks/useDynamicPageSize.ts"; -- 2.49.1 From 1fc69c1de11fdec6341ff6639e0e81b975e81b8b Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Sun, 5 Apr 2026 21:28:02 -0300 Subject: [PATCH 8/8] feat: add chapter navigation buttons to header and footer in both viewing modes --- src/pages/Chapter.tsx | 112 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 5 deletions(-) diff --git a/src/pages/Chapter.tsx b/src/pages/Chapter.tsx index 66ca869..1bb75e4 100644 --- a/src/pages/Chapter.tsx +++ b/src/pages/Chapter.tsx @@ -134,6 +134,25 @@ const Chapter = () => { } const images = data.data.contentImageKeys; + const previousContentId = data.data.previousContentId; + const nextContentId = data.data.nextContentId; + + const goToNextChapter = () => { + if (nextContentId) { + setCurrentPage(1); + setVisibleCount(1); + window.scrollTo({ top: 0 }); + navigate(`/manga/${mangaId}/chapter/${nextContentId}`); + } + }; + const goToPreviousChapter = () => { + if (previousContentId) { + setCurrentPage(1); + setVisibleCount(1); + window.scrollTo({ top: 0 }); + navigate(`/manga/${mangaId}/chapter/${previousContentId}`); + } + }; /** Standard navigation (non-infinite mode) */ const goToNextPage = () => { @@ -180,10 +199,33 @@ const Chapter = () => {

{data.data.mangaTitle}

-

- Chapter {chapterNumber} +

+ {chapterNumber}

+ + {previousContentId && ( + + )} + {nextContentId && ( + + )}
@@ -211,7 +253,7 @@ const Chapter = () => { {data.data.mangaTitle}

- Chapter {chapterNumber} + {chapterNumber}

@@ -233,6 +275,36 @@ const Chapter = () => { {/* LOAD MORE SENTINEL */}
+ + {/* CHAPTER NAVIGATION (infinite scroll mode) */} + {(previousContentId || nextContentId) && ( +
+ {previousContentId ? ( + + ) : ( +
+ )} + {nextContentId ? ( + + ) : ( +
+ )} +
+ )}
) : ( /* ------------------------------------------------------------------ */ @@ -243,8 +315,8 @@ const Chapter = () => { {`Page {
+ + {/* CHAPTER NAVIGATION */} + {(previousContentId || nextContentId) && ( +
+ {previousContentId ? ( + + ) : ( +
+ )} + {nextContentId ? ( + + ) : ( +
+ )} +
+ )}
)} -- 2.49.1