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
4 changes: 2 additions & 2 deletions frontend/react/package-lock.json

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

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions frontend/react/src/assets/images/icons/icons-sprite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.copy-button {
position: relative;
width: 24px;
height: 24px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import IconButton from "../icon-button/IconButton";
import type { DeleteButtonProps } from "./types";
import "./DeleteButton.scss";

const DeleteButton = ({ onClick }: DeleteButtonProps) => {
const handleClick = () => {
onClick?.();
};

return (
<div className="copy-button">
<IconButton iconName="trash" color="green" onClick={handleClick} />
</div>
);
};

export default DeleteButton;
12 changes: 12 additions & 0 deletions frontend/react/src/components/common/delete-button/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const BUTTON_COLORS = {
GREEN: "green",
WHITE: "white",
} as const;

export type ButtonColor = (typeof BUTTON_COLORS)[keyof typeof BUTTON_COLORS];

export interface DeleteButtonProps {
onClick?: () => void;
successMessage?: string;
errorMessage?: string;
}
9 changes: 8 additions & 1 deletion frontend/react/src/components/common/icon-button/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import type { ButtonHTMLAttributes } from "react";

export type IconName = "copy" | "edit" | "save" | "info" | "link" | "cross";
export type IconName =
| "copy"
| "edit"
| "save"
| "info"
| "link"
| "cross"
| "trash";
export type IconColor = "green" | "white";

export type IconButtonProps = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Modal from "../modal/Modal";
import type { DeleteUserModalProps } from "./types";

const DeleteUserModal = ({
isOpen = false,
onClose,
onConfirm,
participant,
isLoading = false,
}: DeleteUserModalProps) => {
const handleConfirm = () => {
onConfirm(participant);
};

return (
<Modal
isOpen={isOpen}
onClose={onClose}
onConfirm={handleConfirm}
title="Delete confirmation"
description=""
iconName="trash"
withConfirm
isLoading={isLoading}
>
<div className="">
<p className="">
Are you sure you want to delete
<b> {participant.firstName + " " + participant.lastName}</b> ?
</p>
</div>
</Modal>
);
};

export default DeleteUserModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Participant } from "@types/api";

export interface DeleteUserModalProps {
isOpen?: boolean;
onClose: () => void;
onConfirm: (participant: Participant) => void;
participant: Participant;
isLoading?: boolean;
}
11 changes: 9 additions & 2 deletions frontend/react/src/components/common/modals/modal/Modal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
padding-left: 86px;
text-align: left;
background: left center / 74px 74px no-repeat;
min-height: 74px;

&--presents {
background-image: url("../../../../assets/images/backgrounds/modal-background-sprite.svg#presents");
Expand All @@ -33,6 +34,10 @@
&--car {
background-image: url("../../../../assets/images/backgrounds/modal-background-sprite.svg#car");
}

&--trash {
background-image: url("../../../../assets/images/backgrounds/modal-background-sprite.svg#trash");
}
}

&__title {
Expand All @@ -57,8 +62,10 @@
right: 44px;
}

