- Are you sure you want to unblock{' '}
- {selectedUser?.username}?
-
-
- This will restore their ability to create listings and make
- reservations.
-
-
- >
- );
-};
+ {/* Unblock User Modal */}
+ setUnblockModalVisible(false)}
+ loading={loading}
+ />
+ >
+ );
+}
diff --git a/apps/ui-sharethrift/src/components/shared/user-display-name.ts b/apps/ui-sharethrift/src/components/shared/user-display-name.ts
new file mode 100644
index 000000000..ba2672904
--- /dev/null
+++ b/apps/ui-sharethrift/src/components/shared/user-display-name.ts
@@ -0,0 +1,22 @@
+/**
+ * Formats a user display name from their profile information.
+ * Prioritizes first/last name combination, falls back to username, then to a default.
+ *
+ * @param user - User object with firstName, lastName, and username
+ * @returns Formatted display name
+ */
+export function getUserDisplayName(user: {
+ firstName?: string | null;
+ lastName?: string | null;
+ username?: string | null;
+}): string {
+ const nameParts = [user.firstName, user.lastName].filter(
+ (part) => Boolean(part) && part !== "N/A",
+ );
+
+ if (nameParts.length > 0) {
+ return nameParts.join(" ");
+ }
+
+ return user.username || "Listing User";
+}
diff --git a/apps/ui-sharethrift/src/components/shared/user-modals/block-user-modal.stories.tsx b/apps/ui-sharethrift/src/components/shared/user-modals/block-user-modal.stories.tsx
new file mode 100644
index 000000000..ee47bdf2d
--- /dev/null
+++ b/apps/ui-sharethrift/src/components/shared/user-modals/block-user-modal.stories.tsx
@@ -0,0 +1,268 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { BlockUserModal } from './block-user-modal';
+import type { BlockUserFormValues } from './block-user-modal';
+import { expect, fn, userEvent, within, waitFor } from 'storybook/test';
+
+const meta: Meta = {
+ title: 'Shared/User Modals/BlockUserModal',
+ component: BlockUserModal,
+ argTypes: {
+ onConfirm: { action: 'confirm' },
+ onCancel: { action: 'cancel' },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ visible: true,
+ userName: 'Jane Doe',
+ loading: false,
+ onConfirm: (values: BlockUserFormValues) => {
+ console.log('confirm', values);
+ },
+ onCancel: () => {
+ console.log('cancel');
+ },
+ },
+};
+
+export const Loading: Story = {
+ args: {
+ visible: true,
+ userName: 'Jane Doe',
+ loading: true,
+ onConfirm: () => {},
+ onCancel: () => {},
+ },
+};
+
+export const Hidden: Story = {
+ args: {
+ visible: false,
+ userName: 'Jane Doe',
+ loading: false,
+ onConfirm: () => {},
+ onCancel: () => {},
+ },
+};
+
+export const FillFormAndSubmit: Story = {
+ args: {
+ visible: true,
+ userName: 'Jane Doe',
+ loading: false,
+ onConfirm: fn(),
+ onCancel: fn(),
+ },
+ play: async ({ args }) => {
+ const body = within(document.body);
+
+ // Wait for modal to render
+ await waitFor(() => {
+ expect(body.getByText(/Are you sure you want to block/)).toBeInTheDocument();
+ });
+
+ // Click on the select control (not the text, but the control itself)
+ const selectControl = document.querySelector('.ant-select-selector');
+ if (selectControl) {
+ await userEvent.click(selectControl);
+ }
+
+ // Wait for dropdown and select first option
+ await waitFor(() => {
+ const firstOption = document.querySelector('.ant-select-item');
+ expect(firstOption).toBeTruthy();
+ });
+ const firstOption = document.querySelector('.ant-select-item');
+ if (firstOption) {
+ await userEvent.click(firstOption);
+ }
+
+ // Fill description
+ const descriptionField = body.getByPlaceholderText('This message will be shown to the user');
+ await userEvent.type(descriptionField, 'Test block description');
+
+ // Submit form
+ const blockButton = body.getByRole('button', { name: /Block/i });
+ await userEvent.click(blockButton);
+
+ await waitFor(() => {
+ expect(args.onConfirm).toHaveBeenCalled();
+ });
+ },
+};
+
+export const CancelModal: Story = {
+ args: {
+ visible: true,
+ userName: 'Jane Doe',
+ loading: false,
+ onConfirm: fn(),
+ onCancel: fn(),
+ },
+ play: async ({ args }) => {
+ const body = within(document.body);
+
+ // Wait for modal to render
+ await waitFor(() => {
+ expect(body.getByText(/Are you sure you want to block/)).toBeInTheDocument();
+ });
+
+ const cancelButton = body.getByRole('button', { name: /Cancel/i });
+ await userEvent.click(cancelButton);
+
+ await expect(args.onCancel).toHaveBeenCalled();
+ },
+};
+
+export const SubmitWithoutReason: Story = {
+ args: {
+ visible: true,
+ userName: 'Jane Doe',
+ loading: false,
+ onConfirm: fn(),
+ onCancel: fn(),
+ },
+ play: async ({ args }) => {
+ const body = within(document.body);
+
+ // Wait for modal to render
+ await waitFor(() => {
+ expect(body.getByText(/Are you sure you want to block/)).toBeInTheDocument();
+ });
+
+ // Try to submit without filling form
+ const blockButton = body.getByRole('button', { name: /Block/i });
+ await userEvent.click(blockButton);
+
+ // Should show validation error
+ await waitFor(() => {
+ const errorMessage = body.queryByText('Please select a reason');
+ expect(errorMessage).toBeInTheDocument();
+ });
+
+ // onConfirm should not be called
+ await expect(args.onConfirm).not.toHaveBeenCalled();
+ },
+};
+
+export const SubmitWithoutDescription: Story = {
+ args: {
+ visible: true,
+ userName: 'Jane Doe',
+ loading: false,
+ onConfirm: fn(),
+ onCancel: fn(),
+ },
+ play: async ({ args }) => {
+ const body = within(document.body);
+
+ // Wait for modal to render
+ await waitFor(() => {
+ expect(body.getByText(/Are you sure you want to block/)).toBeInTheDocument();
+ });
+
+ // Click on the select control
+ const selectControl = document.querySelector('.ant-select-selector');
+ if (selectControl) {
+ await userEvent.click(selectControl);
+ }
+
+ await waitFor(() => {
+ expect(document.querySelector('.ant-select-item')).toBeTruthy();
+ });
+
+ const firstOption = document.querySelector('.ant-select-item');
+ if (firstOption) {
+ await userEvent.click(firstOption);
+ }
+
+ // Try to submit without description
+ const blockButton = body.getByRole('button', { name: /Block/i });
+ await userEvent.click(blockButton);
+
+ // Should show validation error
+ await waitFor(() => {
+ const errorMessage = body.queryByText('Please provide a description');
+ expect(errorMessage).toBeInTheDocument();
+ });
+
+ // onConfirm should not be called
+ await expect(args.onConfirm).not.toHaveBeenCalled();
+ },
+};
+
+export const SelectAllReasons: Story = {
+ args: {
+ visible: true,
+ userName: 'Jane Doe',
+ loading: false,
+ onConfirm: fn(),
+ onCancel: fn(),
+ },
+ play: async () => {
+ const body = within(document.body);
+
+ // Wait for modal to render
+ await waitFor(() => {
+ expect(body.getByText(/Are you sure you want to block/)).toBeInTheDocument();
+ });
+
+ // Click on the select control
+ const selectControl = document.querySelector('.ant-select-selector');
+ if (selectControl) {
+ await userEvent.click(selectControl);
+ }
+
+ // Verify all options are present
+ await waitFor(() => {
+ expect(document.querySelector('.ant-select-item')).toBeInTheDocument();
+ });
+ },
+};
+
+export const ErrorDuringSubmit: Story = {
+ args: {
+ visible: true,
+ userName: 'Jane Doe',
+ loading: false,
+ onConfirm: async () => {
+ throw new Error('Test error');
+ },
+ onCancel: fn(),
+ },
+ play: async () => {
+ const body = within(document.body);
+
+ // Wait for modal to render
+ await waitFor(() => {
+ expect(body.getByText(/Are you sure you want to block/)).toBeInTheDocument();
+ });
+
+ // Click on the select control
+ const selectControl = document.querySelector('.ant-select-selector');
+ if (selectControl) {
+ await userEvent.click(selectControl);
+ }
+
+ await waitFor(() => {
+ expect(document.querySelector('.ant-select-item')).toBeTruthy();
+ });
+
+ const firstOption = document.querySelector('.ant-select-item');
+ if (firstOption) {
+ await userEvent.click(firstOption);
+ }
+
+ const descriptionField = body.getByPlaceholderText('This message will be shown to the user');
+ await userEvent.type(descriptionField, 'Test description');
+
+ // Submit form - should handle error gracefully
+ const blockButton = body.getByRole('button', { name: /Block/i });
+ await userEvent.click(blockButton);
+ },
+};
\ No newline at end of file
diff --git a/apps/ui-sharethrift/src/components/shared/user-modals/block-user-modal.tsx b/apps/ui-sharethrift/src/components/shared/user-modals/block-user-modal.tsx
new file mode 100644
index 000000000..18fb813d8
--- /dev/null
+++ b/apps/ui-sharethrift/src/components/shared/user-modals/block-user-modal.tsx
@@ -0,0 +1,109 @@
+import { Modal, Typography, Input, Form, Select } from 'antd';
+import { ExclamationCircleOutlined } from '@ant-design/icons';
+
+const { Text, Paragraph } = Typography;
+const { TextArea } = Input;
+
+export interface BlockUserFormValues {
+ reason: string;
+ description: string;
+}
+
+interface BlockUserModalProps {
+ visible: boolean;
+ userName: string;
+ onConfirm: (values: BlockUserFormValues) => void | Promise;
+ onCancel: () => void;
+ loading?: boolean;
+}
+
+const BLOCK_REASONS = [
+ "Late Return",
+ "Item Damage",
+ "Policy Violation",
+ "Inappropriate Behavior",
+ "Other",
+];
+
+export const BlockUserModal: React.FC> = ({
+ visible,
+ userName,
+ onConfirm,
+ onCancel,
+ loading = false,
+}) => {
+ const [blockForm] = Form.useForm();
+
+ const handleOk = async () => {
+ try {
+ const values = await blockForm.validateFields();
+ await Promise.resolve(onConfirm(values));
+ blockForm.resetFields();
+ } catch (error) {
+ console.error("Failed to block user:", error);
+ return;
+ }
+ };
+
+ const handleCancel = () => {
+ blockForm.resetFields();
+ onCancel();
+ };
+
+ return (
+
+
+ Block User
+
+ }
+ open={visible}
+ onOk={handleOk}
+ onCancel={handleCancel}
+ okText="Block"
+ okButtonProps={{ danger: true, loading }}
+ cancelButtonProps={{ disabled: loading }}
+ width={500}
+ maskClosable={!loading}
+ closable={!loading}
+ >
+
+
+ Are you sure you want to block {userName}?
+
+
+
+ Blocking this user will prevent them from accessing the platform and
+ interacting with other users.
+
+
+
+
+
+
+
+
+
+
+
+ Are you sure you want to unblock {userName}?
+
+
+ Unblocking this user will restore their access to the platform and allow
+ them to interact with other users again.
+
+ {blockReason && (
+