Skip to content

Conversation

Stereco-btc
Copy link
Collaborator

@Stereco-btc Stereco-btc commented Oct 16, 2025

Overview: This PR updates the inviteMemberSchema to include a required email field and exports its inferred input type.

Changes

  • Added email: z.string().email().nonempty() to the inviteMemberSchema in src/actions/collaboration.ts.
  • Ensured inviteMemberSchema is exported for broader use.
  • Exported the InviteMemberInput type alias, which will help handlers and UI components import the validated and typed input.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added new dropdown menu shortcut component for enhanced UI customization.
  • Style

    • Improved codebase consistency with standardized code formatting across all components and modules.
  • Chores

    • Updated documentation with improved code examples and formatting.

@coderabbitai
Copy link

coderabbitai bot commented Oct 16, 2025

Walkthrough

Large-scale code style standardization across 80+ files, converting double quotes to single quotes, adding trailing semicolons, and normalizing formatting while maintaining functional equivalence. Includes minor UI enhancements such as form state synchronization and dropdown menu refinements.

Changes

Cohort / File(s) Summary
Configuration & Build
.prettierrc, eslint.config.mjs, postcss.config.mjs, tailwind.config.js
Quote normalization, semicolon additions, and minor formatting adjustments; trailing comma removed from postcss plugins object.
Documentation
README.md, CODE_OF_CONDUCT.md
Added code block examples to README installation steps; whitespace/line-wrapping adjustments in CODE_OF_CONDUCT.
Server Actions & Core Logic
src/actions/analytics.ts, src/actions/collaboration.ts, src/actions/user.ts, src/actions/projects/*
Quote normalization and minor formatting; collaboration functions reformatted across multiple lines; error message phrasing standardized.
Library Utilities
src/lib/*.ts
Quote and semicolon normalization; MOBILE_BREAKPOINT removed from exports in responsive.ts; exportProjects signature updated to use single-quoted union type in export.ts.
UI Components (Base)
src/components/ui/alert.tsx, src/components/ui/card.tsx, src/components/ui/checkbox.tsx, src/components/ui/dialog.tsx, src/components/ui/dropdown-menu.tsx, src/components/ui/form.tsx, src/components/ui/input.tsx, src/components/ui/label.tsx, src/components/ui/select.tsx, src/components/ui/separator.tsx, src/components/ui/sheet.tsx, src/components/ui/skeleton.tsx, src/components/ui/sidebar.tsx, src/components/ui/table.tsx, src/components/ui/tabs.tsx, src/components/ui/tooltip.tsx
Quote normalization and type signature updates (React.ComponentProps<'div'> format); new DropdownMenuShortcut component and DropdownMenuSeparator alias added in dropdown-menu; data-slot attribute quoting standardized.
Feature Components
src/components/models/analytics/ProjectStats.tsx, src/components/models/auth/SigninForm.tsx, src/components/models/auth/SignupForm.tsx, src/components/models/projects/CreateProjectDialog.tsx, src/components/models/projects/CreateProjectForm.tsx, src/components/models/projects/DeleteProjectDialog.tsx, src/components/models/projects/ExportDialog.tsx, src/components/models/projects/InviteMemberDialog.tsx, src/components/models/projects/ProjectMembersTable.tsx, src/components/models/projects/ProjectsTable.tsx, src/components/models/templates/TemplateCard.tsx, src/components/models/templates/TemplateSelector.tsx, src/components/models/user/ChangePasswordForm.tsx, src/components/models/user/ProfileForm.tsx
Quote normalization and formatting; CreateProjectForm adds form state synchronization (form.setValue('templateId', selectedTemplateId)); error messages and className strings standardized to single quotes.
Layout & Page Components
src/components/layouts/Header.tsx, src/components/pages/home/HomePageContainer.tsx, src/app/(general)/page.tsx, src/app/profile/page.tsx, src/components/common/*
Quote and whitespace normalization; error logging and conditional rendering reformatted with single-quoted strings.
React Utilities & Context
src/hooks/use-debounce.ts, src/hooks/use-mobile.ts, src/contexts/LoadingContext.tsx
Quote normalization and minor semicolon additions; no functional behavior changes.
Root & Middleware
src/middleware.ts, src/lib/jwt.ts, src/lib/permissions.ts, src/lib/prisma.ts, src/lib/validations/auth.ts, src/lib/validations/projects.ts, src/lib/constants.ts, src/lib/utils.ts, src/app/globals.css
Quote and semicolon normalization; trailing comma removed from middleware matcher array; blank line removed from jwt.ts.

Sequence Diagram

sequenceDiagram
    participant User
    participant Form as CreateProjectForm
    participant State as Form State
    participant Handler as onSubmit Handler
    
    User->>Form: Select Template
    Form->>State: form.setValue('templateId', selectedTemplateId)
    Note over State: Sync selected template<br/>to form state
    State-->>Form: Update form value
    User->>Form: Submit Form
    Form->>Handler: Trigger onSubmit
    Handler->>Handler: Build payload with synced templateId
    Handler-->>User: Success/Error Response
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

The changes follow a highly consistent, repetitive pattern (quote normalization, semicolons, whitespace) across the vast majority of files, reducing per-file complexity. However, the substantial breadth (~80+ files) and a handful of functional modifications (CreateProjectForm form sync, dropdown-menu new component, MOBILE_BREAKPOINT removal) warrant thorough scanning to identify non-cosmetic edits.

Possibly related PRs

Suggested reviewers

  • fehranbit

🐰 With quotes now single and semicolons in place,
Our code hops forward at a consistent pace,
Templates sync'd, components refined,
A code style garden, neatly aligned!
*~ CodeRabbit* ✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The pull request title promises the addition of an email field and export of an input type for inviteMemberSchema, but the provided summaries show only repository-wide formatting and quoting changes with no schema updates or new exports. This mismatch means the title does not accurately reflect the actual changes in the diff. Clarity is compromised because reviewers will expect feature work that is not present. Please update the pull request title to describe the actual scope of the changes (e.g., repository formatting and quote style normalization), or revise the diff to include the intended schema updates and type exports for inviteMemberSchema so that the title aligns with the implementation.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/invite-member-schema-email

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

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

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (9)
src/components/models/templates/TemplateCard.tsx (1)

13-21: Clickable Card lacks keyboard access and role (a11y).

Add role, tabIndex, and keyboard activation; include focus-visible styles.

Apply this diff:

     <Card
-      className={cn(
-        'cursor-pointer hover:border-primary transition-colors',
+      className={cn(
+        'cursor-pointer hover:border-primary transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2',
         isSelected && 'border-primary ring-2 ring-primary ring-offset-2',
         className
       )}
       onClick={() => onSelect(template.id)}
+      role="button"
+      tabIndex={0}
+      aria-label={`Select template ${template.name}`}
+      onKeyDown={(e) => {
+        if (e.key === 'Enter' || e.key === ' ') {
+          e.preventDefault();
+          onSelect(template.id);
+        }
+      }}
       {...props}
     >
src/contexts/LoadingContext.tsx (1)

1-1: Add ‘use client’ directive to client components using React hooks

Add the following as the first line in src/contexts/LoadingContext.tsx (and any other .tsx/.ts files that use useState/useContext/useEffect, e.g. src/hooks/use-action.ts):

+'use client';
 import React, { createContext, useContext, useState, ReactNode } from 'react';
src/middleware.ts (1)

19-25: Matcher vs. runtime check mismatch makes /projects unprotected without a token

Your config.matcher applies middleware to /projects/:path*, but the no‑token branch redirects only for paths starting with /protected. Result: unauthenticated access to /projects slips through. Also, the ..._protected suffix in the first matcher looks unintended.

Fix by aligning matcher with intent and removing the redundant runtime path check. Two options:

Option A (protect both /protected and /projects):

 export async function middleware(request: NextRequest) {
   const token = request.cookies.get('auth_token')?.value;

   if (token) {
     try {
       await verifyJWT(token);
       return NextResponse.next();
     } catch (error) {
       console.error('JWT verification failed:', error);
       return NextResponse.redirect(new URL('/signin', request.url));
     }
   }

-  // If no token, redirect to sign-in page for protected routes
-  // You might want to define which routes are protected
-  if (request.nextUrl.pathname.startsWith('/protected')) {
-    return NextResponse.redirect(new URL('/signin', request.url));
-  }
-
-  return NextResponse.next();
+  // If no token, redirect (these routes are scoped by config.matcher)
+  return NextResponse.redirect(new URL('/signin', request.url));
 }

 export const config = {
-  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)_protected', '/projects/:path*']
+  matcher: ['/protected/:path*', '/projects/:path*']
 };

Option B (protect only /protected):

-  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)_protected', '/projects/:path*']
+  matcher: ['/protected/:path*']

Choose one based on intended protected surface. Avoid mixing broad matchers with ad‑hoc pathname checks—let matcher define scope, and always redirect on missing token within middleware.

src/app/globals.css (1)

73-77: Applying border to all elements is likely unintended; use border-border instead.

@apply border; on * adds a 1px border to every element. Typically only the border color token is applied globally.

Apply this change and consider setting body colors:

 @layer base {
   * {
-    @apply border;
+    @apply border-border;
   }
+  body {
+    @apply bg-background text-foreground;
+  }
 }
src/hooks/use-debounce.ts (1)

7-7: Fix debounce default to respect 0ms delays.

delay || 500 coerces 0 to 500. Use nullish coalescing.

-    const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
+    const timer = setTimeout(() => setDebouncedValue(value), delay ?? 500);
src/components/models/projects/InviteMemberDialog.tsx (1)

20-20: Critical: Incorrect type usage - Zod schema used as TypeScript type.

inviteMemberSchema is a Zod schema object and cannot be used directly as a TypeScript type parameter. According to the PR objectives, an InviteMemberInput type alias should be exported from @/actions/collaboration.

Apply this diff to use the correct type:

-  const form = useForm<inviteMemberSchema>({
+  const form = useForm<InviteMemberInput>({

Alternatively, if InviteMemberInput is not exported, use:

-  const form = useForm<inviteMemberSchema>({
+  const form = useForm<z.infer<typeof inviteMemberSchema>>({

And add the import:

+import { z } from 'zod';
README.md (1)

1-148: Mismatch between PR description and changes
PR description claims inviteMemberSchema in src/actions/collaboration.ts was updated (added required email field and exported schema plus inferred type), but that file is unchanged in this PR. Include those edits or update the description to match the actual changes.

src/components/models/projects/CreateProjectForm.tsx (1)

33-35: Wrap setValue in useEffect to prevent unnecessary re-renders.

Calling form.setValue unconditionally on every render will mark the form as dirty on each render cycle and may trigger unnecessary re-renders. This should be wrapped in a useEffect with selectedTemplateId as a dependency.

Apply this diff to fix the issue:

+import { useEffect } from 'react';
+
 export function CreateProjectForm({ selectedTemplateId, setDialogOpen }: CreateProjectFormProps) {
   const form = useForm<CreateProjectFormValues>({
     mode: 'onChange',
     resolver: zodResolver(CreateProjectSchema),
     reValidateMode: 'onChange',
     defaultValues: {
       title: '',
       type: ProjectType.article,
       templateId: selectedTemplateId
     }
   });
 
-  // Update default value for templateId when selectedTemplateId changes
-  // This ensures the form reflects the currently selected template in the dialog
-  form.setValue('templateId', selectedTemplateId);
+  // Update templateId when selectedTemplateId changes
+  useEffect(() => {
+    form.setValue('templateId', selectedTemplateId);
+  }, [form, selectedTemplateId]);
src/components/common/Filter/FilterDropdown.tsx (1)

29-37: State synchronization gap: URL changes won't update local state.

The useEffect syncs selectedValue to URL params, but doesn't sync URL changes back to selectedValue. If the user navigates using browser back/forward buttons, the URL will update but selectedValue will remain stale, causing the dropdown to display an incorrect value.

Consider updating the effect to sync bidirectionally or removing searchParams from the dependency array:

  useEffect(() => {
+   // Sync URL params to local state
+   const currentValue = searchParams.get(paramName)?.toString() || '';
+   if (currentValue !== selectedValue) {
+     setSelectedValue(currentValue);
+     return;
+   }
+   
+   // Sync local state to URL params
    const params = new URLSearchParams(searchParams);
    if (selectedValue) {
      params.set(paramName, selectedValue);
    } else {
      params.delete(paramName);
    }
    replace(`${pathname}?${params.toString()}`);
  }, [selectedValue, paramName, pathname, replace, searchParams]);
♻️ Duplicate comments (1)
src/components/models/auth/SignupForm.tsx (1)

25-26: Password helper text vs schema. Helper says 8 chars; schema (current main) enforces 6. See my auth.ts comment proposing min(8) to align.

Also applies to: 81-85

🧹 Nitpick comments (20)
src/actions/projects/deleteProject/schema.ts (1)

3-5: Tighten input validation and disallow unknown fields.

Current id: z.string() accepts empty strings and the object allows extraneous keys.

Consider:

-export const deleteProjectSchema = z.object({
-  id: z.string()
-});
+export const deleteProjectSchema = z
+  .object({
+    // If IDs are UUIDs, prefer `.uuid()`; otherwise at least trim + min length:
+    // id: z.string().uuid()
+    id: z.string().trim().min(1, 'id is required'),
+  })
+  .strict();

If your IDs are UUIDs/CUIDs, swap .trim().min(1) for .uuid() or .cuid2(). Please confirm the expected ID format to choose the correct validator.

src/lib/jwt.ts (1)

7-15: Dev stub looks fine; consider clearer prod guard/message.

No functional change; OK. Optionally, tighten the guard and clarify the error to avoid confusion about Edge vs non‑Edge runtimes.

Proposed tweak:

-  if (APP_ENV === 'production') {
-    throw new Error('verifyJWT should not be called in production in an Edge environment');
-  }
+  if (APP_ENV === 'production') {
+    throw new Error('verifyJWT is a dev-only stub and must never run in production');
+  }

Also verify APP_ENV reliably equals 'production' across all deploy targets so this stub cannot execute in prod by misconfiguration.

src/components/layouts/Header.tsx (1)

44-49: Avoid nesting interactive elements (Link wrapping Button).
Next.js Link typically renders an anchor, resulting in . This is invalid HTML and hurts accessibility/keyboard/screen-reader behavior. Prefer rendering the Button as the anchor via asChild.

Apply this diff:

-        <Link href='/profile'>
-          <Button variant='ghost' size='sm' className='flex items-center gap-2'>
-            <User size={16} />
-            Profile
-          </Button>
-        </Link>
+        <Button asChild variant='ghost' size='sm' className='flex items-center gap-2'>
+          <Link href='/profile'>
+            <User size={16} />
+            Profile
+          </Link>
+        </Button>

src/contexts/LoadingContext.tsx (1)

13-14: LGTM on formatting; consider memoizing context value.

Current change is fine. Optionally memoize the value to avoid recreating the object on unrelated rerenders.

Apply within this block:

 export function LoadingProvider({ children }: { children: ReactNode }) {
   const [isLoading, setLoading] = useState(false);

-  return <LoadingContext.Provider value={{ isLoading, setLoading }}>{children}</LoadingContext.Provider>;
+  const value = React.useMemo(() => ({ isLoading, setLoading }), [isLoading]);
+  return <LoadingContext.Provider value={value}>{children}</LoadingContext.Provider>;
 }

Also update imports:

import React, { createContext, useContext, useMemo, useState, ReactNode } from 'react';
CODE_OF_CONDUCT.md (1)

55-56: Unify “email” spelling

Use a single form across the doc (prefer “email” over “e‑mail”) for consistency.

-Examples of representing our community include using an official e-mail address,
+Examples of representing our community include using an official email address,
src/components/common/Search/SearchInput.tsx (2)

20-28: Guard replace and avoid trailing “?”; depend on stable query string

Prevent redundant navigations and clean URLs. Compare current vs next query, and omit “?” when empty. Also depend on searchParams.toString() to avoid identity churn.

   useEffect(() => {
-    const params = new URLSearchParams(searchParams);
-    if (debouncedSearchValue) {
-      params.set('search', debouncedSearchValue);
-    } else {
-      params.delete('search');
-    }
-    replace(`${pathname}?${params.toString()}`);
-  }, [debouncedSearchValue, pathname, replace, searchParams]);
+    const nextParams = new URLSearchParams(searchParams);
+    if (debouncedSearchValue) {
+      nextParams.set('search', debouncedSearchValue);
+    } else {
+      nextParams.delete('search');
+    }
+    const next = nextParams.toString();
+    const current = searchParams.toString();
+    if (next !== current) {
+      replace(next ? `${pathname}?${next}` : pathname);
+    }
+  }, [debouncedSearchValue, pathname, replace, searchParams.toString()]);

Optional (keeps input in sync when navigating via back/forward or external links):

useEffect(() => {
  const fromUrl = searchParams.get('search') ?? '';
  if (fromUrl !== searchValue) setSearchValue(fromUrl);
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams.toString()]);

17-17: Simplify initial state; avoid unnecessary toString

Use nullish coalescing; get() already returns string | null.

-  const [searchValue, setSearchValue] = useState(searchParams.get('search')?.toString() || '');
+  const [searchValue, setSearchValue] = useState(searchParams.get('search') ?? '');
src/hooks/use-mobile.ts (3)

3-3: Deduplicate MOBILE_BREAKPOINT: import shared constant from src/lib/responsive

Avoid drift by reusing the exported constant.

-import * as React from 'react';
+import * as React from 'react';
+import { MOBILE_BREAKPOINT } from '../lib/responsive';
-
-const MOBILE_BREAKPOINT = 768;

Based on relevant code snippet (src/lib/responsive.ts).


9-15: Use mql.matches and add Safari fallback for MediaQueryList listeners

Align state updates with the media query and support older Safari.

-  const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
-  const onChange = () => {
-    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
-  };
-  mql.addEventListener('change', onChange);
-  setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
-  return () => mql.removeEventListener('change', onChange);
+  const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
+  const onChange = (e: MediaQueryListEvent) => setIsMobile(e.matches);
+  // Safari <14 fallback
+  if (typeof mql.addEventListener === 'function') {
+    mql.addEventListener('change', onChange);
+  } else {
+    // @ts-expect-error: older Safari support
+    mql.addListener(onChange);
+  }
+  setIsMobile(mql.matches);
+  return () => {
+    if (typeof mql.removeEventListener === 'function') {
+      mql.removeEventListener('change', onChange);
+    } else {
+      // @ts-expect-error: older Safari support
+      mql.removeListener(onChange);
+    }
+  };

6-6: Simplify state typing and return

No need for boolean | undefined + double-bang; initialize false and return directly.

-  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
+  const [isMobile, setIsMobile] = React.useState(false);
...
-  return !!isMobile;
+  return isMobile;

Also applies to: 18-18

src/lib/constants.ts (1)

3-3: Consider secure cookie naming and centralization.

If you set Secure, Path=/, and no Domain, adopt the “__Host-” prefix for stronger cookie scoping; also consider centralizing cookie name + options in one export.

src/components/models/user/ProfileForm.tsx (1)

41-43: Accessibility: add aria-invalid/aria-describedby and announce errors

Improve screen reader support by linking inputs to error text and marking invalid state.

-        <Input id='name' {...form.register('name')} />
-        {form.formState.errors.name && <p className='text-sm text-red-500'>{form.formState.errors.name.message}</p>}
+        <Input
+          id='name'
+          aria-invalid={!!form.formState.errors.name}
+          aria-describedby={form.formState.errors.name ? 'name-error' : undefined}
+          {...form.register('name')}
+        />
+        {form.formState.errors.name && (
+          <p id='name-error' role='alert' className='text-sm text-red-500'>
+            {form.formState.errors.name.message}
+          </p>
+        )}
...
-        <Input id='email' type='email' {...form.register('email')} disabled />
-        {form.formState.errors.email && <p className='text-sm text-red-500'>{form.formState.errors.email.message}</p>}
+        <Input
+          id='email'
+          type='email'
+          aria-invalid={!!form.formState.errors.email}
+          aria-describedby={form.formState.errors.email ? 'email-error' : undefined}
+          {...form.register('email')}
+          /* readOnly or disabled per chosen approach above */
+        />
+        {form.formState.errors.email && (
+          <p id='email-error' role='alert' className='text-sm text-red-500'>
+            {form.formState.errors.email.message}
+          </p>
+        )}

