refactor: extract Button to component, reformat all files with Prettier
This commit is contained in:
		
							parent
							
								
									1080d8a0cd
								
							
						
					
					
						commit
						bfdaafeb0a
					
				
					 15 changed files with 504 additions and 335 deletions
				
			
		
							
								
								
									
										1
									
								
								frontend/.prettierignore
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/.prettierignore
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| .next | ||||
							
								
								
									
										65
									
								
								frontend/components/Button.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								frontend/components/Button.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,65 @@ | |||
| import { MouseEventHandler, ReactNode } from "react"; | ||||
| 
 | ||||
| export enum ButtonStyle { | ||||
|   primary, | ||||
|   success, | ||||
|   danger, | ||||
| } | ||||
| 
 | ||||
| export interface Props { | ||||
|   onClick?: MouseEventHandler<HTMLButtonElement>; | ||||
|   style?: ButtonStyle; | ||||
|   bold?: boolean; | ||||
|   children?: ReactNode; | ||||
| } | ||||
| 
 | ||||
| export default function Button(props: Props) { | ||||
|   if (props.style === undefined) { | ||||
|     return PrimaryButton(props); | ||||
|   } | ||||
| 
 | ||||
|   switch (props.style) { | ||||
|     case ButtonStyle.primary: | ||||
|       return PrimaryButton(props); | ||||
|     case ButtonStyle.success: | ||||
|       return SuccessButton(props); | ||||
|     case ButtonStyle.danger: | ||||
|       return DangerButton(props); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function PrimaryButton(props: Props) { | ||||
|   return ( | ||||
|     <button | ||||
|       type="button" | ||||
|       onClick={props.onClick} | ||||
|       className="bg-blue-500 dark:bg-blue-500 hover:bg-blue-700 hover:dark:bg-blue-800 p-2 rounded-md text-white" | ||||
|     > | ||||
|       <span className={props.bold ? "font-bold" : ""}>{props.children}</span> | ||||
|     </button> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| function SuccessButton(props: Props) { | ||||
|   return ( | ||||
|     <button | ||||
|       type="button" | ||||
|       onClick={props.onClick} | ||||
|       className="bg-green-600 dark:bg-green-700 hover:bg-green-700 hover:dark:bg-green-800 p-2 rounded-md text-white" | ||||
|     > | ||||
|       <span className={props.bold ? "font-bold" : ""}>{props.children}</span> | ||||
|     </button> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| function DangerButton(props: Props) { | ||||
|   return ( | ||||
|     <button | ||||
|       type="button" | ||||
|       onClick={props.onClick} | ||||
|       className="bg-red-600 dark:bg-red-700 hover:bg-red-700 hover:dark:bg-red-800 p-2 rounded-md text-white" | ||||
|     > | ||||
|       <span className={props.bold ? "font-bold" : ""}>{props.children}</span> | ||||
|     </button> | ||||
|   ); | ||||
| } | ||||
|  | @ -1,128 +1,129 @@ | |||
| import { | ||||
|     EmojiLaughing, | ||||
|     HandThumbsDown, | ||||
|     HandThumbsUp, | ||||
|     Heart, | ||||
|     People, | ||||
|     Trash3, | ||||
|   EmojiLaughing, | ||||
|   HandThumbsDown, | ||||
|   HandThumbsUp, | ||||
|   Heart, | ||||
|   People, | ||||
|   Trash3, | ||||
| } from "react-bootstrap-icons"; | ||||
| 
 | ||||
| import Card from "./Card"; | ||||
| import TextInput from "./TextInput"; | ||||
| import Button, { ButtonStyle } from "./Button"; | ||||
| 
 | ||||
| export interface EditField { | ||||
|     id: number; | ||||
|     name: string; | ||||
|     pronouns: Record<string, PronounChoice>; | ||||
|   id: number; | ||||
|   name: string; | ||||
|   pronouns: Record<string, PronounChoice>; | ||||
| } | ||||
| 
 | ||||
| export enum PronounChoice { | ||||
|     favourite, | ||||
|     okay, | ||||
|     jokingly, | ||||
|     friendsOnly, | ||||
|     avoid, | ||||
|   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>; | ||||
|   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> | ||||
|     ); | ||||
|   const footer = ( | ||||
|     <div className="flex justify-between"> | ||||
|       <TextInput value={props.field.name} onChange={props.onChangeName} /> | ||||
|       <Button style={ButtonStyle.danger} onClick={props.onClickDelete}> | ||||
|         <Trash3 aria-hidden className="inline" /> Delete | ||||
|       </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> | ||||
|     ); | ||||
|   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> | ||||
|   ); | ||||
| } | ||||
|  |  | |||
|  | @ -1,19 +1,24 @@ | |||
| import { ChangeEventHandler } from "react"; | ||||
| 
 | ||||
| export type Props = { | ||||
|     defaultValue?: string; | ||||
|     value?: string; | ||||
|     onChange?: ChangeEventHandler<HTMLInputElement>; | ||||
|   contrastBackground?: boolean; | ||||
|   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} | ||||
|         /> | ||||
|     ); | ||||
|   const bg = props.contrastBackground | ||||
|     ? "bg-slate-50 dark:bg-slate-700" | ||||
|     : "bg-white dark:bg-slate-800"; | ||||
| 
 | ||||
|   return ( | ||||
|     <input | ||||
|       type="text" | ||||
|       className={`p-1 lg:p-2 rounded-md ${bg} border-slate-300 text-black dark:border-slate-900 dark:text-white`} | ||||
|       defaultValue={props.defaultValue} | ||||
|       value={props.value} | ||||
|       onChange={props.onChange} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  |  | |||
|  | @ -69,13 +69,28 @@ export enum WordStatus { | |||
| export enum ErrorCode { | ||||
|   BadRequest = 400, | ||||
|   Forbidden = 403, | ||||
|   NotFound = 404, | ||||
|   MethodNotAllowed = 405, | ||||
|   TooManyRequests = 429, | ||||
|   InternalServerError = 500, | ||||
| 
 | ||||
|   InvalidState = 1001, | ||||
|   InvalidOAuthCode = 1002, | ||||
|   InvalidToken = 1003, | ||||
|   InviteRequired = 1004, | ||||
|   InvalidTicket = 1005, | ||||
|   InvalidUsername = 1006, | ||||
|   UsernameTaken = 1007, | ||||
|   InvitesDisabled = 1008, | ||||
|   InviteLimitReached = 1009, | ||||
|   InviteAlreadyUsed = 1010, | ||||
| 
 | ||||
|   UserNotFound = 2001, | ||||
| 
 | ||||
|   MemberNotFound = 3001, | ||||
|   MemberLimitReached = 3002, | ||||
| 
 | ||||
|   RequestTooBig = 4001, | ||||
| } | ||||
| 
 | ||||
| export interface SignupRequest { | ||||
|  |  | |||
|  | @ -32,6 +32,7 @@ | |||
|     "eslint": "8.19.0", | ||||
|     "eslint-config-next": "12.2.2", | ||||
|     "postcss": "^8.4.14", | ||||
|     "prettier": "2.7.1", | ||||
|     "tailwindcss": "^3.1.6", | ||||
|     "typescript": "4.7.4" | ||||
|   } | ||||
|  |  | |||
|  | @ -16,10 +16,10 @@ | |||
|  *  - https://reactjs.org/docs/error-boundaries.html
 | ||||
|  */ | ||||
| 
 | ||||
| import * as Sentry from '@sentry/nextjs'; | ||||
| import NextErrorComponent from 'next/error'; | ||||
| import * as Sentry from "@sentry/nextjs"; | ||||
| import NextErrorComponent from "next/error"; | ||||
| 
 | ||||
| const CustomErrorComponent = props => { | ||||
| const CustomErrorComponent = (props) => { | ||||
|   // If you're using a Nextjs version prior to 12.2.1, uncomment this to
 | ||||
|   // compensate for https://github.com/vercel/next.js/issues/8592
 | ||||
|   // Sentry.captureUnderscoreErrorException(props);
 | ||||
|  | @ -27,7 +27,7 @@ const CustomErrorComponent = props => { | |||
|   return <NextErrorComponent statusCode={props.statusCode} />; | ||||
| }; | ||||
| 
 | ||||
| CustomErrorComponent.getInitialProps = async contextData => { | ||||
| CustomErrorComponent.getInitialProps = async (contextData) => { | ||||
|   // In case this is running in a serverless function, await this in order to give Sentry
 | ||||
|   // time to send the error before the lambda exits
 | ||||
|   await Sentry.captureUnderscoreErrorException(contextData); | ||||
|  |  | |||
|  | @ -1,3 +1,3 @@ | |||
| export default function EditMember() { | ||||
|     return <>Editing a member!</>; | ||||
| } | ||||
|   return <>Editing a member!</>; | ||||
| } | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ import { useEffect } from "react"; | |||
| import Loading from "../../../components/Loading"; | ||||
| 
 | ||||
| export default function Redirect() { | ||||
|     const router = useRouter(); | ||||
|     useEffect(() => { | ||||
|         router.push("/") | ||||
|     }, []) | ||||
|   const router = useRouter(); | ||||
|   useEffect(() => { | ||||
|     router.push("/"); | ||||
|   }, []); | ||||
| 
 | ||||
|     return <Loading />; | ||||
| } | ||||
|   return <Loading />; | ||||
| } | ||||
|  |  | |||
|  | @ -9,156 +9,162 @@ import cloneDeep from "lodash/cloneDeep"; | |||
| import { ReactSortable } from "react-sortablejs"; | ||||
| import Card from "../../components/Card"; | ||||
| 
 | ||||
| import { EditableCard, EditField, PronounChoice } from "../../components/Editable"; | ||||
| 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]) | ||||
|   const [user, setUser] = useRecoilState(userState); | ||||
|   const router = useRouter(); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (!user) { | ||||
|         return <Loading />; | ||||
|       router.push("/"); | ||||
|     } | ||||
|   }, [user]); | ||||
| 
 | ||||
|     const [state, setState] = useState(cloneDeep(user)); | ||||
|   if (!user) { | ||||
|     return <Loading />; | ||||
|   } | ||||
| 
 | ||||
|     const originalOrder = state.fields ? state.fields.map((f, i) => { | ||||
|   const [state, setState] = useState(cloneDeep(user)); | ||||
| 
 | ||||
|   const originalOrder = state.fields | ||||
|     ? state.fields.map((f, i) => { | ||||
|         const field: EditField = { | ||||
|             id: i, | ||||
|             name: f.name, | ||||
|             pronouns: {}, | ||||
|           id: i, | ||||
|           name: f.name, | ||||
|           pronouns: {}, | ||||
|         }; | ||||
| 
 | ||||
|         f.favourite?.forEach((val) => { | ||||
|             field.pronouns[val] = PronounChoice.favourite; | ||||
|           field.pronouns[val] = PronounChoice.favourite; | ||||
|         }); | ||||
|         f.okay?.forEach((val) => { | ||||
|             field.pronouns[val] = PronounChoice.okay; | ||||
|           field.pronouns[val] = PronounChoice.okay; | ||||
|         }); | ||||
|         f.jokingly?.forEach((val) => { | ||||
|             field.pronouns[val] = PronounChoice.jokingly; | ||||
|           field.pronouns[val] = PronounChoice.jokingly; | ||||
|         }); | ||||
|         f.friends_only?.forEach((val) => { | ||||
|             field.pronouns[val] = PronounChoice.friendsOnly; | ||||
|           field.pronouns[val] = PronounChoice.friendsOnly; | ||||
|         }); | ||||
|         f.avoid?.forEach((val) => { | ||||
|             field.pronouns[val] = PronounChoice.avoid; | ||||
|           field.pronouns[val] = PronounChoice.avoid; | ||||
|         }); | ||||
| 
 | ||||
|         return field; | ||||
|     }) : []; | ||||
|       }) | ||||
|     : []; | ||||
| 
 | ||||
|     const [fields, setFields] = useState(cloneDeep(originalOrder)); | ||||
|     const fieldsUpdated = !fieldsEqual(fields, originalOrder); | ||||
|   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> | ||||
|     ); | ||||
|   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?.length !== arr2?.length) return false; | ||||
| 
 | ||||
|     if (!arr1.every((_, i) => arr1[i].id === arr2[i].id)) 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] | ||||
|         ) | ||||
|     ); | ||||
|   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[]; | ||||
|   displayName: string; | ||||
|   bio: string; | ||||
|   fields: EditField[]; | ||||
| }) { | ||||
|     const newFields = args.fields.map((editField) => { | ||||
|         const field: Field = { | ||||
|             name: editField.name, | ||||
|             favourite: [], | ||||
|             okay: [], | ||||
|             jokingly: [], | ||||
|             friends_only: [], | ||||
|             avoid: [], | ||||
|         }; | ||||
|   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; | ||||
|     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 await fetchAPI<MeUser>("/users/@me", "PATCH", { | ||||
|         display_name: args.displayName, | ||||
|         bio: args.bio, | ||||
|         fields: newFields, | ||||
|     }); | ||||
|     return field; | ||||
|   }); | ||||
| 
 | ||||
|   return await fetchAPI<MeUser>("/users/@me", "PATCH", { | ||||
|     display_name: args.displayName, | ||||
|     bio: args.bio, | ||||
|     fields: newFields, | ||||
|   }); | ||||
| } | ||||
|  |  | |||
|  | @ -5,6 +5,9 @@ import fetchAPI from "../../lib/fetch"; | |||
| import { userState } from "../../lib/state"; | ||||
| import { APIError, MeUser, SignupResponse } from "../../lib/types"; | ||||
| import TextInput from "../../components/TextInput"; | ||||
| import Loading from "../../components/Loading"; | ||||
| import { stat } from "fs"; | ||||
| import Button, { ButtonStyle } from "../../components/Button"; | ||||
| 
 | ||||
