Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
34 changes: 22 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
"@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.1-dev.9",
"@gisatcz/ptr-be-core": "^0.0.2",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"@gisatcz/ptr-be-core": "^0.0.2",
"@gisatcz/ptr-be-core": "^0.0.7",

"@mantine/core": "^8.0.2",
"@mantine/hooks": "^8.0.2",
"@tabler/icons-react": "^3.31.0",
Expand Down
4 changes: 2 additions & 2 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export default [
{
input: 'src/client/index.ts', // Entry point of the library
output: [
{file: pkg.main, format: 'cjs', sourcemap: true}, // CommonJS output
{file: pkg.module, format: 'esm', sourcemap: true} // ES module output
{file: pkg.main, format: 'cjs', sourcemap: true, banner: "'use client';"}, // CommonJS output
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No NextJS magic in NPM please. We can handle this part per Next app, not in NPM. Also we dont want to have all mandatory parts use client by default (from layouts etc.)

{file: pkg.module, format: 'esm', sourcemap: true, banner: "'use client';"} // ES module output
],
external: id => {
const externals = [
Expand Down
86 changes: 86 additions & 0 deletions src/client/shared/errors/ErrorContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'use client';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope


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;
}
Loading