feat: start edit user page
This commit is contained in:
		
							parent
							
								
									a67ecbf51d
								
							
						
					
					
						commit
						77dea0c5ed
					
				
					 5 changed files with 326 additions and 0 deletions
				
			
		
							
								
								
									
										128
									
								
								frontend/components/Editable.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								frontend/components/Editable.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,128 @@ | |||
| import { | ||||
|     EmojiLaughing, | ||||
|     HandThumbsDown, | ||||
|     HandThumbsUp, | ||||
|     Heart, | ||||
|     People, | ||||
|     Trash3, | ||||
| } from "react-bootstrap-icons"; | ||||
| 
 | ||||
| import Card from "./Card"; | ||||
| import TextInput from "./TextInput"; | ||||
| 
 | ||||
| export interface EditField { | ||||
|     id: number; | ||||
|     name: string; | ||||
|     pronouns: Record<string, PronounChoice>; | ||||
| } | ||||
| 
 | ||||
| export enum PronounChoice { | ||||
|     favourite, | ||||
|     okay, | ||||
|     jokingly, | ||||
|     friendsOnly, | ||||
|     avoid, | ||||
| } | ||||
| 
 | ||||
| type EditableCardProps = { | ||||
|     field: EditField; | ||||
|     onChangeName: React.ChangeEventHandler<HTMLInputElement>; | ||||
|     onChangeFavourite( | ||||
|         e: React.MouseEvent<HTMLButtonElement>, | ||||
|         entry: string | ||||
|     ): void; | ||||
|     onChangeOkay(e: React.MouseEvent<HTMLButtonElement>, entry: string): void; | ||||
|     onChangeJokingly(e: React.MouseEvent<HTMLButtonElement>, entry: string): void; | ||||
|     onChangeFriends(e: React.MouseEvent<HTMLButtonElement>, entry: string): void; | ||||
|     onChangeAvoid(e: React.MouseEvent<HTMLButtonElement>, entry: string): void; | ||||
|     onClickDelete: React.MouseEventHandler<HTMLButtonElement>; | ||||
| }; | ||||
| 
 | ||||
| export function EditableCard(props: EditableCardProps) { | ||||
|     const footer = ( | ||||
|         <div className="flex justify-between"> | ||||
|             <TextInput value={props.field.name} onChange={props.onChangeName} /> | ||||
|             <button | ||||
|                 type="button" | ||||
|                 onClick={props.onClickDelete} | ||||
|                 className="bg-red-600 dark:bg-red-700 hover:bg-red-700 hover:dark:bg-red-800 p-2 rounded-md" | ||||
|             > | ||||
|                 <Trash3 aria-hidden className="inline" />{" "} | ||||
|                 <span className="font-bold">Delete</span> | ||||
|             </button> | ||||
|         </div> | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|         <Card title={props.field.name} draggable footer={footer}> | ||||
|             <ul> | ||||
|                 {Object.keys(props.field.pronouns).map((pronoun, index) => { | ||||
|                     const choice = props.field.pronouns[pronoun]; | ||||
|                     return ( | ||||
|                         <li className="flex justify-between my-1" key={index}> | ||||
|                             <div>{pronoun}</div> | ||||
|                             <div className="rounded-md"> | ||||
|                                 <button | ||||
|                                     type="button" | ||||
|                                     onClick={(e) => props.onChangeFavourite(e, pronoun)} | ||||
|                                     className={`${choice == PronounChoice.favourite | ||||
|                                         ? "bg-slate-500" | ||||
|                                         : "bg-slate-600" | ||||
|                                         } hover:bg-slate-400 p-2`}
 | ||||
|                                 > | ||||
|                                     <Heart /> | ||||
|                                 </button> | ||||
|                                 <button | ||||
|                                     type="button" | ||||
|                                     onClick={(e) => props.onChangeOkay(e, pronoun)} | ||||
|                                     className={`${choice == PronounChoice.okay | ||||
|                                         ? "bg-slate-500" | ||||
|                                         : "bg-slate-600" | ||||
|                                         } hover:bg-slate-400 p-2`}
 | ||||
|                                 > | ||||
|                                     <HandThumbsUp /> | ||||
|                                 </button> | ||||
|                                 <button | ||||
|                                     type="button" | ||||
|                                     onClick={(e) => props.onChangeJokingly(e, pronoun)} | ||||
|                                     className={`${choice == PronounChoice.jokingly | ||||
|                                         ? "bg-slate-500" | ||||
|                                         : "bg-slate-600" | ||||
|                                         } hover:bg-slate-400 p-2`}
 | ||||
|                                 > | ||||
|                                     <EmojiLaughing /> | ||||
|                                 </button> | ||||
|                                 <button | ||||
|                                     type="button" | ||||
|                                     onClick={(e) => props.onChangeFriends(e, pronoun)} | ||||
|                                     className={`${choice == PronounChoice.friendsOnly | ||||
|                                         ? "bg-slate-500" | ||||
|                                         : "bg-slate-600" | ||||
|                                         } hover:bg-slate-400 p-2`}
 | ||||
|                                 > | ||||
|                                     <People /> | ||||
|                                 </button> | ||||
|                                 <button | ||||
|                                     type="button" | ||||
|                                     onClick={(e) => props.onChangeAvoid(e, pronoun)} | ||||
|                                     className={`${choice == PronounChoice.avoid | ||||
|                                         ? "bg-slate-500" | ||||
|                                         : "bg-slate-600" | ||||
|                                         } hover:bg-slate-400 p-2`}
 | ||||
|                                 > | ||||
|                                     <HandThumbsDown /> | ||||
|                                 </button> | ||||
|                                 <button | ||||
|                                     type="button" | ||||
|                                     className="bg-red-600 dark:bg-red-700 hover:bg-red-700 hover:dark:bg-red-800 p-2" | ||||
|                                 > | ||||
|                                     <Trash3 /> | ||||
|                                 </button> | ||||
|                             </div> | ||||
|                         </li> | ||||
|                     ); | ||||
|                 })} | ||||
|             </ul> | ||||
|         </Card> | ||||
|     ); | ||||
| } | ||||
							
								
								
									
										19
									
								
								frontend/components/TextInput.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								frontend/components/TextInput.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| import { ChangeEventHandler } from "react"; | ||||
| 
 | ||||
| export type Props = { | ||||
|     defaultValue?: string; | ||||
|     value?: string; | ||||
|     onChange?: ChangeEventHandler<HTMLInputElement>; | ||||
| }; | ||||
| 
 | ||||
| export default function TextInput(props: Props) { | ||||
|     return ( | ||||
|         <input | ||||
|             type="text" | ||||
|             className="p-1 lg:p-2 rounded-md bg-white border-slate-300 text-black dark:bg-slate-800 dark:border-slate-900 dark:text-white" | ||||
|             defaultValue={props.defaultValue} | ||||
|             value={props.value} | ||||
|             onChange={props.onChange} | ||||
|         /> | ||||
|     ); | ||||
| } | ||||
							
								
								
									
										3
									
								
								frontend/pages/edit/member/[member]/index.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/pages/edit/member/[member]/index.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| export default function EditMember() { | ||||
|     return <>Editing a member!</>; | ||||
| } | ||||
							
								
								
									
										12
									
								
								frontend/pages/edit/member/index.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								frontend/pages/edit/member/index.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | |||
| import { useRouter } from "next/router"; | ||||
| import { useEffect } from "react"; | ||||
| import Loading from "../../../components/Loading"; | ||||
| 
 | ||||
| export default function Redirect() { | ||||
|     const router = useRouter(); | ||||
|     useEffect(() => { | ||||
|         router.push("/") | ||||
|     }, []) | ||||
| 
 | ||||
|     return <Loading />; | ||||
| } | ||||
							
								
								
									
										164
									
								
								frontend/pages/edit/profile.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								frontend/pages/edit/profile.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,164 @@ | |||
| import { useRouter } from "next/router"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import { useRecoilState } from "recoil"; | ||||
| import Loading from "../../components/Loading"; | ||||
| import fetchAPI from "../../lib/fetch"; | ||||
| import { userState } from "../../lib/state"; | ||||
| import { MeUser, Field } from "../../lib/types"; | ||||
| import cloneDeep from "lodash/cloneDeep"; | ||||
| import { ReactSortable } from "react-sortablejs"; | ||||
| import Card from "../../components/Card"; | ||||
| 
 | ||||
| import { EditableCard, EditField, PronounChoice } from "../../components/Editable"; | ||||
| 
 | ||||
| export default function Index() { | ||||
|     const [user, setUser] = useRecoilState(userState); | ||||
|     const router = useRouter(); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         if (!user) { | ||||
|             router.push("/"); | ||||
|         } | ||||
|     }, [user]) | ||||
| 
 | ||||
|     if (!user) { | ||||
|         return <Loading />; | ||||
|     } | ||||
| 
 | ||||
|     const [state, setState] = useState(cloneDeep(user)); | ||||
| 
 | ||||
|     const originalOrder = state.fields ? state.fields.map((f, i) => { | ||||
|         const field: EditField = { | ||||
|             id: i, | ||||
|             name: f.name, | ||||
|             pronouns: {}, | ||||
|         }; | ||||
| 
 | ||||
|         f.favourite?.forEach((val) => { | ||||
|             field.pronouns[val] = PronounChoice.favourite; | ||||
|         }); | ||||
|         f.okay?.forEach((val) => { | ||||
|             field.pronouns[val] = PronounChoice.okay; | ||||
|         }); | ||||
|         f.jokingly?.forEach((val) => { | ||||
|             field.pronouns[val] = PronounChoice.jokingly; | ||||
|         }); | ||||
|         f.friends_only?.forEach((val) => { | ||||
|             field.pronouns[val] = PronounChoice.friendsOnly; | ||||
|         }); | ||||
|         f.avoid?.forEach((val) => { | ||||
|             field.pronouns[val] = PronounChoice.avoid; | ||||
|         }); | ||||
| 
 | ||||
|         return field; | ||||
|     }) : []; | ||||
| 
 | ||||
|     const [fields, setFields] = useState(cloneDeep(originalOrder)); | ||||
|     const fieldsUpdated = !fieldsEqual(fields, originalOrder); | ||||
| 
 | ||||
|     return ( | ||||
|         <div className="container mx-auto"> | ||||
|             <div>{`fieldsUpdated: ${fieldsUpdated}`}</div> | ||||
|             {/* @ts-ignore: This component isn't updated to have a "children" prop yet, but it accepts it just fine. */} | ||||
|             <ReactSortable | ||||
|                 handle=".handle" | ||||
|                 list={fields} | ||||
|                 setList={setFields} | ||||
|                 className="grid grid-cols-1 xl:grid-cols-2 gap-4 py-2" | ||||
|             > | ||||
|                 {fields.map((field, i) => ( | ||||
|                     <EditableCard | ||||
|                         key={i} | ||||
|                         field={field} | ||||
|                         onChangeName={(e) => { | ||||
|                             field.name = e.target.value; | ||||
|                             setFields([...fields]); | ||||
|                         }} | ||||
|                         onChangeFavourite={(e, entry: string) => { | ||||
|                             field.pronouns[entry] = PronounChoice.favourite; | ||||
|                             setFields([...fields]); | ||||
|                         }} | ||||
|                         onChangeOkay={(e, entry: string) => { | ||||
|                             field.pronouns[entry] = PronounChoice.okay; | ||||
|                             setFields([...fields]); | ||||
|                         }} | ||||
|                         onChangeJokingly={(e, entry: string) => { | ||||
|                             field.pronouns[entry] = PronounChoice.jokingly; | ||||
|                             setFields([...fields]); | ||||
|                         }} | ||||
|                         onChangeFriends={(e, entry: string) => { | ||||
|                             field.pronouns[entry] = PronounChoice.friendsOnly; | ||||
|                             setFields([...fields]); | ||||
|                         }} | ||||
|                         onChangeAvoid={(e, entry: string) => { | ||||
|                             field.pronouns[entry] = PronounChoice.avoid; | ||||
|                             setFields([...fields]); | ||||
|                         }} | ||||
|                         onClickDelete={(_) => { | ||||
|                             const newFields = [...fields]; | ||||
|                             newFields.splice(i, 1); | ||||
|                             setFields(newFields); | ||||
|                         }} | ||||
|                     /> | ||||
|                 ))} | ||||
|             </ReactSortable> | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| function fieldsEqual(arr1: EditField[], arr2: EditField[]) { | ||||
|     if (arr1?.length !== arr2?.length) return false; | ||||
| 
 | ||||
|     if (!arr1.every((_, i) => arr1[i].id === arr2[i].id)) return false; | ||||
| 
 | ||||
|     return arr1.every((_, i) => | ||||
|         Object.keys(arr1[i].pronouns).every( | ||||
|             (val) => arr1[i].pronouns[val] === arr2[i].pronouns[val] | ||||
|         ) | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| async function updateUser(args: { | ||||
|     displayName: string; | ||||
|     bio: string; | ||||
|     fields: EditField[]; | ||||
| }) { | ||||
|     const newFields = args.fields.map((editField) => { | ||||
|         const field: Field = { | ||||
|             name: editField.name, | ||||
|             favourite: [], | ||||
|             okay: [], | ||||
|             jokingly: [], | ||||
|             friends_only: [], | ||||
|             avoid: [], | ||||
|         }; | ||||
| 
 | ||||
|         Object.keys(editField).forEach((pronoun) => { | ||||
|             switch (editField.pronouns[pronoun]) { | ||||
|                 case PronounChoice.favourite: | ||||
|                     field.favourite?.push(pronoun); | ||||
|                     break; | ||||
|                 case PronounChoice.okay: | ||||
|                     field.okay?.push(pronoun); | ||||
|                     break; | ||||
|                 case PronounChoice.jokingly: | ||||
|                     field.jokingly?.push(pronoun); | ||||
|                     break; | ||||
|                 case PronounChoice.friendsOnly: | ||||
|                     field.friends_only?.push(pronoun); | ||||
|                     break; | ||||
|                 case PronounChoice.avoid: | ||||
|                     field.avoid?.push(pronoun); | ||||
|                     break; | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         return field; | ||||
|     }); | ||||
| 
 | ||||
|     return await fetchAPI<MeUser>("/users/@me", "PATCH", { | ||||
|         display_name: args.displayName, | ||||
|         bio: args.bio, | ||||
|         fields: newFields, | ||||
|     }); | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue