Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds region-aware legal policy components and pages, a contact page and form, dynamic basePath handling, healthcheck candidate generation plus workflow changes to try multiple endpoints, navigation/footer UI updates, numerous new associate-retailer pages/components, and assorted UI/3D/CSS adjustments. Changes
sequenceDiagram
participant Workflow as Deployment Workflow
participant Script as healthcheck-candidates.mjs
participant Candidates as Candidate List
participant Retry as Retry Loop
participant Endpoint as Health Endpoint
Workflow->>Script: run with HEALTHCHECK_URL, APP_BASE_PATH
Script->>Script: validate HEALTHCHECK_URL (required, absolute)
Script->>Script: build candidates (primary, /health, /api/health, base-path variants)
Script-->>Candidates: output newline-delimited list
Workflow->>Retry: start retry attempts
loop until success or retries exhausted
Retry->>Candidates: iterate candidate URLs
Candidates->>Endpoint: curl candidate
alt 2xx received
Endpoint-->>Retry: success -> exit loop
else non-2xx / error
Endpoint-->>Retry: continue to next candidate
end
end
Retry-->>Workflow: report success or failure
🎯 4 (Complex) | ⏱️ ~40 minutes
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
4 issues found across 30 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/app/refund-policy/page.tsx">
<violation number="1" location="src/app/refund-policy/page.tsx:76">
P2: Fix the legal text typo: "sole desertion" should be "sole discretion" to avoid ambiguity in the refund policy.</violation>
</file>
<file name="src/app/contact/_components/contact-form.tsx">
<violation number="1" location="src/app/contact/_components/contact-form.tsx:108">
P2: The submit CTA is configured with `type="button"`, so the form cannot be submitted when users click "Submit".</violation>
</file>
<file name="src/app/_components/legal/market-policy-tabs.tsx">
<violation number="1" location="src/app/_components/legal/market-policy-tabs.tsx:22">
P2: This file duplicates the existing policy market switcher logic instead of reusing a shared component, increasing maintenance risk and drift between implementations.</violation>
</file>
<file name="src/lib/techpay-menu.js">
<violation number="1" location="src/lib/techpay-menu.js:112">
P2: Avoid forcing `restoreScroll: false` for every overlay link click; only skip scroll restoration for actual same-tab navigations.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| { | ||
| type: "paragraph", | ||
| text: | ||
| "Cancellation Fee: A cancellation fee as prescribed on the TECHPAY.ai APP may apply if the order is cancelled after the specified time window and such request and fee will be at the sole desertion of the facilitating partner.", |
There was a problem hiding this comment.
P2: Fix the legal text typo: "sole desertion" should be "sole discretion" to avoid ambiguity in the refund policy.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/app/refund-policy/page.tsx, line 76:
<comment>Fix the legal text typo: "sole desertion" should be "sole discretion" to avoid ambiguity in the refund policy.</comment>
<file context>
@@ -0,0 +1,257 @@
+ {
+ type: "paragraph",
+ text:
+ "Cancellation Fee: A cancellation fee as prescribed on the TECHPAY.ai APP may apply if the order is cancelled after the specified time window and such request and fee will be at the sole desertion of the facilitating partner.",
+ },
+ {
</file context>
| <div className="mt-7 flex flex-col gap-3 border-t border-white/10 pt-5 sm:flex-row sm:items-center sm:justify-between"> | ||
| <div aria-hidden="true" /> | ||
| <Button | ||
| type="button" |
There was a problem hiding this comment.
P2: The submit CTA is configured with type="button", so the form cannot be submitted when users click "Submit".
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/app/contact/_components/contact-form.tsx, line 108:
<comment>The submit CTA is configured with `type="button"`, so the form cannot be submitted when users click "Submit".</comment>
<file context>
@@ -0,0 +1,208 @@
+ <div className="mt-7 flex flex-col gap-3 border-t border-white/10 pt-5 sm:flex-row sm:items-center sm:justify-between">
+ <div aria-hidden="true" />
+ <Button
+ type="button"
+ leftIcon={<Send className="h-4 w-4" strokeWidth={1.8} aria-hidden="true" />}
+ size="compact"
</file context>
| { label: "Malaysia", market: "MY" as const }, | ||
| ]; | ||
|
|
||
| export default function MarketPolicyTabs({ |
There was a problem hiding this comment.
P2: This file duplicates the existing policy market switcher logic instead of reusing a shared component, increasing maintenance risk and drift between implementations.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/app/_components/legal/market-policy-tabs.tsx, line 22:
<comment>This file duplicates the existing policy market switcher logic instead of reusing a shared component, increasing maintenance risk and drift between implementations.</comment>
<file context>
@@ -0,0 +1,95 @@
+ { label: "Malaysia", market: "MY" as const },
+];
+
+export default function MarketPolicyTabs({
+ ariaLabel,
+ india,
</file context>
| const clickedLink = event.target.closest?.("a"); | ||
|
|
||
| if (clickedLink && menuOverlay.contains(clickedLink)) { | ||
| closeMenuForNavigation(); |
There was a problem hiding this comment.
P2: Avoid forcing restoreScroll: false for every overlay link click; only skip scroll restoration for actual same-tab navigations.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/techpay-menu.js, line 112:
<comment>Avoid forcing `restoreScroll: false` for every overlay link click; only skip scroll restoration for actual same-tab navigations.</comment>
<file context>
@@ -103,6 +106,13 @@ export function setupMenuOverlay({
+ const clickedLink = event.target.closest?.("a");
+
+ if (clickedLink && menuOverlay.contains(clickedLink)) {
+ closeMenuForNavigation();
+ return;
+ }
</file context>
| closeMenuForNavigation(); | |
| const sameTabNavigation = | |
| event.button === 0 && | |
| !event.metaKey && | |
| !event.ctrlKey && | |
| !event.shiftKey && | |
| !event.altKey && | |
| clickedLink.target !== "_blank"; | |
| closeMenu({ restoreScroll: !sameTabNavigation }); |
There was a problem hiding this comment.
Actionable comments posted: 16
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/lib/techpay-menu.js (1)
108-119:⚠️ Potential issue | 🟡 MinorNon-navigating link clicks will reset scroll position.
Any
<a>click insidemenuOverlaycallscloseMenuForNavigation(), which unlocks the body without restoringlockedScrollY. This is correct for normal navigations (the next page starts at top), but for links that do not unload the page —target="_blank",mailto:/tel:/sms:schemes,downloadattributes, same-page hash links (href="#…"), or any handler that callsevent.preventDefault()— the user stays on the current page and is silently scrolled back to the top.Consider only skipping scroll restore when the click will actually navigate away.
🛠️ Suggested refinement
const onOverlayClick = (event) => { const clickedLink = event.target.closest?.("a"); if (clickedLink && menuOverlay.contains(clickedLink)) { - closeMenuForNavigation(); - return; + const href = clickedLink.getAttribute("href") || ""; + const opensInNewTab = + clickedLink.target === "_blank" || clickedLink.hasAttribute("download"); + const isSamePageHash = href.startsWith("#"); + const isSpecialScheme = /^(mailto:|tel:|sms:)/i.test(href); + const willNavigateAway = + !opensInNewTab && !isSamePageHash && !isSpecialScheme && !event.defaultPrevented; + + if (willNavigateAway) { + closeMenuForNavigation(); + } else { + closeMenu(); + } + return; } if (event.target === menuOverlay) { closeMenu(); } };
🧹 Nitpick comments (13)
src/app/_components/home/home-page.tsx (1)
135-135: Consider replacing the hardcoded#ea335awith a theme token.The rest of this file uses semantic Tailwind tokens (
text-techpay-heading,text-techpay-orange,text-techpay-primary, etc.). Hardcoding#ea335ahere breaks that convention and makes future theme updates harder. If this pink already maps to an existing token (e.g.,techpay-pink/accent-pinkreferenced elsewhere in the file viacard-accent accent-pink), prefer that; otherwise add a new token to the theme.♻️ Suggested change
- The world's first <span className="text-[`#ea335a`] font-bold">PHYGITAL</span> retail platform. + The world's first <span className="text-techpay-pink font-bold">PHYGITAL</span> retail platform.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/_components/home/home-page.tsx` at line 135, Replace the hardcoded color class text-[`#ea335a`] used in the span rendering "PHYGITAL" with the project's semantic Tailwind token (e.g., text-techpay-pink or text-techpay-accent) to match the rest of the file; if no existing token maps to this pink, add a new token in the Tailwind theme (e.g., techpay-pink) and then update the span's className (the span containing the "PHYGITAL" text) to use text-<new-token> font-bold so theme updates remain consistent.src/app/api/health/route.ts (1)
1-14: Duplicate health handler — consider sharing the implementation between/healthand/api/health.
src/app/health/route.tsandsrc/app/api/health/route.tsare byte-identical. To avoid drift if one is ever updated (e.g., adding a version field, dependency check, or different headers), extract a shared module:Suggested shared helper
// src/lib/health.ts const headers = { "Cache-Control": "no-store, max-age=0" }; export const healthGet = () => Response.json({ status: "ok" }, { headers }); export const healthHead = () => new Response(null, { status: 200, headers });-export function GET() { - return Response.json({ status: "ok" }, { headers }); -} - -export function HEAD() { - return new Response(null, { status: 200, headers }); -} +export { healthGet as GET, healthHead as HEAD } from "@/lib/health"; + +export const dynamic = "force-dynamic"; +export const runtime = "nodejs";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/api/health/route.ts` around lines 1 - 14, The GET and HEAD handlers and the headers constant are duplicated (GET, HEAD, headers); extract them into a shared module (e.g., export healthGet and healthHead functions that use the headers constant) and replace the duplicated implementations in both route files to import and call those shared healthGet and healthHead exports; ensure the shared functions return the same Response shapes so existing route exports (GET and HEAD) simply delegate to the shared healthGet/healthHead.src/app/contact/page.tsx (1)
11-11:force-staticis fine today, but verify ifContactFormever needs server-side dynamic data.
export const dynamic = "force-static"pre-renders this page. SinceContactFormis a client component, that works today. However, if the form ever needs server-rendered defaults (e.g., region detection from headers, CSRF token, prefilled query params fromsearchParams),force-staticwill silently break those features. Worth a brief comment or reconsidering once form requirements firm up.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/contact/page.tsx` at line 11, The page is forced static via the exported const dynamic = "force-static", which will prevent server-rendered defaults for the ContactForm client component (e.g., header-based region detection, CSRF tokens, or searchParams-derived defaults); either remove or change that export to allow dynamic rendering when the form needs server-side data (e.g., revert to default/auto or use "force-dynamic"), or keep the export but add a clear inline comment near export and in ContactForm noting that server-derived defaults are not supported so future changes consider removing/changing export; reference the exported symbol dynamic and the ContactForm component when making the change..github/workflows/manual-rollback.yml (1)
66-69:NEXT_PUBLIC_BASE_PATHis exported in-line, butAPP_BASE_PATH(used by the health-check script) is set at the job level — keep them in sync via a single source of truth.Today the staging value
/stagingand production value""are duplicated between theenv:block (Lines 28, 107) and the inlineexport NEXT_PUBLIC_BASE_PATH=...inside the SSH script (Lines 66, 145). If one is changed (e.g., adding a new region) and the other isn't, the runtime base path and health-check candidates will diverge silently.Consider passing
APP_BASE_PATHthrough to the SSH script viaenvs:and exportingNEXT_PUBLIC_BASE_PATH="$APP_BASE_PATH"from a single value:Sketch
- envs: ROLLBACK_TAG + envs: ROLLBACK_TAG,APP_BASE_PATH script: | set -euo pipefail cd /home/azureuser/techpay-staging git fetch origin staging --tags --force git checkout --force "$ROLLBACK_TAG" - export NEXT_PUBLIC_BASE_PATH="/staging" + export NEXT_PUBLIC_BASE_PATH="$APP_BASE_PATH" npm install npm run build pm2 restart techpay-staging --update-envAlso applies to: 145-148
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/manual-rollback.yml around lines 66 - 69, The inline export of NEXT_PUBLIC_BASE_PATH should be removed and a single source of truth used: pass the job-level APP_BASE_PATH into the SSH/script environment and inside the SSH block export NEXT_PUBLIC_BASE_PATH="$APP_BASE_PATH" so both the runtime public path and the health-check script use the same value; update both staging and production SSH steps (the blocks that currently set NEXT_PUBLIC_BASE_PATH inline) to read APP_BASE_PATH from the job env and export NEXT_PUBLIC_BASE_PATH from it before running npm install/build and pm2 restart, ensuring NEXT_PUBLIC_BASE_PATH and APP_BASE_PATH remain in sync.next.config.ts (1)
4-12: Edge case: empty/whitespaceNEXT_PUBLIC_BASE_PATHproducesbasePath = ""correctly, but a value of"/"would normalize to/which Next.js treats as invalid.The current normalization handles the common cases (
"","staging","/staging","/staging/") correctly. However, if someone setsNEXT_PUBLIC_BASE_PATH="/",configuredBasePathis"/"(truthy), the slash-strip yields"", and the template literal produces"/"— which Next.js rejects (basePathmust not be just/). Consider treating a stripped-empty result the same as "no base path":Suggested defensive normalization
-const basePath = configuredBasePath - ? `/${configuredBasePath.replace(/^\/+|\/+$/g, "")}` - : ""; +const trimmedBasePath = configuredBasePath.replace(/^\/+|\/+$/g, ""); +const basePath = trimmedBasePath ? `/${trimmedBasePath}` : "";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@next.config.ts` around lines 4 - 12, The normalization can produce an invalid basePath of "/" when NEXT_PUBLIC_BASE_PATH is set to "/" — update the logic around configuredBasePath/basePath so you strip slashes into a temporary cleaned value (e.g., cleaned = configuredBasePath.replace(/^\/+|\/+$/g, "") after .trim()), then set basePath to "" when cleaned === "" (or configuredBasePath is falsy) otherwise set basePath to `/${cleaned}`; reference the existing identifiers defaultBasePath, configuredBasePath, and basePath when making this change..github/workflows/deploy-production.yml (1)
68-69: Comment is slightly misleading for production.In production
APP_BASE_PATH="", so there is no meaningful "fallback using APP_BASE_PATH on the same origin" — it's the same URL as the primary. Either tighten the comment for this workflow ("APP_BASE_PATHis empty in production, so only the configured URL is checked") or keep it generic but note that the candidate list collapses when the base path is empty. Same comment is used in the staging file where it does apply.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/deploy-production.yml around lines 68 - 69, Update the misleading workflow comment about health-check fallback: clarify that in production APP_BASE_PATH is an empty string so the fallback candidates collapse to the same origin (i.e., only the configured URL is effectively checked) or replace the line with tightened wording such as "APP_BASE_PATH is empty in production, so only the configured URL is checked" to avoid implying a distinct fallback; reference the APP_BASE_PATH variable and the existing comment block about the health endpoint when making the change.src/components/site-navbar.tsx (1)
53-53: Minor:contactResolvedHrefis identical in both branches.Since
contactLink.hrefandcontactLink.absoluteHrefare both"/contact"(and there is no in-page#contactanchor on the landing page), the conditional resolves to the same value in both cases. Either drop the conditional and usecontactLink.hrefdirectly, or updatehrefto"#contact"if/when an in-page anchor exists, mirroring howhomeHreftoggles to#hero.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/site-navbar.tsx` at line 53, The conditional assigning contactResolvedHref is redundant because contactLink.href and contactLink.absoluteHref are identical; simplify by removing the ternary and set contactResolvedHref = contactLink.href (or if you intend an in-page anchor on the landing page, update contactLink.href to "#contact" and keep using contactLink.href). Update the reference where contactResolvedHref is used accordingly (symbols: contactResolvedHref, contactLink.href, contactLink.absoluteHref, onLandingPage).src/app/contact/_components/contact-form.tsx (1)
137-139: Suggest associating the visible label with both the trigger and the hidden input.The
<label id="${id}-label">{label}</label>is a plain<label>with nohtmlFor, so clicking the label doesn't focus the trigger button, and the hidden input has no associated label at all. Use<label htmlFor={${id}-button}>(and/or render a<span>instead of<label>, since the trigger isn't a native form control) to match the actual semantics.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/contact/_components/contact-form.tsx` around lines 137 - 139, The visible label element currently has no htmlFor and is not associated with the trigger button or the hidden input; update the markup so the label is properly associated: give the trigger button an id `${id}-button` and set the label to `<label htmlFor={`${id}-button`}>` (or replace the `<label>` with a `<span id={`${id}-label`}>` and set the trigger's aria-labelledby to `${id}-label` if the trigger is not a native control), and also associate the hidden input by adding aria-labelledby={`${id}-label`} (or htmlFor if you change the hidden input to a focusable control) so screen readers and clicks on the label correctly target the trigger and the hidden input is announced.src/app/_components/legal/market-policy-tabs.tsx (2)
65-80:aria-hiddenis redundant whenhiddenis set.The HTML
hiddenattribute already removes the element from the accessibility tree, so duplicating witharia-hiddenis unnecessary (and the W3C recommends not combining them — they can disagree if one is dropped later by CSS overrides). Safe to drop thearia-hiddenprops.♻️ Proposed simplification
<div id={`${panelIdPrefix}-in`} role="tabpanel" hidden={showMalaysiaPolicy} - aria-hidden={showMalaysiaPolicy} > {india} </div> <div id={`${panelIdPrefix}-my`} role="tabpanel" hidden={!showMalaysiaPolicy} - aria-hidden={!showMalaysiaPolicy} > {malaysia} </div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/_components/legal/market-policy-tabs.tsx` around lines 65 - 80, The two tab panel divs using panelIdPrefix and toggled by showMalaysiaPolicy (rendering india and malaysia) currently include redundant aria-hidden attributes; remove the aria-hidden props from both <div id={`${panelIdPrefix}-in`}> and <div id={`${panelIdPrefix}-my`}> and keep role="tabpanel" and the hidden attribute to control visibility so accessibility is handled by the hidden attribute alone.
39-64: Tablist is missing keyboard navigation (arrow keys / Home / End).The WAI-ARIA tabs pattern requires keyboard interaction: arrow keys to move focus between tabs, with
tabindex="0"on the active tab andtabindex="-1"on the others (roving tabindex). Currently, the buttons receive default focus behavior with no special keyboard handling, providing a non-standard experience for keyboard users.Add an
onKeyDownhandler to the tablist to manageArrowLeft/ArrowRight/Home/Endkey presses, and passtabIndex={selected ? 0 : -1}to eachButtonto implement the roving tabindex pattern.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/_components/legal/market-policy-tabs.tsx` around lines 39 - 64, The tablist currently lacks keyboard navigation and roving tabindex; update the tab rendering and add a key handler so keyboard users can navigate per WAI-ARIA tabs pattern: in the tabs.map block set tabIndex on each Button to {selected ? 0 : -1} (use the existing selected/activeMarket logic) and add an onKeyDown handler on the surrounding div (role="tablist") that listens for ArrowLeft/ArrowRight/Home/End to move focus and call setSelectedMarket with the corresponding tab.market; reference the Button component, setSelectedMarket, tabs.map, activeMarket, panelIdPrefix and ariaLabel when locating where to implement the onKeyDown and tabIndex changes.src/app/_components/legal/legal-policy-document.tsx (2)
136-177: Heuristic regex-based heading detection couples content to presentation.
LegalParagraphsilently promotes any paragraph starting with\d+.,\d+.\d+,a), or whose trimmed text matchesstandaloneSubheadingsto an<h2>/<h3>. That means:
- Authors can never have a paragraph that legitimately starts with "1. " (e.g., a quoted clause) without it being rendered as an h2.
- Adding a new heading-like phrase to a policy requires editing this component (the
standaloneSubheadingsset), not just the data file — any new section name not in the set falls back to<p>.- The discriminated union already supports
subheading/paragraph/list. Mixing implicit promotion on top of explicitsubheadingblocks is hard to reason about.Prefer pushing the structure into the data: emit
{ type: "subheading", title: "1. Cancellations" }rather than{ type: "paragraph", text: "1. Cancellations" }and let the renderer be a thin pass-through. This also lets you produce a proper heading hierarchy (h1 → h2 → h3) without per-string heuristics.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/_components/legal/legal-policy-document.tsx` around lines 136 - 177, The LegalParagraph component currently promotes paragraphs to headings via regex heuristics (see function LegalParagraph, standaloneSubheadings and the labelMatch logic); change the renderer to stop implicit promotion and instead render strictly by the content type from the discriminated union (e.g., only treat items with type === "subheading" as <h2>/<h3>, type === "paragraph" as <p>, and type === "list" as lists). Remove or disable the regex-based checks in LegalParagraph (the /^\d+\.\d+/, /^[a-z]\)/, /^\d+\./ patterns and standaloneSubheadings lookup) and update data ingestion/parsing so heading-like strings are emitted as { type: "subheading", title: "…" } when intended; preserve the labelMatch behavior only for inline bold-label paragraphs by keeping the /^([^:\n]{2,45}:)([\s\S]+)$/ handling if desired.
95-113:<li>key uses item text — risks collisions on duplicate strings.
key={item}will collide if two list items have the same text (and React will warn / fail to reconcile correctly). Given these lists are author-controlled it's unlikely today, but using the array index is safer for a static, never-reordered list.♻️ Proposed change
- {block.items.map((item) => ( - <li className="list-disc pl-1" key={item}> + {block.items.map((item, index) => ( + <li className="list-disc pl-1" key={index}> {item} </li> ))}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/_components/legal/legal-policy-document.tsx` around lines 95 - 113, In the "list" case of the switch (where block.items.map is used) the <li> currently uses key={item} which can collide for duplicate strings; update the map to include the index (e.g., .map((item, idx) => ...) and use the index as the key for each <li>) so keys are unique for this static, never-reordered list; locate the mapping around the list case that renders <ul> and change the key on the <li> accordingly (refer to block.items.map and the LegalParagraph usage nearby).src/app/cookie-policy/page.tsx (1)
29-36: Usetype: "list"blocks instead of embedding\n-bullets in paragraphs.These two paragraphs (and similar ones below) embed bullet items as
\n- ...inside thetextfield. BecauseLegalParagraphrenders withwhitespace-pre-line, the result is a single<p>with literal-characters — not a semantic<ul><li>. Screen readers won't announce list structure, list styling won't apply, and the structuredLegalPolicyBlockschema already supports alistblock type that renders properly. Same applies to lines 33–36.♻️ Example for the "Why We Use Cookies" block
{ type: "paragraph", - text: "Why We Use Cookies: Tpay uses cookies and similar technologies to:\n- operate and secure the website and related services;\n- improve user experience;\n- remember user preferences;\n- analyze website performance and usage;\n- measure the effectiveness of content, campaigns, and communications; and\n- where permitted, support personalisation, marketing, advertising, and remarketing.", + text: "Why We Use Cookies: Tpay uses cookies and similar technologies to:", + }, + { + type: "list", + items: [ + "operate and secure the website and related services;", + "improve user experience;", + "remember user preferences;", + "analyze website performance and usage;", + "measure the effectiveness of content, campaigns, and communications; and", + "where permitted, support personalisation, marketing, advertising, and remarketing.", + ], },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/cookie-policy/page.tsx` around lines 29 - 36, Replace the paragraph blocks that embed newline hyphen bullets (e.g., the "Why We Use Cookies" and "Cookie Categories" objects) with proper list blocks supported by the LegalPolicyBlock schema: change type from "paragraph" to "list", remove the "\n- " inline bullets from the text and instead supply an items array (each bullet as its own string), and set the list style (e.g., "unordered") to match the renderer used by LegalParagraph/LegalPolicyBlock so the UI outputs a semantic <ul><li> structure accessible to screen readers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/deploy-production.yml:
- Line 80: The mapfile invocation using process substitution (mapfile -t
HEALTHCHECK_CANDIDATES < <(node .github/scripts/healthcheck-candidates.mjs)) can
hide script failures under set -euo pipefail; change the step to run node
.github/scripts/healthcheck-candidates.mjs first, check its exit code and
non-empty output, and only then populate HEALTHCHECK_CANDIDATES (or fail fast
with an explicit error via exit 1) so that failures are diagnosed immediately;
update both occurrences that use HEALTHCHECK_CANDIDATES/mapfile to follow this
validate-then-consume pattern.
In @.github/workflows/manual-rollback.yml:
- Around line 81-93: Replace the process-substitution usage of mapfile with an
explicit run of the helper and an immediate failure check: invoke node
healthcheck-candidates.mjs, capture its stdout into a variable, check the node
command exit status and if non-zero emit the script's stderr/exit info and exit
non-zero, otherwise populate HEALTHCHECK_CANDIDATES from that stdout; update the
area that currently uses mapfile and the loop that iterates
HEALTHCHECK_CANDIDATES so failures in healthcheck-candidates.mjs are surfaced
immediately (apply the same change where mapfile and HEALTHCHECK_CANDIDATES are
used in the production job).
In `@src/app/_components/legal/market-policy-tabs.tsx`:
- Around line 28-32: The subscribeToMarketPreference passed to
useSyncExternalStore never calls the onStoreChange callback, so snapshot updates
(from getMarketPreferenceSnapshot/getServerMarketPreferenceSnapshot) won't
propagate; either implement subscribeToMarketPreference to attach listeners
(e.g., window.addEventListener('popstate') and
window.addEventListener('storage') and call onStoreChange when those fire,
returning a cleanup that removes those listeners) so the store notifies React on
URL/storage changes, or replace the useSyncExternalStore usage with a useState
initialized from getMarketPreferenceSnapshot inside a useEffect that reads once
on mount if the intent is "read once on mount." Ensure you update the
subscribeToMarketPreference implementation referenced in market-policy-tabs.tsx
(and the analogous subscribe used at lines 85-95) to call onStoreChange or
switch to the useState+useEffect pattern.
In `@src/app/about-us/_components/about-us-page.tsx`:
- Around line 173-181: The page-level h1 element ("Mission & Vision") in the
AboutUsPage component is marked sr-only so its large styling classes
(font-display text-5xl font-bold leading-[0.95] text-white md:text-7xl
xl:text-8xl) are redundant; either (A) if the title should remain visually
hidden, remove those unused classes from the h1 to clean up the markup, or (B)
if the page title should be visible, remove the sr-only class from the h1 and
keep/adjust the large typography classes to match the design and layout. Locate
the h1 in about-us-page.tsx (text "Mission & Vision") and apply the appropriate
change and any necessary spacing/layout tweaks.
- Around line 188-191: The Tailwind class used for the gradient in the div
inside the AboutUs page is using the removed v3 syntax `bg-gradient-to-r`;
update the class to the v4 syntax `bg-linear-to-r` so the gradient works with
Tailwind v4, i.e., in the JSX element that renders the bar (the div with
className={`absolute inset-x-0 top-0 h-1 bg-gradient-to-r ${item.accent}`} in
the AboutUsPage component), replace `bg-gradient-to-r` with `bg-linear-to-r`
(keeping `${item.accent}` intact).
In `@src/app/contact/_components/contact-form.tsx`:
- Around line 19-22: The form in contact-form.tsx currently has no
action/onSubmit and the Submit Button is rendered with type="button" (Button
component forwards type), so clicking it does nothing; fix by wiring a
submission path: either change the Button to type="submit" and implement an
onSubmit handler on the <form> (e.g., function handleSubmit(event) or async
server action submitContact) that calls preventDefault()/validates inputs and
sends data, or keep type="button" but add an explicit onClick handler on the
Button that triggers the same submission function; update the contact-form.tsx
form element and button usage to reference these handler names and ensure native
validation/POST or fetch logic is executed.
- Around line 43-85: The form inputs in contact-form.tsx (the label/input blocks
for name/email/mobile inside the ContactForm component) lack validation
attributes; update the inputs named firstName, lastName, email and mobile to
include required and sensible maxLength values (e.g. firstName/lastName
maxLength ~100, email maxLength ~320), add inputMode="tel" and a basic pattern
(e.g. "^[0-9+\\-()\\s]{7,15}$") to the mobile input and set maxLength for it
(e.g. 15), and add a maxLength (e.g. 1000) and required to the request-details
textarea; keep type="email" on email and ensure required is present so the
browser enforces basic validation before submission.
- Around line 140-205: The dropdown lacks proper listbox keyboard navigation,
uses a self-referential aria-labelledby on the trigger, and relies on brittle
onBlur logic; fix by (1) removing `${id}-button` from the trigger's
aria-labelledby and only reference `${id}-label`, (2) implement
ArrowDown/ArrowUp/Home/End handling in the trigger/listbox (handle in the
onKeyDown that currently only checks "Escape") and surface active option via
aria-activedescendant on the listbox (or implement roving tabindex on the option
buttons rendered in options.map and keep focus on the listbox element), and (3)
replace the onBlur relatedTarget check with a document-level focusin/mousedown
listener registered/cleaned up inside the component (or switch to a vetted
primitive like Radix Select / Headless UI Listbox to get these patterns for
setOpen, setValue, open, and selectedLabel automatically).
In `@src/app/contact/page.tsx`:
- Around line 29-45: The three rows under the div that contains the Mail,
MessageSquareText, and Phone icons in the Contact page (the block starting with
className="mt-8 grid gap-3 text-sm text-techpay-muted") are using placeholder
label text ("Email and contact details", "Subject/category", "Mobile number");
replace those span texts with real contact values or clearer user-facing copy:
either show the actual email (e.g., contact@techpay.ai), a phone number and
office hours, and a subject/category description, or change them to
instructional labels such as "How we can reach you", "Choose a topic", "Add a
callback number" so the column communicates real contact information or
actionable guidance instead of form-field placeholders; update the span contents
adjacent to Mail, MessageSquareText, and Phone accordingly.
In `@src/app/future-of-retail/page.tsx`:
- Around line 656-662: The Contact Us CTA in future-of-retail/page.tsx uses
ButtonLink with href="/coming-soon" but the PR adds a real contact page and
other components (site-footer.tsx and navbar) point to "/contact"; update the
ButtonLink href in the future-of-retail page from "/coming-soon" to "/contact"
so it matches the new src/app/contact/page.tsx and the links in site-footer.tsx.
In `@src/app/privacy-policy/page.tsx`:
- Around line 381-396: The array entries containing paragraphs with text "7.
Document Information" followed by "Document Version History" and "8. Internal
Implementation Notes" are exposing empty/internal headings; either remove the
"7. Document Information" and "Document Version History" entries (or replace
"Document Version History" with a real, populated version-history entry), and
rename the "8. Internal Implementation Notes" paragraph heading to a
public-facing label such as "Related Policies" or "Interpretation" and adjust
the following paragraph text accordingly; locate and update the objects whose
text fields equal "7. Document Information", "Document Version History", and "8.
Internal Implementation Notes" in the privacy policy content array (the
paragraph objects with type: "paragraph") to implement these changes.
- Around line 173-184: Two paragraph objects in the policy content contain PDF
extraction artifacts "[page:1]" in their text properties; locate the paragraph
objects where type === "paragraph" and the text includes "[page:1]" (the objects
shown with text starting "...specific, lawful..." and "...not be sold,
rented...") and remove the "[page:1]" substring from those text values (either
by editing the literal strings or applying a trim/replace on the text property)
so the published policy shows clean sentences without the artifact.
In `@src/app/refund-policy/page.tsx`:
- Around line 14-55: The indiaRefundPolicy object contains placeholder legal
copy (e.g., title, meta Effective Date "To be updated", summary) and
metadata.description indicates placeholder content; either remove or hide this
from main by gating rendering: wrap usage of indiaRefundPolicy (and any
metadata.description pointing to India placeholder) behind a feature flag or
environment check (e.g., IS_MARKET_IN or showIndiaPolicy) or replace the
structured policy with a simple "Coming soon" component until finalized; update
the page/component that imports/uses indiaRefundPolicy so it conditionally
renders the real policy only when the flag is enabled and otherwise returns the
coming-soon placeholder, and ensure no placeholder metadata.description is
exposed to crawlers when the feature flag is off.
- Around line 231-240: Update the two paragraph entries that contain the dispute
and contact lines: replace "courts in KL, Malaysia" with "courts in Kuala
Lumpur, Malaysia" in the paragraph whose text starts "Any disputes arising out
of cancellations or returns...", and change the email local-part casing from
"Contact@techpay.ai" to "contact@techpay.ai" in the paragraph whose text starts
"For further assistance, please email us at...".
- Around line 73-77: In the refund policy paragraph object (the entry with type:
"paragraph" whose text begins "Cancellation Fee: A cancellation fee as
prescribed on the TECHPAY.ai APP..."), fix the typo by replacing the word
"desertion" with "discretion" so the sentence reads "sole discretion of the
facilitating partner"; update that string in src/app/refund-policy/page.tsx
accordingly.
In `@src/app/terms-and-conditions/page.tsx`:
- Around line 30-32: The paragraph object with type "paragraph" containing the
long Tpay terms text has a missing space after "Tpay Platform Private Limited."
resulting in "Limited.If"; update that string to insert a space after the period
and, for readability, split this very long text into two separate paragraph
entries (preserving the same surrounding structure) so the sentence starting "If
there is a conflict between the Terms..." becomes the first sentence of the new
paragraph.
---
Nitpick comments:
In @.github/workflows/deploy-production.yml:
- Around line 68-69: Update the misleading workflow comment about health-check
fallback: clarify that in production APP_BASE_PATH is an empty string so the
fallback candidates collapse to the same origin (i.e., only the configured URL
is effectively checked) or replace the line with tightened wording such as
"APP_BASE_PATH is empty in production, so only the configured URL is checked" to
avoid implying a distinct fallback; reference the APP_BASE_PATH variable and the
existing comment block about the health endpoint when making the change.
In @.github/workflows/manual-rollback.yml:
- Around line 66-69: The inline export of NEXT_PUBLIC_BASE_PATH should be
removed and a single source of truth used: pass the job-level APP_BASE_PATH into
the SSH/script environment and inside the SSH block export
NEXT_PUBLIC_BASE_PATH="$APP_BASE_PATH" so both the runtime public path and the
health-check script use the same value; update both staging and production SSH
steps (the blocks that currently set NEXT_PUBLIC_BASE_PATH inline) to read
APP_BASE_PATH from the job env and export NEXT_PUBLIC_BASE_PATH from it before
running npm install/build and pm2 restart, ensuring NEXT_PUBLIC_BASE_PATH and
APP_BASE_PATH remain in sync.
In `@next.config.ts`:
- Around line 4-12: The normalization can produce an invalid basePath of "/"
when NEXT_PUBLIC_BASE_PATH is set to "/" — update the logic around
configuredBasePath/basePath so you strip slashes into a temporary cleaned value
(e.g., cleaned = configuredBasePath.replace(/^\/+|\/+$/g, "") after .trim()),
then set basePath to "" when cleaned === "" (or configuredBasePath is falsy)
otherwise set basePath to `/${cleaned}`; reference the existing identifiers
defaultBasePath, configuredBasePath, and basePath when making this change.
In `@src/app/_components/home/home-page.tsx`:
- Line 135: Replace the hardcoded color class text-[`#ea335a`] used in the span
rendering "PHYGITAL" with the project's semantic Tailwind token (e.g.,
text-techpay-pink or text-techpay-accent) to match the rest of the file; if no
existing token maps to this pink, add a new token in the Tailwind theme (e.g.,
techpay-pink) and then update the span's className (the span containing the
"PHYGITAL" text) to use text-<new-token> font-bold so theme updates remain
consistent.
In `@src/app/_components/legal/legal-policy-document.tsx`:
- Around line 136-177: The LegalParagraph component currently promotes
paragraphs to headings via regex heuristics (see function LegalParagraph,
standaloneSubheadings and the labelMatch logic); change the renderer to stop
implicit promotion and instead render strictly by the content type from the
discriminated union (e.g., only treat items with type === "subheading" as
<h2>/<h3>, type === "paragraph" as <p>, and type === "list" as lists). Remove or
disable the regex-based checks in LegalParagraph (the /^\d+\.\d+/, /^[a-z]\)/,
/^\d+\./ patterns and standaloneSubheadings lookup) and update data
ingestion/parsing so heading-like strings are emitted as { type: "subheading",
title: "…" } when intended; preserve the labelMatch behavior only for inline
bold-label paragraphs by keeping the /^([^:\n]{2,45}:)([\s\S]+)$/ handling if
desired.
- Around line 95-113: In the "list" case of the switch (where block.items.map is
used) the <li> currently uses key={item} which can collide for duplicate
strings; update the map to include the index (e.g., .map((item, idx) => ...) and
use the index as the key for each <li>) so keys are unique for this static,
never-reordered list; locate the mapping around the list case that renders <ul>
and change the key on the <li> accordingly (refer to block.items.map and the
LegalParagraph usage nearby).
In `@src/app/_components/legal/market-policy-tabs.tsx`:
- Around line 65-80: The two tab panel divs using panelIdPrefix and toggled by
showMalaysiaPolicy (rendering india and malaysia) currently include redundant
aria-hidden attributes; remove the aria-hidden props from both <div
id={`${panelIdPrefix}-in`}> and <div id={`${panelIdPrefix}-my`}> and keep
role="tabpanel" and the hidden attribute to control visibility so accessibility
is handled by the hidden attribute alone.
- Around line 39-64: The tablist currently lacks keyboard navigation and roving
tabindex; update the tab rendering and add a key handler so keyboard users can
navigate per WAI-ARIA tabs pattern: in the tabs.map block set tabIndex on each
Button to {selected ? 0 : -1} (use the existing selected/activeMarket logic) and
add an onKeyDown handler on the surrounding div (role="tablist") that listens
for ArrowLeft/ArrowRight/Home/End to move focus and call setSelectedMarket with
the corresponding tab.market; reference the Button component, setSelectedMarket,
tabs.map, activeMarket, panelIdPrefix and ariaLabel when locating where to
implement the onKeyDown and tabIndex changes.
In `@src/app/api/health/route.ts`:
- Around line 1-14: The GET and HEAD handlers and the headers constant are
duplicated (GET, HEAD, headers); extract them into a shared module (e.g., export
healthGet and healthHead functions that use the headers constant) and replace
the duplicated implementations in both route files to import and call those
shared healthGet and healthHead exports; ensure the shared functions return the
same Response shapes so existing route exports (GET and HEAD) simply delegate to
the shared healthGet/healthHead.
In `@src/app/contact/_components/contact-form.tsx`:
- Around line 137-139: The visible label element currently has no htmlFor and is
not associated with the trigger button or the hidden input; update the markup so
the label is properly associated: give the trigger button an id `${id}-button`
and set the label to `<label htmlFor={`${id}-button`}>` (or replace the
`<label>` with a `<span id={`${id}-label`}>` and set the trigger's
aria-labelledby to `${id}-label` if the trigger is not a native control), and
also associate the hidden input by adding aria-labelledby={`${id}-label`} (or
htmlFor if you change the hidden input to a focusable control) so screen readers
and clicks on the label correctly target the trigger and the hidden input is
announced.
In `@src/app/contact/page.tsx`:
- Line 11: The page is forced static via the exported const dynamic =
"force-static", which will prevent server-rendered defaults for the ContactForm
client component (e.g., header-based region detection, CSRF tokens, or
searchParams-derived defaults); either remove or change that export to allow
dynamic rendering when the form needs server-side data (e.g., revert to
default/auto or use "force-dynamic"), or keep the export but add a clear inline
comment near export and in ContactForm noting that server-derived defaults are
not supported so future changes consider removing/changing export; reference the
exported symbol dynamic and the ContactForm component when making the change.
In `@src/app/cookie-policy/page.tsx`:
- Around line 29-36: Replace the paragraph blocks that embed newline hyphen
bullets (e.g., the "Why We Use Cookies" and "Cookie Categories" objects) with
proper list blocks supported by the LegalPolicyBlock schema: change type from
"paragraph" to "list", remove the "\n- " inline bullets from the text and
instead supply an items array (each bullet as its own string), and set the list
style (e.g., "unordered") to match the renderer used by
LegalParagraph/LegalPolicyBlock so the UI outputs a semantic <ul><li> structure
accessible to screen readers.
In `@src/components/site-navbar.tsx`:
- Line 53: The conditional assigning contactResolvedHref is redundant because
contactLink.href and contactLink.absoluteHref are identical; simplify by
removing the ternary and set contactResolvedHref = contactLink.href (or if you
intend an in-page anchor on the landing page, update contactLink.href to
"#contact" and keep using contactLink.href). Update the reference where
contactResolvedHref is used accordingly (symbols: contactResolvedHref,
contactLink.href, contactLink.absoluteHref, onLandingPage).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: df28b72a-f4bd-45bd-8a78-cd80053c4e43
⛔ Files ignored due to path filters (5)
public/file.svgis excluded by!**/*.svgpublic/globe.svgis excluded by!**/*.svgpublic/next.svgis excluded by!**/*.svgpublic/vercel.svgis excluded by!**/*.svgpublic/window.svgis excluded by!**/*.svg
📒 Files selected for processing (25)
.github/scripts/healthcheck-candidates.mjs.github/workflows/deploy-production.yml.github/workflows/deploy-staging.yml.github/workflows/manual-rollback.ymlnext.config.tssrc/app/_components/home/home-page.tsxsrc/app/_components/legal/legal-policy-document.tsxsrc/app/_components/legal/market-policy-tabs.tsxsrc/app/about-us/_components/about-us-page.tsxsrc/app/about-us/_components/following-pointer-demo.tsxsrc/app/api/health/route.tssrc/app/contact/_components/contact-form.tsxsrc/app/contact/page.tsxsrc/app/cookie-policy/page.tsxsrc/app/future-of-retail/page.tsxsrc/app/health/route.tssrc/app/privacy-policy/_components/policy-market-switcher.tsxsrc/app/privacy-policy/page.tsxsrc/app/refund-policy/page.tsxsrc/app/terms-and-conditions/page.tsxsrc/components/menu-setup.tsxsrc/components/site-footer.tsxsrc/components/site-navbar.tsxsrc/lib/techpay-menu.jssrc/lib/techpay-scene.js
💤 Files with no reviewable changes (1)
- src/components/menu-setup.tsx
| exit 1 | ||
| fi | ||
|
|
||
| mapfile -t HEALTHCHECK_CANDIDATES < <(node .github/scripts/healthcheck-candidates.mjs) |
There was a problem hiding this comment.
mapfile < <(node …) will not fail the step if the script errors.
Process substitution exit codes are not propagated under set -euo pipefail, so if node .github/scripts/healthcheck-candidates.mjs crashes or prints nothing, HEALTHCHECK_CANDIDATES becomes an empty array; the inner for loop is then skipped on every retry and the step only fails after the full 2-minute sleep budget — with no diagnostic about why. Consider running the script first, validating output, and aborting early:
🛡️ Defensive variant
- mapfile -t HEALTHCHECK_CANDIDATES < <(node .github/scripts/healthcheck-candidates.mjs)
+ CANDIDATES_RAW="$(node .github/scripts/healthcheck-candidates.mjs)"
+ mapfile -t HEALTHCHECK_CANDIDATES <<<"$CANDIDATES_RAW"
+ if [ "${`#HEALTHCHECK_CANDIDATES`[@]}" -eq 0 ] || [ -z "${HEALTHCHECK_CANDIDATES[0]}" ]; then
+ echo "No health check candidates produced by healthcheck-candidates.mjs."
+ exit 1
+ fiAlso applies to: 144-144
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/deploy-production.yml at line 80, The mapfile invocation
using process substitution (mapfile -t HEALTHCHECK_CANDIDATES < <(node
.github/scripts/healthcheck-candidates.mjs)) can hide script failures under set
-euo pipefail; change the step to run node
.github/scripts/healthcheck-candidates.mjs first, check its exit code and
non-empty output, and only then populate HEALTHCHECK_CANDIDATES (or fail fast
with an explicit error via exit 1) so that failures are diagnosed immediately;
update both occurrences that use HEALTHCHECK_CANDIDATES/mapfile to follow this
validate-then-consume pattern.
| mapfile -t HEALTHCHECK_CANDIDATES < <(node .github/scripts/healthcheck-candidates.mjs) | ||
|
|
||
| for attempt in {1..12}; do | ||
| echo "Staging rollback health check attempt ${attempt}/12: ${HEALTHCHECK_URL}" | ||
| for index in "${!HEALTHCHECK_CANDIDATES[@]}"; do | ||
| candidate_number=$((index + 1)) | ||
|
|
||
| echo "Staging rollback health check attempt ${attempt}/12, candidate ${candidate_number}/${#HEALTHCHECK_CANDIDATES[@]}" | ||
|
|
||
| if curl --fail --silent --show-error --max-time 10 "$HEALTHCHECK_URL" > /dev/null; then | ||
| echo "Staging rollback health check passed." | ||
| exit 0 | ||
| fi | ||
| if curl --fail --silent --show-error --max-time 10 "${HEALTHCHECK_CANDIDATES[$index]}" > /dev/null; then | ||
| echo "Staging rollback health check passed." | ||
| exit 0 | ||
| fi | ||
| done |
There was a problem hiding this comment.
mapfile < <(node ...) swallows the script's non-zero exit; a misconfigured HEALTHCHECK_URL will silently spin for ~2 minutes before failing.
Process substitution exit codes are not propagated to set -e. If healthcheck-candidates.mjs exits non-zero (e.g., absolute-URL parse failure), HEALTHCHECK_CANDIDATES is left empty and the outer loop runs 12 attempts × 0 candidates × 10s sleep before reporting "Manual intervention required" with no useful diagnostic.
Fail fast and surface the underlying error:
Suggested fix (apply analogously to the production job at Lines 160-172)
- mapfile -t HEALTHCHECK_CANDIDATES < <(node .github/scripts/healthcheck-candidates.mjs)
+ if ! candidates_output=$(node .github/scripts/healthcheck-candidates.mjs); then
+ echo "Failed to compute health-check candidates."
+ exit 1
+ fi
+ mapfile -t HEALTHCHECK_CANDIDATES <<< "$candidates_output"
+ if [ "${`#HEALTHCHECK_CANDIDATES`[@]}" -eq 0 ]; then
+ echo "No health-check candidates were generated."
+ exit 1
+ fi📝 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.
| mapfile -t HEALTHCHECK_CANDIDATES < <(node .github/scripts/healthcheck-candidates.mjs) | |
| for attempt in {1..12}; do | |
| echo "Staging rollback health check attempt ${attempt}/12: ${HEALTHCHECK_URL}" | |
| for index in "${!HEALTHCHECK_CANDIDATES[@]}"; do | |
| candidate_number=$((index + 1)) | |
| echo "Staging rollback health check attempt ${attempt}/12, candidate ${candidate_number}/${#HEALTHCHECK_CANDIDATES[@]}" | |
| if curl --fail --silent --show-error --max-time 10 "$HEALTHCHECK_URL" > /dev/null; then | |
| echo "Staging rollback health check passed." | |
| exit 0 | |
| fi | |
| if curl --fail --silent --show-error --max-time 10 "${HEALTHCHECK_CANDIDATES[$index]}" > /dev/null; then | |
| echo "Staging rollback health check passed." | |
| exit 0 | |
| fi | |
| done | |
| if ! candidates_output=$(node .github/scripts/healthcheck-candidates.mjs); then | |
| echo "Failed to compute health-check candidates." | |
| exit 1 | |
| fi | |
| mapfile -t HEALTHCHECK_CANDIDATES <<< "$candidates_output" | |
| if [ "${`#HEALTHCHECK_CANDIDATES`[@]}" -eq 0 ]; then | |
| echo "No health-check candidates were generated." | |
| exit 1 | |
| fi | |
| for attempt in {1..12}; do | |
| for index in "${!HEALTHCHECK_CANDIDATES[@]}"; do | |
| candidate_number=$((index + 1)) | |
| echo "Staging rollback health check attempt ${attempt}/12, candidate ${candidate_number}/${`#HEALTHCHECK_CANDIDATES`[@]}" | |
| if curl --fail --silent --show-error --max-time 10 "${HEALTHCHECK_CANDIDATES[$index]}" > /dev/null; then | |
| echo "Staging rollback health check passed." | |
| exit 0 | |
| fi | |
| done |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/manual-rollback.yml around lines 81 - 93, Replace the
process-substitution usage of mapfile with an explicit run of the helper and an
immediate failure check: invoke node healthcheck-candidates.mjs, capture its
stdout into a variable, check the node command exit status and if non-zero emit
the script's stderr/exit info and exit non-zero, otherwise populate
HEALTHCHECK_CANDIDATES from that stdout; update the area that currently uses
mapfile and the loop that iterates HEALTHCHECK_CANDIDATES so failures in
healthcheck-candidates.mjs are surfaced immediately (apply the same change where
mapfile and HEALTHCHECK_CANDIDATES are used in the production job).
| const preferredMarket = useSyncExternalStore( | ||
| subscribeToMarketPreference, | ||
| getMarketPreferenceSnapshot, | ||
| getServerMarketPreferenceSnapshot | ||
| ); |
There was a problem hiding this comment.
useSyncExternalStore subscribe is a no-op — preference changes won't propagate.
subscribeToMarketPreference returns a no-op cleanup without ever invoking the onStoreChange callback that React passes in. As a result, if the URL/route or persisted market changes after mount (e.g., client-side navigation from a market-aware route into a policy page, or another tab updating storage), getMarketPreferenceSnapshot is never re-read and the preferred tab will not update until the user clicks one.
For the current static policy pages this is usually fine, but if you want the tab to track URL/storage changes, wire onStoreChange to popstate, a storage event listener, or whatever event source backs getPreferredMarket. Otherwise, consider replacing useSyncExternalStore with a simple useEffect-initialized useState to make the intent ("read once on mount") explicit.
Also applies to: 85-95
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/_components/legal/market-policy-tabs.tsx` around lines 28 - 32, The
subscribeToMarketPreference passed to useSyncExternalStore never calls the
onStoreChange callback, so snapshot updates (from
getMarketPreferenceSnapshot/getServerMarketPreferenceSnapshot) won't propagate;
either implement subscribeToMarketPreference to attach listeners (e.g.,
window.addEventListener('popstate') and window.addEventListener('storage') and
call onStoreChange when those fire, returning a cleanup that removes those
listeners) so the store notifies React on URL/storage changes, or replace the
useSyncExternalStore usage with a useState initialized from
getMarketPreferenceSnapshot inside a useEffect that reads once on mount if the
intent is "read once on mount." Ensure you update the
subscribeToMarketPreference implementation referenced in market-policy-tabs.tsx
(and the analogous subscribe used at lines 85-95) to call onStoreChange or
switch to the useState+useEffect pattern.
| <div className="mx-auto max-w-5xl text-center"> | ||
| <p className="mb-4 text-sm font-semibold uppercase text-techpay-primary md:text-base"> | ||
| About TechPay.ai | ||
| </p> | ||
|
|
||
| <h1 className="sr-only font-display text-5xl font-bold leading-[0.95] text-white md:text-7xl xl:text-8xl"> | ||
| Mission & Vision | ||
| </h1> | ||
|
|
||
| </div> |
There was a problem hiding this comment.
Confirm intent: visible h1 was hidden, leaving redundant style classes.
The h1 is now sr-only, so the visual page no longer renders any primary heading text — the largest visible labels are the per-card "MISSION"/"VISION" h2s. Two follow-ups:
- The styling classes (
font-display text-5xl font-bold leading-[0.95] text-white md:text-7xl xl:text-8xl) have no effect undersr-onlyand can be dropped. - Confirm hiding the page title visually is the intended design (rather than, say, only hiding it on the mission/vision section). If unintended, the h1 should be made visible again.
🧹 Suggested cleanup if `sr-only` is intentional
- <h1 className="sr-only font-display text-5xl font-bold leading-[0.95] text-white md:text-7xl xl:text-8xl">
+ <h1 className="sr-only">
Mission & Vision
</h1>
- 🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/about-us/_components/about-us-page.tsx` around lines 173 - 181, The
page-level h1 element ("Mission & Vision") in the AboutUsPage component is
marked sr-only so its large styling classes (font-display text-5xl font-bold
leading-[0.95] text-white md:text-7xl xl:text-8xl) are redundant; either (A) if
the title should remain visually hidden, remove those unused classes from the h1
to clean up the markup, or (B) if the page title should be visible, remove the
sr-only class from the h1 and keep/adjust the large typography classes to match
the design and layout. Locate the h1 in about-us-page.tsx (text "Mission &
Vision") and apply the appropriate change and any necessary spacing/layout
tweaks.
| <div | ||
| className={`absolute inset-x-0 top-0 h-1 ${ | ||
| index === 0 | ||
| ? "bg-gradient-to-r from-techpay-primary via-techpay-orange to-transparent" | ||
| : "bg-gradient-to-r from-techpay-purple via-techpay-primary to-transparent" | ||
| }`} | ||
| className={`absolute inset-x-0 top-0 h-1 bg-gradient-to-r ${item.accent}`} | ||
| aria-hidden="true" | ||
| /> |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd -t f 'package.json' -d 3 --exec sh -c 'echo "=== {} ==="; cat {} | python -c "import json,sys; d=json.load(sys.stdin); deps={**d.get(\"dependencies\",{}), **d.get(\"devDependencies\",{})}; [print(k,v) for k,v in deps.items() if \"tailwind\" in k.lower()]"'
echo "--- legacy bg-gradient-to-* usages remaining ---"
rg -nP '\bbg-gradient-to-(r|l|t|b|tr|tl|br|bl)\b' --type=ts --type=tsx --type=js --type=jsx --type=html --type=css
echo "--- v4 bg-linear-to-* usages ---"
rg -nP '\bbg-linear-to-(r|l|t|b|tr|tl|br|bl)\b' --type=ts --type=tsx --type=js --type=jsx --type=html --type=cssRepository: rtksng/techpay
Length of output: 1794
🌐 Web query:
Is bg-gradient-to-r still supported in Tailwind CSS v4 or has it been removed in favor of bg-linear-to-r?
💡 Result:
No, bg-gradient-to-r is not supported in Tailwind CSS v4. It has been deprecated and renamed to bg-linear-to-r. In Tailwind CSS v4, the gradient utilities were updated for clarity and to make room for new gradient types like radial and conic gradients. The old bg-gradient-to-* classes from v3 were renamed to bg-linear-to-* to explicitly indicate they create linear gradients. Official Tailwind CSS v4 documentation lists bg-linear-to-r (among others like bg-linear-to-t, bg-linear-to-tr) for linear gradients, paired with from-, via-, and to-* utilities. The v4.0 announcement states: "You may notice we've renamed bg-gradient-* to bg-linear-* too." A migration tool in Tailwind CSS v4 automatically updates bg-gradient-* to bg-linear-* in templates (PR #14537). In v4.0, legacy bg-gradient-* classes may generate deprecation warnings but still work; by v4.1+, they are removed. Real-world reports confirm: Reddit users note bg-gradient-to-br fails in v4, replaced by bg-linear-to-br; migration guides list it as a required rename.
Citations:
- 1: https://tailwindcss.com/docs/background-image
- 2: https://tailwindcss.com/blog/tailwindcss-v4
- 3: https://github.com/tailwindlabs/tailwindcss/blob/8feb6a758ab75ea63d22a59b5530f68d35b34ab8/CHANGELOG.md
- 4: https://www.digitalapplied.com/blog/tailwind-css-v4-migration-new-features-guide
- 5: https://tailwindcss.com/docs/gradient-color-stops
- 6: Template migrations: Migrate
bg-gradient-*utilities tobg-linear-*tailwindlabs/tailwindcss#14537 - 7: https://tailwindcss.com/docs/upgrade-guide
- 8: https://www.reddit.com/r/tailwindcss/comments/1ms50f6/tailwind_css_bggradienttobr_not_working_in_my/
Use Tailwind v4 bg-linear-to-r—bg-gradient-to-r is not supported in v4.
Tailwind v4 removed the bg-gradient-to-* syntax entirely in favor of bg-linear-to-*. While v4.0 may emit deprecation warnings, v4.1+ will not recognize the class. The codebase already uses the v4 form elsewhere (e.g., following-pointer-demo.tsx). Update this to bg-linear-to-r:
♻️ Change required
- className={`absolute inset-x-0 top-0 h-1 bg-gradient-to-r ${item.accent}`}
+ className={`absolute inset-x-0 top-0 h-1 bg-linear-to-r ${item.accent}`}📝 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.
| <div | |
| className={`absolute inset-x-0 top-0 h-1 ${ | |
| index === 0 | |
| ? "bg-gradient-to-r from-techpay-primary via-techpay-orange to-transparent" | |
| : "bg-gradient-to-r from-techpay-purple via-techpay-primary to-transparent" | |
| }`} | |
| className={`absolute inset-x-0 top-0 h-1 bg-gradient-to-r ${item.accent}`} | |
| aria-hidden="true" | |
| /> | |
| <div | |
| className={`absolute inset-x-0 top-0 h-1 bg-linear-to-r ${item.accent}`} | |
| aria-hidden="true" | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/about-us/_components/about-us-page.tsx` around lines 188 - 191, The
Tailwind class used for the gradient in the div inside the AboutUs page is using
the removed v3 syntax `bg-gradient-to-r`; update the class to the v4 syntax
`bg-linear-to-r` so the gradient works with Tailwind v4, i.e., in the JSX
element that renders the bar (the div with className={`absolute inset-x-0 top-0
h-1 bg-gradient-to-r ${item.accent}`} in the AboutUsPage component), replace
`bg-gradient-to-r` with `bg-linear-to-r` (keeping `${item.accent}` intact).
| { | ||
| type: "paragraph", | ||
| text: "7. Document Information", | ||
| }, | ||
| { | ||
| type: "paragraph", | ||
| text: "Document Version History", | ||
| }, | ||
| { | ||
| type: "paragraph", | ||
| text: "8. Internal Implementation Notes", | ||
| }, | ||
| { | ||
| type: "paragraph", | ||
| text: "This Policy will be read together with Tpay\u2019s internal data retention schedule, information security policies, incident response procedures, employee confidentiality obligations, contractual data processing terms, and any privacy notice, consent wording, cookie notice, or product-specific data handling documentation issued by Tpay from time to time. Where any applicable law imposes additional or more specific obligations than those set out in this Policy, such legal requirements shall prevail to the extent of the inconsistency.", | ||
| }, |
There was a problem hiding this comment.
Empty/internal-looking sections appear in the public privacy policy.
Two sections at the end of the India privacy policy look unfinished or out of place for a public-facing document:
- Lines 384–388:
7. Document Informationis followed only by aDocument Version Historyheading-like paragraph, with no actual version entries beneath it. With the privacy notice required to be presented and be understandable independently under DPDP/DPDP Rules 2025, leaving an empty section header on the published page reads as broken. - Lines 391–396:
8. Internal Implementation Notes— the heading itself signals to a reader that internal-only material is being shown to the public. The content of the paragraph (cross-references to internal procedures) is reasonable, but the section heading should be reframed (e.g., "Related Policies" or "Interpretation") or moved to internal documentation.
Either populate Document Version History with the real entries (or remove the section), and rename Internal Implementation Notes for an external audience.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/privacy-policy/page.tsx` around lines 381 - 396, The array entries
containing paragraphs with text "7. Document Information" followed by "Document
Version History" and "8. Internal Implementation Notes" are exposing
empty/internal headings; either remove the "7. Document Information" and
"Document Version History" entries (or replace "Document Version History" with a
real, populated version-history entry), and rename the "8. Internal
Implementation Notes" paragraph heading to a public-facing label such as
"Related Policies" or "Interpretation" and adjust the following paragraph text
accordingly; locate and update the objects whose text fields equal "7. Document
Information", "Document Version History", and "8. Internal Implementation Notes"
in the privacy policy content array (the paragraph objects with type:
"paragraph") to implement these changes.
|
|
||
| const indiaRefundPolicy: LegalPolicy = { | ||
| title: "Refund and Cancellation Policy - TechPay.ai India", | ||
| meta: [ | ||
| { label: "Effective Date", value: "To be updated" }, | ||
| { label: "Entity", value: "TechPay.ai India" }, | ||
| ], | ||
| summary: | ||
| "This India Refund and Cancellation Policy is placeholder content and will be updated with the final country-specific policy.", | ||
| sections: [ | ||
| { | ||
| title: "1. Cancellations", | ||
| blocks: [ | ||
| { | ||
| type: "paragraph", | ||
| text: | ||
| "Orders may be cancelled before dispatch, subject to the applicable retail partner, lender, and product terms shown during purchase.", | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| title: "2. Returns and Refunds", | ||
| blocks: [ | ||
| { | ||
| type: "paragraph", | ||
| text: | ||
| "Returns and refunds may be processed after product inspection and will follow the retail partner's return policy, payment provider rules, and applicable law.", | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| title: "3. Contact", | ||
| blocks: [ | ||
| { | ||
| type: "paragraph", | ||
| text: | ||
| "For refund or cancellation assistance, please contact TechPay.ai at contact@techpay.ai.", | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }; |
There was a problem hiding this comment.
India refund policy is placeholder content — confirm before merging to main.
The indiaRefundPolicy is explicitly placeholder text (Effective Date: "To be updated", summary saying "this is placeholder content and will be updated"). The PR is targeting main from staging, and metadata.description (line 9-10) also tells search engines / link previews that India has placeholder content. Shipping a legal page that publicly says "this is placeholder" is a compliance and brand risk for refund/cancellation terms in particular.
Either gate this page behind a feature flag for the IN market, render a "coming soon" experience instead of a fake structured policy, or hold this PR until real India copy is available.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/refund-policy/page.tsx` around lines 14 - 55, The indiaRefundPolicy
object contains placeholder legal copy (e.g., title, meta Effective Date "To be
updated", summary) and metadata.description indicates placeholder content;
either remove or hide this from main by gating rendering: wrap usage of
indiaRefundPolicy (and any metadata.description pointing to India placeholder)
behind a feature flag or environment check (e.g., IS_MARKET_IN or
showIndiaPolicy) or replace the structured policy with a simple "Coming soon"
component until finalized; update the page/component that imports/uses
indiaRefundPolicy so it conditionally renders the real policy only when the flag
is enabled and otherwise returns the coming-soon placeholder, and ensure no
placeholder metadata.description is exposed to crawlers when the feature flag is
off.
| { | ||
| type: "paragraph", | ||
| text: | ||
| "Cancellation Fee: A cancellation fee as prescribed on the TECHPAY.ai APP may apply if the order is cancelled after the specified time window and such request and fee will be at the sole desertion of the facilitating partner.", | ||
| }, |
There was a problem hiding this comment.
Typo: "desertion" → "discretion".
"...such request and fee will be at the sole desertion of the facilitating partner."
Should read "sole discretion". This appears on the live (Malaysia) tab so it will be customer-visible.
✏️ Proposed fix
- "Cancellation Fee: A cancellation fee as prescribed on the TECHPAY.ai APP may apply if the order is cancelled after the specified time window and such request and fee will be at the sole desertion of the facilitating partner.",
+ "Cancellation Fee: A cancellation fee as prescribed on the TECHPAY.ai APP may apply if the order is cancelled after the specified time window and such request and fee will be at the sole discretion of the facilitating partner.",📝 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.
| { | |
| type: "paragraph", | |
| text: | |
| "Cancellation Fee: A cancellation fee as prescribed on the TECHPAY.ai APP may apply if the order is cancelled after the specified time window and such request and fee will be at the sole desertion of the facilitating partner.", | |
| }, | |
| { | |
| type: "paragraph", | |
| text: | |
| "Cancellation Fee: A cancellation fee as prescribed on the TECHPAY.ai APP may apply if the order is cancelled after the specified time window and such request and fee will be at the sole discretion of the facilitating partner.", | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/refund-policy/page.tsx` around lines 73 - 77, In the refund policy
paragraph object (the entry with type: "paragraph" whose text begins
"Cancellation Fee: A cancellation fee as prescribed on the TECHPAY.ai APP..."),
fix the typo by replacing the word "desertion" with "discretion" so the sentence
reads "sole discretion of the facilitating partner"; update that string in
src/app/refund-policy/page.tsx accordingly.
| { | ||
| type: "paragraph", | ||
| text: | ||
| "Any disputes arising out of cancellations or returns will be subject to the exclusive jurisdiction of the courts in KL, Malaysia.", | ||
| }, | ||
| { | ||
| type: "paragraph", | ||
| text: | ||
| "For further assistance, please email us at Contact@techpay.ai.", | ||
| }, |
There was a problem hiding this comment.
Use the formal city name and consistent email casing.
Two small consistency nits in the disputes/contact block:
- "courts in KL, Malaysia" — every other policy page in this PR uses "Kuala Lumpur" (e.g., Malaysia T&C line 197). Legal venue clauses should use the formal city name.
- "Contact@techpay.ai" capitalises the local part inconsistently with
contact@techpay.aiused everywhere else in this PR (cookie/T&C/privacy). Email local parts are technically case-insensitive but the inconsistent casing looks unintentional.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/refund-policy/page.tsx` around lines 231 - 240, Update the two
paragraph entries that contain the dispute and contact lines: replace "courts in
KL, Malaysia" with "courts in Kuala Lumpur, Malaysia" in the paragraph whose
text starts "Any disputes arising out of cancellations or returns...", and
change the email local-part casing from "Contact@techpay.ai" to
"contact@techpay.ai" in the paragraph whose text starts "For further assistance,
please email us at...".
| type: "paragraph", | ||
| text: "Tpay may provide the User with a user ID and password to enable the User to access certain areas of this Site or other contents or services. The User must ensure that the user ID and password are kept confidential. Tpay may terminate the User\u2019s access at any time for any reason. The provisions regarding the disclaimer of warranty,\u00a0 the accuracy of the information, and indemnification shall survive such termination. Tpay may monitor access to the Site. All content present on this Site is the exclusive property of Tpay. The software, content, images, graphics, videos, and audio used on this Site belong to Tpay. Certain content, software, text, images, graphics, videos, and audio used on the Site may belong to the authorized service provider/partner of Tpay. No material from this Site may be copied, modified, reproduced, republished, uploaded, transmitted, posted or distributed in any form without prior written permission from Tpay. All rights not expressly granted herein are reserved. Unauthorized use of the materials appearing on this Site may violate copyright, trademark and other applicable laws, and could result in criminal or civil penalties. Tpay is a to-be-registered trademark of Tpay Platform Private Limited. This trademark may not be used in any manner without prior written consent from Tpay Platform Private Limited.If there is a conflict between the Terms and Terms of use posted for a specific area of the Site, the latter shall have precedence with respect to your use of that area of the Site.", | ||
| }, |
There was a problem hiding this comment.
Typo: missing space between sentences ("Limited.If").
Inside the long paragraph at line 31, two sentences run together without a space:
"...without prior written consent from Tpay Platform Private Limited.If there is a conflict between the Terms..."
Add a space after the period (or, better, split this very long paragraph the same way the Malaysia version does at lines 129–134 for readability).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/terms-and-conditions/page.tsx` around lines 30 - 32, The paragraph
object with type "paragraph" containing the long Tpay terms text has a missing
space after "Tpay Platform Private Limited." resulting in "Limited.If"; update
that string to insert a space after the period and, for readability, split this
very long text into two separate paragraph entries (preserving the same
surrounding structure) so the sentence starting "If there is a conflict between
the Terms..." becomes the first sentence of the new paragraph.
There was a problem hiding this comment.
3 issues found across 11 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/app/(audiences)/associate-retailer/_components/associate-application-form.tsx">
<violation number="1" location="src/app/(audiences)/associate-retailer/_components/associate-application-form.tsx:189">
P1: The submit handler shows a successful application state without validating or sending any form data, so users can get a false “Application Received” confirmation.</violation>
<violation number="2" location="src/app/(audiences)/associate-retailer/_components/associate-application-form.tsx:737">
P1: Legal/consent checkboxes are not enforced, so users can submit the application without accepting the stated terms.</violation>
</file>
<file name="src/app/about-us/_components/offices-grid.tsx">
<violation number="1" location="src/app/about-us/_components/offices-grid.tsx:136">
P2: Bottom-border calculation is incorrect for 5-column grids with partial last rows, causing inconsistent separators.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| function ConsentText({ children }: { children: ReactNode }) { | ||
| return ( | ||
| <label className="flex gap-3 rounded-2xl border border-slate-200 bg-slate-50 p-4"> | ||
| <input className="mt-1 h-4 w-4 shrink-0 accent-techpay-primary" type="checkbox" /> |
There was a problem hiding this comment.
P1: Legal/consent checkboxes are not enforced, so users can submit the application without accepting the stated terms.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/app/(audiences)/associate-retailer/_components/associate-application-form.tsx, line 737:
<comment>Legal/consent checkboxes are not enforced, so users can submit the application without accepting the stated terms.</comment>
<file context>
@@ -0,0 +1,759 @@
+function ConsentText({ children }: { children: ReactNode }) {
+ return (
+ <label className="flex gap-3 rounded-2xl border border-slate-200 bg-slate-50 p-4">
+ <input className="mt-1 h-4 w-4 shrink-0 accent-techpay-primary" type="checkbox" />
+ <span className="text-sm leading-7 text-slate-600">{children}</span>
+ </label>
</file context>
|
|
||
| function submitForm(event: FormEvent<HTMLFormElement>) { | ||
| event.preventDefault(); | ||
| setSubmitted(true); |
There was a problem hiding this comment.
P1: The submit handler shows a successful application state without validating or sending any form data, so users can get a false “Application Received” confirmation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/app/(audiences)/associate-retailer/_components/associate-application-form.tsx, line 189:
<comment>The submit handler shows a successful application state without validating or sending any form data, so users can get a false “Application Received” confirmation.</comment>
<file context>
@@ -0,0 +1,759 @@
+
+ function submitForm(event: FormEvent<HTMLFormElement>) {
+ event.preventDefault();
+ setSubmitted(true);
+ scrollToForm();
+ }
</file context>
| const isFirstDesktopColumn = index % desktopColumns === 0; | ||
| const isFirstDesktopRow = index < desktopColumns; | ||
| const shouldShowDesktopBottomBorder = | ||
| desktopColumns === 4 ? index < 4 : index + desktopColumns < itemCount; |
There was a problem hiding this comment.
P2: Bottom-border calculation is incorrect for 5-column grids with partial last rows, causing inconsistent separators.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/app/about-us/_components/offices-grid.tsx, line 136:
<comment>Bottom-border calculation is incorrect for 5-column grids with partial last rows, causing inconsistent separators.</comment>
<file context>
@@ -108,34 +117,43 @@ const Feature = ({
+ const isFirstDesktopColumn = index % desktopColumns === 0;
+ const isFirstDesktopRow = index < desktopColumns;
+ const shouldShowDesktopBottomBorder =
+ desktopColumns === 4 ? index < 4 : index + desktopColumns < itemCount;
+ const defaultHoverClassName = isFirstDesktopRow
+ ? "bg-linear-to-t from-techpay-primary/8 via-techpay-purple/6 to-transparent"
</file context>
| desktopColumns === 4 ? index < 4 : index + desktopColumns < itemCount; | |
| index < Math.floor((itemCount - 1) / desktopColumns) * desktopColumns; |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
src/components/site-footer.tsx (1)
109-111: Derive the copyright year dynamically.Hard-coding
2026will make the footer stale next year and force another content-only patch. Usingnew Date().getFullYear()keeps the footer current.Small cleanup
- <p className="text-[0.85rem] text-techpay-muted"> - © 2026 TECHPAY.ai. All rights reserved. - </p> + <p className="text-[0.85rem] text-techpay-muted"> + © {new Date().getFullYear()} TECHPAY.ai. All rights reserved. + </p>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/site-footer.tsx` around lines 109 - 111, The footer currently hard-codes "2026" inside the <p> element in the SiteFooter component; change it to render the current year dynamically by computing the year (e.g., const year = new Date().getFullYear()) and using that variable (or directly using new Date().getFullYear()) in place of the literal so the copyright text in the SiteFooter component updates automatically each year.
🤖 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/`(audiences)/associate-retailer/_components/associate-application-form.tsx:
- Around line 204-208: The submitForm handler currently prevents default and
flips UI state optimistically; change submitForm to perform the async API call
(e.g., call the existing application API helper or create a new
sendApplication/sendApplicationRequest function) and await its result before
calling setSubmitted and scrollToForm; while awaiting, set a loading state
(e.g., setIsSubmitting) to disable the submit button and prevent duplicate
submits, and on API failure show an error state/message and do not mark
submitted; update related submit logic used around lines 574-633 (the other
submit flow) to follow the same pattern: await API, setSubmitted only on
success, handle/set error on failure, and ensure cleanup/reset of loading state.
- Around line 694-733: The TextField and SelectField components are currently
missing name and required support, so browser validation and form payload
mapping don't work; update their props to accept name?: string and required?:
boolean, pass those through to the underlying <input> and <select> (i.e., in
TextField and SelectField add name and required to the prop types and
attributes), and ensure the Field primitive preserves/propagates the required
state if it manages label/asterisk rendering; also apply the same additions to
the other field primitives mentioned (the similar components around the same
area) so every form control has a name and can be marked required for native
validation and payload mapping.
In `@src/app/`(audiences)/associate-retailer/_components/associate-community.tsx:
- Around line 304-312: The primary CTA Buttons in this component (the Button
with label "Post" and the Buttons labeled "Register"/"Share") are currently
no-ops; update them so they either are disabled until wiring is complete or are
wired to real handlers: add a disabled prop when not ready, or implement and
attach concise handler functions (e.g., handlePost, handleRegister, handleShare)
that live in the same component and perform the expected action (call the API,
navigate, or show a temporary toast/modal) and pass them as onClick to the
corresponding <Button> elements to prevent broken UX.
In `@src/app/about-us/_components/offices-grid.tsx`:
- Around line 135-137: The bottom-border logic in shouldShowDesktopBottomBorder
is using a column-occupancy check and wrongly hides borders for items in
incomplete rows; replace it with row-based math: compute currentRow =
Math.floor(index / desktopColumns) and totalRows = Math.ceil(itemCount /
desktopColumns), then set shouldShowDesktopBottomBorder to currentRow <
totalRows - 1 (so only items in the last row omit the bottom border); keep
existing variables (desktopColumns, index, itemCount) and ensure this new
condition is used instead of the current index + desktopColumns check.
In `@src/components/ui/features-section-demo-1.tsx`:
- Around line 192-202: The JSX wraps feature.highlight (a ReactNode) in a <p>,
which can break when callers pass block-level elements; update the render in the
features-section-demo-1 component to use a <div> instead of <p> for the
conditional block that uses feature.highlight, preserving the existing className
composition (cn(..., highlightClassName, feature.highlightClass)) and the same
conditional check so block JSX like <div> or <ul> is valid.
---
Nitpick comments:
In `@src/components/site-footer.tsx`:
- Around line 109-111: The footer currently hard-codes "2026" inside the <p>
element in the SiteFooter component; change it to render the current year
dynamically by computing the year (e.g., const year = new Date().getFullYear())
and using that variable (or directly using new Date().getFullYear()) in place of
the literal so the copyright text in the SiteFooter component updates
automatically each year.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: af983750-a794-4be2-aa6b-d8cd6be874e9
📒 Files selected for processing (12)
src/app/(audiences)/associate-retailer/_components/associate-application-form.tsxsrc/app/(audiences)/associate-retailer/_components/associate-community.tsxsrc/app/(audiences)/associate-retailer/_components/associate-earnings-calculator.tsxsrc/app/(audiences)/associate-retailer/_components/associate-retailer-page.tsxsrc/app/(audiences)/associate-retailer/apply/page.tsxsrc/app/(audiences)/associate-retailer/community/page.tsxsrc/app/(audiences)/associate-retailer/page.tsxsrc/app/about-us/_components/offices-grid.tsxsrc/app/globals.csssrc/components/site-footer.tsxsrc/components/site-navbar.tsxsrc/components/ui/features-section-demo-1.tsx
✅ Files skipped from review due to trivial changes (1)
- src/app/globals.css
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/site-navbar.tsx
| function submitForm(event: FormEvent<HTMLFormElement>) { | ||
| event.preventDefault(); | ||
| setSubmitted(true); | ||
| scrollToForm(); | ||
| } |
There was a problem hiding this comment.
Form submit currently shows success without actually sending an application.
On Line 205–Line 207, submission is intercepted and only local UI state is flipped; the “Application Received” state is therefore optimistic with no persistence/error path.
💡 Proposed fix (submit only after API success)
+import { FormEvent, ReactNode, useRef, useState } from "react";
...
export default function AssociateApplicationForm() {
const router = useRouter();
const [activeStep, setActiveStep] = useState(1);
const [submitted, setSubmitted] = useState(false);
+ const [submitError, setSubmitError] = useState<string | null>(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
const sectionRef = useRef<HTMLElement | null>(null);
...
- function submitForm(event: FormEvent<HTMLFormElement>) {
+ async function submitForm(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
- setSubmitted(true);
+ setSubmitError(null);
+ setIsSubmitting(true);
+ const formData = new FormData(event.currentTarget);
+
+ try {
+ const response = await fetch("/api/associate-retailer/applications", {
+ method: "POST",
+ body: formData,
+ });
+
+ if (!response.ok) {
+ throw new Error("Unable to submit application");
+ }
+
+ setSubmitted(true);
+ } catch {
+ setSubmitError("We could not submit your application. Please try again.");
+ } finally {
+ setIsSubmitting(false);
+ }
scrollToForm();
}Also applies to: 574-633
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/app/`(audiences)/associate-retailer/_components/associate-application-form.tsx
around lines 204 - 208, The submitForm handler currently prevents default and
flips UI state optimistically; change submitForm to perform the async API call
(e.g., call the existing application API helper or create a new
sendApplication/sendApplicationRequest function) and await its result before
calling setSubmitted and scrollToForm; while awaiting, set a loading state
(e.g., setIsSubmitting) to disable the submit button and prevent duplicate
submits, and on API failure show an error state/message and do not mark
submitted; update related submit logic used around lines 574-633 (the other
submit flow) to follow the same pattern: await API, setSubmitted only on
success, handle/set error on failure, and ensure cleanup/reset of loading state.
| function TextField({ | ||
| hint, | ||
| label, | ||
| maxLength, | ||
| placeholder, | ||
| type = "text", | ||
| }: { | ||
| hint?: string; | ||
| label: string; | ||
| maxLength?: number; | ||
| placeholder?: string; | ||
| type?: string; | ||
| }) { | ||
| return ( | ||
| <Field label={label} hint={hint}> | ||
| <input | ||
| className={inputClass} | ||
| maxLength={maxLength} | ||
| placeholder={placeholder} | ||
| type={type} | ||
| /> | ||
| </Field> | ||
| ); | ||
| } | ||
|
|
||
| function SelectField({ | ||
| label, | ||
| options, | ||
| }: { | ||
| label: string; | ||
| options: readonly string[]; | ||
| }) { | ||
| return ( | ||
| <Field label={label}> | ||
| <select className={inputClass}> | ||
| {options.map((option) => ( | ||
| <option key={option}>{option}</option> | ||
| ))} | ||
| </select> | ||
| </Field> |
There was a problem hiding this comment.
Mandatory fields/consent are only visual right now (not enforceable).
Fields marked with * are not actually required, and controls also lack name attributes, so browser validation and reliable payload mapping are both missing.
💡 Proposed fix (add `name`/`required` support in field primitives)
function TextField({
hint,
label,
maxLength,
+ name,
placeholder,
+ required,
type = "text",
}: {
hint?: string;
label: string;
maxLength?: number;
+ name: string;
placeholder?: string;
+ required?: boolean;
type?: string;
}) {
return (
<Field label={label} hint={hint}>
<input
className={inputClass}
maxLength={maxLength}
+ name={name}
placeholder={placeholder}
+ required={required}
type={type}
/>
</Field>
);
}
...
function SelectField({
label,
+ name,
options,
+ required,
}: {
label: string;
+ name: string;
options: readonly string[];
+ required?: boolean;
}) {
return (
<Field label={label}>
- <select className={inputClass}>
+ <select className={inputClass} name={name} required={required}>
{options.map((option) => (
<option key={option}>{option}</option>
))}
</select>
</Field>
);
}
...
function TextareaField({
label,
minHeightClassName,
+ name,
placeholder,
+ required,
}: {
label: string;
minHeightClassName?: string;
+ name: string;
placeholder: string;
+ required?: boolean;
}) {
return (
<Field label={label}>
<textarea
className={`${textareaClass} ${minHeightClassName ?? ""}`}
+ name={name}
placeholder={placeholder}
+ required={required}
/>
</Field>
);
}
...
-function ConsentText({ children }: { children: ReactNode }) {
+function ConsentText({
+ children,
+ name,
+ required,
+}: {
+ children: ReactNode;
+ name: string;
+ required?: boolean;
+}) {
return (
<label className="flex gap-3 rounded-2xl border border-slate-200 bg-slate-50 p-4">
- <input className="mt-1 h-4 w-4 shrink-0 accent-techpay-primary" type="checkbox" />
+ <input
+ className="mt-1 h-4 w-4 shrink-0 accent-techpay-primary"
+ name={name}
+ required={required}
+ type="checkbox"
+ />
<span className="text-sm leading-7 text-slate-600">{children}</span>
</label>
);
}Also applies to: 737-753, 778-783
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/app/`(audiences)/associate-retailer/_components/associate-application-form.tsx
around lines 694 - 733, The TextField and SelectField components are currently
missing name and required support, so browser validation and form payload
mapping don't work; update their props to accept name?: string and required?:
boolean, pass those through to the underlying <input> and <select> (i.e., in
TextField and SelectField add name and required to the prop types and
attributes), and ensure the Field primitive preserves/propagates the required
state if it manages label/asterisk rendering; also apply the same additions to
the other field primitives mentioned (the similar components around the same
area) so every form control has a name and can be marked required for native
validation and payload mapping.
| <Button | ||
| className="!text-white [&_span]:!text-white" | ||
| rightIcon={<ArrowRight aria-hidden="true" className="h-4 w-4" />} | ||
| size="compact" | ||
| type="button" | ||
| variant="primary" | ||
| > | ||
| Post | ||
| </Button> |
There was a problem hiding this comment.
Primary community actions are no-op buttons right now.
On Line 304–Line 312 (Post) and Line 415–Line 435 (Register/Share), users can click CTAs that perform no action, which creates a broken-flow experience.
💡 Proposed interim fix (disable until wired) or replace with real handlers/links
<Button
className="!text-white [&_span]:!text-white"
+ disabled
+ aria-disabled="true"
rightIcon={<ArrowRight aria-hidden="true" className="h-4 w-4" />}
size="compact"
type="button"
variant="primary"
>
Post
</Button>
...
{"attending" in post ? (
<Button
className="!text-white [&_span]:!text-white"
+ disabled
+ aria-disabled="true"
rightIcon={<ArrowRight aria-hidden="true" className="h-4 w-4" />}
size="compact"
type="button"
variant="primary"
>
Register
</Button>
) : (
<Button
className="min-h-0 !border-0 !bg-transparent px-0 py-0 text-sm !text-slate-600 shadow-none hover:!bg-transparent [&_span]:!text-current"
+ disabled
+ aria-disabled="true"
leftIcon={<Repeat2 aria-hidden="true" className="h-4 w-4" />}
size="compact"
type="button"
variant="lightIcon"
>
Share
</Button>
)}Also applies to: 415-435
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/`(audiences)/associate-retailer/_components/associate-community.tsx
around lines 304 - 312, The primary CTA Buttons in this component (the Button
with label "Post" and the Buttons labeled "Register"/"Share") are currently
no-ops; update them so they either are disabled until wiring is complete or are
wired to real handlers: add a disabled prop when not ready, or implement and
attach concise handler functions (e.g., handlePost, handleRegister, handleShare)
that live in the same component and perform the expected action (call the API,
navigate, or show a temporary toast/modal) and pass them as onClick to the
corresponding <Button> elements to prevent broken UX.
| const shouldShowDesktopBottomBorder = | ||
| desktopColumns === 4 ? index < 4 : index + desktopColumns < itemCount; | ||
| const defaultHoverClassName = isFirstDesktopRow |
There was a problem hiding this comment.
Bottom-border logic is incorrect for incomplete last rows.
On Line 136, the condition is column-occupancy-based, not row-based. Example: with 8 items and desktopColumns=5, cards at indices 3 and 4 are in the first row but get no bottom border.
Use row math instead:
Suggested fix
- const shouldShowDesktopBottomBorder =
- desktopColumns === 4 ? index < 4 : index + desktopColumns < itemCount;
+ const desktopRow = Math.floor(index / desktopColumns);
+ const lastDesktopRow = Math.floor((itemCount - 1) / desktopColumns);
+ const shouldShowDesktopBottomBorder = desktopRow < lastDesktopRow;📝 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 shouldShowDesktopBottomBorder = | |
| desktopColumns === 4 ? index < 4 : index + desktopColumns < itemCount; | |
| const defaultHoverClassName = isFirstDesktopRow | |
| const desktopRow = Math.floor(index / desktopColumns); | |
| const lastDesktopRow = Math.floor((itemCount - 1) / desktopColumns); | |
| const shouldShowDesktopBottomBorder = desktopRow < lastDesktopRow; | |
| const defaultHoverClassName = isFirstDesktopRow |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/about-us/_components/offices-grid.tsx` around lines 135 - 137, The
bottom-border logic in shouldShowDesktopBottomBorder is using a column-occupancy
check and wrongly hides borders for items in incomplete rows; replace it with
row-based math: compute currentRow = Math.floor(index / desktopColumns) and
totalRows = Math.ceil(itemCount / desktopColumns), then set
shouldShowDesktopBottomBorder to currentRow < totalRows - 1 (so only items in
the last row omit the bottom border); keep existing variables (desktopColumns,
index, itemCount) and ensure this new condition is used instead of the current
index + desktopColumns check.
| {feature.highlight ? ( | ||
| <p | ||
| className={cn( | ||
| "mt-5 bg-techpay-primary/8 px-4 py-3 text-sm font-semibold text-techpay-primary", | ||
| highlightClassName, | ||
| feature.highlightClass | ||
| )} | ||
| > | ||
| {feature.highlight} | ||
| </p> | ||
| ) : null} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether non-text/block JSX is being passed as `highlight`.
# Expected:
# - If all usages are plain text, current behavior may appear fine.
# - If any usage passes JSX blocks, <p> wrapping is invalid and should be changed to <div>.
set -euo pipefail
rg -n --type=tsx -C2 '\bhighlight\s*:' src
rg -n --type=tsx -C2 '\bhighlight=\{' srcRepository: rtksng/techpay
Length of output: 86
🏁 Script executed:
#!/bin/bash
# Find the file and check its structure
find src -name "*features-section*" -type fRepository: rtksng/techpay
Length of output: 104
🏁 Script executed:
#!/bin/bash
# List files in the ui directory
ls -la src/components/ui/ | head -20Repository: rtksng/techpay
Length of output: 666
🏁 Script executed:
#!/bin/bash
# Read the specific file to understand the component type definition
head -250 src/components/ui/features-section-demo-1.tsx | tail -100Repository: rtksng/techpay
Length of output: 3147
🏁 Script executed:
#!/bin/bash
# Read the beginning of the file to see type definitions
head -100 src/components/ui/features-section-demo-1.tsxRepository: rtksng/techpay
Length of output: 2380
🏁 Script executed:
#!/bin/bash
# Search for Feature type or interface definition in the file
grep -n "type Feature\|interface Feature" src/components/ui/features-section-demo-1.tsxRepository: rtksng/techpay
Length of output: 137
🏁 Script executed:
#!/bin/bash
# Search for where this component is used/imported
grep -r "features-section-demo-1\|FeatureSection" src --include="*.ts" --include="*.tsx" -nRepository: rtksng/techpay
Length of output: 1511
🏁 Script executed:
#!/bin/bash
# Check solution-section.tsx for highlight usage
grep -n "highlight" src/app/_components/home/solution-section.tsx -A2 -B2Repository: rtksng/techpay
Length of output: 40
🏁 Script executed:
#!/bin/bash
# Check impact-numbers-section.tsx for highlight usage
grep -n "highlight" src/app/_components/home/impact-numbers-section.tsx -A2 -B2Repository: rtksng/techpay
Length of output: 40
🏁 Script executed:
#!/bin/bash
# Check future-of-retail for highlight usage
grep -n "highlight" src/app/future-of-retail/page.tsx -A2 -B2Repository: rtksng/techpay
Length of output: 40
🏁 Script executed:
#!/bin/bash
# Check associate-retailer-page for highlight usage
grep -n "highlight" src/app/\(audiences\)/associate-retailer/_components/associate-retailer-page.tsx -A2 -B2Repository: rtksng/techpay
Length of output: 4045
🏁 Script executed:
#!/bin/bash
# Check the other files more carefully by reading them
# Let's look for highlight in solution-section.tsx
find src -name "solution-section.tsx" -exec cat {} \;Repository: rtksng/techpay
Length of output: 4349
🏁 Script executed:
#!/bin/bash
# Check impact-numbers-section.tsx
find src -name "impact-numbers-section.tsx" -exec cat {} \;Repository: rtksng/techpay
Length of output: 3438
🏁 Script executed:
#!/bin/bash
# Check future-of-retail page.tsx
find src -name "page.tsx" -path "*/future-of-retail/*" -exec cat {} \;Repository: rtksng/techpay
Length of output: 26796
Use a neutral container for ReactNode highlight content.
On Line 193, feature.highlight (typed as ReactNode) is wrapped in <p>. If a caller passes block JSX (<div>, <ul>, etc.), this creates invalid HTML nesting. Prefer a <div> container here.
Suggested fix
- {feature.highlight ? (
- <p
+ {feature.highlight ? (
+ <div
className={cn(
"mt-5 bg-techpay-primary/8 px-4 py-3 text-sm font-semibold text-techpay-primary",
highlightClassName,
feature.highlightClass
)}
>
{feature.highlight}
- </p>
+ </div>
) : null}📝 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.
| {feature.highlight ? ( | |
| <p | |
| className={cn( | |
| "mt-5 bg-techpay-primary/8 px-4 py-3 text-sm font-semibold text-techpay-primary", | |
| highlightClassName, | |
| feature.highlightClass | |
| )} | |
| > | |
| {feature.highlight} | |
| </p> | |
| ) : null} | |
| {feature.highlight ? ( | |
| <div | |
| className={cn( | |
| "mt-5 bg-techpay-primary/8 px-4 py-3 text-sm font-semibold text-techpay-primary", | |
| highlightClassName, | |
| feature.highlightClass | |
| )} | |
| > | |
| {feature.highlight} | |
| </div> | |
| ) : null} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/ui/features-section-demo-1.tsx` around lines 192 - 202, The
JSX wraps feature.highlight (a ReactNode) in a <p>, which can break when callers
pass block-level elements; update the render in the features-section-demo-1
component to use a <div> instead of <p> for the conditional block that uses
feature.highlight, preserving the existing className composition (cn(...,
highlightClassName, feature.highlightClass)) and the same conditional check so
block JSX like <div> or <ul> is valid.
Summary by cubic
Adds health endpoints and hardens deploys with automatic health checks and dynamic base path handling. Launches the Associate Retailer program plus Contact and legal policy pages, with updated navigation/footer and UI polish.
New Features
/healthand/api/health(200 on GET/HEAD)./associate-retailer(Apply, Community, application form, earnings calculator).LegalPolicyDocument+MarketPolicyTabs(India/Malaysia).Refactors
NEXT_PUBLIC_BASE_PATH/APP_BASE_PATH, runpm2 --update-env, and use.github/scripts/healthcheck-candidates.mjsfor fallback health checks.next.config.tsderivesbasePathfromNEXT_PUBLIC_BASE_PATH(defaults to/stagingin dev, empty in prod) and appliesassetPrefixonly when needed.Written for commit 233d606. Summary will update on new commits. Review in cubic
Summary by CodeRabbit
New Features
Improvements