aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/components/alert
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src/components/alert')
-rw-r--r--FrontEnd/src/components/alert/AlertHost.tsx82
-rw-r--r--FrontEnd/src/components/alert/AlertService.ts114
-rw-r--r--FrontEnd/src/components/alert/alert.css21
-rw-r--r--FrontEnd/src/components/alert/index.ts8
4 files changed, 225 insertions, 0 deletions
diff --git a/FrontEnd/src/components/alert/AlertHost.tsx b/FrontEnd/src/components/alert/AlertHost.tsx
new file mode 100644
index 00000000..59f8f27c
--- /dev/null
+++ b/FrontEnd/src/components/alert/AlertHost.tsx
@@ -0,0 +1,82 @@
+import { useEffect, useState } from "react";
+import classNames from "classnames";
+
+import { ThemeColor, useC, Text } from "../common";
+import IconButton from "../button/IconButton";
+
+import { alertService, AlertInfoWithId } from "./AlertService";
+
+import "./alert.css";
+
+interface AutoCloseAlertProps {
+ color: ThemeColor;
+ message: Text;
+ onDismiss?: () => void;
+ onIn?: () => void;
+ onOut?: () => void;
+}
+
+function Alert({
+ color,
+ message,
+ onDismiss,
+ onIn,
+ onOut,
+}: AutoCloseAlertProps) {
+ const c = useC();
+
+ return (
+ <div
+ className={classNames("cru-alert", `cru-theme-${color}`)}
+ onPointerEnter={onIn}
+ onPointerLeave={onOut}
+ >
+ <div className="cru-alert-message">{c(message)}</div>
+ <IconButton
+ icon="x"
+ color="danger"
+ className="cru-alert-close-button"
+ onClick={onDismiss}
+ />
+ </div>
+ );
+}
+
+export default function AlertHost() {
+ const [alerts, setAlerts] = useState<AlertInfoWithId[]>([]);
+
+ useEffect(() => {
+ const listener = (alerts: AlertInfoWithId[]) => {
+ setAlerts(alerts);
+ };
+
+ alertService.registerListener(listener);
+
+ return () => {
+ alertService.unregisterListener(listener);
+ };
+ }, []);
+
+ return (
+ <div className="alert-container">
+ {alerts.map((alert) => {
+ return (
+ <Alert
+ key={alert.id}
+ message={alert.message}
+ color={alert.color ?? "primary"}
+ onIn={() => {
+ alertService.clearDismissTimer(alert.id);
+ }}
+ onOut={() => {
+ alertService.resetDismissTimer(alert.id);
+ }}
+ onDismiss={() => {
+ alertService.dismiss(alert.id);
+ }}
+ />
+ );
+ })}
+ </div>
+ );
+}
diff --git a/FrontEnd/src/components/alert/AlertService.ts b/FrontEnd/src/components/alert/AlertService.ts
new file mode 100644
index 00000000..b9cda752
--- /dev/null
+++ b/FrontEnd/src/components/alert/AlertService.ts
@@ -0,0 +1,114 @@
+import { ThemeColor, Text } from "../common";
+
+const defaultDismissTime = 5000;
+
+export interface AlertInfo {
+ color?: ThemeColor;
+ message: Text;
+ dismissTime?: number | "never";
+}
+
+export interface AlertInfoWithId extends AlertInfo {
+ id: number;
+}
+
+interface AlertServiceAlert extends AlertInfoWithId {
+ timerId: number | null;
+}
+
+export type AlertsListener = (alerts: AlertInfoWithId[]) => void;
+
+export class AlertService {
+ private listeners: AlertsListener[] = [];
+ private alerts: AlertServiceAlert[] = [];
+ private currentId = 1;
+
+ getAlert(alertId?: number | null | undefined): AlertServiceAlert | null {
+ for (const alert of this.alerts) {
+ if (alert.id === alertId) return alert;
+ }
+ return null;
+ }
+
+ registerListener(listener: AlertsListener): void {
+ this.listeners.push(listener);
+ listener(this.alerts);
+ }
+
+ unregisterListener(listener: AlertsListener): void {
+ this.listeners = this.listeners.filter((l) => l !== listener);
+ }
+
+ notify() {
+ for (const listener of this.listeners) {
+ listener(this.alerts);
+ }
+ }
+
+ push(alert: AlertInfo): void {
+ const newAlert: AlertServiceAlert = {
+ ...alert,
+ id: this.currentId++,
+ timerId: null,
+ };
+
+ this.alerts = [...this.alerts, newAlert];
+ this._resetDismissTimer(newAlert);
+
+ this.notify();
+ }
+
+ private _dismiss(alert: AlertServiceAlert) {
+ if (alert.timerId != null) {
+ window.clearTimeout(alert.timerId);
+ }
+ this.alerts = this.alerts.filter((a) => a !== alert);
+ this.notify();
+ }
+
+ dismiss(alertId?: number | null | undefined) {
+ const alert = this.getAlert(alertId);
+ if (alert != null) {
+ this._dismiss(alert);
+ }
+ }
+
+ private _clearDismissTimer(alert: AlertServiceAlert) {
+ if (alert.timerId != null) {
+ window.clearTimeout(alert.timerId);
+ alert.timerId = null;
+ }
+ }
+
+ clearDismissTimer(alertId?: number | null | undefined) {
+ const alert = this.getAlert(alertId);
+ if (alert != null) {
+ this._clearDismissTimer(alert);
+ }
+ }
+
+ private _resetDismissTimer(
+ alert: AlertServiceAlert,
+ dismissTime?: number | null | undefined,
+ ) {
+ this._clearDismissTimer(alert);
+
+ const realDismissTime =
+ dismissTime ?? alert.dismissTime ?? defaultDismissTime;
+
+ if (typeof realDismissTime === "number") {
+ alert.timerId = window.setTimeout(() => {
+ this._dismiss(alert);
+ }, realDismissTime);
+ }
+ }
+
+ resetDismissTimer(alertId?: number | null | undefined) {
+ const alert = this.getAlert(alertId);
+ if (alert != null) {
+ this._resetDismissTimer(alert);
+ }
+ }
+}
+
+export const alertService = new AlertService();
diff --git a/FrontEnd/src/components/alert/alert.css b/FrontEnd/src/components/alert/alert.css
new file mode 100644
index 00000000..948256de
--- /dev/null
+++ b/FrontEnd/src/components/alert/alert.css
@@ -0,0 +1,21 @@
+.alert-container {
+ position: fixed;
+ z-index: 1040;
+}
+
+.cru-alert {
+ border-radius: 5px;
+ border: var(--cru-theme-color) 2px solid;
+ color: var(--cru-text-primary-color);
+ background-color: var(--cru-container-background-color);
+
+ margin: 1em;
+ padding: 0.5em 1em;
+
+ display: flex;
+ align-items: center;
+}
+
+.cru-alert-close-button {
+ margin-left: auto;
+} \ No newline at end of file
diff --git a/FrontEnd/src/components/alert/index.ts b/FrontEnd/src/components/alert/index.ts
new file mode 100644
index 00000000..1be0c2ec
--- /dev/null
+++ b/FrontEnd/src/components/alert/index.ts
@@ -0,0 +1,8 @@
+import { alertService, AlertInfo } from "./AlertService";
+import { default as AlertHost } from "./AlertHost";
+
+export { alertService, AlertHost };
+
+export function pushAlert(alert: AlertInfo): void {
+ alertService.push(alert);
+}