Skip to content

Add asset demand forecasting dashboard with API utilities#47

Merged
Ross1116 merged 6 commits into
mainfrom
staging
Mar 10, 2026
Merged

Add asset demand forecasting dashboard with API utilities#47
Ross1116 merged 6 commits into
mainfrom
staging

Conversation

@Ross1116

@Ross1116 Ross1116 commented Mar 10, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Interactive Lookahead dashboard for asset-demand forecasting: project selector, window presets, upload workflow with status polling, alerts, version history, and rich visualisations
    • Booking demo modal with validated form, provider, and submission flow
  • Updates & Improvements

    • New Lookahead navigation item and landing-page CTA integrations
    • Landing page: "How It Works" section, refreshed feature styling, improved lazy-load placeholders
    • Scroll animations now handle dynamically added content

@vercel

vercel Bot commented Mar 10, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
sitespace-app Ready Ready Preview, Comment Mar 10, 2026 7:22am

@coderabbitai

coderabbitai Bot commented Mar 10, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds a new Lookahead forecasting feature: a client-side LookaheadDashboard with upload/polling/version history, SWR hooks and API helpers, lookahead UI state persistence and types, a server /api/contact POST route, landing-page demo modal/CTA integration, and small navigation/ui adjustments.

Changes

Cohort / File(s) Summary
Lookahead UI & Page
src/app/(dashboard)/lookahead/_components/LookaheadDashboard.tsx, src/app/(dashboard)/lookahead/page.tsx
New LookaheadDashboard page component implementing project selector, upload state machine with polling, snapshot/alerts/versions workflows, heatmap/forecast rendering, alerts, version history with delete flow, skeletons and responsive UI.
Lookahead Hooks & Keys
src/hooks/lookahead/api.ts, src/hooks/lookahead/keys.ts, src/hooks/lookahead/useLookaheadData.ts
New API helpers (fetch/upload/delete/status), URL key builders, and SWR hooks for snapshot, alerts, and programme versions with memoized keys and mutate wrappers.
Types
src/types/index.ts
Adds Lookahead/programme typings and response shapes (ProgrammeUploadStatus, DemandLevel, LookaheadRow, LookaheadSnapshotResponse, LookaheadAlertsResponse, ProgrammeVersion, UploadStatusResponse, UploadAcceptedResponse).
UI Intent Store
src/stores/uiIntentStore.ts
Extends persisted UI intent store with lookahead-scoped state: LookaheadWindowSize type, lookaheadByScope & access tracking, migration/partialize integration, and get/set APIs for window size.
Upload & Server Contact
src/hooks/lookahead/api.ts, src/app/api/contact/route.ts
Upload helper posts FormData to proxied route and supports polling for status; new server POST /api/contact with Zod validation, Mailtrap integration, timeout handling, and sandbox/credential branching.
Landing & Modal Integration
src/components/landing/ContactModal.tsx, src/components/landing/ClientDynamics.tsx, src/components/landing/LandingPage.tsx, src/components/landing/TopBar.tsx, src/components/landing/ShowcaseSection.tsx, src/components/landing/ScrollAnimations.tsx
Adds DemoModalProvider and DemoRequestCTA, replaces static demo buttons with CTA, adds dynamic-import loading placeholders, FeatureBlurb API changes (icon/accent/title type), MutationObserver for fade-in nodes, and minor styling tweaks.
Navigation
src/components/SideNav.tsx
Adds Lookahead top navigation item with BarChart3 icon visible to admin/manager roles.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/Client
    participant Dashboard as LookaheadDashboard
    participant Proxy as Upload Proxy (/api/proxy)
    participant Poller as Polling Loop
    participant Server as Backend API
    participant Store as UI Intent Store

    User->>Dashboard: select project & upload file
    Dashboard->>Proxy: POST FormData (returns uploadId)
    Dashboard->>Dashboard: set state -> uploading, start polling
    loop polling
        Poller->>Server: GET /programmes/{uploadId}/status
        Server-->>Poller: UploadStatusResponse
        alt status == "committed"
            Poller->>Server: GET snapshot/alerts/versions
            Server-->>Dashboard: snapshot, alerts, versions
            Dashboard->>Store: persist window size / update intent
            Poller->>Dashboard: stop polling (done)
        else status == "processing"
            Poller-->>Poller: continue polling
        else status == "degraded" or error
            Poller->>Dashboard: set error state, stop polling
        end
    end
    Dashboard->>User: render heatmap, stats, versions, alerts
