Skip to content

Commit

Permalink
520 frontend implement profile editing (#526)
Browse files Browse the repository at this point in the history
* working edit popups

* fix undefined errors and lazy load

* remove unused file

* make reusable component for handling the modal submission logic

* refactor panel opening logic

* fix builds

* fix phone bug

* move all into submit handler instead
  • Loading branch information
choden-dev authored Jun 27, 2024
1 parent f5ae030 commit ffe5305
Show file tree
Hide file tree
Showing 13 changed files with 384 additions and 92 deletions.
6 changes: 2 additions & 4 deletions client/src/components/composite/Booking/BookingContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,7 @@ export const BookingContextProvider = ({

const [allergies, setAllergies] = useState<string>("")

const { mutateAsync: updateAllergies } = useEditSelfMutation({
dietary_requirements: allergies
})
const { mutateAsync: updateAllergies } = useEditSelfMutation()

const getExistingSession = async () => {
if (bookingPaymentData?.stripeClientSecret) navigate("/bookings/payment")
Expand All @@ -87,7 +85,7 @@ export const BookingContextProvider = ({
startDate: Timestamp,
endDate: Timestamp
) => {
await updateAllergies()
await updateAllergies({ dietary_requirements: allergies })
await mutateAsync(
{ startDate, endDate },
{
Expand Down

This file was deleted.

134 changes: 116 additions & 18 deletions client/src/components/composite/Profile/ProfileEdit/ProfileEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,47 @@ import { ReducedUserAdditionalInfo } from "models/User"
import TextInput from "components/generic/TextInputComponent/TextInput"
import Button from "components/generic/FigmaButtons/FigmaButton"
import CloseButton from "assets/icons/x.svg?react"
import { Timestamp } from "firebase/firestore"
import { DateUtils, UnknownTimestamp } from "components/utils/DateUtils"
import { useState } from "react"

interface IProfileEdit<T extends Partial<ReducedUserAdditionalInfo>> {
/**
* The text to be displayed as the heading
*/
title: string
/**
* Callback for when the X button is clicked
*/
onClose: () => void
fields: {
/**
* the **key** of the value in `ReducedUserAdditionalInfo` to display as a field
*/
fieldName: keyof T
defaultFieldValue: string
/**
* The value to display in the field with no edits made
*/
defaultFieldValue?: ReducedUserAdditionalInfo[keyof ReducedUserAdditionalInfo]
}[]
/**
* Callback that provides the fields that were changed in the edit form
*
* @param fields an object of all the changed fields
*/
onEdit: (fields: Partial<T>) => void
/**
* If there is an ongoing operation (i.e calling the edit endpoint)
*/
isPending?: boolean
}

/**
* Gets a semantic name for the keys in user data
*
* @param originalName the key from `ReducedUserAdditionalInfo` to transform
* @returns a semantic name for the original key
*/
const nameTransformer = (
originalName: keyof ReducedUserAdditionalInfo
): string => {
Expand Down Expand Up @@ -50,36 +80,104 @@ const nameTransformer = (
}
}

/**
* Panel for when profile needs to be edited.
*
* The fields displayed should be determened by the generic typing
*
* @example
<ProfileEdit<{
first_name: string
last_name: string
date_of_birth: { seconds: number; nanoseconds: number }
faculty: string
phone_number: number
emergency_contact: string
student_id: string
}>
title="example"
/>
*/
const ProfileEdit = <T extends Partial<ReducedUserAdditionalInfo>>({
title,
fields,
onClose
onClose,
onEdit,
isPending
}: IProfileEdit<T>) => {
const [currentFormState, setCurrentFormState] =
useState<Partial<ReducedUserAdditionalInfo>>()
return (
<div className="flex w-[480px] flex-col items-center justify-center ">
<div className="border-gray-3 mt-4 flex w-full flex-col gap-4 rounded-md border p-4">
<div className="z-50 flex max-w-[480px] flex-col items-center justify-center">
<div className="border-gray-3 mt-4 flex w-full flex-col gap-4 rounded-md border bg-white p-4 ">
<div className="flex w-full">
<h3 className="text-dark-blue-100">{title}</h3>{" "}
<CloseButton
onClick={onClose}
className="hover:fill-light-blue-100 ml-auto w-[15px] cursor-pointer"
/>
</div>
<form
onSubmit={(e) => {
e.preventDefault()
onEdit(currentFormState as T)
setCurrentFormState(undefined)
}}
>
{fields.map((field) => {
const defaultValue = field.defaultFieldValue
const isDate = field.fieldName === "date_of_birth"
const isTel = field.fieldName === "phone_number"
const isBool =
field.fieldName === "does_snowboarding" ||
field.fieldName === "does_ski"
return (
<TextInput
key={field.fieldName.toString()}
label={nameTransformer(
field.fieldName as keyof ReducedUserAdditionalInfo
)}
type={
isDate ? "date" : isTel ? "tel" : isBool ? "checkbox" : "text"
}
onChange={(e) =>
setCurrentFormState({
...currentFormState,
[field.fieldName]: isDate
? // Need to store as timestamp, not date which the input provides
Timestamp.fromDate(
DateUtils.convertLocalDateToUTCDate(
e.target.valueAsDate || new Date()
)
)
: // Does skiing/snowboarding
isBool
? e.target.checked
: // Phone number
e.target.value
})
}
defaultValue={
isDate && defaultValue
? DateUtils.formatDateForInput(
new Date(
DateUtils.timestampMilliseconds(
defaultValue as UnknownTimestamp
)
)
)
: (defaultValue as string)
}
/>
)
})}

{fields.map((field) => {
return (
<TextInput
key={field.fieldName.toString()}
label={nameTransformer(
field.fieldName as keyof ReducedUserAdditionalInfo
)}
/>
)
})}

<div className=" mt-2 w-[200px]">
<Button onClick={() => {}}>Update details</Button>
</div>
<div className=" mt-2 w-[200px]">
<Button type="submit" disabled={isPending || !currentFormState}>
Update details
</Button>
</div>
</form>
</div>
</div>
)
Expand Down
10 changes: 9 additions & 1 deletion client/src/components/utils/DateUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,5 +129,13 @@ export const DateUtils = {
* @param date a date object
* @returns a date string in the nz format `dd-mm-yyyy`
*/
formattedNzDate: (date: Date): string => date.toLocaleDateString("en-NZ")
formattedNzDate: (date: Date): string => date.toLocaleDateString("en-NZ"),

/**
* @param date the date to put into format for input
* @returns the date string for date input to parse
*/
formatDateForInput: (date?: Date) => {
return date?.toLocaleDateString("en-CA", { timeZone: "Pacific/Auckland" })
}
} as const
14 changes: 2 additions & 12 deletions client/src/firebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { initializeApp, type FirebaseOptions } from "firebase/app"
import { getFirestore, connectFirestoreEmulator } from "firebase/firestore"
import { ParsedToken, getAuth } from "firebase/auth"
import { UserClaims } from "models/User"
import fetchClient, { setToken } from "services/OpenApiFetchClient"
import { setToken } from "services/OpenApiFetchClient"
import { StoreInstance } from "store/Store"
import { MembershipPaymentStore } from "store/MembershipPayment"
import queryClient from "services/QueryClient"
Expand Down Expand Up @@ -52,17 +52,7 @@ auth.onIdTokenChanged(async (user) => {
// update fetch client token to use
setToken(token)

// retrieve and update cached user data
let userData
try {
const { data } = await fetchClient.GET("/users/self")
userData = data
} catch (error) {
console.error(
`Failed to fetch user data during auth token change: ${error}`
)
}
StoreInstance.actions.setCurrentUser(user, userData, claims as UserClaims)
StoreInstance.actions.setCurrentUser(user, claims as UserClaims)
})

export { auth, db }
36 changes: 36 additions & 0 deletions client/src/pages/Profile/EditAdditionalPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ReducedUserAdditionalInfo } from "models/User"
import { useSelfDataQuery } from "services/User/UserQueries"
import WrappedProfileEdit, { IGeneralProfileEdit } from "./WrappedProfileEdit"

type EditAdditionalFields = Pick<
Partial<ReducedUserAdditionalInfo>,
"does_snowboarding" | "does_ski" | "dietary_requirements"
>

/**
* Allows the user to edit the miscellaneous information.
*/
const EditAdditionalPanel = ({ isOpen, handleClose }: IGeneralProfileEdit) => {
const { data: currentUserData } = useSelfDataQuery()
return (
<WrappedProfileEdit<EditAdditionalFields>
isOpen={isOpen}
handleClose={handleClose}
fields={[
{
fieldName: "dietary_requirements",
defaultFieldValue: currentUserData?.dietary_requirements
},
{
fieldName: "does_ski",
defaultFieldValue: currentUserData?.does_ski
},
{
fieldName: "does_snowboarding",
defaultFieldValue: currentUserData?.does_snowboarding
}
]}
></WrappedProfileEdit>
)
}
export default EditAdditionalPanel
61 changes: 61 additions & 0 deletions client/src/pages/Profile/EditPersonalPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ReducedUserAdditionalInfo } from "models/User"
import { useSelfDataQuery } from "services/User/UserQueries"
import WrappedProfileEdit, { IGeneralProfileEdit } from "./WrappedProfileEdit"

type EditPersonalFields = Pick<
Partial<ReducedUserAdditionalInfo>,
| "first_name"
| "last_name"
| "gender"
| "student_id"
| "date_of_birth"
| "phone_number"
| "emergency_contact"
>

/**
* Displays the fields required to edit the personal section
*
* TODO: extend to include editing auth details (email etc)
*/
const EditPersonalPanel = ({ isOpen, handleClose }: IGeneralProfileEdit) => {
const { data: currentUserData } = useSelfDataQuery()
return (
<WrappedProfileEdit<EditPersonalFields>
isOpen={isOpen}
handleClose={handleClose}
fields={[
{
fieldName: "first_name",
defaultFieldValue: currentUserData?.first_name
},
{
fieldName: "last_name",
defaultFieldValue: currentUserData?.last_name
},

{
fieldName: "gender",
defaultFieldValue: currentUserData?.gender
},
{
fieldName: "date_of_birth",
defaultFieldValue: currentUserData?.date_of_birth
},
{
fieldName: "phone_number",
defaultFieldValue: currentUserData?.phone_number
},
{
fieldName: "emergency_contact",
defaultFieldValue: currentUserData?.emergency_contact
},
{
fieldName: "student_id",
defaultFieldValue: currentUserData?.student_id
}
]}
></WrappedProfileEdit>
)
}
export default EditPersonalPanel
Loading

0 comments on commit ffe5305

Please sign in to comment.