Skip to content

feat: Implement the initial web application structure, including a dashboard, event management, and core UI components.#39

Merged
Suharshit merged 3 commits into
developfrom
ui/dashboard-pages
Mar 20, 2026
Merged

feat: Implement the initial web application structure, including a dashboard, event management, and core UI components.#39
Suharshit merged 3 commits into
developfrom
ui/dashboard-pages

Conversation

@Suharshit

@Suharshit Suharshit commented Mar 13, 2026

Copy link
Copy Markdown
Owner

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:

  • Added a new dashboard layout and refactored the dashboard page to include a personalized greeting, key performance indicators (KPIs), and a redesigned event grid using new components like OverviewHeader, KPIGroup, EventGrid, and CreateEventCard. The dashboard now fetches and displays user-specific stats and events. [1] [2]
  • Introduced a new API route apps/vibely-web/app/api/dashboard/stats/route.ts to aggregate total photos and storage usage for the user's events and personal vault, supporting dashboard KPIs.

API Route Refactoring and Error Handling:

UI and Component Updates:

  • Updated imports and removed unused icons in the login page, and adjusted layout styling in apps/vibely-web/app/layout.tsx to match the new dashboard design. [1] [2] [3]

Documentation Cleanup:

  • Removed the outdated AGENTS.md file, which previously contained agent instructions and project conventions.

Dashboard and API Enhancements

  • Added new dashboard layout (apps/vibely-web/app/dashboard/layout.tsx) and redesigned dashboard page with personalized greeting, KPIs, and event grid using new components. [1] [2]
  • Added API route for dashboard stats aggregation (apps/vibely-web/app/api/dashboard/stats/route.ts).

API Route Refactoring and Error Handling

UI and Component Updates

  • Cleaned up icon imports in login page and updated layout styling in apps/vibely-web/app/layout.tsx. [1] [2] [3]

Documentation Cleanup

  • Deleted AGENTS.md file.

Summary by CodeRabbit

Release Notes

  • New Features

    • Event detail pages with photo gallery, guest management, and settings tabs
    • Personal photo archive/vault with organized viewing and management
    • Event sharing via QR codes and shareable links
    • Dashboard with storage statistics and photo counts
    • Notifications system with mark-as-read functionality
    • Pricing information section on landing page
    • Mobile bottom navigation for easier app access
  • UI/UX Improvements

    • Redesigned login and signup pages with modern glassmorphism design
    • New event card layouts with member avatars and status indicators
    • Enhanced sidebar navigation with user profile integration
    • Responsive footer with links and copyright information
    • Updated visual design system with improved color tokens and typography
  • Bug Fixes

    • Added client-side validation for login form fields
    • Improved error handling in API endpoints

…shboard, event management, and core UI components.
Copilot AI review requested due to automatic review settings March 13, 2026 18:58
@vercel

vercel Bot commented Mar 13, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
vibely-web Ready Ready Preview, Comment Mar 20, 2026 7:37pm

@coderabbitai

coderabbitai Bot commented Mar 13, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This pull request redesigns the application from a neumorphic to glassmorphism aesthetic, restructures routing with a new (app) route group and shared layout, removes the NavBar in favor of a Sidebar/MobileNav, introduces new event detail and dashboard pages, adds numerous UI components for events and vault management, implements a caching layer for data fetching, and includes new documentation files outlining design, architecture, and conventions.

Changes

Cohort / File(s) Summary
UI Theme & Styling Overhaul
apps/vibely-web/app/globals.css, apps/vibely-web/tailwind.config.ts, apps/vibely-web/next.config.ts, memory-bank/DESIGN.md
Replaced neumorphic design system with glassmorphism: new CSS @theme tokens, semantic color palette, glass-card utilities, Material Symbols icons. Updated Tailwind config with flat color tokens and font families. Extended Next.js image optimization with AVIF/WebP formats and remote image patterns.
Layout Architecture
apps/vibely-web/app/layout.tsx, apps/vibely-web/app/(app)/layout.tsx, apps/vibely-web/components/layout/AppLayout.tsx, apps/vibely-web/components/layout/Sidebar.tsx, apps/vibely-web/components/layout/MobileNav.tsx, apps/vibely-web/components/layout/NavBar.tsx, apps/vibely-web/components/layout/Footer.tsx
Removed NavBar from root layout; added (app) route group with AppGroupLayout wrapper. Created new AppLayout with collapsible Sidebar and MobileNav bottom navigation. Added Footer component with structured layout.
Authentication Pages
apps/vibely-web/app/(auth)/login/page.tsx, apps/vibely-web/app/(auth)/signup/page.tsx
Updated login to glassmorphism with Material Symbols icons, removed password visibility toggle, added client-side email/password validation. Redesigned signup with two-column layout and material icons, added all-fields validation.
Dashboard & Event Pages
apps/vibely-web/app/(app)/dashboard/page.tsx, apps/vibely-web/app/(app)/events/[id]/page.tsx, apps/vibely-web/app/(app)/vault/page.tsx, apps/vibely-web/app/dashboard/page.tsx, apps/vibely-web/app/events/[id]/page.tsx, apps/vibely-web/app/vault/page.tsx, apps/vibely-web/app/pricing/page.tsx
New dashboard with welcome header and stats cards using useDashboardStats hook. New event detail page with EventHero, EventTabs, and tab-specific content (gallery/guests/settings). New vault page with masonry gallery. Removed old dashboard/vault/pricing pages.
Event Components
apps/vibely-web/components/events/EventCard.tsx, apps/vibely-web/components/events/EventGrid.tsx, apps/vibely-web/components/events/EventHero.tsx, apps/vibely-web/components/events/EventTabs.tsx, apps/vibely-web/components/events/EventMemberStack.tsx, apps/vibely-web/components/events/EventQRPanel.tsx, apps/vibely-web/components/events/EventStatsCard.tsx, apps/vibely-web/components/events/EventActionBar.tsx, apps/vibely-web/components/events/EventGallery.tsx, apps/vibely-web/components/events/EditEventForm.tsx, apps/vibely-web/components/events/CreateEventCard.tsx, apps/vibely-web/components/events/LiveActivityFeed.tsx
New component suite for event management: EventCard simplified to accept flat props, EventGrid for responsive layout, EventHero for header, EventTabs for navigation, EventMemberStack for member avatars, EventQRPanel for invite sharing, EventStatsCard for guest info, EventActionBar for controls, EventGallery for photo display, EditEventForm with cover image upload, CreateEventCard link, and LiveActivityFeed mock.
Vault Components
apps/vibely-web/components/vault/VaultCard.tsx, apps/vibely-web/components/vault/VaultFilters.tsx, apps/vibely-web/components/vault/VaultSearchBar.tsx
New vault UI components: VaultCard with progressive image loading and save/unsave actions, VaultFilters for filtering pills, VaultSearchBar for search input.
Landing Page
apps/vibely-web/app/page.tsx, apps/vibely-web/components/landing/HeroSection.tsx, apps/vibely-web/components/landing/FeaturesSection.tsx, apps/vibely-web/components/landing/BottomCTA.tsx, apps/vibely-web/components/landing/PricingSection.tsx, apps/vibely-web/components/landing/NavBarHome.tsx, apps/vibely-web/components/landing/PhoneMockup.tsx, apps/vibely-web/components/landing/StepsSection.tsx
Updated home page layout. HeroSection now uses useAuth for conditional routing and new mockup/social proof. FeaturesSection redesigned with glass cards and material icons. Added PricingSection component. NavBarHome simplified with useAuth, removed dropdown menu. Added BottomCTA with auth-aware links. Removed PhoneMockup and StepsSection.
UI Components
apps/vibely-web/components/ui/Logo.tsx
New Logo component linking to /dashboard with optional icon-only mode.
API Routes
apps/vibely-web/app/api/dashboard/stats/route.ts, apps/vibely-web/app/api/notifications/route.ts, apps/vibely-web/app/api/notifications/[id]/route.ts
New GET /api/dashboard/stats endpoint returning totalPhotos and totalBytes. Refactored notifications routes with explicit JSON parsing guard and separate try/catch for update query.
Data Fetching Hooks
apps/vibely-web/hooks/useDashboardStats.ts, apps/vibely-web/hooks/useNotifications.ts, apps/vibely-web/hooks/useEvents.ts, apps/vibely-web/hooks/usePhotos.ts, apps/vibely-web/hooks/useVault.ts, apps/vibely-web/hooks/useProgressiveImage.ts, apps/vibely-web/lib/fetchCache.ts
New useDashboardStats hook with formatBytes utility. New useNotifications hook with optimistic updates. Enhanced useEvents/usePhotos with cache integration. New useProgressiveImage hook for lazy image loading. Added fetchCache module implementing SWR pattern with request deduplication and LRU eviction.
Configuration & Dependencies
apps/vibely-web/package.json, package.json, apps/vibely-web/lib/notify.ts, packages/shared/types/database.types.ts
Added date-fns dependency. Added @tailwindcss/container-queries root dependency. Minor notify.ts formatting. Added trailing comma in database types.
Documentation
memory-bank/DESIGN.md, memory-bank/activeContext.md, memory-bank/architecture.md, memory-bank/conventions.md, memory-bank/decisions.md, memory-bank/projectBrief.md, AGENTS.md
Added comprehensive design guide (glassmorphism rules, color palette, typography, component treatments). Added active context status. Added architecture overview (monorepo, stack). Added coding conventions (TypeScript, Zod, RLS, Tailwind). Added architectural decisions (Supabase Storage, ImageKit, Upstash, pg_cron). Added project brief (overview, tooling, goals). Removed AGENTS.md file.

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

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Whiskers twitched with delight,
New glass cards catch the light,
Events shimmer, vaults aligned,
Sidebar hops, cache refined,
Vibely bounds to design!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: implementing the initial web application structure with dashboard, event management, and core UI components, which aligns with the substantial additions of dashboard pages, event-related components, layout restructuring, and new API routes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ui/dashboard-pages

