e.stopPropagation()}
- >
-
- {/* Title */}
-
- {title}
-
+const ActionConfirmation = ({
+ isOpen,
+ onCloseDelete,
+ onConfirmDelete,
+ title,
+ subtitle = "Are you sure?",
+ boldSubtitle = "",
+ warningMessage = "This action cannot be undone.",
+ variant = "delete",
+}: {
+ isOpen: boolean;
+ onCloseDelete: () => void;
+ onConfirmDelete: () => void;
+ title: string;
+ subtitle: string;
+ boldSubtitle: string;
+ warningMessage: string;
+ variant?: ActionConfirmationVariant;
+}) => {
+ if (!isOpen) return null;
- {/* Message */}
-
- {subtitle + " "}
- {boldSubtitle}
- {"?"}
-
+ const styles =
+ variant === "create"
+ ? {
+ panel: "border-t-4 border-green bg-green-light/30",
+ stripe: "bg-green",
+ box: "bg-green-light",
+ Icon: FaCheckCircle,
+ iconClass: "text-green",
+ label: "Confirm",
+ labelClass: "text-green",
+ textClass: "text-green-dark",
+ cancelClass:
+ "text-grey-700 border-grey-500 hover:border-grey-600 hover:bg-grey-150 active:bg-grey-200",
+ }
+ : variant === "update"
+ ? {
+ panel: "border-t-4 border-grey-400 bg-grey-150",
+ stripe: "bg-grey-500",
+ box: "bg-grey-200",
+ Icon: FaInfoCircle,
+ iconClass: "text-grey-700",
+ label: "Review",
+ labelClass: "text-grey-800",
+ textClass: "text-grey-800",
+ cancelClass:
+ "text-grey-700 border-grey-500 hover:border-grey-600 hover:bg-grey-200 active:bg-grey-300",
+ }
+ : {
+ panel: "border-t-4 border-red bg-red-lightest/40",
+ stripe: "bg-red",
+ box: "bg-red-light",
+ Icon: IoIosWarning,
+ iconClass: "text-red",
+ label: "Warning",
+ labelClass: "text-red",
+ textClass: "text-red",
+ cancelClass:
+ "text-red border-red hover:border-red hover:bg-red-light active:bg-red",
+ };
-
+ const { Icon } = styles;
-
+ return (
+
+
e.stopPropagation()}
+ >
+
{title}
-
-
-
-
- {warningMessage}
-
+
+ {subtitle + " "}
+ {boldSubtitle}
+ {"?"}
+
+
+
-
-
- {/* Buttons */}
-
- {
+
+ {
onConfirmDelete();
onCloseDelete();
- }} className="border-grey-500 hover:text-primary-900" />
-
-
+ }}
+ className="border-grey-500"
+ />
-
- );
- };
+
+ );
+};
- export default ActionConfirmation;
\ No newline at end of file
+export default ActionConfirmation;
diff --git a/frontend/src/main-page/cash-flow/components/CashAddRevenue.tsx b/frontend/src/main-page/cash-flow/components/CashAddRevenue.tsx
index ec5825dc..edde1299 100644
--- a/frontend/src/main-page/cash-flow/components/CashAddRevenue.tsx
+++ b/frontend/src/main-page/cash-flow/components/CashAddRevenue.tsx
@@ -10,6 +10,7 @@ import CashRevenueInstallment, {
EditableInstallment,
} from "./CashRevenueInstallment";
import { createNewRevenue, isValidInstallment, toInstallment } from "../../cash-flow/processCashflowDataEditSave";
+import ActionConfirmation from "../../../components/ActionConfirmation";
type FieldErrors = {
type?: string;
@@ -46,6 +47,10 @@ export default function CashAddRevenue() {
const [errors, setErrors] = useState
({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [successMessage, setSuccessMessage] = useState(null);
+ const [showConfirmModal, setShowConfirmModal] = useState(false);
+ const [pendingRevenue, setPendingRevenue] = useState(
+ null,
+ );
const showSuccessMessage = (message: string) => {
setSuccessMessage(message);
@@ -120,12 +125,19 @@ export default function CashAddRevenue() {
setErrors({});
}
- const handleSubmit = async () => {
+ const requestSubmit = () => {
setSuccessMessage(null);
const payload = buildPayload();
if (!payload) {
return;
}
+ setPendingRevenue(payload);
+ setShowConfirmModal(true);
+ };
+
+ const handleConfirmedSubmit = async () => {
+ if (!pendingRevenue) return;
+ const payload = pendingRevenue;
setIsSubmitting(true);
setErrors((previous) => ({ ...previous, submit: undefined }));
@@ -192,6 +204,22 @@ export default function CashAddRevenue() {
return (
+
{
+ setShowConfirmModal(false);
+ setPendingRevenue(null);
+ }}
+ onConfirmDelete={() => {
+ void handleConfirmedSubmit();
+ setPendingRevenue(null);
+ }}
+ title="Create revenue source"
+ subtitle="Are you sure you want to add"
+ boldSubtitle={pendingRevenue?.name ?? ""}
+ warningMessage="This will create a new revenue line in your cash flow."
+ variant="create"
+ />
{"Add Revenue Source"}
@@ -297,7 +325,7 @@ export default function CashAddRevenue() {
/>
diff --git a/frontend/src/main-page/cash-flow/components/CashEditLineItem.tsx b/frontend/src/main-page/cash-flow/components/CashEditLineItem.tsx
index 19e80957..b456d336 100644
--- a/frontend/src/main-page/cash-flow/components/CashEditLineItem.tsx
+++ b/frontend/src/main-page/cash-flow/components/CashEditLineItem.tsx
@@ -69,6 +69,7 @@ export default function CashEditLineItem({
subtitle={"Are you sure you want to delete"}
boldSubtitle={sourceName}
warningMessage="If you delete this item, it will be permanently removed from the system."
+ variant="delete"
/>
);
diff --git a/frontend/src/main-page/cash-flow/components/CashEditRevenue.tsx b/frontend/src/main-page/cash-flow/components/CashEditRevenue.tsx
index 72eeeefb..05af6821 100644
--- a/frontend/src/main-page/cash-flow/components/CashEditRevenue.tsx
+++ b/frontend/src/main-page/cash-flow/components/CashEditRevenue.tsx
@@ -5,9 +5,8 @@ import { Installment } from "../../../../../middle-layer/types/Installment";
import { RevenueType } from "../../../../../middle-layer/types/RevenueType";
import Button from "../../../components/Button";
import InputField from "../../../components/InputField";
-import {
- saveRevenueEdits,
-} from "../processCashflowDataEditSave";
+import { saveRevenueEdits } from "../processCashflowDataEditSave";
+import ActionConfirmation from "../../../components/ActionConfirmation";
import CashCategoryDropdown from "./CashCategoryDropdown";
import CashRevenueInstallment, {
EditableInstallment,
@@ -71,6 +70,10 @@ export default function CashEditRevenue({
);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
+ const [showConfirmModal, setShowConfirmModal] = useState(false);
+ const [pendingRevenue, setPendingRevenue] = useState(
+ null,
+ );
const isValidInstallment = (installment: EditableInstallment) => {
if (installment.amount === null || installment.date === null) {
@@ -196,11 +199,18 @@ export default function CashEditRevenue({
)
: (singleInstallment.amount ?? 0);
- const handleSave = async () => {
+ const requestSave = () => {
const payload = buildPayload();
if (!payload) {
return;
}
+ setPendingRevenue(payload);
+ setShowConfirmModal(true);
+ };
+
+ const handleConfirmedSave = async () => {
+ if (!pendingRevenue) return;
+ const payload = pendingRevenue;
setIsSubmitting(true);
setErrors((previous) => ({ ...previous, submit: undefined }));
@@ -221,6 +231,22 @@ export default function CashEditRevenue({
return (
+
{
+ setShowConfirmModal(false);
+ setPendingRevenue(null);
+ }}
+ onConfirmDelete={() => {
+ void handleConfirmedSave();
+ setPendingRevenue(null);
+ }}
+ title="Update revenue source"
+ subtitle="Are you sure you want to save changes to"
+ boldSubtitle={pendingRevenue?.name ?? revenueItem.name}
+ warningMessage="This will update this revenue line in your cash flow."
+ variant="update"
+ />
diff --git a/frontend/src/main-page/grants/edit-grant/EditGrant.tsx b/frontend/src/main-page/grants/edit-grant/EditGrant.tsx
index 6e400a08..2e5394d9 100644
--- a/frontend/src/main-page/grants/edit-grant/EditGrant.tsx
+++ b/frontend/src/main-page/grants/edit-grant/EditGrant.tsx
@@ -47,6 +47,7 @@ const EditGrant: React.FC<{
// State to track if form was submitted successfully
const [saving, setSaving] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
+ const [showSaveModal, setShowSaveModal] = useState(false);
const [form, dispatch] = useReducer(reducer, {
organization: grantToEdit?.organization ?? "",
@@ -187,7 +188,7 @@ const EditGrant: React.FC<{
setShowSaveModal(true)}
disabled={saving}
/>
@@ -222,8 +223,29 @@ const EditGrant: React.FC<{
subtitle={"Are you sure you want to delete"}
boldSubtitle={form.organization}
warningMessage="If you delete this grant, it will be permanently removed from the system."
+ variant="delete"
/>
)}
+ setShowSaveModal(false)}
+ onConfirmDelete={() => {
+ handleSubmit();
+ }}
+ title={grantToEdit ? "Save Grant" : "Create Grant"}
+ subtitle={
+ grantToEdit
+ ? "Are you sure you want to save changes to"
+ : "Are you sure you want to create a grant for"
+ }
+ boldSubtitle={form.organization}
+ warningMessage={
+ grantToEdit
+ ? "Saving will update this grant's details in the system."
+ : "A new grant will be added to the system with these details."
+ }
+ variant={grantToEdit ? "update" : "create"}
+ />
{/* Error Popup */}
diff --git a/frontend/src/main-page/navbar/NavBar.tsx b/frontend/src/main-page/navbar/NavBar.tsx
index b7dab5a9..e73ae05d 100644
--- a/frontend/src/main-page/navbar/NavBar.tsx
+++ b/frontend/src/main-page/navbar/NavBar.tsx
@@ -1,3 +1,4 @@
+import { useState } from "react";
import { useNavigate } from "react-router-dom";
import {
clearAllFilters,
@@ -12,6 +13,7 @@ import NavTab, { NavTabProps } from "./NavTab.tsx";
import { faChartLine, faMoneyBill, faClipboardCheck } from "@fortawesome/free-solid-svg-icons";
import { NavBarBranding } from "../../translations/general.ts";
import { saveCashflowSettings } from "../cash-flow/processCashflowDataEditSave";
+import ActionConfirmation from "../../components/ActionConfirmation";
const tabs: NavTabProps[] = [
{ name: "Dashboard", linkTo: "/main/dashboard", icon: faChartLine },
@@ -25,19 +27,32 @@ const NavBar: React.FC = observer(() => {
const navigate = useNavigate();
const user = getAppStore().user;
const isAdmin = user?.position === UserStatus.Admin;
+ const [signOutConfirmOpen, setSignOutConfirmOpen] = useState(false);
- const handleLogout = async () => {
+ const performLogout = async () => {
const { cashflowSettings } = getAppStore();
if (cashflowSettings) {
- await saveCashflowSettings(cashflowSettings);
- }
+ await saveCashflowSettings(cashflowSettings);
+ }
logoutUser();
clearAllFilters();
navigate("/login");
};
-
+
return (
+ setSignOutConfirmOpen(false)}
+ onConfirmDelete={() => {
+ void performLogout();
+ }}
+ title="Sign out"
+ subtitle="Are you sure you want to"
+ boldSubtitle="sign out"
+ warningMessage="Your cash flow settings will be saved to the server, then you will be logged out."
+ variant="update"
+ />
{/* Logo at top */}
@@ -90,7 +105,8 @@ const NavBar: React.FC = observer(() => {
icon={faGear}
/>
setSignOutConfirmOpen(true)}
className="flex items-center gap-3 w-[85%] pl-8 pr-4 py-3 rounded-r-full transition-colors hover:bg-grey-500 hover:text-white text-left border-none font-medium"
>
diff --git a/frontend/src/main-page/notifications/GrantNotification.tsx b/frontend/src/main-page/notifications/GrantNotification.tsx
index a17bb2a6..6ff6f18a 100644
--- a/frontend/src/main-page/notifications/GrantNotification.tsx
+++ b/frontend/src/main-page/notifications/GrantNotification.tsx
@@ -4,7 +4,7 @@ interface GrantNotificationProps {
notificationId: string;
message: string;
alertTime: string;
- onDelete: (notificationId: string) => void;
+ onRequestDelete: (notificationId: string) => void;
avatarUrl: string | null;
firstName: string;
lastName: string;
@@ -23,7 +23,7 @@ const GrantNotification: React.FC = ({
notificationId,
message,
alertTime,
- onDelete,
+ onRequestDelete,
avatarUrl,
firstName,
lastName,
@@ -49,7 +49,7 @@ const GrantNotification: React.FC = ({
onDelete(notificationId)}
+ onClick={() => onRequestDelete(notificationId)}
/>
);
diff --git a/frontend/src/main-page/notifications/NotificationPopup.tsx b/frontend/src/main-page/notifications/NotificationPopup.tsx
index 85957248..3e17ec7a 100644
--- a/frontend/src/main-page/notifications/NotificationPopup.tsx
+++ b/frontend/src/main-page/notifications/NotificationPopup.tsx
@@ -1,107 +1,159 @@
-import { createPortal } from 'react-dom';
+import { createPortal } from "react-dom";
+import { useState } from "react";
import GrantNotification from "./GrantNotification";
import { FaTrashAlt } from "react-icons/fa";
import { api } from "../../api";
import { setNotifications as setNotificationsAction } from "../../external/bcanSatchel/actions";
import { Notification } from "../../../../middle-layer/types/Notification";
import { getAppStore } from "../../external/bcanSatchel/store";
-import { observer } from 'mobx-react-lite';
+import { observer } from "mobx-react-lite";
+import ActionConfirmation from "../../components/ActionConfirmation";
+
+type ConfirmState =
+ | { kind: "none" }
+ | { kind: "one"; id: string; message: string }
+ | { kind: "all" };
interface NotificationPopupProps {
- setOpenModal: (open: boolean) => void;
+ setOpenModal: (open: boolean) => void;
}
-const NotificationPopup: React.FC = observer(({
- setOpenModal
-}) => {
+const NotificationPopup: React.FC = observer(
+ ({ setOpenModal }) => {
const store = getAppStore();
const liveNotifications: Notification[] = store.notifications ?? [];
const user = store.user;
+ const [confirm, setConfirm] = useState({ kind: "none" });
const handleDelete = async (notificationId: string) => {
- try {
- const response = await api(
- `/notifications/${notificationId}`,
- {
- method: "DELETE",
- }
- );
+ try {
+ const response = await api(`/notifications/${notificationId}`, {
+ method: "DELETE",
+ });
if (!response.ok) {
- console.error("Failed to delete notification:", response.statusText);
- return;
+ console.error("Failed to delete notification:", response.statusText);
+ return;
}
-
const fetchResponse = await api(
- `/notifications/user/${store.user?.email}/current`,
- {
- method: "GET",
- }
+ `/notifications/user/${store.user?.email}/current`,
+ {
+ method: "GET",
+ },
);
- if (fetchResponse.ok) {
- const updatedNotifications = await fetchResponse.json();
- setNotificationsAction(updatedNotifications);
- }
- }
- catch (error) {
- console.error("Error deleting notification:", error);
+ if (fetchResponse.ok) {
+ const updatedNotifications = await fetchResponse.json();
+ setNotificationsAction(updatedNotifications);
}
+ } catch (error) {
+ console.error("Error deleting notification:", error);
+ }
};
const handleDeleteAll = async () => {
- try {
- await Promise.allSettled(
- liveNotifications.map((n) =>
- api(`/notifications/${n.notificationId}`, { method: "DELETE" })
- )
- );
- setNotificationsAction([]);
- } catch (error) {
- console.error("Error deleting all notifications:", error);
- }
+ try {
+ await Promise.allSettled(
+ liveNotifications.map((n) =>
+ api(`/notifications/${n.notificationId}`, { method: "DELETE" }),
+ ),
+ );
+ setNotificationsAction([]);
+ } catch (error) {
+ console.error("Error deleting all notifications:", error);
+ }
};
+ const confirmOpen = confirm.kind !== "none";
return createPortal(
+ <>
+ setConfirm({ kind: "none" })}
+ onConfirmDelete={() => {
+ if (confirm.kind === "one") {
+ void handleDelete(confirm.id);
+ } else if (confirm.kind === "all") {
+ void handleDeleteAll();
+ }
+ }}
+ title={
+ confirm.kind === "all"
+ ? "Delete all notifications"
+ : "Delete notification"
+ }
+ subtitle="Are you sure you want to delete"
+ boldSubtitle={
+ confirm.kind === "all"
+ ? "all notifications"
+ : confirm.kind === "one"
+ ? confirm.message.length > 56
+ ? `${confirm.message.slice(0, 56)}…`
+ : confirm.message
+ : ""
+ }
+ warningMessage={
+ confirm.kind === "all"
+ ? "Every notification in your list will be permanently removed."
+ : "This notification will be permanently removed."
+ }
+ variant="delete"
+ />
setOpenModal(false)}>
e.stopPropagation()}>
-
Your Notifications
-
-
-
- Delete All
-
-
+
+ Your Notifications
+
+
+ {
+ if (liveNotifications.length === 0) return;
+ setConfirm({ kind: "all" });
+ }}
+ >
+
+ Delete All
+
+
- {liveNotifications && liveNotifications.length > 0 ? (
- liveNotifications.map((n) => (
-
- ))
- ) : (
-
No new notifications
- )}
+ {liveNotifications && liveNotifications.length > 0 ? (
+ liveNotifications.map((n) => (
+
+ setConfirm({
+ kind: "one",
+ id,
+ message: n.message,
+ })
+ }
+ avatarUrl={user?.profilePicUrl ?? null}
+ firstName={user?.firstName ?? ""}
+ lastName={user?.lastName ?? ""}
+ />
+ ))
+ ) : (
+
+ No new notifications
+
+ )}
+
- ,
- document.body
+ >,
+ document.body,
);
-});
+ },
+);
-export default NotificationPopup;
\ No newline at end of file
+export default NotificationPopup;
diff --git a/frontend/src/main-page/settings/ChangePasswordModal.tsx b/frontend/src/main-page/settings/ChangePasswordModal.tsx
index 2e842872..8628354c 100644
--- a/frontend/src/main-page/settings/ChangePasswordModal.tsx
+++ b/frontend/src/main-page/settings/ChangePasswordModal.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useState, useEffect } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import {
@@ -6,6 +6,7 @@ import {
PasswordRequirements,
isPasswordValid,
} from "../../sign-up";
+import ActionConfirmation from "../../components/ActionConfirmation";
export type ChangePasswordFormValues = {
currentPassword: string;
@@ -28,6 +29,11 @@ export default function ChangePasswordModal({
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [reEnterPassword, setReEnterPassword] = useState("");
+ const [showConfirm, setShowConfirm] = useState(false);
+
+ useEffect(() => {
+ if (!isOpen) setShowConfirm(false);
+ }, [isOpen]);
if (!isOpen) return null;
@@ -46,11 +52,16 @@ export default function ChangePasswordModal({
setCurrentPassword("");
setNewPassword("");
setReEnterPassword("");
+ setShowConfirm(false);
onClose();
};
- const handleSave = () => {
+ const requestSave = () => {
if (!canSave) return;
+ setShowConfirm(true);
+ };
+
+ const handleConfirmedSave = () => {
onSubmit?.({
currentPassword: currentPassword.trim(),
newPassword,
@@ -64,6 +75,16 @@ export default function ChangePasswordModal({
aria-modal="true"
aria-labelledby="change-password-title"
>
+
setShowConfirm(false)}
+ onConfirmDelete={handleConfirmedSave}
+ title="Change password"
+ subtitle="Are you sure you want to change"
+ boldSubtitle="your password"
+ warningMessage="You will use your new password the next time you sign in."
+ variant="update"
+ />
diff --git a/frontend/src/main-page/settings/ProfilePictureModal.tsx b/frontend/src/main-page/settings/ProfilePictureModal.tsx
index 7ade673f..a80c8325 100644
--- a/frontend/src/main-page/settings/ProfilePictureModal.tsx
+++ b/frontend/src/main-page/settings/ProfilePictureModal.tsx
@@ -1,4 +1,4 @@
-import { useState, useCallback } from "react";
+import { useState, useCallback, useEffect } from "react";
import Cropper, { Area } from "react-easy-crop";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
@@ -15,6 +15,7 @@ import { getAppStore } from "../../external/bcanSatchel/store";
import { updateUserProfile } from "../../external/bcanSatchel/actions";
import { setActiveUsers } from "../../external/bcanSatchel/actions";
import { User } from "../../../../middle-layer/types/User";
+import ActionConfirmation from "../../components/ActionConfirmation";
type ProfilePictureModalProps = {
isOpen: boolean;
@@ -36,9 +37,14 @@ export default function ProfilePictureModal({
const [uploadError, setUploadError] = useState(null);
const [isUploading, setIsUploading] = useState(false);
const [validationError, setValidationError] = useState(null);
+ const [showUploadConfirm, setShowUploadConfirm] = useState(false);
const user = getAppStore().user;
+ useEffect(() => {
+ if (!isOpen) setShowUploadConfirm(false);
+ }, [isOpen]);
+
const onCropComplete = useCallback((_croppedArea: Area, croppedAreaPixels: Area) => {
setCroppedAreaPixels(croppedAreaPixels);
}, []);
@@ -51,6 +57,7 @@ export default function ProfilePictureModal({
setUploadError(null);
setValidationError(null);
setIsUploading(false);
+ setShowUploadConfirm(false);
onClose();
};
@@ -82,7 +89,7 @@ export default function ProfilePictureModal({
reader.readAsDataURL(file);
};
- const handleSave = async () => {
+ const performUpload = async () => {
if (!imageSrc || !croppedAreaPixels || !user) return;
setIsUploading(true);
@@ -152,6 +159,18 @@ export default function ProfilePictureModal({
aria-modal="true"
aria-labelledby="profile-picture-title"
>
+ setShowUploadConfirm(false)}
+ onConfirmDelete={() => {
+ void performUpload();
+ }}
+ title="Update profile picture"
+ subtitle="Are you sure you want to upload"
+ boldSubtitle="this profile picture"
+ warningMessage="This will replace your current profile picture for your account."
+ variant="update"
+ />
setShowUploadConfirm(true)}
disabled={isUploading || !croppedAreaPixels}
className="bg-primary-900 text-white"
/>
diff --git a/frontend/src/main-page/settings/Settings.tsx b/frontend/src/main-page/settings/Settings.tsx
index dd653299..0240540c 100644
--- a/frontend/src/main-page/settings/Settings.tsx
+++ b/frontend/src/main-page/settings/Settings.tsx
@@ -13,6 +13,7 @@ import ChangePasswordModal, { ChangePasswordFormValues } from "./ChangePasswordM
import { getAppStore } from "../../external/bcanSatchel/store";
import { setActiveUsers, updateUserProfile } from "../../external/bcanSatchel/actions";
import { User } from "../../../../middle-layer/types/User";
+import ActionConfirmation from "../../components/ActionConfirmation";
import { InputField } from "../../sign-up";
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -31,7 +32,11 @@ function Settings() {
const [isChangePasswordModalOpen, setIsChangePasswordModalOpen] = useState(false);
const [changePasswordError, setChangePasswordError] = useState(null);
const [isProfilePictureModalOpen, setIsProfilePictureModalOpen] = useState(false);
+ const [isRemoveProfilePicModalOpen, setIsRemoveProfilePicModalOpen] = useState(false);
+ const [isSaveProfileModalOpen, setIsSaveProfileModalOpen] = useState(false);
const [profilePictureMessage, setProfilePictureMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
+ const isEmailChanged =
+ editForm.email.trim().toLowerCase() !== (store.user?.email ?? "").trim().toLowerCase();
useEffect(() => {
if (user) {
@@ -205,7 +210,7 @@ function Settings() {
/>
handleRemoveProfilePic()}
+ onClick={() => setIsRemoveProfilePicModalOpen(true)}
className="bg-white text-black border-2 border-grey-500"
/>
@@ -223,6 +228,34 @@ function Settings() {
onSuccess={() => setProfilePictureMessage({ type: "success", text: "Profile picture updated." })}
onError={(msg) => setProfilePictureMessage({ type: "error", text: msg })}
/>
+
setIsRemoveProfilePicModalOpen(false)}
+ onConfirmDelete={() => {
+ handleRemoveProfilePic();
+ }}
+ title="Remove Profile Picture"
+ subtitle="Are you sure you want to remove your"
+ boldSubtitle="profile picture"
+ warningMessage="Your profile picture will be removed and replaced with the default avatar."
+ variant="delete"
+ />
+ setIsSaveProfileModalOpen(false)}
+ onConfirmDelete={() => {
+ handleSaveEdit();
+ }}
+ title="Save Profile Changes"
+ subtitle="Are you sure you want to save changes to your"
+ boldSubtitle="profile information"
+ warningMessage={
+ isEmailChanged
+ ? "Changing your email will also change the email you use to log in."
+ : "Your personal information will be updated in the system."
+ }
+ variant="update"
+ />
setIsSaveProfileModalOpen(true)}
className="bg-primary-900 text-white"
/>
diff --git a/frontend/src/main-page/users/components/UserApprove.tsx b/frontend/src/main-page/users/components/UserApprove.tsx
index 4cfc7d63..36f52d33 100644
--- a/frontend/src/main-page/users/components/UserApprove.tsx
+++ b/frontend/src/main-page/users/components/UserApprove.tsx
@@ -3,25 +3,48 @@ import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
import { User } from "../../../../../middle-layer/types/User";
import { approveUser, deleteUser } from "../UserActions";
import { useState } from "react";
+import ActionConfirmation from "../../../components/ActionConfirmation";
interface UserApproveProps {
user: User;
}
const UserApprove = ({ user }: UserApproveProps) => {
const [isLoading, setIsLoading] = useState(false);
+ const [isApproveModalOpen, setIsApproveModalOpen] = useState(false);
+ const [isDenyModalOpen, setIsDenyModalOpen] = useState(false);
return (
+
setIsApproveModalOpen(false)}
+ onConfirmDelete={() => approveUser(user, setIsLoading)}
+ title="Approve User"
+ subtitle="Are you sure you want to approve"
+ boldSubtitle={user.email}
+ warningMessage="Approving this user grants access to the application."
+ variant="create"
+ />
+ setIsDenyModalOpen(false)}
+ onConfirmDelete={() => deleteUser(user, setIsLoading)}
+ title="Deny User"
+ subtitle="Are you sure you want to deny"
+ boldSubtitle={user.email}
+ warningMessage="Denying this user will remove their pending access request."
+ variant="delete"
+ />
approveUser(user, setIsLoading)}
+ onClick={() => setIsApproveModalOpen(true)}
disabled={isLoading}
>
deleteUser(user, setIsLoading)}
+ onClick={() => setIsDenyModalOpen(true)}
disabled={isLoading}
>
diff --git a/frontend/src/main-page/users/components/UserMenu.tsx b/frontend/src/main-page/users/components/UserMenu.tsx
index 210f0773..4fed8c31 100644
--- a/frontend/src/main-page/users/components/UserMenu.tsx
+++ b/frontend/src/main-page/users/components/UserMenu.tsx
@@ -32,6 +32,7 @@ const UserMenu = ({ user }: UserMenuProps) => {
? "lose access to sensitive data."
: "gain access to admin pages."
}`}
+ variant="update"
/>
{
subtitle="Are you sure you want to delete"
boldSubtitle={user.email}
warningMessage="If you delete this user, they will be permanently removed from the system."
+ variant="delete"
/>