8000 Rollback Changes + Implement the UI, Desktop and PWA Changes by R44VC0RP · Pull Request #13 · R44VC0RP/agenda.dev · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Rollback Changes + Implement the UI, Desktop and PWA Changes #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 0 commits into from
May 8, 2025

Conversation

R44VC0RP
Copy link
Owner
@R44VC0RP R44VC0RP commented May 3, 2025

Summary by CodeRabbit

  • New Features

    • Added detection of mobile devices to disable drag-and-drop functionality on mobile for improved usability.
    • Introduced keyboard shortcuts for quick workspace switching.
    • Added conditional workspace creation dialog for logged-in users.
    • Implemented periodic synchronization of todos with the server every minute while logged in.
    • Added subscription management with plan limits enforcement on workspace creation.
    • Introduced a settings dialog with subscription status display and billing portal access.
    • Added a new agenda icon component replacing the logo image.
    • Added a static, non-draggable todo list mode for mobile devices.
    • Introduced a new landing hero component showcasing product features and pricing.
    • Added MCP API route with a dice roll tool.
    • Enhanced todo parsing API with validation, subscription-aware AI model selection, loop detection, and fallback logic.
  • Bug Fixes

    • Prevented drag-and-drop actions on mobile devices to avoid unintended behavior.
  • Style

    • Enhanced responsiveness and visual polish across dialogs, todo items, and input components for better mobile and desktop experiences.
    • Updated gradients, backgrounds, borders, and spacing for a more modern and consistent UI.
    • Adjusted UI layout for improved header, input placement, and title visibility on small screens.
    • Improved reminders dialog responsiveness and scaling on various screen sizes.
    • Restyled todo input and todo item components with subtle glow shadows and refined backgrounds.
  • Chores

    • Added a custom build script to generate a minimal, desktop-only static app build.
    • Refined synchronization logic for todos and enhanced logging for rescheduling actions.
    • Integrated Stripe subscription support with database schema, authentication client, and backend enforcement.
    • Removed legacy spinner and toast components.
    • Updated dependencies with Stripe, MCP adapter, and date parsing utilities.

Copy link
vercel bot commented May 3, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
agenda.dev ❌ Failed (Inspect) May 8, 2025 10:26pm

Copy link
coderabbitai bot commented May 3, 2025

Walkthrough

The updates introduce significant improvements focused on mobile responsiveness, drag-and-drop control, and UI polish. A custom React hook detects mobile devices to disable drag-and-drop in the main todo list, with corresponding changes in the TodoList component to support a static, non-draggable view. The synchronization logic between local and server todos is refactored into a reusable async function with periodic syncing. Detailed logging and error handling are added for todo rescheduling. Multiple components, including dialogs and todo items, receive extensive responsive and visual style updates for a more modern and adaptive appearance. A new Bun-based build script is introduced to generate a minimal, desktop-only static Next.js build, handling environment isolation, file validation, output preparation, and cleanup.

Changes

File(s) Change Summary
app/HomeClient.tsx Added useIsMobile hook to detect mobile; disables drag-and-drop on mobile; refactored todo-server sync logic into reusable syncWithServer function with periodic syncing; optimistic updates with temporary IDs for adding todos; enhanced detailed logging and error handling for rescheduling todos; preserved and reformatted CRUD and workspace functions; UI layout refinements for mobile and desktop; conditional rendering of SettingsDialog with auto-open; replaced logo with AgendaIcon; commented out ViewToggle; placeholder for future command palette.
components/todo-list.tsx Added optional disableDrag prop; introduced new StaticColumn component for non-draggable todo list rendering; conditionally renders draggable or static lists based on disableDrag flag.
components/RemindersDialog.tsx
components/SettingsDialog.tsx
Adjusted dialog container widths, heights, padding, font sizes, and spacing for improved responsiveness and scaling across screen sizes; added subscription management UI and logic in SettingsDialog including plan display and billing portal integration; no logic changes in RemindersDialog.
components/todo-input.tsx
components/todo-item.tsx
Major visual and style updates including backdrop blur, semi-transparent backgrounds, increased border radius, simplified shadows, refined button and label styling; no logic or control flow changes.
desktop-build.js New standalone Bun script to build a desktop-only static Next.js app; creates temporary build directory; copies desktop-specific files and assets; sets up environment variables; validates critical files; runs Next.js build; prepares and cleans output directory; creates redirecting root index.html; logs output for debugging.
app/api/[transport]/route.ts Added new API route handler using @vercel/mcp-adapter defining a roll_dice tool with input validation and random output; exports GET, POST, DELETE handlers.
app/api/parse-todo/route.ts Enhanced todo parsing API with Zod validation, subscription-aware AI model selection, loop detection and fallback logic, enriched prompt context, and dynamic time suggestions; stores validation and fallback info in Redis.
app/api/workspaces/route.ts Modified workspace creation POST handler to enforce subscription limits via new requireSubscription function; replaced direct session checks with subscription-based authorization and workspace limit enforcement.
lib/auth-client.ts Added Stripe client plugin to authentication client; exported new subscription member alongside existing auth methods.
lib/auth.ts Integrated Stripe with environment checks; configured Stripe client and webhook; added Stripe plugin to auth with subscription management and workspace limits; commented out Twitter provider.
lib/db/migrations/0009_high_vindicator.sql Added new subscriptions table with columns for subscription metadata and Stripe integration fields.
lib/db/migrations/0010_young_monster_badoon.sql Added stripe_customer_id column to users table.
lib/db/migrations/meta/0009_snapshot.json Added new database schema snapshot including subscriptions table and updated schema.
lib/db/migrations/meta/0010_snapshot.json Added updated database schema snapshot reflecting new tables and columns.
lib/db/migrations/meta/_journal.json Added migration journal entries for new migrations 0009 and 0010.
lib/db/schema.ts Extended users table with stripeCustomerId; added new subscriptions table schema.
lib/requireSubscription.ts New module exporting requireSubscription function enforcing subscription-based workspace limits and authorization; defines plan limits.
components/AgendaIcon.tsx Added new AgendaIcon React component rendering an SVG agenda icon with light/dark mode styling.
components/LandingHero.tsx Added new interactive landing hero component with animated demo dashboard, metrics, features, and pricing plans; includes sign-in dialog trigger.
components/LoginButton.tsx Restyled login dialog with improved padding, typography, accessibility, and added links to Terms and Privacy; Twitter sign-in button commented out.
components/workspace-switcher.tsx Added canCreateNew prop to control enabling/disabling of "New Workspace" button with corresponding styling and click handling.
package.json Added dependencies: @better-auth/stripe, @vercel/mcp-adapter, chrono-node, stripe; downgraded better-auth version.
spinner.css and spinner.tsx Removed spinner CSS and React spinner component.
toast.tsx Removed toast notification React component.
README.md Added "Current Items in Implementation" section listing pending MCP Server and Stripe integration tasks.
mcp-research.md Deleted file containing MCP configuration and CLI notes.
new-hoverbar.md Deleted CSS stylesheet for hoverbar component.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant HomeClient
    participant TodoList
    participant useIsMobile
    participant Server

    User->>HomeClient: Load app
    HomeClient->>useIsMobile: Check screen width
    useIsMobile-->>HomeClient: isMobile flag
    HomeClient->>TodoList: Render with disableDrag = isMobile
    User->>TodoList: Attempt drag-and-drop
    alt isMobile = true
        TodoList-->>User: Drag-and-drop disabled
    else isMobile = false
        TodoList->>HomeClient: handleDragEnd event
        HomeClient->>Server: Update todo order
        Server-->>HomeClient: Response
    end
Loading

Possibly related PRs

  • update #11: Enhances error handling and validation inside the syncWithServer function in app/HomeClient.tsx, directly related to the todo synchronization refactor in this PR.
  • update UI, features, adding workspaces #4: Shares modifications to app/HomeClient.tsx and components/todo-list.tsx for workspace support and todo synchronization, including the disableDrag prop.
  • Major UI Overhaul #5: Related changes in app/HomeClient.tsx touching todo input and drag-and-drop behavior, though focusing on different features and UI implementations.

Poem

A rabbit hopped through fields of code,
Making mobile smooth on every road.
Dragging stopped for screens so small,
While dialogs now fit one and all.
Colors softened, borders grew,
A desktop build script joined the crew!
🐇✨ Hooray for updates, fresh and bright—
Our todo garden feels just right!

Tip

⚡️ Faster reviews with caching
  • CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.