&__back-button {
&__action-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 48px;
text-align: right;
}
}
28 changes: 25 additions & 3 deletions frontend/react/src/components/common/modals/modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const Modal = ({
onClose,
onConfirm,
children,
withConfirm = false,
isLoading = false,
}: ModalProps) => {
useEffect(() => {
document.body.style.overflow = isOpen ? "hidden" : "";
Expand All @@ -38,15 +40,35 @@ const Modal = ({
</div>

<div className="modal__close-button">
<IconButton iconName="cross" onClick={onClose} />
<IconButton
iconName="cross"
onClick={isLoading ? () => {} : onClose}
disabled={isLoading}
/>
</div>

{children}

<div className="modal__back-button">
<Button size="medium" width={225} onClick={onConfirm}>
<div className="modal__action-buttons">
<Button
variant={withConfirm ? "secondary" : "primary"}
size="medium"
width={225}
onClick={isLoading ? undefined : onClose}
disabled={isLoading}
>
Go Back to Room
</Button>
{withConfirm && (
<Button
size="medium"
width={225}
onClick={isLoading ? undefined : onConfirm}
disabled={isLoading}
>
Confirm
</Button>
)}
</div>
</div>
</div>
Expand Down
3 changes: 3 additions & 0 deletions frontend/react/src/components/common/modals/modal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const ICON_NAMES = {
COOKIE: "cookie",
NOTE: "note",
CAR: "car",
TRASH: "trash",
} as const;

type IconName = (typeof ICON_NAMES)[keyof typeof ICON_NAMES];
Expand All @@ -18,4 +19,6 @@ export interface ModalProps {
onClose: () => void;
onConfirm: () => void;
children: ReactNode;
withConfirm?: boolean;
isLoading?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import InfoButton from "../info-button/InfoButton";
import ItemCard from "../item-card/ItemCard";
import { type ParticipantCardProps } from "./types";
import "./ParticipantCard.scss";
import DeleteButton from "../delete-button/DeleteButton";

const ParticipantCard = ({
firstName,
Expand All @@ -13,6 +14,7 @@ const ParticipantCard = ({
adminInfo = "",
participantLink = "",
onInfoButtonClick,
onDeleteUserButtonClick,
}: ParticipantCardProps) => {
return (
<ItemCard title={`${firstName} ${lastName}`} isFocusable>
Expand All @@ -39,6 +41,10 @@ const ParticipantCard = ({
{!isCurrentUser && isAdmin ? (
<InfoButton infoMessage={adminInfo} />
) : null}

{isCurrentUserAdmin && !isAdmin ? (
<DeleteButton onClick={onDeleteUserButtonClick} />
) : null}
</div>
</ItemCard>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export interface ParticipantCardProps {
adminInfo?: string;
participantLink?: string;
onInfoButtonClick?: () => void;
onDeleteUserButtonClick?: () => void;
}
1 change: 1 addition & 0 deletions frontend/react/src/components/room-page/RoomPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const RoomPage = () => {
participants={participants ?? []}
roomDetails={roomDetails ?? ({} as GetRoomResponse)}
onDrawNames={() => fetchRandomize()}
onRefetchParticipants={fetchParticipants}
/>
</main>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,24 @@ import ParticipantCard from "@components/common/participant-card/ParticipantCard
import ParticipantDetailsModal from "@components/common/modals/participant-details-modal/ParticipantDetailsModal";
import type { Participant } from "@types/api";
import {
BASE_API_URL,
MAX_PARTICIPANTS_NUMBER,
generateParticipantLink,
} from "@utils/general";
import { type ParticipantsListProps, type PersonalInformation } from "./types";
import "./ParticipantsList.scss";
import DeleteUserModal from "@components/common/modals/delete-user-modal/DeleteUserModal";

const ParticipantsList = ({ participants }: ParticipantsListProps) => {
const ParticipantsList = ({
participants,
onParticipantDeleted,
}: ParticipantsListProps) => {
const { userCode } = useParams();
const [selectedParticipant, setSelectedParticipant] =
useState<PersonalInformation | null>(null);
const [selectedParticipantToDelete, setSelectedParticipantToDelete] =
useState<Participant | null>(null);
const [isDeleting, setIsDeleting] = useState(false);

const admin = participants?.find((participant) => participant?.isAdmin);
const restParticipants = participants?.filter(
Expand All @@ -36,6 +44,42 @@ const ParticipantsList = ({ participants }: ParticipantsListProps) => {

const handleModalClose = () => setSelectedParticipant(null);

const handleDeleteUserButtonClick = (participant: Participant) => {
setSelectedParticipantToDelete(participant);
};

const handleDeleteUserCancel = () => {
if (isDeleting) return;
setSelectedParticipantToDelete(null);
};

const handleDeleteUserConfirm = async (participant: Participant) => {
if (!participant || isDeleting) return;

setIsDeleting(true);
try {
const response = await fetch(
`${BASE_API_URL}/api/users/${participant.id}?userCode=${userCode}`,
{
method: "DELETE",
},
);

if (!response.ok) {
throw new Error("Failed to delete participant");
}

onParticipantDeleted?.();

setSelectedParticipantToDelete(null);
} catch (error) {
console.error("Error deleting participant:", error);
alert("Failed to delete participant. Please try again.");
} finally {
setIsDeleting(false);
}
};

return (
<div
className={`participant-list ${isParticipantsMoreThanTen ? "participant-list--shift-bg-image" : ""}`}
Expand Down Expand Up @@ -82,6 +126,7 @@ const ParticipantsList = ({ participants }: ParticipantsListProps) => {
? () => handleInfoButtonClick(user)
: undefined
}
onDeleteUserButtonClick={() => handleDeleteUserButtonClick(user)}
/>
))}
</div>
Expand All @@ -93,6 +138,16 @@ const ParticipantsList = ({ participants }: ParticipantsListProps) => {
personalInfoData={selectedParticipant}
/>
) : null}

{selectedParticipantToDelete ? (
<DeleteUserModal
isOpen={!!selectedParticipantToDelete}
onClose={handleDeleteUserCancel}
onConfirm={handleDeleteUserConfirm}
participant={selectedParticipantToDelete}
isLoading={isDeleting}
/>
) : null}
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { GetParticipantsResponse } from "@types/api.ts";

export interface ParticipantsListProps {
participants: GetParticipantsResponse;
onParticipantDeleted?: () => void;
}

export interface PersonalInformation {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const RoomPageContent = ({
participants,
roomDetails,
onDrawNames,
onRefetchParticipants,
}: RoomPageContentProps) => {
const { userCode } = useParams();
const [isUserDetailsModalOpen, setIsUserDetailsModalOpen] = useState(false);
Expand Down Expand Up @@ -102,7 +103,10 @@ const RoomPageContent = ({
</div>

<div className="room-page-content-row">
<ParticipantsList participants={participants} />
<ParticipantsList
participants={participants}
onParticipantDeleted={onRefetchParticipants}
/>

<div className="room-page-content-column">
{isAdmin || (!isAdmin && isRandomized) ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export interface RoomPageContentProps {
participants: GetParticipantsResponse;
roomDetails: GetRoomResponse;
onDrawNames: () => void;
onRefetchParticipants?: () => void | Promise<void>;
}
Loading