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
152 changes: 152 additions & 0 deletions frontend/src/main-page/settings/ChangePasswordModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { useState } from "react";
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;
};

function CloseIcon({ className }: { className?: string }) {
return (
<svg
Copy link
Contributor

Choose a reason for hiding this comment

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

I would recommend using react font awesome icons instead of this svg. There are examples in our code already like in DateFilter.tsx with faXmark

className={className}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
);
}

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 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"
>
<CloseIcon 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)}
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you should add the passwords match as an error for this so that it's clear the form can't be submitted when the password don't match. It would be like what I did here: signup/login streamlining pr commit

/>

<PasswordRequirements password={newPassword} />

{error && (
<div className="rounded-md bg-red-lightest p-3 text-sm text-red-dark">
{error}
</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 ? (
Copy link
Contributor

Choose a reason for hiding this comment

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

For me, the edit info doesn't look right compared to the design. Maybe you're just missing the bg color? Here's what I see:

Image

<div className="w-full max-w-3xl rounded-lg bg-gray-50 p-6 shadow-sm flex flex-col">
<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";