Loading
sequenceDiagram
    participant User as User/Client
    participant Modal as ContactModal (client)
    participant Validator as Zod Validator
    participant API as /api/contact (server)
    participant Mailtrap as Mailtrap API

    User->>Modal: open -> fill -> submit
    Modal->>Validator: validate payload
    alt validation fails
        Validator-->>Modal: field errors
        Modal->>User: show errors
    else validation passes
        Modal->>API: POST contact payload
        API->>API: validate & build email HTML
        alt missing prod config or sandbox without inbox
            API-->>Modal: { ok: true } (no external send)
        else
            API->>Mailtrap: POST email (with timeout)
            alt 2xx
                Mailtrap-->>API: success
                API-->>Modal: { ok: true }
                Modal->>User: show success
            else
                Mailtrap-->>API: non-OK
                API-->>Modal: 500 error
                Modal->>User: show server error
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I nibble data rows at dawn's first light,
I poll and wait until the forecasts bite.
Heatmaps hum, versions line the trail—
A hop, a sniff, the dashboard tells the tale. ✨📈

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly summarizes the main change: adding a comprehensive asset demand forecasting dashboard with supporting API utilities.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch staging

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.

@coderabbitai coderabbitai Bot changed the title @coderabbitai Add asset demand forecasting dashboard with API utilities Mar 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (3)
src/components/landing/ContactModal.tsx (1)

328-344: Fail fast when DemoRequestCTA is missing its provider.

Because DemoModalCtx defaults to null and Line 340 uses optional chaining, this exported CTA silently renders a dead button when it is used outside DemoModalProvider. That kind of wiring mistake is much easier to catch if the component throws or at least warns immediately.

