Skip to content
Open
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
136 changes: 136 additions & 0 deletions frontend/src/main-page/settings/ChangePasswordModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import {
PasswordField,
PasswordRequirements,
isPasswordValid,
} from "../../sign-up";

export type ChangePasswordFormValues = {
currentPassword: string;
newPassword: string;
};

type ChangePasswordModalProps = {
isOpen: boolean;
onClose: () => void;
onSubmit?: (values: ChangePasswordFormValues) => void;
error?: string | null;
};

export default function ChangePasswordModal({
isOpen,
onClose,
onSubmit,
error = null,
}: ChangePasswordModalProps) {
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [reEnterPassword, setReEnterPassword] = useState("");

if (!isOpen) return null;

const newPasswordValid = isPasswordValid(newPassword);
const passwordsMatch = newPassword !== "" && newPassword === reEnterPassword;
const passwordsDontMatch =
reEnterPassword !== "" && newPassword !== reEnterPassword;
const allFilled =
currentPassword.trim() !== "" &&
newPassword !== "" &&
reEnterPassword !== "";
const canSave =
allFilled && newPasswordValid && passwordsMatch;

const handleClose = () => {
setCurrentPassword("");
setNewPassword("");
setReEnterPassword("");
onClose();
};

const handleSave = () => {
if (!canSave) return;
onSubmit?.({
currentPassword: currentPassword.trim(),
newPassword,
});
handleClose();
};

return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
role="dialog"
aria-modal="true"
aria-labelledby="change-password-title"
>
<div className="w-full max-w-md rounded-xl bg-white p-6 shadow-lg">
<div className="flex items-start justify-between gap-4">
<h2
id="change-password-title"
className="text-2xl font-bold text-black"
>
Change Password
</h2>
<button
type="button"
onClick={handleClose}
className="rounded p-1 text-grey-600 hover:bg-grey-200 hover:text-grey-800"
aria-label="Close"
>
<FontAwesomeIcon icon={faXmark} className="h-6 w-6" />
</button>
</div>

<div className="mt-6 space-y-6">
<PasswordField
id="change-password-current"
label="Current Password"
required
placeholder="Enter your current password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
error={!!error}
/>

<PasswordField
id="change-password-new"
label="New Password"
required
placeholder="Enter your new password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>

<PasswordField
id="change-password-reenter"
label="Re-enter Password"
required
placeholder="Re-enter your password"
value={reEnterPassword}
onChange={(e) => setReEnterPassword(e.target.value)}
error={!!error || passwordsDontMatch}
/>

<PasswordRequirements password={newPassword} />

{(error || passwordsDontMatch) && (
<div className="rounded-2xl bg-[#FFEEEE] px-4 py-3 text-sm font-bold text-[#CC0000]">
{error ?? "Your passwords do not match."}
</div>
)}

<button
type="button"
onClick={handleSave}
disabled={!canSave}
className="w-full rounded-md py-2.5 text-base font-semibold text-white transition disabled:cursor-not-allowed disabled:opacity-50 bg-primary-900 enabled:hover:opacity-90"
>
Save
</button>
</div>
</div>
</div>
);
}
129 changes: 107 additions & 22 deletions frontend/src/main-page/settings/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,56 @@
import { useState } from "react";
import Button from "../../components/Button";
import InfoCard from "./components/InfoCard";
import logo from "../../images/logo.svg";
import { faPenToSquare } from "@fortawesome/free-solid-svg-icons";
import ChangePasswordModal from "./ChangePasswordModal";

const initialPersonalInfo = {
firstName: "John",
lastName: "Doe",
email: "john.doe@gmail.com",
};

