@tpzdsp/next-toolkit 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -46
- package/package.json +34 -3
- package/src/components/ErrorBoundary/ErrorFallback.stories.tsx +9 -7
- package/src/components/ErrorBoundary/ErrorFallback.test.tsx +67 -14
- package/src/components/ErrorBoundary/ErrorFallback.tsx +66 -12
- package/src/components/ErrorText/ErrorText.tsx +4 -2
- package/src/components/Heading/Heading.tsx +6 -3
- package/src/components/Modal/Modal.tsx +2 -1
- package/src/components/Paragraph/Paragraph.tsx +3 -1
- package/src/components/skipLink/SkipLink.tsx +2 -1
- package/src/errors/ApiError.ts +97 -23
- package/src/http/constants.ts +111 -0
- package/src/http/fetch.ts +263 -0
- package/src/http/index.ts +6 -0
- package/src/http/logger.ts +163 -0
- package/src/http/proxy.ts +269 -0
- package/src/http/query.ts +287 -0
- package/src/http/stream.ts +77 -0
- package/src/map/MapComponent.tsx +1 -1
- package/src/map/basemaps.ts +4 -4
- package/src/types/api.ts +25 -0
- package/src/utils/date.ts +6 -0
- package/src/utils/http.ts +2 -30
- package/src/utils/index.ts +1 -0
- package/src/utils/schema.ts +30 -0
package/README.md
CHANGED
|
@@ -262,7 +262,7 @@ yarn preview
|
|
|
262
262
|
|
|
263
263
|
## Local Development with Yalc
|
|
264
264
|
|
|
265
|
-
This library works
|
|
265
|
+
This library works well with `yalc` for local testing before publishing to npm. However, **hot reloading doesn't work reliably** with yalc - you'll need to manually push changes and restart your development server.
|
|
266
266
|
|
|
267
267
|
### Setup Yalc (one-time)
|
|
268
268
|
|
|
@@ -275,8 +275,6 @@ npm install -g yalc
|
|
|
275
275
|
|
|
276
276
|
```bash
|
|
277
277
|
# In this library directory
|
|
278
|
-
yarn yalc:publish
|
|
279
|
-
# or
|
|
280
278
|
yalc publish
|
|
281
279
|
```
|
|
282
280
|
|
|
@@ -284,9 +282,9 @@ yalc publish
|
|
|
284
282
|
|
|
285
283
|
```bash
|
|
286
284
|
# In your Next.js app directory
|
|
287
|
-
yalc add @
|
|
285
|
+
yalc add @tpzdsp/next-toolkit
|
|
288
286
|
|
|
289
|
-
# Install dependencies
|
|
287
|
+
# Install dependencies
|
|
290
288
|
yarn install
|
|
291
289
|
```
|
|
292
290
|
|
|
@@ -297,69 +295,48 @@ Add to your `next.config.js`:
|
|
|
297
295
|
```js
|
|
298
296
|
/** @type {import('next').NextConfig} */
|
|
299
297
|
const nextConfig = {
|
|
300
|
-
transpilePackages: ['@
|
|
301
|
-
// Enable if you want faster refresh during development
|
|
298
|
+
transpilePackages: ['@tpzdsp/next-toolkit'],
|
|
302
299
|
experimental: {
|
|
303
|
-
externalDir: true,
|
|
300
|
+
externalDir: true, // May help with yalc symlinks
|
|
304
301
|
},
|
|
305
302
|
};
|
|
306
303
|
|
|
307
304
|
module.exports = nextConfig;
|
|
308
305
|
```
|
|
309
306
|
|
|
310
|
-
### Development Workflow
|
|
307
|
+
### Development Workflow (Manual Process)
|
|
311
308
|
|
|
312
309
|
```bash
|
|
313
|
-
# 1. Make changes to the library
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
# or
|
|
310
|
+
# 1. Make changes to the library components/hooks/etc.
|
|
311
|
+
|
|
312
|
+
# 2. Push changes to connected apps
|
|
317
313
|
yalc push
|
|
318
314
|
|
|
319
|
-
# 3.
|
|
315
|
+
# 3. In your test app, clear Next.js cache and restart
|
|
316
|
+
rm -rf .next && yarn dev
|
|
320
317
|
```
|
|
321
318
|
|
|
319
|
+
### Why No Hot Reloading?
|
|
320
|
+
|
|
321
|
+
- **Symlink Issues**: Next.js file watching doesn't reliably detect changes in yalc-linked packages
|
|
322
|
+
- **Module Resolution**: TypeScript and bundler caching can prevent updates from being picked up
|
|
323
|
+
- **Source Distribution**: While our source distribution strategy helps with tree-shaking, it doesn't solve the hot reload problem with yalc
|
|
324
|
+
|
|
322
325
|
### Removing from Test App
|
|
323
326
|
|
|
324
327
|
```bash
|
|
325
328
|
# In your Next.js app directory
|
|
326
|
-
yalc remove @
|
|
329
|
+
yalc remove @tpzdsp/next-toolkit
|
|
327
330
|
yarn install
|
|
328
331
|
```
|
|
329
332
|
|
|
330
|
-
### Benefits
|
|
333
|
+
### Benefits Despite Manual Process
|
|
331
334
|
|
|
332
|
-
- ✅ **
|
|
335
|
+
- ✅ **Real Environment Testing**: Test in an actual Next.js app setup
|
|
333
336
|
- ✅ **Full TypeScript**: Complete type checking and IntelliSense
|
|
334
|
-
- ✅ **Tree Shaking**: Your app's bundler handles optimization
|
|
335
|
-
- ✅ **
|
|
336
|
-
-
|
|
337
|
-
|
|
338
|
-
## 🚀 Quick Yalc Workflow
|
|
339
|
-
|
|
340
|
-
### Library Development (this repo):
|
|
341
|
-
|
|
342
|
-
```bash
|
|
343
|
-
# First time setup
|
|
344
|
-
yalc publish
|
|
345
|
-
|
|
346
|
-
# After making changes
|
|
347
|
-
yalc push # Pushes to all connected apps
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
### In Your Next.js Test App:
|
|
351
|
-
|
|
352
|
-
```bash
|
|
353
|
-
# Add the library
|
|
354
|
-
yalc add @your-org/nextjs-library
|
|
355
|
-
yarn install
|
|
356
|
-
|
|
357
|
-
# Configure next.config.js (transpilePackages: ['@your-org/nextjs-library'])
|
|
358
|
-
|
|
359
|
-
# Remove when done testing
|
|
360
|
-
yalc remove @your-org/nextjs-library
|
|
361
|
-
yarn install
|
|
362
|
-
```
|
|
337
|
+
- ✅ **Tree Shaking**: Your app's bundler handles optimization
|
|
338
|
+
- ✅ **No Publishing**: Test without polluting npm registry
|
|
339
|
+
- ❌ **Manual Refresh Required**: No automatic hot reloading
|
|
363
340
|
|
|
364
341
|
## 🛠️ Adding New Components
|
|
365
342
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpzdsp/next-toolkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "A reusable React component library for Next.js applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
"import": "./src/utils/http.ts",
|
|
39
39
|
"require": "./src/utils/http.ts"
|
|
40
40
|
},
|
|
41
|
+
"./utils/schema": {
|
|
42
|
+
"types": "./src/utils/schema.ts",
|
|
43
|
+
"import": "./src/utils/schema.ts",
|
|
44
|
+
"require": "./src/utils/schema.ts"
|
|
45
|
+
},
|
|
41
46
|
"./components": {
|
|
42
47
|
"types": "./src/components/index.ts",
|
|
43
48
|
"import": "./src/components/index.ts",
|
|
@@ -48,6 +53,11 @@
|
|
|
48
53
|
"import": "./src/components/select/index.ts",
|
|
49
54
|
"require": "./src/components/select/index.ts"
|
|
50
55
|
},
|
|
56
|
+
"./http": {
|
|
57
|
+
"types": "./src/http/index.ts",
|
|
58
|
+
"import": "./src/http/index.ts",
|
|
59
|
+
"require": "./src/http/index.ts"
|
|
60
|
+
},
|
|
51
61
|
"./map": {
|
|
52
62
|
"types": "./src/map/index.ts",
|
|
53
63
|
"import": "./src/map/index.ts",
|
|
@@ -104,6 +114,7 @@
|
|
|
104
114
|
"release": "semantic-release"
|
|
105
115
|
},
|
|
106
116
|
"devDependencies": {
|
|
117
|
+
"@better-fetch/fetch": "1.1.19-beta.1",
|
|
107
118
|
"@eslint/js": "^9.30.1",
|
|
108
119
|
"@semantic-release/changelog": "^6.0.3",
|
|
109
120
|
"@semantic-release/git": "^10.0.1",
|
|
@@ -113,11 +124,13 @@
|
|
|
113
124
|
"@storybook/addon-interactions": "^8.6.14",
|
|
114
125
|
"@storybook/addon-links": "8.6.14",
|
|
115
126
|
"@storybook/addon-onboarding": "8.6.14",
|
|
127
|
+
"@storybook/preview-api": "^8.6.14",
|
|
116
128
|
"@storybook/react": "8.6.14",
|
|
117
129
|
"@storybook/react-vite": "8.6.14",
|
|
118
130
|
"@storybook/test": "^8.6.14",
|
|
119
131
|
"@storybook/types": "8.6.14",
|
|
120
132
|
"@tailwindcss/typography": "^0.5.16",
|
|
133
|
+
"@tanstack/react-query": "^5.89.0",
|
|
121
134
|
"@testing-library/dom": "^10.4.0",
|
|
122
135
|
"@testing-library/jest-dom": "^6.6.3",
|
|
123
136
|
"@testing-library/react": "^16.3.0",
|
|
@@ -137,6 +150,7 @@
|
|
|
137
150
|
"autoprefixer": "^10.4.21",
|
|
138
151
|
"buffer": "^6.0.3",
|
|
139
152
|
"crypto-browserify": "^3.12.1",
|
|
153
|
+
"date-fns": "^4.1.0",
|
|
140
154
|
"eslint": "^9.30.1",
|
|
141
155
|
"eslint-config-prettier": "^10.1.8",
|
|
142
156
|
"eslint-import-resolver-alias": "^1.1.2",
|
|
@@ -182,14 +196,18 @@
|
|
|
182
196
|
"util": "^0.12.5",
|
|
183
197
|
"vite": "^7.0.4",
|
|
184
198
|
"vite-plugin-dts": "^4.5.4",
|
|
185
|
-
"vitest": "^3.2.4"
|
|
199
|
+
"vitest": "^3.2.4",
|
|
200
|
+
"zod": "^4.1.8"
|
|
186
201
|
},
|
|
187
202
|
"peerDependencies": {
|
|
203
|
+
"@better-fetch/fetch": "1.1.19-beta.1",
|
|
204
|
+
"@tanstack/react-query": "^5.89.0",
|
|
188
205
|
"@testing-library/react": "^16.0.0",
|
|
189
206
|
"@testing-library/user-event": "^14.6.1",
|
|
190
207
|
"@turf/turf": "^7.2.0",
|
|
191
208
|
"@types/geojson": "^7946.0.16",
|
|
192
209
|
"@types/jsonwebtoken": "^9.0.10",
|
|
210
|
+
"date-fns": "^4.1.0",
|
|
193
211
|
"geojson": "^0.5.0",
|
|
194
212
|
"jsonwebtoken": "^9.0.2",
|
|
195
213
|
"next": "^15.4.2",
|
|
@@ -200,9 +218,16 @@
|
|
|
200
218
|
"react": "^19.1.0",
|
|
201
219
|
"react-dom": "^19.1.0",
|
|
202
220
|
"react-error-boundary": "^6.0.0",
|
|
203
|
-
"react-icons": "^5.5.0"
|
|
221
|
+
"react-icons": "^5.5.0",
|
|
222
|
+
"zod": "^4.1.8"
|
|
204
223
|
},
|
|
205
224
|
"peerDependenciesMeta": {
|
|
225
|
+
"@tanstack/react-query": {
|
|
226
|
+
"optional": true
|
|
227
|
+
},
|
|
228
|
+
"@better-fetch/fetch": {
|
|
229
|
+
"optional": true
|
|
230
|
+
},
|
|
206
231
|
"@turf/turf": {
|
|
207
232
|
"optional": true
|
|
208
233
|
},
|
|
@@ -212,6 +237,9 @@
|
|
|
212
237
|
"@types/jsonwebtoken": {
|
|
213
238
|
"optional": true
|
|
214
239
|
},
|
|
240
|
+
"date-fns": {
|
|
241
|
+
"optional": true
|
|
242
|
+
},
|
|
215
243
|
"geojson": {
|
|
216
244
|
"optional": true
|
|
217
245
|
},
|
|
@@ -232,6 +260,9 @@
|
|
|
232
260
|
},
|
|
233
261
|
"react-select": {
|
|
234
262
|
"optional": true
|
|
263
|
+
},
|
|
264
|
+
"zod": {
|
|
265
|
+
"optional": true
|
|
235
266
|
}
|
|
236
267
|
},
|
|
237
268
|
"optionalDependencies": {
|
|
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
|
|
|
3
3
|
|
|
4
4
|
import { ErrorFallback } from './ErrorFallback';
|
|
5
5
|
import { ApiError } from '../../errors/ApiError';
|
|
6
|
-
import {
|
|
6
|
+
import { HttpStatus } from '../../http';
|
|
7
7
|
|
|
8
8
|
const meta = {
|
|
9
9
|
title: 'Components/ErrorFallback',
|
|
@@ -13,7 +13,7 @@ const meta = {
|
|
|
13
13
|
docs: {
|
|
14
14
|
description: {
|
|
15
15
|
component:
|
|
16
|
-
'
|
|
16
|
+
'Displayed when an error is caught by `ErrorBoundary`. Shows the error message, optional details in an Accordion, and Copy/Retry buttons.',
|
|
17
17
|
},
|
|
18
18
|
},
|
|
19
19
|
},
|
|
@@ -23,7 +23,7 @@ const meta = {
|
|
|
23
23
|
export default meta;
|
|
24
24
|
type Story = StoryObj<typeof meta>;
|
|
25
25
|
|
|
26
|
-
// Shared mock reset
|
|
26
|
+
// Shared mock reset function
|
|
27
27
|
const mockReset = () => alert('Reset error boundary called');
|
|
28
28
|
|
|
29
29
|
export const Default: Story = {
|
|
@@ -34,7 +34,8 @@ export const Default: Story = {
|
|
|
34
34
|
parameters: {
|
|
35
35
|
docs: {
|
|
36
36
|
description: {
|
|
37
|
-
story:
|
|
37
|
+
story:
|
|
38
|
+
'Default rendering with a standard JavaScript `Error`. Shows heading, reason, and Copy/Retry buttons.',
|
|
38
39
|
},
|
|
39
40
|
},
|
|
40
41
|
},
|
|
@@ -42,13 +43,14 @@ export const Default: Story = {
|
|
|
42
43
|
|
|
43
44
|
export const WithApiError: Story = {
|
|
44
45
|
args: {
|
|
45
|
-
error: new ApiError('Request failed',
|
|
46
|
+
error: new ApiError('Request failed', HttpStatus.InternalServerError, 'Some technical details'),
|
|
46
47
|
resetErrorBoundary: mockReset,
|
|
47
48
|
},
|
|
48
49
|
parameters: {
|
|
49
50
|
docs: {
|
|
50
51
|
description: {
|
|
51
|
-
story:
|
|
52
|
+
story:
|
|
53
|
+
'Rendering when the error is an `ApiError`. Shows reason, details in an Accordion, digest, and Copy/Retry buttons.',
|
|
52
54
|
},
|
|
53
55
|
},
|
|
54
56
|
},
|
|
@@ -63,7 +65,7 @@ export const WithUnknownError: Story = {
|
|
|
63
65
|
docs: {
|
|
64
66
|
description: {
|
|
65
67
|
story:
|
|
66
|
-
'If the error is not an `Error` or `ApiError`, a generic "Unknown" message is shown.',
|
|
68
|
+
'If the error is not an `Error` or `ApiError`, a generic "Unknown error" message is shown, with Copy/Retry buttons.',
|
|
67
69
|
},
|
|
68
70
|
},
|
|
69
71
|
},
|
|
@@ -1,54 +1,107 @@
|
|
|
1
1
|
import { ErrorFallback } from './ErrorFallback';
|
|
2
2
|
import { ApiError } from '../../errors';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { HttpStatus } from '../../http';
|
|
4
|
+
import { render, screen, userEvent, within } from '../../test/renderers';
|
|
5
5
|
import { identityFn } from '../../utils/utils';
|
|
6
6
|
|
|
7
|
+
const FAKE_UUID = 'fake-uuid';
|
|
8
|
+
|
|
9
|
+
Object.defineProperty(navigator, 'clipboard', {
|
|
10
|
+
value: {
|
|
11
|
+
writeText: vi.fn().mockResolvedValue(undefined),
|
|
12
|
+
},
|
|
13
|
+
configurable: true,
|
|
14
|
+
});
|
|
15
|
+
|
|
7
16
|
describe('ErrorFallback', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
(navigator.clipboard.writeText as unknown) = vi.fn().mockResolvedValue(undefined);
|
|
19
|
+
vi.spyOn(crypto, 'randomUUID').mockReturnValue(FAKE_UUID);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
8
26
|
it('renders a generic error message for an unknown error type', () => {
|
|
9
27
|
render(<ErrorFallback error={{} as unknown as Error} resetErrorBoundary={identityFn} />);
|
|
10
28
|
|
|
11
29
|
const alert = screen.getByRole('alert');
|
|
12
30
|
|
|
13
|
-
expect(alert).toHaveTextContent(/
|
|
31
|
+
expect(alert).toHaveTextContent(/something went wrong/i);
|
|
14
32
|
expect(alert).toHaveTextContent(/reason: unknown/i);
|
|
15
33
|
});
|
|
16
34
|
|
|
17
35
|
it('renders message for standard Error instance', () => {
|
|
18
|
-
const error = new Error('
|
|
36
|
+
const error = new Error('custom error message');
|
|
19
37
|
|
|
20
38
|
render(<ErrorFallback error={error} resetErrorBoundary={identityFn} />);
|
|
21
39
|
|
|
22
|
-
expect(screen.
|
|
40
|
+
expect(screen.getByRole('alert')).toHaveTextContent(/reason: custom error message/i);
|
|
23
41
|
});
|
|
24
42
|
|
|
25
|
-
it('renders message and
|
|
26
|
-
const apiError = new ApiError('Fake Reason',
|
|
43
|
+
it('renders message, details and digest for ApiError instance', async () => {
|
|
44
|
+
const apiError = new ApiError('Fake Reason', HttpStatus.InternalServerError, 'Some details');
|
|
27
45
|
|
|
28
46
|
render(<ErrorFallback error={apiError} resetErrorBoundary={identityFn} />);
|
|
29
47
|
|
|
30
|
-
|
|
48
|
+
// Message shown
|
|
49
|
+
expect(screen.getByRole('alert')).toHaveTextContent(/reason: fake reason/i);
|
|
50
|
+
|
|
51
|
+
// Accordion exists but details hidden until toggled
|
|
52
|
+
const accordionButton = screen.getByRole('button', { name: /show details/i });
|
|
53
|
+
|
|
54
|
+
expect(accordionButton).toBeInTheDocument();
|
|
55
|
+
|
|
56
|
+
// Expand and check details text
|
|
57
|
+
await userEvent.click(accordionButton);
|
|
58
|
+
|
|
59
|
+
const section = screen.getByRole('region', { hidden: false });
|
|
60
|
+
|
|
61
|
+
expect(within(section).getByText(/error id:/i)).toBeInTheDocument();
|
|
62
|
+
expect(within(section).getByText(/some details/i)).toBeInTheDocument();
|
|
31
63
|
});
|
|
32
64
|
|
|
33
|
-
it('renders
|
|
34
|
-
const error = new Error('Has a cause', { cause: [
|
|
65
|
+
it('renders non-string causes', () => {
|
|
66
|
+
const error = new Error('Has a cause', { cause: ['I will display'] });
|
|
35
67
|
|
|
36
68
|
render(<ErrorFallback error={error} resetErrorBoundary={identityFn} />);
|
|
37
69
|
|
|
38
|
-
expect(screen.
|
|
39
|
-
expect(screen.
|
|
70
|
+
expect(screen.getByRole('alert')).toHaveTextContent(/reason: has a cause/i);
|
|
71
|
+
expect(screen.queryByRole('button', { name: /show details/i })).toBeInTheDocument();
|
|
40
72
|
});
|
|
41
73
|
|
|
42
|
-
it('calls resetErrorBoundary when
|
|
74
|
+
it('calls resetErrorBoundary when Retry is clicked', async () => {
|
|
43
75
|
const user = userEvent.setup();
|
|
44
76
|
const resetSpy = vi.fn();
|
|
45
77
|
|
|
46
78
|
render(<ErrorFallback error={new Error('Test error')} resetErrorBoundary={resetSpy} />);
|
|
47
79
|
|
|
48
|
-
const button = screen.getByRole('button', { name: /
|
|
80
|
+
const button = screen.getByRole('button', { name: /retry/i });
|
|
49
81
|
|
|
50
82
|
await user.click(button);
|
|
51
83
|
|
|
52
84
|
expect(resetSpy).toHaveBeenCalledTimes(1);
|
|
53
85
|
});
|
|
86
|
+
|
|
87
|
+
it('copies error info to clipboard when Copy is clicked', async () => {
|
|
88
|
+
const user = userEvent.setup();
|
|
89
|
+
const apiError = new ApiError('Copy Reason', HttpStatus.BadRequest, 'Some details');
|
|
90
|
+
|
|
91
|
+
render(<ErrorFallback error={apiError} resetErrorBoundary={identityFn} />);
|
|
92
|
+
|
|
93
|
+
const copyButton = screen.getByRole('button', { name: /copy/i });
|
|
94
|
+
|
|
95
|
+
await user.click(copyButton);
|
|
96
|
+
|
|
97
|
+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
|
|
98
|
+
expect.stringContaining('App Error: Copy Reason'),
|
|
99
|
+
);
|
|
100
|
+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
|
|
101
|
+
expect.stringContaining('Details: Some details'),
|
|
102
|
+
);
|
|
103
|
+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
|
|
104
|
+
expect.stringContaining(`Digest: ${FAKE_UUID}`),
|
|
105
|
+
);
|
|
106
|
+
});
|
|
54
107
|
});
|
|
@@ -1,30 +1,84 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useId } from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { type FallbackProps } from 'react-error-boundary';
|
|
4
4
|
|
|
5
5
|
import { ApiError } from '../../errors/ApiError';
|
|
6
|
+
import { Accordion } from '../accordion/Accordion';
|
|
7
|
+
import { Button } from '../Button/Button';
|
|
8
|
+
import { Heading } from '../Heading/Heading';
|
|
9
|
+
import { Paragraph } from '../Paragraph/Paragraph';
|
|
6
10
|
|
|
7
11
|
export const ErrorFallback = ({ resetErrorBoundary, error }: FallbackProps) => {
|
|
12
|
+
const id = useId();
|
|
13
|
+
|
|
8
14
|
let message;
|
|
9
|
-
let
|
|
15
|
+
let details = undefined;
|
|
16
|
+
let digest = undefined;
|
|
10
17
|
|
|
11
18
|
if (error instanceof ApiError) {
|
|
12
19
|
message = error.message;
|
|
13
|
-
|
|
20
|
+
details = error.details;
|
|
21
|
+
digest = error.digest;
|
|
14
22
|
} else if (error instanceof Error) {
|
|
15
|
-
|
|
23
|
+
message = error.message;
|
|
24
|
+
details = error.cause?.toString();
|
|
16
25
|
} else {
|
|
17
|
-
message = 'Unknown';
|
|
26
|
+
message = 'Unknown error';
|
|
18
27
|
}
|
|
19
28
|
|
|
29
|
+
const copyToClipboard = () => {
|
|
30
|
+
navigator.clipboard.writeText(
|
|
31
|
+
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
|
32
|
+
`App Error: ${message}, ${details ? `Details: ${details}, ` : ''}${digest ? `Digest: ${digest}` : ''}`,
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
20
36
|
return (
|
|
21
|
-
<div
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
<div
|
|
38
|
+
role="alert"
|
|
39
|
+
aria-labelledby={id}
|
|
40
|
+
className="grid gap-2 border-form border-transparent border-l-error max-w-full pl-2"
|
|
41
|
+
style={{ gridTemplateColumns: '1fr auto' }}
|
|
42
|
+
>
|
|
43
|
+
<div className="flex flex-col gap-0.5">
|
|
44
|
+
<Heading id={id} type="h3" className="text-error text-base font-semibold py-0">
|
|
45
|
+
Something went wrong
|
|
46
|
+
</Heading>
|
|
47
|
+
|
|
48
|
+
<Paragraph className="leading-snug pb-0">
|
|
49
|
+
<strong>Reason:</strong> {message}
|
|
50
|
+
</Paragraph>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div className="flex items-start gap-1">
|
|
54
|
+
<Button onClick={copyToClipboard} variant="secondary" className="shrink-0">
|
|
55
|
+
Copy
|
|
56
|
+
</Button>
|
|
57
|
+
|
|
58
|
+
<Button onClick={resetErrorBoundary} variant="secondary" className="shrink-0">
|
|
59
|
+
Retry
|
|
60
|
+
</Button>
|
|
61
|
+
</div>
|
|
26
62
|
|
|
27
|
-
|
|
63
|
+
{details && (
|
|
64
|
+
<div className="col-span-2">
|
|
65
|
+
<Accordion title="Show details">
|
|
66
|
+
<div className="flex gap-0.5 flex-col">
|
|
67
|
+
{digest && (
|
|
68
|
+
<Paragraph className="leading-snug pb-0">
|
|
69
|
+
<strong>Error ID:</strong> {digest}
|
|
70
|
+
</Paragraph>
|
|
71
|
+
)}
|
|
72
|
+
<pre
|
|
73
|
+
className="text-base bg-white border border-gray-200 p-2 overflow-y-auto max-h-32
|
|
74
|
+
whitespace-pre-wrap"
|
|
75
|
+
>
|
|
76
|
+
{details}
|
|
77
|
+
</pre>
|
|
78
|
+
</div>
|
|
79
|
+
</Accordion>
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
28
82
|
</div>
|
|
29
83
|
);
|
|
30
84
|
};
|
|
@@ -5,13 +5,15 @@ import type { ExtendProps } from '../../types/utils';
|
|
|
5
5
|
export type ErrorTextProps = ExtendProps<'p'>;
|
|
6
6
|
|
|
7
7
|
export const ErrorText = ({ className, children, ...props }: ErrorTextProps) => {
|
|
8
|
+
const Component = typeof children === 'string' ? 'p' : 'div';
|
|
9
|
+
|
|
8
10
|
return (
|
|
9
|
-
<
|
|
11
|
+
<Component
|
|
10
12
|
role="alert"
|
|
11
13
|
className={twMerge('mb-3 text-base text-error font-bold', className)}
|
|
12
14
|
{...props}
|
|
13
15
|
>
|
|
14
16
|
{children}
|
|
15
|
-
</
|
|
17
|
+
</Component>
|
|
16
18
|
);
|
|
17
19
|
};
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { twMerge } from 'tailwind-merge';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import type { ExtendProps } from '../../types';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
4
6
|
type: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
5
|
-
className?: string;
|
|
6
|
-
children: React.ReactNode;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
+
// h1 through h6 have identical props
|
|
10
|
+
export type HeadingProps = ExtendProps<'h1', Props>;
|
|
11
|
+
|
|
9
12
|
export const Heading = ({ type, className, children }: HeadingProps) => {
|
|
10
13
|
switch (type) {
|
|
11
14
|
case 'h1':
|
|
@@ -36,7 +36,8 @@ export const Modal = ({ isOpen, onClose, children }: ModalProps) => {
|
|
|
36
36
|
// dialog elements do have a key handler as you can close them with `Escape`, so this can be ignored
|
|
37
37
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
|
|
38
38
|
<dialog
|
|
39
|
-
className="fixed inset-0 flex items-center justify-center w-full h-full m-0 bg-transparent
|
|
39
|
+
className="fixed inset-0 flex items-center justify-center w-full h-full m-0 bg-transparent
|
|
40
|
+
backdrop:bg-black/50"
|
|
40
41
|
ref={modalRef}
|
|
41
42
|
onCancel={(event) => {
|
|
42
43
|
event.preventDefault();
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { twMerge } from 'tailwind-merge';
|
|
2
|
+
|
|
1
3
|
import type { ExtendProps } from '../../types/utils';
|
|
2
4
|
|
|
3
5
|
export type ParagraphProps = ExtendProps<'p'>;
|
|
4
6
|
|
|
5
7
|
export const Paragraph = ({ className, children, ...props }: ParagraphProps) => {
|
|
6
8
|
return (
|
|
7
|
-
<p className={
|
|
9
|
+
<p className={twMerge('pb-4 text-sm text-text-primary', className)} {...props}>
|
|
8
10
|
{children}
|
|
9
11
|
</p>
|
|
10
12
|
);
|
|
@@ -22,7 +22,8 @@ export const SkipLink = ({ mainContentId = 'main-content' }: SkipLinkProps) => {
|
|
|
22
22
|
return (
|
|
23
23
|
<nav aria-label="Skip navigation">
|
|
24
24
|
<Link
|
|
25
|
-
className="absolute w-full p-3 text-black bg-focus focus:relative focus:top-0 -top-full
|
|
25
|
+
className="absolute w-full p-3 text-black bg-focus focus:relative focus:top-0 -top-full
|
|
26
|
+
visited:text-black hover:text-black skip-link"
|
|
26
27
|
href={`#${mainContentId}`}
|
|
27
28
|
onClick={handleActivate}
|
|
28
29
|
onKeyDown={(event) => {
|