chore: Phase 5 code review - tenant admin pages#12
Conversation
📝 WalkthroughWalkthroughThis PR standardizes code formatting across the application (single to double quotes, spacing, indentation) and introduces functional enhancements: a new forgot-password submission flow, multi-step onboarding wizard, server-side auth/access controls for customer pages, and improved UI structuring in tenant-admin sections. Changes
Sequence DiagramssequenceDiagram
participant User
participant Client as Forgot Password Page
participant API as /api/auth/reset-password
participant Backend as Auth Service
User->>Client: Enter email, submit
Client->>Client: handleSubmit triggered
Client->>API: POST email (JSON)
API->>Backend: Process reset request
alt Success
Backend-->>API: OK response
API-->>Client: Success response
Client->>Client: Set submitted = true
Client->>User: Show success alert
else Error
Backend-->>API: Error response
API-->>Client: Error details
Client->>Client: Console error logged
Client->>User: Show error alert
end
sequenceDiagram
participant User
participant Wizard as Onboarding Wizard
participant FormState as React State
participant API as /api/onboarding
participant Backend as Onboarding Service
User->>Wizard: Start onboarding
Wizard->>Wizard: Display Step 1 (Business Info)
User->>Wizard: Enter business details
Wizard->>FormState: validateStep(1)
alt Step valid
User->>Wizard: Click Next
Wizard->>Wizard: Display Step 2 (Account)
else Invalid
Wizard->>User: Show error toast
end
Note over Wizard: Steps 2-3: Email/Password, NFT Verification
User->>Wizard: Complete Step 4 (Template)
Wizard->>FormState: handleSubmit()
FormState->>API: POST complete onboarding data
API->>Backend: Process registration
Backend-->>API: Success confirmation
API-->>FormState: Success response
FormState->>User: Navigate to dashboard
sequenceDiagram
participant Request as Incoming Request
participant Page as Customer Detail Page
participant Auth as getServerSession
participant DB as Database/Prisma
participant Response as HTTP Response
Request->>Page: GET /customers/[id]
Page->>Auth: Validate session & roles
alt Unauthorized
Auth-->>Response: Redirect to /auth/login
else Authorized
Page->>DB: Check tenant authorization
DB-->>Page: Verify tenantId matches
alt Access denied
Page-->>Response: notFound()
else Allowed
Page->>DB: Fetch customer + counts
DB-->>Page: Customer data
Page-->>Response: Render customer detail page
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
nextjs_space/app/tenant-admin/audit-logs/page.tsx (2)
131-166: Selecting "All" sendsaction=allorentityType=allto the API, which may be unintended.When a user selects "All Actions" or "All Entities", the filter state becomes
"all". Since"all"is truthy, it gets appended to the query params (lines 76-77). This differs from the initial empty string state which correctly skips appending the parameter.If the API doesn't explicitly handle
action=allorentityType=all, this could return unexpected results or no results.🐛 Proposed fix
Either use empty string for the "All" options:
<SelectContent> - <SelectItem value="all">All Actions</SelectItem> + <SelectItem value="">All Actions</SelectItem>Or update the query param logic to skip
"all":- if (actionFilter) params.append("action", actionFilter); - if (entityFilter) params.append("entityType", entityFilter); + if (actionFilter && actionFilter !== "all") params.append("action", actionFilter); + if (entityFilter && entityFilter !== "all") params.append("entityType", entityFilter);
61-66: Page number should reset to 1 when filters change.When
actionFilterorentityFilterchanges, theuseEffectre-fetches logs but keeps the current page number. If the user is on page 5 and applies a filter that yields only 2 pages of results, they may see empty results or an error.🐛 Proposed fix
Reset page to 1 when filters change:
+ // Reset to page 1 when filters change + useEffect(() => { + setPagination((prev) => ({ ...prev, page: 1 })); + }, [actionFilter, entityFilter]); useEffect(() => { fetchLogs(); }, [pagination.page, actionFilter, entityFilter]);Or handle it inline in the filter change handlers:
- <Select value={actionFilter} onValueChange={setActionFilter}> + <Select value={actionFilter} onValueChange={(value) => { + setActionFilter(value); + setPagination((prev) => ({ ...prev, page: 1 })); + }}>nextjs_space/app/tenant-admin/branding/branding-form.tsx (1)
908-920: Hardcoded preview URL should use dynamic tenant URL.The store preview link uses a hardcoded domain
healingbuds.abacusai.appinstead of deriving it from the tenant's subdomain or custom domain. This would show incorrect URLs for other tenants.🔧 Suggested fix using tenant subdomain
<a - href={`https://healingbuds.abacusai.app/store/${tenant.subdomain}`} + href={tenant.customDomain ? `https://${tenant.customDomain}` : `https://${tenant.subdomain}.yourdomain.com`} target="_blank" rel="noopener noreferrer" className="inline-flex items-center justify-center px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors" >Alternatively, import and use the
getTenantUrlutility function used in the tenant-admin page.
🤖 Fix all issues with AI agents
In @nextjs_space/app/onboarding/onboarding-form.tsx:
- Around line 408-485: The variable selectedTemplate declared in case 4 of the
switch is not block-scoped and should be confined to that case; wrap the entire
case 4 body in a block (i.e., add { ... } immediately after case 4) so the const
selectedTemplate = templates.find(...) and the JSX return are inside that block,
preventing the declaration from leaking into other cases; keep the return as-is
inside the new block.
- Around line 385-400: The United Kingdom option in the country <select> in
onboarding-form.tsx uses the non-standard value "UK"; change its value to the
ISO 3166-1 alpha-2 code "GB" (i.e., update the <option value="UK">… to <option
value="GB">…). After changing, search this component for any comparisons,
default values, or form state that reference "UK" and update them to "GB" to
keep behavior consistent with consultation-constants.ts and other components.
In @nextjs_space/app/tenant-admin/branding/page.tsx:
- Around line 1-8: The call to getServerSession() inside BrandingPage is missing
the authOptions parameter so custom session fields (e.g., session.user.id,
tenantId, role) are not populated; update the call to
getServerSession(authOptions) using the same authOptions export used elsewhere
(import or reference the existing authOptions) so session.user.id is defined
before using it in the Prisma query and downstream logic in BrandingPage and
BrandingForm.
- Around line 10-12: The page currently only checks for session?.user?.email and
allows any authenticated user; update the guard to enforce role-based
authorization by verifying session exists and that session.user.role is one of
"TENANT_ADMIN" or "SUPER_ADMIN" (mirroring the customer detail check) and call
redirect("/auth/login") when the check fails; locate the existing session usage
and redirect call in page.tsx and replace the simple email check with the
role-inclusive condition using session.user.role.
In @nextjs_space/app/tenant-admin/templates/page.tsx:
- Around line 63-65: Rename the misspelled variable templatess to a clearer name
like baseTemplates (or marketplaceTemplates) in the template listing statement
and update all its usages (e.g., where templatess is referenced for rendering or
logic) to the new identifier so references at other parts of the file compile
and reflect the new name.
In @nextjs_space/app/tenant-admin/the-wire/page.tsx:
- Around line 31-35: The query uses prisma.posts.findMany including the relation
as users (include: { users: true }) but the consumer component PostsList is
accessing post.author which doesn't exist; update the PostsList rendering to use
post.users (e.g., post.users?.name) wherever post.author is referenced, or
alternatively rename the relation consistently (either change include to author
or rename schema relation) so the relation symbol matches between
prisma.posts.findMany and PostsList.
In @nextjs_space/app/tenant-admin/webhooks/page.tsx:
- Around line 386-393: The webhook secret is shown in plain text (see the Label
and webhook.secret usage); add state to track visibility (e.g., visibleSecrets:
Set<string>) and a toggle function (e.g., toggleSecretVisibility(webhookId)) and
render the secret masked by default (e.g., replace characters with • or use
asterisks) unless the webhook's id is in visibleSecrets; add a reveal/hide
button or icon next to the masked value that calls toggleSecretVisibility and
include an accessible label (aria-pressed/aria-label) so only when toggled the
real webhook.secret is rendered.
- Around line 60-63: The two unused state vars
isEditDialogOpen/setIsEditDialogOpen and editingWebhook/setEditingWebhook are
declared but never referenced; either remove these state declarations from the
component or implement the edit flow: add an "Edit" button that calls
setEditingWebhook(webhook) and setIsEditDialogOpen(true), render an
EditWebhookDialog that reads editingWebhook and calls a save handler to update
the webhook list, and close the dialog by calling setIsEditDialogOpen(false) and
clearing setEditingWebhook(null). Ensure all references (isEditDialogOpen,
setIsEditDialogOpen, editingWebhook, setEditingWebhook) are either removed or
properly wired into the UI and handlers.
🧹 Nitpick comments (41)
nextjs_space/app/tenant-admin/seo/page.tsx (1)
101-106: Type assertion onpageSeocould be fragile.The type assertion assumes the JSON structure in the database matches the expected shape. Consider adding runtime validation or using a schema validation library (e.g., Zod) to ensure data integrity, especially since this data comes from a JSON field that could have arbitrary content.
nextjs_space/app/tenant-admin/seo/seo-page-client.tsx (3)
61-67: UnusedtenantIdprop.The
tenantIdprop is destructured but never used within the component. Either remove it from the props interface and destructuring, or verify if it's needed for future functionality.#!/bin/bash # Verify if tenantId is used anywhere in this file rg -n 'tenantId' nextjs_space/app/tenant-admin/seo/seo-page-client.tsx
87-105: Consider adding error context to thrown errors.The handler throws a generic error on failure. Including response details (status code, error message from body) would help with debugging and provide better error messages to users via the modal's catch block.
♻️ Suggested improvement
const handleSaveProductSeo = async (seo: SeoData) => { if (!selectedProduct) return; const res = await fetch( `/api/tenant-admin/seo/products/${selectedProduct.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(seo), }, ); - if (!res.ok) throw new Error("Failed to save"); + if (!res.ok) { + const error = await res.json().catch(() => ({})); + throw new Error(error.message || `Failed to save (${res.status})`); + } // Update local state setLocalProducts((prev) => prev.map((p) => (p.id === selectedProduct.id ? { ...p, seo } : p)), ); };
142-156: Consider extractingSeoStatusBadgeoutside the component.Defining this component inside
SeoPageClientcauses it to be recreated on every render. While the performance impact is minimal for this UI, extracting it outside the parent component (or memoizing it) is a cleaner pattern.♻️ Suggested extraction
+const SeoStatusBadge = ({ hasCustomSeo }: { hasCustomSeo: boolean }) => ( + <Badge variant={hasCustomSeo ? "default" : "secondary"} className="gap-1"> + {hasCustomSeo ? ( + <> + <CheckCircle className="h-3 w-3" /> + Custom + </> + ) : ( + <> + <AlertCircle className="h-3 w-3" /> + Default + </> + )} + </Badge> +); export function SeoPageClient({ tenantId, baseUrl, products, posts, pageSeo, }: SeoPageClientProps) { // ... state hooks ... - const SeoStatusBadge = ({ hasCustomSeo }: { hasCustomSeo: boolean }) => ( - <Badge variant={hasCustomSeo ? "default" : "secondary"} className="gap-1"> - {hasCustomSeo ? ( - <> - <CheckCircle className="h-3 w-3" /> - Custom - </> - ) : ( - <> - <AlertCircle className="h-3 w-3" /> - Default - </> - )} - </Badge> - );nextjs_space/app/tenant-admin/orders/[id]/packing-slip/page.tsx (1)
62-92: Consider adding AbortController for fetch cleanup.The formatting changes look good. As a recommended improvement, consider adding an AbortController to prevent potential state updates on unmounted components when navigating away during fetch:
♻️ Suggested improvement with AbortController
useEffect(() => { if (!session?.user?.id) return; + const abortController = new AbortController(); + const fetchOrderAndTenant = async () => { try { // Fetch order data const orderResponse = await fetch( `/api/tenant-admin/orders/${orderId}`, + { signal: abortController.signal }, ); if (!orderResponse.ok) { throw new Error("Failed to fetch order"); } const orderData = await orderResponse.json(); setOrder(orderData); // Fetch tenant data for business name - const tenantResponse = await fetch("/api/tenant-admin/tenant"); + const tenantResponse = await fetch("/api/tenant-admin/tenant", { + signal: abortController.signal, + }); if (tenantResponse.ok) { const tenantData = await tenantResponse.json(); setTenant(tenantData); } } catch (error) { + if (error instanceof Error && error.name === "AbortError") return; console.error("Error fetching data:", error); toast.error("Failed to load packing slip"); } finally { setLoading(false); } }; fetchOrderAndTenant(); + + return () => abortController.abort(); }, [session, orderId]);nextjs_space/app/tenant-admin/the-wire/[id]/page.tsx (1)
32-40: Consider removing unused include.The tenant access verification logic is correct. However, the
include: { tenants: true }fetches related data that's never used—onlyuser?.tenantIdis needed for the comparison.♻️ Optional optimization
const user = await prisma.users.findUnique({ where: { id: session.user.id }, - include: { tenants: true }, });nextjs_space/app/tenant-admin/audit-logs/page.tsx (1)
64-66: Consider addingfetchLogsto the dependency array or wrapping it withuseCallback.The
useEffectcallsfetchLogsbut doesn't include it in the dependency array. While this works because the relevant state variables are listed, React's exhaustive-deps lint rule would flag this. WrappingfetchLogsinuseCallbackwith its dependencies would make the relationship explicit and satisfy linting.♻️ Suggested refactor
+ const fetchLogs = useCallback(async () => { - const fetchLogs = async () => { try { setLoading(true); const params = new URLSearchParams({ page: pagination.page.toString(), limit: pagination.limit.toString(), }); if (actionFilter) params.append("action", actionFilter); if (entityFilter) params.append("entityType", entityFilter); const response = await fetch(`/api/tenant-admin/audit-logs?${params}`); const data = await response.json(); if (response.ok) { setLogs(data.logs); setPagination(data.pagination); } } catch (error) { console.error("Failed to fetch audit logs:", error); } finally { setLoading(false); } - }; + }, [pagination.page, pagination.limit, actionFilter, entityFilter]); useEffect(() => { fetchLogs(); - }, [pagination.page, actionFilter, entityFilter]); + }, [fetchLogs]);Don't forget to add
useCallbackto the imports:-import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react";nextjs_space/app/tenant-admin/the-wire/post-form.tsx (4)
17-17: Unused import:ArrowLeftThe
ArrowLefticon is imported but not used anywhere in the component.🔧 Suggested fix
-import { ArrowLeft, Upload, X, Loader2 } from "lucide-react"; +import { Upload, X, Loader2 } from "lucide-react";
42-68: Consider improving error handling and code organization.Two observations:
Code organization: This function references
form(line 59) before it's declared (line 70). While this works at runtime because the function is only called afterformis initialized, moving theuseFormhook before this function would improve readability.Error handling: The upload error handling discards any detailed error message from the server response.
🔧 Suggested improvement for error handling
if (!res.ok) throw new Error("Upload failed"); + if (!res.ok) { + const errorData = await res.json().catch(() => ({})); + throw new Error(errorData.error || "Upload failed"); + }
183-203: Add accessibility attributes to the upload button.The upload button with hidden file input overlay lacks an accessible label. Screen reader users won't understand the button's purpose.
♿ Suggested accessibility improvement
<Button type="button" variant="outline" size="icon" disabled={isUploading} + aria-label="Upload cover image" >
155-209: Consider extractingcoverImageto a variable to avoid repeatedwatchcalls.
form.watch("coverImage")is called multiple times (lines 158, 161). While functionally correct, extracting it to a variable improves readability and avoids redundant subscriptions.🔧 Suggested improvement
+ const coverImage = form.watch("coverImage"); <div className="space-y-2"> <Label htmlFor="coverImage">Cover Image</Label> <div className="flex flex-col gap-4"> - {form.watch("coverImage") && ( + {coverImage && ( <div className="relative aspect-video w-full max-w-sm rounded-lg overflow-hidden border"> <img - src={form.watch("coverImage")} + src={coverImage} alt="Cover preview"nextjs_space/app/tenant-admin/products/page.tsx (1)
98-102: Consider extracting the duplicated search filter.The search filter OR clause is repeated four times across different Prisma queries. Extracting it to a reusable helper would improve maintainability.
♻️ Suggested refactor to eliminate duplication
Add this helper before the Prisma queries:
// Helper to build search filter for reuse const searchFilter = search ? { OR: [ { name: { contains: search, mode: "insensitive" } }, { category: { contains: search, mode: "insensitive" } }, { slug: { contains: search, mode: "insensitive" } }, ], } : {};Then simplify the queries:
const whereClause: Prisma.productsWhereInput = { tenantId, + ...searchFilter, }; -// Apply search filter (case-insensitive across multiple fields) -if (search) { - whereClause.OR = [ - { name: { contains: search, mode: "insensitive" } }, - { category: { contains: search, mode: "insensitive" } }, - { slug: { contains: search, mode: "insensitive" } }, - ]; -}And in the parallel queries:
prisma.products.count({ where: { tenantId, - ...(search - ? { - OR: [ - { name: { contains: search, mode: "insensitive" } }, - { category: { contains: search, mode: "insensitive" } }, - { slug: { contains: search, mode: "insensitive" } }, - ], - } - : {}), + ...searchFilter, stock: { gt: 0 }, }, }),Apply the same pattern to the other count queries.
Also applies to: 145-152, 161-168, 178-185
nextjs_space/app/tenant-admin/analytics/page.tsx (4)
248-255: Edge case:getInitialscrashes on empty or single-character names.If
nameis an empty string,n[0]returnsundefined, and calling.join("")on an array containingundefinedwould produce unexpected results. Similarly, names without spaces return a single character.Consider adding a guard:
Suggested improvement
const getInitials = (name: string) => { + if (!name?.trim()) return "?"; return name .split(" ") - .map((n) => n[0]) + .map((n) => n[0] || "") .join("") .toUpperCase() .slice(0, 2); };
279-283: Missingsession.user.idin dependency array may cause stale closure.The effect checks
session?.user?.idbut only listssessionin dependencies. While React's exhaustive-deps rule typically catches this, explicitly includingsession?.user?.idor restructuring makes the intent clearer.
303-390: Duplicate mock data objects violate DRY principle.The same mock analytics object is defined twice—once in the
elsebranch (lines 303-353) and again in thecatchblock (lines 358-390). Extract to a constant or helper function.Suggested refactor
+const DEFAULT_MOCK_ANALYTICS: AnalyticsData = { + totalProducts: 10, + totalOrders: 25, + totalCustomers: 15, + totalRevenue: 2500.0, + recentOrders: 8, + recentCustomers: 5, + recentRevenue: 850.0, + avgOrderValue: 100.0, + revenueByDay: Array.from({ length: 7 }, (_, i) => ({ + date: `Day ${i + 1}`, + revenue: Math.random() * 500 + 200, + })), + // ... rest of properties +}; const fetchAnalytics = async () => { try { // ... } else { console.error("API error:", response.status, response.statusText); - setAnalytics({ - totalProducts: 10, - // ... duplicate object - }); + setAnalytics(DEFAULT_MOCK_ANALYTICS); } } catch (error) { console.error("Error fetching analytics:", error); - setAnalytics({ - totalProducts: 10, - // ... duplicate object - }); + setAnalytics(DEFAULT_MOCK_ANALYTICS); } };
396-396: Consider movingCOLORSconstant outside the component.The
COLORSarray is static and doesn't depend on component state or props. Defining it outside the component avoids recreating the array on every render.Suggested fix
+const COLORS = ["#10b981", "#3b82f6", "#f59e0b", "#ef4444", "#8b5cf6"]; + export default function TenantAnalyticsPage() { // ... - const COLORS = ["#10b981", "#3b82f6", "#f59e0b", "#ef4444", "#8b5cf6"];nextjs_space/app/tenant-admin/emails/[id]/client.tsx (1)
28-51: Consider extracting detailed error messages from the API response.The error handling currently throws a generic error message when the response is not ok. Consider extracting the error details from the response body to provide more helpful feedback to users.
♻️ Suggested improvement for error handling
const handleSave = async (data: EmailTemplateData) => { setIsSaving(true); try { const res = await fetch( `/api/tenant-admin/email-templates/${template.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }, ); - if (!res.ok) throw new Error("Failed to update template"); + if (!res.ok) { + const errorData = await res.json().catch(() => ({})); + throw new Error(errorData.message || "Failed to update template"); + } toast.success("Template updated successfully"); router.refresh(); router.push("/tenant-admin/emails"); } catch (error) { console.error(error); - toast.error("Failed to update template"); + toast.error(error instanceof Error ? error.message : "Failed to update template"); } finally { setIsSaving(false); } };nextjs_space/app/tenant-admin/emails/[id]/page.tsx (1)
44-44: Consider adding proper type definition instead ofas any.The
as anytype assertion bypasses TypeScript's type safety. Consider defining a proper type for the template props or using a type that matches the Prisma model.♻️ Suggested improvement for type safety
If the
EditTemplateClientPropsinterface inclient.tsxmatches the Prisma model structure, you could cast to that specific type:-return <TenantEditTemplateClient template={template as any} />; +return <TenantEditTemplateClient template={template} />;Or define a serialization helper if Prisma types (Decimal, Date) need conversion:
function serializeTemplate(template: typeof template) { return { id: template.id, name: template.name, subject: template.subject, contentHtml: template.contentHtml, description: template.description, category: template.category, }; } return <TenantEditTemplateClient template={serializeTemplate(template)} />;nextjs_space/app/tenant-admin/emails/page.tsx (1)
32-33: Remove development artifacts.The whitespace pattern
{" "}and developer comment appear to be leftover from development and should be cleaned up before merging.♻️ Proposed cleanup
- <div className="flex justify-start"> - {" "} - {/* Left aligned tabs usually better? Or right? User screenshot had Filter/Tabs. I'll stick to left or standard. */} - <TabsList> + <TabsList> - <TabsTrigger value="templates">Templates</TabsTrigger> - <TabsTrigger value="events">Event Triggers</TabsTrigger> - </TabsList> - - </div> + <TabsTrigger value="templates">Templates</TabsTrigger> + <TabsTrigger value="events">Event Triggers</TabsTrigger> + </TabsList>nextjs_space/app/tenant-admin/webhooks/page.tsx (2)
94-97: Consider adding URL format validation.The current validation only checks if the URL field is not empty, but doesn't verify that it's a valid HTTP/HTTPS URL. Invalid URLs could cause runtime errors when webhooks are triggered.
✨ Proposed enhancement: Add URL validation
const handleCreate = async () => { - if (!formData.url || formData.events.length === 0) { - toast.error("Please fill in all required fields"); + if (!formData.url || formData.events.length === 0) { + toast.error("Please fill in all required fields"); + return; + } + + try { + new URL(formData.url); + if (!formData.url.startsWith('http://') && !formData.url.startsWith('https://')) { + toast.error("Webhook URL must use HTTP or HTTPS protocol"); + return; + } + } catch { + toast.error("Please enter a valid URL"); return; }
147-150: Replaceconfirm()with a Dialog component for consistency.The delete confirmation uses the browser's native
confirm()dialog, which is inconsistent with the Dialog components used elsewhere in this page (e.g., create webhook dialog). This creates an inconsistent user experience.♻️ Proposed refactor: Use Dialog component
Add state for delete confirmation dialog:
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); const [deletingWebhookId, setDeletingWebhookId] = useState<string | null>(null);Update the handler:
- const handleDelete = async (webhookId: string) => { - if (!confirm("Are you sure you want to delete this webhook?")) { - return; - } + const confirmDelete = (webhookId: string) => { + setDeletingWebhookId(webhookId); + setDeleteConfirmOpen(true); + }; + const handleDelete = async () => { + if (!deletingWebhookId) return; + try { - const response = await fetch(`/api/tenant-admin/webhooks/${webhookId}`, { + const response = await fetch(`/api/tenant-admin/webhooks/${deletingWebhookId}`, { method: "DELETE", }); if (response.ok) { toast.success("Webhook deleted successfully"); + setDeleteConfirmOpen(false); + setDeletingWebhookId(null); fetchWebhooks();Then add the Dialog component in the JSX.
nextjs_space/app/tenant-admin/cookie-settings/settings-form.tsx (1)
36-40: UnusedtenantIdprop.The
tenantIdprop is declared in the interface and destructured but never used in the component. If the API endpoint derives the tenant from the session, this prop can be removed to avoid confusion.🔧 Suggested fix
interface CookieSettingsFormProps { - tenantId: string; countryCode: string; initialSettings: { // ... }; } export default function CookieSettingsForm({ - tenantId, countryCode, initialSettings, }: CookieSettingsFormProps) {nextjs_space/app/tenant-admin/orders/orders-table.tsx (2)
1015-1040: Consider usingLoader2consistently instead of inline SVG.The
Loader2icon from lucide-react is already imported and used elsewhere in this file (lines 71, 723). Using it here instead of the inline SVG would improve consistency and reduce code duplication.🔧 Suggested fix
{isProcessing ? ( <> - <span className="animate-spin mr-2"> - <svg - className="h-4 w-4" - viewBox="0 0 24 24" - fill="none" - xmlns="http://www.w3.org/2000/svg" - > - <circle - className="opacity-25" - cx="12" - cy="12" - r="10" - stroke="currentColor" - strokeWidth="4" - /> - <path - className="opacity-75" - fill="currentColor" - d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" - /> - </svg> - </span> + <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Processing... </>
294-309: Minor:getDateRangeLabelfunction called inuseMemowithout being in dependencies.The
getDateRangeLabelfunction is called within theuseMemobut not listed in the dependency array. While the underlying state values (dateRangeFilter,dateFromFilter,dateToFilter) influence the result indirectly throughhasDateFilter, this pattern may trigger ESLint'sexhaustive-depsrule. Consider memoizinggetDateRangeLabelwithuseCallbackor including the actual state values in the dependency array.nextjs_space/app/tenant-admin/customers/[id]/customer-edit-form.tsx (1)
49-51: Consider usingunknowntype instead ofanyfor caught errors.TypeScript best practice is to use
unknownfor caught errors and then narrow the type, rather than usingany.🔧 Suggested fix
- } catch (error: any) { - toast.error(error.message || "Failed to update customer"); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : "Failed to update customer"; + toast.error(message); console.error(error);nextjs_space/app/tenant-admin/templates/clone-button.tsx (2)
45-47: Inconsistent error handling:alert()vstoast.This component uses
alert()for error notifications while the rest of the codebase (includingactivate-button.tsxin the same directory) uses thetoastutility. Consider usingtoast.error()for consistency.🔧 Suggested fix
+import { toast } from "@/components/ui/sonner"; // ... in catch block: } catch (error) { console.error("Clone error:", error); - alert("Failed to clone template. Please try again."); + toast.error("Failed to clone template. Please try again."); } finally {
8-16: UnusedtemplateNameprop.The
templateNameprop is declared in the interface and destructured but never used in the component. If it's intended for future use (e.g., in success/error messages), consider using it; otherwise, remove it.🔧 Suggested fix (if not needed)
interface TemplateCloneButtonProps { templateId: string; - templateName: string; } export default function TemplateCloneButton({ templateId, - templateName, }: TemplateCloneButtonProps) {nextjs_space/app/tenant-admin/templates/page.tsx (1)
24-24: Avoid usinganytype.The
ClonedTemplatetype is set toany, which bypasses TypeScript's type checking benefits. Consider defining a proper type based on the Prisma query result.🔧 Suggested fix
-// Define typed interface for the cloned template with the included base template relation -type ClonedTemplate = any; +// Define typed interface for the cloned template with the included base template relation +type ClonedTemplate = { + id: string; + tenantId: string; + templatesId: string; + templateName: string; + isActive: boolean; + createdAt: Date; + templates: { + thumbnailUrl: string | null; + previewUrl: string | null; + } | null; +};nextjs_space/app/auth/forgot-password/page.tsx (1)
37-41: Consider using toast notifications instead ofalert()for consistency.Other files in this PR (e.g.,
orders/page.tsx,posts-list.tsx,customer-actions.tsx) use thetoastutility from@/components/ui/sonnerfor user feedback. Usingalert()here is inconsistent and provides a worse UX.♻️ Suggested improvement
+import { toast } from "@/components/ui/sonner"; // In handleSubmit: } else { - alert(data.error || "Failed to send reset email"); + toast.error(data.error || "Failed to send reset email"); } } catch (error) { console.error("Password reset error:", error); - alert("Failed to send reset email"); + toast.error("Failed to send reset email"); } finally {nextjs_space/app/tenant-admin/customers/customers-table.tsx (1)
193-200: Consider adding clipboard API availability check.
navigator.clipboard.writeTextmay not be available in all contexts (e.g., non-HTTPS in some browsers). A try-catch with user feedback would improve robustness.♻️ Optional improvement
onClick: () => { - navigator.clipboard.writeText(window.location.origin); + navigator.clipboard.writeText(window.location.origin) + .then(() => toast.success("Store URL copied!")) + .catch(() => toast.error("Failed to copy URL")); },nextjs_space/app/tenant-admin/the-wire/posts-list.tsx (2)
29-31: Consider typinginitialPostswith thePostinterface.The
Postinterface is defined butinitialPostsis typed asany[], losing type safety benefits.♻️ Suggested fix
-export default function PostsList({ initialPosts }: { initialPosts: any[] }) { +export default function PostsList({ initialPosts }: { initialPosts: Post[] }) { const router = useRouter(); - const [posts, setPosts] = useState(initialPosts); + const [posts, setPosts] = useState<Post[]>(initialPosts);
58-59: Consider using AlertDialog instead ofwindow.confirm()for consistency.Other components in this PR (e.g.,
customer-actions.tsx) useAlertDialogfor delete confirmations, providing better UX and consistent styling.nextjs_space/app/tenant-admin/orders/page.tsx (1)
109-119: Client-side auth check may cause brief content flash.Unlike the server components in this PR (e.g.,
customers/page.tsx,settings/page.tsx) that redirect before rendering, this client-side check allows the page to briefly render before redirecting unauthorized users. Consider adding server-side protection via middleware or a server component wrapper.nextjs_space/app/auth/login/page.tsx (1)
41-80: Consider adding cleanup for fetch requests.The
useEffectmakes fetch calls that could complete after component unmount, potentially causing state updates on unmounted components. While React 18+ handles this more gracefully, adding an AbortController would be a cleaner pattern.♻️ Suggested improvement with AbortController
useEffect(() => { + const controller = new AbortController(); // Get tenant from hostname/subdomain const hostname = window.location.hostname; const parts = hostname.split("."); const subdomain = parts.length >= 2 ? parts[0] : null; // Fetch tenant settings based on subdomain if (subdomain && subdomain !== "localhost" && subdomain !== "www") { - fetch(`/api/tenant/${subdomain}`) + fetch(`/api/tenant/${subdomain}`, { signal: controller.signal }) .then((res) => res.json()) .then((data) => { if (data && data.tenant) { setTenantSettings({ businessName: data.tenant.businessName || "BudStack", primaryColor: data.tenant.branding?.primaryColor || "#16a34a", logoUrl: data.tenant.branding?.logoUrl, subdomain: data.tenant.subdomain, }); } }) .catch((err) => { - console.error("Failed to fetch tenant settings:", err); + if (err.name !== 'AbortError') { + console.error("Failed to fetch tenant settings:", err); + } }); } else { // ... similar changes for platform settings fetch } + return () => controller.abort(); }, []);nextjs_space/app/tenant-admin/branding/comprehensive-branding-form.tsx (1)
301-312: Type assertions for dynamic keys could be made safer.The dynamic key access pattern using
formData[\${day}Open` as keyof typeof formData]` works but relies on type assertions. This is acceptable given the initialization guarantees these keys exist, but consider extracting to a helper function for better maintainability in the future.nextjs_space/app/tenant-admin/page.tsx (1)
66-67: Remove redundant variable declaration.Both
tenantStoreUrlandtenantUrlare assigned the same value fromgetTenantUrl(tenant). Only one variable is needed.♻️ Suggested fix
- const tenantStoreUrl = getTenantUrl(tenant); - const tenantUrl = getTenantUrl(tenant); + const tenantUrl = getTenantUrl(tenant);Then replace
tenantStoreUrlwithtenantUrlon lines 86 and 89.nextjs_space/app/tenant-admin/customers/[id]/page.tsx (2)
17-22: Non-null assertion onsession.user.rolecould be avoided.Using
session.user.role!suppresses TypeScript's null check. While the logic still works correctly (an undefined role won't match the array and will redirect), removing the assertion makes the code more explicit.♻️ Suggested improvement
if ( !session || - !["TENANT_ADMIN", "SUPER_ADMIN"].includes(session.user.role!) + !session.user.role || + !["TENANT_ADMIN", "SUPER_ADMIN"].includes(session.user.role) ) { redirect("/auth/login"); }
48-51: Redundant tenant access check - already enforced by the Prisma query.This verification is unnecessary because the Prisma query on lines 28-42 already includes
...(tenantId && { tenantId }). WhentenantIdis defined (forTENANT_ADMIN), only customers matching that tenant are returned. If a customer is found, it's guaranteed to have the correcttenantId.This doesn't cause harm but can be removed to reduce code noise.
♻️ Remove redundant check
if (!customer) { notFound(); } - // Verify tenant access for tenant admins - if (session.user.role === "TENANT_ADMIN" && customer.tenantId !== tenantId) { - notFound(); - } - return (nextjs_space/app/onboarding/onboarding-form.tsx (3)
114-119: Consider allowing hyphens in generated subdomains.The regex
/[^a-z0-9]/gstrips hyphens, which are valid subdomain characters. For business names like "Green-Leaf Dispensary", preserving word boundaries with hyphens (e.g.,greenleaf→green-leaf) would improve readability.That said, users can manually edit the subdomain field, so this is a minor UX consideration.
121-134: Consider adding basic format validation.The validation checks for presence but not format:
- Email: No format validation (could use a simple regex or native HTML5 validation)
- Subdomain: User-edited value isn't validated for valid subdomain characters
While server-side validation should catch these, client-side feedback improves UX.
♻️ Suggested validation improvements
const validateStep = (step: number) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + const subdomainRegex = /^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]?$/; + switch (step) { case 1: - return formData.businessName && formData.subdomain; + return formData.businessName && formData.subdomain && subdomainRegex.test(formData.subdomain); case 2: - return formData.email && formData.password.length >= 6; + return emailRegex.test(formData.email) && formData.password.length >= 6; case 3: return formData.nftTokenId && formData.countryCode; case 4: return formData.templateId; default: return false; } };
433-438: Consider using Next.jsImagecomponent for optimization.Native
<img>tags miss Next.js image optimization benefits (lazy loading, responsive sizing, format optimization). If template images are from known domains, configurenext.config.jsand use theImagecomponent.This is optional since external URLs require explicit domain configuration.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (43)
nextjs_space/app/auth/forgot-password/page.tsxnextjs_space/app/auth/login/page.tsxnextjs_space/app/auth/reset-password/[token]/page.tsxnextjs_space/app/auth/signup/page.tsxnextjs_space/app/onboarding/onboarding-form.tsxnextjs_space/app/onboarding/page.tsxnextjs_space/app/tenant-admin/analytics/page.tsxnextjs_space/app/tenant-admin/audit-logs/page.tsxnextjs_space/app/tenant-admin/branding/branding-form.tsxnextjs_space/app/tenant-admin/branding/comprehensive-branding-form.tsxnextjs_space/app/tenant-admin/branding/page.tsxnextjs_space/app/tenant-admin/cookie-settings/page.tsxnextjs_space/app/tenant-admin/cookie-settings/settings-form.tsxnextjs_space/app/tenant-admin/customers/[id]/customer-actions.tsxnextjs_space/app/tenant-admin/customers/[id]/customer-edit-form.tsxnextjs_space/app/tenant-admin/customers/[id]/page.tsxnextjs_space/app/tenant-admin/customers/customers-table.tsxnextjs_space/app/tenant-admin/customers/page.tsxnextjs_space/app/tenant-admin/emails/[id]/client.tsxnextjs_space/app/tenant-admin/emails/[id]/page.tsxnextjs_space/app/tenant-admin/emails/new/client.tsxnextjs_space/app/tenant-admin/emails/new/page.tsxnextjs_space/app/tenant-admin/emails/page.tsxnextjs_space/app/tenant-admin/layout.tsxnextjs_space/app/tenant-admin/orders/[id]/packing-slip/page.tsxnextjs_space/app/tenant-admin/orders/orders-table.tsxnextjs_space/app/tenant-admin/orders/page.tsxnextjs_space/app/tenant-admin/page.tsxnextjs_space/app/tenant-admin/products/page.tsxnextjs_space/app/tenant-admin/products/products-table.tsxnextjs_space/app/tenant-admin/seo/page.tsxnextjs_space/app/tenant-admin/seo/seo-page-client.tsxnextjs_space/app/tenant-admin/settings/page.tsxnextjs_space/app/tenant-admin/settings/settings-form.tsxnextjs_space/app/tenant-admin/templates/activate-button.tsxnextjs_space/app/tenant-admin/templates/clone-button.tsxnextjs_space/app/tenant-admin/templates/page.tsxnextjs_space/app/tenant-admin/the-wire/[id]/page.tsxnextjs_space/app/tenant-admin/the-wire/new/page.tsxnextjs_space/app/tenant-admin/the-wire/page.tsxnextjs_space/app/tenant-admin/the-wire/post-form.tsxnextjs_space/app/tenant-admin/the-wire/posts-list.tsxnextjs_space/app/tenant-admin/webhooks/page.tsx
🧰 Additional context used
🧬 Code graph analysis (33)
nextjs_space/app/tenant-admin/emails/[id]/client.tsx (2)
nextjs_space/components/admin/email/EmailEditor.tsx (1)
EmailTemplateData(23-29)nextjs_space/components/ui/sonner.tsx (1)
toast(26-170)
nextjs_space/app/tenant-admin/settings/page.tsx (2)
nextjs_space/lib/auth.ts (2)
session(64-71)authOptions(7-80)nextjs_space/components/admin/shared/Breadcrumbs.tsx (1)
Breadcrumbs(66-139)
nextjs_space/app/tenant-admin/orders/[id]/packing-slip/page.tsx (1)
nextjs_space/components/ui/sonner.tsx (1)
toast(26-170)
nextjs_space/app/tenant-admin/emails/[id]/page.tsx (3)
nextjs_space/lib/auth.ts (2)
session(64-71)authOptions(7-80)nextjs_space/lib/db.ts (1)
prisma(34-43)nextjs_space/app/tenant-admin/emails/[id]/client.tsx (1)
TenantEditTemplateClient(22-70)
nextjs_space/app/tenant-admin/cookie-settings/page.tsx (3)
nextjs_space/lib/auth.ts (1)
session(64-71)nextjs_space/lib/db.ts (1)
prisma(34-43)nextjs_space/app/tenant-admin/cookie-settings/settings-form.tsx (1)
CookieSettingsForm(36-264)
nextjs_space/app/tenant-admin/products/page.tsx (4)
nextjs_space/lib/auth.ts (1)
session(64-71)nextjs_space/lib/db.ts (1)
prisma(34-43)nextjs_space/components/admin/shared/Breadcrumbs.tsx (1)
Breadcrumbs(66-139)nextjs_space/components/admin/shared/index.ts (1)
Breadcrumbs(28-28)
nextjs_space/app/tenant-admin/audit-logs/page.tsx (1)
nextjs_space/components/admin/shared/Breadcrumbs.tsx (1)
Breadcrumbs(66-139)
nextjs_space/app/tenant-admin/cookie-settings/settings-form.tsx (7)
nextjs_space/lib/cookie-utils.ts (3)
getConsentModel(76-93)isGDPRRegion(98-103)isPOPIARegion(108-111)nextjs_space/components/ui/sonner.tsx (1)
toast(26-170)nextjs_space/components/ui/card.tsx (5)
Card(80-80)CardHeader(81-81)CardTitle(83-83)CardContent(85-85)CardDescription(84-84)nextjs_space/components/ui/label.tsx (1)
Label(26-26)nextjs_space/components/ui/switch.tsx (1)
Switch(29-29)nextjs_space/components/ui/input.tsx (1)
Input(24-24)nextjs_space/components/ui/button.tsx (1)
Button(57-57)
nextjs_space/app/tenant-admin/the-wire/new/page.tsx (3)
nextjs_space/app/tenant-admin/the-wire/[id]/page.tsx (1)
metadata(7-9)nextjs_space/app/tenant-admin/the-wire/page.tsx (1)
metadata(11-13)nextjs_space/app/tenant-admin/the-wire/post-form.tsx (1)
PostForm(34-274)
nextjs_space/app/tenant-admin/layout.tsx (1)
nextjs_space/lib/auth.ts (1)
session(64-71)
nextjs_space/app/tenant-admin/templates/clone-button.tsx (1)
nextjs_space/components/ui/button.tsx (1)
Button(57-57)
nextjs_space/app/tenant-admin/emails/new/client.tsx (2)
nextjs_space/components/admin/email/EmailEditor.tsx (1)
EmailTemplateData(23-29)nextjs_space/components/ui/sonner.tsx (1)
toast(26-170)
nextjs_space/app/tenant-admin/branding/comprehensive-branding-form.tsx (9)
nextjs_space/lib/types.ts (1)
TenantSettings(35-210)nextjs_space/components/ui/tabs.tsx (1)
TabsTrigger(55-55)nextjs_space/components/ui/label.tsx (1)
Label(26-26)nextjs_space/components/ui/input.tsx (1)
Input(24-24)nextjs_space/components/ui/separator.tsx (1)
Separator(31-31)nextjs_space/components/ui/switch.tsx (1)
Switch(29-29)nextjs_space/components/ui/select.tsx (1)
Select(150-150)nextjs_space/components/ui/card.tsx (3)
CardDescription(84-84)CardHeader(81-81)CardContent(85-85)nextjs_space/components/ui/textarea.tsx (1)
Textarea(23-23)
nextjs_space/app/tenant-admin/seo/page.tsx (3)
nextjs_space/lib/auth.ts (1)
session(64-71)nextjs_space/lib/db.ts (1)
prisma(34-43)nextjs_space/app/tenant-admin/seo/seo-page-client.tsx (1)
SeoPageClient(61-381)
nextjs_space/app/tenant-admin/customers/page.tsx (2)
nextjs_space/components/admin/shared/Breadcrumbs.tsx (1)
Breadcrumbs(66-139)nextjs_space/app/tenant-admin/customers/customers-table.tsx (1)
CustomersTable(61-320)
nextjs_space/app/tenant-admin/orders/page.tsx (2)
nextjs_space/components/admin/shared/Breadcrumbs.tsx (1)
Breadcrumbs(66-139)nextjs_space/components/ui/dialog.tsx (2)
Dialog(112-112)DialogContent(117-117)
nextjs_space/app/tenant-admin/products/products-table.tsx (4)
nextjs_space/lib/utils.ts (1)
cn(4-6)nextjs_space/lib/admin/url-state.ts (1)
useTableState(136-322)nextjs_space/components/admin/shared/index.ts (1)
StatusFilterOption(13-13)nextjs_space/lib/admin/csv-export.ts (1)
exportToCSV(116-147)
nextjs_space/app/tenant-admin/customers/customers-table.tsx (2)
nextjs_space/lib/admin/url-state.ts (1)
useTableState(136-322)nextjs_space/lib/admin/csv-export.ts (1)
exportToCSV(116-147)
nextjs_space/app/tenant-admin/customers/[id]/customer-actions.tsx (2)
nextjs_space/components/ui/sonner.tsx (1)
toast(26-170)nextjs_space/components/ui/alert-dialog.tsx (8)
AlertDialog(130-130)AlertDialogContent(134-134)AlertDialogHeader(135-135)AlertDialogTitle(137-137)AlertDialogDescription(138-138)AlertDialogFooter(136-136)AlertDialogCancel(140-140)AlertDialogAction(139-139)
nextjs_space/app/tenant-admin/emails/new/page.tsx (2)
nextjs_space/lib/auth.ts (2)
session(64-71)authOptions(7-80)nextjs_space/lib/db.ts (1)
prisma(34-43)
nextjs_space/app/tenant-admin/settings/settings-form.tsx (1)
nextjs_space/components/ui/sonner.tsx (1)
toast(26-170)
nextjs_space/app/auth/login/page.tsx (3)
nextjs_space/components/ui/sonner.tsx (1)
toast(26-170)nextjs_space/lib/auth.ts (1)
session(64-71)nextjs_space/components/ui/button.tsx (1)
Button(57-57)
nextjs_space/app/tenant-admin/the-wire/posts-list.tsx (4)
nextjs_space/components/ui/sonner.tsx (1)
toast(26-170)nextjs_space/components/ui/button.tsx (1)
Button(57-57)nextjs_space/components/ui/table.tsx (6)
Table(109-109)TableHeader(110-110)TableRow(114-114)TableHead(113-113)TableBody(111-111)TableCell(115-115)nextjs_space/components/ui/badge.tsx (1)
Badge(37-37)
nextjs_space/app/tenant-admin/the-wire/[id]/page.tsx (2)
nextjs_space/lib/auth.ts (2)
session(64-71)authOptions(7-80)nextjs_space/lib/db.ts (1)
prisma(34-43)
nextjs_space/app/tenant-admin/customers/[id]/customer-edit-form.tsx (5)
nextjs_space/components/ui/sonner.tsx (1)
toast(26-170)nextjs_space/components/ui/card.tsx (4)
Card(80-80)CardHeader(81-81)CardTitle(83-83)CardContent(85-85)nextjs_space/components/ui/button.tsx (1)
Button(57-57)nextjs_space/components/ui/label.tsx (1)
Label(26-26)nextjs_space/components/ui/input.tsx (1)
Input(24-24)
nextjs_space/app/onboarding/onboarding-form.tsx (7)
nextjs_space/components/ui/sonner.tsx (1)
toast(26-170)nextjs_space/components/ui/label.tsx (1)
Label(26-26)nextjs_space/components/ui/input.tsx (1)
Input(24-24)nextjs_space/components/ui/textarea.tsx (1)
Textarea(23-23)nextjs_space/components/ui/badge.tsx (1)
Badge(37-37)nextjs_space/components/ui/card.tsx (5)
Card(80-80)CardHeader(81-81)CardTitle(83-83)CardDescription(84-84)CardContent(85-85)nextjs_space/components/ui/button.tsx (1)
Button(57-57)
nextjs_space/app/tenant-admin/templates/activate-button.tsx (1)
nextjs_space/components/ui/sonner.tsx (1)
toast(26-170)
nextjs_space/app/tenant-admin/the-wire/page.tsx (3)
nextjs_space/app/tenant-admin/the-wire/new/page.tsx (1)
metadata(3-5)nextjs_space/components/admin/shared/Breadcrumbs.tsx (1)
Breadcrumbs(66-139)nextjs_space/app/tenant-admin/the-wire/posts-list.tsx (1)
PostsList(29-178)
nextjs_space/app/tenant-admin/branding/page.tsx (2)
nextjs_space/components/admin/shared/Breadcrumbs.tsx (1)
Breadcrumbs(66-139)nextjs_space/components/admin/shared/index.ts (1)
Breadcrumbs(28-28)
nextjs_space/app/tenant-admin/the-wire/post-form.tsx (7)
nextjs_space/components/ui/sonner.tsx (1)
toast(26-170)nextjs_space/components/ui/card.tsx (2)
Card(80-80)CardContent(85-85)nextjs_space/components/ui/label.tsx (1)
Label(26-26)nextjs_space/components/ui/input.tsx (1)
Input(24-24)nextjs_space/components/ui/textarea.tsx (1)
Textarea(23-23)nextjs_space/components/ui/button.tsx (1)
Button(57-57)nextjs_space/components/ui/switch.tsx (1)
Switch(29-29)
nextjs_space/app/tenant-admin/templates/page.tsx (10)
nextjs_space/lib/auth.ts (2)
session(64-71)authOptions(7-80)nextjs_space/lib/db.ts (1)
prisma(34-43)nextjs_space/components/admin/shared/Breadcrumbs.tsx (1)
Breadcrumbs(66-139)nextjs_space/components/admin/shared/index.ts (1)
Breadcrumbs(28-28)nextjs_space/components/ui/tabs.tsx (4)
Tabs(55-55)TabsList(55-55)TabsTrigger(55-55)TabsContent(55-55)nextjs_space/components/ui/card.tsx (6)
Card(80-80)CardHeader(81-81)CardTitle(83-83)CardDescription(84-84)CardContent(85-85)CardFooter(82-82)nextjs_space/components/ui/badge.tsx (1)
Badge(37-37)nextjs_space/components/ui/button.tsx (1)
Button(57-57)nextjs_space/app/tenant-admin/templates/activate-button.tsx (1)
ActivateButton(15-79)nextjs_space/app/tenant-admin/templates/clone-button.tsx (1)
TemplateCloneButton(13-78)
nextjs_space/app/tenant-admin/analytics/page.tsx (2)
nextjs_space/components/admin/shared/Breadcrumbs.tsx (1)
Breadcrumbs(66-139)nextjs_space/lib/utils.ts (1)
cn(4-6)
nextjs_space/app/tenant-admin/seo/seo-page-client.tsx (4)
nextjs_space/components/ui/badge.tsx (1)
Badge(37-37)nextjs_space/components/ui/tabs.tsx (4)
Tabs(55-55)TabsList(55-55)TabsTrigger(55-55)TabsContent(55-55)nextjs_space/components/ui/card.tsx (5)
Card(80-80)CardHeader(81-81)CardTitle(83-83)CardDescription(84-84)CardContent(85-85)nextjs_space/components/admin/seo/SeoEditorModal.tsx (1)
SeoEditorModal(37-192)
🪛 Biome (2.1.2)
nextjs_space/app/onboarding/onboarding-form.tsx
[error] 410-414: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Safe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
| <option value="PT">Portugal 🇵🇹</option> | ||
| <option value="SA">South Africa 🇿🇦</option> | ||
| <option value="UK">United Kingdom 🇬🇧</option> | ||
| <option value="DE">Germany 🇩🇪</option> | ||
| <option value="ES">Spain 🇪🇸</option> | ||
| <option value="FR">France 🇫🇷</option> | ||
| <option value="IT">Italy 🇮🇹</option> | ||
| <option value="NL">Netherlands 🇳🇱</option> | ||
| <option value="BE">Belgium 🇧🇪</option> | ||
| <option value="AT">Austria 🇦🇹</option> | ||
| <option value="IE">Ireland 🇮🇪</option> | ||
| <option value="CH">Switzerland 🇨🇭</option> | ||
| <option value="US">United States 🇺🇸</option> | ||
| <option value="CA">Canada 🇨🇦</option> | ||
| <option value="AU">Australia 🇦🇺</option> | ||
| </select> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "countryCode" --type=ts --type=tsx -g '!node_modules' -C3Repository: AutomatosAI/budstack-saas
Length of output: 96
🏁 Script executed:
rg -n "\"UK\"|\"GB\"" --type=ts --type=tsx -g '!node_modules'Repository: AutomatosAI/budstack-saas
Length of output: 96
🏁 Script executed:
rg -n "countries|country.*code" --type=ts --type=tsx -g '!node_modules' | head -50Repository: AutomatosAI/budstack-saas
Length of output: 96
🏁 Script executed:
rg "countryCode" -C3 | head -100Repository: AutomatosAI/budstack-saas
Length of output: 8401
🏁 Script executed:
rg "\"UK\"|\"GB\"" -C2 | head -100Repository: AutomatosAI/budstack-saas
Length of output: 4087
🏁 Script executed:
find . -name "onboarding-form.tsx" -type fRepository: AutomatosAI/budstack-saas
Length of output: 118
🏁 Script executed:
rg "formData|countryCode" --type ts -C3 | head -100Repository: AutomatosAI/budstack-saas
Length of output: 7363
Standardize United Kingdom country code to ISO 3166-1 alpha-2 format.
The form uses the non-standard code "UK" while every other country dropdown in the application uses the ISO standard code "GB" (see consultation-constants.ts, ClientOnboarding.tsx, and all consultation form steps). This inconsistency may cause issues with downstream code that expects standard codes. While the cookie compliance utilities explicitly accept both, the rest of the system standardizes on "GB".
Suggested fix
- <option value="UK">United Kingdom 🇬🇧</option>
+ <option value="GB">United Kingdom 🇬🇧</option>📝 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.
| <option value="PT">Portugal 🇵🇹</option> | |
| <option value="SA">South Africa 🇿🇦</option> | |
| <option value="UK">United Kingdom 🇬🇧</option> | |
| <option value="DE">Germany 🇩🇪</option> | |
| <option value="ES">Spain 🇪🇸</option> | |
| <option value="FR">France 🇫🇷</option> | |
| <option value="IT">Italy 🇮🇹</option> | |
| <option value="NL">Netherlands 🇳🇱</option> | |
| <option value="BE">Belgium 🇧🇪</option> | |
| <option value="AT">Austria 🇦🇹</option> | |
| <option value="IE">Ireland 🇮🇪</option> | |
| <option value="CH">Switzerland 🇨🇭</option> | |
| <option value="US">United States 🇺🇸</option> | |
| <option value="CA">Canada 🇨🇦</option> | |
| <option value="AU">Australia 🇦🇺</option> | |
| </select> | |
| <option value="PT">Portugal 🇵🇹</option> | |
| <option value="SA">South Africa 🇿🇦</option> | |
| <option value="GB">United Kingdom 🇬🇧</option> | |
| <option value="DE">Germany 🇩🇪</option> | |
| <option value="ES">Spain 🇪🇸</option> | |
| <option value="FR">France 🇫🇷</option> | |
| <option value="IT">Italy 🇮🇹</option> | |
| <option value="NL">Netherlands 🇳🇱</option> | |
| <option value="BE">Belgium 🇧🇪</option> | |
| <option value="AT">Austria 🇦🇹</option> | |
| <option value="IE">Ireland 🇮🇪</option> | |
| <option value="CH">Switzerland 🇨🇭</option> | |
| <option value="US">United States 🇺🇸</option> | |
| <option value="CA">Canada 🇨🇦</option> | |
| <option value="AU">Australia 🇦🇺</option> | |
| </select> |
🤖 Prompt for AI Agents
In @nextjs_space/app/onboarding/onboarding-form.tsx around lines 385 - 400, The
United Kingdom option in the country <select> in onboarding-form.tsx uses the
non-standard value "UK"; change its value to the ISO 3166-1 alpha-2 code "GB"
(i.e., update the <option value="UK">… to <option value="GB">…). After changing,
search this component for any comparisons, default values, or form state that
reference "UK" and update them to "GB" to keep behavior consistent with
consultation-constants.ts and other components.
| case 4: | ||
| const selectedTemplate = templates.find( | ||
| (t) => t.id === formData.templateId, | ||
| ); | ||
| return ( | ||
| <div className="space-y-6"> | ||
| <div> | ||
| <h3 className="text-lg font-semibold mb-4"> | ||
| Choose Your Store Template | ||
| </h3> | ||
| <div className="grid grid-cols-2 gap-4"> | ||
| {templates.map((template) => ( | ||
| <button | ||
| key={template.id} | ||
| type="button" | ||
| onClick={() => | ||
| setFormData({ ...formData, templateId: template.id }) | ||
| } | ||
| className={`p-4 border-2 rounded-lg text-left transition-all hover:shadow-lg ${ | ||
| formData.templateId === template.id | ||
| ? "border-primary bg-primary/5" | ||
| : "border-gray-200 hover:border-gray-300" | ||
| }`} | ||
| > | ||
| <div className="flex items-center gap-3 mb-3"> | ||
| {template.thumbnailUrl ? ( | ||
| <img | ||
| src={template.thumbnailUrl} | ||
| alt={template.name} | ||
| className="w-12 h-12 object-cover rounded-lg" | ||
| /> | ||
| ) : ( | ||
| <div className="w-12 h-12 rounded-lg bg-slate-100 flex items-center justify-center"> | ||
| <Palette className="h-6 w-6 text-slate-400" /> | ||
| </div> | ||
|
|
||
| {selectedTemplate && ( | ||
| <div className="p-4 bg-green-50 border border-green-200 rounded-lg"> | ||
| <h4 className="text-sm font-semibold text-green-900 mb-2 flex items-center gap-2"> | ||
| <Eye className="h-4 w-4" /> | ||
| Preview: {selectedTemplate.name} | ||
| </h4> | ||
| {selectedTemplate.previewUrl && ( | ||
| <div className="mt-2 aspect-video rounded-lg overflow-hidden border border-green-100"> | ||
| <img src={selectedTemplate.previewUrl} alt="Preview" className="w-full h-full object-cover" /> | ||
| </div> | ||
| )} | ||
| <p className="text-xs text-green-800 mt-3"> | ||
| You can customize everything later from your admin dashboard. | ||
| </p> | ||
| </div> | ||
| )} | ||
| <div className="flex-1"> | ||
| <div className="font-semibold">{template.name}</div> | ||
| {formData.templateId === template.id && ( | ||
| <Badge className="mt-1">Selected</Badge> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
| <p className="text-sm text-gray-600 line-clamp-2"> | ||
| {template.description} | ||
| </p> | ||
| </button> | ||
| ))} | ||
| </div> | ||
| </div> | ||
|
|
||
| default: | ||
| return null; | ||
| } | ||
| }; | ||
| {selectedTemplate && ( | ||
| <div className="p-4 bg-green-50 border border-green-200 rounded-lg"> | ||
| <h4 className="text-sm font-semibold text-green-900 mb-2 flex items-center gap-2"> | ||
| <Eye className="h-4 w-4" /> | ||
| Preview: {selectedTemplate.name} | ||
| </h4> | ||
| {selectedTemplate.previewUrl && ( | ||
| <div className="mt-2 aspect-video rounded-lg overflow-hidden border border-green-100"> | ||
| <img | ||
| src={selectedTemplate.previewUrl} | ||
| alt="Preview" | ||
| className="w-full h-full object-cover" | ||
| /> | ||
| </div> | ||
| )} | ||
| <p className="text-xs text-green-800 mt-3"> | ||
| You can customize everything later from your admin dashboard. | ||
| </p> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
|
|
||
| return ( | ||
| <div className="min-h-screen bg-gradient-to-b from-green-50 to-white py-12 px-4"> | ||
| <div className="max-w-3xl mx-auto"> | ||
| <div className="text-center mb-8"> | ||
| <h1 className="text-4xl font-bold text-gray-900 mb-2">Join BudStack</h1> | ||
| <p className="text-lg text-gray-600">Launch your medical cannabis dispensary in minutes</p> | ||
| </div> | ||
| default: | ||
| return null; | ||
| } | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Wrap case 4 in a block to scope the variable declaration.
The selectedTemplate variable declared on line 409 is flagged by static analysis (Biome) because declarations in switch cases without block scope can be accessed from other cases. While this is unlikely to cause issues here since each case returns, wrapping in a block is the correct pattern.
♻️ Suggested fix
case 4:
+ {
const selectedTemplate = templates.find(
(t) => t.id === formData.templateId,
);
return (
<div className="space-y-6">
// ... rest of the JSX
</div>
);
+ }📝 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.
| case 4: | |
| const selectedTemplate = templates.find( | |
| (t) => t.id === formData.templateId, | |
| ); | |
| return ( | |
| <div className="space-y-6"> | |
| <div> | |
| <h3 className="text-lg font-semibold mb-4"> | |
| Choose Your Store Template | |
| </h3> | |
| <div className="grid grid-cols-2 gap-4"> | |
| {templates.map((template) => ( | |
| <button | |
| key={template.id} | |
| type="button" | |
| onClick={() => | |
| setFormData({ ...formData, templateId: template.id }) | |
| } | |
| className={`p-4 border-2 rounded-lg text-left transition-all hover:shadow-lg ${ | |
| formData.templateId === template.id | |
| ? "border-primary bg-primary/5" | |
| : "border-gray-200 hover:border-gray-300" | |
| }`} | |
| > | |
| <div className="flex items-center gap-3 mb-3"> | |
| {template.thumbnailUrl ? ( | |
| <img | |
| src={template.thumbnailUrl} | |
| alt={template.name} | |
| className="w-12 h-12 object-cover rounded-lg" | |
| /> | |
| ) : ( | |
| <div className="w-12 h-12 rounded-lg bg-slate-100 flex items-center justify-center"> | |
| <Palette className="h-6 w-6 text-slate-400" /> | |
| </div> | |
| {selectedTemplate && ( | |
| <div className="p-4 bg-green-50 border border-green-200 rounded-lg"> | |
| <h4 className="text-sm font-semibold text-green-900 mb-2 flex items-center gap-2"> | |
| <Eye className="h-4 w-4" /> | |
| Preview: {selectedTemplate.name} | |
| </h4> | |
| {selectedTemplate.previewUrl && ( | |
| <div className="mt-2 aspect-video rounded-lg overflow-hidden border border-green-100"> | |
| <img src={selectedTemplate.previewUrl} alt="Preview" className="w-full h-full object-cover" /> | |
| </div> | |
| )} | |
| <p className="text-xs text-green-800 mt-3"> | |
| You can customize everything later from your admin dashboard. | |
| </p> | |
| </div> | |
| )} | |
| <div className="flex-1"> | |
| <div className="font-semibold">{template.name}</div> | |
| {formData.templateId === template.id && ( | |
| <Badge className="mt-1">Selected</Badge> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| <p className="text-sm text-gray-600 line-clamp-2"> | |
| {template.description} | |
| </p> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| default: | |
| return null; | |
| } | |
| }; | |
| {selectedTemplate && ( | |
| <div className="p-4 bg-green-50 border border-green-200 rounded-lg"> | |
| <h4 className="text-sm font-semibold text-green-900 mb-2 flex items-center gap-2"> | |
| <Eye className="h-4 w-4" /> | |
| Preview: {selectedTemplate.name} | |
| </h4> | |
| {selectedTemplate.previewUrl && ( | |
| <div className="mt-2 aspect-video rounded-lg overflow-hidden border border-green-100"> | |
| <img | |
| src={selectedTemplate.previewUrl} | |
| alt="Preview" | |
| className="w-full h-full object-cover" | |
| /> | |
| </div> | |
| )} | |
| <p className="text-xs text-green-800 mt-3"> | |
| You can customize everything later from your admin dashboard. | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| return ( | |
| <div className="min-h-screen bg-gradient-to-b from-green-50 to-white py-12 px-4"> | |
| <div className="max-w-3xl mx-auto"> | |
| <div className="text-center mb-8"> | |
| <h1 className="text-4xl font-bold text-gray-900 mb-2">Join BudStack</h1> | |
| <p className="text-lg text-gray-600">Launch your medical cannabis dispensary in minutes</p> | |
| </div> | |
| default: | |
| return null; | |
| } | |
| }; | |
| case 4: | |
| { | |
| const selectedTemplate = templates.find( | |
| (t) => t.id === formData.templateId, | |
| ); | |
| return ( | |
| <div className="space-y-6"> | |
| <div> | |
| <h3 className="text-lg font-semibold mb-4"> | |
| Choose Your Store Template | |
| </h3> | |
| <div className="grid grid-cols-2 gap-4"> | |
| {templates.map((template) => ( | |
| <button | |
| key={template.id} | |
| type="button" | |
| onClick={() => | |
| setFormData({ ...formData, templateId: template.id }) | |
| } | |
| className={`p-4 border-2 rounded-lg text-left transition-all hover:shadow-lg ${ | |
| formData.templateId === template.id | |
| ? "border-primary bg-primary/5" | |
| : "border-gray-200 hover:border-gray-300" | |
| }`} | |
| > | |
| <div className="flex items-center gap-3 mb-3"> | |
| {template.thumbnailUrl ? ( | |
| <img | |
| src={template.thumbnailUrl} | |
| alt={template.name} | |
| className="w-12 h-12 object-cover rounded-lg" | |
| /> | |
| ) : ( | |
| <div className="w-12 h-12 rounded-lg bg-slate-100 flex items-center justify-center"> | |
| <Palette className="h-6 w-6 text-slate-400" /> | |
| </div> | |
| )} | |
| <div className="flex-1"> | |
| <div className="font-semibold">{template.name}</div> | |
| {formData.templateId === template.id && ( | |
| <Badge className="mt-1">Selected</Badge> | |
| )} | |
| </div> | |
| </div> | |
| <p className="text-sm text-gray-600 line-clamp-2"> | |
| {template.description} | |
| </p> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| {selectedTemplate && ( | |
| <div className="p-4 bg-green-50 border border-green-200 rounded-lg"> | |
| <h4 className="text-sm font-semibold text-green-900 mb-2 flex items-center gap-2"> | |
| <Eye className="h-4 w-4" /> | |
| Preview: {selectedTemplate.name} | |
| </h4> | |
| {selectedTemplate.previewUrl && ( | |
| <div className="mt-2 aspect-video rounded-lg overflow-hidden border border-green-100"> | |
| <img | |
| src={selectedTemplate.previewUrl} | |
| alt="Preview" | |
| className="w-full h-full object-cover" | |
| /> | |
| </div> | |
| )} | |
| <p className="text-xs text-green-800 mt-3"> | |
| You can customize everything later from your admin dashboard. | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| default: | |
| return null; | |
| } | |
| }; |
🧰 Tools
🪛 Biome (2.1.2)
[error] 410-414: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Safe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
🤖 Prompt for AI Agents
In @nextjs_space/app/onboarding/onboarding-form.tsx around lines 408 - 485, The
variable selectedTemplate declared in case 4 of the switch is not block-scoped
and should be confined to that case; wrap the entire case 4 body in a block
(i.e., add { ... } immediately after case 4) so the const selectedTemplate =
templates.find(...) and the JSX return are inside that block, preventing the
declaration from leaking into other cases; keep the return as-is inside the new
block.
| import { getServerSession } from "next-auth"; | ||
| import { redirect } from "next/navigation"; | ||
| import { prisma } from "@/lib/db"; | ||
| import BrandingForm from "./branding-form"; | ||
| import { Breadcrumbs } from "@/components/admin/shared"; | ||
|
|
||
| export default async function BrandingPage() { | ||
| const session = await getServerSession(); |
There was a problem hiding this comment.
Missing authOptions in getServerSession() call will break session data access.
The getServerSession() call on line 8 is missing the authOptions parameter. Without it, next-auth won't populate custom session fields like id, tenantId, or role that are configured in your auth callbacks. This will cause session.user.id on line 15 to be undefined, leading to a failed Prisma query.
The customer detail page in this same PR correctly passes authOptions:
const session = await getServerSession(authOptions);🐛 Proposed fix
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
+import { authOptions } from "@/lib/auth";
import { prisma } from "@/lib/db";
import BrandingForm from "./branding-form";
import { Breadcrumbs } from "@/components/admin/shared";
export default async function BrandingPage() {
- const session = await getServerSession();
+ const session = await getServerSession(authOptions);🤖 Prompt for AI Agents
In @nextjs_space/app/tenant-admin/branding/page.tsx around lines 1 - 8, The call
to getServerSession() inside BrandingPage is missing the authOptions parameter
so custom session fields (e.g., session.user.id, tenantId, role) are not
populated; update the call to getServerSession(authOptions) using the same
authOptions export used elsewhere (import or reference the existing authOptions)
so session.user.id is defined before using it in the Prisma query and downstream
logic in BrandingPage and BrandingForm.
| if (!session?.user?.email) { | ||
| redirect('/auth/login'); | ||
| redirect("/auth/login"); | ||
| } |
There was a problem hiding this comment.
Missing role-based authorization - any authenticated user can access this page.
This page only checks that the user is logged in, but doesn't verify they have appropriate roles (TENANT_ADMIN or SUPER_ADMIN). The customer detail page in this PR implements proper role checks:
if (!session || !["TENANT_ADMIN", "SUPER_ADMIN"].includes(session.user.role!)) {
redirect("/auth/login");
}Consider adding the same role-based guard here for consistency and security.
🔒 Proposed fix
- if (!session?.user?.email) {
+ if (
+ !session ||
+ !["TENANT_ADMIN", "SUPER_ADMIN"].includes(session.user.role!)
+ ) {
redirect("/auth/login");
}🤖 Prompt for AI Agents
In @nextjs_space/app/tenant-admin/branding/page.tsx around lines 10 - 12, The
page currently only checks for session?.user?.email and allows any authenticated
user; update the guard to enforce role-based authorization by verifying session
exists and that session.user.role is one of "TENANT_ADMIN" or "SUPER_ADMIN"
(mirroring the customer detail check) and call redirect("/auth/login") when the
check fails; locate the existing session usage and redirect call in page.tsx and
replace the simple email check with the role-inclusive condition using
session.user.role.
| const templatess = await prisma.templates.findMany({ | ||
| where: { isActive: true, isPublic: true }, | ||
| }); |
There was a problem hiding this comment.
Typo in variable name: templatess.
The variable templatess appears to be a typo (double "s"). Consider renaming to baseTemplates or marketplaceTemplates for clarity and to avoid confusion.
🔧 Suggested fix
// 2. Fetch Available Base Templates (Marketplace)
- const templatess = await prisma.templates.findMany({
+ const baseTemplates = await prisma.templates.findMany({
where: { isActive: true, isPublic: true },
});Then update references at lines 168 and 227:
- {templatess.map((template: templates) => (
+ {baseTemplates.map((template: templates) => (- {templatess.length === 0 && (
+ {baseTemplates.length === 0 && (📝 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.
| const templatess = await prisma.templates.findMany({ | |
| where: { isActive: true, isPublic: true }, | |
| }); | |
| const baseTemplates = await prisma.templates.findMany({ | |
| where: { isActive: true, isPublic: true }, | |
| }); |
🤖 Prompt for AI Agents
In @nextjs_space/app/tenant-admin/templates/page.tsx around lines 63 - 65,
Rename the misspelled variable templatess to a clearer name like baseTemplates
(or marketplaceTemplates) in the template listing statement and update all its
usages (e.g., where templatess is referenced for rendering or logic) to the new
identifier so references at other parts of the file compile and reflect the new
name.
| const posts = await prisma.posts.findMany({ | ||
| where: { tenantId: user.tenants.id }, | ||
| orderBy: { createdAt: "desc" }, | ||
| include: { users: true }, | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd "schema.prisma" --exec cat {} \;Repository: AutomatosAI/budstack-saas
Length of output: 18642
Fix the mismatch between query include and component access: relation field is users, not author.
The schema defines the relation field as users (not author). The query correctly includes { users: true }, but if the PostsList component accesses post.author?.name, it will fail at runtime since that property doesn't exist. The component must access post.users?.name instead, or the schema relation field should be renamed to author.
🤖 Prompt for AI Agents
In @nextjs_space/app/tenant-admin/the-wire/page.tsx around lines 31 - 35, The
query uses prisma.posts.findMany including the relation as users (include: {
users: true }) but the consumer component PostsList is accessing post.author
which doesn't exist; update the PostsList rendering to use post.users (e.g.,
post.users?.name) wherever post.author is referenced, or alternatively rename
the relation consistently (either change include to author or rename schema
relation) so the relation symbol matches between prisma.posts.findMany and
PostsList.
| const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); | ||
| const [editingWebhook, setEditingWebhook] = useState<WebhookData | null>(null); | ||
| const [editingWebhook, setEditingWebhook] = useState<WebhookData | null>( | ||
| null, | ||
| ); |
There was a problem hiding this comment.
Remove unused edit dialog state or implement edit functionality.
The isEditDialogOpen and editingWebhook state variables are initialized but never used. There's no UI or handler to edit existing webhooks, only create and delete operations.
🧹 Proposed fix: Remove unused state
const [loading, setLoading] = useState(true);
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
- const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
- const [editingWebhook, setEditingWebhook] = useState<WebhookData | null>(
- null,
- );Alternatively, if edit functionality is planned, implement the edit dialog and wire up the state properly.
📝 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.
| const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); | |
| const [editingWebhook, setEditingWebhook] = useState<WebhookData | null>(null); | |
| const [editingWebhook, setEditingWebhook] = useState<WebhookData | null>( | |
| null, | |
| ); |
🤖 Prompt for AI Agents
In @nextjs_space/app/tenant-admin/webhooks/page.tsx around lines 60 - 63, The
two unused state vars isEditDialogOpen/setIsEditDialogOpen and
editingWebhook/setEditingWebhook are declared but never referenced; either
remove these state declarations from the component or implement the edit flow:
add an "Edit" button that calls setEditingWebhook(webhook) and
setIsEditDialogOpen(true), render an EditWebhookDialog that reads editingWebhook
and calls a save handler to update the webhook list, and close the dialog by
calling setIsEditDialogOpen(false) and clearing setEditingWebhook(null). Ensure
all references (isEditDialogOpen, setIsEditDialogOpen, editingWebhook,
setEditingWebhook) are either removed or properly wired into the UI and
handlers.
Summary by CodeRabbit
Release Notes
New Features
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.