Enjoy the performance boost—your workflow just got faster.

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

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

Support

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

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

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

CodeRabbit Configuration File (.coderabbit.yaml)

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

Documentation and Community

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

Copy link
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🔭 Outside diff range comments (2)
components/SettingsDialog.tsx (1)

126-131: 🛠️ Refactor suggestion

Validate numeric input & enforce the declared min value

parseInt(e.target.value) will happily return 0, NaN, or any other number below the <Input min={1} …> constraint if users clear the field or type non-digits.
Consider clamping the parsed value (or falling back to 1) so the component state can never drift below the UI constraint.

-  reminderMinutes: parseInt(e.target.value) || 30
+  const val = Math.max(1, parseInt(e.target.value) || 30)
+  reminderMinutes: val
components/todo-item.tsx (1)

136-186: ⚠️ Potential issue

Reset isReminderCommand on failure to avoid a stuck shine border

setIsReminderCommand(false) is only invoked inside the try block.
If the POST to /api/reminders fails, the catch‐branch adds an error comment but never clears the flag, so ShineBorder will keep pulsing indefinitely, misleading the user into thinking a reminder is still being created.

-    } catch (error) {
+    } catch (error) {
       console.error('Error creating reminder:', error);
       ...
+      // Clear the visual indicator even on failure
+      setIsReminderCommand(false);
     } finally {
       setIsProcessingReminder(false);
     }
🧹 Nitpick comments (6)
components/RemindersDialog.tsx (1)

93-101: 90vh dialog height may collide with mobile safe-area / on-screen keyboard

Capping to h-[90vh] can push the confirm buttons under iOS/Android home-indicator or the soft-keyboard.
Adding pb-[env(safe-area-inset-bottom)] (or safe bottom classes offered by Tailwind 3.4+) keeps critical UI reachable.

components/todo-list.tsx (1)

198-282: StaticColumn keeps dead code & unused state

hasOverflow, opacity and the bounce indicator are now commented out, but the expensive ResizeObserver and useScroll hooks still run on every render.
Removing them (or re-enabling the indicator) will save work on low-power devices.

desktop-build.js (2)

154-166: originalWebpack(config) signature mismatch – may drop build-time params

next.config’s custom webpack fn receives (config, options).
Calling originalWebpack(config) omits the options argument, potentially breaking plugins that rely on it.

-      config = originalWebpack(config);
+      config = originalWebpack(config, { ...arguments[1] });

Also guard with typeof originalWebpack === 'function' to avoid runtime errors when the parent config exports an object, not a function.


174-176: Redundant copy of next.config.mjs

The original next.config.mjs is first copied (line 42) and then overwritten by the desktop-specific version (line 174).
You can skip the initial copy to save I/O and avoid confusion.

app/HomeClient.tsx (2)

57-76: useIsMobile adds resize listener on every mount – consider debouncing

Frequent window-​resize events can fire dozens of times per second, triggering a full React render cascade because state updates every pixel change.

-      window.addEventListener('resize', checkMobile);
+      const debounced = _.debounce(checkMobile, 150); // lodash/throttle utils
+      window.addEventListener('resize', debounced);
...
-      return () => window.removeEventListener('resize', checkMobile);
+      return () => window.removeEventListener('resize', debounced);

At minimum, wrap the handler in requestAnimationFrame or a small throttle to cut unnecessary renders on rapid resizing.


357-411: Extensive console logging – strip before production

The reschedule flow logs >10 messages per invocation. While helpful during development, it bloats production bundles and clutters the JS console.

Recommend gating behind process.env.NODE_ENV !== 'production' or removing entirely before release.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae3ccbb and 58d6af4.

