feat: Implement the initial web application structure, including a dashboard, event management, and core UI components.#39
Conversation
…shboard, event management, and core UI components.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughThis pull request redesigns the application from a neumorphic to glassmorphism aesthetic, restructures routing with a new Changes
Sequence Diagram(s)sequenceDiagram
participant User as User/Browser
participant App as Dashboard Page
participant Auth as Auth Service
participant API as API Routes
participant DB as Supabase DB
participant Cache as Fetch Cache
User->>App: Load /dashboard
App->>Auth: Get current user
Auth-->>App: User data
App->>App: Initialize useDashboardStats
App->>Cache: Check cached stats
Cache-->>App: Cache miss
par Concurrent Loads
App->>API: GET /api/dashboard/stats
App->>API: GET /api/events
end
API->>Auth: Authenticate request
API->>DB: Query event memberships & photos
DB-->>API: Photos & bytes data
API-->>App: { totalPhotos, totalBytes }
API->>DB: Query user events
DB-->>API: Events list
API-->>App: Events array
App->>Cache: Store stats + events
App->>App: Render dashboard with stats & event grid
User->>App: Click on event
App->>App: Navigate to /dashboard/events/[id]
sequenceDiagram
participant User as User/Browser
participant Page as Event Detail Page
participant Hook as useEvent/usePhotos
participant API as API Routes
participant DB as Supabase DB
participant Auth as Auth Service
User->>Page: Load /dashboard/events/[id]
Page->>Hook: useEvent(id) + usePhotos(id)
Hook->>Auth: Get user role for event
Hook->>API: GET /api/events/[id]
API->>DB: Fetch event + members
DB-->>API: Event data
API-->>Hook: Event details
par Tab Navigation
User->>Page: Click "Gallery" tab
Hook->>API: GET /api/photos?eventId=[id]
API->>DB: Fetch photos (paginated)
DB-->>API: Photo data
API-->>Hook: Photos array + pagination
and
User->>Page: Click "Guests" tab
Page->>Page: Render event.event_members
and
User->>Page: Click "Settings" tab
Page->>Hook: Show EditEventForm (if host)
end
Hook-->>Page: Render EventGallery with photos
User->>Page: Upload photo
Hook->>API: POST upload to signed URL
API->>DB: Activate photo record
Hook->>Hook: Invalidate gallery cache
Page->>Page: Refresh gallery display
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment Tip CodeRabbit can use your project's `biome` configuration to improve the quality of JS/TS/CSS/JSON code reviews.Add a configuration file to your project to customize how CodeRabbit runs |
There was a problem hiding this comment.
Pull request overview
This PR establishes an initial “dashboard-first” web app structure for apps/vibely-web, adding a new dashboard layout/page, a dashboard stats API endpoint, and new reusable UI/components (navbar, KPIs, event grid, notifications UI), along with some API route refactoring for notifications.
Changes:
- Introduces a new
/dashboardlayout + redesigned dashboard page (greeting, KPIs, event grid, create-event card). - Adds
/api/dashboard/statsendpoint and client hooks to fetch/display aggregated photo/storage KPIs. - Refactors notifications API responses and adds a notifications dropdown + hook on the client.
Reviewed changes
Copilot reviewed 27 out of 28 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Locks new dependency versions (adds date-fns). |
| packages/shared/types/database.types.ts | Minor generated-types formatting tweak. |
| apps/vibely-web/package.json | Adds date-fns dependency for notification time formatting. |
| apps/vibely-web/lib/notify.ts | Formats notification insert for readability. |
| apps/vibely-web/hooks/useNotifications.ts | Adds notifications fetching + mark-all-read helper hook. |
| apps/vibely-web/hooks/useDashboardStats.ts | Adds dashboard stats hook + byte formatting helper. |
| apps/vibely-web/components/ui/SearchBar.tsx | New search bar UI component for dashboard navbar. |
| apps/vibely-web/components/ui/Logo.tsx | New logo component linking to /dashboard. |
| apps/vibely-web/components/ui/KPIBadge.tsx | New KPI pill component used by dashboard KPIs. |
| apps/vibely-web/components/ui/IconButton.tsx | New icon-only button primitive for navbar actions. |
| apps/vibely-web/components/layout/SettingsButton.tsx | Adds settings icon button in navbar. |
| apps/vibely-web/components/layout/ProfileButton.tsx | Adds profile dropdown button (avatar + menu). |
| apps/vibely-web/components/layout/NotificationDropdown.tsx | Adds notifications dropdown using date-fns relative time formatting. |
| apps/vibely-web/components/layout/NavBar.tsx | Replaces prior navbar with new dashboard-focused navbar composition. |
| apps/vibely-web/components/layout/Footer.tsx | Adds dashboard footer links. |
| apps/vibely-web/components/events/EventGrid.tsx | Adds grid layout wrapper for event cards. |
| apps/vibely-web/components/events/EventCard.tsx | Redesigns event card UI and props shape for dashboard grid. |
| apps/vibely-web/components/events/CreateEventCard.tsx | Adds “Create New Event” card for the event grid. |
| apps/vibely-web/components/dashboard/OverviewHeader.tsx | Adds greeting header component for dashboard. |
| apps/vibely-web/components/dashboard/KPIGroup.tsx | Adds KPI group rendering (events/photos/vault). |
| apps/vibely-web/app/layout.tsx | Removes global navbar mount; applies background styling at root main. |
| apps/vibely-web/app/dashboard/page.tsx | Rebuilds dashboard page to use new components and dashboard stats hook. |
| apps/vibely-web/app/dashboard/layout.tsx | Adds new dashboard layout wrapper (navbar/main/footer). |
| apps/vibely-web/app/api/notifications/route.ts | Refactors notification list + mark-read handler formatting. |
| apps/vibely-web/app/api/notifications/[id]/route.ts | Refactors single-notification mark-read handler formatting. |
| apps/vibely-web/app/api/dashboard/stats/route.ts | Adds new stats aggregation endpoint (photos count + vault bytes). |
| apps/vibely-web/app/(auth)/login/page.tsx | Cleans up unused icon imports. |
| AGENTS.md | Removes outdated agent/conventions documentation file. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/vibely-web/app/api/notifications/route.ts (1)
80-114:⚠️ Potential issue | 🟠 MajorNarrow the
catchscope to avoid masking server failures as 400.Lines 112-113 currently return
Invalid JSONfor any thrown error inside the whole block, including unexpected server-side failures.Proposed change
export async function PATCH(req: Request) { @@ - try { - const body = await req.json(); + let body: unknown; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); + } + + try { const result = markReadSchema.safeParse(body); @@ - } catch { - return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); + } catch { + return NextResponse.json( + { error: "Failed to update notifications" }, + { status: 500 } + ); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/vibely-web/app/api/notifications/route.ts` around lines 80 - 114, The catch currently wraps the entire handler and maps any thrown error to a 400 "Invalid JSON", which masks server failures; narrow the scope by separating JSON parsing from the rest: call await req.json() inside its own try/catch (or use a dedicated try for the parse) and return 400 only if markReadSchema.safeParse/req.json() fails, while wrapping the database update (supabase.from(...).update(...) / updateQuery) in a separate try that on unexpected errors logs the error and returns a 500; reference markReadSchema, req.json(), updateQuery and the supabase update flow when making the change.
🧹 Nitpick comments (10)
apps/vibely-web/components/ui/SearchBar.tsx (1)
23-29: Add an explicit accessible name for the input.Line 23 renders an unlabeled text input (placeholder is not a reliable label). Please add an
aria-label(oraria-labelledby) prop path so this is consistently discoverable by assistive tech.Proposed change
interface SearchBarProps { placeholder?: string; className?: string; + ariaLabel?: string; onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void; value?: string; } export default function SearchBar({ placeholder = "Search events...", className = "", + ariaLabel = "Search events", onChange, value, }: SearchBarProps) { @@ <input type="text" + aria-label={ariaLabel} className="block w-full pl-11 pr-4 py-2.5 bg-gray-100/80 border-transparent rounded-full text-sm placeholder-gray-400 focus:border-indigo-300 focus:bg-white focus:ring-4 focus:ring-indigo-500/10 transition-all outline-none"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/vibely-web/components/ui/SearchBar.tsx` around lines 23 - 29, The input in the SearchBar component lacks an accessible name; update the input element inside SearchBar (the <input> using placeholder, onChange, value) to include an explicit aria-label or aria-labelledby prop (e.g., accept an ariaLabel prop on SearchBar or derive one from placeholder) and pass it through to the input so assistive tech can reliably identify the field; ensure the new prop is optional with a sensible default and used in the input element.apps/vibely-web/components/layout/ProfileButton.tsx (1)
64-68: Expose dropdown state on the trigger button.Please add
aria-haspopup="menu"andaria-expanded={isOpen}on the trigger so assistive tech can understand menu state.Also applies to: 84-85
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/vibely-web/components/layout/ProfileButton.tsx` around lines 64 - 68, The profile menu trigger button in the ProfileButton component doesn't expose its dropdown state to assistive tech; update the button(s) that call setIsOpen(!isOpen) (the trigger in ProfileButton and the other trigger usage) to include aria-haspopup="menu" and aria-expanded={isOpen} so screen readers can detect the menu role and whether it's open; keep existing attributes and only add these two props to the same <button> elements that toggle isOpen.apps/vibely-web/app/dashboard/layout.tsx (1)
9-13: Redundant max-width and excessive nested padding.The layout has two issues:
max-w-[1440px]is applied to both the outerdiv(line 9) and the innermain(line 11). The inner constraint has no effect since the parent already limits the width.- Nested padding (
px-12on outer +px-6on main) results in 72px + 24px = 96px total horizontal padding, which may be excessive and inconsistent with NavBar's ownpx-6.🔧 Proposed simplification
export default function DashboardLayout({ children }: { children: ReactNode }) { return ( - <div className="min-h-screen bg-[`#F0F2f5`] flex flex-col font-sans text-gray-900 max-w-[1440px] mx-auto px-12"> + <div className="min-h-screen bg-[`#F0F2F5`] flex flex-col font-sans text-gray-900 max-w-[1440px] mx-auto"> <NavBar /> - <main className="flex-1 w-full max-w-[1440px] mx-auto px-6 py-8"> + <main className="flex-1 w-full px-6 py-8"> {children} </main> <Footer /> </div> ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/vibely-web/app/dashboard/layout.tsx` around lines 9 - 13, The outer layout applies duplicate width and layered padding causing redundant constraints and excessive horizontal spacing: remove the inner max-w-[1440px] on the main element and consolidate padding so it isn't additive (e.g., keep px-12 on the outer div and change main's px-6 to px-0 or remove main padding), updating the JSX in layout.tsx where the container div (className containing min-h-screen bg... max-w-[1440px] mx-auto px-12) and the main element (className containing flex-1 w-full max-w-[1440px] mx-auto px-6 py-8) are defined; ensure NavBar (NavBar component referenced in this layout) alignment still matches by keeping a single consistent horizontal padding value.apps/vibely-web/components/layout/NotificationDropdown.tsx (2)
84-104: Layout inconsistency for read vs unread notifications.When a notification is read (
notif.is_read === true), the unread indicator dot is not rendered, but no equivalent spacing is added. This causes the text content to shift left for read items compared to unread items, creating visual misalignment in the list.🔧 Proposed fix to maintain consistent alignment
<Link key={notif.id} href={"#"} // TODO: Add link path onClick={() => setIsOpen(false)} className={`p-4 border-b last:border-0 border-gray-50 hover:bg-gray-50 transition-colors flex gap-3 ${!notif.is_read ? "bg-indigo-50/20" : ""}`} > - {!notif.is_read && ( - <div className="mt-1.5 w-2 h-2 rounded-full bg-indigo-500 flex-shrink-0" /> - )} + <div className={`mt-1.5 w-2 h-2 rounded-full flex-shrink-0 ${!notif.is_read ? "bg-indigo-500" : "bg-transparent"}`} /> <div className="flex flex-col gap-1 w-full">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/vibely-web/components/layout/NotificationDropdown.tsx` around lines 84 - 104, The notification list items shift because the unread indicator dot is only rendered when notif.is_read is false; update the NotificationDropdown notification rendering (inside notifications.map / the Link element) to always reserve the same space for the indicator: when notif.is_read is false render the existing colored dot, otherwise render an invisible spacer element with the same dimensions and margin (e.g., a div with class "mt-1.5 w-2 h-2 flex-shrink-0" but transparent or visibility-hidden) so text alignment stays consistent between read and unread items.
99-102: Consider handling invalid dates gracefully.If
notif.created_atis null, undefined, or an invalid date string,new Date(notif.created_at)will produce an "Invalid Date", andformatDistanceToNowmay throw or display unexpected output.🛡️ Proposed defensive date handling
<span className="text-[11px] text-gray-500 font-medium"> - {formatDistanceToNow(new Date(notif.created_at), { - addSuffix: true, - })} + {notif.created_at + ? formatDistanceToNow(new Date(notif.created_at), { + addSuffix: true, + }) + : "Unknown time"} </span>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/vibely-web/components/layout/NotificationDropdown.tsx` around lines 99 - 102, Handle potential invalid or missing dates for notif.created_at in NotificationDropdown.tsx by validating the parsed date before passing it to formatDistanceToNow; for example, parse notif.created_at into a Date (or use date-fns parseISO) and check isValid(date) (or fallback on a safe value) and then call formatDistanceToNow only when valid, otherwise render a fallback string like "unknown" or omit the timestamp. Ensure you update the span that currently contains formatDistanceToNow(new Date(notif.created_at), { addSuffix: true }) to perform this validation and use the fallback to avoid throwing or showing "Invalid Date".apps/vibely-web/components/layout/NavBar.tsx (1)
34-37: Conflicting flex behavior may cause unexpected layout.The SearchBar wrapper has
flex-1which tries to grow, but its parent container hasflex-shrink-0which prevents shrinking. This combination inside a justifiedspace-betweenlayout may not behave as intended—the SearchBar won't actually grow since there's no available space to fill.Consider whether
flex-1is needed here, or if a fixedmax-w-*alone would suffice.🔧 Suggested simplification
- <div className="flex-1 max-w-3xl mx-2 hidden md:block"> + <div className="max-w-md mx-2 hidden md:block"> <SearchBar className="w-full shadow-sm rounded-full border-2 border-zinc-300/50" /> </div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/vibely-web/components/layout/NavBar.tsx` around lines 34 - 37, The wrapper div around SearchBar uses conflicting flex utilities (flex-1 on the inner wrapper and flex-shrink-0 on its parent), which prevents the SearchBar from growing as intended; fix by removing the flex-1 class from the inner wrapper (keep max-w-3xl and w-full on SearchBar) or alternatively remove flex-shrink-0 from the parent if you want the whole group to be flexible—adjust the class list on the div containing SearchBar and the parent div to use either fixed max width or flexible growth, not both (refer to the wrapper div with class "flex-1 max-w-3xl mx-2 hidden md:block" and the parent div with "flex items-center gap-4 flex-shrink-0").apps/vibely-web/components/dashboard/OverviewHeader.tsx (1)
8-11: Hardcoded "Good evening" greeting is time-insensitive.The greeting always says "Good evening" regardless of the actual time of day. Consider dynamically selecting the greeting based on the current hour.
🔧 Proposed time-aware greeting
+function getGreeting(): string { + const hour = new Date().getHours(); + if (hour < 12) return "Good morning"; + if (hour < 18) return "Good afternoon"; + return "Good evening"; +} + export default function OverviewHeader({ userName }: OverviewHeaderProps) { return ( <div className="flex flex-col gap-1"> <h1 className="text-4xl font-extrabold text-[`#1a1b26`] tracking-tight"> - Good evening, {userName}{" "} + {getGreeting()}, {userName}{" "} <span className="inline-block animate-wave">👋</span> </h1>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/vibely-web/components/dashboard/OverviewHeader.tsx` around lines 8 - 11, Replace the hardcoded "Good evening" in the OverviewHeader component with a time-aware greeting: add a small helper (e.g., getGreeting or useMemo within OverviewHeader) that reads the current hour via new Date().getHours() and returns "Good morning"/"Good afternoon"/"Good evening" (and optionally "Good night") based on ranges, then render that greeting before {userName} (keep the existing <span className="inline-block animate-wave">👋</span>). Ensure the logic is colocated with the OverviewHeader component so it updates on client render and references the existing userName variable.apps/vibely-web/components/dashboard/KPIGroup.tsx (1)
21-22: Minor:formatNumrecreated on each render.The
formatNumfunction and itsIntl.NumberFormatinstance are recreated on every render. While the performance impact is minimal, moving it outside the component would be cleaner.🔧 Proposed optimization
+const numberFormatter = new Intl.NumberFormat("en-US"); +const formatNum = (num: number) => numberFormatter.format(num); + export default function KPIGroup({ totalEvents, totalPhotos, formattedSize, }: KPIGroupProps) { - // Simple number formatting for thousands - const formatNum = (num: number) => new Intl.NumberFormat("en-US").format(num); - return (🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/vibely-web/components/dashboard/KPIGroup.tsx` around lines 21 - 22, The formatNum function currently constructs a new Intl.NumberFormat on every render; move the formatter to module scope and reuse it: create a module-level const (e.g., formatter = new Intl.NumberFormat("en-US")) and update formatNum (or replace it) to call formatter.format(num) so KPIGroup no longer recreates the Intl.NumberFormat on each render; update any references to formatNum inside the component to use the module-scoped formatter-based function.apps/vibely-web/components/events/EventCard.tsx (1)
8-16: Unusedtagsprop in interface.The
tagsprop is defined inEventCardProps(line 15) but is commented out in the destructuring (line 25) and rendering (lines 72-82). Either implement the tags feature or remove the prop from the interface to avoid confusion.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/vibely-web/components/events/EventCard.tsx` around lines 8 - 16, EventCardProps declares a tags: string[] prop but the EventCard component currently omits/destructures and doesn't render it; either remove tags from EventCardProps or implement the feature—preferably implement: add tags to the EventCard function parameter destructuring (e.g., ... , tags), and in the JSX map over tags to render a small list of elements (span/div) with a stable key (e.g., `tag-${i}` or the tag value), appropriate className for styling, and guard against undefined/empty arrays (render nothing when tags.length === 0). Ensure references to EventCardProps, the EventCard component signature, and the tags rendering block are updated consistently.apps/vibely-web/app/dashboard/page.tsx (1)
18-22: Surface dashboard stats failures instead of silently rendering0KPIs.
useDashboardStats()exposes anerror, but it’s not consumed. If stats fetch fails, Line 56–60 still renders KPI values from defaults, which can mislead users.Suggested patch
const { totalPhotos, formattedSize, isLoading: statsLoading, + error: statsError, } = useDashboardStats(); @@ - {statsLoading || eventsLoading ? ( + {statsError ? ( + <div className="rounded-xl border border-red-100 bg-red-50 px-4 py-2 text-sm text-red-700"> + {statsError} + </div> + ) : statsLoading || eventsLoading ? ( <div className="flex gap-4 animate-pulse"> <div className="h-10 w-28 bg-gray-200 rounded-full"></div> <div className="h-10 w-28 bg-gray-200 rounded-full"></div> <div className="h-10 w-28 bg-gray-200 rounded-full"></div> </div> ) : (Also applies to: 49-61
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/vibely-web/app/dashboard/page.tsx` around lines 18 - 22, The dashboard currently ignores the error from useDashboardStats and renders default KPI zeros; update the component to destructure error (e.g., const { totalPhotos, formattedSize, isLoading: statsLoading, error } = useDashboardStats()) and, when error is truthy, render an explicit error state (banner/message or disabled KPI placeholders) instead of the default numeric KPIs so failures are visible to users; ensure the KPI rendering logic (the block that reads totalPhotos/formattedSize when statsLoading is false) checks error and uses the error-path UI rather than the fallback zeros.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/vibely-web/app/api/dashboard/stats/route.ts`:
- Around line 39-47: The route currently masks query failures by treating failed
supabase results (e.g., photosError/photosCount and similarly
vaultError/vaultCount) as successful zero counts; update the handling so that if
photosError or vaultError is truthy or the count is null you surface the error
instead of silently setting totalPhotos/totalVaultFiles to 0—either throw or
return a 5xx response with the error details (or include an errors field in the
JSON response) and only set totalPhotos/totalVaultFiles when count is non-null
and error is falsy; locate the supabase queries for photos
(photosCount/photosError) and the vault query (vaultCount/vaultError) and
implement this explicit error-path behavior.
In `@apps/vibely-web/app/dashboard/page.tsx`:
- Around line 93-95: The Unsplash fallback URL used when computing coverImg (in
the EventCard / cover_image_url logic) is hosted on images.unsplash.com but that
domain isn't allowed by Next.js image remotePatterns; update next.config.ts to
add a remotePatterns entry allowing protocol "https", hostname
"images.unsplash.com" and pathname "/**" so Next/Image can load the fallback
image at runtime.
In `@apps/vibely-web/components/dashboard/OverviewHeader.tsx`:
- Line 10: The component OverviewHeader.tsx uses a non-existent CSS class
"animate-wave" on the element with the waving emoji; add a matching animation
definition in your Tailwind config by extending theme.extend.animation with a
name like "wave" (or "animate-wave") and adding the corresponding keyframes
under theme.extend.keyframes, or remove/replace the class in OverviewHeader.tsx
with an existing Tailwind utility such as "animate-fade-in"; update
tailwind.config.ts (theme.extend.animation and theme.extend.keyframes) to
include the wave keyframes and animation name so the class used in
OverviewHeader.tsx is defined.
In `@apps/vibely-web/components/events/EventCard.tsx`:
- Around line 85-95: The buttons inside EventCard.tsx (the conditional buttons
rendered when isExpired is true/false) are nested within a Link and cause double
navigation; fix by either moving the interactive button elements out of the Link
wrapper so the card Link and the action buttons are separate clickable areas, or
add an onClick handler on the buttons that calls e.preventDefault() and
e.stopPropagation() (and then performs the intended button action) to prevent
the Link from also activating; update the JSX around the conditional buttons and
the Link accordingly (referencing isExpired and the View Archive/View Vault
button elements) so only the intended click handler runs.
In `@apps/vibely-web/components/layout/Footer.tsx`:
- Line 7: The footer currently hardcodes "© 2024 Vibely. All rights reserved."
in the Footer component; update it to render the current year dynamically (e.g.,
use new Date().getFullYear()) instead of the literal 2024 so the span inside
Footer displays the current year automatically; locate the span in Footer.tsx
and replace the hardcoded year string with an expression that computes the year
at runtime.
In `@apps/vibely-web/components/layout/ProfileButton.tsx`:
- Around line 56-60: The handleThemeToggle function is a placeholder that only
logs to console; replace it with real theme toggling by wiring into your app's
theme state (e.g., a ThemeContext or the existing theme provider). In
ProfileButton's handleThemeToggle, call the context's toggleTheme or setTheme
function (or create a ThemeContext with currentTheme and toggleTheme if none
exists), update/persist the selected theme (e.g., localStorage or provider
state), and remove the console.log; ensure the component consumes the context
(useContext(ThemeContext)) and that toggleTheme flips between 'light' and 'dark'
so the UI action actually changes the app theme.
- Around line 50-54: The sign-out flow in handleSignOut calls
supabase.auth.signOut() but always redirects via router.push("/login") even if
signOut fails; change handleSignOut to await the signOut result and handle
errors: call createClient() then either use try/catch around await
supabase.auth.signOut() or check the returned { error } object, and only call
router.push("/login") when there is no error; on error, log it (or surface to
the user via existing toast/error UI) and avoid the redirect so the auth state
stays consistent.
In `@apps/vibely-web/components/layout/SettingsButton.tsx`:
- Around line 7-10: The component currently nests Link and IconButton (with
Settings icon), which creates an anchor wrapping a button; change this to a
single interactive element by rendering either the Link as the button or
rendering the IconButton as the link (do not nest). Concretely, update
SettingsButton.tsx to remove the outer Link/IconButton nesting: use Link as the
sole interactive element styled/accessorized like IconButton (including
aria-label and the Settings icon), or modify IconButton to render as an
anchor/Link (e.g., an "as" or "component" prop) and pass href="/settings" to
IconButton so it becomes the link; ensure aria-label remains and the Settings
icon and className "stroke-[2.5]" are preserved.
In `@apps/vibely-web/components/ui/IconButton.tsx`:
- Around line 15-20: IconButton currently renders a <button> that defaults to
type="submit" in forms; update the IconButton component to provide a safe
default by adding a type prop default of "button" (e.g., destructure type =
"button" from props or use props.type ?? "button") and pass that type into the
rendered <button>, ensuring callers can still override by providing their own
type in props; reference the IconButton component and the <button> element in
the component when making this change.
In `@apps/vibely-web/components/ui/Logo.tsx`:
- Around line 11-24: The Logo component's Link becomes unlabeled when iconOnly
is true; update the Link (in the Logo component) to provide an accessible name
by adding an aria-label (e.g., aria-label="Vibely") or rendering visually-hidden
text when iconOnly is true, and consider exposing a prop (like ariaLabel) to
allow custom labels; ensure the accessible name is present on the Link element
that wraps AutoAwesomeIcon so screen readers always have a label.
In `@apps/vibely-web/hooks/useNotifications.ts`:
- Around line 49-51: The catch in markAllAsRead currently only logs errors, so
update the useNotifications hook to surface failures: add hook state (e.g.,
isMarkingAll and markAllError) to useNotifications, set isMarkingAll = true when
markAllAsRead starts, on success set isMarkingAll = false and clear
markAllError, and in the catch set isMarkingAll = false and store the caught
error into markAllError (or a normalized message) so callers can read the
failure state; ensure markAllAsRead still resolves/rejects consistently with the
new state semantics so consumers can react to both state and promise outcome.
---
Outside diff comments:
In `@apps/vibely-web/app/api/notifications/route.ts`:
- Around line 80-114: The catch currently wraps the entire handler and maps any
thrown error to a 400 "Invalid JSON", which masks server failures; narrow the
scope by separating JSON parsing from the rest: call await req.json() inside its
own try/catch (or use a dedicated try for the parse) and return 400 only if
markReadSchema.safeParse/req.json() fails, while wrapping the database update
(supabase.from(...).update(...) / updateQuery) in a separate try that on
unexpected errors logs the error and returns a 500; reference markReadSchema,
req.json(), updateQuery and the supabase update flow when making the change.
---
Nitpick comments:
In `@apps/vibely-web/app/dashboard/layout.tsx`:
- Around line 9-13: The outer layout applies duplicate width and layered padding
causing redundant constraints and excessive horizontal spacing: remove the inner
max-w-[1440px] on the main element and consolidate padding so it isn't additive
(e.g., keep px-12 on the outer div and change main's px-6 to px-0 or remove main
padding), updating the JSX in layout.tsx where the container div (className
containing min-h-screen bg... max-w-[1440px] mx-auto px-12) and the main element
(className containing flex-1 w-full max-w-[1440px] mx-auto px-6 py-8) are
defined; ensure NavBar (NavBar component referenced in this layout) alignment
still matches by keeping a single consistent horizontal padding value.
In `@apps/vibely-web/app/dashboard/page.tsx`:
- Around line 18-22: The dashboard currently ignores the error from
useDashboardStats and renders default KPI zeros; update the component to
destructure error (e.g., const { totalPhotos, formattedSize, isLoading:
statsLoading, error } = useDashboardStats()) and, when error is truthy, render
an explicit error state (banner/message or disabled KPI placeholders) instead of
the default numeric KPIs so failures are visible to users; ensure the KPI
rendering logic (the block that reads totalPhotos/formattedSize when
statsLoading is false) checks error and uses the error-path UI rather than the
fallback zeros.
In `@apps/vibely-web/components/dashboard/KPIGroup.tsx`:
- Around line 21-22: The formatNum function currently constructs a new
Intl.NumberFormat on every render; move the formatter to module scope and reuse
it: create a module-level const (e.g., formatter = new
Intl.NumberFormat("en-US")) and update formatNum (or replace it) to call
formatter.format(num) so KPIGroup no longer recreates the Intl.NumberFormat on
each render; update any references to formatNum inside the component to use the
module-scoped formatter-based function.
In `@apps/vibely-web/components/dashboard/OverviewHeader.tsx`:
- Around line 8-11: Replace the hardcoded "Good evening" in the OverviewHeader
component with a time-aware greeting: add a small helper (e.g., getGreeting or
useMemo within OverviewHeader) that reads the current hour via new
Date().getHours() and returns "Good morning"/"Good afternoon"/"Good evening"
(and optionally "Good night") based on ranges, then render that greeting before
{userName} (keep the existing <span className="inline-block
animate-wave">👋</span>). Ensure the logic is colocated with the OverviewHeader
component so it updates on client render and references the existing userName
variable.
In `@apps/vibely-web/components/events/EventCard.tsx`:
- Around line 8-16: EventCardProps declares a tags: string[] prop but the
EventCard component currently omits/destructures and doesn't render it; either
remove tags from EventCardProps or implement the feature—preferably implement:
add tags to the EventCard function parameter destructuring (e.g., ... , tags),
and in the JSX map over tags to render a small list of elements (span/div) with
a stable key (e.g., `tag-${i}` or the tag value), appropriate className for
styling, and guard against undefined/empty arrays (render nothing when
tags.length === 0). Ensure references to EventCardProps, the EventCard component
signature, and the tags rendering block are updated consistently.
In `@apps/vibely-web/components/layout/NavBar.tsx`:
- Around line 34-37: The wrapper div around SearchBar uses conflicting flex
utilities (flex-1 on the inner wrapper and flex-shrink-0 on its parent), which
prevents the SearchBar from growing as intended; fix by removing the flex-1
class from the inner wrapper (keep max-w-3xl and w-full on SearchBar) or
alternatively remove flex-shrink-0 from the parent if you want the whole group
to be flexible—adjust the class list on the div containing SearchBar and the
parent div to use either fixed max width or flexible growth, not both (refer to
the wrapper div with class "flex-1 max-w-3xl mx-2 hidden md:block" and the
parent div with "flex items-center gap-4 flex-shrink-0").
In `@apps/vibely-web/components/layout/NotificationDropdown.tsx`:
- Around line 84-104: The notification list items shift because the unread
indicator dot is only rendered when notif.is_read is false; update the
NotificationDropdown notification rendering (inside notifications.map / the Link
element) to always reserve the same space for the indicator: when notif.is_read
is false render the existing colored dot, otherwise render an invisible spacer
element with the same dimensions and margin (e.g., a div with class "mt-1.5 w-2
h-2 flex-shrink-0" but transparent or visibility-hidden) so text alignment stays
consistent between read and unread items.
- Around line 99-102: Handle potential invalid or missing dates for
notif.created_at in NotificationDropdown.tsx by validating the parsed date
before passing it to formatDistanceToNow; for example, parse notif.created_at
into a Date (or use date-fns parseISO) and check isValid(date) (or fallback on a
safe value) and then call formatDistanceToNow only when valid, otherwise render
a fallback string like "unknown" or omit the timestamp. Ensure you update the
span that currently contains formatDistanceToNow(new Date(notif.created_at), {
addSuffix: true }) to perform this validation and use the fallback to avoid
throwing or showing "Invalid Date".
In `@apps/vibely-web/components/layout/ProfileButton.tsx`:
- Around line 64-68: The profile menu trigger button in the ProfileButton
component doesn't expose its dropdown state to assistive tech; update the
button(s) that call setIsOpen(!isOpen) (the trigger in ProfileButton and the
other trigger usage) to include aria-haspopup="menu" and aria-expanded={isOpen}
so screen readers can detect the menu role and whether it's open; keep existing
attributes and only add these two props to the same <button> elements that
toggle isOpen.
In `@apps/vibely-web/components/ui/SearchBar.tsx`:
- Around line 23-29: The input in the SearchBar component lacks an accessible
name; update the input element inside SearchBar (the <input> using placeholder,
onChange, value) to include an explicit aria-label or aria-labelledby prop
(e.g., accept an ariaLabel prop on SearchBar or derive one from placeholder) and
pass it through to the input so assistive tech can reliably identify the field;
ensure the new prop is optional with a sensible default and used in the input
element.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0167c84c-8334-4d34-87b2-f48aa80b3dcb
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (27)
AGENTS.mdapps/vibely-web/app/(auth)/login/page.tsxapps/vibely-web/app/api/dashboard/stats/route.tsapps/vibely-web/app/api/notifications/[id]/route.tsapps/vibely-web/app/api/notifications/route.tsapps/vibely-web/app/dashboard/layout.tsxapps/vibely-web/app/dashboard/page.tsxapps/vibely-web/app/layout.tsxapps/vibely-web/components/dashboard/KPIGroup.tsxapps/vibely-web/components/dashboard/OverviewHeader.tsxapps/vibely-web/components/events/CreateEventCard.tsxapps/vibely-web/components/events/EventCard.tsxapps/vibely-web/components/events/EventGrid.tsxapps/vibely-web/components/layout/Footer.tsxapps/vibely-web/components/layout/NavBar.tsxapps/vibely-web/components/layout/NotificationDropdown.tsxapps/vibely-web/components/layout/ProfileButton.tsxapps/vibely-web/components/layout/SettingsButton.tsxapps/vibely-web/components/ui/IconButton.tsxapps/vibely-web/components/ui/KPIBadge.tsxapps/vibely-web/components/ui/Logo.tsxapps/vibely-web/components/ui/SearchBar.tsxapps/vibely-web/hooks/useDashboardStats.tsapps/vibely-web/hooks/useNotifications.tsapps/vibely-web/lib/notify.tsapps/vibely-web/package.jsonpackages/shared/types/database.types.ts
💤 Files with no reviewable changes (1)
- AGENTS.md
…ith new components and statistics.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (7)
apps/vibely-web/components/ui/IconButton.tsx (1)
18-20: Add a visible focus indicator for keyboard accessibility.The
outline-noneclass removes the browser's default focus ring, but there's no replacement focus styling. This makes the button invisible to keyboard users when focused, which is an accessibility concern (WCAG 2.4.7 Focus Visible).Consider adding a
focus-visible:state:♿ Proposed fix to add focus indicator
className={`flex items-center justify-center w-10 h-10 rounded-full transition-all duration-200 outline-none + focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 ${active ? "bg-indigo-100 text-indigo-600" : "bg-white shadow-sm border border-gray-100 text-gray-500 hover:bg-gray-50 hover:text-gray-900"} ${className}`}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/vibely-web/components/ui/IconButton.tsx` around lines 18 - 20, The button removes the default focus ring via `outline-none` in the IconButton component's className, leaving no visible keyboard focus; update the class string in IconButton.tsx (the template literal that builds className) to add a visible focus indicator such as `focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2` (you can keep `outline-none` to suppress the browser default but ensure the `focus-visible:` classes provide an accessible custom ring), and ensure these classes are applied alongside the existing active/idle variants and `${className}`.apps/vibely-web/app/api/dashboard/stats/route.ts (2)
60-70: Remove unnecessary initialization.The
let totalBytes = 0;on line 60 is dead code—either the error check returns early, or line 67 overwrites the value immediately.♻️ Suggested cleanup
const { data: vaultItems, error: vaultError } = await supabase .from("personal_vault") .select("photo:photos(file_size)") .eq("user_id", user.id); - let totalBytes = 0; if (vaultError) { return NextResponse.json( { error: "Failed to fetch vault items" }, { status: 500 } ); } - totalBytes = vaultItems.reduce( + const totalBytes = vaultItems.reduce( (acc, item) => acc + (item.photo?.file_size ?? 0), 0 );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/vibely-web/app/api/dashboard/stats/route.ts` around lines 60 - 70, Remove the dead initialization of totalBytes: eliminate "let totalBytes = 0;" and instead declare totalBytes as a const assigned from the vaultItems.reduce(...) after the vaultError early-return; i.e., keep the vaultError check that returns on error, then compute const totalBytes = vaultItems.reduce((acc, item) => acc + (item.photo?.file_size ?? 0), 0) so totalBytes is not pre-initialized or mutable.
35-36: Consider filtering out potential null event_ids.If the database schema allows null
event_idvalues inevent_members, the.in()filter would include nulls which may cause unexpected query behavior.🛡️ Defensive filtering suggestion
- const eventIds = userEvents.map((em) => em.event_id); + const eventIds = userEvents + .map((em) => em.event_id) + .filter((id): id is string => id != null);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/vibely-web/app/api/dashboard/stats/route.ts` around lines 35 - 36, Filter out null/undefined event_id values before using eventIds in the DB query: replace the raw mapping (eventIds = userEvents.map((em) => em.event_id)) with a filtered array that excludes null/undefined values (so .in(eventIds) only receives valid IDs). Update any logic that depends on eventIds (e.g., subsequent queries that use eventIds and variable totalPhotos) to handle the case when the filtered array is empty to avoid running queries with invalid params.apps/vibely-web/app/dashboard/page.tsx (2)
117-121: Hardcoded mock tags may confuse users.The tags are statically assigned based on expired status ("Professional", "Archive" vs "Outdoor", "Social") without reflecting actual event data. This could mislead users if tags are visible in the UI.
Note: The
EventCardcomponent currently has tag rendering commented out, so this is not user-visible yet. Consider removing these mock values or adding a TODO comment to replace with actual event tags when the feature is implemented.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/vibely-web/app/dashboard/page.tsx` around lines 117 - 121, Replace the hardcoded mock tags passed as the tags prop (currently using the expired conditional to return ["Professional","Archive"] or ["Outdoor","Social"]) so they don't confuse users; either remove the tags prop entirely from the EventCard usage or replace it with a placeholder that clearly indicates it's temporary (e.g., add a TODO comment) and wire it to real event tag data when available—locate the tags prop in the EventCard invocation and the expired variable in this file and update accordingly.
26-42: Consider using async/await and adding error handling for user fetch.The nested
.then()chains reduce readability. If either query fails, errors are silently swallowed. While the fallback to "User" is graceful, logging the error would aid debugging.♻️ Proposed refactor using async/await
useEffect(() => { - const supabase = createClient(); - supabase.auth.getUser().then(({ data: { user } }) => { - if (!user) return; - supabase - .from("users") - .select("name") - .eq("id", user.id) - .single() - .then(({ data }) => { - if (data && data.name) { - // Pick first name - setUserName(data.name.split(" ")[0]); - } - }); - }); + async function fetchUserName() { + try { + const supabase = createClient(); + const { data: { user }, error: authError } = await supabase.auth.getUser(); + if (authError || !user) return; + + const { data, error } = await supabase + .from("users") + .select("name") + .eq("id", user.id) + .single(); + + if (data?.name) { + setUserName(data.name.split(" ")[0]); + } + } catch (err) { + console.error("Failed to fetch user name:", err); + } + } + fetchUserName(); }, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/vibely-web/app/dashboard/page.tsx` around lines 26 - 42, Refactor the useEffect block that calls createClient() and chains supabase.auth.getUser().then(...).from("users").then(...) to use async/await and add error handling: create an async function inside useEffect that awaits supabase.auth.getUser() and, if a user exists, awaits supabase.from("users").select("name").eq("id", user.id).single(); wrap both awaits in try/catch and call processLogger/error logging (or console.error) on failure while still preserving the fallback setUserName("User") behavior; reference the useEffect, createClient, supabase.auth.getUser, and supabase.from("users").select(...).single() calls when making the changes.apps/vibely-web/components/events/EventCard.tsx (1)
15-15: Unusedtagsprop creates dead code.The
tagsprop is defined as required in the interface but is commented out in the destructuring and never rendered. Either remove it from the interface or implement the tag rendering.♻️ Option A: Remove unused prop from interface
interface EventCardProps { id: string; title: string; dateStr: string; description: string; imageUrl: string; status: "ACTIVE" | "EXPIRED"; - tags: string[]; // Simplest format as string array } export default function EventCard({ id, title, dateStr, description, imageUrl, status, - // tags, }: EventCardProps) {♻️ Option B: Make prop optional if planned for future
interface EventCardProps { id: string; title: string; dateStr: string; description: string; imageUrl: string; status: "ACTIVE" | "EXPIRED"; - tags: string[]; // Simplest format as string array + tags?: string[]; // Optional until tag feature is implemented }Also applies to: 25-26
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/vibely-web/components/events/EventCard.tsx` at line 15, The EventCard component interface currently declares a required tags: string[] prop but that prop is not destructured or used (dead code); either remove tags from the props interface or make it optional (tags?: string[]) and implement rendering where EventCard destructures props (see the EventCard props/interface and the commented-out destructuring around lines 25-26) — if you implement rendering, map tags to UI (e.g., small badges) inside the EventCard render function and include tags in the destructuring; otherwise delete the tags declaration from the interface to eliminate the unused prop.apps/vibely-web/components/dashboard/OverviewHeader.tsx (1)
8-11: Hardcoded "Good evening" greeting doesn't reflect actual time of day.The greeting is always "Good evening" regardless of when the user visits. Consider making this dynamic based on the current hour.
🕐 Proposed fix for dynamic greeting
+function getGreeting(): string { + const hour = new Date().getHours(); + if (hour < 12) return "Good morning"; + if (hour < 18) return "Good afternoon"; + return "Good evening"; +} + export default function OverviewHeader({ userName }: OverviewHeaderProps) { return ( <div className="flex flex-col gap-1"> <h1 className="text-4xl font-extrabold text-[`#1a1b26`] tracking-tight"> - Good evening, {userName}{" "} + {getGreeting()}, {userName}{" "} <span className="inline-block animate-wave">👋</span> </h1>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/vibely-web/components/dashboard/OverviewHeader.tsx` around lines 8 - 11, The greeting is hardcoded as "Good evening" in the OverviewHeader component; replace it with a dynamic greeting computed from the current hour (e.g., "Good morning"/"Good afternoon"/"Good evening"/"Good night") by adding a helper function (getGreeting) and a greeting state (useState) set inside useEffect using new Date().getHours(), then render {greeting}, userName, and the wave span inside the existing h1; ensure you update the h1 content that currently uses "Good evening" so it reads "{greeting}, {userName} <span.../>" to avoid hydration mismatches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/vibely-web/app/dashboard/page.tsx`:
- Around line 93-94: The fallback image path used when computing coverImg (const
coverImg = event.cover_image_url || "/images/event-fallback.jpg") points to a
non-existent asset; either add the missing file at
public/images/event-fallback.jpg or change the fallback to an existing public
asset (for example "/file.svg"); update the fallback in the coverImg expression
so it references the new or existing path and ensure the file is placed in
apps/vibely-web/public/ if you add it.
In `@apps/vibely-web/hooks/useNotifications.ts`:
- Around line 12-28: fetchNotifications can finish after markAllAsRead and
overwrite optimistic state; fix by wiring an AbortController into the fetch flow
and cancelling any in-flight request before applying mark-all updates (and
before starting a new fetch). Specifically, add a ref (e.g.,
notificationsAbortRef) and have fetchNotifications create an AbortController,
store it in the ref, and pass controller.signal to fetch; ensure fetch handles
AbortError and skips setNotifications/setUnreadCount when aborted. In
markAllAsRead (and any code that applies optimistic
setNotifications/setUnreadCount), call notificationsAbortRef.current?.abort()
before updating state so stale responses cannot overwrite the optimistic unread
state.
---
Nitpick comments:
In `@apps/vibely-web/app/api/dashboard/stats/route.ts`:
- Around line 60-70: Remove the dead initialization of totalBytes: eliminate
"let totalBytes = 0;" and instead declare totalBytes as a const assigned from
the vaultItems.reduce(...) after the vaultError early-return; i.e., keep the
vaultError check that returns on error, then compute const totalBytes =
vaultItems.reduce((acc, item) => acc + (item.photo?.file_size ?? 0), 0) so
totalBytes is not pre-initialized or mutable.
- Around line 35-36: Filter out null/undefined event_id values before using
eventIds in the DB query: replace the raw mapping (eventIds =
userEvents.map((em) => em.event_id)) with a filtered array that excludes
null/undefined values (so .in(eventIds) only receives valid IDs). Update any
logic that depends on eventIds (e.g., subsequent queries that use eventIds and
variable totalPhotos) to handle the case when the filtered array is empty to
avoid running queries with invalid params.
In `@apps/vibely-web/app/dashboard/page.tsx`:
- Around line 117-121: Replace the hardcoded mock tags passed as the tags prop
(currently using the expired conditional to return ["Professional","Archive"] or
["Outdoor","Social"]) so they don't confuse users; either remove the tags prop
entirely from the EventCard usage or replace it with a placeholder that clearly
indicates it's temporary (e.g., add a TODO comment) and wire it to real event
tag data when available—locate the tags prop in the EventCard invocation and the
expired variable in this file and update accordingly.
- Around line 26-42: Refactor the useEffect block that calls createClient() and
chains supabase.auth.getUser().then(...).from("users").then(...) to use
async/await and add error handling: create an async function inside useEffect
that awaits supabase.auth.getUser() and, if a user exists, awaits
supabase.from("users").select("name").eq("id", user.id).single(); wrap both
awaits in try/catch and call processLogger/error logging (or console.error) on
failure while still preserving the fallback setUserName("User") behavior;
reference the useEffect, createClient, supabase.auth.getUser, and
supabase.from("users").select(...).single() calls when making the changes.
In `@apps/vibely-web/components/dashboard/OverviewHeader.tsx`:
- Around line 8-11: The greeting is hardcoded as "Good evening" in the
OverviewHeader component; replace it with a dynamic greeting computed from the
current hour (e.g., "Good morning"/"Good afternoon"/"Good evening"/"Good night")
by adding a helper function (getGreeting) and a greeting state (useState) set
inside useEffect using new Date().getHours(), then render {greeting}, userName,
and the wave span inside the existing h1; ensure you update the h1 content that
currently uses "Good evening" so it reads "{greeting}, {userName} <span.../>" to
avoid hydration mismatches.
In `@apps/vibely-web/components/events/EventCard.tsx`:
- Line 15: The EventCard component interface currently declares a required tags:
string[] prop but that prop is not destructured or used (dead code); either
remove tags from the props interface or make it optional (tags?: string[]) and
implement rendering where EventCard destructures props (see the EventCard
props/interface and the commented-out destructuring around lines 25-26) — if you
implement rendering, map tags to UI (e.g., small badges) inside the EventCard
render function and include tags in the destructuring; otherwise delete the tags
declaration from the interface to eliminate the unused prop.
In `@apps/vibely-web/components/ui/IconButton.tsx`:
- Around line 18-20: The button removes the default focus ring via
`outline-none` in the IconButton component's className, leaving no visible
keyboard focus; update the class string in IconButton.tsx (the template literal
that builds className) to add a visible focus indicator such as
`focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2`
(you can keep `outline-none` to suppress the browser default but ensure the
`focus-visible:` classes provide an accessible custom ring), and ensure these
classes are applied alongside the existing active/idle variants and
`${className}`.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8c968285-b199-420f-b6ee-6f263dcf083b
📒 Files selected for processing (13)
apps/vibely-web/app/api/dashboard/stats/route.tsapps/vibely-web/app/api/notifications/route.tsapps/vibely-web/app/dashboard/layout.tsxapps/vibely-web/app/dashboard/page.tsxapps/vibely-web/components/dashboard/OverviewHeader.tsxapps/vibely-web/components/events/EventCard.tsxapps/vibely-web/components/layout/Footer.tsxapps/vibely-web/components/layout/NotificationDropdown.tsxapps/vibely-web/components/layout/ProfileButton.tsxapps/vibely-web/components/ui/IconButton.tsxapps/vibely-web/components/ui/Logo.tsxapps/vibely-web/hooks/useNotifications.tsapps/vibely-web/next.config.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- apps/vibely-web/components/layout/ProfileButton.tsx
- apps/vibely-web/app/api/notifications/route.ts
- apps/vibely-web/app/dashboard/layout.tsx
- apps/vibely-web/components/layout/NotificationDropdown.tsx
- apps/vibely-web/components/ui/Logo.tsx
| const coverImg = | ||
| event.cover_image_url || "/images/event-fallback.jpg"; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Verify fallback image exists in public directory
fd -t f "event-fallback" apps/vibely-web/public/Repository: Suharshit/vibely
Length of output: 42
🏁 Script executed:
# Check if the public directory exists and list its structure
ls -la apps/vibely-web/public/ 2>/dev/null || echo "public directory not found"Repository: Suharshit/vibely
Length of output: 466
🏁 Script executed:
# Check if images subdirectory exists and list contents
ls -la apps/vibely-web/public/images/ 2>/dev/null || echo "images directory not found"Repository: Suharshit/vibely
Length of output: 86
🏁 Script executed:
# Search more broadly for event-fallback in the entire repository
fd -t f "event-fallback" apps/vibely-web/ 2>/dev/null || echo "No event-fallback files found"Repository: Suharshit/vibely
Length of output: 42
Create the missing fallback image or update the path to an existing asset.
The fallback path /images/event-fallback.jpg references a non-existent directory and file. The public/images/ directory does not exist, and there are no image files in public/ (only SVG files). Either:
- Create
apps/vibely-web/public/images/event-fallback.jpg - Update the fallback path to use an existing asset (e.g.,
/file.svg)
Without this, the fallback will 404 when an event lacks a cover image.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/vibely-web/app/dashboard/page.tsx` around lines 93 - 94, The fallback
image path used when computing coverImg (const coverImg = event.cover_image_url
|| "/images/event-fallback.jpg") points to a non-existent asset; either add the
missing file at public/images/event-fallback.jpg or change the fallback to an
existing public asset (for example "/file.svg"); update the fallback in the
coverImg expression so it references the new or existing path and ensure the
file is placed in apps/vibely-web/public/ if you add it.
| const fetchNotifications = useCallback(async () => { | ||
| setIsLoading(true); | ||
| setError(null); | ||
| try { | ||
| const res = await fetch("/api/notifications"); | ||
| if (!res.ok) { | ||
| throw new Error("Failed to fetch notifications"); | ||
| } | ||
| const data = await res.json(); | ||
| setNotifications(data.notifications || []); | ||
| setUnreadCount(data.unread_count || 0); | ||
| } catch (err) { | ||
| setError(err instanceof Error ? err.message : "Unknown error"); | ||
| } finally { | ||
| setIsLoading(false); | ||
| } | ||
| }, []); |
There was a problem hiding this comment.
Prevent stale GET responses from undoing markAllAsRead state.
An in-flight fetchNotifications can complete after markAllAsRead and overwrite Line 48–49 optimistic updates with stale unread data. Invalidate/abort in-flight fetches before applying mark-all updates.
Proposed fix
-import { useState, useEffect, useCallback } from "react";
+import { useState, useEffect, useCallback, useRef } from "react";
@@
export function useNotifications() {
+ const activeFetchController = useRef<AbortController | null>(null);
@@
const fetchNotifications = useCallback(async () => {
+ activeFetchController.current?.abort();
+ const controller = new AbortController();
+ activeFetchController.current = controller;
+
setIsLoading(true);
setError(null);
try {
- const res = await fetch("/api/notifications");
+ const res = await fetch("/api/notifications", { signal: controller.signal });
@@
const data = await res.json();
setNotifications(data.notifications || []);
setUnreadCount(data.unread_count || 0);
} catch (err) {
+ if (err instanceof DOMException && err.name === "AbortError") return;
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
- setIsLoading(false);
+ if (activeFetchController.current === controller) {
+ activeFetchController.current = null;
+ setIsLoading(false);
+ }
}
}, []);
@@
const markAllAsRead = useCallback(async () => {
try {
setError(null);
+ activeFetchController.current?.abort();
const res = await fetch("/api/notifications", {Also applies to: 36-49
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/vibely-web/hooks/useNotifications.ts` around lines 12 - 28,
fetchNotifications can finish after markAllAsRead and overwrite optimistic
state; fix by wiring an AbortController into the fetch flow and cancelling any
in-flight request before applying mark-all updates (and before starting a new
fetch). Specifically, add a ref (e.g., notificationsAbortRef) and have
fetchNotifications create an AbortController, store it in the ref, and pass
controller.signal to fetch; ensure fetch handles AbortError and skips
setNotifications/setUnreadCount when aborted. In markAllAsRead (and any code
that applies optimistic setNotifications/setUnreadCount), call
notificationsAbortRef.current?.abort() before updating state so stale responses
cannot overwrite the optimistic unread state.
…ts, data hooks, and project documentation.
This pull request introduces a new dashboard layout and page for the Vibely web app, adds a dashboard stats API route, and refactors several API route handlers for improved clarity and error handling. The changes also include UI enhancements and code cleanup, such as updating component imports and removing outdated documentation.
Dashboard and API Enhancements:
dashboardlayout and refactored the dashboard page to include a personalized greeting, key performance indicators (KPIs), and a redesigned event grid using new components likeOverviewHeader,KPIGroup,EventGrid, andCreateEventCard. The dashboard now fetches and displays user-specific stats and events. [1] [2]apps/vibely-web/app/api/dashboard/stats/route.tsto aggregate total photos and storage usage for the user's events and personal vault, supporting dashboard KPIs.API Route Refactoring and Error Handling:
apps/vibely-web/app/api/notifications/route.tsand[id]/route.ts), including more readable JSON responses and consistent user authentication checks. [1] [2] [3] [4] [5] apps/vibely-web/app/api/notifications/[id]/route.tsL16-R18, apps/vibely-web/app/api/notifications/[id]/route.tsL30-R35)UI and Component Updates:
apps/vibely-web/app/layout.tsxto match the new dashboard design. [1] [2] [3]Documentation Cleanup:
AGENTS.mdfile, which previously contained agent instructions and project conventions.Dashboard and API Enhancements
apps/vibely-web/app/dashboard/layout.tsx) and redesigned dashboard page with personalized greeting, KPIs, and event grid using new components. [1] [2]apps/vibely-web/app/api/dashboard/stats/route.ts).API Route Refactoring and Error Handling
apps/vibely-web/app/api/notifications/route.ts,[id]/route.ts). [1] [2] [3] [4] [5] apps/vibely-web/app/api/notifications/[id]/route.tsL16-R18, apps/vibely-web/app/api/notifications/[id]/route.tsL30-R35)UI and Component Updates
apps/vibely-web/app/layout.tsx. [1] [2] [3]Documentation Cleanup
AGENTS.mdfile.Summary by CodeRabbit
Release Notes
New Features
UI/UX Improvements
Bug Fixes