Dev#5
Conversation
- Update channels API to return channels grouped by category with uncategorized channels separate, both sorted by position - Wire up channel list component to fetch real data via useQuery - Add credentials: "include" to API client for auth cookies - Add seed script for populating test channels (apps/api/scripts/)
📝 WalkthroughWalkthroughThis PR replaces mock data with dynamic API-driven functionality across the application. The API adds a channel seeding script and modifies channel handlers to group channels by categories. The frontend refactors sidebar components to fetch and display guilds and channels from the API, introduces new authenticated guild routes, and includes credentials in HTTP requests. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In `@apps/web/src/components/sidebar/channel-panel/channel-list.tsx`:
- Around line 87-112: ChannelItem currently renders a <button> but has no click
handler so channel clicks do nothing; update ChannelItem to accept an onClick
prop (e.g., onClick?: () => void or onSelect channelName) and forward it to the
button element, or alternatively accept a href and use Next.js Link/useRouter to
navigate to the channel route inside ChannelItem; ensure the prop signature is
added to the function parameters (ChannelItem) and that the button uses that
handler (and add a TODO comment if this is intentionally WIP).
- Around line 28-39: The channels query uses useQuery with queryKey ["channels",
guildSlug] but calls apiClient.v1.channels.$get() without passing guild context,
which can return channels for the previous guild; fix by making the request
include the guild identifier (e.g., pass guildSlug or guildId as a query
param/body to apiClient.v1.channels.$get or use a dedicated endpoint like
apiClient.v1.channels._guild(guildSlug).$get) so the server returns the correct
guild's channels, or alternatively gate the query with the layout's setActive
completion (ensure setActive resolves before enabling the useQuery) by wiring
the enabled flag to the setActive promise resolution; reference useQuery,
queryKey ["channels", guildSlug], apiClient.v1.channels.$get(), and setActive in
the layout to locate the code to change.
In `@apps/web/src/components/sidebar/channel-panel/guild-header.tsx`:
- Around line 9-15: The queryKey includes guildSlug but the queryFn calls
authClient.organization.getFullOrganization() which ignores the slug, causing
stale/mismatched cache entries; fix by either (A) setting the active org before
fetching—call authClient.organization.setActive({ slug: guildSlug }) (e.g., in
an async queryFn or a prefetch/side-effect) then call getFullOrganization(), or
(B) replace getFullOrganization() with an API method that accepts the slug
(e.g., organization.getBySlug(guildSlug) or similar) so the queryFn uses
guildSlug directly; update the useQuery queryFn accordingly to ensure the
returned data corresponds to the queryKey.
In `@apps/web/src/routes/_authenticated/`$guildSlug.tsx:
- Around line 13-26: The query functions for useQuery (queryFn for keys
["guilds"] and ["active-guild", guildSlug]) assume res.data exists; instead
check the API response and surface errors to React Query by throwing when data
is missing or res.error is present. Update the queryFn for
authClient.organization.list() and authClient.organization.getFullOrganization()
to validate the response (e.g., if (!res.data) throw new Error(...) including
res.error/message/status) so that guilds and activeOrg are never silently
undefined and React Query will set an error state.
- Around line 33-37: The useEffect that calls authClient.organization.setActive
inside the component currently treats setActive as fire-and-forget (useEffect
referencing guild and activeOrg?.id); update it to await the async
authClient.organization.setActive({ organizationId: guild.id }) call,
handle/rethrow or log errors, and after it completes explicitly invalidate or
refetch downstream queries that depend on the active organization (e.g.,
channels and active-guild queries) so state is consistent with activeOrg; ensure
the effect is async-safe (avoid unmounted updates) and only runs when guild
exists and activeOrg?.id !== guild.id.
🧹 Nitpick comments (4)
apps/api/src/app.ts (1)
35-49: Remove commented-out dead code.The old loop-based routing is superseded by the chained approach on Line 31. Keeping ~15 lines of commented code adds noise; it's preserved in git history if needed.
apps/api/scripts/seed-channels.ts (1)
56-107: Wrap the seed in a transaction to avoid partial state on failure.If an insert fails mid-way, the guild is left with its channels already deleted but only partially re-seeded. Wrapping in
db.transaction(async (tx) => { ... })and usingtxfor all operations prevents this.Also, uncategorized channels and category rows both start
positionat0. If the API sorts all top-level entries (whereparentIdis null) byposition, these will interleave unpredictably. Consider offsetting category positions:- position: catIdx, + position: uncategorized.length + catIdx,apps/web/src/components/sidebar/guild-bar/guild-bar.tsx (2)
61-74: Consider extracting shared guild/activeOrg queries into a custom hook.The same two queries (
["guilds"]and["active-guild", guildSlug]) with identicalqueryFnimplementations exist in both this component and$guildSlug.tsx. While React Query deduplicates at runtime, the duplicated code will drift over time. A shared hook likeuseGuilds()/useActiveOrg()would keep them in sync.Also,
res.datais returned without checking for errors — same concern as noted in the layout file.
18-22: Edge case:w[0]isundefinedfor empty-string segments.If
namecontains consecutive spaces (e.g."Foo Bar"),split(" ")produces empty strings andw[0]yieldsundefined. The result still works (joins as"undefinedF"etc.) but produces unexpected initials.A safer approach:
const initials = name .split(" ") + .filter(Boolean) .map((w) => w[0]) .join("") .slice(0, 2)
| const { data } = useQuery({ | ||
| queryKey: ["channels", guildSlug], | ||
| queryFn: async () => { | ||
| const res = await apiClient.v1.channels.$get() | ||
| if (!res.ok) { | ||
| throw new Error("Failed to fetch channels") | ||
| } | ||
| const json = await res.json() | ||
| return json.data | ||
| }, | ||
| enabled: !!guildSlug, | ||
| }) |
There was a problem hiding this comment.
Channel query depends on server-side active org but doesn't pass guild context explicitly.
apiClient.v1.channels.$get() doesn't include guildSlug (or guild ID) in the request. The query key includes guildSlug, so navigating between guilds triggers a refetch, but the server determines which guild's channels to return based on session state. Combined with the fire-and-forget setActive call in the layout, there's a window where this query fetches channels for the previous guild.
Consider either passing the guild ID as a query parameter, or ensuring setActive completes before this query is enabled.
🤖 Prompt for AI Agents
In `@apps/web/src/components/sidebar/channel-panel/channel-list.tsx` around lines
28 - 39, The channels query uses useQuery with queryKey ["channels", guildSlug]
but calls apiClient.v1.channels.$get() without passing guild context, which can
return channels for the previous guild; fix by making the request include the
guild identifier (e.g., pass guildSlug or guildId as a query param/body to
apiClient.v1.channels.$get or use a dedicated endpoint like
apiClient.v1.channels._guild(guildSlug).$get) so the server returns the correct
guild's channels, or alternatively gate the query with the layout's setActive
completion (ensure setActive resolves before enabling the useQuery) by wiring
the enabled flag to the setActive promise resolution; reference useQuery,
queryKey ["channels", guildSlug], apiClient.v1.channels.$get(), and setActive in
the layout to locate the code to change.
| function ChannelItem({ | ||
| name, | ||
| type, | ||
| active = false, | ||
| }: { | ||
| name: string | ||
| type: string | ||
| active?: boolean | ||
| }) { | ||
| return ( | ||
| <button | ||
| type="button" | ||
| className={cn( | ||
| "relative flex w-full items-center gap-2 rounded-lg px-2 py-[6px] text-[14px] hover:bg-foreground/[0.06]", | ||
| active | ||
| ? "bg-foreground/[0.06] font-medium text-foreground" | ||
| : "text-muted-foreground" | ||
| )} | ||
| > | ||
| {active && ( | ||
| <div className="absolute left-0 top-1/2 h-4 w-[3px] -translate-y-1/2 rounded-r-full bg-primary" /> | ||
| )} | ||
| <ChannelIcon type={type} /> | ||
| <span className="truncate">{name}</span> | ||
| </button> | ||
| ) |
There was a problem hiding this comment.
ChannelItem buttons have no click handler — channels aren't navigable.
The items render as <button> elements but have no onClick, so clicking a channel does nothing. If this is intentional WIP, consider adding a TODO comment. Otherwise, wire up navigation to a channel route.
Would you like me to help scaffold the channel navigation logic?
🤖 Prompt for AI Agents
In `@apps/web/src/components/sidebar/channel-panel/channel-list.tsx` around lines
87 - 112, ChannelItem currently renders a <button> but has no click handler so
channel clicks do nothing; update ChannelItem to accept an onClick prop (e.g.,
onClick?: () => void or onSelect channelName) and forward it to the button
element, or alternatively accept a href and use Next.js Link/useRouter to
navigate to the channel route inside ChannelItem; ensure the prop signature is
added to the function parameters (ChannelItem) and that the button uses that
handler (and add a TODO comment if this is intentionally WIP).
| const { data: activeOrg } = useQuery({ | ||
| queryKey: ["active-guild", guildSlug], | ||
| queryFn: async () => { | ||
| const res = await authClient.organization.getFullOrganization() | ||
| return res.data | ||
| }, | ||
| }) |
There was a problem hiding this comment.
queryFn doesn't use guildSlug, but queryKey implies it should.
The query key includes guildSlug, so React Query treats each slug as a distinct cache entry. However, getFullOrganization() fetches whatever org is currently active on the auth client session — it doesn't accept a slug parameter. If the active org isn't switched before this component renders with a new guildSlug, you'll get stale/incorrect data cached under the new key.
Consider either:
- Setting the active organization (e.g.,
authClient.organization.setActive({ slug: guildSlug })) before or within the query, or - Passing the slug to an API that resolves the org by slug.
🤖 Prompt for AI Agents
In `@apps/web/src/components/sidebar/channel-panel/guild-header.tsx` around lines
9 - 15, The queryKey includes guildSlug but the queryFn calls
authClient.organization.getFullOrganization() which ignores the slug, causing
stale/mismatched cache entries; fix by either (A) setting the active org before
fetching—call authClient.organization.setActive({ slug: guildSlug }) (e.g., in
an async queryFn or a prefetch/side-effect) then call getFullOrganization(), or
(B) replace getFullOrganization() with an API method that accepts the slug
(e.g., organization.getBySlug(guildSlug) or similar) so the queryFn uses
guildSlug directly; update the useQuery queryFn accordingly to ensure the
returned data corresponds to the queryKey.
| const { data: guilds, isPending: guildsLoading } = useQuery({ | ||
| queryKey: ["guilds"], | ||
| queryFn: async () => { | ||
| const res = await authClient.organization.list() | ||
| return res.data | ||
| }, | ||
| }) | ||
| const { data: activeOrg } = useQuery({ | ||
| queryKey: ["active-guild", guildSlug], | ||
| queryFn: async () => { | ||
| const res = await authClient.organization.getFullOrganization() | ||
| return res.data | ||
| }, | ||
| }) |
There was a problem hiding this comment.
No error handling for API calls — res.data may be undefined.
Both authClient.organization.list() and authClient.organization.getFullOrganization() can return error responses where res.data is undefined. Returning undefined silently from queryFn means guilds could be undefined even after loading completes (isPending is false), yet the code assumes it's an array on line 29.
Consider checking res.error or throwing so React Query surfaces the error state.
🤖 Prompt for AI Agents
In `@apps/web/src/routes/_authenticated/`$guildSlug.tsx around lines 13 - 26, The
query functions for useQuery (queryFn for keys ["guilds"] and ["active-guild",
guildSlug]) assume res.data exists; instead check the API response and surface
errors to React Query by throwing when data is missing or res.error is present.
Update the queryFn for authClient.organization.list() and
authClient.organization.getFullOrganization() to validate the response (e.g., if
(!res.data) throw new Error(...) including res.error/message/status) so that
guilds and activeOrg are never silently undefined and React Query will set an
error state.
| useEffect(() => { | ||
| if (!guild) return | ||
| if (activeOrg?.id === guild.id) return | ||
| authClient.organization.setActive({ organizationId: guild.id }) | ||
| }, [guild, activeOrg?.id]) |
There was a problem hiding this comment.
setActive is fire-and-forget — errors are silently swallowed and dependent queries aren't invalidated.
When the active organization is switched, downstream queries (channels, active-guild) depend on that state being committed server-side. Without awaiting setActive and invalidating queries afterward, there's a race where activeOrg and channel data may reflect the previous guild.
Proposed fix
+ import { useQueryClient } from "@tanstack/react-query"
...
function GuildLayout() {
+ const queryClient = useQueryClient()
...
useEffect(() => {
if (!guild) return
if (activeOrg?.id === guild.id) return
- authClient.organization.setActive({ organizationId: guild.id })
+ authClient.organization.setActive({ organizationId: guild.id }).then(() => {
+ queryClient.invalidateQueries({ queryKey: ["active-guild"] })
+ queryClient.invalidateQueries({ queryKey: ["channels"] })
+ })
}, [guild, activeOrg?.id])🤖 Prompt for AI Agents
In `@apps/web/src/routes/_authenticated/`$guildSlug.tsx around lines 33 - 37, The
useEffect that calls authClient.organization.setActive inside the component
currently treats setActive as fire-and-forget (useEffect referencing guild and
activeOrg?.id); update it to await the async authClient.organization.setActive({
organizationId: guild.id }) call, handle/rethrow or log errors, and after it
completes explicitly invalidate or refetch downstream queries that depend on the
active organization (e.g., channels and active-guild queries) so state is
consistent with activeOrg; ensure the effect is async-safe (avoid unmounted
updates) and only runs when guild exists and activeOrg?.id !== guild.id.
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes