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
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>
);
}
140 changes: 125 additions & 15 deletions frontend/src/main-page/settings/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,67 @@
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 EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

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 [personalInfoError, setPersonalInfoError] = useState<string | null>(null);
const [isChangePasswordModalOpen, setIsChangePasswordModalOpen] = useState(false);
const [changePasswordError, setChangePasswordError] = useState<string | null>(null);

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

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

const handleSaveEdit = () => {
if (!EMAIL_REGEX.test(editForm.email)) {
setPersonalInfoError("Email is not valid.");
return;
}

setPersonalInfo(editForm);
setIsEditingPersonalInfo(false);
setPersonalInfoError(null);
};

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 @@ -45,19 +81,80 @@ export default function Settings() {
<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"
/>
!isEditingPersonalInfo && (
<Button
text="Edit"
onClick={handleStartEdit}
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" },
{ label: "First Name", value: personalInfo.firstName },
{ label: "Last Name", value: personalInfo.lastName },
{ label: "Email Address", value: personalInfo.email },
]}
isEditing={isEditingPersonalInfo}
editContent={
<>
<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 bg-white text-gray-900 ${
personalInfoError ? "border-[#CC0000]" : "border-gray-300"
}`}
/>
</div>
</div>
{personalInfoError && (
<div className="mb-4 rounded-2xl bg-[#FFEEEE] px-4 py-3 text-sm font-bold text-[#CC0000]">
{personalInfoError}
</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 className="flex gap-24 items-center mt-12">
Expand All @@ -70,10 +167,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>
);
}
34 changes: 23 additions & 11 deletions frontend/src/main-page/settings/components/InfoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,17 @@ type InfoCardProps = {
title?: string;
fields: InfoField[];
action?: ReactNode;
isEditing?: boolean;
editContent?: ReactNode;
};

export default function InfoCard({ title, fields, action }: InfoCardProps) {
export default function InfoCard({
title,
fields,
action,
isEditing,
editContent,
}: InfoCardProps) {
return (
<div className="w-full max-w-3xl rounded-lg bg-white p-6 shadow-sm flex flex-col">
{(title || action) && (
Expand All @@ -25,16 +33,20 @@ export default function InfoCard({ title, fields, action }: InfoCardProps) {
</div>
)}

<div className="grid grid-cols-2 gap-6 text-left">
{fields.map((field) => (
<div key={field.label}>
<p className="text-sm text-gray-500">{field.label}</p>
<p className="text-base font-medium text-gray-900">
{field.value}
</p>
</div>
))}
</div>
{isEditing && editContent ? (
editContent
) : (
<div className="grid grid-cols-2 gap-6 text-left">
{fields.map((field) => (
<div key={field.label}>
<p className="text-sm text-gray-500">{field.label}</p>
<p className="text-base font-medium text-gray-900">
{field.value}
</p>
</div>
))}
</div>
)}
</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
Loading