-
Notifications
You must be signed in to change notification settings - Fork 1
feat: render collections #62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds Collections support: new Collection type, CollectionCard component, CollectionPage route (/collections/:id), DiscoverPage now toggles between articles and collections with unified Fuse search, article route changed to /articles/, EditorRoom gated by Dust client, and a minor CollectionWizard toast text update. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Router
participant CollectionPage
participant Stash as Stash/Records
participant Posts as Posts Source
participant UI
User->>Router: Navigate to /collections/:id
Router->>CollectionPage: Render with {id}
CollectionPage->>Stash: fetch Collection, CollectionPosts, PlayerName
Stash-->>CollectionPage: collection + articleIds + owner name
CollectionPage->>Posts: fetch articles
Posts-->>CollectionPage: articles list
CollectionPage->>UI: Render collection header, description, cover, article list
sequenceDiagram
autonumber
actor User
participant DiscoverPage
participant Stash as Stash/Records
participant Fuse as Fuse.js
User->>DiscoverPage: Load page
DiscoverPage->>Stash: load Articles & Collections
Note over DiscoverPage: contentType = "articles" | "collections"
User->>DiscoverPage: Type in search
alt contentType = articles
DiscoverPage->>Fuse: search(author, categories, excerpt, title)
Fuse-->>DiscoverPage: filtered articles
DiscoverPage->>User: Render ArticleCard grid
else contentType = collections
DiscoverPage->>Fuse: search(author, description, title)
Fuse-->>DiscoverPage: filtered collections
DiscoverPage->>User: Render CollectionCard grid
end
sequenceDiagram
autonumber
actor User
participant EditorRoomPage
participant Dust as useDustClient
User->>EditorRoomPage: Navigate to Editor Room
EditorRoomPage->>Dust: get dustClient
alt dustClient available
EditorRoomPage-->>User: Render tabs (published/drafts)
else unavailable
EditorRoomPage-->>User: "Please open the app in DUST to use the Editor Room."
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/app/src/Routes.tsx (1)
21-29: Preserve old article links with a redirectChanging
/article/:id→/articles/:idcan break bookmarks and external links. Add a legacy redirect.Apply this diff:
<Route path={EDITOR_PAGE_PATH} element={<EditorRoomPage />} /> <Route path={`${ARTICLE_PAGE_PATH}:id`} element={<ArticlePage />} /> + <Route + path="/article/:id" + element={<Navigate to={`${ARTICLE_PAGE_PATH}:id`} replace />} + /> <Route path={`${COLLECTION_PAGE_PATH}:id`} element={<CollectionPage />} />
🧹 Nitpick comments (17)
packages/app/src/components/editor/CollectionWizard.tsx (1)
120-121: Success toast: add actionable CTA to DiscoverGreat copy tweak. Consider adding a toast action that navigates to Discover so users can jump there in one click.
Apply this diff:
- toast.success("Collection Published! View in the Discover page."); + toast.success("Collection Published", { + action: { + label: "View in Discover", + onClick: () => navigate(DISCOVER_PAGE_PATH), + }, + });Additional changes needed outside this range:
+ import { useNavigate } from "react-router-dom"; + import { DISCOVER_PAGE_PATH } from "@/Routes"; // or "@/paths" if you extract path constantsInside the component (near other hooks):
+ const navigate = useNavigate();packages/app/src/utils/types.ts (1)
1-11: Tighten IDs and addresses with branded hex typesStrengthen type safety for IDs/addresses to catch misuse at compile time.
Apply this diff:
+export type Hex = `0x${string}`; + -export type Collection = { - id: string; - articleIds: string[]; - author: string; - coverImage: string; - createdAt: bigint; - description: string; - owner: string; - title: string; - updatedAt: bigint; -}; +export type Collection = { + id: Hex; + articleIds: Hex[]; // verify: are post IDs hex? adjust if not + author: string; + coverImage: string; + createdAt: bigint; // unix seconds + description: string; + owner: Hex; + title: string; + updatedAt: bigint; // unix seconds +};If article IDs aren’t hex, keep them as
string[].packages/app/src/Routes.tsx (1)
1-1: Add Navigate import to support legacy redirectsNeeded for the redirect below.
Apply this diff:
-import { Route, Routes } from "react-router-dom"; +import { Route, Routes, Navigate } from "react-router-dom";packages/app/src/components/CollectionCard.tsx (5)
15-21: Make the media area clickable tooUsers expect the image to link to the collection like the title.
Apply this diff (wrap the media block with Link):
- <div className="border border-neutral-900 my-2 overflow-hidden relative"> + <Link + to={`${COLLECTION_PAGE_PATH}${encodeURIComponent(collection.id)}`} + aria-label={`Open collection ${collection.title}`} + className="block border border-neutral-900 my-2 overflow-hidden relative" + > {/* shimmer placeholder while image loads */} {!imgLoaded && ( <div aria-hidden className="absolute inset-0 bg-neutral-200" style={{ backgroundImage: "linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.5) 50%, rgba(255,255,255,0) 100%)", backgroundSize: "200% 100%", animation: "shimmer 1.2s linear infinite", }} /> )} <img alt={collection.title} src={collection.coverImage} loading="lazy" + decoding="async" onLoad={() => setImgLoaded(true)} className={`aspect-video grayscale hover:grayscale-0 object-cover w-full transition-all duration-500 ${ imgLoaded ? "opacity-100" : "opacity-0" }`} width={800} height={450} /> - <style>{`@keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }`}</style> - </div> + </Link>
24-35: Move keyframes out of the component to avoid duplicate <style> tagsDefining
@keyframesper card bloats the DOM. Prefer a global CSS utility (e.g.,.animate-shimmer) and aprefers-reduced-motionsafeguard.Create a global class:
@keyframes shimmer { 0% { background-position: -200% 0 } 100% { background-position: 200% 0 } } .animate-shimmer { background-image: linear-gradient(90deg, transparent 0%, rgba(255,255,255,.5) 50%, transparent 100%); background-size: 200% 100%; animation: shimmer 1.2s linear infinite } @media (prefers-reduced-motion: reduce) { .animate-shimmer { animation: none } }Then replace the inline style with
className="absolute inset-0 bg-neutral-200 animate-shimmer", and remove the<style>block.Also applies to: 47-47
49-55: Use CSS line-clamp instead of manual substringSubstring can cut graphemes/emojis. Prefer multi-line clamp.
Apply this diff:
- <p className={"text-[15px] leading-relaxed text-neutral-800"}> - {collection.description.length > 50 ? ( - <>{collection.description.slice(0, 50)}...</> - ) : ( - collection.description - )} - </p> + <p className="text-[15px] leading-relaxed text-neutral-800 line-clamp-2"> + {collection.description} + </p>Ensure the Tailwind line-clamp plugin is enabled; otherwise use CSS
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;.
56-58: Use semantic for publish dateImproves accessibility and machine readability.
Apply this diff:
- <div className={cn("font-accent", "mt-1 text-[10px]")}> - By @{collection.author} on {formatDate(collection.createdAt)} - </div> + <div className={cn("font-accent", "mt-1 text-[10px]")}> + {(() => { + const iso = new Date(Number(collection.createdAt) * 1000).toISOString(); + return ( + <> + By @{collection.author} on{" "} + <time dateTime={iso}>{formatDate(collection.createdAt)}</time> + </> + ); + })()} + </div>
5-7: Avoid importing path constants from Routes to reduce couplingIf you extract paths to
@/paths(see Routes.tsx comment), update this import accordingly.-import { COLLECTION_PAGE_PATH } from "@/Routes"; +import { COLLECTION_PAGE_PATH } from "@/paths";packages/app/src/pages/CollectionPage.tsx (5)
20-23: Decode route param before matching records.
CollectionCardencodes IDs; decode here to ensure exact match.Apply:
- const { id } = useParams<{ id: string }>(); + const { id: idParam } = useParams<{ id: string }>(); + const id = useMemo(() => (idParam ? decodeURIComponent(idParam) : ""), [idParam]);- const collection = useMemo( - () => collections.find((p) => p.id === id), - [id, collections] - ); + const collection = useMemo( + () => collections.find((p) => p.id === id), + [id, collections] + );Also applies to: 63-66
109-118: Add a11y and resilience to “copy address” button.Provide context for screen readers and avoid default submit behavior.
Apply:
- <button + <button + type="button" + aria-label={`Copy ${shortenAddress(collection.owner)} to clipboard`} + title="Copy address to clipboard" onClick={() => { copyToClipboard(collection.owner); toast.success(`Copied ${shortenAddress(collection.owner)}`); }} >Optionally, update
useCopyto catch clipboard errors and surfacetoast.error.
186-201: Render category badges only when present.Empty arrays are truthy; the wrapper renders even with no categories.
Apply:
- {article.categories && ( + {article.categories.length > 0 && ( <div className="flex gap-1 mt-2"> {article.categories.map((category) => (
130-136: Defer loading of the hero image.Improves LCP when the image is below the fold on smaller screens.
Apply:
- <img + <img alt={collection.title} className="duration-500 grayscale hover:grayscale-0 object-cover transition-all w-full" height={720} src={collection.coverImage} + loading="lazy" width={1200} />
24-61: Duplicate “collection shaping” logic — extract a shared hook.This mapping appears here and in DiscoverPage; drift risk is high.
Create
useCollections()that encapsulatesuseRecords+ owner decoding +uriToHttp+articleIdsresolve + sorting, reuse in both pages. I can draft it if you want.packages/app/src/pages/DiscoverPage.tsx (4)
120-123: Make helper text reflect the active content type.Avoid confusion when browsing collections.
Apply:
- <p className="text-neutral-700 text-sm">Search all articles</p> + <p className="text-neutral-700 text-sm"> + Search {contentType === "articles" ? "articles" : "collections"} + </p>
126-131: Dynamic placeholder based on content type.Apply:
- <Input + <Input className="border-neutral-900 max-w-xs" onChange={(e) => setSearchQuery(e.target.value)} - placeholder="Search stories..." + placeholder={contentType === "articles" ? "Search stories..." : "Search collections..."} value={searchQuery} />
205-207: Format distance.Show whole blocks for readability.
Apply:
- Distance: {a.distance} blocks{" "} + Distance: {Math.round(a.distance)} blocks{" "}
35-72: Duplicate “collection shaping” logic — centralize via a hook.Same mapping exists in CollectionPage; extract
useCollections()to reduce drift and test surface.I can provide a minimal hook with memoized outputs.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
packages/app/src/Routes.tsx(3 hunks)packages/app/src/components/CollectionCard.tsx(1 hunks)packages/app/src/components/editor/CollectionWizard.tsx(1 hunks)packages/app/src/pages/CollectionPage.tsx(1 hunks)packages/app/src/pages/DiscoverPage.tsx(4 hunks)packages/app/src/pages/EditorRoomPage.tsx(3 hunks)packages/app/src/utils/types.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
packages/app/src/Routes.tsx (1)
packages/app/src/pages/CollectionPage.tsx (1)
CollectionPage(19-247)
packages/app/src/pages/CollectionPage.tsx (7)
packages/app/src/common/useCopy.ts (1)
useCopy(3-16)packages/app/src/common/usePosts.tsx (1)
usePosts(12-105)packages/app/src/mud/stash.ts (1)
tables(16-21)packages/app/src/utils/types.ts (1)
Collection(1-11)packages/app/src/utils/helpers.ts (3)
uriToHttp(58-90)shortenAddress(47-50)formatDate(6-13)packages/app/src/lib/utils.ts (1)
cn(4-6)packages/app/src/Routes.tsx (2)
DISCOVER_PAGE_PATH(14-14)FRONT_PAGE_PATH(11-11)
packages/app/src/components/CollectionCard.tsx (4)
packages/app/src/utils/types.ts (1)
Collection(1-11)packages/app/src/lib/utils.ts (1)
cn(4-6)packages/app/src/Routes.tsx (1)
COLLECTION_PAGE_PATH(17-17)packages/app/src/utils/helpers.ts (1)
formatDate(6-13)
packages/app/src/pages/DiscoverPage.tsx (9)
packages/app/src/common/useDustClient.ts (1)
useDustClient(4-18)packages/app/src/common/usePosts.tsx (1)
usePosts(12-105)packages/app/src/common/useCategories.tsx (1)
useCategories(6-47)packages/app/src/common/useWaypoint.tsx (1)
useWaypoint(8-39)packages/app/src/mud/stash.ts (1)
tables(16-21)packages/app/src/utils/types.ts (1)
Collection(1-11)packages/app/src/utils/helpers.ts (1)
uriToHttp(58-90)packages/app/src/components/ArticleCard.tsx (1)
ArticleCard(8-71)packages/app/src/components/CollectionCard.tsx (1)
CollectionCard(9-61)
packages/app/src/pages/EditorRoomPage.tsx (1)
packages/app/src/common/useDustClient.ts (1)
useDustClient(4-18)
🔇 Additional comments (4)
packages/app/src/Routes.tsx (1)
16-18: Verified: no remaining/article/literals
All article routes reference the newARTICLE_PAGE_PATHconstant; no hard-coded/article/paths were found.packages/app/src/pages/CollectionPage.tsx (1)
1-1: Verify use of @latticexyz/stash/internal.Internal import paths can break on upgrades; prefer public APIs if available.
If internal is required, pin package versions and add a code comment noting why.
packages/app/src/pages/DiscoverPage.tsx (2)
1-1: Verify use of @latticexyz/stash/internal.Using internal entrypoints can break on dependency updates.
Prefer public exports if available; otherwise document the rationale and pin versions.
238-241: No change needed: className interpolation is correct
The ternary expression uses matching double quotes ("opacity-100"and"opacity-0") and is valid TSX; there’s no stray quote to fix.Likely an incorrect or invalid review comment.
| if (!collection) { | ||
| return ( | ||
| <div className="p-6"> | ||
| <h1 className={cn("font-heading", "text-3xl")}>Collection not found</h1> | ||
| <p className="mt-2"> | ||
| We couldn't find that collection. Try the Discover page. | ||
| </p> | ||
| <div className="mt-4"> | ||
| <Link className="cursor-pointer underline" to={DISCOVER_PAGE_PATH}> | ||
| Go to Discover | ||
| </Link> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Avoid “not found” flash while data is loading.
When useRecords returns an initial empty array, the page briefly shows “Collection not found.”
Apply:
- if (!collection) {
- return (
- <div className="p-6">
+ if (!collection) {
+ if (collections.length === 0) {
+ return (
+ <div className="p-6">
+ <p className="text-neutral-600 text-sm">Loading collection…</p>
+ </div>
+ );
+ }
+ return (
+ <div className="p-6">
<h1 className={cn("font-heading", "text-3xl")}>Collection not found</h1>
<p className="mt-2">
We couldn't find that collection. Try the Discover page.
</p>
<div className="mt-4">
<Link className="cursor-pointer underline" to={DISCOVER_PAGE_PATH}>
Go to Discover
</Link>
</div>
</div>
);
}| <Link | ||
| to={`/articles/${article.id}`} | ||
| className="hover:underline" | ||
| > | ||
| {article.title} | ||
| </Link> | ||
| </h2> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Use path constants and URL-encode IDs for article links.
Hardcoding "/articles/${id}" and skipping encoding risks broken links if IDs contain special chars and drifts from centralized routing.
Apply:
- import { DISCOVER_PAGE_PATH, FRONT_PAGE_PATH } from "@/Routes";
+ import { ARTICLE_PAGE_PATH, DISCOVER_PAGE_PATH, FRONT_PAGE_PATH } from "@/Routes";- <Link
- to={`/articles/${article.id}`}
- className="hover:underline"
- >
+ <Link
+ to={`${ARTICLE_PAGE_PATH}${encodeURIComponent(article.id)}`}
+ className="hover:underline"
+ >- <Link to={`/articles/${article.id}`}>
+ <Link to={`${ARTICLE_PAGE_PATH}${encodeURIComponent(article.id)}`}>Also applies to: 218-227, 15-15
🤖 Prompt for AI Agents
In packages/app/src/pages/CollectionPage.tsx around lines 158-164 (and also
update the similar usages at 218-227 and line 15), replace hardcoded string
links like `/articles/${article.id}` with the centralized route constant or
helper (e.g. use ROUTES.ARTICLE or a buildArticlePath helper) and ensure the id
is URL-encoded using encodeURIComponent(article.id) when composing the path;
import the route constant/helper at the top if missing and update all three
occurrences to use the same helper so routing is consistent and safe for special
characters.
| const { data: dustClient } = useDustClient(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Expose query status for better gating UX
Include isPending/isError so the UI can distinguish “loading” from “not in DUST”.
Apply this diff:
- const { data: dustClient } = useDustClient();
+ const { data: dustClient, isPending, isError } = useDustClient();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { data: dustClient } = useDustClient(); | |
| const { data: dustClient, isPending, isError } = useDustClient(); |
🤖 Prompt for AI Agents
In packages/app/src/pages/EditorRoomPage.tsx around lines 33-34, the
useDustClient call only destructures data and doesn't expose query status;
update the hook destructuring to include isPending and isError (e.g. const {
data: dustClient, isPending, isError } = useDustClient()) and then pass or use
those flags where the UI needs to gate behavior (show loading state while
isPending, show error state or fallback when isError, and only treat absence of
dustClient as "not in DUST" when not pending and not errored).
| export const ARTICLE_PAGE_PATH = "/articles/"; | ||
| export const COLLECTION_PAGE_PATH = "/collections/"; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Avoid importing path constants from a file that also imports pages
Importing route constants from this module in leaf components risks circular deps. Extract paths to a dedicated module (e.g., @/paths) and re-export here.
Example new file:
// packages/app/src/paths.ts
export const FRONT_PAGE_PATH = "/";
export const LOCAL_PAGE_PATH = "/local";
export const BACK_PAGE_PATH = "/back-page";
export const DISCOVER_PAGE_PATH = "/discover";
export const EDITOR_PAGE_PATH = "/editor";
export const ARTICLE_PAGE_PATH = "/articles/";
export const COLLECTION_PAGE_PATH = "/collections/";🤖 Prompt for AI Agents
In packages/app/src/Routes.tsx around lines 16 to 18, the ARTICLE_PAGE_PATH and
COLLECTION_PAGE_PATH constants should be moved into a dedicated paths module to
avoid circular imports; create packages/app/src/paths.ts that exports all route
constants (including ARTICLE_PAGE_PATH and COLLECTION_PAGE_PATH), update
Routes.tsx to import those constants from '@/paths' and re-export them if other
modules rely on Routes.tsx, and remove any page imports from the new paths file
so it only contains plain string constants.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
packages/app/src/pages/EditorRoomPage.tsx (2)
33-34: Prefer isPending/isError from the query for precise gating.Expose both states to avoid ambiguous “loading” handling and enable explicit error UI.
- const { data: dustClient, isLoading } = useDustClient(); + const { data: dustClient, isPending, isError } = useDustClient();
121-131: Gate with loading/error states to avoid “open in DUST” flash and handle failures.Show a connecting state while pending; only show the DUST message on error or confirmed absence.
- if (isLoading) { - return <div className="p-4">Loading...</div>; - } - - if (!dustClient) { - return ( - <div className="p-4"> - Please open the app in DUST to use the Editor Room. - </div> - ); - } + if (isPending) { + return <div className="p-4">Connecting to DUST…</div>; + } + if (isError || !dustClient) { + return ( + <div className="p-4"> + Please open the app in DUST to use the Editor Room. + </div> + ); + }
🧹 Nitpick comments (5)
packages/app/src/pages/DiscoverPage.tsx (5)
91-94: Compare BigInt timestamps without Number coercionAvoid
Number(b.createdAt - a.createdAt)to prevent potential precision loss if timestamps grow. Use direct BigInt comparison.- results.sort((a, b) => { - if (dateSort === "newest") return Number(b.createdAt - a.createdAt); - return Number(a.createdAt - b.createdAt); - }); + results.sort((a, b) => { + if (a.createdAt === b.createdAt) return 0; + return dateSort === "newest" + ? (a.createdAt < b.createdAt ? 1 : -1) + : (a.createdAt < b.createdAt ? -1 : 1); + });Apply the same comparator in both articles and collections lists.
Also applies to: 113-116
99-119: Avoid rebuilding Fuse index on each keystrokePrecompute a Fuse index per dataset and reuse it for queries.
Inside the component (outside this useMemo):
const articlesFuse = useMemo( () => new Fuse(articles, { keys: ["author", "categories", "excerpt", "title"], includeScore: true }), [articles] ); const collectionsFuse = useMemo( () => new Fuse(collections, { keys: ["author", "description", "title"], includeScore: true }), [collections] );Then here:
- const filteredCollections = useMemo(() => { - const term = searchQuery.trim().toLowerCase(); + const filteredCollections = useMemo(() => { + const term = searchQuery.trim(); let results = collections.slice(); if (term) { - results = new Fuse(collections, { - keys: ["author", "description", "title"], - includeScore: true, - }) - .search(term) - .map((res) => res.item); + results = collectionsFuse.search(term).map((res) => res.item); } results.sort((a, b) => { if (dateSort === "newest") return Number(b.createdAt - a.createdAt); return Number(a.createdAt - b.createdAt); }); return results; - }, [collections, dateSort, searchQuery]); + }, [collections, collectionsFuse, dateSort, searchQuery]);
125-127: Fix user-facing copy to reflect selected content typeThe subtitle says “Search all articles” even when viewing collections; align the placeholder too.
- <p className="text-neutral-700 text-sm">Search all articles</p> + <p className="text-neutral-700 text-sm"> + {contentType === "collections" ? "Search all collections" : "Search all articles"} + </p> ... - placeholder="Search stories..." + placeholder={contentType === "collections" ? "Search collections..." : "Search articles..."}Also applies to: 131-135
151-176: Add basic tab a11y semanticsProvide roles and selection state for screen readers.
- <div className="flex gap-1 border-b border-neutral-300"> + <div className="flex gap-1 border-b border-neutral-300" role="tablist" aria-label="Content type"> <button + role="tab" + aria-selected={contentType === "articles"} + aria-controls="tab-panel-articles" className={cn( ... <button + role="tab" + aria-selected={contentType === "collections"} + aria-controls="tab-panel-collections" className={cn(And add
role="tabpanel"to the respective sections:- {contentType === "articles" && ( - <div className="flex flex-col gap-6"> + {contentType === "articles" && ( + <div id="tab-panel-articles" role="tabpanel" className="flex flex-col gap-6"> ... - {contentType === "collections" && ( - <Card className="border-neutral-900"> + {contentType === "collections" && ( + <Card id="tab-panel-collections" role="tabpanel" className="border-neutral-900">
78-85: Minor: Drop manual lowercasing of the queryFuse does case-insensitive matching by default; lowercasing the term is redundant and may slightly affect scoring nuances.
No code change needed if adopting the earlier diffs; otherwise:
- const term = searchQuery.trim().toLowerCase(); + const term = searchQuery.trim();Also applies to: 105-111
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
packages/app/src/pages/CollectionPage.tsx(1 hunks)packages/app/src/pages/DiscoverPage.tsx(4 hunks)packages/app/src/pages/EditorRoomPage.tsx(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/app/src/pages/CollectionPage.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
packages/app/src/pages/DiscoverPage.tsx (9)
packages/app/src/common/useDustClient.ts (1)
useDustClient(4-18)packages/app/src/common/usePosts.tsx (1)
usePosts(12-105)packages/app/src/common/useCategories.tsx (1)
useCategories(6-47)packages/app/src/common/useWaypoint.tsx (1)
useWaypoint(8-39)packages/app/src/mud/stash.ts (1)
tables(16-21)packages/app/src/utils/types.ts (1)
Collection(1-11)packages/app/src/utils/helpers.ts (1)
uriToHttp(58-90)packages/app/src/components/ArticleCard.tsx (1)
ArticleCard(8-71)packages/app/src/components/CollectionCard.tsx (1)
CollectionCard(9-61)
packages/app/src/pages/EditorRoomPage.tsx (1)
packages/app/src/common/useDustClient.ts (1)
useDustClient(4-18)
🔇 Additional comments (4)
packages/app/src/pages/EditorRoomPage.tsx (2)
3-3: Import looks good.No issues with bringing in useDustClient.
30-31: Moving TabKey to module scope is correct.Prevents re-creation on each render and clarifies typing.
packages/app/src/pages/DiscoverPage.tsx (2)
35-73: LGTM on Collections projectionGood defensive defaults (author fallback, cover fallback), clean mapping, and stable sorting. Matches usage patterns in usePosts.
1-3: Confirm public export of getRecord and update all internal imports
EnsuregetRecordis exported by the public@latticexyz/stashpackage, then replace everyimport { getRecord } from "@latticexyz/stash/internal";with
import { getRecord } from "@latticexyz/stash";in the following files:
- packages/app/src/common/usePlayerName.tsx
- packages/app/src/common/useCategories.tsx
- packages/app/src/common/usePosts.tsx
- packages/app/src/mud/stash.ts
- packages/app/src/pages/CollectionPage.tsx
- packages/app/src/pages/DiscoverPage.tsx
- packages/app/src/pages/FrontPage.tsx
- packages/app/src/components/editor/ArticleWizard.tsx
Once confirmed, you may run a codemod to update all occurrences consistently.
| const filteredArticles = useMemo(() => { | ||
| const term = searchQuery.trim().toLowerCase(); | ||
| let results = articles.slice(); | ||
|
|
||
| if (selectedCategory) { | ||
| results = results.filter((a) => | ||
| (a.categories || []).includes(selectedCategory) | ||
| ); | ||
| if (term) { | ||
| results = new Fuse(articles, { | ||
| keys: ["author", "categories", "excerpt", "title"], | ||
| includeScore: true, | ||
| }) | ||
| .search(term) | ||
| .map((res) => res.item); | ||
| } | ||
|
|
||
| if (authorFilter.trim()) { | ||
| const af = authorFilter.trim().toLowerCase(); | ||
| results = results.filter((a) => | ||
| (a.owner || "").toLowerCase().includes(af) | ||
| ); | ||
| if (selectedCategory) { | ||
| results = results.filter((a) => a.categories.includes(selectedCategory)); | ||
| } | ||
|
|
||
| results.sort((a, b) => { | ||
| if (dateSort === "newest") return Number(b.createdAt - a.createdAt); | ||
| return Number(a.createdAt - b.createdAt); | ||
| }); | ||
|
|
||
| return results; | ||
| }, [articles, dateSort, searchQuery, selectedCategory]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Compose filters before search and search the filtered dataset
Apply category filter before Fuse so search/ordering operates on the restricted set. Also no need to lowercase the term; Fuse is case-insensitive by default.
- const filteredArticles = useMemo(() => {
- const term = searchQuery.trim().toLowerCase();
- let results = articles.slice();
-
- if (term) {
- results = new Fuse(articles, {
+ const filteredArticles = useMemo(() => {
+ const term = searchQuery.trim();
+ let results = articles.slice();
+
+ if (selectedCategory) {
+ results = results.filter((a) => a.categories.includes(selectedCategory));
+ }
+
+ if (term) {
+ results = new Fuse(results, {
keys: ["author", "categories", "excerpt", "title"],
includeScore: true,
})
.search(term)
.map((res) => res.item);
}
- if (selectedCategory) {
- results = results.filter((a) => a.categories.includes(selectedCategory));
- }
-
results.sort((a, b) => {
if (dateSort === "newest") return Number(b.createdAt - a.createdAt);
return Number(a.createdAt - b.createdAt);
});
return results;
}, [articles, dateSort, searchQuery, selectedCategory]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const filteredArticles = useMemo(() => { | |
| const term = searchQuery.trim().toLowerCase(); | |
| let results = articles.slice(); | |
| if (selectedCategory) { | |
| results = results.filter((a) => | |
| (a.categories || []).includes(selectedCategory) | |
| ); | |
| if (term) { | |
| results = new Fuse(articles, { | |
| keys: ["author", "categories", "excerpt", "title"], | |
| includeScore: true, | |
| }) | |
| .search(term) | |
| .map((res) => res.item); | |
| } | |
| if (authorFilter.trim()) { | |
| const af = authorFilter.trim().toLowerCase(); | |
| results = results.filter((a) => | |
| (a.owner || "").toLowerCase().includes(af) | |
| ); | |
| if (selectedCategory) { | |
| results = results.filter((a) => a.categories.includes(selectedCategory)); | |
| } | |
| results.sort((a, b) => { | |
| if (dateSort === "newest") return Number(b.createdAt - a.createdAt); | |
| return Number(a.createdAt - b.createdAt); | |
| }); | |
| return results; | |
| }, [articles, dateSort, searchQuery, selectedCategory]); | |
| const filteredArticles = useMemo(() => { | |
| // No need to lowercase—Fuse is case-insensitive by default | |
| const term = searchQuery.trim(); | |
| let results = articles.slice(); | |
| // Apply category filtering first, so the search is scoped to this subset | |
| if (selectedCategory) { | |
| results = results.filter((a) => a.categories.includes(selectedCategory)); | |
| } | |
| if (term) { | |
| // Run Fuse against the filtered results | |
| results = new Fuse(results, { | |
| keys: ["author", "categories", "excerpt", "title"], | |
| includeScore: true, | |
| }) | |
| .search(term) | |
| .map((res) => res.item); | |
| } | |
| results.sort((a, b) => { | |
| if (dateSort === "newest") return Number(b.createdAt - a.createdAt); | |
| return Number(a.createdAt - b.createdAt); | |
| }); | |
| return results; | |
| }, [articles, dateSort, searchQuery, selectedCategory]); |
🤖 Prompt for AI Agents
In packages/app/src/pages/DiscoverPage.tsx around lines 74 to 97, the current
logic runs Fuse search on the full articles array and lowercases the term;
instead, apply the selectedCategory filter first to narrow the dataset, then run
Fuse against that filtered list (so searching and scoring are restricted to the
category), remove the manual term.toLowerCase() since Fuse is case-insensitive,
and keep the sort step after filtering/searching (sorting the final results by
createdAt according to dateSort).
This pull request introduces support for "collections" in the application, allowing users to view, search, and interact with grouped sets of articles. The main changes include adding new types and components for collections, updating routing to support collection pages, and enhancing the Discover page to browse both articles and collections. Additionally, there are minor UX improvements and some refactoring.
Collections Feature Implementation
Collectiontype toutils/types.ts, representing a group of articles with metadata such as title, description, cover image, author, and timestamps.CollectionCardcomponent for displaying collection summaries, including cover image, description, and author.CollectionPagecomponent that displays the details of a collection and its articles, with navigation and copy-to-clipboard functionality for the author's address.Routing Updates
Routes.tsxto include a route for individual collection pages (/collections/:id) and adjusted the articles route to/articles/:id. [1] [2] [3]Discover Page Enhancements
CollectionCardfor collection display. [1] [2] [3] [4] [5]Editor Room and UX Improvements
Summary by CodeRabbit
New Features
Improvements
API