| interface CallbackResponse { | ||||
|   has_account: boolean; | ||||
|  | @ -41,41 +44,47 @@ export default function Discord() { | |||
|     error: null, | ||||
|     requireInvite: false, | ||||
|   }); | ||||
|   const [formData, setFormData] = useState<{ username: string, invite: string }>({ username: "", invite: "" }); | ||||
|   const [formData, setFormData] = useState<{ | ||||
|     username: string; | ||||
|     invite: string; | ||||
|   }>({ username: "", invite: "" }); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (!router.query.code || !router.query.state) { return; } | ||||
|     if (!router.query.code || !router.query.state) { | ||||
|       return; | ||||
|     } | ||||
|     if (state.ticket || state.token) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     fetchAPI<CallbackResponse>( | ||||
|       "/auth/discord/callback", | ||||
|       "POST", | ||||
|       { | ||||
|         callback_domain: window.location.origin, | ||||
|         code: router.query.code, | ||||
|         state: router.query.state, | ||||
|       } | ||||
|     ).then(resp => { | ||||
|       setState({ | ||||
|         hasAccount: resp.has_account, | ||||
|         isLoading: false, | ||||
|         token: resp.token || null, | ||||
|         user: resp.user || null, | ||||
|         discord: resp.discord || null, | ||||
|         ticket: resp.ticket || null, | ||||
|         requireInvite: resp.require_invite, | ||||
|       }) | ||||
|     }).catch(e => { | ||||
|       setState({ | ||||
|         hasAccount: false, | ||||
|         isLoading: false, | ||||
|         error: e, | ||||
|         token: null, | ||||
|         user: null, | ||||
|         discord: null, | ||||
|         ticket: null, | ||||
|         requireInvite: false, | ||||
|       }); | ||||
|     fetchAPI<CallbackResponse>("/auth/discord/callback", "POST", { | ||||
|       callback_domain: window.location.origin, | ||||
|       code: router.query.code, | ||||
|       state: router.query.state, | ||||
|     }) | ||||
|       .then((resp) => { | ||||
|         setState({ | ||||
|           hasAccount: resp.has_account, | ||||
|           isLoading: false, | ||||
|           token: resp.token || null, | ||||
|           user: resp.user || null, | ||||
|           discord: resp.discord || null, | ||||
|           ticket: resp.ticket || null, | ||||
|           requireInvite: resp.require_invite, | ||||
|         }); | ||||
|       }) | ||||
|       .catch((e) => { | ||||
|         setState({ | ||||
|           hasAccount: false, | ||||
|           isLoading: false, | ||||
|           error: e, | ||||
|           token: null, | ||||
|           user: null, | ||||
|           discord: null, | ||||
|           ticket: null, | ||||
|           requireInvite: false, | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|     // we got a token + user, save it and return to the home page
 | ||||
|     if (state.token) { | ||||
|  | @ -86,14 +95,29 @@ export default function Discord() { | |||
|     } | ||||
|   }, [state.token, state.user, setState, router]); | ||||
| 
 | ||||
|   if (!state.ticket && !state.error) { | ||||
|     return <Loading />; | ||||
|   } else if (state.error) { | ||||
|     return ( | ||||
|       <div className="bg-red-600 dark:bg-red-700 p-2 rounded-md"> | ||||
|         <p>Error: {state.error.message ?? state.error}</p> | ||||
|         <p>Try again?</p> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   // user needs to create an account
 | ||||
|   const signup = async () => { | ||||
|     try { | ||||
|       const resp = await fetchAPI<SignupResponse>("/auth/discord/signup", "POST", { | ||||
|         ticket: state.ticket, | ||||
|         username: formData.username, | ||||
|         invite_code: formData.invite, | ||||
|       }); | ||||
|       const resp = await fetchAPI<SignupResponse>( | ||||
|         "/auth/discord/signup", | ||||
|         "POST", | ||||
|         { | ||||
|           ticket: state.ticket, | ||||
|           username: formData.username, | ||||
|           invite_code: formData.invite, | ||||
|         } | ||||
|       ); | ||||
| 
 | ||||
|       setUser(resp.user); | ||||
|       localStorage.setItem("pronouns-token", resp.token); | ||||
|  | @ -104,33 +128,46 @@ export default function Discord() { | |||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   return <> | ||||
|     <h1 className="font-bold text-lg">Get started</h1> | ||||
|     <p>You{"'"}ve logged in with Discord as <strong className="font-bold">{state.discord}</strong>.</p> | ||||
|   return ( | ||||
|     <> | ||||
|       <h1 className="font-bold text-lg">Get started</h1> | ||||
|       <p> | ||||
|         You{"'"}ve logged in with Discord as{" "} | ||||
|         <strong className="font-bold">{state.discord}</strong>. | ||||
|       </p> | ||||
| 
 | ||||
|     {state.error && ( | ||||
|       <div className="bg-red-600 dark:bg-red-700 p-2 rounded-md"> | ||||
|         <p>Error: {state.error.message ?? state.error}</p> | ||||
|         <p>Try again?</p> | ||||
|       </div> | ||||
|     )} | ||||
|       {state.error && ( | ||||
|         <div className="bg-red-600 dark:bg-red-700 p-2 rounded-md"> | ||||
|           <p>Error: {state.error.message ?? state.error}</p> | ||||
|           <p>Try again?</p> | ||||
|         </div> | ||||
|       )} | ||||
| 
 | ||||
|     <label> | ||||
|       <span className="font-bold">Username</span> | ||||
|       <TextInput value={formData.username} onChange={(e) => setFormData({ ...formData, username: e.target.value })} /> | ||||
|     </label> | ||||
|     {state.requireInvite && ( | ||||
|       <label> | ||||
|         <span className="font-bold">Invite code</span> | ||||
|         <TextInput value={formData.invite} onChange={(e) => setFormData({ ...formData, invite: e.target.value })} /> | ||||
|         <span className="font-bold">Username</span> | ||||
|         <TextInput | ||||
|           contrastBackground | ||||
|           value={formData.username} | ||||
|           onChange={(e) => | ||||
|             setFormData({ ...formData, username: e.target.value }) | ||||
|           } | ||||
|         /> | ||||
|       </label> | ||||
|     )} | ||||
|     <button | ||||
|       type="button" | ||||
|       onClick={() => signup()} | ||||
|       className="bg-green-600 dark:bg-green-700 hover:bg-green-700 hover:dark:bg-green-800 p-2 rounded-md" | ||||
|     > | ||||
|       <span className="font-bold">Create account</span> | ||||
|     </button> | ||||
|   </>; | ||||
|       {state.requireInvite && ( | ||||
|         <label> | ||||
|           <span className="font-bold">Invite code</span> | ||||
|           <TextInput | ||||
|             contrastBackground | ||||
|             value={formData.invite} | ||||
|             onChange={(e) => | ||||
|               setFormData({ ...formData, invite: e.target.value }) | ||||
|             } | ||||
|           /> | ||||
|         </label> | ||||
|       )} | ||||
|       <Button style={ButtonStyle.success} onClick={() => signup()}> | ||||
|         Create account | ||||
|       </Button> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  |  | |||
|  | @ -11,7 +11,13 @@ import { useRecoilValue } from "recoil"; | |||
| import Link from "next/link"; | ||||
| import FallbackImage from "../../../components/FallbackImage"; | ||||
| import { ReactNode } from "react"; | ||||
| import { EmojiLaughing, HandThumbsDown, HandThumbsUp, HeartFill, People } from "react-bootstrap-icons"; | ||||
| import { | ||||
|   EmojiLaughing, | ||||
|   HandThumbsDown, | ||||
|   HandThumbsUp, | ||||
|   HeartFill, | ||||
|   People, | ||||
| } from "react-bootstrap-icons"; | ||||
| 
 | ||||
| interface Props { | ||||
|   user: User; | ||||
|  | @ -54,10 +60,11 @@ export default function Index({ user }: Props) { | |||
|               <h1 className="text-2xl font-bold">{user.display_name}</h1> | ||||
|             )} | ||||
|             <h3 | ||||
|               className={`${user.display_name | ||||
|                 ? "text-xl italic text-slate-600 dark:text-slate-400" | ||||
|                 : "text-2xl font-bold" | ||||
|                 }`}
 | ||||
|               className={`${ | ||||
|                 user.display_name | ||||
|                   ? "text-xl italic text-slate-600 dark:text-slate-400" | ||||
|                   : "text-2xl font-bold" | ||||
|               }`}
 | ||||
|             > | ||||
|               @{user.username} | ||||
|             </h3> | ||||
|  | @ -82,12 +89,20 @@ export default function Index({ user }: Props) { | |||
|             )} | ||||
|           </div> | ||||
|         </div> | ||||
|         {user.names?.length > 0 && <div className="border-b border-slate-200 dark:border-slate-700"> | ||||
|           {user.names.map((name, index) => <NameEntry name={name} key={index} />)} | ||||
|         </div>} | ||||
|         {user.pronouns?.length > 0 && <div className="border-b border-slate-200 dark:border-slate-700"> | ||||
|           {user.pronouns.map((pronoun, index) => <PronounEntry pronoun={pronoun} key={index} />)} | ||||
|         </div>} | ||||
|         {user.names?.length > 0 && ( | ||||
|           <div className="border-b border-slate-200 dark:border-slate-700"> | ||||
|             {user.names.map((name, index) => ( | ||||
|               <NameEntry name={name} key={index} /> | ||||
|             ))} | ||||
|           </div> | ||||
|         )} | ||||
|         {user.pronouns?.length > 0 && ( | ||||
|           <div className="border-b border-slate-200 dark:border-slate-700"> | ||||
|             {user.pronouns.map((pronoun, index) => ( | ||||
|               <PronounEntry pronoun={pronoun} key={index} /> | ||||
|             ))} | ||||
|           </div> | ||||
|         )} | ||||
|         <div className="grid grid-cols-1 md:grid-cols-3 gap-4 py-2"> | ||||
|           {user.fields?.map((field, index) => ( | ||||
|             <FieldCard key={index} field={field}></FieldCard> | ||||
|  | @ -112,30 +127,44 @@ const entryIcon = (status: WordStatus) => { | |||
|       icon = <EmojiLaughing className="inline" />; | ||||
|       break; | ||||
|     case WordStatus.FriendsOnly: | ||||
|       icon = <People className="inline" /> | ||||
|       icon = <People className="inline" />; | ||||
|       break; | ||||
|     case WordStatus.Avoid: | ||||
|       icon = <HandThumbsDown className="inline" /> | ||||
|       icon = <HandThumbsDown className="inline" />; | ||||
|       break; | ||||
|   } | ||||
| 
 | ||||
|   return icon; | ||||
| } | ||||
| }; | ||||
| 
 | ||||
| function NameEntry(props: { name: Name }) { | ||||
|   const { name } = props; | ||||
| 
 | ||||
|   return <p className={`text-lg ${name.status === WordStatus.Favourite && "font-bold"}`}> | ||||
|     {entryIcon(name.status)} {name.name} | ||||
|   </p> | ||||
|   return ( | ||||
|     <p | ||||
|       className={`text-lg ${ | ||||
|         name.status === WordStatus.Favourite && "font-bold" | ||||
|       }`}
 | ||||
|     > | ||||
|       {entryIcon(name.status)} {name.name} | ||||
|     </p> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| function PronounEntry(props: { pronoun: Pronoun }) { | ||||
|   const { pronoun } = props; | ||||
| 
 | ||||
|   return <p className={`text-lg ${pronoun.status === WordStatus.Favourite && "font-bold"}`}> | ||||
|     {entryIcon(pronoun.status)} {pronoun.display_text ?? pronoun.pronouns.split("/").slice(0, 2).join("/")} | ||||
|   </p> | ||||
|   return ( | ||||
|     <p | ||||
|       className={`text-lg ${ | ||||
|         pronoun.status === WordStatus.Favourite && "font-bold" | ||||
|       }`}
 | ||||
|     > | ||||
|       {entryIcon(pronoun.status)}{" "} | ||||
|       {pronoun.display_text ?? | ||||
|         pronoun.pronouns.split("/").slice(0, 2).join("/")} | ||||
|     </p> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export const getServerSideProps: GetServerSideProps = async (context) => { | ||||
|  |  | |||
|  | @ -2,12 +2,14 @@ | |||
| // The config you add here will be used whenever a page is visited.
 | ||||
| // https://docs.sentry.io/platforms/javascript/guides/nextjs/
 | ||||
| 
 | ||||
| import * as Sentry from '@sentry/nextjs'; | ||||
| import * as Sentry from "@sentry/nextjs"; | ||||
| 
 | ||||
| const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN; | ||||
| 
 | ||||
| Sentry.init({ | ||||
|   dsn: SENTRY_DSN || 'https://91af8c15c9cf4153aa260b7f57457d8f@o575775.ingest.sentry.io/6390139', | ||||
|   dsn: | ||||
|     SENTRY_DSN || | ||||
|     "https://91af8c15c9cf4153aa260b7f57457d8f@o575775.ingest.sentry.io/6390139", | ||||
|   // Adjust this value in production, or use tracesSampler for greater control
 | ||||
|   tracesSampleRate: 1.0, | ||||
|   // ...
 | ||||
|  |  | |||
|  | @ -2,12 +2,14 @@ | |||
| // The config you add here will be used whenever the server handles a request.
 | ||||
| // https://docs.sentry.io/platforms/javascript/guides/nextjs/
 | ||||
| 
 | ||||
| import * as Sentry from '@sentry/nextjs'; | ||||
| import * as Sentry from "@sentry/nextjs"; | ||||
| 
 | ||||
| const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN; | ||||
| 
 | ||||
| Sentry.init({ | ||||
|   dsn: SENTRY_DSN || 'https://91af8c15c9cf4153aa260b7f57457d8f@o575775.ingest.sentry.io/6390139', | ||||
|   dsn: | ||||
|     SENTRY_DSN || | ||||
|     "https://91af8c15c9cf4153aa260b7f57457d8f@o575775.ingest.sentry.io/6390139", | ||||
|   // Adjust this value in production, or use tracesSampler for greater control
 | ||||
|   tracesSampleRate: 1.0, | ||||
|   // ...
 | ||||
|  |  | |||
|  | @ -2379,6 +2379,11 @@ prelude-ls@^1.2.1: | |||
|   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" | ||||
|   integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== | ||||
| 
 | ||||
| prettier@2.7.1: | ||||
|   version "2.7.1" | ||||
|   resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64" | ||||
|   integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== | ||||
| 
 | ||||
| process-nextick-args@~2.0.0: | ||||
|   version "2.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue