feat(anipic): revamp image pages, SEO gallery flow, and landing experience#25
Open
CodesWithSubham wants to merge 1 commit into
Open
feat(anipic): revamp image pages, SEO gallery flow, and landing experience#25CodesWithSubham wants to merge 1 commit into
CodesWithSubham wants to merge 1 commit into
Conversation
…ience Introduce a complete ImagePages system for AniPic with a new gallery architecture, landing page sections, cursor-based SEO pagination, image detail improvements, and loading UX enhancements. Landing page - Add full hero section with search bar, headline, and CTA buttons - Add features section highlighting core platform capabilities - Add popular tags cloud with image counts - Add trending, most downloaded, and recent image strip sections - Add cached landing page data loader Gallery & tag pages - Replace numeric pagination with cursor-based SEO URLs - Add dedicated /gallery route as the main browsing page - Pre-generate gallery and tag cursor pages statically at build time - Support infinite scrolling from preloaded cursor positions - Add filter bar with search and sort controls directly in the grid - Render first tag page directly without redirecting - Pre-generate top tag cursor pages statically Cursor generation - Add aggregation-based cursor generator using $setWindowFields + $documentNumber - Generate cursors for the full collection without document limits - Fetch only boundary _ids for improved efficiency - Add cache-tag-based invalidation support Image detail pages - Add zoomable lightbox with scroll/pinch zoom and drag-to-pan - Add share action with Web Share API and clipboard fallback - Add metadata sidebar with resolution, downloads, and upload date - Add related images section based on shared tags - Include createdAt in image payloads - Add modal preview route support for image overlays Skeleton loading system - Add reusable Shimmer animation primitive - Add responsive masonry skeleton loaders matching grid breakpoints - Add route-level loading UIs for home, gallery, tag, and image pages - Add detailed image page skeleton layout - Replace legacy ImageContent loader system API & data layer - Fix GET route incorrectly reading from req.body instead of searchParams - Make nextCursor nullable in API response schema - Add new image loaders, schemas, types, utilities, and secure download flow - Add cached image and landing-page data utilities UI & infrastructure - Add new ImagePages app route with shared layout and modal support - Add MasonryImageGrid, FilterBar, ImageCard, ModalBackdrop, ZoomableImage, and ImageClientAction components - Add useColumnCount hook for responsive masonry layouts - Add secure server-side download action with token flow - Remove legacy sno-based image pages and deprecated loaders - Update AniPic model, upload actions, and package metadata
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
There was a problem hiding this comment.
Pull request overview
This PR introduces a new “ImagePages” routing and data layer for AniPic, replacing the legacy sno-based image browsing/pages with a cursor-based gallery/tag architecture, a revamped landing experience, and improved loading UX (skeletons + masonry grid).
Changes:
- Added a new image browsing system:
/gallery,/tag/[tag], cursor routes, and/i/[id]image detail with modal overlay support. - Implemented cursor-based pagination + loaders: image list loader, cursor generation for sitemap/static params, and an
/api/imagesendpoint for infinite scroll. - Added a new masonry-based UI grid, filter bar, and skeleton/shimmer primitives; removed legacy grid and numeric pagination pages.
Reviewed changes
Copilot reviewed 51 out of 52 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Locks new dependency additions (masonry virtualization). |
| apps/anipic/utils/const.ts | Removes legacy page-size constant used by old pagination. |
| apps/anipic/package.json | Adds @virtuoso.dev/masonry dependency for the new masonry grid. |
| apps/anipic/lib/db/models/AniPic.ts | Updates AniPic schema (removes sno, adds cursor-friendly indexes, makes width/height required). |
| apps/anipic/hooks/useColumnCount.ts | Adds responsive column-count hook for masonry layout. |
| apps/anipic/features/images/utils.ts | Adds cursor encode/decode helpers. |
| apps/anipic/features/images/types.ts | Introduces shared image/cursor types for the new image system. |
| apps/anipic/features/images/schemas.ts | Adds Zod schemas for the images API request/response. |
| apps/anipic/features/images/loadLandingPageData.ts | Adds cached landing-page aggregation/queries for homepage sections. |
| apps/anipic/features/images/loadImages.ts | Implements cursor-based image loading (sorting/filtering/pagination). |
| apps/anipic/features/images/generateCursors.ts | Adds Mongo aggregation-based cursor boundary generation + sitemap URL helpers. |
| apps/anipic/features/images/const.ts | Centralizes new constants and base content filter. |
| apps/anipic/components/skeletons/Shimmer.tsx | Adds shimmer primitive for skeleton UIs. |
| apps/anipic/components/skeletons/Shimmer.module.css | Adds shimmer animation CSS. |
| apps/anipic/components/skeletons/MasonrySkeleton.tsx | Adds responsive masonry skeleton matching grid breakpoints. |
| apps/anipic/components/skeletons/ImageDetailSkeleton.tsx | Adds image detail page skeleton layout. |
| apps/anipic/components/skeletons/HomePageSkeleton.tsx | Adds landing/home skeleton layout. |
| apps/anipic/components/skeletons/GalleryHeaderSkeleton.tsx | Adds gallery header skeleton. |
| apps/anipic/components/skeletons/FilterBarSkeleton.tsx | Adds filter bar skeleton. |
| apps/anipic/components/MasonryImageGrid.tsx | Adds masonry grid with infinite scroll using /api/images. |
| apps/anipic/components/imageGrid.tsx | Removes legacy column-based grid component using sno routes. |
| apps/anipic/components/ImageCard.tsx | Adds new card renderer compatible with the masonry grid. |
| apps/anipic/components/FilterBar.tsx | Adds sort/search UI that drives URL query params. |
| apps/anipic/app/upload/actions.ts | Updates upload action to stop generating sno and rely on _id. |
| apps/anipic/app/sitemap.ts | Replaces numeric pagination sitemap with cursor-based gallery/tag URLs. |
| apps/anipic/app/page.tsx | Removes legacy homepage using numeric pagination and sno-based linking. |
| apps/anipic/app/api/images/route.ts | Adds GET /api/images to serve cursor-based list data for infinite scroll. |
| apps/anipic/app/(ImagePages)/tag/[tag]/page.tsx | Adds first tag page using new loader + masonry UI. |
| apps/anipic/app/(ImagePages)/tag/[tag]/loading.tsx | Adds tag first-page loading skeletons. |
| apps/anipic/app/(ImagePages)/tag/[tag]/[cursor]/page.tsx | Adds tag cursor page + static param generation. |
| apps/anipic/app/(ImagePages)/tag/[tag]/[cursor]/loading.tsx | Adds tag cursor-page loading skeletons. |
| apps/anipic/app/(ImagePages)/page/[page]/page.tsx | Removes legacy numbered pagination route. |
| apps/anipic/app/(ImagePages)/page.tsx | Adds new landing page (hero/features/tags/strips) backed by cached loader. |
| apps/anipic/app/(ImagePages)/loading.tsx | Adds route-level home loading UI. |
| apps/anipic/app/(ImagePages)/layout.tsx | Introduces ImagePages layout with parallel modal slot. |
| apps/anipic/app/(ImagePages)/i/[sno]/page.tsx | Removes old sno-based image detail page. |
| apps/anipic/app/(ImagePages)/i/[sno]/loading.tsx | Removes old image loading UI tied to legacy loader. |
| apps/anipic/app/(ImagePages)/i/[sno]/ImageClientAction.tsx | Removes old client actions for sno-based image pages. |
| apps/anipic/app/(ImagePages)/i/[sno]/action.ts | Removes old download token action tied to sno. |
| apps/anipic/app/(ImagePages)/i/[id]/ZoomableImage.tsx | Adds zoomable lightbox experience for image viewing. |
| apps/anipic/app/(ImagePages)/i/[id]/page.tsx | Adds new _id-based image detail page with related images. |
| apps/anipic/app/(ImagePages)/i/[id]/loading.tsx | Adds image detail loading skeleton. |
| apps/anipic/app/(ImagePages)/i/[id]/ImageClientAction.tsx | Adds download/share client actions for new image detail pages. |
| apps/anipic/app/(ImagePages)/i/[id]/action.ts | Adds secure server action to mint download token + increment downloads. |
| apps/anipic/app/(ImagePages)/i/[id]/_data.ts | Adds cached server data loader for image detail payload. |
| apps/anipic/app/(ImagePages)/gallery/page.tsx | Adds main gallery route with filterable masonry grid. |
| apps/anipic/app/(ImagePages)/gallery/loading.tsx | Adds gallery loading skeletons. |
| apps/anipic/app/(ImagePages)/gallery/[cursor]/page.tsx | Adds gallery cursor route + static param generation. |
| apps/anipic/app/(ImagePages)/gallery/[cursor]/loading.tsx | Adds cursor gallery loading skeletons. |
| apps/anipic/app/(ImagePages)/@modal/default.tsx | Adds default parallel-route modal slot. |
| apps/anipic/app/(ImagePages)/@modal/(.)i/[id]/page.tsx | Adds modal overlay route for image previews. |
| apps/anipic/app/(ImagePages)/@modal/(.)i/[id]/ModalBackdrop.tsx | Adds modal backdrop client component with close behavior. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+32
to
+34
| function buildCursorCondition(sort: ImageSort, decodedCursor: CursorPayload) { | ||
| const objectId = new Types.ObjectId(decodedCursor.id); | ||
|
|
Comment on lines
+82
to
+85
| if (skipId) { | ||
| const objectId = new Types.ObjectId(skipId); | ||
| baseFilter.$and.push({ _id: { $ne: objectId } }); | ||
| } |
Comment on lines
+87
to
+89
| const total = await AniPic.countDocuments(baseFilter); | ||
|
|
||
| const sortQuery = ((): Record<string, 1 | -1> => { |
Comment on lines
+160
to
+172
| export async function getAllTags(shortBy: "name" | "count" = "name"): Promise<string[]> { | ||
| "use cache"; | ||
| cacheLife("days"); | ||
| cacheTag("anipicTags"); | ||
|
|
||
| const AniPic = await getAniPicModel(); | ||
|
|
||
| const tagAgg = await AniPic.aggregate<{ _id: string; count: number }>([ | ||
| { $match: BASE_FILTER }, | ||
| { $unwind: "$tags" }, | ||
| { $group: { _id: "$tags", count: { $sum: 1 } } }, | ||
| { $sort: shortBy === "count" ? { count: -1 } : { _id: 1 } }, | ||
| ]); |
Comment on lines
+20
to
+29
| export const ImageApiRequestBodySchema = z.object({ | ||
| cursor: z.string().optional(), | ||
| tags: z | ||
| .string() | ||
| .transform((val) => (val ? val.split(",").filter(Boolean) : [])) | ||
| .optional(), | ||
| q: z.string().optional(), | ||
| sort: z.enum(["latest", "popular", "views", "downloads"]).optional(), | ||
| skipId: z.string().optional(), // For related images, to exclude the current image | ||
| }); |
Comment on lines
+38
to
+43
| const { tag } = await params; | ||
| const decoded = decodeURIComponent(tag); | ||
| return { | ||
| title: `#${capitalize(decoded)} AI Anime Art · AniPic`, | ||
| description: `Browse AI-generated anime images tagged "${decoded}" on AniPic.`, | ||
| alternates: { canonical: `/tag/${tag}` }, |
Comment on lines
+11
to
+20
| interface Props { | ||
| params: Promise<{ cursor: string }>; | ||
| searchParams: Promise<{ sort?: string; q?: string; tags?: string }>; | ||
| } | ||
|
|
||
| export async function generateStaticParams() { | ||
| const cursors = await generateGalleryCursors(); // no limit — all pages | ||
|
|
||
| if (cursors.length === 0) return [{ cursor: "__empty__" }]; // dummy page to avoid build error when DB is empty | ||
| return cursors.map((cursor) => ({ cursor })); |
Comment on lines
+37
to
+43
| const { sort: rawSort, q, tags: rawTags } = await searchParams; | ||
|
|
||
| const sort = (rawSort as SortOption | undefined) ?? "latest"; | ||
| const tags = rawTags ? rawTags.split(",").filter(Boolean) : []; | ||
|
|
||
| const { images, hasMore, nextCursor } = await loadImages({ cursor, sort, tags, q }); | ||
|
|
Comment on lines
+11
to
+27
| // back after 300ms (to allow modal close animation to play) | ||
| const close = () => { | ||
| setIsClosing(true); | ||
| setTimeout(() => { | ||
| setIsClosing(false); | ||
| router.back(); | ||
| }, 300); | ||
| }; | ||
|
|
||
| // Close on Escape key | ||
| useEffect(() => { | ||
| const onKey = (e: KeyboardEvent) => { | ||
| if (e.key === "Escape") close(); | ||
| }; | ||
| document.addEventListener("keydown", onKey); | ||
| return () => document.removeEventListener("keydown", onKey); | ||
| }, [close]); |
Comment on lines
+20
to
+24
| const img = await AniPic.findOneAndUpdate( | ||
| { _id: id, approved: true }, | ||
| { $inc: { downloads: 1 } }, | ||
| { new: true }, | ||
| ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Introduce a complete ImagePages system for AniPic with a new gallery architecture, landing page sections, cursor-based SEO pagination, image detail improvements, and loading UX enhancements.
Landing page
Gallery & tag pages
Cursor generation
Image detail pages
Skeleton loading system
API & data layer
UI & infrastructure