@postxl/generators 1.11.6 → 1.12.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.
Files changed (42) hide show
  1. package/dist/backend-view/model-view-service.generator.js +4 -1
  2. package/dist/backend-view/model-view-service.generator.js.map +1 -1
  3. package/dist/backend-view/template/query.utils.test.ts +34 -0
  4. package/dist/backend-view/template/query.utils.ts +89 -0
  5. package/dist/frontend-admin/admin.generator.d.ts +9 -0
  6. package/dist/frontend-admin/admin.generator.js +19 -0
  7. package/dist/frontend-admin/admin.generator.js.map +1 -1
  8. package/dist/frontend-admin/generators/admin-sidebar.generator.js +1 -1
  9. package/dist/frontend-admin/generators/audit-log-sidebar.generator.js +107 -113
  10. package/dist/frontend-admin/generators/audit-log-sidebar.generator.js.map +1 -1
  11. package/dist/frontend-admin/generators/comment-sidebar.generator.d.ts +9 -0
  12. package/dist/frontend-admin/generators/comment-sidebar.generator.js +246 -0
  13. package/dist/frontend-admin/generators/comment-sidebar.generator.js.map +1 -0
  14. package/dist/frontend-admin/generators/detail-sidebar.generator.d.ts +9 -0
  15. package/dist/frontend-admin/generators/detail-sidebar.generator.js +148 -0
  16. package/dist/frontend-admin/generators/detail-sidebar.generator.js.map +1 -0
  17. package/dist/frontend-admin/generators/model-admin-page.generator.js +40 -6
  18. package/dist/frontend-admin/generators/model-admin-page.generator.js.map +1 -1
  19. package/dist/frontend-core/frontend.generator.js +2 -1
  20. package/dist/frontend-core/frontend.generator.js.map +1 -1
  21. package/dist/frontend-core/template/docs/LOGIN_PROCESS.d2 +372 -0
  22. package/dist/frontend-core/template/docs/LOGIN_PROCESS.md +214 -0
  23. package/dist/frontend-core/template/docs/LOGIN_PROCESS.svg +914 -0
  24. package/dist/frontend-core/template/src/components/admin/table-filter.stories.tsx +265 -0
  25. package/dist/frontend-core/template/src/components/admin/table-filter.tsx +224 -128
  26. package/dist/frontend-core/template/src/components/admin/table-view-panel.tsx +159 -0
  27. package/dist/frontend-core/template/src/context-providers/auth-context-provider.tsx +54 -43
  28. package/dist/frontend-core/template/src/hooks/use-table-view-config.tsx +249 -0
  29. package/dist/frontend-core/template/src/pages/dashboard/dashboard.page.tsx +5 -5
  30. package/dist/frontend-core/template/src/pages/error/auth-error.page.tsx +37 -0
  31. package/dist/frontend-core/template/src/pages/error/default-error.page.tsx +24 -40
  32. package/dist/frontend-core/template/src/pages/error/not-found-error.page.tsx +8 -10
  33. package/dist/frontend-core/template/src/pages/login/login.page.tsx +39 -13
  34. package/dist/frontend-core/template/src/pages/unauthorized/unauthorized.page.tsx +2 -2
  35. package/dist/frontend-core/template/src/routes/_auth-routes.tsx +21 -8
  36. package/dist/frontend-core/template/src/routes/auth-error.tsx +19 -0
  37. package/dist/frontend-core/template/vite.config.ts +5 -0
  38. package/dist/frontend-core/types/hook.d.ts +1 -1
  39. package/dist/frontend-tables/generators/model-table.generator.js +30 -5
  40. package/dist/frontend-tables/generators/model-table.generator.js.map +1 -1
  41. package/dist/types/template/query.types.ts +19 -1
  42. package/package.json +5 -5
