Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,354 changes: 646 additions & 708 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@
"@deck.gl/layers": "^9.2.2",
"@deck.gl/react": "^9.2.2",
"@gisatcz/deckgl-geolib": "1.12.0-dev.5",
"@gisatcz/ptr-be-core": "^0.0.2",
"@mantine/core": "^8.0.2",
"@mantine/hooks": "^8.0.2",
"@tabler/icons-react": "^3.31.0",
Expand All @@ -110,6 +109,7 @@
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7",
"swr": "^2.3.0",
"uuid": "^11.1.0"
"uuid": "^11.1.0",
"@gisatcz/ptr-be-core": "^0.0.7"
}
}
84 changes: 84 additions & 0 deletions src/client/shared/errors/ErrorContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { createContext, useState, useCallback, useContext, ReactNode, useEffect } from 'react';

/**
* Shape of a single error item.
*/
export interface AppError {
id: string;
message: string;
title?: string;
type?: 'error' | 'warning' | 'info';
autoClose?: boolean;
}

/**
* The shape of the error context.
*/
interface ErrorContextType {
errors: AppError[];
addError: (error: Omit<AppError, 'id'>) => void;
removeError: (id: string) => void;
}

/**
* React Context for global error state management.
*/
const ErrorContext = createContext<ErrorContextType | undefined>(undefined);

/**
* Custom hook to access the ErrorContext.
* Throws an error if used outside of an ErrorProvider.
*/
export const useError = () => {
const context = useContext(ErrorContext);
if (!context) {
throw new Error('useError must be used within an ErrorProvider');
}
return context;
};

/**
* Provider component that encapsulates the error state and logic.
*/
export const ErrorProvider = ({ children }: { children: ReactNode }) => {
const [errors, setErrors] = useState<AppError[]>([]);

const removeError = useCallback((id: string) => {
setErrors((currentErrors) => currentErrors.filter((err) => err.id !== id));
}, []);

const addError = useCallback((error: Omit<AppError, 'id'>) => {
const id = Math.random().toString(36).substring(7);
const newError = { ...error, id };
setErrors((currentErrors) => [...currentErrors, newError]);

if (error.autoClose !== false) {
setTimeout(() => {
removeError(id);
}, 5000);
}
}, [removeError]);

// Effect to listen for custom events to add errors from outside React components
useEffect(() => {
const handleAddErrorEvent = (event: Event) => {
const errorDetail = (event as CustomEvent).detail;
if (errorDetail) {
addError(errorDetail);
}
};

window.addEventListener('add-app-error', handleAddErrorEvent);
return () => {
window.removeEventListener('add-app-error', handleAddErrorEvent);
};
}, [addError]);


return (
<ErrorContext.Provider value={{ errors, addError, removeError }}>
{children}
</ErrorContext.Provider>
);
};

83 changes: 83 additions & 0 deletions src/client/shared/errors/ErrorNotifications.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useEffect } from 'react';
import { useError } from './ErrorContext';
import { HandledError } from './store';
import './errorStyle.css';

/**
* Global error notification renderer.
*
* This component:
* - consumes the `ErrorContext` to display all current errors as dismissible notifications.
* - subscribes to global `window` events (`error` and `unhandledrejection`)
* to automatically capture and display unexpected runtime errors.
*
* It ignores `HandledError` instances to prevent duplicate notifications when
* the `throwError` utility is used.
*/
export const ErrorNotifications = () => {
const { errors, removeError, addError } = useError();

useEffect(() => {
const handleGlobalError = (event: ErrorEvent) => {
// Ignore errors that have already been handled by our system.
if (event.error instanceof HandledError) return;

addError({
title: 'Unexpected Error',
message: event.message || 'An unexpected error occurred',
type: 'error',
});
};

const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
// Ignore promise rejections from errors that have already been handled.
if (event.reason instanceof HandledError) return;

addError({
title: 'Unhandled Promise Rejection',
message: event.reason?.message || String(event.reason),
type: 'error',
});
};

window.addEventListener('error', handleGlobalError);
window.addEventListener('unhandledrejection', handleUnhandledRejection);

// Cleanup listeners on component unmount.
return () => {
window.removeEventListener('error', handleGlobalError);
window.removeEventListener('unhandledrejection', handleUnhandledRejection);
};
}, [addError]);

if (errors.length === 0) {
return null;
}