Comment @coderabbitai help to get the list of available commands and usage tips.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 /dashboard layout + redesigned dashboard page (greeting, KPIs, event grid, create-event card).
  • Adds /api/dashboard/stats endpoint 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.

Comment thread apps/vibely-web/components/events/EventCard.tsx Outdated
Comment thread apps/vibely-web/components/events/EventCard.tsx Outdated
Comment thread apps/vibely-web/app/dashboard/page.tsx Outdated
Comment thread apps/vibely-web/components/layout/NotificationDropdown.tsx Outdated
Comment thread apps/vibely-web/components/layout/ProfileButton.tsx Outdated
Comment thread apps/vibely-web/app/dashboard/layout.tsx Outdated
Comment thread apps/vibely-web/components/layout/SettingsButton.tsx Outdated
Comment thread apps/vibely-web/app/dashboard/page.tsx Outdated
Comment thread apps/vibely-web/hooks/useNotifications.ts Outdated
Comment thread apps/vibely-web/app/api/dashboard/stats/route.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟠 Major

Narrow the catch scope to avoid masking server failures as 400.

Lines 112-113 currently return Invalid JSON for 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 (or aria-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" and aria-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:

  1. max-w-[1440px] is applied to both the outer div (line 9) and the inner main (line 11). The inner constraint has no effect since the parent already limits the width.
  2. Nested padding (px-12 on outer + px-6 on main) results in 72px + 24px = 96px total horizontal padding, which may be excessive and inconsistent with NavBar's own px-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_at is null, undefined, or an invalid date string, new Date(notif.created_at) will produce an "Invalid Date", and formatDistanceToNow may 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-1 which tries to grow, but its parent container has flex-shrink-0 which prevents shrinking. This combination inside a justified space-between layout may not behave as intended—the SearchBar won't actually grow since there's no available space to fill.

Consider whether flex-1 is needed here, or if a fixed max-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: formatNum recreated on each render.

The formatNum function and its Intl.NumberFormat instance 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: Unused tags prop in interface.

The tags prop is defined in EventCardProps (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 rendering 0 KPIs.

useDashboardStats() exposes an error, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7f2d3a6 and 1bb4aa8.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (27)
  • AGENTS.md
  • apps/vibely-web/app/(auth)/login/page.tsx
  • apps/vibely-web/app/api/dashboard/stats/route.ts
  • apps/vibely-web/app/api/notifications/[id]/route.ts
  • apps/vibely-web/app/api/notifications/route.ts
  • apps/vibely-web/app/dashboard/layout.tsx
  • apps/vibely-web/app/dashboard/page.tsx
  • apps/vibely-web/app/layout.tsx
  • apps/vibely-web/components/dashboard/KPIGroup.tsx
  • apps/vibely-web/components/dashboard/OverviewHeader.tsx
  • apps/vibely-web/components/events/CreateEventCard.tsx
  • apps/vibely-web/components/events/EventCard.tsx
  • apps/vibely-web/components/events/EventGrid.tsx
  • apps/vibely-web/components/layout/Footer.tsx
  • apps/vibely-web/components/layout/NavBar.tsx
  • apps/vibely-web/components/layout/NotificationDropdown.tsx
  • apps/vibely-web/components/layout/ProfileButton.tsx
  • apps/vibely-web/components/layout/SettingsButton.tsx
  • apps/vibely-web/components/ui/IconButton.tsx
  • apps/vibely-web/components/ui/KPIBadge.tsx
  • apps/vibely-web/components/ui/Logo.tsx
  • apps/vibely-web/components/ui/SearchBar.tsx
  • apps/vibely-web/hooks/useDashboardStats.ts
  • apps/vibely-web/hooks/useNotifications.ts
  • apps/vibely-web/lib/notify.ts
  • apps/vibely-web/package.json
  • packages/shared/types/database.types.ts
💤 Files with no reviewable changes (1)
  • AGENTS.md

Comment thread apps/vibely-web/app/api/dashboard/stats/route.ts
Comment thread apps/vibely-web/app/dashboard/page.tsx Outdated
Comment thread apps/vibely-web/components/dashboard/OverviewHeader.tsx Outdated
Comment thread apps/vibely-web/components/events/EventCard.tsx Outdated
Comment thread apps/vibely-web/components/layout/Footer.tsx Outdated
Comment thread apps/vibely-web/components/layout/ProfileButton.tsx Outdated
Comment thread apps/vibely-web/components/layout/SettingsButton.tsx Outdated
Comment thread apps/vibely-web/components/ui/IconButton.tsx Outdated
Comment thread apps/vibely-web/components/ui/Logo.tsx
Comment thread apps/vibely-web/hooks/useNotifications.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-none class 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_id values in event_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 EventCard component 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: Unused tags prop creates dead code.

The tags prop 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">&#x1F44B;</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

📥 Commits

Reviewing files that changed from the base of the PR and between 1bb4aa8 and bf45866.

📒 Files selected for processing (13)
  • apps/vibely-web/app/api/dashboard/stats/route.ts
  • apps/vibely-web/app/api/notifications/route.ts
  • apps/vibely-web/app/dashboard/layout.tsx
  • apps/vibely-web/app/dashboard/page.tsx
  • apps/vibely-web/components/dashboard/OverviewHeader.tsx
  • apps/vibely-web/components/events/EventCard.tsx
  • apps/vibely-web/components/layout/Footer.tsx
  • apps/vibely-web/components/layout/NotificationDropdown.tsx
  • apps/vibely-web/components/layout/ProfileButton.tsx
  • apps/vibely-web/components/ui/IconButton.tsx
  • apps/vibely-web/components/ui/Logo.tsx
  • apps/vibely-web/hooks/useNotifications.ts
  • apps/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

Comment thread apps/vibely-web/app/dashboard/page.tsx Outdated
Comment on lines +93 to +94
const coverImg =
event.cover_image_url || "/images/event-fallback.jpg";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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:

  1. Create apps/vibely-web/public/images/event-fallback.jpg
  2. 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.

Comment on lines +12 to +28
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);
}
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

@Suharshit Suharshit merged commit e8353d4 into develop Mar 20, 2026
5 of 6 checks passed
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