⛔ Files ignored due to path filters (1)
  • public/noise-light.png is excluded by !**/*.png
📒 Files selected for processing (7)
  • app/HomeClient.tsx (2 hunks)
  • components/RemindersDialog.tsx (3 hunks)
  • components/SettingsDialog.tsx (5 hunks)
  • components/todo-input.tsx (6 hunks)
  • components/todo-item.tsx (8 hunks)
  • components/todo-list.tsx (3 hunks)
  • desktop-build.js (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
components/todo-list.tsx (1)
lib/db/schema.ts (1)
  • todos (76-86)
🔇 Additional comments (3)
components/todo-input.tsx (1)

124-127: Verify static asset paths referenced in pseudo-elements

after:bg-[url('/noise-light.png')] will 404 if the texture isn’t placed in /public/noise-light.png at build time (both PWA and desktop build).
Please confirm the asset is present or adjust the path to next/image or a CDN.

components/todo-item.tsx (1)

278-285: Nice visual polish on the completion toggle

The revised classes give the checkbox a much softer, modern look while still conveying state clearly.
No functional issues spotted here. 👍

app/HomeClient.tsx (1)

57-76: Memory-safe but still SSR-safe 👍

Good job guarding window access inside useEffect; this keeps the hook SSR-friendly.

Copy link
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
app/HomeClient.tsx (2)

53-76: Consider useLayoutEffect to avoid first-paint flicker when detecting mobile
useIsMobile initialises isMobile to false and only updates inside an async useEffect.
During the first paint the component therefore believes it is on desktop, renders the draggable list, and only then re-renders as non-draggable once the effect runs. This can cause a perceptible layout jump and hydration warning flashes.

-const useIsMobile = () => {
-  const [isMobile, setIsMobile] = useState(false);
+const useIsMobile = () => {
+  // Initialise from `window.innerWidth` when possible to avoid a mismatch
+  const getMatch = () =>
+    typeof window !== 'undefined' ? window.innerWidth < 768 : false;
+
+  const [isMobile, setIsMobile] = useState(getMatch);
+
+  // useLayoutEffect runs before paint → no flicker
+  useLayoutEffect(() => {
+    if (typeof window === 'undefined') return;
+    const  => setIsMobile(getMatch());
+    window.addEventListener('resize', onResize);
+    return () => window.removeEventListener('resize', onResize);
+  }, []);
   return isMobile;
 };

This keeps the first render consistent with the eventual hydrated state.


136-156: Keyboard shortcut listener leaks if effect re-runs frequently
handleKeyDown is re-declared whenever workspaces changes, leading to a remove-then-add cycle every render. Consider using useCallback + empty-dependency useEffect or add the listener just once and read the latest state via refs.

This avoids excessive listener churn on large workspace lists.

desktop-build.js (1)

42-46: Redundant copy: next.config.mjs is overwritten later
The original next.config.mjs is copied into the temp dir (line 42) and overwritten at line 178 with the desktop variant.
Removing the first copy eliminates one FS operation and avoids confusion.

-fs.copyFileSync('next.config.mjs', path.join(tempDir, 'next.config.mjs'));
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 58d6af4 and 1714f69.

📒 Files selected for processing (3)
  • app/HomeClient.tsx (2 hunks)
  • components/todo-list.tsx (3 hunks)
  • desktop-build.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/todo-list.tsx
🔇 Additional comments (1)
desktop-build.js (1)

158-172: process/browser alias requires the polyfill to be installed
The alias relies on process/browser (provided by process package). Ensure it is declared in package.json dependencies; otherwise the export build will fail at module resolution.

If the alias is only needed for a minority of packages, consider importing process explicitly where required instead of a global alias.

Comment on lines 577 to 663
}

if (columnIndex === 0) {
// Today's column
newDueDate = new Date();
} else {
// Upcoming column
newDueDate.setDate(newDueDate.getDate() + 1);
}
}

// Create new array with updated todo
const newTodos = todos.filter(t => t.id !== draggableId);
newTodos.splice(destination.index, 0, updatedTodo);

// Update state
setTodos(newTodos);

// Log the update
console.log(`Todo "${todo.title}" moved to ${destination.droppableId} at index ${destination.index}`);

// Update the database after animations finish
if (session?.user) {
setTimeout(async () => {
try {
const res = await fetch('/api/todos', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: draggableId, dueDate: updatedTodo.dueDate }),
});
if (!res.ok) throw new Error('Failed to update todo via drag-and-drop');
const serverTodo = await res.json();
// Sync state with server response
setTodos(prev => prev.map(t => t.id === draggableId ? serverTodo : t));
console.log('✅ Todo dueDate updated on server via drag:', serverTodo);
} catch (error) {
console.error('❌ Error updating todo via drag:', error);
}
}, 350);
}
// Update the todo's due date
const updatedTodo = {
...todo,
dueDate: newDueDate.toISOString()
};

return (
<div className="min-h-screen flex flex-col bg-gray-100 dark:bg-[#09090B] text-gray-900 dark:text-white p-4 transition-colors duration-200">
<div className="flex flex-row items-center justify-left">
<Image src="/logo.png" alt="agenda.dev" width={32} height={32} className="mr-2" />
<h1 className="text-xl">agenda.dev</h1>
</div>
<div className="relative mx-auto mb-4 flex items-center space-x-2 justify-center md:absolute md:top-4 md:right-4 md:mb-0 md:mx-0 md:justify-start">
{session?.user && (
<WorkspaceSwitcher
workspaces={workspaces}
currentWorkspace={currentWorkspace}
>
=> setIsNewWorkspaceDialogOpen(true)}
>
todos={todos}
/>
)}
<CompletedToggle showCompleted={showCompleted} setShowCompleted={setShowCompleted} />
<ViewToggle isTableView={isTableView} setIsTableView={setIsTableView} />
<ThemeToggle />
<FeedbackWidget />
<LoginButton />
</div>

// Create new array with updated todo
const newTodos = todos.filter(t => t.id !== draggableId);
newTodos.splice(destination.index, 0, updatedTodo);

// Update state
setTodos(newTodos);

// Log the update
console.log(`Todo "${todo.title}" moved to ${destination.droppableId} at index ${destination.index}`);

// Update the database after animations finish
if (session?.user) {
setTimeout(async () => {
try {
const res = await fetch('/api/todos', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: draggableId, dueDate: updatedTodo.dueDate }),
});
if (!res.ok) throw new Error('Failed to update todo via drag-and-drop');
const serverTodo = await res.json();
// Sync state with server response
setTodos(prev => prev.map(t => t.id === draggableId ? serverTodo : t));
console.log('✅ Todo dueDate updated on server via drag:', serverTodo);
} catch (error) {
console.error('❌ Error updating todo via drag:', error);
}
}, 350);
}
};
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

Date logic for ‘Next 7 days’ and ‘Upcoming’ columns is off & time-zone sensitive

  1. Dropping into column index 1 adds only 1 day, not up to 7.
  2. Column index 2 adds 8 days unconditionally, skipping the 2-7-day window.
  3. new Date().toISOString() converts to UTC which may shift the date (e.g. 23:00-01:00). Storing an ISO date-time while the rest of the app treats dueDate as a plain date string will create off-by-one-day errors.
-} else if (columnIndex === 1) {
-  // Next 7 days column - set to tomorrow
-  newDueDate.setDate(newDueDate.getDate() + 1);
-} else {
-  // Upcoming column - set to next week
-  newDueDate.setDate(newDueDate.getDate() + 8);
+} else if (columnIndex === 1) {
+  // Any day within the next 7 days – default to +3 days
+  newDueDate.setDate(newDueDate.getDate() + 3);
+} else {
+  // Upcoming (>7 days) – default to +14 days
+  newDueDate.setDate(newDueDate.getDate() + 14);
 }
 ...
-  dueDate: newDueDate.toISOString()
+  // Persist as date-only string to avoid TZ shifts
+  dueDate: newDueDate.toISOString().slice(0, 10)

Please review the UX requirements and adjust the offsets / storage format accordingly.


Fix due date calculation and storage format

The current logic for the “Next 7 days” and “Upcoming” columns always jumps by exactly 1 day or 8 days, and storing via toISOString() can introduce off-by-one errors across time zones.

  • Column index 1 (“Next 7 days”) should place the due date anywhere within the next 7 days, not strictly tomorrow.
  • Column index 2 (“Upcoming”) should cover > 7 days, not always 8 days.
  • Using newDueDate.toISOString() persists a full UTC timestamp, which may shift the date when your UI treats dueDate as a plain date.

Please adjust offsets to match your UX requirements and persist only a date-only string to avoid TZ shifts. For example:

-} else if (columnIndex === 1) {
-  // Next 7 days column - set to tomorrow
-  newDueDate.setDate(newDueDate.getDate() + 1);
-} else {
-  // Upcoming column - set to next week
-  newDueDate.setDate(newDueDate.getDate() + 8);
+} else if (columnIndex === 1) {
+  // Any day within the next 7 days – e.g. +3 days
+  newDueDate.setDate(newDueDate.getDate() + 3);
+} else {
+  // >7 days out – e.g. +14 days
+  newDueDate.setDate(newDueDate.getDate() + 14);
 }
 …
-  dueDate: newDueDate.toISOString()
+  // Persist only the YYYY-MM-DD to prevent TZ-related shifts
+  dueDate: newDueDate.toISOString().substring(0, 10)

Location: app/HomeClient.tsx lines 577–663

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleDragEnd = (result: DropResult) => {
if (isMobile) return; // Prevent drag end handling on mobile
const { destination, source, draggableId } = result;
// If there's no destination or the item was dropped in its original position
if (!destination ||
(destination.droppableId === source.droppableId &&
destination.index === source.index)) {
return;
}
// Update the todo's due date
const updatedTodo = {
...todo,
dueDate: newDueDate.toISOString()
};
// Find the todo that was dragged
const todo = todos.find(t => t.id === draggableId);
if (!todo) return;
// Calculate new due date based on destination column
let newDueDate = new Date();
if (destination.droppableId.startsWith('desktop')) {
const columnIndex = Number.parseInt(destination.droppableId.split('-')[2] ?? '', 10);
if (Number.isNaN(columnIndex)) {
console.warn('Unhandled droppableId:', destination.droppableId);
return;
}
if (columnIndex === 0) {
// Today's column - keep current date
newDueDate = new Date();
} else if (columnIndex === 1) {
// Next 7 days column - set to tomorrow
newDueDate.setDate(newDueDate.getDate() + 1);
} else {
// Upcoming column - set to next week
newDueDate.setDate(newDueDate.getDate() + 8);
}
} else if (destination.droppableId.startsWith('tablet')) {
const columnIndex = Number.parseInt(destination.droppableId.split('-')[2] ?? '', 10);
if (Number.isNaN(columnIndex)) {
console.warn('Unhandled droppableId:', destination.droppableId);
return;
}
if (columnIndex === 0) {
// Today's column
newDueDate = new Date();
} else {
// Upcoming column
newDueDate.setDate(newDueDate.getDate() + 1);
}
}
// Create new array with updated todo
const newTodos = todos.filter(t => t.id !== draggableId);
newTodos.splice(destination.index, 0, updatedTodo);
// Update state
setTodos(newTodos);
// Log the update
console.log(`Todo "${todo.title}" moved to ${destination.droppableId} at index ${destination.index}`);
// Update the database after animations finish
if (session?.user) {
setTimeout(async () => {
try {
const res = await fetch('/api/todos', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: draggableId, dueDate: updatedTodo.dueDate }),
});
if (!res.ok) throw new Error('Failed to update todo via drag-and-drop');
const serverTodo = await res.json();
// Sync state with server response
setTodos(prev => prev.map(t => t.id === draggableId ? serverTodo : t));
console.log('✅ Todo dueDate updated on server via drag:', serverTodo);
} catch (error) {
console.error('❌ Error updating todo via drag:', error);
}
}, 350);
}
// Update the todo's due date
const updatedTodo = {
...todo,
dueDate: newDueDate.toISOString()
};
return (
<div className="min-h-screen flex flex-col bg-gray-100 dark:bg-[#09090B] text-gray-900 dark:text-white p-4 transition-colors duration-200">
<div className="flex flex-row items-center justify-left">
<Image src="/logo.png" alt="agenda.dev" width={32} height={32} className="mr-2" />
<h1 className="text-xl">agenda.dev</h1>
</div>
<div className="relative mx-auto mb-4 flex items-center space-x-2 justify-center md:absolute md:top-4 md:right-4 md:mb-0 md:mx-0 md:justify-start">
{session?.user && (
<WorkspaceSwitcher
workspaces={workspaces}
currentWorkspace={currentWorkspace}
onSwitch={setCurrentWorkspace}
onCreateNew={() => setIsNewWorkspaceDialogOpen(true)}
onDelete={deleteWorkspace}
todos={todos}
/>
)}
<CompletedToggle showCompleted={showCompleted} setShowCompleted={setShowCompleted} />
<ViewToggle isTableView={isTableView} setIsTableView={setIsTableView} />
<ThemeToggle />
<FeedbackWidget />
<LoginButton />
</div>
// Create new array with updated todo
const newTodos = todos.filter(t => t.id !== draggableId);
newTodos.splice(destination.index, 0, updatedTodo);
// Update state
setTodos(newTodos);
// Log the update
console.log(`Todo "${todo.title}" moved to ${destination.droppableId} at index ${destination.index}`);
// Update the database after animations finish
if (session?.user) {
setTimeout(async () => {
try {
const res = await fetch('/api/todos', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: draggableId, dueDate: updatedTodo.dueDate }),
});
if (!res.ok) throw new Error('Failed to update todo via drag-and-drop');
const serverTodo = await res.json();
// Sync state with server response
setTodos(prev => prev.map(t => t.id === draggableId ? serverTodo : t));
console.log('✅ Todo dueDate updated on server via drag:', serverTodo);
} catch (error) {
console.error('❌ Error updating todo via drag:', error);
}
}, 350);
}
};
const handleDragEnd = (result: DropResult) => {
if (isMobile) return; // Prevent drag end handling on mobile
const { destination, source, draggableId } = result;
// If there's no destination or the item was dropped in its original position
if (
!destination ||
(destination.droppableId === source.droppableId && destination.index === source.index)
) {
return;
}
// Find the todo that was dragged
const todo = todos.find(t => t.id === draggableId);
if (!todo) return;
// Calculate new due date based on destination column
let newDueDate = new Date();
if (destination.droppableId.startsWith('desktop')) {
const columnIndex = Number.parseInt(destination.droppableId.split('-')[2] ?? '', 10);
if (Number.isNaN(columnIndex)) {
console.warn('Unhandled droppableId:', destination.droppableId);
return;
}
if (columnIndex === 0) {
// Today's column - keep current date
newDueDate = new Date();
} else if (columnIndex === 1) {
// Any day within the next 7 days – default to +3 days
newDueDate.setDate(newDueDate.getDate() + 3);
} else {
// Upcoming (>7 days) – default to +14 days
newDueDate.setDate(newDueDate.getDate() + 14);
}
} else if (destination.droppableId.startsWith('tablet')) {
const columnIndex = Number.parseInt(destination.droppableId.split('-')[2] ?? '', 10);
if (Number.isNaN(columnIndex)) {
console.warn('Unhandled droppableId:', destination.droppableId);
return;
}
if (columnIndex === 0) {
// Today's column
newDueDate = new Date();
} else {
// Upcoming column
newDueDate.setDate(newDueDate.getDate() + 1);
}
}
// Update the todo's due date
const updatedTodo = {
...todo,
// Persist only the YYYY-MM-DD to prevent TZ-related shifts
dueDate: newDueDate.toISOString().substring(0, 10)
};
// Create new array with updated todo
const newTodos = todos.filter(t => t.id !== draggableId);
newTodos.splice(destination.index, 0, updatedTodo);
// Update state
setTodos(newTodos);
// Log the update
console.log(
`Todo "${todo.title}" moved to ${destination.droppableId} at index ${destination.index}`
);
// Update the database after animations finish
if (session?.user) {
setTimeout(async () => {
try {
const res = await fetch('/api/todos', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: draggableId, dueDate: updatedTodo.dueDate }),
});
if (!res.ok) throw new Error('Failed to update todo via drag-and-drop');
const serverTodo = await res.json();
// Sync state with server response
setTodos(prev => prev.map(t => (t.id === draggableId ? serverTodo : t)));
console.log('✅ Todo dueDate updated on server via drag:', serverTodo);
} catch (error) {
console.error('❌ Error updating todo via drag:', error);
}
}, 350);
}
};

Copy link
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
app/HomeClient.tsx (5)

136-156: Consider improving keyboard shortcut ergonomics

The current implementation requires both Ctrl AND Meta (Command) keys to be pressed together, which is an uncommon combination. Most applications use either Ctrl (Windows/Linux) OR Meta (macOS).

-if (e.ctrlKey && e.metaKey && !e.altKey && !e.shiftKey) {
+if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey) {

289-299: Improve temporary ID generation

The current temporary ID approach is functional but could be more robust by using UUID when available.

-const tempId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+const tempId = `temp-${crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`}`;

364-419: Consider environment-aware logging

The extensive console logging in the rescheduleTodo function is helpful for debugging but may clutter the console in production.

Consider wrapping detailed logs in a development-only conditional:

-console.log('🎯 Starting reschedule flow:', { id, newDate })
-console.log('📅 Previous due date:', todoToUpdate.dueDate)
+if (process.env.NODE_ENV !== 'production') {
+  console.log('🎯 Starting reschedule flow:', { id, newDate })
+  console.log('📅 Previous due date:', todoToUpdate.dueDate)
+}

780-823: Large commented-out CommandPalette section

There's a substantial commented-out section for a CommandPalette component with a "Will add in later" comment. This could cause confusion about the component's status.

Consider:

  1. Moving this code to a separate branch if it's a work in progress
  2. Creating a TODO issue for tracking this feature
  3. Adding a more descriptive comment with a timeline or issue reference

159-249: Sync logic only runs on session change

The server synchronization logic only runs when the session changes, not when todos are modified client-side. This could lead to inconsistencies if multiple devices are used simultaneously.

Consider adding a periodic sync mechanism or triggering sync after successful CRUD operations:

// Example: add periodic sync
+useEffect(() => {
+  if (!session?.user) return;
+  
+  const syncInterval = setInterval(() => {
+    syncWithServer();
+  }, 60000); // Sync every minute
+  
+  return () => clearInterval(syncInterval);
+}, [session?.user]);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1714f69 and 9239da6.

📒 Files selected for processing (2)
  • app/HomeClient.tsx (2 hunks)
  • desktop-build.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • desktop-build.js
🔇 Additional comments (5)
app/HomeClient.tsx (5)

54-76: Well-implemented mobile detection hook!

The useIsMobile hook is cleanly implemented with proper event listener cleanup and SSR compatibility checking with typeof window !== 'undefined'.


607-647: Date calculation logic improvements look good

The improved date calculation logic for drag and drop operations now correctly:

  1. Uses safer Number.parseInt with radix and null coalescing
  2. Adds error handling for invalid column indices
  3. Uses more intuitive date ranges (today, today+3, today+14)
  4. Properly formats dates to avoid timezone issues

These changes effectively address previous concerns about date handling.


715-731: Good responsive layout implementation

The split input implementation with fixed positioning at the bottom for mobile and sticky positioning at the top for desktop creates an intuitive user experience on different devices.


750-750: Properly handling mobile drag-disable

Passing the isMobile flag to disable drag functionality on mobile devices improves usability on touch devices.


704-704: Commented-out ViewToggle component

There's a commented ViewToggle component that suggests incomplete work or a temporarily disabled feature.

Is this intentional or should the ViewToggle be completely removed or re-enabled?

Copy link
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🔭 Outside diff range comments (1)
lib/auth.ts (1)

24-26: ⚠️ Potential issue

Environment check for Twitter creds is now misleading

You still throw when TWITTER_CLIENT_ID/SECRET are missing, but the Twitter provider is commented out.
Drop the env-var guard or re-enable the provider to avoid blocking startup unnecessarily.

♻️ Duplicate comments (1)
app/HomeClient.tsx (1)

128-148: ⚠️ Potential issue

computeNewDueDate returns local dates that later get forced to UTC midnight

The helper returns a Date at local midnight.
Later (handleDragEnd, lines 760-763) the date is serialized with "T00:00:00.000Z" which shifts the day backwards/forwards for non-UTC users.

Recommendation: persist date-only strings and let the server/UI interpret them in the user’s locale:

-const formattedDate = `${yyy}-${mm}-${dd}`;
-const updatedTodo = { ...todo, dueDate: `${formattedDate}T00:00:00.000Z` };
+const formattedDate = `${yyy}-${mm}-${dd}`; // YYYY-MM-DD
+const updatedTodo  = { ...todo, dueDate: formattedDate };

Make sure corresponding API routes accept the YYYY-MM-DD format.

🧹 Nitpick comments (14)
README.md (1)

19-23: Missing period after abbreviation "etc"

In line 22, "etc" should include a period to follow proper American English conventions.

-  - [ ] Add Stripe on trial/subscription end etc emails via resend.
+  - [ ] Add Stripe on trial/subscription end etc. emails via resend.
🧰 Tools
🪛 LanguageTool

[style] ~22-~22: In American English, abbreviations like “etc.” require a period.
Context: ... ] Add Stripe on trial/subscription end etc emails via resend. ## Tech Stack - **...

(ETC_PERIOD)

components/workspace-switcher.tsx (2)

101-105: Early-exit already covered – tighten the click handler

The onClick handler calls setIsOpen(false) regardless of canCreateNew. Because the button is also disabled={!canCreateNew}, the inner if is redundant in practice.
You can simplify the handler to an early return for readability:

- => {
-  if (canCreateNew) {
-    onCreateNew()
-  }
-  setIsOpen(false)
-}}
+ => {
+  if (!canCreateNew) return;
+  onCreateNew();
+  setIsOpen(false);
+}}

106-110: Use a class helper instead of manual concatenation

Manual string concatenation is error-prone (e.g. missing whitespace) and harder to maintain.
Since the project already uses Tailwind, consider clsx, cn, or an array join:

-import clsx from 'clsx'          // if not present yet
...
-className={
-  `w-full ... gap-2 ` +
-  `${canCreateNew ? 'hover:bg-gray-100 ...' : 'opacity-50 cursor-not-allowed ...'}`
-}
+className={clsx(
+  'w-full px-4 py-2 text-left text-sm transition-colors duration-200 flex items-center gap-2',
+  canCreateNew
+    ? 'hover:bg-gray-100 dark:hover:bg-white/5 text-gray-900 dark:text-white'
+    : 'opacity-50 cursor-not-allowed text-gray-500 dark:text-gray-600'
+)}

This keeps the Tailwind classes declarative and removes string-glue logic.

components/LoginButton.tsx (1)

103-108: Unused Twitter component import

TwitterSignInButton is still imported (line 20-21) but its usage was commented out (line 120). This will raise an “unused import” lint error.

-import TwitterSignInButton from "./TwitterSignInButton"

If you plan to re-enable Twitter later, wrap the import in a TODO comment instead.

app/api/workspaces/route.ts (1)

67-75: Error classification can mis-label 401 vs 403

status = msg.includes('limit') ? 403 : 401;

If the error message wording changes, this heuristic fails. Prefer explicit error types from requireSubscription (e.g., custom SubscriptionLimitError class) to decide status codes.

lib/auth.ts (1)

72-109: Centralise plan limits to avoid duplication

limits: { workspaces: 5 } here must stay in sync with PLAN_LIMITS used in requireSubscription.ts.
Consider importing the constants from a single module to prevent drift.

lib/requireSubscription.ts (2)

13-53: Consider using custom error types and centralizing defaults.

The function implementation is well-structured with clear steps for authorization and limit validation, but has a few areas for improvement:

  1. Using generic Error objects makes it difficult for callers to distinguish between different error types.
  2. The default workspace limit (2) is duplicated in line 36 and implied in the default case.

Consider implementing these improvements:

+ // Custom error classes for better error handling
+ export class UnauthorizedError extends Error {
+   constructor(message: string = "Unauthorized") {
+     super(message);
+     this.name = "UnauthorizedError";
+   }
+ }
+
+ export class WorkspaceLimitError extends Error {
+   constructor(message: string) {
+     super(message);
+     this.name = "WorkspaceLimitError";
+   }
+ }
+
+ // Default limits as a constant
+ const DEFAULT_WORKSPACE_LIMIT = 2;
+
const PLAN_LIMITS: Record<string, PlanLimits> = {
  pro: { workspaces: 5 }
};

export async function requireSubscription(
  headers: Headers
): Promise<{ userId: string; limits: PlanLimits }> {
  // 1) Validate session
  const session = await auth.api.getSession({ headers });
  if (!session?.user?.id) {
-   throw new Error("Unauthorized");
+   throw new UnauthorizedError();
  }
  const userId = session.user.id;

  // 2) Fetch all subscriptions for this reference
  const subs = await db.query.subscriptions.findMany({
    where: eq(subscriptions.referenceId, userId),
  });

  // 3) Determine active/trialing subscription
  const active = subs.find(
    (s) => s.status === "active" || s.status === "trialing"
  );

  // 4) Derive limits: default free users get 2 workspaces
  const limits: PlanLimits = active
-   ? PLAN_LIMITS[active.plan] ?? { workspaces: 2 }
-   : { workspaces: 2 };
+   ? PLAN_LIMITS[active.plan] ?? { workspaces: DEFAULT_WORKSPACE_LIMIT }
+   : { workspaces: DEFAULT_WORKSPACE_LIMIT };

  // 5) Count existing workspaces
  const existingCount = (
    await db.query.workspaces.findMany({
      where: eq(workspaces.ownerId, userId),
    })
  ).length;

  // 6) Enforce limit
  if (existingCount >= limits.workspaces) {
-   throw new Error(
+   throw new WorkspaceLimitError(
      `Workspace limit reached for plan${active?.plan ? ` (${active.plan})` : ""}.`
    );
  }

  return { userId, limits };
}

13-53: Consider optimizing database queries for performance.

The function makes two separate database queries (one for subscriptions and one for workspaces count). This could potentially be optimized for better performance.

Consider combining the queries or utilizing a cache mechanism for the workspace count to reduce database load, especially for high-traffic scenarios.

lib/db/migrations/0009_high_vindicator.sql (1)

1-16: Consider adding indexes for performance optimization.

While the table structure is sound, frequently queried fields like reference_id and status should have indexes to improve query performance, especially as the number of subscriptions grows.

CREATE TABLE "subscriptions" (
	"id" text PRIMARY KEY NOT NULL,
	"plan" text NOT NULL,
	"reference_id" text NOT NULL,
	"stripe_customer_id" text,
	"stripe_subscription_id" text,
	"status" text NOT NULL,
	"period_start" timestamp,
	"period_end" timestamp,
	"cancel_at_period_end" boolean,
	"seats" integer,
	"trial_start" timestamp,
	"trial_end" timestamp,
	"created_at" timestamp DEFAULT now() NOT NULL,
	"updated_at" timestamp DEFAULT now() NOT NULL
);
+ CREATE INDEX idx_subscriptions_reference_id ON "subscriptions" ("reference_id");
+ CREATE INDEX idx_subscriptions_status ON "subscriptions" ("status");
+ CREATE INDEX idx_subscriptions_period_end ON "subscriptions" ("period_end");
lib/db/schema.ts (1)

111-126: Consider adding foreign key constraints for referential integrity.

The referenceId field doesn't have a foreign key constraint, which might lead to referential integrity issues if a user or organization is deleted while having active subscriptions.

Since referenceId can reference either a user or an organization, consider adding an additional field to specify the reference type, then creating appropriate foreign key constraints:

export const subscriptions = pgTable("subscriptions", {
    id: text('id').primaryKey(),
    plan: text('plan').notNull(),
    referenceId: text('reference_id').notNull(),           // user-ID or org-ID
+   referenceType: text('reference_type').notNull(),       // 'user' or 'organization'
    stripeCustomerId: text('stripe_customer_id'),
    stripeSubscriptionId: text('stripe_subscription_id'),
    status: text('status').notNull(),                      // active, trialing, canceled…
    periodStart: timestamp('period_start'),
    periodEnd: timestamp('period_end'),
    cancelAtPeriodEnd: boolean('cancel_at_period_end'),
    seats: integer('seats'),
    trialStart: timestamp('trial_start'),
    trialEnd: timestamp('trial_end'),
    createdAt: timestamp('created_at').notNull().defaultNow(),
    updatedAt: timestamp('updated_at').notNull().defaultNow(),
});

Then you could create foreign key constraints through migration scripts based on the reference type.

components/LandingHero.tsx (3)

31-79: Use relative dates for example todos to prevent staleness.

The example todos use absolute dates calculated from the current time, which is good for keeping the examples relevant. However, there's a small inconsistency in how the dates are displayed.

Consider using a more consistent approach for all example dates:

// Example todos for demonstration
const exampleTodos: PreviewTodo[] = [
  {
    id: "example-1",
    title: "Meet with investors",
    completed: false,
-   dueDate: new Date(Date.now() + 4 * 24 * 60 * 60 * 1000).toISOString(), // 4 days from now
+   dueDate: new Date(Date.now() + 4 * 24 * 60 * 60 * 1000).toISOString(),
    urgency: 5.0,
    comments: [
      // ...
    ]
  },
  // Apply similar changes to other example todos...
];

+ // Helper function to create relative dates for examples
+ const getDaysFromNow = (days: number) => {
+   return new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString();
+ };

287-290: Consider performance optimizations for background gradients.

The background gradients with large blur effects can cause performance issues on lower-end devices, especially when combined with animations.

Consider:

  1. Using smaller blur radii (e.g., blur-2xl instead of blur-3xl)
  2. Adding the will-change: opacity, transform CSS property to optimize rendering
  3. Using @media (prefers-reduced-motion) to disable or reduce animations for users who prefer reduced motion

442-455: Use Lucide React components consistently for SVG icons.

While most icons are imported from lucide-react, some icons are defined inline as SVG elements. This inconsistency can make maintenance more difficult.

Extract these inline SVGs to proper Lucide React imports or create custom icon components for consistency:

- icon={<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-6 h-6 text-[#7c5aff]"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>}
+ icon={<Monitor className="w-6 h-6 text-[#7c5aff]" />}

- icon={<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-6 h-6 text-[#7c5aff]"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"></rect><line x1="12" y1="18" x2="12.01" y2="18"></line></svg>}
+ icon={<Smartphone className="w-6 h-6 text-[#7c5aff]" />}

Add the corresponding imports at the top of the file.

app/HomeClient.tsx (1)

754-763: Guard against invalid colIdx & use Number.parseInt

Although you added a NaN check, feeding parseInt an undefined element can still yield unexpected results.
A safer slice avoids mutation by architecture changes:

-const parts = destination.droppableId.split('-');
-const colIdx = parseInt(parts[parts.length - 1]!, 10);
+const colIdx = Number.parseInt(destination.droppableId.split('-').at(-1) ?? '', 10);

Keeps the logic robust if the Droppable ID schema evolves.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0631f6c and 6f9e477.

⛔ Files ignored due to path filters (6)
  • app/opengraph-image.png is excluded by !**/*.png
  • app/twitter-image.png is excluded by !**/*.png
  • bun.lock is excluded by !**/*.lock
  • bun.lockb is excluded by !**/bun.lockb
  • package-lock.json is excluded by !**/package-lock.json
  • public/noise-dark.png is excluded by !**/*.png
📒 Files selected for processing (26)
  • README.md (1 hunks)
  • app/HomeClient.tsx (2 hunks)
  • app/api/[transport]/route.ts (1 hunks)
  • app/api/parse-todo/route.ts (8 hunks)
  • app/api/workspaces/route.ts (3 hunks)
  • changelog.md (0 hunks)
  • components/AgendaIcon.tsx (1 hunks)
  • components/LandingHero.tsx (1 hunks)
  • components/LoginButton.tsx (1 hunks)
  • components/SettingsDialog.tsx (5 hunks)
  • components/workspace-switcher.tsx (3 hunks)
  • lib/auth-client.ts (1 hunks)
  • lib/auth.ts (3 hunks)
  • lib/db/migrations/0009_high_vindicator.sql (1 hunks)
  • lib/db/migrations/0010_young_monster_badoon.sql (1 hunks)
  • lib/db/migrations/meta/0009_snapshot.json (1 hunks)
  • lib/db/migrations/meta/0010_snapshot.json (1 hunks)
  • lib/db/migrations/meta/_journal.json (1 hunks)
  • lib/db/schema.ts (2 hunks)
  • lib/requireSubscription.ts (1 hunks)
  • mcp-research.md (0 hunks)
  • new-hoverbar.md (0 hunks)
  • package.json (3 hunks)
  • spinner.css (0 hunks)
  • spinner.tsx (0 hunks)
  • toast.tsx (0 hunks)
💤 Files with no reviewable changes (6)
  • mcp-research.md
  • spinner.tsx
  • spinner.css
  • changelog.md
  • new-hoverbar.md
  • toast.tsx
✅ Files skipped from review due to trivial changes (5)
  • lib/db/migrations/0010_young_monster_badoon.sql
  • lib/db/migrations/meta/_journal.json
  • lib/db/migrations/meta/0009_snapshot.json
  • components/AgendaIcon.tsx
  • lib/db/migrations/meta/0010_snapshot.json
🧰 Additional context used
🧬 Code Graph Analysis (4)
components/LoginButton.tsx (3)
components/ui/dialog.tsx (5)
  • DialogTrigger (123-123)
  • DialogContent (124-124)
  • DialogHeader (125-125)
  • DialogTitle (127-127)
  • DialogDescription (128-128)
components/GoogleSignInButton.tsx (1)
  • GoogleSignInButton (4-21)
components/GithubSignInButton.tsx (1)
  • GithubSignInButton (4-21)
lib/requireSubscription.ts (3)
lib/auth.ts (1)
  • auth (44-110)
lib/db/index.ts (1)
  • db (10-10)
lib/db/schema.ts (2)
  • subscriptions (111-126)
  • workspaces (60-66)
app/api/workspaces/route.ts (2)
lib/requireSubscription.ts (1)
  • requireSubscription (13-53)
lib/db/schema.ts (1)
  • workspaceMembers (68-75)
components/LandingHero.tsx (2)
lib/utils.ts (1)
  • formatDate (9-43)
components/ui/button.tsx (1)
  • Button (52-52)
🪛 LanguageTool
README.md

[style] ~22-~22: In American English, abbreviations like “etc.” require a period.
Context: ... ] Add Stripe on trial/subscription end etc emails via resend. ## Tech Stack - **...

(ETC_PERIOD)

🔇 Additional comments (12)
app/api/[transport]/route.ts (1)

1-16: MCP handler implementation looks well-structured

The implementation of the dice rolling tool using the MCP adapter is clean and follows good practices. The input validation with zod ensures proper error handling for invalid inputs.

This appears to be related to the "MCP Server for Workspaces" item mentioned in the README as a current implementation task.

package.json (1)

18-18: Dependencies align with implementation needs

The added dependencies for Stripe integration (@better-auth/stripe, stripe), MCP adapter (@vercel/mcp-adapter), and date parsing (chrono-node) appropriately support the functionality being added in the PR.

The fixed version of better-auth (1.2.6) ensures compatibility with the @better-auth/stripe package which is at the same version.

Also applies to: 58-58, 61-62, 92-92

lib/auth-client.ts (1)

2-2: Well-structured Stripe client integration

The Stripe client plugin is cleanly integrated into the auth client configuration with clear comments explaining its purpose. The export of the subscription functionality alongside other auth methods provides a consistent API for authentication and subscription management.

This implementation aligns with the Stripe integration task mentioned in the README.

Also applies to: 6-9, 12-13

components/LoginButton.tsx (1)

111-126: Missing semantic role & focus trapping in custom dialog

Nice visual improvements! Two accessibility gaps remain:

  1. DialogContent lacks aria-modal="true" and focus trapping for keyboard users. Verify that your Dialog abstraction handles this automatically; if not, add it.
  2. The Terms / Privacy links open in the same tab, potentially losing user context. Consider target="_blank" rel="noopener noreferrer".
lib/requireSubscription.ts (2)

1-5: Clean imports with clear purpose.

The imports correctly bring in all the necessary dependencies for authentication, database access, and schema operations.


6-11: Good use of type definition for plan limits.

The PlanLimits type and PLAN_LIMITS constant provide a clear mapping between subscription plans and their workspace limits, making it easy to manage and update plan configurations.

lib/db/migrations/0009_high_vindicator.sql (1)

1-16: Well-structured table definition with comprehensive fields.

The migration script creates a well-designed subscriptions table with all necessary fields for subscription management. The schema includes:

  1. Primary identifiers and references
  2. Stripe integration fields
  3. Subscription status and period tracking
  4. Proper timestamps

The column types and constraints are appropriate for each field.

lib/db/schema.ts (2)

9-9: Good addition of Stripe customer ID to user schema.

Adding stripeCustomerId as an optional field to the users table is appropriate and aligns with the Stripe integration requirements.


111-126: Comprehensive subscription schema with clear comments.

The subscriptions table schema is well-defined with all necessary fields for subscription management. The inline comments provide helpful context about field purposes (e.g., // user-ID or org-ID and // active, trialing, canceled…).

components/LandingHero.tsx (2)

1-9: Clean imports with correct component dependencies.

The imports are well-organized, bringing in necessary React hooks, UI components, and icons.


11-28: Well-defined TypeScript interfaces for preview data.

The PreviewTodo and PreviewComment interfaces provide clear type definitions that ensure type safety throughout the component.

app/api/parse-todo/route.ts (1)

118-126: Reliance on NEXT_PUBLIC_APP_URL may break on self-hosted / preview deployments

convertRelativeDate() builds the endpoint with

`${process.env.NEXT_PUBLIC_APP_URL}/api/convert-date`

If the env-var is unset (common in Vercel preview or local dev) the URL becomes "undefined/api/convert-date"ENOTFOUND.

Prefer deriving the base URL from the incoming request or falling back to an absolute path:

-const response = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/convert-date`, {
+const base =
+  process.env.NEXT_PUBLIC_APP_URL ??
+  process.env.VERCEL_URL && `https://${process.env.VERCEL_URL}` ??
+  ''; // empty string fallback => relative call
+const response = await fetch(`${base}/api/convert-date`, {

This keeps the function environment-agnostic.

Comment on lines 15 to 16
canCreateNew: boolean
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Prop is now mandatory – update all call-sites

canCreateNew was added as a required boolean prop, which will break any existing usage of <WorkspaceSwitcher /> that has not been updated.
Consider giving it a sensible default (true) or making it optional to avoid runtime/TypeScript errors across the codebase.

Comment on lines 64 to 66
const cookieStore = await cookies();

const session = await auth.api.getSession({
headers: new Headers({
cookie: cookieStore.toString()
})
});

// More robust session validation
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const headers = new Headers({ cookie: cookieStore.toString() });
let userId: string;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

cookies() is synchronous – remove the unnecessary await

cookies() from next/headers returns a ReadonlyRequestCookies instance, not a Promise.
Using await here (and in GET/DELETE above) causes a TypeScript error:

-const cookieStore = await cookies();
+const cookieStore = cookies();

Apply the same fix where await cookies() appears elsewhere in this file.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const cookieStore = await cookies();
const session = await auth.api.getSession({
headers: new Headers({
cookie: cookieStore.toString()
})
});
// More robust session validation
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const headers = new Headers({ cookie: cookieStore.toString() });
let userId: string;
const cookieStore = cookies();
const headers = new Headers({ cookie: cookieStore.toString() });
let userId: string;

lib/auth.ts Outdated
Comment on lines 40 to 42
const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2025-02-24.acacia",
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Invalid Stripe API version string

"2025-02-24.acacia" is not a valid Stripe API version (latest as of 2024 is 2023-10-16).
Using an unknown version will make the SDK throw on initialization.

-  apiVersion: "2025-02-24.acacia",
+  apiVersion: "2023-10-16",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2025-02-24.acacia",
});
const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2023-10-16",
});

Comment on lines 28 to 37
// 3) Determine active/trialing subscription
const active = subs.find(
(s) => s.status === "active" || s.status === "trialing"
);

// 4) Derive limits: default free users get 2 workspaces
const limits: PlanLimits = active
? PLAN_LIMITS[active.plan] ?? { workspaces: 2 }
: { workspaces: 2 };

Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify the subscription status logic for edge cases.

The current implementation checks for "active" or "trialing" status to determine an active subscription, but there might be other valid statuses in Stripe that aren't being handled explicitly.


🏁 Script executed:

#!/bin/bash
# Check for all possible subscription statuses in the Stripe API documentation

curl -s https://api.stripe.com/v1/subscription_items -G \
  -d limit=1 \
  -H "Authorization: Bearer dummy_key" 2>&1 | grep -i "error" | grep -i "status"

# Alternatively, we can search the codebase for places where subscription status is set
rg "s(ub)?\.status\s*(=|==|===|!=|!==|!===)\s*['\"]" --type ts

Length of output: 512


🌐 Web query:

List all possible `status` values for a Stripe Subscription object according to the Stripe API documentation

💡 Result:

According to the Stripe API documentation, a Subscription object can have the following status values:

  • incomplete
  • incomplete_expired
  • trialing
  • active
  • past_due
  • canceled
  • unpaid
  • paused

These statuses represent various stages in the lifecycle of a subscription, from initial creation to cancellation or pausing. (docs.stripe.com)

Citations:


Audit Stripe subscription statuses

Stripe supports these subscription statuses: incomplete, incomplete_expired, trialing, active, past_due, canceled, unpaid, and paused (see Stripe docs¹). Currently, in lib/requireSubscription.ts (and similarly in SettingsDialog.tsx and HomeClient.tsx), only active and trialing are treated as active subscriptions:

const active = subs.find(
  (s) => s.status === "active" || s.status === "trialing"
);

Please review whether other statuses—most notably past_due—should also count as “active” for workspace limits. If so, update the predicate and consider centralizing this logic for clarity:

• File: lib/requireSubscription.ts (lines 28–37)
• Files with the same pattern:
– git/components/SettingsDialog.tsx
– git/app/HomeClient.tsx

Suggested improvements:

  • Extend the check, e.g.:
    - (s) => s.status === "active" || s.status === "trialing"
    + (s) => ["active", "trialing", "past_due"].includes(s.status)
  • Encapsulate status logic in a helper or enum.
  • Add unit tests covering all Stripe statuses.

¹ https://docs.stripe.com/api/subscriptions/object

Comment on lines 488 to 504
<ul className="space-y-4 mb-8 flex-grow">
<li className="flex items-start">
<span className="text-green-500 mr-2.5 flex-shrink-0 mt-0.5">✓</span>
<span className="text-gray-700 dark:text-gray-300">Up to 3 workspaces</span>
</li>
<li className="flex items-start">
<span className="text-green-500 mr-2.5 flex-shrink-0 mt-0.5">✓</span>
<span className="text-gray-700 dark:text-gray-300">Add 1 collaborator per workspace</span>
</li>
<li className="flex items-start">
<span className="text-green-500 mr-2.5 flex-shrink-0 mt-0.5">✓</span>
<span className="text-gray-700 dark:text-gray-300">Basic AI model with limited context</span>
</li>
<li className="flex items-start">
<span className="text-green-500 mr-2.5 flex-shrink-0 mt-0.5">✓</span>
<span className="text-gray-700 dark:text-gray-300">Unlimited todos</span>
</li>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Subscription plan inconsistency with implementation.

Line 491 states that the free plan allows "Up to 3 workspaces", but in requireSubscription.ts the default for free users is set to 2 workspaces. This discrepancy could confuse users.

<li className="flex items-start">
  <span className="text-green-500 mr-2.5 flex-shrink-0 mt-0.5">✓</span>
-  <span className="text-gray-700 dark:text-gray-300">Up to 3 workspaces</span>
+  <span className="text-gray-700 dark:text-gray-300">Up to 2 workspaces</span>
</li>

Make sure to align the advertised limits with the actual implementation to avoid confusion and potential customer complaints.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<ul className="space-y-4 mb-8 flex-grow">
<li className="flex items-start">
<span className="text-green-500 mr-2.5 flex-shrink-0 mt-0.5"></span>
<span className="text-gray-700 dark:text-gray-300">Up to 3 workspaces</span>
</li>
<li className="flex items-start">
<span className="text-green-500 mr-2.5 flex-shrink-0 mt-0.5"></span>
<span className="text-gray-700 dark:text-gray-300">Add 1 collaborator per workspace</span>
</li>
<li className="flex items-start">
<span className="text-green-500 mr-2.5 flex-shrink-0 mt-0.5"></span>
<span className="text-gray-700 dark:text-gray-300">Basic AI model with limited context</span>
</li>
<li className="flex items-start">
<span className="text-green-500 mr-2.5 flex-shrink-0 mt-0.5"></span>
<span className="text-gray-700 dark:text-gray-300">Unlimited todos</span>
</li>
<ul className="space-y-4 mb-8 flex-grow">
<li className="flex items-start">
<span className="text-green-500 mr-2.5 flex-shrink-0 mt-0.5"></span>
- <span className="text-gray-700 dark:text-gray-300">Up to 3 workspaces</span>
+ <span className="text-gray-700 dark:text-gray-300">Up to 2 workspaces</span>
</li>
<li className="flex items-start">
<span className="text-green-500 mr-2.5 flex-shrink-0 mt-0.5"></span>
<span className="text-gray-700 dark:text-gray-300">Add 1 collaborator per workspace</span>
</li>
<li className="flex items-start">
<span className="text-green-500 mr-2.5 flex-shrink-0 mt-0.5"></span>
<span className="text-gray-700 dark:text-gray-300">Basic AI model with limited context</span>
</li>
<li className="flex items-start">
<span className="text-green-500 mr-2.5 flex-shrink-0 mt-0.5"></span>
<span className="text-gray-700 dark:text-gray-300">Unlimited todos</span>
</li>
</ul>

Comment on lines 277 to 282
const openAuthDialog = () => {
const loginButton = document.querySelector('button[aria-label="Sign In"]') as HTMLButtonElement
if (loginButton) {
loginButton.click()
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid direct DOM manipulation for dialog opening.

The openAuthDialog function uses direct DOM manipulation to find and click a login button, which creates a tight coupling to the DOM structure and can be brittle if the structure changes.

Consider using a more declarative approach by lifting the authentication state up and sharing it between components:

- const openAuthDialog = () => {
-   const loginButton = document.querySelector('button[aria-label="Sign In"]') as HTMLButtonElement
-   if (loginButton) {
-     loginButton.click()
-   }
- }
+ import { useAuthDialog } from "@/lib/auth-client"; // Assuming this context/hook exists
+ 
+ // In the LandingHero component
+ const { openAuthDialog } = useAuthDialog();

This approach would be more maintainable and less prone to breaking when the DOM structure changes.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 81 to 93
const formatDate = (dateStr: string) => {
const dueDate = new Date(dateStr)
const now = new Date()
const diffDays = Math.round((dueDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))

const month = dueDate.toLocaleString('default', { month: 'short' })
const day = dueDate.getDate()

if (diffDays === 0) return 'Today'
if (diffDays === 1) return 'Tomorrow'
if (diffDays < 7) return `${diffDays} days`
return `${month} ${day}`
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Reuse the existing date formatting utility.

You've implemented a formatDate function that duplicates functionality already available in lib/utils.ts. This can lead to inconsistencies in date formatting across the application.

Replace this implementation with the existing utility:

- const formatDate = (dateStr: string) => {
-   const dueDate = new Date(dateStr)
-   const now = new Date()
-   const diffDays = Math.round((dueDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
-   
-   const month = dueDate.toLocaleString('default', { month: 'short' })
-   const day = dueDate.getDate()
-   
-   if (diffDays === 0) return 'Today'
-   if (diffDays === 1) return 'Tomorrow'
-   if (diffDays < 7) return `${diffDays} days`
-   return `${month} ${day}`
- }
+ import { formatDate } from "@/lib/utils";

Or if you need slightly different formatting for the landing page, create a variant that builds on the existing utility.

Comment on lines 45 to 52
try {
const resp = await subscription.list()
const subs = resp.data ?? []
const active = subs.find((s: any) => s.status === "active" || s.status === "trialing")
setPlan(active?.plan ?? "free")
} catch (err) {
console.error("Error fetching subscription:", err)
// Don't show error toast for the known date error
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Normalize plan value before equality checks

subscription.list() often returns identifiers such as "pro-monthly" or "pro-yearly".
The subsequent UI logic (plan === 'pro') assumes the value is exactly "pro", which will silently downgrade Pro users to the Free flow (wrong button label, unexpected cancel vs upgrade call).

-const active = subs.find((s: any) => s.status === "active" || s.status === "trialing")
-setPlan(active?.plan ?? "free")
+const active = subs.find((s: any) =>
+  s.status === "active" || s.status === "trialing",
+)
+// Strip billing cadence suffixes like `-monthly`
+const tier = (active?.plan ?? "free").split("-")[0]
+setPlan(tier)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const resp = await subscription.list()
const subs = resp.data ?? []
const active = subs.find((s: any) => s.status === "active" || s.status === "trialing")
setPlan(active?.plan ?? "free")
} catch (err) {
console.error("Error fetching subscription:", err)
// Don't show error toast for the known date error
try {
const resp = await subscription.list()
const subs = resp.data ?? []
const active = subs.find((s: any) =>
s.status === "active" || s.status === "trialing",
)
// Strip billing cadence suffixes like `-monthly`
const tier = (active?.plan ?? "free").split("-")[0]
setPlan(tier)
} catch (err) {
console.error("Error fetching subscription:", err)
// Don't show error toast for the known date error

Comment on lines 130 to 157
// Launch Stripe Customer Portal or Upgrade
const handlePortal = async () => {
setIsPortalLoading(true)
try {
let result: any
if (plan === 'pro') {
const { data, error } = await subscription.cancel({
returnUrl: new URL('/?settings=true', window.location.origin).toString()
});

if (error) {
console.error("Error canceling subscription:", error)
throw error;
}

if (data?.url) {
window.location.href = data.url;
}
} else {
result = await subscription.upgrade({
plan: 'pro',
successUrl: new URL('/?settings=true', window.location.origin).toString(),
cancelUrl: new URL('/?settings=true', window.location.origin).toString()
})
const url = result.data?.url
if (url) window.location.href = url
}
} catch (err) {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

handlePortal triggers a cancellation instead of opening the Billing Portal

When plan === 'pro' the code calls subscription.cancel(...).
Users clicking “Manage Subscription” generally expect the Stripe customer-portal where they can update payment methods or change plans, not an immediate cancellation flow.

Unless your SDK’s cancel method is purposely named that way, consider:

-if (plan === 'pro') {
-  const { data, error } = await subscription.cancel({
-    returnUrl: new URL('/?settings=true', window.location.origin).toString()
-  });
+if (plan === 'pro') {
+  // Opens Stripe’s self-service portal; users choose cancel/upgrade themselves
+  const { data, error } = await subscription.portal({
+    returnUrl: new URL('/?settings=true', window.location.origin).toString()
+  });

This avoids accidental cancellations and aligns user expectation.
(If your SDK truly has no portal helper, rename the button and warn users explicitly.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Launch Stripe Customer Portal or Upgrade
const handlePortal = async () => {
setIsPortalLoading(true)
try {
let result: any
if (plan === 'pro') {
const { data, error } = await subscription.cancel({
returnUrl: new URL('/?settings=true', window.location.origin).toString()
});
if (error) {
console.error("Error canceling subscription:", error)
throw error;
}
if (data?.url) {
window.location.href = data.url;
}
} else {
result = await subscription.upgrade({
plan: 'pro',
successUrl: new URL('/?settings=true', window.location.origin).toString(),
cancelUrl: new URL('/?settings=true', window.location.origin).toString()
})
const url = result.data?.url
if (url) window.location.href = url
}
} catch (err) {
// Launch Stripe Customer Portal or Upgrade
const handlePortal = async () => {
setIsPortalLoading(true)
try {
let result: any
if (plan === 'pro') {
// Opens Stripe’s self-service portal; users choose cancel/upgrade themselves
const { data, error } = await subscription.portal({
returnUrl: new URL('/?settings=true', window.location.origin).toString()
});
if (error) {
console.error("Error canceling subscription:", error)
throw error;
}
if (data?.url) {
window.location.href = data.url;
}
} else {
result = await subscription.upgrade({
plan: 'pro',
successUrl: new URL('/?settings=true', window.location.origin).toString(),
cancelUrl: new URL('/?settings=true', window.location.origin).toString()
})
const url = result.data?.url
if (url) window.location.href = url
}
} catch (err) {

Comment on lines 68 to 80
// Query the subscriptions table for an active subscription
const userSubscriptions = await db.select({
plan: subscriptionsTable.plan,
status: subscriptionsTable.status,
})
.from(subscriptionsTable)
.where(
and(
eq(subscriptionsTable.referenceId, userId),
eq(subscriptionsTable.status, 'active')
)
)
.limit(1)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

hasProPlan overlooks “trialing” subscriptions

Front-end logic treats both "active" and "trialing" as Pro (see SettingsDialog) but the DB query here filters only status = 'active'.
Trial users will therefore get the lower-tier model even though the UI advertises Pro features.

-where(
-  and(
-    eq(subscriptionsTable.referenceId, userId),
-    eq(subscriptionsTable.status, 'active')
-  )
-)
+where(
+  and(
+    eq(subscriptionsTable.referenceId, userId),
+    subscriptionsTable.status.in(['active', 'trialing'])
+  )
+)

(Adjust syntax to drizzle-orm’s inArray helper if needed.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Query the subscriptions table for an active subscription
const userSubscriptions = await db.select({
plan: subscriptionsTable.plan,
status: subscriptionsTable.status,
})
.from(subscriptionsTable)
.where(
and(
eq(subscriptionsTable.referenceId, userId),
eq(subscriptionsTable.status, 'active')
)
)
.limit(1)
// Query the subscriptions table for an active or trialing subscription
const userSubscriptions = await db.select({
plan: subscriptionsTable.plan,
status: subscriptionsTable.status,
})
.from(subscriptionsTable)
.where(
and(
eq(subscriptionsTable.referenceId, userId),
// include both active and trialing as “Pro”
subscriptionsTable.status.in(['active', 'trialing'])
)
)
.limit(1)

@R44VC0RP R44VC0RP merged commit 6f9e477 into main May 8, 2025
2 of 3 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.

1 participant
0