@@ -1,6 +1,6 @@
1
- import type { UserRoles, ViewerResult } from '@authentication/authentication.types'
1
+ import type { Viewer } from '@authentication/auth.guard'
2
+ import type { ViewerResult } from '@authentication/authentication.types'
2
3
  import { useQuery } from '@tanstack/react-query'
3
- import type { User } from '@types'
4
4
 
5
5
  import Keycloak from 'keycloak-js'
6
6
  import * as React from 'react'
@@ -11,17 +11,18 @@ import { createUseContext } from '@lib/react'
11
11
  import { useTRPC } from '@lib/trpc'
12
12
  import { Result } from '@postxl/utils'
13
13
 
14
- type AppUser = {
15
- userDetails: User | null
16
- userRoles: UserRoles
17
- }
14
+ // Frontend version of Viewer without userInfo (which is only available server-side)
15
+ type FrontendViewer = Omit<Viewer, 'userInfo'>
16
+
17
+ export const authErrorTypes = ['errorInit', 'errorLoadUser', 'errorTokenRefresh'] as const
18
+ export type AuthErrorType = (typeof authErrorTypes)[number]
19
+ export type AuthState = 'inProgress' | 'loggedIn' | 'loggedOut'
18
20
 
19
21
  export type AuthContext = {
20
- isAuthenticated: boolean
21
- isInitComplete: boolean
22
+ loginState: AuthState | AuthErrorType
22
23
  login: () => Promise<void>
23
- logout: () => Promise<void>
24
- user: AppUser | null
24
+ logout: (options?: { redirectUri?: string }) => Promise<void>
25
+ viewerData: FrontendViewer | null
25
26
  }
26
27
 
27
28
  const AuthContext = createContext<AuthContext | null>(null)
@@ -42,7 +43,7 @@ export function getAuthHeaders(options?: RequestInit) {
42
43
  const headers = new Headers(options?.headers)
43
44
 
44
45
  if (token) {
45
- headers.set('Authorization', `Bearer${token}`)
46
+ headers.set('Authorization', `Bearer ${token}`)
46
47
  }
47
48
  return headers
48
49
  }
@@ -72,9 +73,8 @@ keycloak.onAuthError = () => {
72
73
  }
73
74
 
74
75
  export function AuthProvider({ children }: Readonly<{ children: React.ReactNode }>) {
75
- const [user, setUser] = React.useState<AppUser | null>(null)
76
- const [isAuthenticated, setIsAuthenticated] = useState(!APP_CONFIG.AUTH)
77
- const [isInitComplete, setIsInitComplete] = useState(!APP_CONFIG.AUTH)
76
+ const [viewerData, setViewerData] = React.useState<FrontendViewer | null>(null)
77
+ const [loginState, setLoginState] = useState<AuthState | AuthErrorType>('inProgress')
78
78
 
79
79
  const trpc = useTRPC()
80
80
 
@@ -86,13 +86,13 @@ export function AuthProvider({ children }: Readonly<{ children: React.ReactNode
86
86
  trpc.viewer.viewer.queryOptions(
87
87
  { pageUrl: location.href },
88
88
  // load user data when keycloak init complete and user is authenticated
89
- { enabled: isInitComplete && isAuthenticated },
89
+ { enabled: loginState === 'loggedIn' },
90
90
  ),
91
91
  )
92
92
 
93
- const logout = useCallback(async () => {
93
+ const logout = useCallback(async (options?: { redirectUri?: string }) => {
94
94
  setAuthToken()
95
- await keycloak.logout()
95
+ await keycloak.logout(options)
96
96
  }, [])
97
97
 
98
98
  const login = useCallback(async () => {
@@ -120,59 +120,66 @@ export function AuthProvider({ children }: Readonly<{ children: React.ReactNode
120
120
  checkLoginIframe: APP_CONFIG.checkLoginIframe,
121
121
  })
122
122
 
123
- if (authenticated) {
123
+ console.log(
124
+ 'Keycloak initialized COMPLETE, user authenticated:',
125
+ authenticated,
126
+ 'token:',
127
+ keycloak.token ? 'present' : 'missing',
128
+ )
129
+
130
+ if (authenticated && keycloak.token) {
124
131
  setAuthToken(keycloak.token)
125
- setIsAuthenticated(true)
132
+ setLoginState('loggedIn')
133
+ } else {
134
+ // if keycloak initialized and the user is not authenticated,
135
+ // the user will be redirected to the keycloak login page (see "/pages/Login")
136
+ setLoginState('loggedOut')
126
137
  }
127
- // if keycloak initialized and the user is not authenticated,
128
- // the user will be redirected to the keycloak login page (see "/pages/Login")
129
- console.log('Keycloak initialized COMPLETE, user authenticated:', authenticated)
130
- setIsInitComplete(true)
131
138
  }
132
139
  } catch (error) {
133
140
  console.error('Failed to initialize adapter:', error)
134
- void logout() // NOSONAR
141
+ setLoginState('errorInit')
135
142
  }
136
143
  }
137
144
 
138
145
  if (APP_CONFIG.AUTH) {
139
146
  void initKeycloak()
147
+ } else {
148
+ setLoginState('loggedIn')
140
149
  }
141
- }, [logout])
150
+ }, [])
142
151
 
143
152
  /**
144
153
  * #2 - user data loading
145
154
  * when user authenticated by keycloak, the user data is loaded from the backend
146
155
  */
147
156
  useEffect(() => {
148
- if (isInitComplete && isUserDataError) {
149
- void logout() // NOSONAR
157
+ if (loginState === 'loggedIn' && isUserDataError) {
158
+ setLoginState('errorLoadUser')
150
159
  return
151
160
  }
152
161
 
153
162
  if (userData && isUserSuccessfullyLoaded) {
154
- const userDataResult: ViewerResult = Result.fromObject(userData as unknown as any) as unknown as ViewerResult
163
+ const userDataResult: ViewerResult = Result.fromObject(userData as any) as ViewerResult
155
164
  if (userDataResult.isOk()) {
156
165
  const value = userDataResult.unwrap()
157
- console.log('User data loaded successfully:', value)
158
- setUser({
159
- userRoles: value.userRoles,
160
- userDetails: value.user,
161
- })
166
+ console.log('User data loaded successfully')
167
+ setViewerData(value)
162
168
  } else if (userDataResult.isErr()) {
163
- console.log('User data loaded with an error => logout()', { error: userDataResult.unwrapErr() })
164
- // when we get an error result on loading the user, we log out the user
165
- void logout() // NOSONAR
169
+ console.log('User data loaded with an error => logout and redirect to auth-error', {
170
+ error: userDataResult.unwrapErr(),
171
+ })
172
+ setLoginState('errorLoadUser')
166
173
  }
167
174
  }
168
- }, [userData, isUserSuccessfullyLoaded, isUserDataError, isInitComplete, logout])
175
+ }, [isUserDataError, isUserSuccessfullyLoaded, loginState, userData])
169
176
 
170
177
  /**
171
178
  * #3 - token refreshing
172
179
  * when the user is authenticated, the token is refreshed every 5 seconds
173
180
  */
174
181
  useEffect(() => {
175
- if (!isInitComplete || !isAuthenticated || !APP_CONFIG.AUTH) {
182
+ if (loginState !== 'loggedIn' || !APP_CONFIG.AUTH) {
176
183
  return
177
184
  }
178
185
 
@@ -188,17 +195,21 @@ export function AuthProvider({ children }: Readonly<{ children: React.ReactNode
188
195
  }
189
196
  } catch (error) {
190
197
  console.error('Failed to refresh token', error)
191
- // if the token could not be refreshed, log out the user
192
- void logout() // NOSONAR
198
+ setLoginState('errorTokenRefresh')
193
199
  }
194
200
  }, 5000)
195
201
 
196
202
  return () => clearInterval(refreshIntervalId)
197
- }, [isAuthenticated, isInitComplete, logout])
203
+ }, [loginState])
198
204
 
199
205
  const memoizedValues = useMemo(
200
- () => ({ isAuthenticated, login, logout, user, isInitComplete }),
201
- [isAuthenticated, login, logout, user, isInitComplete],
206
+ () => ({
207
+ loginState,
208
+ login,
209
+ logout,
210
+ viewerData,
211
+ }),
212
+ [loginState, login, logout, viewerData],
202
213
  )
203
214
 
204
215
  return <AuthContext.Provider value={memoizedValues}>{children}</AuthContext.Provider>
@@ -0,0 +1,249 @@
1
+ import type { TableViewId, TableViewViewModel } from '@types'
2
+
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
4
+ import { z } from 'zod'
5
+
6
+ import { useTableViews } from './use-table-views'
7
+
8
+ /** Creates a Zod decoder that parses a JSON string and validates the result against the given schema. */
9
+ const jsonDecoder = <T,>(schema: z.ZodType<T>) =>
10
+ z
11
+ .string()
12
+ .transform((s) => JSON.parse(s) as unknown)
13
+ .pipe(schema)
14
+
15
+ const filtersDecoder = jsonDecoder(z.record(z.string(), z.unknown()))
16
+ const sortingDecoder = jsonDecoder(z.array(z.object({ field: z.string(), direction: z.enum(['asc', 'desc']) })))
17
+ const columnVisibilityDecoder = jsonDecoder(z.record(z.string(), z.boolean()))
18
+ const columnOrderDecoder = jsonDecoder(z.array(z.string()))
19
+ const columnPinningDecoder = jsonDecoder(
20
+ z.object({ left: z.array(z.string()).optional(), right: z.array(z.string()).optional() }),
21
+ )
22
+ const columnSizingDecoder = jsonDecoder(z.record(z.string(), z.number()))
23
+
24
+ /**
25
+ * Parsed table state stored in a TableView.
26
+ * All JSON string fields are deserialized into their typed representations.
27
+ */
28
+ export type TableViewState = {
29
+ filters?: z.infer<typeof filtersDecoder>
30
+ sorting?: z.infer<typeof sortingDecoder>
31
+ columnVisibility?: z.infer<typeof columnVisibilityDecoder>
32
+ columnOrder?: z.infer<typeof columnOrderDecoder>
33
+ columnPinning?: z.infer<typeof columnPinningDecoder>
34
+ columnSizing?: z.infer<typeof columnSizingDecoder>
35
+ }
36
+
37
+ export type UseTableViewConfigOptions = {
38
+ /** The model/table name to scope views to (e.g. 'user', 'publication'). */
39
+ model: string
40
+ /** The current user's ID — used to filter views (own + global). */
41
+ currentUserId?: string
42
+ }
43
+
44
+ export type UseTableViewConfigResult = {
45
+ /** Views for this model visible to the current user (own + global). */
46
+ views: TableViewViewModel[]
47
+ /** Views mapped to the SavedView shape expected by DataGridViewMenu. */
48
+ savedViews: { id: string; name: string; isGlobal: boolean; userId: string }[]
49
+ /** Whether the initial data has been loaded. */
50
+ isLoaded: boolean
51
+ /** The currently active/applied view ID (local state, persisted to localStorage). */
52
+ activeViewId: string | null
53
+ /** Set the active view ID (also persists to localStorage). */
54
+ setActiveViewId: (id: string | null) => void
55
+ /** Clear the active view. */
56
+ clearActiveView: () => void
57
+ /** Parse a view's JSON string fields into typed TableViewState. */
58
+ parseViewState: (view: TableViewViewModel) => TableViewState
59
+ /** Get the parsed state of the currently active view, or null. */
60
+ getActiveViewState: () => TableViewState | null
61
+ /** Save a new view with the given state. Sets it as active on success. */
62
+ saveView: (args: { name: string; isGlobal?: boolean; state: TableViewState }) => Promise<TableViewViewModel>
63
+ /** Update an existing view's state. */
64
+ updateView: (viewId: string, state: TableViewState) => Promise<TableViewViewModel>
65
+ /** Rename an existing view. */
66
+ renameView: (viewId: string, name: string) => Promise<TableViewViewModel>
67
+ /** Delete a view. Clears active view if it was the deleted one. */
68
+ deleteView: (viewId: string) => Promise<void>
69
+ }
70
+
71
+ const STORAGE_PREFIX = 'pxl.tableView:'
72
+
73
+ function getStorageKey(model: string): string {
74
+ return `${STORAGE_PREFIX}${model}`
75
+ }
76
+
77
+ function readPersistedViewId(model: string): string | null {
78
+ try {
79
+ return localStorage.getItem(getStorageKey(model))
80
+ } catch {
81
+ return null
82
+ }
83
+ }
84
+
85
+ function persistViewId(model: string, viewId: string | null): void {
86
+ try {
87
+ const key = getStorageKey(model)
88
+ if (viewId) {
89
+ localStorage.setItem(key, viewId)
90
+ } else {
91
+ localStorage.removeItem(key)
92
+ }
93
+ } catch {
94
+ // ignore storage errors
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Hook that wraps the generated `useTableViews` CRUD hook and adds:
100
+ * - Scoping by model name and current user
101
+ * - JSON parsing/serialization for table state fields
102
+ * - Active view tracking (persisted to localStorage)
103
+ * - Auto-restore of previously active view on mount
104
+ * - Simplified save/update/rename/delete API
105
+ */
106
+ export function useTableViewConfig(options: UseTableViewConfigOptions): UseTableViewConfigResult {
107
+ const { model, currentUserId } = options
108
+ const tableViews = useTableViews()
109
+ const [activeViewId, setActiveViewIdState] = useState<string | null>(() => readPersistedViewId(model))
110
+ const hasRestoredRef = useRef(false)
111
+
112
+ // Persist active view ID to localStorage whenever it changes
113
+ const setActiveViewId = useCallback(
114
+ (id: string | null) => {
115
+ setActiveViewIdState(id)
116
+ persistViewId(model, id)
117
+ },
118
+ [model],
119
+ )
120
+
121
+ // Filter views: only this model, and only the user's own or global views
122
+ const views = useMemo(
123
+ (): TableViewViewModel[] =>
124
+ tableViews.list.filter((v) => v.model === model && (v.isGlobal || v.userId === currentUserId)),
125
+ [tableViews.list, model, currentUserId],
126
+ )
127
+
128
+ // Map to the shape expected by DataGridViewMenu
129
+ const savedViews = useMemo(
130
+ () => views.map((v) => ({ id: v.id, name: v.name, isGlobal: v.isGlobal, userId: v.userId })),
131
+ [views],
132
+ )
133
+
134
+ // Once views are loaded, validate the persisted view ID still exists.
135
+ // If the persisted view was deleted, clear it.
136
+ useEffect(() => {
137
+ if (!tableViews.isLoaded || hasRestoredRef.current) return
138
+ hasRestoredRef.current = true
139
+
140
+ if (activeViewId && !views.find((v) => v.id === activeViewId)) {
141
+ setActiveViewId(null)
142
+ }
143
+ }, [tableViews.isLoaded, views, activeViewId, setActiveViewId])
144
+
145
+ const parseViewState = useCallback((view: TableViewViewModel): TableViewState => {
146
+ const decode = <T,>(json: string | null | undefined, decoder: z.ZodType<T>): T | undefined => {
147
+ if (!json) {
148
+ return undefined
149
+ }
150
+ const result = decoder.safeParse(json)
151
+ return result.success ? result.data : undefined
152
+ }
153
+ return {
154
+ filters: decode(view.filters, filtersDecoder),
155
+ sorting: decode(view.sorting, sortingDecoder),
156
+ columnVisibility: decode(view.columnVisibility, columnVisibilityDecoder),
157
+ columnOrder: decode(view.columnOrder, columnOrderDecoder),
158
+ columnPinning: decode(view.columnPinning, columnPinningDecoder),
159
+ columnSizing: decode(view.columnSizing, columnSizingDecoder),
160
+ }
161
+ }, [])
162
+
163
+ const getActiveViewState = useCallback((): TableViewState | null => {
164
+ if (!activeViewId) return null
165
+ const view = views.find((v) => v.id === activeViewId)
166
+ if (!view) return null
167
+ return parseViewState(view)
168
+ }, [activeViewId, views, parseViewState])
169
+
170
+ const serializeState = useCallback(
171
+ (state: TableViewState) => ({
172
+ filters: state.filters ? JSON.stringify(state.filters) : null,
173
+ sorting: state.sorting ? JSON.stringify(state.sorting) : null,
174
+ columnVisibility: state.columnVisibility ? JSON.stringify(state.columnVisibility) : null,
175
+ columnOrder: state.columnOrder ? JSON.stringify(state.columnOrder) : null,
176
+ columnPinning: state.columnPinning ? JSON.stringify(state.columnPinning) : null,
177
+ columnSizing: state.columnSizing ? JSON.stringify(state.columnSizing) : null,
178
+ }),
179
+ [],
180
+ )
181
+
182
+ const saveView = useCallback(
183
+ async (args: { name: string; isGlobal?: boolean; state: TableViewState }) => {
184
+ if (!currentUserId) {
185
+ throw new Error('Cannot save a view without a currentUserId')
186
+ }
187
+ const result = await tableViews.create({
188
+ name: args.name,
189
+ model,
190
+ userId: currentUserId as TableViewViewModel['userId'],
191
+ isGlobal: args.isGlobal ?? false,
192
+ ...serializeState(args.state),
193
+ })
194
+ setActiveViewId(result.id)
195
+ return result
196
+ },
197
+ [tableViews, model, currentUserId, serializeState, setActiveViewId],
198
+ )
199
+
200
+ const updateView = useCallback(
201
+ async (viewId: string, state: TableViewState) => {
202
+ const existing = views.find((v) => v.id === viewId)
203
+ if (!existing) throw new Error(`View ${viewId} not found`)
204
+ return tableViews.update({
205
+ id: viewId as TableViewId,
206
+ name: existing.name,
207
+ model: existing.model,
208
+ userId: existing.userId,
209
+ isGlobal: existing.isGlobal,
210
+ ...serializeState(state),
211
+ })
212
+ },
213
+ [tableViews, views, serializeState],
214
+ )
215
+
216
+ const renameView = useCallback(
217
+ async (viewId: string, name: string) => {
218
+ return tableViews.updateField({ id: viewId as TableViewId, field: 'name', value: name })
219
+ },
220
+ [tableViews],
221
+ )
222
+
223
+ const deleteView = useCallback(
224
+ async (viewId: string) => {
225
+ await tableViews.delete(viewId as TableViewId)
226
+ if (activeViewId === viewId) {
227
+ setActiveViewId(null)
228
+ }
229
+ },
230
+ [tableViews, activeViewId, setActiveViewId],
231
+ )
232
+
233
+ const clearActiveView = useCallback(() => setActiveViewId(null), [setActiveViewId])
234
+
235
+ return {
236
+ views,
237
+ savedViews,
238
+ isLoaded: tableViews.isLoaded,
239
+ activeViewId,
240
+ setActiveViewId,
241
+ clearActiveView,
242
+ parseViewState,
243
+ getActiveViewState,
244
+ saveView,
245
+ updateView,
246
+ renameView,
247
+ deleteView,
248
+ }
249
+ }
@@ -16,7 +16,7 @@ import { APP_CONFIG } from '@lib/config'
16
16
  import { Button, ContentFrame } from '@postxl/ui-components'
17
17
 
18
18
  export const DashboardPage = () => {
19
- const { logout, user } = useAuth()
19
+ const { logout, viewerData } = useAuth()
20
20
 
21
21
  const [selectedPost, setSelectedPost] = useState<Post | null>(null)
22
22
  const [isUpdateModalOpen, setIsUpdateModalOpen] = useState(false)
@@ -42,16 +42,16 @@ export const DashboardPage = () => {
42
42
  controls={[
43
43
  <ColorModeToggle key="btn-color-mode" onClick={onToggleTheme} __e2e_test_id__="button-toggle-theme" />,
44
44
  APP_CONFIG.AUTH && (
45
- <Button key="btn-logout" onClick={logout}>
45
+ <Button key="btn-logout" onClick={() => logout()}>
46
46
  <PersonIcon /> Logout
47
47
  </Button>
48
48
  ),
49
49
  ]}
50
50
  >
51
51
  <div className="p-3">
52
- <h3 className="mb-4">Hi {user?.userDetails?.name}!</h3>
53
- <p>eMail: {user?.userDetails?.email}</p>
54
- <p>roles: {user?.userRoles.join(', ')}</p>
52
+ <h3 className="mb-4">Hi {viewerData?.user?.name}!</h3>
53
+ <p>eMail: {viewerData?.user?.email}</p>
54
+ <p>roles: {viewerData?.userRoles.join(', ')}</p>
55
55
  </div>
56
56
  <div className="p-3">
57
57
  <h3 className="mb-4">Posts:</h3>
@@ -0,0 +1,37 @@
1
+ import { AuthErrorType, useAuth } from '@context-providers/auth-context-provider'
2
+
3
+ import { Button, InfoCard } from '@postxl/ui-components'
4
+
5
+ const errorMessages: Record<AuthErrorType, { title: string; message: string }> = {
6
+ errorInit: {
7
+ title: 'Authentication Failed',
8
+ message: 'Failed to initialize the authentication system. Please try logging in again.',
9
+ },
10
+ errorLoadUser: {
11
+ title: 'Unable to Load User Data',
12
+ message: 'There was an error loading your user information. Please try logging in again.',
13
+ },
14
+ errorTokenRefresh: {
15
+ title: 'Session Expired',
16
+ message: 'Your session could not be refreshed. Please log in again to continue.',
17
+ },
18
+ }
19
+
20
+ export const AuthErrorPage = ({ errorType }: { errorType?: AuthErrorType }) => {
21
+ const errorInfo = errorMessages[errorType ?? 'errorInit']
22
+ const { logout } = useAuth()
23
+
24
+ return (
25
+ <InfoCard variant="error" title={errorInfo.title} message={errorInfo.message}>
26
+ <div className="flex gap-2">
27
+ <Button asChild>
28
+ {/* we don't use a tanstack link here to force a page reload. So all auth states will be re-initialized */}
29
+ <a href="/">Try Again</a>
30
+ </Button>
31
+ <Button variant="secondary" onClick={() => void logout({ redirectUri: window.location.origin })}>
32
+ Login Again
33
+ </Button>
34
+ </div>
35
+ </InfoCard>
36
+ )
37
+ }
@@ -1,6 +1,6 @@
1
- import { type ErrorComponentProps, Link } from '@tanstack/react-router'
1
+ import { type ErrorComponentProps } from '@tanstack/react-router'
2
2
 
3
- import { Button, Card, CardContent, CardFooter, CardHeader, CardTitle } from '@postxl/ui-components'
3
+ import { InfoCard } from '@postxl/ui-components'
4
4
 
5
5
  const isDev = import.meta.env.DEV
6
6
 
@@ -10,44 +10,28 @@ export const DefaultErrorPage = ({ error }: { error: ErrorComponentProps }) => {
10
10
  const errorStack = actualError instanceof Error ? actualError.stack : undefined
11
11
 
12
12
  return (
13
- <div className="flex w-full pt-4 px-4 h-(--content-without-header) items-center justify-center">
14
- <Card className="max-w-3xl w-full">
15
- <CardHeader>
16
- <CardTitle className="text-destructive">An unexpected error occurred</CardTitle>
17
- </CardHeader>
18
- <CardContent className="space-y-4">
19
- <div className="p-4 bg-destructive/10 border border-destructive/20 rounded-md">
20
- <p className="font-mono text-sm text-destructive break-words">{errorMessage}</p>
21
- </div>
13
+ <InfoCard variant="error" title="An unexpected error occurred" message={errorMessage} showHomeButton>
14
+ {isDev && errorStack && (
15
+ <details>
16
+ <summary className="cursor-pointer text-sm text-muted-foreground hover:text-foreground">
17
+ Show stack trace
18
+ </summary>
19
+ <pre className="mt-2 p-4 bg-muted rounded-md overflow-auto text-xs font-mono whitespace-pre-wrap break-words max-h-96">
20
+ {errorStack}
21
+ </pre>
22
+ </details>
23
+ )}
22
24
 
23
- {isDev && errorStack && (
24
- <details>
25
- <summary className="cursor-pointer text-sm text-muted-foreground hover:text-foreground">
26
- Show stack trace
27
- </summary>
28
- <pre className="mt-2 p-4 bg-muted rounded-md overflow-auto text-xs font-mono whitespace-pre-wrap break-words max-h-96">
29
- {errorStack}
30
- </pre>
31
- </details>
32
- )}
33
-
34
- {isDev && (
35
- <details>
36
- <summary className="cursor-pointer text-sm text-muted-foreground hover:text-foreground">
37
- Show raw error data
38
- </summary>
39
- <pre className="mt-2 p-4 bg-muted rounded-md overflow-auto text-xs font-mono whitespace-pre-wrap break-words max-h-64">
40
- {JSON.stringify(error, null, 2)}
41
- </pre>
42
- </details>
43
- )}
44
- </CardContent>
45
- <CardFooter>
46
- <Button asChild>
47
- <Link to={'/'}>Back to Home</Link>
48
- </Button>
49
- </CardFooter>
50
- </Card>
51
- </div>
25
+ {isDev && (
26
+ <details>
27
+ <summary className="cursor-pointer text-sm text-muted-foreground hover:text-foreground">
28
+ Show raw error data
29
+ </summary>
30
+ <pre className="mt-2 p-4 bg-muted rounded-md overflow-auto text-xs font-mono whitespace-pre-wrap break-words max-h-64">
31
+ {JSON.stringify(error, null, 2)}
32
+ </pre>
33
+ </details>
34
+ )}
35
+ </InfoCard>
52
36
  )
53
37
  }
@@ -1,17 +1,15 @@
1
- import { Link, NotFoundError } from '@tanstack/react-router'
1
+ import { NotFoundError } from '@tanstack/react-router'
2
2
 
3
- import { Button } from '@postxl/ui-components'
3
+ import { InfoCard } from '@postxl/ui-components'
4
4
 
5
5
  export const NotFoundErrorPage = ({ error }: { error: NotFoundError }) => {
6
6
  console.log(error)
7
7
  return (
8
- <div className="flex w-full pt-4 px-4 h-(--content-without-header)">
9
- <div className="flex flex-col gap-4 border-1 border-(--discreet-border) w-full h-full items-center justify-center">
10
- <h2>The page you’re looking for doesn’t exist.</h2>
11
- <Button asChild>
12
- <Link to={'/'}>Back to Home</Link>
13
- </Button>
14
- </div>
15
- </div>
8
+ <InfoCard
9
+ variant="not-found"
10
+ title="Page Not Found"
11
+ message="The page you're looking for doesn't exist."
12
+ showHomeButton
13
+ />
16
14
  )
17
15
  }