🧭 Suggested fix
 export function DemoRequestCTA({
   label = "Book a Demo",
   className,
 }: {
   label?: string;
   className?: string;
 }) {
   const ctx = useContext(DemoModalCtx);
+  if (!ctx) {
+    throw new Error("DemoRequestCTA must be used within DemoModalProvider");
+  }
+
   return (
     <button
       type="button"
       className={className}
-      onClick={() => ctx?.openModal()}
+      onClick={() => ctx.openModal()}
     >
       {label}
     </button>
   );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/landing/ContactModal.tsx` around lines 328 - 344,
DemoRequestCTA silently no-ops when used outside its provider; update the
component (DemoRequestCTA) to fail fast by checking the value from
useContext(DemoModalCtx) and throwing or logging a clear error if it's
null/undefined (e.g., "DemoRequestCTA must be used within DemoModalProvider"),
and replace the optional chaining onClick (ctx?.openModal()) with a direct call
(ctx.openModal()) after the null check so the component either works or
immediately surfaces the missing DemoModalProvider.
src/components/landing/LandingPage.tsx (1)

45-49: Minor JSX indentation inconsistency.

The <div> on line 46 should be indented inside <DemoModalProvider> for better readability. This is purely cosmetic but improves code clarity.

     <DemoModalProvider>
-    <div
+      <div
       id="landing-root"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/landing/LandingPage.tsx` around lines 45 - 49, Adjust JSX
indentation so the <div id="landing-root"> is indented as a child of
<DemoModalProvider> for readability; locate the opening <DemoModalProvider> in
LandingPage.tsx and re-indent the nested <div id="landing-root" ...> (and its
closing tag) to be visually nested under DemoModalProvider.
src/hooks/lookahead/api.ts (1)

47-49: Consider using a key builder for consistency.

deleteProgrammeVersion hardcodes the path /programmes/${uploadId} while other functions use lookaheadKeys.*. If lookaheadKeys doesn't have a delete path, consider adding one for consistency and centralized URL management.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/lookahead/api.ts` around lines 47 - 49, deleteProgrammeVersion
currently hardcodes the endpoint string `/programmes/${uploadId}` instead of
using the shared key builder; add a delete route in the lookaheadKeys key
builder (e.g., a method or property like lookaheadKeys.deleteProgramme or
lookaheadKeys.programme.delete) that accepts uploadId and returns the path, then
update deleteProgrammeVersion to call
api.delete(lookaheadKeys.deleteProgramme(uploadId)) so all lookahead endpoints
are centralized and consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`(dashboard)/lookahead/_components/LookaheadDashboard.tsx:
- Around line 222-243: The delete handler currently only logs errors in
handleDeleteVersion when deleteProgrammeVersion fails; update the catch block to
surface the error to the user (e.g., show a toast or set an inline error state)
using the error message from getApiErrorMessage(err), while keeping the existing
console.error and finally logic; ensure the UI is updated consistently by
invoking the existing mutation functions (mutateVersions, mutateSnapshot,
mutateAlerts) or updating setUploadPhase as appropriate after showing the error
so the user sees failure feedback tied to the failing uploadId.
- Around line 196-219: The startPolling implementation can run forever if status
stays "processing"; modify startPolling (and related pollingRef/stopPolling) to
enforce a max retry count or timeout: add a counter (e.g., attempts) or deadline
(startTime + POLL_TIMEOUT_MS) inside the setInterval callback and
increment/check it each tick, and if exceeded call stopPolling(), setUploadPhase
to an error/timeout kind, and perform the same
mutateSnapshot/mutateAlerts/mutateVersions flow as appropriate; make
POLL_MAX_ATTEMPTS or POLL_TIMEOUT_MS constants (or props) so limits are
configurable and ensure stopPolling clears pollingRef and any timers to avoid
leaks.

In `@src/app/api/contact/route.ts`:
- Around line 71-79: The handler currently returns { ok: true } and logs the
full submission when email delivery is skipped; change this so skipped-delivery
only returns a non-success or explicit "skipped" response in non-production
(check process.env.NODE_ENV !== 'production' or use a dedicated env like
NEXT_PUBLIC_ALLOW_NO_MAIL) and do not acknowledge success to callers in
production. Replace console.info/console.warn logging of the raw data with a
non-PII debug log (e.g., log only that delivery was skipped and minimal
identifiers), and gate the demo/noop branch behind a dev/sandbox flag so
functions/variables apiToken, toEmail, useSandbox, inboxId and the
NextResponse.json call reflect a failure/skipped response in production rather
than { ok: true }.
- Around line 92-112: The Mailtrap fetch can reject or hang causing the route to
throw or timeout; wrap the outbound fetch(url, { ... }) call in a try/catch and
use an AbortController with a short timeout (e.g., a few seconds) to abort long
requests, then handle non-ok responses as you already do. Specifically, create
an AbortController, pass controller.signal to fetch, set a timer that calls
controller.abort(), await the fetch inside try, and on any catch (including
abort) log the error (console.error with the thrown error) and return the same
NextResponse.json({ error: "Failed to send email. Please try again." }, {
status: 500 }). Keep the existing handling for mailtrapRes.ok and parsing
errBody when responses come back.
- Around line 14-45: The buildEmailHtml function currently interpolates raw user
input into the email markup; update it to HTML-escape every dynamic value before
constructing tableRows (e.g., create or use a helper like escapeHtml and apply
it to data.name, data.email, data.company, data.role, data.phone,
data.projectSize, data.message while preserving the existing nullish "—"
fallbacks) so that tableRows uses escaped values and prevents injection/phishing
risks.

In `@src/components/landing/ContactModal.tsx`:
- Around line 291-297: The dialog is rendered with z-50 (Dialog.Portal /
Dialog.Overlay / Dialog.Content) which puts it behind the landing mobile drawer
that uses z-[98]/z-[99]; update the modal stacking to a higher z-index so it
appears above the mobile drawer (for example change z-50 to z-[100] or another
value >99) in the Dialog.Overlay and Dialog.Content className strings to ensure
the modal is on top on mobile.

---

Nitpick comments:
In `@src/components/landing/ContactModal.tsx`:
- Around line 328-344: DemoRequestCTA silently no-ops when used outside its
provider; update the component (DemoRequestCTA) to fail fast by checking the
value from useContext(DemoModalCtx) and throwing or logging a clear error if
it's null/undefined (e.g., "DemoRequestCTA must be used within
DemoModalProvider"), and replace the optional chaining onClick
(ctx?.openModal()) with a direct call (ctx.openModal()) after the null check so
the component either works or immediately surfaces the missing
DemoModalProvider.

In `@src/components/landing/LandingPage.tsx`:
- Around line 45-49: Adjust JSX indentation so the <div id="landing-root"> is
indented as a child of <DemoModalProvider> for readability; locate the opening
<DemoModalProvider> in LandingPage.tsx and re-indent the nested <div
id="landing-root" ...> (and its closing tag) to be visually nested under
DemoModalProvider.

In `@src/hooks/lookahead/api.ts`:
- Around line 47-49: deleteProgrammeVersion currently hardcodes the endpoint
string `/programmes/${uploadId}` instead of using the shared key builder; add a
delete route in the lookaheadKeys key builder (e.g., a method or property like
lookaheadKeys.deleteProgramme or lookaheadKeys.programme.delete) that accepts
uploadId and returns the path, then update deleteProgrammeVersion to call
api.delete(lookaheadKeys.deleteProgramme(uploadId)) so all lookahead endpoints
are centralized and consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 07e5e887-2eb4-4fe2-9b05-94f0e0dd396b

📥 Commits

Reviewing files that changed from the base of the PR and between cf87de7 and 8827fab.

📒 Files selected for processing (15)
  • src/app/(dashboard)/lookahead/_components/LookaheadDashboard.tsx
  • src/app/(dashboard)/lookahead/page.tsx
  • src/app/api/contact/route.ts
  • src/components/SideNav.tsx
  • src/components/landing/ClientDynamics.tsx
  • src/components/landing/ContactModal.tsx
  • src/components/landing/LandingPage.tsx
  • src/components/landing/ScrollAnimations.tsx
  • src/components/landing/ShowcaseSection.tsx
  • src/components/landing/TopBar.tsx
  • src/hooks/lookahead/api.ts
  • src/hooks/lookahead/keys.ts
  • src/hooks/lookahead/useLookaheadData.ts
  • src/stores/uiIntentStore.ts
  • src/types/index.ts

Comment thread src/app/(dashboard)/lookahead/_components/LookaheadDashboard.tsx
Comment thread src/app/(dashboard)/lookahead/_components/LookaheadDashboard.tsx
Comment thread src/app/api/contact/route.ts
Comment on lines +71 to +79
if (!apiToken || !toEmail) {
console.info("[contact] Demo request received (no Mailtrap token configured):", data);
return NextResponse.json({ ok: true });
}

if (useSandbox && !inboxId) {
console.warn("[contact] MAILTRAP_USE_SANDBOX=true but MAILTRAP_INBOX_ID not set");
return NextResponse.json({ ok: true });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't return success when the email was never sent.

The branches on Lines 71-79 acknowledge the request with { ok: true } even though delivery is skipped, so the UI shows success while the lead is silently dropped. Line 72 also logs the full submission object, which includes PII. At minimum, gate this noop behavior to non-production and stop logging the raw payload.

🚨 Suggested fix
+  const allowNoop = process.env.NODE_ENV !== "production";
+
   if (!apiToken || !toEmail) {
-    console.info("[contact] Demo request received (no Mailtrap token configured):", data);
-    return NextResponse.json({ ok: true });
+    console.error("[contact] Mail delivery is not configured");
+    return NextResponse.json(
+      allowNoop
+        ? { ok: true }
+        : { error: "Demo requests are temporarily unavailable. Please try again later." },
+      { status: allowNoop ? 200 : 503 },
+    );
   }
 
   if (useSandbox && !inboxId) {
-    console.warn("[contact] MAILTRAP_USE_SANDBOX=true but MAILTRAP_INBOX_ID not set");
-    return NextResponse.json({ ok: true });
+    console.error("[contact] Mailtrap sandbox is enabled but inbox ID is missing");
+    return NextResponse.json(
+      allowNoop
+        ? { ok: true }
+        : { error: "Demo requests are temporarily unavailable. Please try again later." },
+      { status: allowNoop ? 200 : 503 },
+    );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/contact/route.ts` around lines 71 - 79, The handler currently
returns { ok: true } and logs the full submission when email delivery is
skipped; change this so skipped-delivery only returns a non-success or explicit
"skipped" response in non-production (check process.env.NODE_ENV !==
'production' or use a dedicated env like NEXT_PUBLIC_ALLOW_NO_MAIL) and do not
acknowledge success to callers in production. Replace console.info/console.warn
logging of the raw data with a non-PII debug log (e.g., log only that delivery
was skipped and minimal identifiers), and gate the demo/noop branch behind a
dev/sandbox flag so functions/variables apiToken, toEmail, useSandbox, inboxId
and the NextResponse.json call reflect a failure/skipped response in production
rather than { ok: true }.

Comment thread src/app/api/contact/route.ts Outdated
Comment thread src/components/landing/ContactModal.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`(dashboard)/lookahead/_components/LookaheadDashboard.tsx:
- Line 131: The dismissedAlerts Set (state variable dismissedAlerts and updater
setDismissedAlerts) currently persists across uploads and hides fresh anomalies;
reset it whenever a new upload arrives by clearing dismissedAlerts when
latestVersion?.upload_id changes (e.g., add a useEffect that calls
setDismissedAlerts(new Set()) with [latestVersion?.upload_id] as dependency), or
alternatively include upload_id in the keys stored in dismissedAlerts so
dismissal is scoped per-upload; update the logic that reads dismissedAlerts
accordingly (references: dismissedAlerts, setDismissedAlerts, latestVersion).
- Around line 189-194: The polling cleanup currently only clears the interval in
stopPolling(), but in-flight async calls from startPolling() (e.g.,
fetchUploadStatus()) can still resolve after handleProjectSelect() switches
projects and then call setUploadPhase()/mutate*() for the wrong project; fix
this by adding a cancellation mechanism: store an AbortController (or a
per-polling "generation" id) in a ref (e.g., pollingAbortRef or
pollingGenerationRef) when startPolling() begins each cycle, pass its signal (or
capture the generation) into fetchUploadStatus(), and in stopPolling() /
handleProjectSelect() call abort() (or advance the generation) so any resolved
promises ignore results for stale projects before calling setUploadPhase(),
mutateUploads(), mutateArtifacts(), etc.; update fetchUploadStatus() and any
callers to accept and respect the abort signal or generation check.
- Around line 163-169: The effect only sets windowSizeLocal when
useUIIntentStore.getState().getLookaheadIntent(uiScopeKey) returns a
persisted.windowSize, so switching to a scope with no saved intent leaves the
previous window active; update the effect (which depends on hasUIIntentHydrated
and uiScopeKey) to explicitly reset windowSizeLocal when persisted is missing
(e.g., call setWindowSizeLocal to the component's default/initial value or
undefined) so the UI falls back to the default when
getLookaheadIntent(uiScopeKey) returns nothing.

In `@src/components/landing/ContactModal.tsx`:
- Around line 293-297: The transition is targeting "scale" which doesn't animate
because Tailwind sets scale via the transform property; in Dialog.Content update
the inline style transition string (the one currently containing "scale 300ms
cubic-bezier(0.16,1,0.3,1)") to animate "transform" instead (e.g., "transform
300ms ...") so the data-[state=open]/data-[state=closed] scale classes smoothly
interpolate; refer to the Dialog.Content element and its inline style and the
data-[state=...] scale classes when making the change.
- Around line 41-67: The Field component renders a label and error but doesn't
associate them with the actual control; update Field to accept an id (string)
prop and use it to set label's htmlFor and to generate an error id (e.g.,
`${id}-error`) that is applied to the error <p> as its id; then ensure callers
give the inner input/select/textarea that same id and set aria-invalid={!!error}
and aria-describedby={error ? `${id}-error` : undefined}. Reference the Field
component and the error element id pattern when making the changes so labels and
errors are properly linked to their controls.
- Around line 77-80: The fields state is initialized empty which makes required
keys undefined and causes Zod to emit generic type errors; update initialization
or normalization so required ContactFields keys start as empty strings (e.g.,
initialize useState<Partial<ContactFields>> with { name: "", email: "", message:
"" }) or, alternatively, before calling zodSchema.safeParse, map any
missing/undefined ContactFields keys on the current fields object to "" so the
schema runs your custom validations and returns user-friendly messages; adjust
any related form handlers that assume empty->undefined (references: fields,
setFields, ContactFields, errors, setErrors, and the safeParse call).

In `@src/hooks/lookahead/api.ts`:
- Around line 73-79: The error handling when response.ok is false only inspects
payload.detail and falls back to a generic message; change it to prefer
payload.message (if a string) first, then payload.detail (if a string), and only
then the generic "Upload failed. Please try again." Update the branch that
builds detail (around the response.json() result and the const detail assignment
in src/hooks/lookahead/api.ts) so it checks typeof payload?.message === "string"
? payload.message : typeof payload?.detail === "string" ? payload.detail :
"Upload failed. Please try again.", then throw the Error(detail).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 46966eda-06db-40a9-b81a-f340f04be115

📥 Commits

Reviewing files that changed from the base of the PR and between 8827fab and 1853a36.

📒 Files selected for processing (5)
  • src/app/(dashboard)/lookahead/_components/LookaheadDashboard.tsx
  • src/app/api/contact/route.ts
  • src/components/landing/ContactModal.tsx
  • src/hooks/lookahead/api.ts
  • src/hooks/lookahead/keys.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/hooks/lookahead/keys.ts
  • src/app/api/contact/route.ts

Comment thread src/app/(dashboard)/lookahead/_components/LookaheadDashboard.tsx
Comment thread src/app/(dashboard)/lookahead/_components/LookaheadDashboard.tsx
Comment thread src/app/(dashboard)/lookahead/_components/LookaheadDashboard.tsx
Comment thread src/components/landing/ContactModal.tsx
Comment thread src/components/landing/ContactModal.tsx Outdated
Comment on lines +293 to +297
<Dialog.Content
className="fixed left-1/2 top-1/2 z-[100] w-full max-w-2xl -translate-x-1/2 -translate-y-1/2 rounded-2xl border border-white/10 bg-[#0a0a14] p-6 shadow-2xl max-h-[90vh] overflow-y-auto data-[state=closed]:opacity-0 data-[state=open]:opacity-100 data-[state=open]:scale-100 data-[state=closed]:scale-[0.93]"
style={{
transition: "opacity 220ms ease, scale 300ms cubic-bezier(0.16,1,0.3,1)",
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Animate transform, not scale.

The open/closed classes here change Tailwind's transform output, so transition: "... scale 300ms ..." will not animate the zoom. The dialog will snap between scales.

🎞️ Proposed fix
             style={{
-              transition: "opacity 220ms ease, scale 300ms cubic-bezier(0.16,1,0.3,1)",
+              transition: "opacity 220ms ease, transform 300ms cubic-bezier(0.16,1,0.3,1)",
             }}
📝 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
<Dialog.Content
className="fixed left-1/2 top-1/2 z-[100] w-full max-w-2xl -translate-x-1/2 -translate-y-1/2 rounded-2xl border border-white/10 bg-[#0a0a14] p-6 shadow-2xl max-h-[90vh] overflow-y-auto data-[state=closed]:opacity-0 data-[state=open]:opacity-100 data-[state=open]:scale-100 data-[state=closed]:scale-[0.93]"
style={{
transition: "opacity 220ms ease, scale 300ms cubic-bezier(0.16,1,0.3,1)",
}}
<Dialog.Content
className="fixed left-1/2 top-1/2 z-[100] w-full max-w-2xl -translate-x-1/2 -translate-y-1/2 rounded-2xl border border-white/10 bg-[`#0a0a14`] p-6 shadow-2xl max-h-[90vh] overflow-y-auto data-[state=closed]:opacity-0 data-[state=open]:opacity-100 data-[state=open]:scale-100 data-[state=closed]:scale-[0.93]"
style={{
transition: "opacity 220ms ease, transform 300ms cubic-bezier(0.16,1,0.3,1)",
}}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/landing/ContactModal.tsx` around lines 293 - 297, The
transition is targeting "scale" which doesn't animate because Tailwind sets
scale via the transform property; in Dialog.Content update the inline style
transition string (the one currently containing "scale 300ms
cubic-bezier(0.16,1,0.3,1)") to animate "transform" instead (e.g., "transform
300ms ...") so the data-[state=open]/data-[state=closed] scale classes smoothly
interpolate; refer to the Dialog.Content element and its inline style and the
data-[state=...] scale classes when making the change.

Comment thread src/hooks/lookahead/api.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/app/(dashboard)/lookahead/_components/LookaheadDashboard.tsx (1)

277-284: ⚠️ Potential issue | 🟡 Minor

Stop active polling when switching projects.

While the generation-based invalidation correctly prevents stale state updates, handleProjectSelect doesn't call stopPolling(). If polling is active when switching projects, the interval continues making API calls for the old project until timeout, which is wasteful and could show a confusing timeout error.

🛡️ Proposed fix
 const handleProjectSelect = (proj: ApiProject) => {
   if (!proj?.id) return;
+  stopPolling();
   setShowProjectSelector(false);
   setProjectId(proj.id);
   setUploadPhase({ kind: "idle" });
   setDismissedAlerts(new Set());
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(dashboard)/lookahead/_components/LookaheadDashboard.tsx around
lines 277 - 284, handleProjectSelect currently switches project state but never
calls stopPolling(), so any active polling for the previous project continues;
modify the handleProjectSelect function to call stopPolling() (if available)
before or immediately after hiding the selector and before changing project
state (i.e., call stopPolling() alongside setShowProjectSelector(false), then
setProjectId(proj.id), setUploadPhase({ kind: "idle" }) and
setDismissedAlerts(new Set()) ) so background intervals are cancelled when
switching projects.
🧹 Nitpick comments (2)
src/app/api/contact/route.ts (2)

73-79: Consider documenting the sandbox-default behavior.

useSandbox defaults to true when MAILTRAP_USE_SANDBOX is unset. If a production deployment sets MAILTRAP_INBOX_ID but forgets to set MAILTRAP_USE_SANDBOX=false, emails will silently route to the sandbox inbox instead of actual delivery. Consider either:

  1. Adding a startup log warning when NODE_ENV=production and useSandbox=true
  2. Documenting required environment variables in a README or .env.example
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/contact/route.ts` around lines 73 - 79, The code currently
defaults useSandbox to true when MAILTRAP_USE_SANDBOX is unset which can
silently route production mail to the sandbox; update the startup flow to detect
this and either log a clear warning or fail-fast: when process.env.NODE_ENV ===
"production" and the useSandbox variable (computed from MAILTRAP_USE_SANDBOX) is
true while MAILTRAP_INBOX_ID is present, emit a prominent processLogger.warn (or
processLogger.error and exit) explaining that MAILTRAP_USE_SANDBOX is unset/true
in production, and add/update documentation (.env.example or README) to list
MAILTRAP_USE_SANDBOX and MAILTRAP_INBOX_ID requirements so operators must
explicitly set MAILTRAP_USE_SANDBOX=false for real deliveries.

4-12: Consider adding max length constraints to prevent abuse.

The schema validates minimum lengths but has no upper bounds. Malicious users could submit extremely large values, potentially causing issues with email generation or downstream services.

🛡️ Suggested fix
 const contactSchema = z.object({
-  name:        z.string().min(2),
-  email:       z.string().email(),
-  company:     z.string().min(1),
-  role:        z.string().min(1),
-  phone:       z.string().optional(),
-  projectSize: z.string().optional(),
-  message:     z.string().optional(),
+  name:        z.string().min(2).max(100),
+  email:       z.string().email().max(254),
+  company:     z.string().min(1).max(200),
+  role:        z.string().min(1).max(100),
+  phone:       z.string().max(30).optional(),
+  projectSize: z.string().max(100).optional(),
+  message:     z.string().max(5000).optional(),
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/contact/route.ts` around lines 4 - 12, contactSchema currently
enforces minimums but no upper bounds; add z.string().max(...) constraints to
each field to prevent extremely large inputs (e.g., name → max 100, email → max
254, company → max 100, role → max 100, phone → max 20, projectSize → max 50,
message → max 2000) by updating the contactSchema definition (the named symbol
contactSchema and its properties name, email, company, role, phone, projectSize,
message) to use z.string().max(n) where appropriate and keep optional() on
optional fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`(dashboard)/lookahead/_components/LookaheadDashboard.tsx:
- Around line 528-556: The file input's value isn't cleared before
programmatically opening the file dialog, so selecting the same file twice won't
trigger onChange; update the Button's onClick handler (where
fileInputRef.current?.click() is called) to first clear the input value on
fileInputRef.current (e.g., reset .value or set to null) and then call .click(),
ensuring handleFileSelected still handles the selected file from the input's
onChange.

---

Duplicate comments:
In `@src/app/`(dashboard)/lookahead/_components/LookaheadDashboard.tsx:
- Around line 277-284: handleProjectSelect currently switches project state but
never calls stopPolling(), so any active polling for the previous project
continues; modify the handleProjectSelect function to call stopPolling() (if
available) before or immediately after hiding the selector and before changing
project state (i.e., call stopPolling() alongside setShowProjectSelector(false),
then setProjectId(proj.id), setUploadPhase({ kind: "idle" }) and
setDismissedAlerts(new Set()) ) so background intervals are cancelled when
switching projects.

---

Nitpick comments:
In `@src/app/api/contact/route.ts`:
- Around line 73-79: The code currently defaults useSandbox to true when
MAILTRAP_USE_SANDBOX is unset which can silently route production mail to the
sandbox; update the startup flow to detect this and either log a clear warning
or fail-fast: when process.env.NODE_ENV === "production" and the useSandbox
variable (computed from MAILTRAP_USE_SANDBOX) is true while MAILTRAP_INBOX_ID is
present, emit a prominent processLogger.warn (or processLogger.error and exit)
explaining that MAILTRAP_USE_SANDBOX is unset/true in production, and add/update
documentation (.env.example or README) to list MAILTRAP_USE_SANDBOX and
MAILTRAP_INBOX_ID requirements so operators must explicitly set
MAILTRAP_USE_SANDBOX=false for real deliveries.
- Around line 4-12: contactSchema currently enforces minimums but no upper
bounds; add z.string().max(...) constraints to each field to prevent extremely
large inputs (e.g., name → max 100, email → max 254, company → max 100, role →
max 100, phone → max 20, projectSize → max 50, message → max 2000) by updating
the contactSchema definition (the named symbol contactSchema and its properties
name, email, company, role, phone, projectSize, message) to use
z.string().max(n) where appropriate and keep optional() on optional fields.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4d9865cd-c738-408d-981c-9a0ef3f776a8

📥 Commits

Reviewing files that changed from the base of the PR and between 1853a36 and 1ca0220.

📒 Files selected for processing (4)
  • src/app/(dashboard)/lookahead/_components/LookaheadDashboard.tsx
  • src/app/api/contact/route.ts
  • src/components/landing/ContactModal.tsx
  • src/hooks/lookahead/api.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/hooks/lookahead/api.ts
  • src/components/landing/ContactModal.tsx

Comment thread src/app/(dashboard)/lookahead/_components/LookaheadDashboard.tsx
@Ross1116

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 10, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant