feat: loading states and error handling #200#211
Conversation
📝 WalkthroughWalkthroughReplaces internal mock property rendering with prop-driven hooks (useProperties, useApiCall), adds loading/error UI and retry flows, updates dashboards and search to consume API hooks, introduces Storybook and Vitest configs, and adds related stories and tests. Changes
Sequence Diagram(s)mermaid Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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: 1
🤖 Fix all issues with AI agents
In `@apps/web/src/components/ui/loading-skeleton.tsx`:
- Around line 43-49: LoadingGrid uses a dynamic Tailwind class
xl:grid-cols-${columns} (which will be purged) and unstable keys generated by
crypto.randomUUID() (causing remounts); replace the dynamic class with a mapping
from allowed columns to static class names (e.g., map 1..6 to
"xl:grid-cols-1".."xl:grid-cols-6" and default to a safe value) and use stable
keys for the skeletons by iterating with Array.from({ length: count }).map((_,
idx) => ...) and keying PropertyCardSkeleton with a stable identifier like
`skeleton-${idx}`; update the LoadingGrid component to compute the static
colClass and use it in the container className and remove crypto.randomUUID()
usage.
🧹 Nitpick comments (3)
apps/web/src/hooks/useDashboard.ts (1)
180-226: Consider reusing bookings data to avoid duplicate/api/bookingsretries.
refreshAlltriggers both bookings and stats fetches, which can double the booking calls (and retries). If bookings are already present, consider deriving stats from state or passing the fresh bookings payload intofetchStats.apps/web/src/hooks/useApiCall.ts (2)
3-8: Improve type safety for callback parameters.The
onSuccesscallback acceptsunknown, but it receives data of typeT. Consider making the interface generic to provide proper type inference for consumers.Suggested improvement
-interface UseApiCallOptions { +interface UseApiCallOptions<T> { retryCount?: number; retryDelay?: number; - onSuccess?: (data: unknown) => void; + onSuccess?: (data: T) => void; onError?: (error: Error) => void; }Then update the hook signature:
export function useApiCall<T>( apiFunction: (...args: unknown[]) => Promise<T>, - options: UseApiCallOptions = {} + options: UseApiCallOptions<T> = {} ): UseApiCallReturn<T> {
97-118: Consider standardizing parameter naming with the hook.The utility uses
maxRetrieswhileuseApiCallusesretryCount. Both correctly represent the number of retry attempts (not total attempts), but the inconsistent naming could confuse consumers.Suggested change for consistency
export async function retryApiCall<T>( apiFunction: () => Promise<T>, - maxRetries = 3, + retryCount = 3, delay = 1000 ): Promise<T> { let lastError: Error = new Error('API call failed'); - for (let attempt = 0; attempt <= maxRetries; attempt++) { + for (let attempt = 0; attempt <= retryCount; attempt++) { try { return await apiFunction(); } catch (error) { lastError = error instanceof Error ? error : new Error('Unknown error'); - if (attempt < maxRetries) { - console.log(`Retry attempt ${attempt + 1} of ${maxRetries}...`); + if (attempt < retryCount) { + console.log(`Retry attempt ${attempt + 1} of ${retryCount}...`); await new Promise((resolve) => setTimeout(resolve, delay)); } } } throw lastError; }
| export const LoadingGrid = ({ count = 8, columns = 4 }: LoadingGridProps) => { | ||
| return ( | ||
| <div className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-${columns} gap-8`}> | ||
| {Array.from({ length: count }, () => crypto.randomUUID()).map((id) => ( | ||
| <PropertyCardSkeleton key={id} /> | ||
| ))} | ||
| </div> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check Tailwind configuration for safelists and grid-cols classes
find . -name 'tailwind.config.*' -o -name 'tailwind.config.ts' -o -name 'tailwind.config.js' | head -20Repository: Stellar-Rent/stellar-rent
Length of output: 98
🏁 Script executed:
# Search for Tailwind config files
fd -n 'tailwind.config\.' .Repository: Stellar-Rent/stellar-rent
Length of output: 299
🏁 Script executed:
# Look for any existing safelist configurations
rg -n 'safelist' . -t ts -t js -t tsx -t jsxRepository: Stellar-Rent/stellar-rent
Length of output: 96
🏁 Script executed:
# Check for xl:grid-cols usage in the codebase
rg -n 'xl:grid-cols' apps/web/src -g'*.{ts,tsx,js,jsx}'Repository: Stellar-Rent/stellar-rent
Length of output: 650
🏁 Script executed:
# Examine the loading-skeleton file context
cat -n apps/web/src/components/ui/loading-skeleton.tsxRepository: Stellar-Rent/stellar-rent
Length of output: 2967
🏁 Script executed:
# Read the Tailwind configuration to check for safelist and content settings
cat -n apps/web/tailwind.config.jsRepository: Stellar-Rent/stellar-rent
Length of output: 2776
🏁 Script executed:
# Check if there are any other safelist configurations in the codebase
rg -i 'safelist|content.*grid-cols' . -t jsRepository: Stellar-Rent/stellar-rent
Length of output: 51
Fix dynamic Tailwind class and unstable keys in LoadingGrid.
The dynamic class xl:grid-cols-${columns} will be purged by Tailwind in production builds since the Tailwind config has no safelist and relies on static content scanning. Additionally, crypto.randomUUID() regenerates keys on every render, causing unnecessary remounts of PropertyCardSkeleton.
Map columns to static class names and use stable indices for skeleton keys:
♻️ Refactor
export const LoadingGrid = ({ count = 8, columns = 4 }: LoadingGridProps) => {
+ const columnClasses: Record<number, string> = {
+ 1: 'xl:grid-cols-1',
+ 2: 'xl:grid-cols-2',
+ 3: 'xl:grid-cols-3',
+ 4: 'xl:grid-cols-4',
+ 5: 'xl:grid-cols-5',
+ 6: 'xl:grid-cols-6',
+ };
+ const xlColumnsClass = columnClasses[columns] ?? 'xl:grid-cols-4';
+
return (
- <div className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-${columns} gap-8`}>
- {Array.from({ length: count }, () => crypto.randomUUID()).map((id) => (
- <PropertyCardSkeleton key={id} />
+ <div className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 ${xlColumnsClass} gap-8`}>
+ {Array.from({ length: count }).map((_, idx) => (
+ <PropertyCardSkeleton key={idx} />
))}
</div>
);
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const LoadingGrid = ({ count = 8, columns = 4 }: LoadingGridProps) => { | |
| return ( | |
| <div className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-${columns} gap-8`}> | |
| {Array.from({ length: count }, () => crypto.randomUUID()).map((id) => ( | |
| <PropertyCardSkeleton key={id} /> | |
| ))} | |
| </div> | |
| export const LoadingGrid = ({ count = 8, columns = 4 }: LoadingGridProps) => { | |
| const columnClasses: Record<number, string> = { | |
| 1: 'xl:grid-cols-1', | |
| 2: 'xl:grid-cols-2', | |
| 3: 'xl:grid-cols-3', | |
| 4: 'xl:grid-cols-4', | |
| 5: 'xl:grid-cols-5', | |
| 6: 'xl:grid-cols-6', | |
| }; | |
| const xlColumnsClass = columnClasses[columns] ?? 'xl:grid-cols-4'; | |
| return ( | |
| <div className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 ${xlColumnsClass} gap-8`}> | |
| {Array.from({ length: count }).map((_, idx) => ( | |
| <PropertyCardSkeleton key={idx} /> | |
| ))} | |
| </div> | |
| ); | |
| }; |
🤖 Prompt for AI Agents
In `@apps/web/src/components/ui/loading-skeleton.tsx` around lines 43 - 49,
LoadingGrid uses a dynamic Tailwind class xl:grid-cols-${columns} (which will be
purged) and unstable keys generated by crypto.randomUUID() (causing remounts);
replace the dynamic class with a mapping from allowed columns to static class
names (e.g., map 1..6 to "xl:grid-cols-1".."xl:grid-cols-6" and default to a
safe value) and use stable keys for the skeletons by iterating with Array.from({
length: count }).map((_, idx) => ...) and keying PropertyCardSkeleton with a
stable identifier like `skeleton-${idx}`; update the LoadingGrid component to
compute the static colClass and use it in the container className and remove
crypto.randomUUID() usage.
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/components/search/PropertyGrid.tsx (1)
203-224: Potential duplicate keys when parsing string IDs fails.On line 206, if
Number.parseInt(property.id, 10)returnsNaNfor multiple properties, they'll all default toid: 0, causing duplicate React keys on line 223. Consider preserving the original string id or generating a unique fallback.🐛 Suggested fix
- id: typeof property.id === 'string' ? Number.parseInt(property.id, 10) || 0 : property.id, + id: typeof property.id === 'string' ? Number.parseInt(property.id, 10) || property.id : property.id,Alternatively, use
property.iddirectly as the key since it's already unique:- return <PropertyCard key={property.id} property={cardProperty} />; + return <PropertyCard key={String(property.id)} property={cardProperty} />;
🧹 Nitpick comments (2)
apps/web/src/components/search/PropertyGrid.tsx (2)
190-198: Minor: Redundant null check.The
!displayPropertiescheck is unnecessary sincedisplayPropertiesis guaranteed to be defined after line 188 (it either holdspropertiesormockProperties). ThedisplayProperties.length === 0check alone suffices.♻️ Suggested simplification
- if (!displayProperties || displayProperties.length === 0) { + if (displayProperties.length === 0) {
221-221: Consider defaultingverifiedtofalseinstead oftrue.Defaulting
verifiedtotruewhen undefined could misleadingly display unverified properties with a verified badge. A safer default would befalseto avoid misrepresenting property verification status.♻️ Suggested change
- verified: property.verified !== undefined ? property.verified : true, + verified: property.verified ?? false,
respp
left a comment
There was a problem hiding this comment.
You should connect the PropertyGrid with the loading and error states in page.tsx, and ensure that isLoading and error props are correctly passed to the PropertyGrid within search/page.tsx. Additionally, it is crucial to verify that the dashboard components are properly displaying errors caught by the useDashboard hook. Medium Priority: To improve consistency, use the InlineError component in the SearchBar instead of the current custom div, and consider creating a useProperties hook for the main landing page if one doesn't exist yet. Optional Improvements: Further polish could include adding unit tests for the newly created components and documenting them in Storybook where applicable. While the components and hooks are well-implemented, final integration in a few key areas is still required to bring the task to the completion.
|
There will be some conflicts to resolve as we merged some big changes. |
06275b8 to
ebd3e71
Compare
There was a problem hiding this comment.
Actionable comments posted: 16
🤖 Fix all issues with AI agents
In `@apps/web/biome_errors.txt`:
- Around line 1-110: Delete the generated lint output file from the PR and fix
the root TypeScript and formatting issues in src/app/tenant-dashboard/page.tsx:
replace the any types by adding a Booking type/interface and use it for the
booking parameter in handleViewDetails and handleCancelBooking (these functions
reference apiBookings and booking.id), remove the `as any` assertions passed
into <ProfileManagement> (user prop, onUpdateProfile which is handleUpdateUser,
and transactions) by typing user, handleUpdateUser, and transactions correctly
according to the ProfileManagement prop types, and run the code
formatter/Prettier to fix the reported JSX formatting inconsistencies.
In `@apps/web/package.json`:
- Around line 12-16: package.json currently pins React 18 while Next.js 15.5.3
(App Router) requires React 19; update the dependency entries "react" and
"react-dom" in package.json to a React 19 compatible version (e.g., ^19.0.0) and
ensure any peer/dependency constraints (like next) are aligned, or alternatively
downgrade the "next" version to a release that supports React 18; after changing
versions, run yarn/npm install and re-run Storybook and tests to confirm no
mismatched React bundles (fix any lockfile or hoisting issues so the project
uses the same React 19 for Next and Storybook).
In `@apps/web/src/app/dashboard/host-dashboard/page.tsx`:
- Around line 66-81: The bookings mapping currently hard-codes propertyId and
canCancel; update the mapping that builds bookings (where bookings is derived
from apiBookings in page.tsx) to pull propertyId from the API object (e.g.,
b.propertyId or b.property?.id) instead of the literal '1', and compute
canCancel based on the booking status (for example allow cancel when status is
'pending' or 'confirmed' and disallow when 'completed' or 'cancelled'); ensure
types align (Booking.propertyId type) and that canReview remains derived from
b.status === 'completed'.
In `@apps/web/src/app/dashboard/tenant-dashboard/page.tsx`:
- Around line 267-275: The refresh icon button lacks an accessible label and
isn't disabled during loading; update the JSX for the button that uses
handleRefresh and the RefreshCw icon to include an aria-label (e.g.,
aria-label="Refresh bookings") and set disabled={isLoadingBookings ||
isLoadingProfile} (also add visually/semantically appropriate classes when
disabled or use aria-disabled if needed) so the button is both screen-reader
friendly and prevented from being clicked while loading.
- Around line 181-249: handleCancelBooking, handleUpdateProfile, and
handleUploadAvatar always emit success notifications even though
apiCancelBooking, apiUpdateProfile, and apiUploadAvatar return boolean success
flags rather than throwing; update each handler to capture the returned boolean
(e.g., const success = await apiCancelBooking(...)) and only create/set
notifications and increment unread when success === true, otherwise handle the
false case (log an informative error via console.error or show a failure
notification) so failures do not produce success messages; include these checks
in the functions handleCancelBooking, handleUpdateProfile, and
handleUploadAvatar and ensure setNotifications and setUnreadNotifications are
only called on successful API responses.
In `@apps/web/src/app/tenant-dashboard/page.tsx`:
- Around line 79-94: The booking mapping currently hardcodes propertyId and
canCancel; update the transform in the bookings mapping (the const bookings =
apiBookings.map(...) block) to use real API fields instead of literals: set
propertyId to b.propertyId (or the actual API key if named differently) and
derive canCancel from API-provided metadata (e.g., b.canCancel) or from booking
status/dates (e.g., status === 'upcoming' and within cancellation window) rather
than always true; keep canReview as is (b.status === 'completed') if desired.
In `@apps/web/src/components/dashboard/ProfileManagement.tsx`:
- Around line 732-749: The Delete Account button can appear with no handler,
producing a no-op; update the JSX in ProfileManagement to guard against a
missing onDeleteAccount by either not rendering the button when onDeleteAccount
is falsy or explicitly disabling it (e.g., add disabled={!onDeleteAccount ||
isLoading} and set aria-disabled when !onDeleteAccount), and adjust the onClick
wrapper so it only calls await onDeleteAccount() when onDeleteAccount exists;
reference the button block with onClick, disabled={isLoading}, isLoading, and
the onDeleteAccount prop to locate the change.
In `@apps/web/src/components/guards/RoleGuard.tsx`:
- Around line 43-65: The current RoleGuard only enforces access when
requiredRole === 'host' (using requiredRole, canAccessHostDashboard,
fallbackPath, children), leaving 'guest' and 'dual' unprotected; either restrict
the prop type of requiredRole to only 'host' (update the RoleGuard props/type
signature and any callers to pass only 'host') or extend the guard logic to
enforce other roles by adding checks for 'guest' and 'dual' (e.g., evaluate user
role/permissions and gate rendering like the host branch, redirect to
fallbackPath or show appropriate UI), and clarify whether this RoleGuard is
intended to replace the older RoleGuard in hooks/auth/—if replacing, update
imports/usages across the codebase to use this file and match the enforcement
semantics of the original.
In `@apps/web/src/components/search/PropertyCard.tsx`:
- Around line 49-51: The component currently hardcodes "(100+)" instead of using
real review data; update the FullPropertyProps type to include a reviewCount (or
reviews) optional numeric field and then change the PropertyCard.tsx rendering
(where Star and {property.rating} are used) to display the dynamic value e.g.
use property.reviewCount with a safe fallback (like 0 or "—") and format it
appropriately so the span shows the real review count instead of the hardcoded
text.
In `@apps/web/src/components/shared/Testimonials.tsx`:
- Line 22: The Testimonials component references three WebP avatar files that
are missing, causing broken images; add the three image files named exactly
avatar-1.webp, avatar-2.webp, and avatar-3.webp into the public/images directory
used by the web app so the paths '/images/avatar-1.webp',
'/images/avatar-2.webp', and '/images/avatar-3.webp' resolve at runtime; ensure
the filenames and casing match the references in the Testimonials component and
that the files are committed to the repo.
In `@apps/web/src/hooks/__tests__/useDashboard.test.ts`:
- Around line 78-87: The test for useDashboard (and the adjacent updateProfile
test) relies on stale fetch mock implementations because beforeEach only calls
mockClear(); fix by ensuring fetch mocks are reset or explicitly stubbed per
test: either change test setup to call fetch.mockReset() (or
jest.resetAllMocks()) in beforeEach to clear implementations, or add explicit
fetch.mockImplementationOnce handlers in this test for the hook's initial
refetchAll calls to /api/bookings and /api/profile before calling
result.current.cancelBooking('test-id'); reference the useDashboard hook, its
useEffect/refetchAll behavior, and the cancelBooking method when adding the
mocks.
In `@apps/web/src/hooks/__tests__/useProperties.test.ts`:
- Around line 6-8: vi.useFakeTimers() is called at module scope in the test file
which can leak fake timers to other suites; add a cleanup to restore real timers
(e.g., add an afterAll or afterEach hook that calls vi.useRealTimers()) so that
the fake timer set by vi.useFakeTimers() is reverted after the tests run and
prevents cross-test leakage.
In `@apps/web/src/hooks/useBookingDetails.ts`:
- Line 52: The fallback image reference in useBookingDetails
(data.property?.image || '/images/property-placeholder.webp') points to a
missing asset; either add a placeholder WebP at
apps/web/public/images/property-placeholder.webp or change the fallback to an
existing asset path (e.g., an existing file under public/images) and update the
string in useBookingDetails accordingly so the fallback resolves at runtime.
In `@apps/web/src/hooks/useDashboard.ts`:
- Around line 74-121: The calls to retryApiCall in fetchBookings and
fetchProfile currently pass 3, but retryApiCall uses attempt <= maxRetries so
that value causes 4 total attempts; update both call sites in fetchBookings and
fetchProfile to pass 2 so there are 3 total attempts, and add a short inline
comment next to each call clarifying that maxRetries is the number of retries
after the initial attempt (or alternatively rename/document the parameter in
retryApiCall to avoid confusion).
In `@apps/web/vitest.config.ts`:
- Around line 12-16: The config uses __dirname (not available in ES modules)
inside the alias object; replace it with an ES-module-safe directory value by
deriving __dirname via the file URL (e.g., use fileURLToPath(import.meta.url)
and dirname(fileURLToPath(import.meta.url))) or use process.cwd(); then update
the alias entries that call path.resolve(__dirname, './src') / './public' to use
the derived dirname variable instead so path.resolve works in vitest.config.ts.
In `@tsconfig.json`:
- Around line 31-34: The tsconfig's global "types" array currently includes
"jsdom" which is a test-only dependency; remove "jsdom" from the root "types"
array to avoid leaking test types into production code, leaving "node" only, and
instead add "jsdom" to your test/coverage tsconfig (e.g., tsconfig.spec.json or
a tests-only tsconfig) under its "types" array (or to the "types" setting in
your test runner config) so tests still get jsdom typings while the main project
does not.
🧹 Nitpick comments (8)
tsconfig.json (1)
28-30: ExplicittypeRootsis fine but typically unnecessary.Setting
typeRootsto only./node_modules/@typesmirrors the default behavior. This is harmless, but if the project has custom type definitions elsewhere (e.g., atypes/or@types/folder at the repo root), they would no longer be discovered automatically.apps/web/src/components/guards/RoleGuard.tsx (1)
31-41: Consider reusing shared loading UI for consistency.This duplicates spinner styling. If the PR introduces a shared
Spinner/FullPageLoader, consider using it here to keep loading states consistent and reduce bespoke styling.apps/web/src/types/index.ts (1)
161-161: Consider documenting the data limitation.Hardcoding
totalBookings: 0when transforming from legacy profiles will show incorrect counts for existing users. If legacy booking counts are available elsewhere, consider enriching this transformation or adding a comment explaining the limitation.apps/web/src/components/search/PropertyCard.tsx (1)
7-11: Avoid importing types from thepublic/folder.The
public/folder is conventionally reserved for static assets served directly by the web server. Importing TypeScript types frompublic/mock-datacouples production components to mock data structures and may cause build/bundling issues.Additionally, this creates a conflicting
PropertyCardPropsdefinition—apps/web/src/lib/types/property.tsalready definesPropertyCardPropswith aPropertytype.Consider moving
FullPropertyPropsto a proper types directory (e.g.,lib/types/property.ts) and consolidating the type definitions to avoid duplication.apps/web/src/components/ui/error-display.stories.tsx (1)
29-37: InconsistentonRetryhandling across stories.The meta
argTypesalready configuresonRetry: { action: 'retry' }for automatic action logging. The explicitconsole.logimplementations inDestructiveandWithoutTitlestories override this, creating inconsistency withDefaultandWarningstories.Proposed fix: Remove explicit console.log implementations
export const Destructive: Story = { args: { title: 'Action Failed', message: 'The requested action could not be completed because the resource was not found or has been deleted.', variant: 'destructive', - onRetry: () => console.log('Retrying...'), }, };export const WithoutTitle: Story = { args: { message: 'An unexpected error occurred. Please try again later.', - onRetry: () => console.log('Retrying...'), }, };Storybook's Actions addon will automatically log the retry action when triggered.
apps/web/src/components/ui/loading-skeleton.stories.tsx (1)
4-37: Consider splitting stories into separate files by component for proper autodocs and controls.With a single
Metatied toLoadingGrid, Storybook's autodocs will only infer controls and prop tables forLoadingGrid. The stories forPropertyCardSkeleton,Spinner, andFullPageLoaderwon't have individual autodocs support. Per Storybook best practices, each independently reusable component should have its own.stories.tsxfile with its ownMetaexport.apps/web/src/hooks/__tests__/useProperties.test.ts (1)
21-23: Wrap timer advances inact()to flush React updates reliably.
This prevents act warnings and reduces flakiness when advancing fake timers.⏱️ Example fix (apply to each advance)
-vi.advanceTimersByTime(800); +act(() => { + vi.advanceTimersByTime(800); +});Also applies to: 35-36, 45-46
apps/web/src/hooks/useProperties.ts (1)
12-18: InitializeisLoadingtotrueto avoid a first-render empty-state flash.
Since the hook always fetches on mount, starting in loading state keeps UI consistent until the first fetch resolves.✅ Suggested tweak
- const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(true);
| src/app/tenant-dashboard/page.tsx:119:39 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
|
|
||
| × Unexpected any. Specify a different type. | ||
|
|
||
| 117 │ }, []); | ||
| 118 │ | ||
| > 119 │ const handleViewDetails = (booking: any): void => { | ||
| │ ^^^ | ||
| 120 │ // Legacy mapping or handle directly | ||
| 121 │ const found = apiBookings.find((b) => b.id === booking.id); | ||
|
|
||
| i any disables many type checking rules. Its use should be avoided. | ||
|
|
||
|
|
||
| src/app/tenant-dashboard/page.tsx:128:41 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
|
|
||
| × Unexpected any. Specify a different type. | ||
|
|
||
| 126 │ }; | ||
| 127 │ | ||
| > 128 │ const handleCancelBooking = (booking: any): void => { | ||
| │ ^^^ | ||
| 129 │ const found = apiBookings.find((b) => b.id === booking.id); | ||
| 130 │ if (found) { | ||
|
|
||
| i any disables many type checking rules. Its use should be avoided. | ||
|
|
||
|
|
||
| src/app/tenant-dashboard/page.tsx:359:29 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
|
|
||
| × Unexpected any. Specify a different type. | ||
|
|
||
| 357 │ ) : user ? ( | ||
| 358 │ <ProfileManagement | ||
| > 359 │ user={user as any} | ||
| │ ^^^ | ||
| 360 │ onUpdateProfile={handleUpdateUser as any} | ||
| 361 │ onUploadAvatar={async (file) => { | ||
|
|
||
| i any disables many type checking rules. Its use should be avoided. | ||
|
|
||
|
|
||
| src/app/tenant-dashboard/page.tsx:360:52 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
|
|
||
| × Unexpected any. Specify a different type. | ||
|
|
||
| 358 │ <ProfileManagement | ||
| 359 │ user={user as any} | ||
| > 360 │ onUpdateProfile={handleUpdateUser as any} | ||
| │ ^^^ | ||
| 361 │ onUploadAvatar={async (file) => { | ||
| 362 │ await apiUploadAvatar(user.id.toString(), file); | ||
|
|
||
| i any disables many type checking rules. Its use should be avoided. | ||
|
|
||
|
|
||
| src/app/tenant-dashboard/page.tsx:396:45 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
|
|
||
| × Unexpected any. Specify a different type. | ||
|
|
||
| 394 │ walletBalance={walletBalance} | ||
| 395 │ pendingTransactions={pendingTransactions} | ||
| > 396 │ transactions={transactions as any} | ||
| │ ^^^ | ||
| 397 │ onExportTransactions={apiExportTransactions} | ||
| 398 │ /> | ||
|
|
||
| i any disables many type checking rules. Its use should be avoided. | ||
|
|
||
|
|
||
| src/app/tenant-dashboard/page.tsx format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
|
|
||
| × Formatter would have printed the following content: | ||
|
|
||
| 207 207 │ <div className="fixed top-4 right-4 z-50 max-w-sm"> | ||
| 208 208 │ <div | ||
| 209 │ - ··········className={`rounded-lg·p-4·shadow-lg·${toast.type·===·'success' | ||
| 210 │ - ············?·'bg-green-100·border·border-green-200·text-green-800' | ||
| 211 │ - ············:·'bg-red-100·border·border-red-200·text-red-800' | ||
| 212 │ - ············}`} | ||
| 209 │ + ··········className={`rounded-lg·p-4·shadow-lg·${ | ||
| 210 │ + ············toast.type·===·'success' | ||
| 211 │ + ··············?·'bg-green-100·border·border-green-200·text-green-800' | ||
| 212 │ + ··············:·'bg-red-100·border·border-red-200·text-red-800' | ||
| 213 │ + ··········}`} | ||
| 213 214 │ > | ||
| 214 215 │ <div className="flex items-center"> | ||
| ······· │ | ||
| 316 317 │ type="button" | ||
| 317 318 │ onClick={() => setActiveTab(tab.id)} | ||
| 318 │ - ··················className={`flex·items-center·space-x-2·py-4·px-1·border-b-2·font-medium·text-sm·${activeTab·===·tab.id | ||
| 319 │ - ····················?·'border-blue-500·text-blue-600·dark:text-blue-400' | ||
| 320 │ - ····················:·'border-transparent·text-gray-500·dark:text-white·hover:border-gray-300' | ||
| 321 │ - ····················}`} | ||
| 319 │ + ··················className={`flex·items-center·space-x-2·py-4·px-1·border-b-2·font-medium·text-sm·${ | ||
| 320 │ + ····················activeTab·===·tab.id | ||
| 321 │ + ······················?·'border-blue-500·text-blue-600·dark:text-blue-400' | ||
| 322 │ + ······················:·'border-transparent·text-gray-500·dark:text-white·hover:border-gray-300' | ||
| 323 │ + ··················}`} | ||
| 322 324 │ > | ||
| 323 325 │ <Icon className="w-5 h-5" /> | ||
|
|
||
|
|
||
| Checked 1 file in 7ms. No fixes applied. | ||
| Found 6 errors. | ||
| check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
|
|
||
| × Some errors were emitted while running checks. | ||
|
|
||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for files with lint/error/biome patterns
echo "=== Looking for lint/error output files ==="
fd -e txt -e log | rg -i "error|lint|biome|biome_errors" || echo "No obvious lint output files found"
# Check what's in the actual file (if it exists)
if [ -f "apps/web/biome_errors.txt" ]; then
echo "=== File exists: apps/web/biome_errors.txt ==="
wc -l apps/web/biome_errors.txt
head -20 apps/web/biome_errors.txt
fi
# Check .gitignore
if [ -f .gitignore ]; then
echo "=== Checking .gitignore for relevant patterns ==="
rg -i "biome|lint|\.txt|\.log" .gitignore || echo "No matching patterns in .gitignore"
else
echo "No .gitignore found"
fi
# Check for any biome config
echo "=== Checking for biome config ==="
fd -e "json|jsonc" | rg -i "biome" | head -20
# Check git status to see if this file is being added
echo "=== Checking git info ==="
git ls-files apps/web/biome_errors.txt 2>/dev/null && echo "File is tracked" || echo "File is not currently tracked"Repository: Stellar-Rent/stellar-rent
Length of output: 1111
Remove this lint error log file from version control and fix the source errors directly.
Committing lint error output files is not standard practice. The file apps/web/biome_errors.txt is currently tracked in git, but it should not be. The 6 noExplicitAny errors and formatting issues documented here should be resolved in the source file (src/app/tenant-dashboard/page.tsx) rather than committed to the repository.
Required fixes:
- Remove the file from the PR
- Fix the documented errors in
src/app/tenant-dashboard/page.tsx:- Define proper types for
bookingparameter inhandleViewDetailsandhandleCancelBooking(lines 119, 128) - Remove
as anytype assertions foruser,handleUpdateUser, andtransactionsprops (lines 359, 360, 396) - Run formatter to resolve the reported formatting issues (lines 207-213, 318-321)
- Define proper types for
If these issues require deferred attention, track them via GitHub issues or a tracking document instead.
🧰 Tools
🪛 LanguageTool
[grammar] ~104-~104: Ensure spelling is correct
Context: ...Name="w-5 h-5" /> Checked 1 file in 7ms. No fixes applied. Found 6 errors. chec...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
🤖 Prompt for AI Agents
In `@apps/web/biome_errors.txt` around lines 1 - 110, Delete the generated lint
output file from the PR and fix the root TypeScript and formatting issues in
src/app/tenant-dashboard/page.tsx: replace the any types by adding a Booking
type/interface and use it for the booking parameter in handleViewDetails and
handleCancelBooking (these functions reference apiBookings and booking.id),
remove the `as any` assertions passed into <ProfileManagement> (user prop,
onUpdateProfile which is handleUpdateUser, and transactions) by typing user,
handleUpdateUser, and transactions correctly according to the ProfileManagement
prop types, and run the code formatter/Prettier to fix the reported JSX
formatting inconsistencies.
| "test:e2e:headed": "playwright test --headed", | ||
| "storybook": "storybook dev -p 6006", | ||
| "build-storybook": "storybook build", | ||
| "test": "vitest run", | ||
| "test:watch": "vitest" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n apps/web/package.json | head -100Repository: Stellar-Rent/stellar-rent
Length of output: 3250
🌐 Web query:
Storybook 8.6.14 Next.js 15 React 18 compatibility
💡 Result:
- Next.js 15 + React 18: Next.js 15’s App Router uses React 19 (RC), while React 18 is only supported for the Pages Router. [1] If you follow the Next.js 15 upgrade guide for the App Router, the minimum
react/react-domis 19. [2] - Storybook 8.6.14 + Next.js 15: Storybook’s
@storybook/nextjsframework requires Next.js ≥ 14.1, so Next.js 15 is in-range. [3] - Main “gotcha” (React version mismatch): With Next.js 15,
@storybook/nextjscan end up rendering with Next’s bundled/compiled React (React 19) even when your app (Pages Router) is on React 18, which can cause inconsistencies/bugs. This is a known issue in Storybook’s tracker. [4]
Practical guidance: If you’re on Next.js 15 App Router, assume React 19 for compatibility; if you must stay on React 18, use Next.js 15 Pages Router—but be aware Storybook may still pull in React 19 via Next’s compiled React unless/until that issue is addressed. [1][4]
Sources:
[1] (nextjs.org)
[2] (nextjs.org)
[3] (storybook.js.org)
[4] (github.com)
Citations:
- 1: https://nextjs.org/blog/next-15?utm_source=openai
- 2: https://nextjs.org/docs/app/guides/upgrading/version-15?utm_source=openai
- 3: https://storybook.js.org/docs/get-started/frameworks/nextjs?utm_source=openai
- 4: [Bug]: @storybook/nextjs React versions are inconsistent between Next.js and Storybook. storybookjs/storybook#30646
🌐 Web query:
Vitest 4.0.18 jsdom 27 compatibility
💡 Result:
Vitest 4.0.18 should be compatible with jsdom 27 in the sense that Vitest treats jsdom as an optional peer dependency and does not pin it to a specific major version (it’s declared as jsdom: "*" in Vitest 4.x dependency metadata). [1]
What typically breaks on upgrade to jsdom 27 is not Vitest itself, but behavior changes in jsdom that can cause previously-passing DOM/assertion/snapshot tests to fail, e.g. the user-agent stylesheet change (computed styles can differ). [2]
Also ensure your Node.js version meets jsdom 27’s minimum requirements (jsdom 27 requires Node 20+, and 27.0.1 notes tighter minimum patch levels due to dependencies). [2]
Sources: [1] [2]
🌐 Web query:
@testing-library/react 14.2.2 React 18 support
💡 Result:
@testing-library/react@14.2.2 supports React 18—in fact, React Testing Library v13+ requires React v18 (so v14.2.2 is intended for React 18 projects). It also lists react and react-dom as peer dependencies. [1]
If you need to test with React 17 or older, you should use @testing-library/react@12 instead. [1]
Release date: 2024-03-19. [2]
Sources:
[1] (github.com)
[2] (github.com)
Citations:
- 1: https://github.com/testing-library/react-testing-library/tree/v14.2.2
- 2: https://github.com/testing-library/react-testing-library/releases/tag/v14.2.2
Critical: Next.js 15.5.3 requires React 19, but package.json specifies React 18.
Storybook 8.6.14, Vitest 4.0.18, jsdom 27.4.0, and @testing-library/react 14.2.2 are mutually compatible. However, Next.js 15 App Router requires React 19 as minimum—React 18 is only supported for the legacy Pages Router. With Next.js 15.5.3, you must upgrade to React 19 or downgrade Next.js. Additionally, there is a known issue where Storybook may render with Next's bundled React 19 even when React 18 is specified, causing inconsistencies.
Also applies to: 57-83
🤖 Prompt for AI Agents
In `@apps/web/package.json` around lines 12 - 16, package.json currently pins
React 18 while Next.js 15.5.3 (App Router) requires React 19; update the
dependency entries "react" and "react-dom" in package.json to a React 19
compatible version (e.g., ^19.0.0) and ensure any peer/dependency constraints
(like next) are aligned, or alternatively downgrade the "next" version to a
release that supports React 18; after changing versions, run yarn/npm install
and re-run Storybook and tests to confirm no mismatched React bundles (fix any
lockfile or hoisting issues so the project uses the same React 19 for Next and
Storybook).
| const user = apiUser || mockUser; | ||
| const bookings: Booking[] = apiBookings.map((b) => ({ | ||
| id: b.id, | ||
| propertyTitle: b.propertyTitle, | ||
| propertyImage: b.propertyImage, | ||
| location: b.propertyLocation, | ||
| checkIn: b.checkIn, | ||
| checkOut: b.checkOut, | ||
| guests: b.guests, | ||
| totalAmount: b.totalAmount, | ||
| status: b.status as 'pending' | 'confirmed' | 'completed' | 'cancelled', | ||
| bookingDate: b.bookingDate, | ||
| propertyId: '1', | ||
| canCancel: true, | ||
| canReview: b.status === 'completed', | ||
| })); |
There was a problem hiding this comment.
Avoid hard‑coded booking metadata.
propertyId: '1' and canCancel: true will misrepresent bookings and can expose invalid actions. Prefer mapping from the API (or derive cancelability from status).
💡 Suggested fix
- propertyId: '1',
- canCancel: true,
+ propertyId: b.propertyId,
+ canCancel: b.status === 'pending' || b.status === 'confirmed',📝 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 user = apiUser || mockUser; | |
| const bookings: Booking[] = apiBookings.map((b) => ({ | |
| id: b.id, | |
| propertyTitle: b.propertyTitle, | |
| propertyImage: b.propertyImage, | |
| location: b.propertyLocation, | |
| checkIn: b.checkIn, | |
| checkOut: b.checkOut, | |
| guests: b.guests, | |
| totalAmount: b.totalAmount, | |
| status: b.status as 'pending' | 'confirmed' | 'completed' | 'cancelled', | |
| bookingDate: b.bookingDate, | |
| propertyId: '1', | |
| canCancel: true, | |
| canReview: b.status === 'completed', | |
| })); | |
| const user = apiUser || mockUser; | |
| const bookings: Booking[] = apiBookings.map((b) => ({ | |
| id: b.id, | |
| propertyTitle: b.propertyTitle, | |
| propertyImage: b.propertyImage, | |
| location: b.propertyLocation, | |
| checkIn: b.checkIn, | |
| checkOut: b.checkOut, | |
| guests: b.guests, | |
| totalAmount: b.totalAmount, | |
| status: b.status as 'pending' | 'confirmed' | 'completed' | 'cancelled', | |
| bookingDate: b.bookingDate, | |
| propertyId: b.propertyId, | |
| canCancel: b.status === 'pending' || b.status === 'confirmed', | |
| canReview: b.status === 'completed', | |
| })); |
🤖 Prompt for AI Agents
In `@apps/web/src/app/dashboard/host-dashboard/page.tsx` around lines 66 - 81, The
bookings mapping currently hard-codes propertyId and canCancel; update the
mapping that builds bookings (where bookings is derived from apiBookings in
page.tsx) to pull propertyId from the API object (e.g., b.propertyId or
b.property?.id) instead of the literal '1', and compute canCancel based on the
booking status (for example allow cancel when status is 'pending' or 'confirmed'
and disallow when 'completed' or 'cancelled'); ensure types align
(Booking.propertyId type) and that canReview remains derived from b.status ===
'completed'.
| const handleCancelBooking = async (bookingId: string) => { | ||
| setIsLoading(true); | ||
| try { | ||
| // Simulate API call | ||
| await new Promise((resolve) => setTimeout(resolve, 1000)); | ||
|
|
||
| setBookings((prev) => | ||
| prev.map((booking) => | ||
| booking.id === bookingId | ||
| ? { ...booking, status: 'cancelled' as const, canCancel: false } | ||
| : booking | ||
| ) | ||
| ); | ||
|
|
||
| await apiCancelBooking(bookingId); | ||
| const newNotification = { | ||
| id: Date.now().toString(), | ||
| type: 'booking', | ||
| type: 'booking' as const, | ||
| title: 'Booking Cancelled', | ||
| message: 'Your booking has been successfully cancelled', | ||
| timestamp: new Date(), | ||
| read: false, | ||
| priority: 'medium' as const, | ||
| }; | ||
|
|
||
| setNotifications((prev) => [newNotification, ...prev]); | ||
| setUnreadNotifications((prev) => prev + 1); | ||
| } catch (error) { | ||
| console.error('Failed to cancel booking:', error); | ||
| } finally { | ||
| setIsLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| const handleUpdateProfile = async (updatedProfile: Partial<UserProfile>) => { | ||
| setIsLoading(true); | ||
| const handleUpdateProfile = async (updates: Partial<UserProfile>) => { | ||
| try { | ||
| await new Promise((resolve) => setTimeout(resolve, 1000)); | ||
|
|
||
| setUser((prev) => ({ ...prev, ...updatedProfile })); | ||
|
|
||
| if (!apiUser) return; | ||
| const safeUpdates = { | ||
| ...updates, | ||
| preferences: updates.preferences | ||
| ? { | ||
| currency: apiUser.preferences?.currency || 'USD', | ||
| language: apiUser.preferences?.language || 'en', | ||
| notifications: updates.preferences.notifications, | ||
| emailNotifications: updates.preferences.emailUpdates, | ||
| marketingEmails: apiUser.preferences?.marketingEmails, | ||
| } | ||
| : apiUser.preferences, | ||
| }; | ||
| await apiUpdateProfile({ ...apiUser, ...safeUpdates } as unknown as ApiUserProfile); | ||
| const newNotification = { | ||
| id: Date.now().toString(), | ||
| type: 'system', | ||
| type: 'system' as const, | ||
| title: 'Profile Updated', | ||
| message: 'Your profile has been successfully updated', | ||
| timestamp: new Date(), | ||
| read: false, | ||
| priority: 'low' as const, | ||
| }; | ||
|
|
||
| setNotifications((prev) => [newNotification, ...prev]); | ||
| setUnreadNotifications((prev) => prev + 1); | ||
| } catch (error) { | ||
| console.error('Failed to update profile:', error); | ||
| } finally { | ||
| setIsLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| const handleUploadAvatar = async (file: File) => { | ||
| setIsLoading(true); | ||
| try { | ||
| await new Promise((resolve) => setTimeout(resolve, 2000)); | ||
|
|
||
| const avatarUrl = URL.createObjectURL(file); | ||
| setUser((prev) => ({ ...prev, avatar: avatarUrl })); | ||
|
|
||
| if (!user) return; | ||
| await apiUploadAvatar(user.id.toString(), file); | ||
| const newNotification = { | ||
| id: Date.now().toString(), | ||
| type: 'system', | ||
| type: 'system' as const, | ||
| title: 'Avatar Updated', | ||
| message: 'Your profile picture has been successfully updated', | ||
| timestamp: new Date(), | ||
| read: false, | ||
| priority: 'low' as const, | ||
| }; | ||
|
|
||
| setNotifications((prev) => [newNotification, ...prev]); | ||
| setUnreadNotifications((prev) => prev + 1); | ||
| } catch (error) { | ||
| console.error('Failed to upload avatar:', error); | ||
| } finally { | ||
| setIsLoading(false); | ||
| } |
There was a problem hiding this comment.
Gate success notifications on action results.
apiCancelBooking, apiUpdateProfile, and apiUploadAvatar return booleans without throwing; the current code always emits success notifications even when these return false.
🐛 Proposed fix to honor boolean results
- await apiCancelBooking(bookingId);
+ const cancelled = await apiCancelBooking(bookingId);
+ if (!cancelled) throw new Error('Failed to cancel booking');
@@
- await apiUpdateProfile({ ...apiUser, ...safeUpdates } as unknown as ApiUserProfile);
+ const updated = await apiUpdateProfile({ ...apiUser, ...safeUpdates } as unknown as ApiUserProfile);
+ if (!updated) throw new Error('Failed to update profile');
@@
- await apiUploadAvatar(user.id.toString(), file);
+ const uploaded = await apiUploadAvatar(user.id.toString(), file);
+ if (!uploaded) throw new Error('Failed to upload avatar');🤖 Prompt for AI Agents
In `@apps/web/src/app/dashboard/tenant-dashboard/page.tsx` around lines 181 - 249,
handleCancelBooking, handleUpdateProfile, and handleUploadAvatar always emit
success notifications even though apiCancelBooking, apiUpdateProfile, and
apiUploadAvatar return boolean success flags rather than throwing; update each
handler to capture the returned boolean (e.g., const success = await
apiCancelBooking(...)) and only create/set notifications and increment unread
when success === true, otherwise handle the false case (log an informative error
via console.error or show a failure notification) so failures do not produce
success messages; include these checks in the functions handleCancelBooking,
handleUpdateProfile, and handleUploadAvatar and ensure setNotifications and
setUnreadNotifications are only called on successful API responses.
| <button | ||
| type="button" | ||
| onClick={handleRefresh} | ||
| className="text-gray-500 dark:text-white hover:text-gray-700 dark:hover:text-gray-300 transition-colors" | ||
| > | ||
| <RefreshCw | ||
| className={`w-6 h-6 ${isLoadingBookings || isLoadingProfile ? 'animate-spin' : ''}`} | ||
| /> | ||
| </button> |
There was a problem hiding this comment.
Add an accessible label (and disable while loading) for the refresh icon button.
Icon-only buttons need an aria-label for screen readers; disabling during load also avoids overlapping refetches.
♿ Suggested accessibility tweak
- <button
- type="button"
- onClick={handleRefresh}
- className="text-gray-500 dark:text-white hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
- >
+ <button
+ type="button"
+ onClick={handleRefresh}
+ aria-label="Refresh dashboard"
+ disabled={isLoadingBookings || isLoadingProfile}
+ className="text-gray-500 dark:text-white hover:text-gray-700 dark:hover:text-gray-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
+ >📝 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.
| <button | |
| type="button" | |
| onClick={handleRefresh} | |
| className="text-gray-500 dark:text-white hover:text-gray-700 dark:hover:text-gray-300 transition-colors" | |
| > | |
| <RefreshCw | |
| className={`w-6 h-6 ${isLoadingBookings || isLoadingProfile ? 'animate-spin' : ''}`} | |
| /> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={handleRefresh} | |
| aria-label="Refresh dashboard" | |
| disabled={isLoadingBookings || isLoadingProfile} | |
| className="text-gray-500 dark:text-white hover:text-gray-700 dark:hover:text-gray-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" | |
| > | |
| <RefreshCw | |
| className={`w-6 h-6 ${isLoadingBookings || isLoadingProfile ? 'animate-spin' : ''}`} | |
| /> | |
| </button> |
🤖 Prompt for AI Agents
In `@apps/web/src/app/dashboard/tenant-dashboard/page.tsx` around lines 267 - 275,
The refresh icon button lacks an accessible label and isn't disabled during
loading; update the JSX for the button that uses handleRefresh and the RefreshCw
icon to include an aria-label (e.g., aria-label="Refresh bookings") and set
disabled={isLoadingBookings || isLoadingProfile} (also add visually/semantically
appropriate classes when disabled or use aria-disabled if needed) so the button
is both screen-reader friendly and prevented from being clicked while loading.
| // Mock the timeout to make tests faster | ||
| vi.useFakeTimers(); | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n apps/web/src/hooks/__tests__/useProperties.test.tsRepository: Stellar-Rent/stellar-rent
Length of output: 1922
Restore real timers after the suite to avoid cross-test leakage.
vi.useFakeTimers() is called at module scope (line 7) without a corresponding cleanup hook. This can cause fake timers to persist across other test suites and lead to timeouts or unexpected behavior.
vi.useFakeTimers();
+
+afterEach(() => {
+ vi.useRealTimers();
+});📝 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.
| // Mock the timeout to make tests faster | |
| vi.useFakeTimers(); | |
| // Mock the timeout to make tests faster | |
| vi.useFakeTimers(); | |
| afterEach(() => { | |
| vi.useRealTimers(); | |
| }); |
🤖 Prompt for AI Agents
In `@apps/web/src/hooks/__tests__/useProperties.test.ts` around lines 6 - 8,
vi.useFakeTimers() is called at module scope in the test file which can leak
fake timers to other suites; add a cleanup to restore real timers (e.g., add an
afterAll or afterEach hook that calls vi.useRealTimers()) so that the fake timer
set by vi.useFakeTimers() is reverted after the tests run and prevents
cross-test leakage.
| property: { | ||
| title: data.property?.title || 'Unknown Property', | ||
| image: data.property?.image || '/images/property-placeholder.jpg', | ||
| image: data.property?.image || '/images/property-placeholder.webp', |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for the property-placeholder.webp file
fd -t f "property-placeholder.webp"Repository: Stellar-Rent/stellar-rent
Length of output: 51
🏁 Script executed:
#!/bin/bash
# Also check if there are any property placeholder images at all
fd -t f "property-placeholder" -e webp -e jpg -e pngRepository: Stellar-Rent/stellar-rent
Length of output: 51
🏁 Script executed:
#!/bin/bash
# Check for public directory structure
find . -type d -name "public" -o -type d -name "images" -o -type d -name "static" | head -20Repository: Stellar-Rent/stellar-rent
Length of output: 111
🏁 Script executed:
#!/bin/bash
# Look for any .webp or image files in the repo
fd -t f "\.(webp|jpg|png|svg)$" | head -30Repository: Stellar-Rent/stellar-rent
Length of output: 985
🏁 Script executed:
#!/bin/bash
# Check the actual file to see the context of this change
cat -n apps/web/src/hooks/useBookingDetails.ts | sed -n '40,60p'Repository: Stellar-Rent/stellar-rent
Length of output: 1056
The placeholder image /images/property-placeholder.webp does not exist and must be created.
The code references /images/property-placeholder.webp as a fallback, but this file is missing from the apps/web/public/images/ directory. Either create the WebP placeholder image or update the reference to point to an existing asset.
🤖 Prompt for AI Agents
In `@apps/web/src/hooks/useBookingDetails.ts` at line 52, The fallback image
reference in useBookingDetails (data.property?.image ||
'/images/property-placeholder.webp') points to a missing asset; either add a
placeholder WebP at apps/web/public/images/property-placeholder.webp or change
the fallback to an existing asset path (e.g., an existing file under
public/images) and update the string in useBookingDetails accordingly so the
fallback resolves at runtime.
| const fetchBookings = useCallback(async () => { | ||
| if (!userId) return; | ||
|
|
||
| setIsLoadingBookings(true); | ||
| setError(null); | ||
|
|
||
| setBookingsError(null); | ||
| try { | ||
| const response = await fetch('/api/bookings'); | ||
| if (!response.ok) { | ||
| throw new Error(`Failed to fetch bookings: ${response.statusText}`); | ||
| } | ||
| const response = await retryApiCall( | ||
| async () => { | ||
| const res = await fetch('/api/bookings'); | ||
| if (!res.ok) throw new Error(`Failed to fetch bookings: ${res.statusText}`); | ||
| return res; | ||
| }, | ||
| 3, | ||
| 1000 | ||
| ); | ||
| const data = await response.json(); | ||
| // Handle the response structure from backend | ||
| const bookingsData = data.data?.bookings || data.bookings || data || []; | ||
| setBookings(Array.isArray(bookingsData) ? bookingsData : []); | ||
| } catch (err) { | ||
| const errorMessage = err instanceof Error ? err.message : 'Failed to fetch bookings'; | ||
| setError(errorMessage); | ||
| console.error('Failed to fetch bookings:', err); | ||
| setBookings([]); // Reset to empty array on error | ||
| const message = err instanceof Error ? err.message : 'Failed to fetch bookings'; | ||
| setBookingsError(message); | ||
| setBookings([]); | ||
| } finally { | ||
| setIsLoadingBookings(false); | ||
| } | ||
| }, [userId]); | ||
| }, []); | ||
|
|
||
| const fetchProfile = useCallback(async () => { | ||
| if (!userId) return; | ||
|
|
||
| setIsLoadingProfile(true); | ||
| setError(null); | ||
|
|
||
| setProfileError(null); | ||
| try { | ||
| const response = await fetch('/api/profile'); | ||
| if (!response.ok) { | ||
| throw new Error(`Failed to fetch profile: ${response.statusText}`); | ||
| } | ||
| const response = await retryApiCall( | ||
| async () => { | ||
| const res = await fetch('/api/profile'); | ||
| if (!res.ok) throw new Error(`Failed to fetch profile: ${res.statusText}`); | ||
| return res; | ||
| }, | ||
| 3, | ||
| 1000 | ||
| ); | ||
| const data = await response.json(); | ||
| setProfile(data || null); | ||
| setUser(data || null); | ||
| } catch (err) { | ||
| const errorMessage = err instanceof Error ? err.message : 'Failed to fetch profile'; | ||
| setError(errorMessage); | ||
| console.error('Failed to fetch profile:', err); | ||
| setProfile(null); // Reset to null on error | ||
| const message = err instanceof Error ? err.message : 'Failed to fetch profile'; | ||
| setProfileError(message); | ||
| setUser(null); | ||
| } finally { | ||
| setIsLoadingProfile(false); | ||
| } | ||
| }, [userId]); | ||
| }, []); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find retryApiCall function definition
rg -n "retryApiCall" --type ts -A 15 | head -100Repository: Stellar-Rent/stellar-rent
Length of output: 5837
Confirm retry attempt count: maxRetries parameter uses <= logic, making 3 result in 4 total attempts.
The retryApiCall function (apps/web/src/hooks/useApiCall.ts:104) loops with attempt <= maxRetries, meaning passing 3 executes 4 total attempts. If the intended behavior is 3 total attempts, change both call sites to pass 2. If 4 attempts are intentional, no change is needed but the parameter naming or documentation should clarify that maxRetries represents retries after the initial attempt.
🤖 Prompt for AI Agents
In `@apps/web/src/hooks/useDashboard.ts` around lines 74 - 121, The calls to
retryApiCall in fetchBookings and fetchProfile currently pass 3, but
retryApiCall uses attempt <= maxRetries so that value causes 4 total attempts;
update both call sites in fetchBookings and fetchProfile to pass 2 so there are
3 total attempts, and add a short inline comment next to each call clarifying
that maxRetries is the number of retries after the initial attempt (or
alternatively rename/document the parameter in retryApiCall to avoid confusion).
| alias: { | ||
| '@': path.resolve(__dirname, './src'), | ||
| '~': path.resolve(__dirname, './src'), | ||
| public: path.resolve(__dirname, './public'), | ||
| }, |
There was a problem hiding this comment.
__dirname is not defined in ES modules.
The configuration uses __dirname which is not available in ES module scope. This will cause a ReferenceError when running tests.
🐛 Proposed fix using ES module compatible approach
import path from 'node:path';
+import { fileURLToPath } from 'node:url';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vitest/config';
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test/setup.ts'],
exclude: ['**/node_modules/**', '**/dist/**', '**/tests-e2e/**', '**/*.spec.ts'],
alias: {
'@': path.resolve(__dirname, './src'),
'~': path.resolve(__dirname, './src'),
public: path.resolve(__dirname, './public'),
},
},
});📝 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.
| alias: { | |
| '@': path.resolve(__dirname, './src'), | |
| '~': path.resolve(__dirname, './src'), | |
| public: path.resolve(__dirname, './public'), | |
| }, | |
| import path from 'node:path'; | |
| import { fileURLToPath } from 'node:url'; | |
| import react from '@vitejs/plugin-react'; | |
| import { defineConfig } from 'vitest/config'; | |
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | |
| export default defineConfig({ | |
| plugins: [react()], | |
| test: { | |
| environment: 'jsdom', | |
| globals: true, | |
| setupFiles: ['./src/test/setup.ts'], | |
| exclude: ['**/node_modules/**', '**/dist/**', '**/tests-e2e/**', '**/*.spec.ts'], | |
| alias: { | |
| '@': path.resolve(__dirname, './src'), | |
| '~': path.resolve(__dirname, './src'), | |
| public: path.resolve(__dirname, './public'), | |
| }, | |
| }, | |
| }); |
🤖 Prompt for AI Agents
In `@apps/web/vitest.config.ts` around lines 12 - 16, The config uses __dirname
(not available in ES modules) inside the alias object; replace it with an
ES-module-safe directory value by deriving __dirname via the file URL (e.g., use
fileURLToPath(import.meta.url) and dirname(fileURLToPath(import.meta.url))) or
use process.cwd(); then update the alias entries that call
path.resolve(__dirname, './src') / './public' to use the derived dirname
variable instead so path.resolve works in vitest.config.ts.
| "types": [ | ||
| "node", | ||
| "jsdom" | ||
| ] |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
echo "=== TypeScript configuration files ==="
fd -type f -name 'tsconfig*.json' | head -20
echo -e "\n=== Content of tsconfig.json (focus on types and root config) ==="
cat tsconfig.json
echo -e "\n=== Check for test-specific tsconfig files ==="
ls -la tsconfig*.json 2>/dev/null || echo "No additional tsconfig files found at root"
echo -e "\n=== Node.js globals usage in src (excluding tests) ==="
rg -n '\b(process|Buffer|__dirname|__filename)\b' --type ts --type tsx -g '!**/*.test.*' -g '!**/*.spec.*' -g '!**/test/**' -g '!**/tests/**' 2>/dev/null | head -30 || echo "No Node.js global usages found"
echo -e "\n=== jsdom globals usage in src (excluding tests) ==="
rg -n '\b(window|document|fetch|localStorage|sessionStorage)\b' --type ts --type tsx -g '!**/*.test.*' -g '!**/*.spec.*' -g '!**/test/**' -g '!**/tests/**' 2>/dev/null | head -20 || echo "jsdom globals might be used"
echo -e "\n=== Checking package.json for dependencies ==="
jq '.dependencies, .devDependencies' package.json 2>/dev/null | head -40Repository: Stellar-Rent/stellar-rent
Length of output: 1899
Review the necessity of globally including "jsdom" types.
"jsdom" is a testing utility and is listed in devDependencies. While it's reasonable to include "node" types in a backend-focused project, "jsdom" is primarily for test environments and may not need to be available globally alongside production source code types.
If jsdom is only used in tests, consider whether it should be excluded from the root tsconfig and included only where needed.
🤖 Prompt for AI Agents
In `@tsconfig.json` around lines 31 - 34, The tsconfig's global "types" array
currently includes "jsdom" which is a test-only dependency; remove "jsdom" from
the root "types" array to avoid leaking test types into production code, leaving
"node" only, and instead add "jsdom" to your test/coverage tsconfig (e.g.,
tsconfig.spec.json or a tests-only tsconfig) under its "types" array (or to the
"types" setting in your test runner config) so tests still get jsdom typings
while the main project does not.
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/app/dashboard/host-dashboard/page.tsx (1)
160-176: Gate success notifications on action results.
apiCancelBooking,apiUpdateProfile, andapiUploadAvatarreturn booleans, but notifications are always emitted regardless of the result. Check the return value before adding success notifications.🐛 Proposed fix for handleCancelBooking
const handleCancelBooking = async (bookingId: string) => { try { - await apiCancelBooking(bookingId); + const success = await apiCancelBooking(bookingId); + if (!success) { + console.error('Failed to cancel booking'); + return; + } addNotification({ id: Date.now().toString(), type: 'booking' as const,Apply similar pattern to
handleUpdateProfileandhandleUploadAvatar.Also applies to: 178-210, 212-228
🤖 Fix all issues with AI agents
In `@apps/web/src/app/booking/page.tsx`:
- Around line 117-124: setCurrentBookingData is being populated with
createdBooking.data.escrowAddress defaulting to '', which can allow proceeding
without a valid escrow address; instead, validate
createdBooking.data.escrowAddress immediately after the API response (before
calling setCurrentBookingData or starting payment) and treat its absence as a
hard error: stop the flow, surface an error to the user (or throw) and do not
call setCurrentBookingData or continue to the payment step. Update the logic
around createdBooking / createdBooking.data (where setCurrentBookingData is
invoked) to explicitly check escrowAddress and handle the failure path (show
error/return) so downstream code never receives an empty string escrow address.
In `@apps/web/src/app/tenant-dashboard/page.tsx`:
- Around line 356-358: The onCancelBooking inline callback currently awaits
apiCancelBooking(id) but ignores its return value; update the onCancelBooking
handler (the lambda passed as onCancelBooking) to check the result from
apiCancelBooking (similar to handleConfirmCancel) and handle failures—e.g.,
inspect the returned success/error indicator, show an error notification or
toast, and avoid proceeding as if cancellation succeeded when the API returns
failure. Ensure you reference and reuse the same success/failure handling logic
used in handleConfirmCancel to keep behavior consistent.
In `@apps/web/src/components/search/PropertyGrid.tsx`:
- Around line 57-77: The current mapping in properties.map creates a mismatched
cardProperty (id coercion and renamed fields) which causes PropertyCard to
receive undefined fields and can route to /property/0; remove the manual mapping
and pass the incoming FullPropertyProps directly into PropertyCard: delete the
cardProperty object and the Number.parseInt/parseFloat coercions, return
<PropertyCard key={property.id} property={property} /> (ensure the map's item
type is FullPropertyProps or the PropertyCard prop type accepts
FullPropertyProps) so the original id, images, maxGuests, bedrooms, etc. are
preserved and the stray "as any" cast is removed.
In `@apps/web/src/hooks/useDashboard.ts`:
- Around line 127-143: The mockTransactions array uses numeric ids but the
Transaction interface expects id: string; update the mock entries in
mockTransactions so each id is a string (e.g., "1", "2") or otherwise coerce to
string when constructing the mock data; ensure every Transaction object in the
mockTransactions array (and any other test fixtures in useDashboard) uses string
ids to match the Transaction type.
- Around line 174-184: The cancelBooking implementation currently uses a
simulated delay and only updates local state; replace it with a real API call by
invoking api.cancelBooking(id) (import the service if needed), await the
response, check for success/failure, and only call setBookings(prev =>
prev.filter(b => b.id !== id)) after the API confirms cancellation; on error,
call setError(err instanceof Error ? err.message : 'Failed to cancel booking')
and return false, otherwise return true. Ensure cancelBooking remains async and
preserves existing signature and useCallback dependencies.
🧹 Nitpick comments (7)
apps/web/src/components/dashboard/BookingHistory.tsx (1)
638-657: Remove unusedonContactHostprop from BookingCard.
BookingCarddeclaresonContactHostin its type signature (line 642) but does not destructure or use it in the component implementation. This creates dead code.♻️ Suggested fix
const BookingCard: React.FC<{ booking: Booking; onCancel: () => void; onViewDetails: () => void; - onContactHost: () => void; onLeaveReview: () => void; getStatusColor: (status: Booking['status']) => string; getStatusIcon: (status: Booking['status']) => React.ReactNode; formatDate: (date: string) => string; calculateNights: (checkIn: string, checkOut: string) => number; }> = ({ booking, onCancel, onViewDetails, onLeaveReview, getStatusColor, getStatusIcon, formatDate, calculateNights, }) => (apps/web/src/hooks/useDashboard.ts (1)
59-60: Wallet data is hardcoded; consider fetching or documenting as mock.
walletBalanceandpendingTransactionsare initialized with static values and never updated.isLoadingWallethas an unused setter. If wallet data should be fetched, add the fetch logic; otherwise, add a comment indicating these are intentional placeholders for future implementation.Also applies to: 66-66
apps/web/src/app/dashboard/host-dashboard/page.tsx (1)
256-264: Add accessible label to refresh button.Icon-only buttons need an
aria-labelfor screen readers. Consider also disabling during load to prevent overlapping refetches.♿ Suggested accessibility fix
<button type="button" onClick={handleRefresh} + aria-label="Refresh dashboard" + disabled={isLoadingBookings || isLoadingProfile} - className="text-gray-500 dark:text-white hover:text-gray-700 dark:hover:text-gray-300 transition-colors" + className="text-gray-500 dark:text-white hover:text-gray-700 dark:hover:text-gray-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" >apps/web/src/app/tenant-dashboard/page.tsx (1)
375-386: Consider addressing type mismatches to remove biome-ignore comments.Multiple
biome-ignore lint/suspicious/noExplicitAnycomments indicate underlying type incompatibilities between legacy and API user types. Consider creating proper type adapters or updating interfaces to eliminate the need foranycasts.apps/web/src/components/guards/RoleGuard.tsx (3)
42-52: Consider using the sharedSpinnercomponent for consistency.The PR introduces a reusable
Spinnercomponent inapps/web/src/components/ui/, but this file implements an inline CSS spinner. For consistency across the codebase:♻️ Suggested refactor to use shared Spinner
+import { Spinner } from '~/components/ui/spinner'; ... if (isLoading) { return ( <div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-[`#0B1D39`]"> <div className="text-center"> - <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4" /> + <Spinner size="lg" className="mx-auto mb-4" /> <p className="text-gray-600 dark:text-gray-400">Verifying access...</p> </div> </div> ); }
54-72: Hardcoded "Host Access Required" message limits reusability.The access denied UI text is hardcoded for the host role, but this guard can be used with
requiredRoleset to'guest'or'dual'. If reused for other roles, the message would be misleading.♻️ Suggested refactor to make message role-aware
interface RoleGuardProps { children: ReactNode; requiredRole: UserRole; fallbackPath?: string; + accessDeniedTitle?: string; + accessDeniedMessage?: string; } export function RoleGuard({ children, requiredRole, fallbackPath = '/become-host', + accessDeniedTitle = 'Host Access Required', + accessDeniedMessage = 'You need a verified host profile with properties to access this page. Join our community of hosts and start earning.', }: RoleGuardProps) { ... if (!hasAccess) { return ( <div className="flex flex-col items-center justify-center min-h-screen p-4 bg-gray-50 dark:bg-[`#0B1D39`]"> <div className="text-center max-w-md bg-white dark:bg-[`#0B1D39`]/90 p-8 rounded-xl shadow-lg border border-gray-100 dark:border-gray-800"> - <h2 className="text-2xl font-bold mb-4 dark:text-white">Host Access Required</h2> + <h2 className="text-2xl font-bold mb-4 dark:text-white">{accessDeniedTitle}</h2> - <p className="text-gray-600 dark:text-gray-400 mb-6"> - You need a verified host profile with properties to access this page. Join our community of hosts and start earning. - </p> + <p className="text-gray-600 dark:text-gray-400 mb-6">{accessDeniedMessage}</p>
35-40: Inconsistent navigation behavior between auto-redirect and button.The
useEffectusesrouter.replace()(no history entry), but the button usesrouter.push()(adds history entry). This means:
- Auto-redirect: user cannot navigate back
- Button click: user can navigate back to the access denied page
If this is intentional UX (manual action = preserves history), the current behavior is fine. Otherwise, consider aligning them.
♻️ Suggested fix if consistent behavior is desired
<button type="button" - onClick={() => router.push(fallbackPath)} + onClick={() => router.replace(fallbackPath)} className="w-full bg-[`#0B1D39`] dark:bg-blue-600 text-white px-6 py-3 rounded-lg hover:opacity-90 transition-colors font-semibold" >Also applies to: 63-65
| setCurrentBookingData({ | ||
| bookingId: createdBooking.bookingId, | ||
| bookingId: createdBooking.data.id, | ||
| property: data.property, | ||
| dates: data.dates, | ||
| guests: data.guests, | ||
| totalAmount: data.totalAmount, | ||
| escrowAddress: createdBooking.escrowAddress, | ||
| escrowAddress: createdBooking.data.escrowAddress || '', | ||
| }); |
There was a problem hiding this comment.
Don’t proceed to payment with a missing escrow address.
Falling back to '' can lead to invalid payments or downstream failures. Treat a missing escrowAddress as a hard error and stop the flow.
🛑 Proposed fix (fail fast on missing escrowAddress)
- setCurrentBookingData({
- bookingId: createdBooking.data.id,
- property: data.property,
- dates: data.dates,
- guests: data.guests,
- totalAmount: data.totalAmount,
- escrowAddress: createdBooking.data.escrowAddress || '',
- });
+ const escrowAddress = createdBooking.data.escrowAddress;
+ if (!escrowAddress) {
+ throw new Error('Escrow address missing from booking response.');
+ }
+
+ setCurrentBookingData({
+ bookingId: createdBooking.data.id,
+ property: data.property,
+ dates: data.dates,
+ guests: data.guests,
+ totalAmount: data.totalAmount,
+ escrowAddress,
+ });🤖 Prompt for AI Agents
In `@apps/web/src/app/booking/page.tsx` around lines 117 - 124,
setCurrentBookingData is being populated with createdBooking.data.escrowAddress
defaulting to '', which can allow proceeding without a valid escrow address;
instead, validate createdBooking.data.escrowAddress immediately after the API
response (before calling setCurrentBookingData or starting payment) and treat
its absence as a hard error: stop the flow, surface an error to the user (or
throw) and do not call setCurrentBookingData or continue to the payment step.
Update the logic around createdBooking / createdBooking.data (where
setCurrentBookingData is invoked) to explicitly check escrowAddress and handle
the failure path (show error/return) so downstream code never receives an empty
string escrow address.
| onCancelBooking={async (id) => { | ||
| await apiCancelBooking(id); | ||
| }} |
There was a problem hiding this comment.
Check return value before proceeding silently.
The onCancelBooking callback calls apiCancelBooking but ignores its return value. For consistency with handleConfirmCancel, check the result and handle failures appropriately.
🐛 Suggested fix
- onCancelBooking={async (id) => {
- await apiCancelBooking(id);
- }}
+ onCancelBooking={async (id) => {
+ const success = await apiCancelBooking(id);
+ if (!success) {
+ showToast('Failed to cancel booking', 'error');
+ }
+ }}🤖 Prompt for AI Agents
In `@apps/web/src/app/tenant-dashboard/page.tsx` around lines 356 - 358, The
onCancelBooking inline callback currently awaits apiCancelBooking(id) but
ignores its return value; update the onCancelBooking handler (the lambda passed
as onCancelBooking) to check the result from apiCancelBooking (similar to
handleConfirmCancel) and handle failures—e.g., inspect the returned
success/error indicator, show an error notification or toast, and avoid
proceeding as if cancellation succeeded when the API returns failure. Ensure you
reference and reuse the same success/failure handling logic used in
handleConfirmCancel to keep behavior consistent.
| const mockTransactions: Transaction[] = [ | ||
| { | ||
| id: '1', | ||
| id: 1, | ||
| date: '2025-05-28', | ||
| description: 'Luxury Downtown Apartment', | ||
| description: 'Luxury Apartment', | ||
| amount: -1250, | ||
| type: 'booking', | ||
| type: 'payment', | ||
| status: 'completed', | ||
| }, | ||
| { | ||
| id: '2', | ||
| id: 2, | ||
| date: '2025-05-26', | ||
| description: 'Cozy Beach House', | ||
| description: 'Beach House', | ||
| amount: -900, | ||
| type: 'booking', | ||
| type: 'payment', | ||
| status: 'pending', | ||
| }, |
There was a problem hiding this comment.
Type mismatch: Transaction id should be string, not number.
The Transaction interface (from apps/web/src/types/shared.ts) defines id: string, but the mock data uses numeric ids (id: 1, id: 2). This may cause type errors or runtime issues when the data is consumed.
🐛 Suggested fix
const mockTransactions: Transaction[] = [
{
- id: 1,
+ id: '1',
date: '2025-05-28',
description: 'Luxury Apartment',
amount: -1250,
type: 'payment',
status: 'completed',
},
{
- id: 2,
+ id: '2',
date: '2025-05-26',
description: 'Beach House',
amount: -900,
type: 'payment',
status: 'pending',
},
];📝 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 mockTransactions: Transaction[] = [ | |
| { | |
| id: '1', | |
| id: 1, | |
| date: '2025-05-28', | |
| description: 'Luxury Downtown Apartment', | |
| description: 'Luxury Apartment', | |
| amount: -1250, | |
| type: 'booking', | |
| type: 'payment', | |
| status: 'completed', | |
| }, | |
| { | |
| id: '2', | |
| id: 2, | |
| date: '2025-05-26', | |
| description: 'Cozy Beach House', | |
| description: 'Beach House', | |
| amount: -900, | |
| type: 'booking', | |
| type: 'payment', | |
| status: 'pending', | |
| }, | |
| const mockTransactions: Transaction[] = [ | |
| { | |
| id: '1', | |
| date: '2025-05-28', | |
| description: 'Luxury Apartment', | |
| amount: -1250, | |
| type: 'payment', | |
| status: 'completed', | |
| }, | |
| { | |
| id: '2', | |
| date: '2025-05-26', | |
| description: 'Beach House', | |
| amount: -900, | |
| type: 'payment', | |
| status: 'pending', | |
| }, |
🤖 Prompt for AI Agents
In `@apps/web/src/hooks/useDashboard.ts` around lines 127 - 143, The
mockTransactions array uses numeric ids but the Transaction interface expects
id: string; update the mock entries in mockTransactions so each id is a string
(e.g., "1", "2") or otherwise coerce to string when constructing the mock data;
ensure every Transaction object in the mockTransactions array (and any other
test fixtures in useDashboard) uses string ids to match the Transaction type.
| const cancelBooking = useCallback(async (id: string) => { | ||
| try { | ||
| // Simulate API call | ||
| await new Promise((resolve) => setTimeout(resolve, 1000)); | ||
| setBookings((prev) => prev.filter((b) => b.id !== id)); | ||
| return true; | ||
| } catch (err) { | ||
| setError(err instanceof Error ? err.message : 'Failed to cancel booking'); | ||
| return false; | ||
| } | ||
| }, []); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's check the services directory structure and search for cancelBooking
find apps/web/src -type d -name "services" 2>/dev/null | head -5Repository: Stellar-Rent/stellar-rent
Length of output: 90
🏁 Script executed:
# Search for cancelBooking in the codebase
rg "cancelBooking" apps/web/src --type ts -n | head -30Repository: Stellar-Rent/stellar-rent
Length of output: 777
🏁 Script executed:
# Check the API service files to understand the pattern
fd "api\." apps/web/src/services --type ts 2>/dev/null | head -10Repository: Stellar-Rent/stellar-rent
Length of output: 51
🏁 Script executed:
# Let's explore what's in the services directory
ls -la apps/web/src/services/ 2>/dev/null || echo "Services directory not found at expected location"Repository: Stellar-Rent/stellar-rent
Length of output: 443
🏁 Script executed:
# Check the useDashboard.ts file to see imports and understand the context better
head -50 apps/web/src/hooks/useDashboard.tsRepository: Stellar-Rent/stellar-rent
Length of output: 1525
🏁 Script executed:
# Check the api.ts cancelBooking implementation
sed -n '145,165p' apps/web/src/services/api.tsRepository: Stellar-Rent/stellar-rent
Length of output: 587
🏁 Script executed:
# Check imports in useDashboard.ts to see if api.ts is already imported
head -20 apps/web/src/hooks/useDashboard.ts | grep -E "^import|^from"Repository: Stellar-Rent/stellar-rent
Length of output: 247
🏁 Script executed:
# Check other similar operations in useDashboard to see the pattern
sed -n '150,180p' apps/web/src/hooks/useDashboard.tsRepository: Stellar-Rent/stellar-rent
Length of output: 889
🏁 Script executed:
# Check if there are other API calls in useDashboard for comparison
rg "setError|setBookings|useCallback" apps/web/src/hooks/useDashboard.ts -A 3 -B 1 | head -50Repository: Stellar-Rent/stellar-rent
Length of output: 1699
🏁 Script executed:
# Check how other functions in the hook handle API calls - look for retryApiCall usage
sed -n '100,140p' apps/web/src/hooks/useDashboard.tsRepository: Stellar-Rent/stellar-rent
Length of output: 1176
🏁 Script executed:
# Look at the api.ts file to see if cancelBooking is exported
rg "export.*cancelBooking" apps/web/src/services/api.tsRepository: Stellar-Rent/stellar-rent
Length of output: 51
🏁 Script executed:
# Check if api is imported in useDashboard
rg "import.*api\|from.*services" apps/web/src/hooks/useDashboard.tsRepository: Stellar-Rent/stellar-rent
Length of output: 51
🏁 Script executed:
# Check the full cancelBooking implementation to see error handling
sed -n '174,185p' apps/web/src/hooks/useDashboard.tsRepository: Stellar-Rent/stellar-rent
Length of output: 449
🏁 Script executed:
# Let's check the entire cancelBooking implementation and surrounding context
sed -n '170,190p' apps/web/src/hooks/useDashboard.tsRepository: Stellar-Rent/stellar-rent
Length of output: 702
🏁 Script executed:
# Check if the api service is imported elsewhere in the codebase for reference
rg "import.*api.*from.*services" apps/web/src --type ts | head -10Repository: Stellar-Rent/stellar-rent
Length of output: 51
🏁 Script executed:
# Check how the hook is being used in the dashboard pages to understand expected behavior
sed -n '60,75p' apps/web/src/app/dashboard/host-dashboard/page.tsxRepository: Stellar-Rent/stellar-rent
Length of output: 570
🏁 Script executed:
# Look for any comments or TODOs around the cancelBooking mock implementation
sed -n '165,195p' apps/web/src/hooks/useDashboard.tsRepository: Stellar-Rent/stellar-rent
Length of output: 993
🏁 Script executed:
# Check if there's a comment or pattern indicating this is a mock/test hook
head -30 apps/web/src/hooks/useDashboard.ts | grep -i "mock\|test\|fake"Repository: Stellar-Rent/stellar-rent
Length of output: 51
🏁 Script executed:
# Check how api service is used elsewhere in hooks
rg "from.*services.*api|import.*api" apps/web/src/hooks/ --type tsRepository: Stellar-Rent/stellar-rent
Length of output: 259
🏁 Script executed:
# Verify if updateProfile and other mock functions also exist in api.ts
rg "async updateProfile|async deleteAccount|async uploadAvatar" apps/web/src/services/api.tsRepository: Stellar-Rent/stellar-rent
Length of output: 159
🏁 Script executed:
# Let's check the complete api.ts exports to understand the API service pattern
head -30 apps/web/src/services/api.tsRepository: Stellar-Rent/stellar-rent
Length of output: 924
🏁 Script executed:
# Check how other API calls are structured in api.ts for consistency
sed -n '1,50p' apps/web/src/services/api.tsRepository: Stellar-Rent/stellar-rent
Length of output: 1471
🏁 Script executed:
# Check if there's any configuration or environment flag that toggles mock mode
rg "MOCK|mock|TEST|test" apps/web/src/hooks/useDashboard.ts -iRepository: Stellar-Rent/stellar-rent
Length of output: 253
🏁 Script executed:
# Check if other functions in the hook that are mocked have corresponding api.ts implementations
rg "async updateProfile|async uploadAvatar|async deleteAccount" apps/web/src/services/api.ts -B 2 -A 5Repository: Stellar-Rent/stellar-rent
Length of output: 422
Replace mock cancelBooking with actual API call.
The current implementation uses setTimeout to simulate an API call and only updates local state without persisting the cancellation. Replace this with a call to api.cancelBooking(id) from the services layer, handle the API response, and only update local state after confirmation from the backend.
🤖 Prompt for AI Agents
In `@apps/web/src/hooks/useDashboard.ts` around lines 174 - 184, The cancelBooking
implementation currently uses a simulated delay and only updates local state;
replace it with a real API call by invoking api.cancelBooking(id) (import the
service if needed), await the response, check for success/failure, and only call
setBookings(prev => prev.filter(b => b.id !== id)) after the API confirms
cancellation; on error, call setError(err instanceof Error ? err.message :
'Failed to cancel booking') and return false, otherwise return true. Ensure
cancelBooking remains async and preserves existing signature and useCallback
dependencies.
|
@respp , you can check these changes now. I made use of Storybook to verify the UI consistency across different states (loading, error, empty) and ensure the merged components look solid. All merge conflicts are now resolved and unified. you can now run: |
…r-rent into error-handling # Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with '#' will be ignored, and an empty message aborts # the commit.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@apps/web/src/components/search/SearchBar.tsx`:
- Around line 13-28: The searchSchema's date checks use different baselines and
lack a cross-field rule; make a single consistent "start of today" baseline
(e.g., compute todayStart = new Date(); todayStart.setHours(0,0,0,0)) and use it
for both checkIn and checkOut validations (e.g., checkIn must be >= todayStart
and checkOut must be >= todayStart), and add a schema-level refine on
searchSchema that, when both checkIn and checkOut are present, enforces checkOut
> checkIn (with a clear error message) so the check-out cannot be the same or
before check-in.
- Around line 11-13: Missing import for the Zod namespace: add an import for z
(e.g., import { z } from 'zod') at the top of SearchBar.tsx alongside other
imports so the searchSchema and any other z.* usages (lines where searchSchema
is defined and the usage around line 87) resolve at runtime.
- Around line 240-244: The InlineError retry currently only clears state
(onRetry={() => setError(null)}) which wrongly makes the "Retry" button dismiss
errors instead of reattempting the action; update the usage in SearchBar.tsx to
either omit onRetry for validation errors or call the real retry handler: detect
if the current error is a validation error and in that case pass no onRetry to
InlineError, otherwise pass an onRetry that invokes the actual submit flow (call
handleSubmit or trigger the same function that performs the search) and then
clear the error (e.g., onRetry={() => { handleSubmit(); setError(null); }}),
keeping references to InlineError, setError, handleSubmit and the error variable
so reviewers can find the change.
🧹 Nitpick comments (1)
apps/web/src/components/search/SearchBar.tsx (1)
200-210: Consider disallowing same-day check-out.The disabled logic correctly prevents dates before check-in, but allows check-out on the same day as check-in. Typically bookings require at least one night. If same-day checkout is invalid, adjust:
- disabled={(d: Date) => d < (checkIn || new Date(new Date().setHours(0, 0, 0, 0)))} + disabled={(d: Date) => d <= (checkIn || new Date(new Date().setHours(0, 0, 0, 0)))}
| const searchSchema = z.object({ | ||
| location: z.string().min(2, 'Location must be at least 2 characters').max(100), | ||
| checkIn: z | ||
| .date() | ||
| .optional() | ||
| .refine((date) => !date || date > new Date(new Date().setHours(0, 0, 0, 0)), { | ||
| message: 'Check-in date must be in the future', | ||
| }), | ||
| checkOut: z | ||
| .date() | ||
| .optional() | ||
| .refine((date) => !date || date >= new Date(), { | ||
| message: 'Check-out date must be today or later', | ||
| }), | ||
| guests: z.number().int().min(1, 'At least 1 guest required').max(16).optional(), | ||
| }); |
There was a problem hiding this comment.
Schema has inconsistent date validation and missing cross-field check.
Two issues:
-
Inconsistent comparison bases: Check-in uses midnight today (
new Date().setHours(0,0,0,0)) while check-out uses current time (new Date()). This can cause unexpected validation results. -
Missing check-out > check-in validation: Users can select a check-out date that precedes check-in.
🔧 Proposed fix using Zod's refine at schema level
const searchSchema = z.object({
location: z.string().min(2, 'Location must be at least 2 characters').max(100),
checkIn: z
.date()
.optional()
- .refine((date) => !date || date > new Date(new Date().setHours(0, 0, 0, 0)), {
+ .refine((date) => !date || date >= new Date(new Date().setHours(0, 0, 0, 0)), {
message: 'Check-in date must be in the future',
}),
checkOut: z
.date()
.optional()
- .refine((date) => !date || date >= new Date(), {
+ .refine((date) => !date || date >= new Date(new Date().setHours(0, 0, 0, 0)), {
message: 'Check-out date must be today or later',
}),
guests: z.number().int().min(1, 'At least 1 guest required').max(16).optional(),
-});
+}).refine(
+ (data) => !data.checkIn || !data.checkOut || data.checkOut > data.checkIn,
+ { message: 'Check-out must be after check-in', path: ['checkOut'] }
+);📝 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 searchSchema = z.object({ | |
| location: z.string().min(2, 'Location must be at least 2 characters').max(100), | |
| checkIn: z | |
| .date() | |
| .optional() | |
| .refine((date) => !date || date > new Date(new Date().setHours(0, 0, 0, 0)), { | |
| message: 'Check-in date must be in the future', | |
| }), | |
| checkOut: z | |
| .date() | |
| .optional() | |
| .refine((date) => !date || date >= new Date(), { | |
| message: 'Check-out date must be today or later', | |
| }), | |
| guests: z.number().int().min(1, 'At least 1 guest required').max(16).optional(), | |
| }); | |
| const searchSchema = z.object({ | |
| location: z.string().min(2, 'Location must be at least 2 characters').max(100), | |
| checkIn: z | |
| .date() | |
| .optional() | |
| .refine((date) => !date || date >= new Date(new Date().setHours(0, 0, 0, 0)), { | |
| message: 'Check-in date must be in the future', | |
| }), | |
| checkOut: z | |
| .date() | |
| .optional() | |
| .refine((date) => !date || date >= new Date(new Date().setHours(0, 0, 0, 0)), { | |
| message: 'Check-out date must be today or later', | |
| }), | |
| guests: z.number().int().min(1, 'At least 1 guest required').max(16).optional(), | |
| }).refine( | |
| (data) => !data.checkIn || !data.checkOut || data.checkOut > data.checkIn, | |
| { message: 'Check-out must be after check-in', path: ['checkOut'] } | |
| ); |
🤖 Prompt for AI Agents
In `@apps/web/src/components/search/SearchBar.tsx` around lines 13 - 28, The
searchSchema's date checks use different baselines and lack a cross-field rule;
make a single consistent "start of today" baseline (e.g., compute todayStart =
new Date(); todayStart.setHours(0,0,0,0)) and use it for both checkIn and
checkOut validations (e.g., checkIn must be >= todayStart and checkOut must be
>= todayStart), and add a schema-level refine on searchSchema that, when both
checkIn and checkOut are present, enforces checkOut > checkIn (with a clear
error message) so the check-out cannot be the same or before check-in.
| {error && ( | ||
| <div className="px-2 animate-in fade-in slide-in-from-top-1 duration-200"> | ||
| <InlineError message={error} onRetry={() => setError(null)} /> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
"Retry" button only dismisses error without retrying.
The onRetry callback just clears the error state. Users clicking "Retry" expect the action to be re-attempted. For validation errors, either:
- Don't pass
onRetry(no retry button for validation errors) - Actually re-trigger
handleSubmiton retry
🔧 Option 1: Remove retry for validation errors
- <InlineError message={error} onRetry={() => setError(null)} />
+ <InlineError message={error} />📝 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.
| {error && ( | |
| <div className="px-2 animate-in fade-in slide-in-from-top-1 duration-200"> | |
| <InlineError message={error} onRetry={() => setError(null)} /> | |
| </div> | |
| )} | |
| {error && ( | |
| <div className="px-2 animate-in fade-in slide-in-from-top-1 duration-200"> | |
| <InlineError message={error} /> | |
| </div> | |
| )} |
🤖 Prompt for AI Agents
In `@apps/web/src/components/search/SearchBar.tsx` around lines 240 - 244, The
InlineError retry currently only clears state (onRetry={() => setError(null)})
which wrongly makes the "Retry" button dismiss errors instead of reattempting
the action; update the usage in SearchBar.tsx to either omit onRetry for
validation errors or call the real retry handler: detect if the current error is
a validation error and in that case pass no onRetry to InlineError, otherwise
pass an onRetry that invokes the actual submit flow (call handleSubmit or
trigger the same function that performs the search) and then clear the error
(e.g., onRetry={() => { handleSubmit(); setError(null); }}), keeping references
to InlineError, setError, handleSubmit and the error variable so reviewers can
find the change.
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/app/dashboard/host-dashboard/page.tsx (1)
156-172: Add user-facing error feedback on cancel failure.When
apiCancelBookingfails, the error is only logged to console. The user receives no indication that the cancellation failed. Consider adding an error notification or UI feedback.Suggested improvement
} catch (error) { console.error('Failed to cancel booking:', error); + addNotification({ + id: Date.now().toString(), + type: 'system' as const, + title: 'Cancellation Failed', + message: 'Unable to cancel the booking. Please try again.', + priority: 'high', + isRead: false, + createdAt: new Date().toISOString(), + userId: user.id.toString(), + }); }
🧹 Nitpick comments (3)
apps/web/src/app/dashboard/host-dashboard/page.tsx (3)
208-224: Consistent with other handlers but lacks error feedback.Same pattern as
handleCancelBooking- errors are only logged. Consider adding user-facing error notifications here as well for consistency.
424-433: Legacy type compatibility usinganycasts is fragile.The double
biome-ignoreforanycasts suggests type definitions are out of sync between API and local types. This technical debt should be tracked and resolved by aligning the type definitions.Would you like me to open an issue to track unifying the
UserProfiletype definitions between the API layer and local components?
191-191: Unsafe double type cast bypasses type safety due to mismatched preference structures.The
as unknown as ApiUserProfilecast masks a real type mismatch: the localUserProfile.preferenceshas onlynotifications,emailUpdates, andpushNotifications, while the API expectscurrency,language,notifications,emailNotifications,smsNotifications, andmarketingEmails. ThesafeUpdatesconstruction attempts to bridge this (line 185 renamesemailUpdatestoemailNotifications), butcurrencyandlanguageare missing, and the merge is unvalidated.Use an explicit transformer function similar to the pattern in
tenant-dashboard/page.tsx(seetransformFromLegacyUser) to safely convert and validate the merged object before passing it to the API.
|
@respp yet? |
respp
left a comment
There was a problem hiding this comment.
Nnce job Once you've fixed the merge conflicts, I'll get this merged.
|
Alright will look into this |
|
@respp can't find where ther merge conflict is could you plese point that out, cuz i can see |
|
@respp i would appreciate compliment points thanks as you do |

closes #200
Feature Implemented:
Loading States and Error Handling
I Implemente comprehensive loading states and error handling to improve UX during API calls and long-running operations.
Added
Error Components
ErrorDisplay and InlineError with retry functionality
Loading Components - Skeleton loaders, spinners, and loading grids
Retry Logic - Hook and utility for automatic API retry (3 attempts, 1s delay)
Enhanced useDashboard - Added retry logic to all API calls with better error handling
PropertyGrid - Added loading and error states with retry support
Providers - Replaced text loading with Spinner component
Summary by CodeRabbit
New Features
Bug Fixes
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.