Skip to content

Conversation

@ECWireless
Copy link
Member

@ECWireless ECWireless commented Aug 29, 2025

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

  • Added a new Collection type to utils/types.ts, representing a group of articles with metadata such as title, description, cover image, author, and timestamps.
  • Created a new CollectionCard component for displaying collection summaries, including cover image, description, and author.
  • Added a new CollectionPage component that displays the details of a collection and its articles, with navigation and copy-to-clipboard functionality for the author's address.

Routing Updates

  • Updated Routes.tsx to include a route for individual collection pages (/collections/:id) and adjusted the articles route to /articles/:id. [1] [2] [3]

Discover Page Enhancements

  • Enhanced the Discover page to allow users to toggle between browsing articles and collections, including search and sorting functionality for both content types. Integrated the new CollectionCard for collection display. [1] [2] [3] [4] [5]

Editor Room and UX Improvements

  • Added a check in the Editor Room to ensure the user is running the app in DUST, displaying a message if not. [1] [2] [3]
  • Improved the toast message after publishing a collection to suggest viewing it in the Discover page.

Summary by CodeRabbit

  • New Features

    • Browse and view Collections with a dedicated Collection page, Collection cards, and a public route for collections.
    • Discover now supports toggling between Articles and Collections, with unified search and improved sorting/filtering.
    • Editor Room integrates DUST detection and shows guidance when not available.
    • Article URLs standardized to /articles/:id.
  • Improvements

    • Publishing a collection shows a success toast linking users to Discover.
  • API

    • Added a public Collection data type for use across the app.

@vercel
Copy link

vercel bot commented Aug 29, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
thedailydust Ready Ready Preview Comment Aug 29, 2025 0:29am

@coderabbitai
Copy link

coderabbitai bot commented Aug 29, 2025

Walkthrough

Adds 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

Cohort / File(s) Summary of changes
Routing updates
packages/app/src/Routes.tsx
Added COLLECTION_PAGE_PATH ("/collections/") and route for /collections/:id rendering CollectionPage. Changed ARTICLE_PAGE_PATH from "/article/" to "/articles/". Imported CollectionPage.
Discover page & search
packages/app/src/pages/DiscoverPage.tsx
Added stash-backed collections loading, owner name decoding, Fuse-based unified search for articles and collections, contentType toggle between "articles" and "collections", date sorting, and rendering branch to show ArticleCard or CollectionCard.
Collection UI / page / types
packages/app/src/components/CollectionCard.tsx, packages/app/src/pages/CollectionPage.tsx, packages/app/src/utils/types.ts
New Collection type. New CollectionCard component with lazy image + shimmer. New CollectionPage that reads collection id, loads collection + related article ids and renders collection details and article list with navigation/fallback states.
EditorRoom Dust gate & types
packages/app/src/pages/EditorRoomPage.tsx
Integrated useDustClient and added early-return messages when Dust client loading/unavailable. Moved TabKey type to module scope; exported component signature unchanged.
Collection publish UX
packages/app/src/components/editor/CollectionWizard.tsx
Updated success toast message text after publishing to mention Discover page.

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
Loading
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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

I nibble paths and stitch a card,
Collections bloom and routes go far.
I sniff the Dust, then pause to see —
A page, a list, a friend to be.
Hop on, click through — hooray for me! 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/render-collections

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a 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 redirect

Changing /article/:id/articles/:id can 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 Discover

Great 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 constants

Inside the component (near other hooks):

+ const navigate = useNavigate();
packages/app/src/utils/types.ts (1)

1-11: Tighten IDs and addresses with branded hex types

Strengthen 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 redirects

Needed 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 too

Users 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> tags

Defining @keyframes per card bloats the DOM. Prefer a global CSS utility (e.g., .animate-shimmer) and a prefers-reduced-motion safeguard.

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 substring

Substring 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 date

Improves 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 coupling

If 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.

CollectionCard encodes 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 useCopy to catch clipboard errors and surface toast.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 encapsulates useRecords + owner decoding + uriToHttp + articleIds resolve + 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.

📥 Commits

Reviewing files that changed from the base of the PR and between f5065dd and a3b049e.

📒 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 new ARTICLE_PAGE_PATH constant; 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.

Comment on lines +76 to +90
if (!collection) {
return (
<div className="p-6">
<h1 className={cn("font-heading", "text-3xl")}>Collection not found</h1>
<p className="mt-2">
We couldn&apos;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>
);
}
Copy link

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&apos;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>
     );
   }

Comment on lines 158 to 164
<Link
to={`/articles/${article.id}`}
className="hover:underline"
>
{article.title}
</Link>
</h2>
Copy link

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.

Comment on lines 33 to 34
const { data: dustClient } = useDustClient();

Copy link

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.

Suggested change
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).

Comment on lines +16 to 18
export const ARTICLE_PAGE_PATH = "/articles/";
export const COLLECTION_PAGE_PATH = "/collections/";

Copy link

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.

Copy link

@coderabbitai coderabbitai bot left a 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 coercion

Avoid 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 keystroke

Precompute 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 type

The 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 semantics

Provide 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 query

Fuse 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.

📥 Commits

Reviewing files that changed from the base of the PR and between a3b049e and bd38888.

📒 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 projection

Good 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
Ensure getRecord is exported by the public @latticexyz/stash package, then replace every

import { 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.

Comment on lines +74 to +97
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]);
Copy link

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.

Suggested change
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).

@ECWireless ECWireless merged commit f1f6075 into main Aug 29, 2025
4 checks passed
@ECWireless ECWireless deleted the feat/render-collections branch August 29, 2025 01:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants