Skip to content

Commit b11e757

Browse files
committed
Handle nested API error
1 parent c35f61a commit b11e757

File tree

3 files changed

+115
-92
lines changed

3 files changed

+115
-92
lines changed

src/App.tsx

+81-82
Original file line numberDiff line numberDiff line change
@@ -138,93 +138,92 @@ export default function App() {
138138
</div>
139139

140140
{/* Table */}
141-
{error ? (
141+
{error && (
142142
<div className="text-center text-red-500 p-4 whitespace-pre-line">
143143
{error}
144144
</div>
145-
) : (
146-
<div className="relative">
147-
{loading && (
148-
<div className="absolute inset-0 bg-white/70 flex items-center justify-center z-10">
149-
<div className="text-gray-600">Loading...</div>
150-
</div>
151-
)}
152-
<div className="overflow-x-auto">
153-
<table className="min-w-full bg-white shadow-md rounded-lg">
154-
<thead className="bg-gray-50">
155-
<tr>
156-
<th
157-
onClick={() => handleSort("date")}
158-
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100">
159-
Date{" "}
160-
<SortIcon
161-
column="date"
162-
currentSort={sortBy}
163-
order={sortOrder}
164-
/>
165-
</th>
166-
<th
167-
onClick={() => handleSort("revenue")}
168-
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100">
169-
Revenue{" "}
170-
<SortIcon
171-
column="revenue"
172-
currentSort={sortBy}
173-
order={sortOrder}
174-
/>
175-
</th>
176-
<th
177-
onClick={() => handleSort("net_income")}
178-
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100">
179-
Net Income{" "}
180-
<SortIcon
181-
column="net_income"
182-
currentSort={sortBy}
183-
order={sortOrder}
184-
/>
185-
</th>
186-
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
187-
Gross Profit
188-
</th>
189-
<th
190-
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
191-
title="Earnings Per Share">
192-
EPS
193-
</th>
194-
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
195-
Operating Income
196-
</th>
197-
</tr>
198-
</thead>
199-
200-
<tbody className="divide-y divide-gray-200">
201-
{statements.map((statement) => (
202-
<tr key={statement.date} className="hover:bg-gray-50">
203-
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
204-
{statement.date}
205-
</td>
206-
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
207-
${formatMoney(statement.revenue)}
208-
</td>
209-
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
210-
${formatMoney(statement.net_income)}
211-
</td>
212-
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
213-
${formatMoney(statement.gross_profit)}
214-
</td>
215-
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
216-
${statement.eps.toFixed(2)}
217-
</td>
218-
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
219-
${formatMoney(statement.operating_income)}
220-
</td>
221-
</tr>
222-
))}
223-
</tbody>
224-
</table>
145+
)}
146+
<div className="relative">
147+
{loading && (
148+
<div className="absolute inset-0 bg-white/70 flex items-center justify-center z-10">
149+
<div className="text-gray-600">Loading...</div>
225150
</div>
151+
)}
152+
<div className="overflow-x-auto">
153+
<table className="min-w-full bg-white shadow-md rounded-lg">
154+
<thead className="bg-gray-50">
155+
<tr>
156+
<th
157+
onClick={() => handleSort("date")}
158+
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100">
159+
Date{" "}
160+
<SortIcon
161+
column="date"
162+
currentSort={sortBy}
163+
order={sortOrder}
164+
/>
165+
</th>
166+
<th
167+
onClick={() => handleSort("revenue")}
168+
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100">
169+
Revenue{" "}
170+
<SortIcon
171+
column="revenue"
172+
currentSort={sortBy}
173+
order={sortOrder}
174+
/>
175+
</th>
176+
<th
177+
onClick={() => handleSort("net_income")}
178+
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100">
179+
Net Income{" "}
180+
<SortIcon
181+
column="net_income"
182+
currentSort={sortBy}
183+
order={sortOrder}
184+
/>
185+
</th>
186+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
187+
Gross Profit
188+
</th>
189+
<th
190+
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
191+
title="Earnings Per Share">
192+
EPS
193+
</th>
194+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
195+
Operating Income
196+
</th>
197+
</tr>
198+
</thead>
199+
200+
<tbody className="divide-y divide-gray-200">
201+
{statements.map((statement) => (
202+
<tr key={statement.date} className="hover:bg-gray-50">
203+
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
204+
{statement.date}
205+
</td>
206+
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
207+
${formatMoney(statement.revenue)}
208+
</td>
209+
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
210+
${formatMoney(statement.net_income)}
211+
</td>
212+
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
213+
${formatMoney(statement.gross_profit)}
214+
</td>
215+
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
216+
${statement.eps.toFixed(2)}
217+
</td>
218+
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
219+
${formatMoney(statement.operating_income)}
220+
</td>
221+
</tr>
222+
))}
223+
</tbody>
224+
</table>
226225
</div>
227-
)}
226+
</div>
228227
</div>
229228
);
230229
}

src/types/index.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ export interface ValidationError {
1616
type: string;
1717
}
1818

19+
export interface APIErrorDetails {
20+
type: string;
21+
message: string;
22+
details: string;
23+
}
24+
25+
export interface NestedAPIError {
26+
error: APIErrorDetails;
27+
}
28+
1929
export interface APIError {
20-
detail?: string | ValidationError[];
30+
detail?: string | ValidationError[] | NestedAPIError;
2131
}

src/utils/index.ts

+23-9
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,37 @@
11
import { AxiosError } from "axios";
2-
import { APIError, ValidationError } from "../types";
2+
import { APIError, ValidationError, NestedAPIError } from "../types";
33
import numeral from "numeral";
44

5+
const isValidationErrorArray = (
6+
detail: APIError["detail"]
7+
): detail is ValidationError[] => {
8+
return Array.isArray(detail);
9+
};
10+
11+
const isNestedError = (
12+
detail: APIError["detail"]
13+
): detail is NestedAPIError => {
14+
return typeof detail === "object" && detail !== null && "error" in detail;
15+
};
16+
517
export const formatError = (error: unknown) => {
618
if (error instanceof AxiosError) {
719
if (error.response) {
820
const apiError = error.response?.data as APIError;
21+
const { detail } = apiError;
922

10-
if (Array.isArray(apiError.detail)) {
11-
// Handle validation errors
12-
return apiError.detail
13-
.map(
14-
(err: ValidationError) =>
15-
`${err.loc[1].replace("_", " ")}: ${err.msg}`
16-
)
23+
if (isValidationErrorArray(detail)) {
24+
return detail
25+
.map((err) => `${err.loc[1].replace("_", " ")}: ${err.msg}`)
1726
.join("\n");
1827
}
1928

20-
return apiError.detail || error.message;
29+
if (isNestedError(detail)) {
30+
const { error: nestedError } = detail as NestedAPIError;
31+
return nestedError.message || nestedError.details || error.message;
32+
}
33+
34+
return detail || error.message;
2135
}
2236

2337
return error.message;

0 commit comments

Comments
 (0)