Also applies to: 46-47

src/actions/projects/createProject/logic.ts (1)

19-20: Prefer nullish coalescing for default content.

Use ?? so empty strings are preserved intentionally and only null/undefined default to ''.

Apply:

-      content: content || ''
+      content: content ?? ''
src/components/pages/home/HomePageContainer.tsx (1)

26-26: Optional: surface the error to the user and use structured logging.

Consider a toast/alert for UX and routing errors to a logger service instead of console.error.

src/actions/user.ts (2)

27-27: Await revalidatePath for deterministic cache invalidation.

-  revalidatePath('/profile');
+  await revalidatePath('/profile');

Also applies to: 64-64


44-45: Limit fetched fields to what you need (passwordHash) to reduce exposure.

-  const user = await prisma.user.findUnique({
-    where: { id: userId }
-  });
+  const user = await prisma.user.findUnique({
+    where: { id: userId },
+    select: { passwordHash: true }
+  });
postcss.config.mjs (1)

4-5: Remove redundant autoprefixer entry from postcss.config.mjs
Tailwind v4’s @tailwindcss/postcss uses Lightning CSS for vendor prefixing, so explicit autoprefixer can be omitted unless you require custom browser targets.

src/actions/collaboration.ts (3)

8-8: Normalize email; nonempty() is redundant