return (
<div className='errorNotification-position-div'>
{errors.map((error) => (
<div
key={error.id}
className={
error.type === 'warning'
? 'errorNotification-div errorNotification-div-warningType'
: 'errorNotification-div errorNotification-div-errorType'
}
>
<div>
{error.title && <strong>{error.title}</strong>}
<div>{error.message}</div>
</div>
<button
onClick={() => removeError(error.id)}
className="errorNotification-closeBtn"
>
&times;
</button>
</div>
))}
</div>
);
};

39 changes: 39 additions & 0 deletions src/client/shared/errors/GlobalError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import './errorStyle.css';

/**
* Props for the global error component used by consuming applications.
*
* - `error` – The error object caught by a Next.js error boundary. In addition
* to the standard `Error` fields, it may contain a `digest` field
* that Next.js uses internally to identify/log the error.
* - `reset` – Callback provided by Next.js that allows the UI to request
* a retry. When called, Next.js will try to re-render the
* affected route segment and recover from the error.
*/
interface GlobalErrorProps {
error: Error & { digest?: string };
reset: () => void;
}

/**
* Reusable global error UI for applications using the ptr-fe-core package.
*
* This component is intended to be used from the app's root `error.tsx`
* boundary. It displays a generic error message, shows the error's message
* text, and provides a “Try again” button which invokes the `reset` callback
* so Next.js can attempt to re-render and recover from the failure.
*/
export const GlobalError = ({ error, reset }: GlobalErrorProps) => {
return (
<div className="globalError-div">
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button
onClick={() => reset()}
className="globalError-btn">
Try again
</button>
</div>
);
};

59 changes: 59 additions & 0 deletions src/client/shared/errors/errorStyle.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
.globalError-div{
padding: 2rem;
text-align: center;
}
.globalError-btn{
padding: 0.5rem 1rem;
margin-top: 1rem;
cursor: pointer;
background-color: #0070f3;
color: white;
border: none;
border-radius: 4px
}

.ptrNotFound-link{
color: #0070f3;
text-decoration: underline;
}
.errorNotification-position-div{
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 10px;
}

.errorNotification-div{
padding: 1rem;
border: 1px solid;
border-radius: 4px;
min-width: 300px;
box-shadow: 0 2px 5px #00000033;
display: flex;
justify-content: space-between;
align-items: start;
}

.errorNotification-div-warningType{
background-color: #fff3cd;
border-color: #ffeeba;
color: #856404;
}

.errorNotification-div-errorType{
background-color: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
}

.errorNotification-closeBtn{
background: none;
border: none;
cursor: pointer;
font-size: 1.2rem;
margin-left: 10px;
color: inherit;
}
30 changes: 30 additions & 0 deletions src/client/shared/errors/ptrNotFound.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import './errorStyle.css';

/**
* Reusable "Not Found" (404) component for applications using ptr-fe-core.
*
* Renders a simple message that the requested resource could not be found and
* optionally displays a navigation link (by default a "Return Home" anchor).
* The layout and visual styling are handled via `errorStyle.css`.
*
* @param link - Optional React node rendered as the navigation element under
* the message. When omitted, a default "Return Home" link to `/` is used.
*/
export const PtrNotFound = ({
link = (
<a href="/" className="ptrNotFound-link">
Return Home
</a>
),
}: {
link?: React.ReactNode;
}) => {
return (
<div className="globalError-div">
<h2>Not Found</h2>
<p>Could not find requested resource</p>
{link}
</div>
);
};
43 changes: 43 additions & 0 deletions src/client/shared/errors/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* This file defines shared types and utility functions for the error handling system.
* It is designed to be used with the React Context-based error provider.
*/


/**
* Custom error type used to mark errors that have already been
* handled (i.e., a notification has been triggered for them).
*
* Global listeners can detect this error type and avoid showing a
* duplicate notification for the same issue.
*/
export class HandledError extends Error {
constructor(message: string) {
super(message);
this.name = 'HandledError';
}
}

/**
* Utility for throwing an error from non-component code while still triggering a UI notification.
*
* This function performs two actions:
* 1. Dispatches a global `add-app-error` custom event with the error details.
* The `ErrorProvider` listens for this event and adds the error to its state.
* 2. Throws a `HandledError` to interrupt execution flow, which can be caught
* by React/Next.js error boundaries.
*
* @param message The error message.
* @param title An optional title for the error notification.
*/
export const throwError = (message: string, title?: string) => {
const errorDetail = {
title: title || 'Error',
message,
type: 'error' as const
};
// Dispatch a global event that the ErrorProvider will listen for.
window.dispatchEvent(new CustomEvent('add-app-error', { detail: errorDetail }));
// Throw a specific error type to be caught by boundaries and ignored by global listeners.
throw new HandledError(message);
}