export default function Settings() {
const [personalInfo, setPersonalInfo] = useState(initialPersonalInfo);
const [isEditingPersonalInfo, setIsEditingPersonalInfo] = useState(false);
const [editForm, setEditForm] = useState(initialPersonalInfo);
const [isChangePasswordModalOpen, setIsChangePasswordModalOpen] = useState(false);
const [changePasswordError, setChangePasswordError] = useState<string | null>(null);

const handleStartEdit = () => {
setEditForm(personalInfo);
setIsEditingPersonalInfo(true);
};

const handleCancelEdit = () => {
setEditForm(personalInfo);
setIsEditingPersonalInfo(false);
};

const handleSaveEdit = () => {
setPersonalInfo(editForm);
setIsEditingPersonalInfo(false);
};

return (
<div className="max-w-5xl ">
<h1 className="text-3xl lg:text-4xl font-bold mb-8 flex justify-start">Settings</h1>

<div className="mb-12">
<div className="flex items-center gap-6">
{/* Avatar */}
<img
src={logo}
alt="Profile"
className="w-24 h-24 rounded-full object-cover"
/>

{/* Buttons + helper text */}
<div className="flex flex-col gap-2">
<h2 className="text-2xl font-bold mb-1 flex justify-start">Profile Picture</h2>
<div className="flex gap-3">

<Button
text="Upload Image"
onClick={() => alert("add upload functionality")}
//To-do: add a upload logo next to the "Upload Image" button
className="bg-primary-900 text-white"
/>
<Button
Expand All @@ -42,23 +67,70 @@ export default function Settings() {
</div>
</div>

<InfoCard
title="Personal Information"
action={
<Button
text="Edit"
onClick={() => alert("edit personal info")}
className="bg-white text-black border-2 border-grey-500"
logo={faPenToSquare}
logoPosition="right"
/>
}
fields={[
{ label: "First Name", value: "John" },
{ label: "Last Name", value: "Doe" },
{ label: "Email Address", value: "john.doe@gmail.com" },
]}
/>
{isEditingPersonalInfo ? (
<div className="w-full max-w-3xl rounded-lg bg-white p-6 shadow-sm flex flex-col">
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, didn't notice this earlier, but It would be better to move the edit info part into the info card component itself. If you look at the design, the form takes the place of the static fields, so the size of the larger container doesn't change at all

<h2 className="text-xl font-bold mb-4 flex justify-start">Personal Information</h2>
<div className="grid grid-cols-2 gap-6 text-left mb-6">
<div>
<label className="block text-sm text-gray-500 mb-1">First Name</label>
<input
type="text"
value={editForm.firstName}
onChange={(e) => setEditForm((f) => ({ ...f, firstName: e.target.value }))}
className="w-full px-3 py-2 rounded-md border border-gray-300 bg-white text-gray-900"
/>
</div>
<div>
<label className="block text-sm text-gray-500 mb-1">Last Name</label>
<input
type="text"
value={editForm.lastName}
onChange={(e) => setEditForm((f) => ({ ...f, lastName: e.target.value }))}
className="w-full px-3 py-2 rounded-md border border-gray-300 bg-white text-gray-900"
/>
</div>
<div className="col-span-2">
<label className="block text-sm text-gray-500 mb-1">Email Address</label>
<input
type="email"
value={editForm.email}
onChange={(e) => setEditForm((f) => ({ ...f, email: e.target.value }))}
className="w-full px-3 py-2 rounded-md border border-gray-300 bg-white text-gray-900"
/>
</div>
</div>
<div className="flex justify-end gap-3">
<Button
text="Cancel"
onClick={handleCancelEdit}
className="bg-white text-gray-600 border-2 border-grey-500"
/>
<Button
text="Save"
onClick={handleSaveEdit}
className="bg-primary-900 text-white"
/>
</div>
</div>
) : (
<InfoCard
title="Personal Information"
action={
<Button
text="Edit"
onClick={handleStartEdit}
className="bg-white text-black border-2 border-grey-500"
logo={faPenToSquare}
logoPosition="right"
/>
}
fields={[
{ label: "First Name", value: personalInfo.firstName },
{ label: "Last Name", value: personalInfo.lastName },
{ label: "Email Address", value: personalInfo.email },
]}
/>
)}

<div className="flex gap-24 items-center mt-12">
<div>
Expand All @@ -70,10 +142,23 @@ export default function Settings() {

<Button
text="Change Password"
onClick={() => alert("change password")}
onClick={() => {
setChangePasswordError(null);
setIsChangePasswordModalOpen(true);
}}
className="bg-white text-black border-2 border-grey-500"
/>
</div>

<ChangePasswordModal
isOpen={isChangePasswordModalOpen}
onClose={() => setIsChangePasswordModalOpen(false)}
error={changePasswordError}
onSubmit={(values) => {
// Backend: call API with values.currentPassword and values.newPassword
void values;
}}
/>
</div>
);
}
4 changes: 2 additions & 2 deletions frontend/src/sign-up/PasswordField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ export default function PasswordField({
tabIndex={-1}
>
{visible ? (
<EyeSlashIcon className="h-5 w-5" />
) : (
<EyeIcon className="h-5 w-5" />
) : (
<EyeSlashIcon className="h-5 w-5" />
)}
</button>
</div>
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/sign-up/PasswordRequirements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ export const PASSWORD_REQUIREMENTS: PasswordRequirement[] = [
{ id: "lower", label: "1 Lowercase", check: (p) => /[a-z]/.test(p) },
];

/** Returns true if the password meets all requirements (same logic as sign-up). */
export function isPasswordValid(password: string): boolean {
return PASSWORD_REQUIREMENTS.every((r) => r.check(password));
}

type PasswordRequirementsProps = {
password: string;
};
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/sign-up/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export { default as BrandingPanel } from "./BrandingPanel";
export { default as InputField } from "../components/InputField";
export { default as LoginPrompt } from "./LoginPrompt";
export { default as PasswordField } from "./PasswordField";
export { default as PasswordRequirements } from "./PasswordRequirements";
export { default as PasswordRequirements, isPasswordValid } from "./PasswordRequirements";
export { default as SignUpButton } from "./SignUpButton";
export { default as SignUpForm } from "./SignUpForm";
export type { SignUpFormProps, SignUpFormValues } from "./SignUpForm";