refactor: revamp bitcoindeepa tma#64
Conversation
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR introduces a comprehensive UI component library and rebuilds the home page and dashboard surfaces. The new ChangesShared UI Component Library
Home Page & Navigation Shell
Dashboard Pages
Dev & Configuration
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 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)
⚔️ Resolve merge conflicts
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install timed out. The project may have too many dependencies for the sandbox. 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.
Actionable comments posted: 15
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/dashboard/page.tsx`:
- Around line 312-319: The KYC check currently only redirects when the fetch
succeeds and returns data.status !== "APPROVED", allowing failures (non-OK
responses or errors) to bypass the gate; update the useEffect fetch to treat any
non-ok response or thrown error as untrusted and redirect to
router.push("/verification") — i.e., inspect the response (from
fetch("/api/user/kyc/status") with authToken) and if !r.ok or data is missing or
data.status !== "APPROVED", call router.push("/verification"), and also call
router.push("/verification") in the catch handler to fail closed.
- Around line 263-270: The query for useQuery (queryKey "subscription-current")
is returning the API shape { subscription, message } not the UI Subscription
expected by PlanCard, so normalize the response before passing it down: in the
queryFn that fetches "/api/subscription/current" (used by useQuery) extract the
inner subscription object, map/transform its fields to the Subscription UI shape
PlanCard expects (ensure properties like planName, price, startDate, endDate are
present or set to null/defaults), and return that normalized Subscription | null
instead of the raw API envelope so PlanCard receives the correct shape.
- Line 264: Replace the hard-coded query key array used in the useQuery call
(queryKey: ["subscription-current"]) with the canonical query key exported from
"`@/lib/query-keys`"; import the appropriate symbol (e.g., SUBSCRIPTION_CURRENT or
subscriptionCurrentKey) at the top of src/app/dashboard/page.tsx and use that
constant in the queryKey prop so all components share the same contract and
invalidations/refetches can reference the shared key.
- Around line 250-257: Replace the raw fetch usage inside the queryFn for
useQuery (queryKey: queryKeys.walletSummary) with the shared fetchy wrapper from
"`@/lib/fetchy`"; use the same Authorization handling (authToken) and error
handling pattern that other dashboard code uses (e.g., subscription page) so all
HTTP calls go through fetchy — locate the async queryFn in
src/app/dashboard/page.tsx (the block returning res.json()) and refactor it to
call fetchy("/api/transaction/dca-summary", { method: "GET", headers: {
Authorization: `Bearer ${authToken}` } }) and propagate/throw errors
consistently with the repo's fetchy conventions; apply the same replacement in
src/app/dashboard/subscription/page.tsx to ensure both wallet surfaces use the
identical client.
In `@src/app/dashboard/subscription/page.tsx`:
- Around line 272-281: The fetch error handling swallows KYC-block responses so
users never get redirected for verification; update the POST
/api/subscription/payhere-link handling in the async block that calls
redirectToPayHereViaPage so that after parsing result you first check for
result.redirectTo === "/verification" and, if present, call the app's
redirect/route helper to send the user to "/verification" (or
window.location.href = "/verification") before throwing or returning; otherwise
keep the existing check for res.ok and redirectToPayHereViaPage(result.link).
Also ensure the catch block does not suppress navigation — surface the error to
the user or rethrow after any needed redirect.
- Around line 217-246: Replace the manual useEffect fetch that reads
"/api/subscription/current" and calls setSubscription with a TanStack Query:
import useQuery from '`@tanstack/react-query`' and the appropriate query key
factory from '`@/lib/query-keys`' (e.g., getCurrentMembershipKey or
QUERY_KEYS.currentMembership), then implement a useQuery that uses authToken and
the query key, performs the same fetch, transforms the result into the same
shape (using calculateEndDate, packages lookup for planName/price, and fields
like payhere_sub_id, package_id, user_id, created_at, next_billing_date,
is_active), and in onSuccess update the store via setSubscription (or derive UI
directly from the query) instead of the effect; remove the effect and its empty
.catch(), ensure errors other than 404 are surfaced/handled via the query's
onError or error state, and setSelectedPlanId from the query result rather than
the effect.
In `@src/app/page.tsx`:
- Around line 21-22: The assignment to setCount overwrites a real zero because
data.count is using || which treats 0 as falsy; in the fetchy.get call handling
(around fetchy.get<any>("/api/user") and the setCount(...) call) replace the
fallback logic with a nullish check so that setCount uses data.count when it is
0 but falls back to 80 only when data.count is null or undefined (e.g., use the
?? operator or an explicit typeof/data.count === "number" check before calling
setCount).
- Around line 226-234: Restore the navigation handler so the CTAs actually route
to the dashboard: implement goToDashboard to navigate to "/dashboard" and wire
it back into ExistingUserScreen and NewUserScreen (currently passed as
onOpen/onStart). Locate the goToDashboard function and replace the no-op with a
router push call (use the appropriate router hook for this component, e.g.,
useRouter from next/navigation or useRouter from next/router depending on
whether this is a client component), ensuring the component imports the router
hook and calls router.push("/dashboard") inside goToDashboard.
In `@src/components/bottomNavigation.tsx`:
- Line 93: The active-tab check (const isActive = pathname === item.href) is too
strict for nested routes; update the logic in the BottomNavigation/item render
(where isActive is defined) to treat an item as active when the pathname equals
item.href OR when the item is not the root and the pathname starts with the
item's href (e.g., pathname === item.href || (item.href !== '/' &&
pathname.startsWith(item.href + '/'))), ensuring root ("/") still only matches
exactly to avoid false positives on all routes.
In `@src/components/ui/button.tsx`:
- Around line 46-60: Add a safe default button type to avoid implicit submit
behavior: ensure the rendered <button> uses an explicit type defaulting to
"button" while still allowing callers to override it via props (e.g., derive
type from props or use props.type ?? "button"). Update the Button component that
renders <button ref={ref} disabled={disabled || loading} ... {...props}> so the
type attribute is applied explicitly (using the component's props/type value or
default) and avoid spreading props that would be overwritten; reference the
existing button element, props, ref, disabled, loading, variantClasses and
sizeClasses when making the change.
In `@src/components/ui/category-filter.tsx`:
- Around line 19-21: The CategoryFilter and TogglePlan components render plain
<button> elements which default to type="submit" and can inadvertently submit
enclosing forms; update the button elements in
src/components/ui/category-filter.tsx (the button with onClick and cn(...)
className in the CategoryFilter component) and in
src/components/ui/toggle-plan.tsx (the toggle button inside the TogglePlan
component) to include an explicit type="button" attribute so they no longer act
as submit buttons.
In `@src/components/ui/copy-field.tsx`:
- Around line 20-30: The CopyButton renders an icon-only button (using onCopy,
CopyIcon and className) with no accessible name; update the component to add an
accessible label by adding aria-label="Copy" to the button element (or accept a
prop like label/ariaLabel and pass it to the button) so screen readers announce
the action; ensure the default remains "Copy" if no prop is provided and keep
the existing onClick/onCopy behavior.
- Around line 43-49: The clipboard write in handleCopy currently only handles
success; add a .catch(...) to navigator.clipboard.writeText(value) to handle
failures: in the catch block setCopied(false) (to avoid leaving the UI in a
copied state), log or surface the error (e.g., console.error or invoke an
onError handler if provided), and ensure onCopy?.(value) is only called on
success. Update handleCopy to include this error path so denied permissions or
write failures are handled deterministically.
In `@src/components/ui/text-field.tsx`:
- Around line 18-21: The label elements in the TextField component are currently
visual-only; update the TextField (src/components/ui/text-field.tsx) to accept
and pass an id prop to the underlying input/textarea (or generate one via
React's useId if id is not provided), set the label's htmlFor to that id, and
add aria-invalid={!!error} and aria-describedby={error ? `${id}-error` :
undefined} on the control; also ensure the error element uses the matching id
(e.g., `${id}-error`) so assistive tech can associate the label, control, and
error.
In `@src/components/ui/visible-toggle.tsx`:
- Around line 34-46: The toggle and selection controls currently only change
visual styles; make their state available to assistive tech by adding ARIA state
attributes: in VisibleToggle (the button using onToggle and prop visible) add
aria-pressed={visible} (and keep its type="button" and onClick) so screen
readers know it’s a toggle; in the plan card component (PlanCard or whatever
component uses selection/onSelect) expose selection via an appropriate role and
state—e.g., role="radio" or role="button" plus aria-checked={selected} (or
aria-pressed for a toggle-like card) and ensure keyboard activation uses the
same onSelect handler—so the semantic state matches the visual state.
🪄 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: 657635cc-4acc-4e72-8b6c-9c8122443670
⛔ Files ignored due to path filters (6)
bun.lockbis excluded by!**/bun.lockbpackage-lock.jsonis excluded by!**/package-lock.jsonpublic/BDLogo_Black.svgis excluded by!**/*.svgpublic/bd-wordmark-light.pngis excluded by!**/*.pngpublic/btc-coin-3d.pngis excluded by!**/*.pngpublic/gift-emoji-3d.pngis excluded by!**/*.png
📒 Files selected for processing (17)
package.jsonsrc/app/dashboard/page.tsxsrc/app/dashboard/subscription/page.tsxsrc/app/dev/page.tsxsrc/app/page.tsxsrc/components/LoadingPage.tsxsrc/components/bottomNavigation.tsxsrc/components/ui/button.tsxsrc/components/ui/category-filter.tsxsrc/components/ui/clickable-card.tsxsrc/components/ui/copy-field.tsxsrc/components/ui/page-title.tsxsrc/components/ui/plan-card.tsxsrc/components/ui/search-field.tsxsrc/components/ui/text-field.tsxsrc/components/ui/toggle-plan.tsxsrc/components/ui/visible-toggle.tsx
| const { data: summary, isLoading } = useQuery<DCSummary>({ | ||
| queryKey: queryKeys.walletSummary, | ||
| queryFn: async () => { | ||
| const res = await fetch("/api/transaction/dca-summary", { | ||
| headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}` }, | ||
| }); | ||
| if (!res.ok) throw new Error("Failed to fetch wallet summary"); | ||
| return res.json(); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use the shared HTTP client consistently across src/app/dashboard/page.tsx and src/app/dashboard/subscription/page.tsx.
Both wallet surfaces introduce new raw fetch calls for API traffic even though the repo standardizes on fetchy. That splits request/auth/error behavior across two clients in the same flow and makes future fixes/instrumentation inconsistent across src/app/dashboard/page.tsx and src/app/dashboard/subscription/page.tsx.
As per coding guidelines, "All HTTP requests must use the custom fetchy wrapper from @/lib/fetchy".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/dashboard/page.tsx` around lines 250 - 257, Replace the raw fetch
usage inside the queryFn for useQuery (queryKey: queryKeys.walletSummary) with
the shared fetchy wrapper from "`@/lib/fetchy`"; use the same Authorization
handling (authToken) and error handling pattern that other dashboard code uses
(e.g., subscription page) so all HTTP calls go through fetchy — locate the async
queryFn in src/app/dashboard/page.tsx (the block returning res.json()) and
refactor it to call fetchy("/api/transaction/dca-summary", { method: "GET",
headers: { Authorization: `Bearer ${authToken}` } }) and propagate/throw errors
consistently with the repo's fetchy conventions; apply the same replacement in
src/app/dashboard/subscription/page.tsx to ensure both wallet surfaces use the
identical client.
Source: Coding guidelines
| const { data: subscription } = useQuery<Subscription | null>({ | ||
| queryKey: ["subscription-current"], | ||
| queryFn: async () => { | ||
| const res = await fetch("/api/subscription/current", { | ||
| headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}` }, | ||
| }); | ||
| if (!res.ok) return null; | ||
| return res.json(); |
There was a problem hiding this comment.
Normalize /api/subscription/current before passing it to PlanCard.
This query is typed as Subscription | null, but the route returns { subscription, message }, and the inner payload is not the same UI shape that PlanCard reads. As written, the card is consuming the wrong object and will look up fields like planName, price, startDate, and endDate on properties that are not there.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/dashboard/page.tsx` around lines 263 - 270, The query for useQuery
(queryKey "subscription-current") is returning the API shape { subscription,
message } not the UI Subscription expected by PlanCard, so normalize the
response before passing it down: in the queryFn that fetches
"/api/subscription/current" (used by useQuery) extract the inner subscription
object, map/transform its fields to the Subscription UI shape PlanCard expects
(ensure properties like planName, price, startDate, endDate are present or set
to null/defaults), and return that normalized Subscription | null instead of the
raw API envelope so PlanCard receives the correct shape.
| }); | ||
|
|
||
| const { data: subscription } = useQuery<Subscription | null>({ | ||
| queryKey: ["subscription-current"], |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Source the subscription query key from @/lib/query-keys.
Hard-coding ["subscription-current"] makes this cache key local knowledge inside one page. Any invalidation/refetch from other wallet surfaces now has to duplicate the literal instead of sharing the repo’s query-key contract.
As per coding guidelines, "Use TanStack Query for server state with query keys sourced from @/lib/query-keys".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/dashboard/page.tsx` at line 264, Replace the hard-coded query key
array used in the useQuery call (queryKey: ["subscription-current"]) with the
canonical query key exported from "`@/lib/query-keys`"; import the appropriate
symbol (e.g., SUBSCRIPTION_CURRENT or subscriptionCurrentKey) at the top of
src/app/dashboard/page.tsx and use that constant in the queryKey prop so all
components share the same contract and invalidations/refetches can reference the
shared key.
Source: Coding guidelines
| useEffect(() => { | ||
| if (!authToken) return; | ||
| fetch("/api/user/kyc/status", { | ||
| headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}` }, | ||
| }) | ||
| .then((r) => (r.ok ? r.json() : null)) | ||
| .then((data) => { if (data && data.status !== "APPROVED") router.push("/verification"); }) | ||
| .catch(() => {}); |
There was a problem hiding this comment.
Fail closed when the KYC status check cannot be trusted.
The redirect only happens when a successful response says status !== "APPROVED". If the request returns 401/500 or throws, the code falls through and the wallet still renders, so the KYC gate disappears whenever the check is unavailable.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/dashboard/page.tsx` around lines 312 - 319, The KYC check currently
only redirects when the fetch succeeds and returns data.status !== "APPROVED",
allowing failures (non-OK responses or errors) to bypass the gate; update the
useEffect fetch to treat any non-ok response or thrown error as untrusted and
redirect to router.push("/verification") — i.e., inspect the response (from
fetch("/api/user/kyc/status") with authToken) and if !r.ok or data is missing or
data.status !== "APPROVED", call router.push("/verification"), and also call
router.push("/verification") in the catch handler to fail closed.
| useEffect(() => { | ||
| if (!authToken || packages.length === 0) return; | ||
| fetch("/api/subscription/current", { | ||
| headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}` }, | ||
| }) | ||
| .then((r) => r.json()) | ||
| .then((result) => { | ||
| if (result.subscription) { | ||
| const sub = result.subscription; | ||
| const pkg = packages.find((p) => p.id === sub.package_id); | ||
| setSubscription({ | ||
| id: sub.payhere_sub_id, | ||
| planName: pkg?.name ?? "Unknown Plan", | ||
| planType: sub.frequency ?? "monthly", | ||
| price: pkg?.amount ?? 0, | ||
| currency: "Rs", | ||
| startDate: sub.created_at, | ||
| endDate: sub.next_billing_date ?? (pkg ? calculateEndDate(sub.created_at, pkg.type) : sub.updated_at), | ||
| isActive: sub.is_active, | ||
| packageId: sub.package_id, | ||
| userId: sub.user_id, | ||
| payhereSubId: sub.payhere_sub_id, | ||
| }); | ||
| setSelectedPlanId(sub.package_id); | ||
| } else { | ||
| setSubscription(null); | ||
| } | ||
| }) | ||
| .catch(() => {}); | ||
| }, [authToken, packages, setSubscription, calculateEndDate]); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift
Model current membership as a TanStack query instead of effect-managed store state.
This is server state, but it is fetched in an effect, normalized into the store, and treated as null even when the backend returns a non-404 error. The dashboard page reads the same resource through React Query, so the app now has two different sources of truth for current membership.
As per coding guidelines, "Use TanStack Query for server state with query keys sourced from @/lib/query-keys".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/dashboard/subscription/page.tsx` around lines 217 - 246, Replace the
manual useEffect fetch that reads "/api/subscription/current" and calls
setSubscription with a TanStack Query: import useQuery from
'`@tanstack/react-query`' and the appropriate query key factory from
'`@/lib/query-keys`' (e.g., getCurrentMembershipKey or
QUERY_KEYS.currentMembership), then implement a useQuery that uses authToken and
the query key, performs the same fetch, transforms the result into the same
shape (using calculateEndDate, packages lookup for planName/price, and fields
like payhere_sub_id, package_id, user_id, created_at, next_billing_date,
is_active), and in onSuccess update the store via setSubscription (or derive UI
directly from the query) instead of the effect; remove the effect and its empty
.catch(), ensure errors other than 404 are surfaced/handled via the query's
onError or error state, and setSelectedPlanId from the query result rather than
the effect.
Source: Coding guidelines
| <button | ||
| onClick={onClick} | ||
| className={cn( |
There was a problem hiding this comment.
Shared root cause: implicit submit buttons in reusable controls.
src/components/ui/category-filter.tsx and src/components/ui/toggle-plan.tsx both render <button> elements without type="button", so they can accidentally submit enclosing forms. Set explicit button types in both files.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/ui/category-filter.tsx` around lines 19 - 21, The
CategoryFilter and TogglePlan components render plain <button> elements which
default to type="submit" and can inadvertently submit enclosing forms; update
the button elements in src/components/ui/category-filter.tsx (the button with
onClick and cn(...) className in the CategoryFilter component) and in
src/components/ui/toggle-plan.tsx (the toggle button inside the TogglePlan
component) to include an explicit type="button" attribute so they no longer act
as submit buttons.
| <button | ||
| onClick={onCopy} | ||
| className={cn( | ||
| "flex items-center justify-center size-9 rounded-[12px]", | ||
| "bg-white border border-[#e2e8f0] transition-colors", | ||
| "hover:bg-[#f8fafc] active:bg-[#f1f5f9]", | ||
| className | ||
| )} | ||
| > | ||
| <CopyIcon /> | ||
| </button> |
There was a problem hiding this comment.
Add an accessible name to the icon-only copy button.
At Line 20, CopyButton has no text label, so screen readers won’t announce the action. Add aria-label="Copy" (or accept a label prop).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/ui/copy-field.tsx` around lines 20 - 30, The CopyButton
renders an icon-only button (using onCopy, CopyIcon and className) with no
accessible name; update the component to add an accessible label by adding
aria-label="Copy" to the button element (or accept a prop like label/ariaLabel
and pass it to the button) so screen readers announce the action; ensure the
default remains "Copy" if no prop is provided and keep the existing
onClick/onCopy behavior.
| const handleCopy = () => { | ||
| navigator.clipboard.writeText(value).then(() => { | ||
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 2000); | ||
| onCopy?.(value); | ||
| }); | ||
| }; |
There was a problem hiding this comment.
Handle clipboard-write failures to avoid silent failure paths.
At Line 44, navigator.clipboard.writeText(value) only handles success. Add a .catch(...) branch so denied permissions/failures don’t leave the UX in an undefined state.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/ui/copy-field.tsx` around lines 43 - 49, The clipboard write
in handleCopy currently only handles success; add a .catch(...) to
navigator.clipboard.writeText(value) to handle failures: in the catch block
setCopied(false) (to avoid leaving the UI in a copied state), log or surface the
error (e.g., console.error or invoke an onError handler if provided), and ensure
onCopy?.(value) is only called on success. Update handleCopy to include this
error path so denied permissions or write failures are handled
deterministically.
| {label && ( | ||
| <label className="text-[12px] font-normal text-[#475569] leading-[16px]"> | ||
| {label} | ||
| </label> |
There was a problem hiding this comment.
Associate labels/errors with inputs for accessible form semantics.
At Line 19 and Line 62, the <label> elements are visual only. At Line 23 and Line 66, the controls should expose id, aria-invalid, and aria-describedby (when error exists) so assistive tech can connect label/error to the field.
Proposed patch
const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
({ className, state = "default", label, error, disabled, ...props }, ref) => {
const isDisabled = disabled || state === "disabled";
+ const inputId = props.id ?? props.name;
+ const errorId = error && inputId ? `${inputId}-error` : undefined;
return (
<div className="flex flex-col gap-1 w-full">
{label && (
- <label className="text-[12px] font-normal text-[`#475569`] leading-[16px]">
+ <label htmlFor={inputId} className="text-[12px] font-normal text-[`#475569`] leading-[16px]">
{label}
</label>
)}
<input
ref={ref}
+ id={inputId}
disabled={isDisabled}
+ aria-invalid={!!error}
+ aria-describedby={errorId}
className={cn(
@@
{error && (
- <p className="text-[12px] text-[`#f13131`] leading-[16px]">{error}</p>
+ <p id={errorId} className="text-[12px] text-[`#f13131`] leading-[16px]">{error}</p>
)}
</div>
);
}
);
@@
const TextAreaField = forwardRef<HTMLTextAreaElement, TextAreaFieldProps>(
({ className, state = "default", label, error, disabled, ...props }, ref) => {
const isDisabled = disabled || state === "disabled";
+ const inputId = props.id ?? props.name;
+ const errorId = error && inputId ? `${inputId}-error` : undefined;
return (
<div className="flex flex-col gap-1 w-full">
{label && (
- <label className="text-[12px] font-normal text-[`#475569`] leading-[16px]">
+ <label htmlFor={inputId} className="text-[12px] font-normal text-[`#475569`] leading-[16px]">
{label}
</label>
)}
<textarea
ref={ref}
+ id={inputId}
disabled={isDisabled}
+ aria-invalid={!!error}
+ aria-describedby={errorId}
@@
{error && (
- <p className="text-[12px] text-[`#f13131`] leading-[16px]">{error}</p>
+ <p id={errorId} className="text-[12px] text-[`#f13131`] leading-[16px]">{error}</p>
)}
</div>
);
}
);Also applies to: 23-41, 61-65, 66-84
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/ui/text-field.tsx` around lines 18 - 21, The label elements in
the TextField component are currently visual-only; update the TextField
(src/components/ui/text-field.tsx) to accept and pass an id prop to the
underlying input/textarea (or generate one via React's useId if id is not
provided), set the label's htmlFor to that id, and add aria-invalid={!!error}
and aria-describedby={error ? `${id}-error` : undefined} on the control; also
ensure the error element uses the matching id (e.g., `${id}-error`) so assistive
tech can associate the label, control, and error.
| <button | ||
| type="button" | ||
| onClick={onToggle} | ||
| className={cn( | ||
| "flex items-center justify-center w-9 h-9 rounded-[12px] transition-colors", | ||
| dark | ||
| ? "bg-transparent hover:bg-white/5 active:bg-white/10" | ||
| : "bg-white border border-[#e2e8f0] drop-shadow-[1px_1px_3px_rgba(203,213,225,0.3)] hover:bg-[#f8fafc] active:bg-[#f1f5f9]", | ||
| className | ||
| )} | ||
| > | ||
| {visible ? <EyeOpen dark={dark} /> : <EyeClosed dark={dark} />} | ||
| </button> |
There was a problem hiding this comment.
Shared root cause: interactive state is visual-only in src/components/ui/visible-toggle.tsx and src/components/ui/plan-card.tsx.
Both components rely on visual styling to convey toggle/selection state but do not fully expose that state via ARIA. Please add semantic state attributes so the controls are operable and understandable with assistive technologies.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/ui/visible-toggle.tsx` around lines 34 - 46, The toggle and
selection controls currently only change visual styles; make their state
available to assistive tech by adding ARIA state attributes: in VisibleToggle
(the button using onToggle and prop visible) add aria-pressed={visible} (and
keep its type="button" and onClick) so screen readers know it’s a toggle; in the
plan card component (PlanCard or whatever component uses selection/onSelect)
expose selection via an appropriate role and state—e.g., role="radio" or
role="button" plus aria-checked={selected} (or aria-pressed for a toggle-like
card) and ensure keyboard activation uses the same onSelect handler—so the
semantic state matches the visual state.
Summary by CodeRabbit
Release Notes
New Features
Refactor
Style