Recommend trimming and lowercasing to avoid duplicate users due to casing/whitespace; .email() already rejects empty strings.

-  email: z.string().email().nonempty(),
+  email: z.string().trim().toLowerCase().email(),

17-17: Fix lint: unused ctx.user parameter

Destructured user is unused; rename to _user to satisfy lint rule.

-  async ({ projectId, email, role }, { user }) => {
+  async ({ projectId, email, role }, { user: _user }) => {
@@
-  async ({ projectId, memberId }, { user }) => {
+  async ({ projectId, memberId }, { user: _user }) => {
@@
-  async ({ projectId, memberId, role }, { user }) => {
+  async ({ projectId, memberId, role }, { user: _user }) => {
@@
-  async ({ projectId }, { user }) => {
+  async ({ projectId }, { user: _user }) => {

Also applies to: 46-46, 69-69, 93-93


26-34: Guard against duplicate membership (idempotent invite)

Creating without checking may throw on unique (projectId,userId). Check first and no-op if already a member.

-    await prisma.projectMember.create({
-      data: {
-        projectId,
-        userId: existingUser.id,
-        role
-      }
-    });
+    const existingMember = await prisma.projectMember.findUnique({
+      where: { projectId_userId: { projectId, userId: existingUser.id } }
+    });
+    if (!existingMember) {
+      await prisma.projectMember.create({
+        data: { projectId, userId: existingUser.id, role }
+      });
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b0fce47 and 9ac8b2c.

📒 Files selected for processing (68)
  • .prettierrc (1 hunks)
  • CODE_OF_CONDUCT.md (2 hunks)
  • README.md (2 hunks)
  • eslint.config.mjs (1 hunks)
  • postcss.config.mjs (1 hunks)
  • src/actions/analytics.ts (1 hunks)
  • src/actions/collaboration.ts (1 hunks)
  • src/actions/projects/createProject/action.ts (1 hunks)
  • src/actions/projects/createProject/logic.ts (1 hunks)
  • src/actions/projects/createProject/schema.ts (1 hunks)
  • src/actions/projects/deleteProject/schema.ts (1 hunks)
  • src/actions/projects/getProjects.ts (1 hunks)
  • src/actions/user.ts (1 hunks)
  • src/app/(general)/page.tsx (1 hunks)
  • src/app/globals.css (1 hunks)
  • src/app/profile/page.tsx (1 hunks)
  • src/components/common/ErrorBoundary.tsx (1 hunks)
  • src/components/common/Filter/FilterDropdown.tsx (3 hunks)
  • src/components/common/Search/SearchInput.tsx (3 hunks)
  • src/components/common/Skeleton/FormSkeleton.tsx (1 hunks)
  • src/components/common/Skeleton/TableSkeleton.tsx (3 hunks)
  • src/components/layouts/Header.tsx (2 hunks)
  • src/components/models/analytics/ProjectStats.tsx (1 hunks)
  • src/components/models/auth/SigninForm.tsx (2 hunks)
  • src/components/models/auth/SignupForm.tsx (2 hunks)
  • src/components/models/projects/CreateProjectDialog.tsx (1 hunks)
  • src/components/models/projects/CreateProjectForm.tsx (4 hunks)
  • src/components/models/projects/DeleteProjectDialog.tsx (3 hunks)
  • src/components/models/projects/ExportDialog.tsx (3 hunks)
  • src/components/models/projects/InviteMemberDialog.tsx (3 hunks)
  • src/components/models/projects/ProjectMembersTable.tsx (4 hunks)
  • src/components/models/projects/ProjectsTable.tsx (2 hunks)
  • src/components/models/templates/TemplateCard.tsx (1 hunks)
  • src/components/models/templates/TemplateSelector.tsx (1 hunks)
  • src/components/models/user/ChangePasswordForm.tsx (3 hunks)
  • src/components/models/user/ProfileForm.tsx (2 hunks)
  • src/components/pages/home/HomePageContainer.tsx (2 hunks)
  • src/components/ui/alert.tsx (1 hunks)
  • src/components/ui/card.tsx (1 hunks)
  • src/components/ui/checkbox.tsx (1 hunks)
  • src/components/ui/dialog.tsx (3 hunks)
  • src/components/ui/dropdown-menu.tsx (7 hunks)
  • src/components/ui/form.tsx (1 hunks)
  • src/components/ui/input.tsx (1 hunks)
  • src/components/ui/label.tsx (1 hunks)
  • src/components/ui/select.tsx (5 hunks)
  • src/components/ui/separator.tsx (1 hunks)
  • src/components/ui/sheet.tsx (1 hunks)
  • src/components/ui/sidebar.tsx (5 hunks)
  • src/components/ui/skeleton.tsx (1 hunks)
  • src/components/ui/table.tsx (1 hunks)
  • src/components/ui/tabs.tsx (1 hunks)
  • src/components/ui/tooltip.tsx (2 hunks)
  • src/contexts/LoadingContext.tsx (1 hunks)
  • src/hooks/use-debounce.ts (1 hunks)
  • src/hooks/use-mobile.ts (1 hunks)
  • src/lib/action.ts (4 hunks)
  • src/lib/constants.ts (1 hunks)
  • src/lib/export.ts (1 hunks)
  • src/lib/jwt.ts (1 hunks)
  • src/lib/permissions.ts (1 hunks)
  • src/lib/prisma.ts (1 hunks)
  • src/lib/responsive.ts (0 hunks)
  • src/lib/utils.ts (1 hunks)
  • src/lib/validations/auth.ts (1 hunks)
  • src/lib/validations/projects.ts (1 hunks)
  • src/middleware.ts (1 hunks)
  • tailwind.config.js (1 hunks)
💤 Files with no reviewable changes (1)
  • src/lib/responsive.ts
🧰 Additional context used
🧬 Code graph analysis (36)
src/components/models/projects/ProjectsTable.tsx (2)
src/components/common/Search/SearchInput.tsx (1)
  • SearchInput (12-38)
src/components/common/Filter/FilterDropdown.tsx (1)
  • FilterDropdown (22-60)
src/actions/projects/createProject/action.ts (1)
src/lib/templates.ts (1)
  • projectTemplates (8-27)
src/components/ui/skeleton.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
src/components/ui/dialog.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
src/hooks/use-mobile.ts (1)
src/lib/responsive.ts (1)
  • MOBILE_BREAKPOINT (1-1)
src/actions/projects/createProject/logic.ts (3)
src/actions/projects/createProject/schema.ts (1)
  • CreateProjectInput (12-12)
src/lib/result.ts (1)
  • Result (18-18)
src/lib/prisma.ts (1)
  • prisma (7-7)
src/lib/constants.ts (1)
src/lib/env.ts (2)
  • IS_PRODUCTION (12-12)
  • APP_ENV (12-12)
src/components/ui/separator.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
src/components/common/ErrorBoundary.tsx (1)
src/components/common/BasicAlert.tsx (1)
  • BasicAlert (5-21)
src/actions/projects/getProjects.ts (2)
src/lib/result.ts (1)
  • Result (18-18)
src/lib/prisma.ts (1)
  • prisma (7-7)
src/components/models/projects/CreateProjectDialog.tsx (1)
src/components/models/templates/TemplateSelector.tsx (1)
  • TemplateSelector (9-22)
src/components/ui/label.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
src/components/models/auth/SigninForm.tsx (1)
src/lib/result.ts (1)
  • error (38-44)
src/actions/analytics.ts (2)
src/lib/action.ts (1)
  • protectedAction (67-98)
src/lib/prisma.ts (1)
  • prisma (7-7)
src/components/ui/tabs.tsx (2)
src/components/common/Tabs.tsx (1)
  • Tabs (18-54)
src/lib/utils.ts (1)
  • cn (4-6)
src/components/common/Skeleton/FormSkeleton.tsx (1)
src/components/ui/skeleton.tsx (1)
  • Skeleton (7-7)
src/components/ui/alert.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
src/components/ui/sheet.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
src/app/(general)/page.tsx (5)
src/lib/session.ts (1)
  • getSession (22-25)
src/lib/async.ts (1)
  • safeAwait (3-10)
src/actions/projects/getProjects.ts (1)
  • getProjects (11-26)
src/actions/analytics.ts (1)
  • getProjectAnalytics (7-42)
src/components/common/BasicAlert.tsx (1)
  • BasicAlert (5-21)
src/components/common/Search/SearchInput.tsx (1)
src/hooks/use-debounce.ts (1)
  • useDebounce (3-15)
src/actions/collaboration.ts (2)
src/lib/action.ts (1)
  • protectedAction (67-98)
src/lib/prisma.ts (1)
  • prisma (7-7)
src/components/ui/card.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
src/lib/export.ts (1)
src/actions/projects/getProjects.ts (1)
  • getProjects (11-26)
src/components/ui/sidebar.tsx (2)
src/hooks/use-mobile.ts (1)
  • useIsMobile (5-19)
src/lib/utils.ts (1)
  • cn (4-6)
src/components/ui/tooltip.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
src/components/ui/input.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
src/components/models/projects/DeleteProjectDialog.tsx (2)
src/components/ui/dialog.tsx (3)
  • DialogDescription (116-116)
  • DialogHeader (118-118)
  • DialogFooter (117-117)
src/components/ui/button.tsx (1)
  • Button (56-56)
src/components/models/projects/InviteMemberDialog.tsx (2)
src/components/common/Form/FormInput.tsx (1)
  • FormInput (6-72)
src/components/common/Form/FormSelect.tsx (1)
  • FormSelect (20-56)
src/actions/user.ts (2)
src/lib/action.ts (2)
  • authActionClient (33-65)
  • getAuthenticatedUserId (100-106)
src/lib/prisma.ts (1)
  • prisma (7-7)
src/components/ui/table.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
src/components/ui/select.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
src/components/ui/form.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
src/components/ui/checkbox.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
src/lib/action.ts (1)
src/lib/session.ts (1)
  • getSession (22-25)
src/components/models/user/ChangePasswordForm.tsx (1)
src/components/common/BasicAlert.tsx (1)
  • BasicAlert (5-21)
src/components/ui/dropdown-menu.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
🪛 GitHub Actions: Lint and Build
src/contexts/LoadingContext.tsx

[error] 1-1: React useState (state hook) used in a server component. Mark the component tree as Client Component with 'use client' or refactor to avoid client-side hooks in server components.

src/components/models/user/ChangePasswordForm.tsx

[error] 10-10: Client components cannot use React useState. The component must be a Client Component (marked with 'use client').

🪛 GitHub Check: lint-and-build
src/components/models/projects/ProjectMembersTable.tsx

[warning] 1-1:
'useState' is defined but never used

src/app/(general)/page.tsx

[warning] 20-20:
'analyticsResult' is assigned a value but never used


[warning] 20-20:
'analyticsError' is assigned a value but never used


[warning] 6-6:
'ProjectStats' is defined but never used


[warning] 5-5:
'ExportDialog' is defined but never used

src/actions/collaboration.ts

[warning] 93-93:
'user' is defined but never used. Allowed unused args must match /^_/u


[warning] 69-69:
'user' is defined but never used. Allowed unused args must match /^_/u


[warning] 46-46:
'user' is defined but never used. Allowed unused args must match /^_/u


[warning] 17-17:
'user' is defined but never used. Allowed unused args must match /^_/u

🪛 LanguageTool
CODE_OF_CONDUCT.md

[grammar] ~20-~20: There might be a mistake here.
Context: ...empathy and kindness toward other people - Being respectful of differing opinions, ...

(QB_NEW_EN)


[grammar] ~21-~21: There might be a mistake here.
Context: ...ng opinions, viewpoints, and experiences - Giving and gracefully accepting construc...

(QB_NEW_EN)


[grammar] ~22-~22: There might be a mistake here.
Context: ...acefully accepting constructive feedback - Accepting responsibility and apologizing...

(QB_NEW_EN)


[grammar] ~24-~24: There might be a mistake here.
Context: ...akes, and learning from the experience - Focusing on what is best not just for us...

(QB_NEW_EN)


[grammar] ~32-~32: There might be a mistake here.
Context: ...ion or advances of any kind - Trolling, insulting or derogatory comments, and p...

(QB_NEW_EN)


[style] ~32-~32: Try using a synonym here to strengthen your wording.
Context: ...ind - Trolling, insulting or derogatory comments, and personal or political attacks - Pu...

(COMMENT_REMARK)


[grammar] ~109-~109: There might be a mistake here.
Context: ...inappropriate behavior, harassment of an individual, or aggression toward or disp...

(QB_NEW_EN)

🔇 Additional comments (54)
src/components/models/templates/TemplateSelector.tsx (1)

11-11: LGTM: formatting-only change

Quote normalization in JSX; no semantic impact. Ensure ESLint/Prettier config enforces this to avoid churn.

src/lib/permissions.ts (1)

1-1: LGTM — import style normalization only.

No functional change; safe to merge.

src/actions/projects/deleteProject/schema.ts (1)

1-1: LGTM on import style.

Consistent single quotes; no behavior change.

src/components/layouts/Header.tsx (1)

28-29: No behavior change; formatting is fine.
The multi-line formatting of errorMessage is equivalent to previous logic. LGTM.

src/components/ui/sidebar.tsx (1)

1-677: LGTM! Formatting standardization applied correctly.

The formatting changes across this file are consistent and correct. All modifications are style-related (double→single quotes, semicolons, explicit type literals) with no functional impact.

src/components/models/templates/TemplateCard.tsx (1)

27-27: Style-only change looks good.

Consistent single quotes; no behavioral impact.

CODE_OF_CONDUCT.md (4)

20-26: Formatting-only changes look good

Bullet wrap/punctuation adjustments preserve meaning.


30-37: LGTM on standards section tweaks

Line-wrapping only; no semantic changes.


108-110: LGTM on permanent ban paragraph

Pure reflow; content unchanged.


117-128: All external links resolve with HTTP 200 Verified each URL returns 200 after following redirects; no dead links detected.

src/components/common/Search/SearchInput.tsx (1)

1-1: LGTM on formatting updates

Imports, directive, and className quoting changes look good.

Also applies to: 3-6, 35-35

src/app/globals.css (1)

1-2: Import statements and Tailwind configuration verified
Project uses tailwindcss v4.1.14 and tw-animate-css v1.3.8; no legacy @tailwind directives found.

src/lib/utils.ts (1)

1-2: LGTM — style-only; no behavior change.

Also applies to: 5-5

src/components/models/projects/CreateProjectDialog.tsx (1)

20-25: Verified: styling changes and schema/type exports all confirmed.

The CreateProjectDialog styling changes are good with no behavioral impact. The PR objective is also complete: inviteMemberSchema is properly defined in src/actions/collaboration.ts (line 6–10) with email validation (z.string().email().nonempty()), and InviteMemberInput type is correctly exported (line 12) and in use across the codebase.

src/actions/projects/createProject/action.ts (1)

19-21: LGTM – formatting only. No functional change; keep as-is.

.prettierrc (1)

10-10: LGTM – config formatting. No semantic changes.

src/lib/prisma.ts (1)

7-9: LGTM – singleton pattern intact. Matches common Next.js Prisma usage; no behavior change.

src/components/common/Skeleton/TableSkeleton.tsx (1)

16-16: LGTM – formatting only. No runtime changes.

Also applies to: 26-26

src/lib/validations/projects.ts (1)

7-7: LGTM – schema unchanged. Formatting only.

Also applies to: 10-10

src/components/models/projects/ProjectsTable.tsx (1)

22-26: LGTM — formatting-only changes.

Also applies to: 27-32, 53-58, 62-62

src/components/common/ErrorBoundary.tsx (1)

35-38: LGTM! Formatting changes only.

The quote normalization aligns with the project-wide style standardization. No functional changes detected.

src/components/ui/tabs.tsx (1)

1-42: LGTM! Formatting standardization only.

Quote normalization and semicolon additions with no functional changes. Aligns with project-wide style updates.

src/components/models/projects/DeleteProjectDialog.tsx (1)

1-67: LGTM! Formatting changes only.

Quote standardization and minor text formatting. No behavioral changes detected.

tailwind.config.js (1)

3-7: LGTM! Configuration formatting update.

Array formatting simplified with no semantic changes to Tailwind configuration.

src/components/ui/sheet.tsx (1)

1-103: LGTM! Formatting standardization only.

Single-quote normalization and data-slot attribute updates with no functional changes. Component API remains unchanged.

eslint.config.mjs (1)

1-61: LGTM! Configuration formatting update.

Quote standardization with no changes to ESLint rules or configuration behavior.

src/components/ui/checkbox.tsx (1)

1-29: LGTM! Formatting standardization only.

Quote normalization and semicolon additions with no functional changes. Component API unchanged.

src/components/models/projects/InviteMemberDialog.tsx (1)

24-26: Confirm inviteMemberSchema includes email and exports InviteMemberInput
The schema in src/actions/collaboration.ts defines email: z.string().email().nonempty() and exports InviteMemberInput.

src/app/profile/page.tsx (1)

7-16: LGTM: formatting-only change

src/actions/collaboration.ts (1)

94-99: PII exposure check: returning member emails

Confirm that exposing each member’s email to all project members is intended and compliant. If not, drop email from selection or gate behind ADMIN.

src/components/ui/label.tsx (1)

11-14: LGTM: formatting-only; public API unchanged

src/components/common/Skeleton/FormSkeleton.tsx (1)

9-16: LGTM: formatting-only change

src/components/ui/separator.tsx (1)

1-28: LGTM: Consistent formatting updates.

The changes apply consistent quote style and semicolon usage without altering functionality.

src/components/ui/dialog.tsx (1)

1-123: LGTM: Formatting standardization applied consistently.

All changes are stylistic (quote normalization, semicolons) with no impact on component behavior or public API.

src/components/ui/input.tsx (1)

1-21: LGTM: Formatting updates only.

The quote style and type literal changes maintain identical semantics.

src/components/models/user/ChangePasswordForm.tsx (1)

22-110: LGTM: Formatting standardization applied.

All changes are quote style normalization and minor formatting adjustments without altering the component's logic or behavior.

src/components/ui/table.tsx (1)

1-75: LGTM: Consistent formatting updates.

The changes normalize quote styles and data-slot attributes without affecting component functionality.

src/components/models/analytics/ProjectStats.tsx (1)

1-72: LGTM: Formatting updates maintain functionality.

All changes are stylistic (quote normalization, attribute formatting) with no impact on the chart rendering or component behavior.

src/components/ui/tooltip.tsx (1)

1-48: LGTM: Formatting standardization.

The changes apply consistent quote style and semicolons without altering component behavior.

README.md (1)

36-66: LGTM: Documentation improvements enhance clarity.

The added code blocks make the installation steps more actionable and easier to follow.

src/components/models/projects/CreateProjectForm.tsx (2)

46-46: LGTM!

The error message construction is correctly simplified to a single-line expression while preserving the same fallback chain.


57-57: LGTM!

Formatting changes align with the project-wide style standardization.

Also applies to: 80-80

src/components/ui/skeleton.tsx (1)

1-7: LGTM!

Formatting changes align with the project-wide style standardization (single quotes, consistent import/export formatting).

src/components/models/projects/ProjectMembersTable.tsx (1)

2-16: LGTM!

Formatting changes (quote normalization, import restructuring) align with the project-wide style standardization. The queryKey array update and all other changes are purely stylistic with no behavioral impact.

Also applies to: 33-103, 106-106

src/actions/projects/getProjects.ts (1)

11-24: LGTM!

Formatting changes align with the project-wide style standardization (single quotes, function signature formatting).

src/components/ui/select.tsx (1)

1-160: LGTM!

Formatting changes align with the project-wide style standardization (single quotes for string literals, data-slot attributes, and type annotations).

src/components/ui/form.tsx (1)

1-138: LGTM!

Formatting changes align with the project-wide style standardization (single quotes for imports, string literals, data-slot attributes, and type annotations). The signature change for FormItem from React.ComponentProps<"div"> to React.ComponentProps<'div'> is purely stylistic.

src/components/ui/card.tsx (1)

1-56: LGTM!

Formatting changes align with the project-wide style standardization (single quotes for imports, data-slot attributes, and type annotations). All Card component signatures are consistently updated.

src/components/models/auth/SigninForm.tsx (1)

35-35: LGTM!

Formatting changes align with the project-wide style standardization. The error message construction is correctly simplified to a single-line expression, and the button formatting is consistently updated.

Also applies to: 63-63

src/components/ui/alert.tsx (1)

1-48: LGTM! Formatting standardization applied correctly.

All changes in this file are purely stylistic—converting double quotes to single quotes and adding trailing semicolons. The component logic and public API remain unchanged.

src/lib/export.ts (1)

1-29: LGTM! Formatting and type signature standardization.

The changes maintain functional equivalence while applying single-quote conventions consistently. The export logic remains correct.

src/components/models/projects/ExportDialog.tsx (1)

24-36: Excellent refactor: export logic properly encapsulated.

The new handleExport function cleanly separates async export logic from the component's render flow, with proper loading states and error handling via toast notifications.

src/components/ui/dropdown-menu.tsx (2)

150-162: Well-structured component additions and refactoring.

The internal renaming to DropMenuSeparator with an export alias maintains backwards compatibility while avoiding naming conflicts. The new DropdownMenuShortcut component follows established patterns for displaying keyboard shortcuts in menus.


76-76: Consistent API enhancement: inset prop added.

The inset prop on DropdownMenuItem and DropdownMenuLabel follows the existing pattern established in DropdownMenuSubTrigger, providing a uniform API for indented menu items.

Also applies to: 139-139

Comment on lines +18 to 25
by: ['createdAt'],
_count: {
createdAt: true,
createdAt: true
},
orderBy: {
createdAt: "asc",
},
createdAt: 'asc'
}
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Monthly aggregation is incorrect — grouping by createdAt produces per-row buckets.

You need month-level aggregation. Two options:

Option A (Postgres, preferred — true month grouping in DB):

-  const projectsByCreationMonth = await prisma.project.groupBy({
-    by: ['createdAt'],
-    _count: {
-      createdAt: true
-    },
-    orderBy: {
-      createdAt: 'asc'
-    }
-  });
+  const projectsByCreationMonth = await prisma.$queryRaw<
+    { month: Date; count: bigint }[]
+  >`SELECT date_trunc('month', "createdAt") AS month, COUNT(*)::bigint AS count
+    FROM "Project"
+    GROUP BY 1
+    ORDER BY 1 ASC`;
-  const formattedProjectsByCreationMonth = projectsByCreationMonth.map((item) => ({
-    name: new Date(item.createdAt).toLocaleString('default', { month: 'short', year: 'numeric' }),
-    value: item._count.createdAt
-  }));
+  const formattedProjectsByCreationMonth = projectsByCreationMonth.map((row) => ({
+    name: new Date(row.month).toLocaleString('en', { month: 'short', year: 'numeric' }),
+    value: Number(row.count)
+  }));

Option B (DB-agnostic — reduce in JS):

const dates = await prisma.project.findMany({ select: { createdAt: true } });
const monthMap = new Map<string, number>();
for (const { createdAt } of dates) {
  const k = new Date(createdAt);
  const key = `${k.getUTCFullYear()}-${k.getUTCMonth()}`; // YYYY-M
  monthMap.set(key, (monthMap.get(key) ?? 0) + 1);
}
const formattedProjectsByCreationMonth = Array.from(monthMap.entries())
  .sort(([a], [b]) => a.localeCompare(b))
  .map(([key, count]) => {
    const [y, m] = key.split('-').map(Number);
    const d = new Date(Date.UTC(y, m, 1));
    return { name: d.toLocaleString('en', { month: 'short', year: 'numeric' }), value: count };
  });

Also applies to: 32-35

🤖 Prompt for AI Agents
In src/actions/analytics.ts around lines 18-25 (and also apply the same fix at
lines 32-35), the current grouping by createdAt yields per-row buckets; replace
this with month-level aggregation: for Postgres, change the query to group by a
date_trunc('month', createdAt) selection and order by that truncated month so
the DB returns counts per month (preferred); if DB-agnostic, fetch only
createdAt values and reduce in JS into a month-keyed map, then convert and sort
into the formatted month/count array as shown in the reviewer comment. Ensure
the result uses the month start (UTC) for keys and sorts chronologically before
formatting.

Comment on lines +5 to 8
title: z.string().trim().min(1, { message: 'Title is required' }).max(200, { message: 'Title is too long' }),
type: z.enum(ProjectType, {
message: "Invalid project type",
message: 'Invalid project type'
}),
Copy link

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

Verify enum type: use z.nativeEnum if ProjectType is a TS enum

If ProjectType is a TS enum (as with Prisma), z.enum(ProjectType) will be incorrect; use z.nativeEnum(ProjectType).

Run to confirm the ProjectType shape:


🏁 Script executed:

#!/bin/bash
# Find where ProjectType is declared to verify if it's a TS enum or a const tuple
rg -nC2 --type=ts '\b(ProjectType)\b' src | sed -n '1,120p'
rg -nC2 'enum\s+ProjectType\b|export\s+const\s+ProjectType\s*=\s*\['

Length of output: 2924


Change z.enum(ProjectType) to z.nativeEnum(ProjectType) at line 6

ProjectType is a Prisma-generated enum, not a string tuple. The correct Zod validator is z.nativeEnum(ProjectType), matching the pattern already used in src/lib/validations/projects.ts line 6. Replace:

  type: z.enum(ProjectType, {
    message: 'Invalid project type'
  }),

with:

  type: z.nativeEnum(ProjectType, {
    message: 'Invalid project type'
  }),
🤖 Prompt for AI Agents
In src/actions/projects/createProject/schema.ts around lines 5 to 8, the schema
uses z.enum(ProjectType) but ProjectType is a Prisma-generated enum (not a
string tuple); replace z.enum(ProjectType, { message: 'Invalid project type' })
with z.nativeEnum(ProjectType, { message: 'Invalid project type' }) so Zod
validates the native TypeScript/Prisma enum correctly.

Comment on lines +1 to +9
import { getProjects } from '@/actions/projects/getProjects';
import { getProjectAnalytics } from '@/actions/analytics';
import { BasicAlert } from '@/components/common/BasicAlert';
import { HomePageContainer } from '@/components/pages/home/HomePageContainer';
import { ExportDialog } from '@/components/models/projects/ExportDialog';
import { ProjectStats } from '@/components/models/analytics/ProjectStats';
import { safeAwait } from '@/lib/async';
import { getSession } from '@/lib/session';
import { redirect } from 'next/navigation';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix getProjects call and undefined variable; remove unused analytics/components. Current code calls getProjects(userId) (expects object) and later uses result.data (undefined). Also unused imports/vars trigger lint.

Apply:

-import { ExportDialog } from '@/components/models/projects/ExportDialog';
-import { ProjectStats } from '@/components/models/analytics/ProjectStats';
-import { getProjectAnalytics } from '@/actions/analytics';
+// removed unused imports

@@
-  const [projectsError, projectsResult] = await safeAwait(getProjects(userId));
-  const [analyticsError, analyticsResult] = await safeAwait(getProjectAnalytics());
+  const [projectsError, projectsResult] = await safeAwait(getProjects({ userId }));
+  // removed unused analytics call

@@
-  if (projectsError) {
+  if (projectsError) {
     return <BasicAlert variant='destructive' title='Error loading projects' description={projectsError.message} />;
   }
@@
-  if (!projectsResult.success) {
+  if (!projectsResult.success) {
     return <BasicAlert variant='destructive' title='Error loading projects' description={projectsResult.error} />;
   }
@@
-  const initialProjects = result.data;
+  const initialProjects = projectsResult.data;

Also applies to: 23-28

🧰 Tools
🪛 GitHub Check: lint-and-build

[warning] 6-6:
'ProjectStats' is defined but never used


[warning] 5-5:
'ExportDialog' is defined but never used

🤖 Prompt for AI Agents
In src/app/(general)/page.tsx around lines 1-9 (and also applies to lines
23-28): the code calls getProjects(userId) but the function expects an object
and the code later uses result.data (which is undefined); additionally several
analytics/components and helper imports/vars are unused and causing lints. Fix
by calling getProjects with an object (e.g., getProjects({ userId })), assign
the returned value directly to the projects variable instead of accessing .data,
remove unused imports and any unused variables (getProjectAnalytics,
ProjectStats, ExportDialog, safeAwait, etc.), and update any related variable
names/usages to match the function's actual return shape so ESLint no longer
flags unused/undefined references.

}
});

const onSubmit = async (data: inviteMemberSchema) => {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Same type usage issue in function parameter.

Same issue as line 20 - inviteMemberSchema cannot be used as a TypeScript type.

Apply this diff:

-  const onSubmit = async (data: inviteMemberSchema) => {
+  const onSubmit = async (data: InviteMemberInput) => {

Or:

-  const onSubmit = async (data: inviteMemberSchema) => {
+  const onSubmit = async (data: z.infer<typeof inviteMemberSchema>) => {
📝 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 onSubmit = async (data: inviteMemberSchema) => {
const onSubmit = async (data: z.infer<typeof inviteMemberSchema>) => {
🤖 Prompt for AI Agents
In src/components/models/projects/InviteMemberDialog.tsx around line 29, the
onSubmit parameter is typed as inviteMemberSchema (a runtime Zod schema) which
is invalid as a TypeScript type; replace that usage with the inferred static
type (e.g., create an alias using z.infer<typeof inviteMemberSchema> or import
the existing inferred type) and update the function signature to use that
inferred type for the data parameter; ensure z is imported if needed and remove
the runtime schema from any type position.

import { toast } from "sonner";
import { Role } from "@/generated/prisma/client";
import { ColumnDef } from "@tanstack/react-table";
import { useState } from 'react';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove unused import.

The useState import is not used anywhere in this file and should be removed.

Apply this diff:

-import { useState } from 'react';
 import { toast } from 'sonner';
📝 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
import { useState } from 'react';
import { toast } from 'sonner';
🧰 Tools
🪛 GitHub Check: lint-and-build

[warning] 1-1:
'useState' is defined but never used

🤖 Prompt for AI Agents
In src/components/models/projects/ProjectMembersTable.tsx at line 1, the
imported symbol `useState` is unused; remove the `useState` import from the
import list to eliminate the unused import and related linter warnings, leaving
only the imports that are actually referenced in the file.

Comment on lines 17 to 20
email: z.string().email({
message: "Please enter a valid email address.",
}),
message: 'Please enter a valid email address.'
})
});
Copy link

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

Blocked submit: required email + disabled field + empty default makes form impossible to submit

  • Email is required by schema, but the input is disabled and defaults to ''. Disabled fields are omitted from submission; zod then fails on email, and users can’t fix it.
  • Fix either by making email non-editable but submitted, or by not validating it here.

Two viable fixes (pick one):

Option A — keep email non-editable but submit value (prefer):

  • Use readOnly instead of disabled.
  • Ensure defaultValues are populated from the authenticated user before render.
-  email: z.string().email({
+  email: z.string().email({
     message: 'Please enter a valid email address.'
-  })
+  })
...
-        <Input id='email' type='email' {...form.register('email')} disabled />
+        <Input id='email' type='email' {...form.register('email')} readOnly />

Option B — validate email elsewhere (server/auth context) and allow empty here:

-  email: z.string().email({
+  email: z.string().email({
     message: 'Please enter a valid email address.'
-  })
+  }).optional()

At minimum, populate defaultValues from user data before enabling validation.

Also applies to: 28-30, 46-47


I'll verify the concerns raised in the review comment by inspecting the actual ProfileForm.tsx file.


🏁 Script executed:

#!/bin/bash
# Inspect the ProfileForm.tsx file to verify review comment claims
cat src/components/models/user/ProfileForm.tsx

Length of output: 1734


Fix required email field to avoid perpetual validation failure

  • The email input is disabled with a default of '', but the Zod schema requires a valid email—validation always fails and blocks submission.
    Choose one:

Option A: keep email read-only and populate defaultValues.email from the user

- <Input id="email" type="email" {...form.register('email')} disabled />
+ <Input id="email" type="email" {...form.register('email')} readOnly />

Ensure defaultValues.email is initialized with the authenticated user’s email before render.

Option B: make email optional in the schema

- email: z.string().email({ message: 'Please enter a valid email address.' })
+ email: z.string().email({ message: 'Please enter a valid email address.' }).optional()

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

🤖 Prompt for AI Agents
In src/components/models/user/ProfileForm.tsx around lines 17 to 20, the Zod
schema requires a valid email but the form renders the email input disabled with
default '' causing perpetual validation failure; either populate
defaultValues.email with the authenticated user’s email before render (Option A)
— fetch/derive the user email and set it into the form's defaultValues (or wait
to render the form until defaultValues is set), or make the email schema
optional (Option B) — change the Zod rule to accept
z.string().email(...).optional() (and handle undefined downstream). Implement
one of these fixes so the initial value and schema agree.

Comment on lines 33 to 35
function onSubmit(data: ProfileFormValues) {
console.log(data); // TODO: Implement actual update logic
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid logging PII (name/email) in production

Console logging form data can leak PII. Remove or guard by environment.

-  function onSubmit(data: ProfileFormValues) {
-    console.log(data); // TODO: Implement actual update logic
-  }
+  function onSubmit(data: ProfileFormValues) {
+    if (process.env.NODE_ENV !== 'production') {
+      // Debug only
+      console.debug('ProfileForm submit', { hasName: !!data.name, hasEmail: !!data.email });
+    }
+    // TODO: Implement actual update logic
+  }
📝 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
function onSubmit(data: ProfileFormValues) {
console.log(data); // TODO: Implement actual update logic
}
function onSubmit(data: ProfileFormValues) {
if (process.env.NODE_ENV !== 'production') {
// Debug only
console.debug('ProfileForm submit', { hasName: !!data.name, hasEmail: !!data.email });
}
// TODO: Implement actual update logic
}
🤖 Prompt for AI Agents
In src/components/models/user/ProfileForm.tsx around lines 33 to 35, the
onSubmit handler currently console.logs the entire form payload which can expose
PII (name/email); remove the console.log in production and either implement the
actual update logic (call the update API/mutation and handle success/error) or
if you must log for debugging, wrap the log in an environment check (e.g., only
when NODE_ENV === "development") or redact PII before logging (mask email/name)
and use a secure logger that avoids printing sensitive fields. Ensure the final
change replaces the temporary console.log with production-safe update/error
handling and no unguarded PII output.

}
},
user: user, // Add the full user object to the context
user: user // Add the full user object to the context
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Do not place the full Prisma user object in ctx (risk of leaking sensitive fields).

Select only safe fields (exclude passwordHash, tokens, etc.) and attach that to ctx. Also prefer findUnique when querying by unique id.

Apply this diff locally for the ctx assignment:

-      user: user // Add the full user object to the context
+      user: safeUser // Attach a safe projection only

Then fetch and project the user safely (example):

// Replace the current user fetch:
const user = await prisma.user.findUnique({
  where: { id: userId },
  select: { id: true, email: true, name: true, role: true } // exclude sensitive fields
});
if (!user) {
  console.error('Not Authorised', new Error('Invalid user, not allowed'));
  throw new Error('Not Authorised');
}
const safeUser = user; // already projected
🤖 Prompt for AI Agents
In src/lib/action.ts around line 62, do not attach the full Prisma user object
to ctx; instead fetch the user with prisma.user.findUnique using a select
projection that only includes safe fields (e.g., id, email, name, role) to
exclude passwordHash/tokens, handle the case where no user is found by logging
and throwing a Not Authorised error, and then assign that projected safeUser to
ctx (replace the existing user: user assignment).

Comment on lines +1 to +4
import { APP_ENV } from './env';

export const cookieName = "authormaton-session";
export const IS_PRODUCTION = APP_ENV === "production";
export const cookieName = 'authormaton-session';
export const IS_PRODUCTION = APP_ENV === 'production';
Copy link

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

Single-source IS_PRODUCTION; avoid duplicate definitions.

Likely duplicate of IS_PRODUCTION in env.ts. Re-export it here instead of recomputing to prevent drift and import confusion.

Apply this diff:

-import { APP_ENV } from './env';
+export { IS_PRODUCTION } from './env';

 export const cookieName = 'authormaton-session';
-export const IS_PRODUCTION = APP_ENV === 'production';

Run to verify no other conflicting definitions/import cycles:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Defs of IS_PRODUCTION:"
rg -nP --type=ts '\bexport\s+const\s+IS_PRODUCTION\b' -C2

echo
echo "env.ts importing constants.ts?"
rg -nP --type=ts -C2 $'from\\s+[\'"][.][./\\w-]*constants[\'"]' src/lib/env.ts || true

echo
echo "Usages of cookieName:"
rg -nP --type=ts '\bcookieName\b' -n -C2

Length of output: 1343


</script>

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Checking if env.ts exports APP_ENV:"
rg -nP --type=ts '\bexport\s+(const|let|var)\s+APP_ENV\b' src/lib/env.ts -C3 || echo "Not found as direct export"

echo
echo "All exports from env.ts:"
rg -nP --type=ts '\bexport\b' src/lib/env.ts -n

Length of output: 139


</script>

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Full content of src/lib/env.ts:"
cat src/lib/env.ts

Length of output: 422


Single-source IS_PRODUCTION; eliminate duplicate definition.

Confirmed: IS_PRODUCTION is defined in both env.ts (line 4) and constants.ts (line 4), creating two independent sources of the same value. The session.ts file imports from constants.ts, not env.ts, which increases the risk of drift if either definition changes. Re-export from env.ts to make it the single source of truth.

Apply the suggested diff to consolidate:

-import { APP_ENV } from './env';
+export { IS_PRODUCTION } from './env';

 export const cookieName = 'authormaton-session';
-export const IS_PRODUCTION = APP_ENV === 'production';
📝 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
import { APP_ENV } from './env';
export const cookieName = "authormaton-session";
export const IS_PRODUCTION = APP_ENV === "production";
export const cookieName = 'authormaton-session';
export const IS_PRODUCTION = APP_ENV === 'production';
export { IS_PRODUCTION } from './env';
export const cookieName = 'authormaton-session';
🤖 Prompt for AI Agents
In src/lib/constants.ts around lines 1 to 4, IS_PRODUCTION is duplicated from
env.ts; remove the local IS_PRODUCTION constant and re-export the value from
env.ts instead so there is a single source of truth. Specifically, replace the
local declaration by importing IS_PRODUCTION (or APP_ENV if you prefer) from
./env and then export it (export { IS_PRODUCTION } from './env' or export const
IS_PRODUCTION = APP_ENV === 'production' via env); ensure all modules that
currently import IS_PRODUCTION from constants continue to work by keeping the
exported name unchanged and remove the duplicate definition in this file.

export const SigninSchema = z.object({
email: z.string().email({ message: 'Invalid email address.' }),
password: z.string().min(6, { message: 'Password must be at least 6 characters.' }),
password: z.string().min(6, { message: 'Password must be at least 6 characters.' })
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Password policy mismatch (6 vs 8). UI copy says “at least 8 characters,” schema allows 6. Align schema to 8.

Apply this diff:

-  password: z.string().min(6, { message: 'Password must be at least 6 characters.' })
+  password: z.string().min(8, { message: 'Password must be at least 8 characters.' })
-  password: z.string().min(6, { message: 'Password must be at least 6 characters.' })
+  password: z.string().min(8, { message: 'Password must be at least 8 characters.' })

Also applies to: 11-11

🤖 Prompt for AI Agents
In src/lib/validations/auth.ts around lines 5 and 11, the password Zod schema
currently allows a minimum of 6 characters but the UI copy requires at least 8;
update both schema validators to require .min(8) and change the validation
message to 'Password must be at least 8 characters.' so the backend validation
matches the UI.

@fehranbit
Copy link
Member

the email field addition is nice and will really help with validation! merging this now.

@fehranbit fehranbit merged commit 06a1f86 into main Oct 17, 2025
1 of 2 checks passed
@fehranbit fehranbit deleted the feat/invite-member-schema-email branch October 17, 2025 02:27
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