refactor(frontend): extract profile view to component shared between users and members
This commit is contained in:
		
							parent
							
								
									dc18ab60d2
								
							
						
					
					
						commit
						4514216405
					
				
					 3 changed files with 121 additions and 85 deletions
				
			
		
							
								
								
									
										111
									
								
								Foxnouns.Frontend/app/components/profile/BaseProfile.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								Foxnouns.Frontend/app/components/profile/BaseProfile.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,111 @@ | |||
| import { CustomPreference, User } from "~/lib/api/user"; | ||||
| import { Member } from "~/lib/api/member"; | ||||
| import { defaultAvatarUrl } from "~/lib/utils"; | ||||
| import ProfileFlag from "~/components/profile/ProfileFlag"; | ||||
| import ProfileLink from "~/components/profile/ProfileLink"; | ||||
| import ProfileField from "~/components/profile/ProfileField"; | ||||
| import { useTranslation } from "react-i18next"; | ||||
| import { renderMarkdown } from "~/lib/markdown"; | ||||
| 
 | ||||
| export type Props = { | ||||
| 	name: string; | ||||
| 	fullName?: string; | ||||
| 	avatarI18nKey: string; | ||||
| 	profile: User | Member; | ||||
| 	customPreferences: Record<string, CustomPreference>; | ||||
| }; | ||||
| 
 | ||||
| export default function BaseProfile({ | ||||
| 	name, | ||||
| 	avatarI18nKey, | ||||
| 	fullName, | ||||
| 	profile, | ||||
| 	customPreferences, | ||||
| }: Props) { | ||||
| 	const { t } = useTranslation(); | ||||
| 	const bio = renderMarkdown(profile.bio); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<> | ||||
| 			<div className="grid row-gap-3"> | ||||
| 				<div className="row"> | ||||
| 					<div className="col-md-4 text-center"> | ||||
| 						<img | ||||
| 							src={profile.avatar_url || defaultAvatarUrl} | ||||
| 							alt={t(avatarI18nKey, { username: name })} | ||||
| 							width={200} | ||||
| 							height={200} | ||||
| 							className="rounded-circle img-fluid" | ||||
| 						/> | ||||
| 						{profile.flags && profile.bio && ( | ||||
| 							<div className="d-flex flex-wrap m-4"> | ||||
| 								{profile.flags.map((f, i) => ( | ||||
| 									<ProfileFlag flag={f} key={i} /> | ||||
| 								))} | ||||
| 							</div> | ||||
| 						)} | ||||
| 					</div> | ||||
| 					<div className="col-md"> | ||||
| 						{profile.display_name ? ( | ||||
| 							<> | ||||
| 								<h2>{profile.display_name}</h2> | ||||
| 								<p className="fs-5 text-body-secondary">{fullName || `@${name}`}</p> | ||||
| 							</> | ||||
| 						) : ( | ||||
| 							<> | ||||
| 								<h2>{fullName || `@${name}`}</h2> | ||||
| 							</> | ||||
| 						)} | ||||
| 						{bio && ( | ||||
| 							<> | ||||
| 								<hr /> | ||||
| 								<p dangerouslySetInnerHTML={{ __html: bio }}></p> | ||||
| 							</> | ||||
| 						)} | ||||
| 					</div> | ||||
| 					{profile.links.length > 0 && ( | ||||
| 						<div className="col-md d-flex align-items-center"> | ||||
| 							<ul className="list-unstyled"> | ||||
| 								{profile.links.map((l, i) => ( | ||||
| 									<ProfileLink link={l} key={i} /> | ||||
| 								))} | ||||
| 							</ul> | ||||
| 						</div> | ||||
| 					)} | ||||
| 				</div> | ||||
| 				<div className="row row-cols-1 row-cols-sm-2 row-cols-md-3"> | ||||
| 					{profile.names.length > 0 && ( | ||||
| 						<ProfileField | ||||
| 							name={t("user.heading.names")} | ||||
| 							entries={profile.names} | ||||
| 							preferences={customPreferences} | ||||
| 						/> | ||||
| 					)} | ||||
| 					{profile.pronouns.length > 0 && ( | ||||
| 						<ProfileField | ||||
| 							name={t("user.heading.pronouns")} | ||||
| 							entries={profile.pronouns} | ||||
| 							preferences={customPreferences} | ||||
| 						/> | ||||
| 					)} | ||||
| 					{profile.fields.map((f, i) => ( | ||||
| 						<ProfileField | ||||
| 							name={f.name} | ||||
| 							entries={f.entries} | ||||
| 							preferences={customPreferences} | ||||
| 							key={i} | ||||
| 						/> | ||||
| 					))} | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			{/* If a user or member has no bio, flags are displayed in a row below the other profile info, rather than just below the avatar */} | ||||
| 			{profile.flags && !profile.bio && ( | ||||
| 				<div className="d-flex flex-wrap m-4"> | ||||
| 					{profile.flags.map((f, i) => ( | ||||
| 						<ProfileFlag flag={f} key={i} /> | ||||
| 					))} | ||||
| 				</div> | ||||
| 			)} | ||||
| 		</> | ||||
| 	); | ||||
| } | ||||
|  | @ -1,5 +1,7 @@ | |||
| import { PartialMember, PrideFlag } from "~/lib/api/user"; | ||||
| import { Field, PartialMember, PrideFlag } from "~/lib/api/user"; | ||||
| 
 | ||||
| export type Member = PartialMember & { | ||||
| 	fields: Field[]; | ||||
| 	flags: PrideFlag[]; | ||||
| 	links: string[]; | ||||
| }; | ||||
|  |  | |||
|  | @ -6,13 +6,10 @@ import { loader as rootLoader } from "~/root"; | |||
| import { Alert, Button, Pagination } from "react-bootstrap"; | ||||
| import { Trans, useTranslation } from "react-i18next"; | ||||
| import { renderMarkdown } from "~/lib/markdown"; | ||||
| import ProfileLink from "~/components/profile/ProfileLink"; | ||||
| import ProfileField from "~/components/profile/ProfileField"; | ||||
| import { PersonPlusFill } from "react-bootstrap-icons"; | ||||
| import { defaultAvatarUrl } from "~/lib/utils"; | ||||
| import MemberCard from "~/routes/$username/MemberCard"; | ||||
| import { ReactNode } from "react"; | ||||
| import ProfileFlag from "~/components/profile/ProfileFlag"; | ||||
| import BaseProfile from "~/components/profile/BaseProfile"; | ||||
| 
 | ||||
| export const meta: MetaFunction<typeof loader> = ({ data }) => { | ||||
| 	const { user } = data!; | ||||
|  | @ -45,7 +42,6 @@ export default function UserPage() { | |||
| 	const { meUser } = useRouteLoaderData<typeof rootLoader>("root") || { meUser: undefined }; | ||||
| 
 | ||||
| 	const isMeUser = meUser && meUser.id === user.id; | ||||
| 	const bio = renderMarkdown(user.bio); | ||||
| 
 | ||||
| 	const paginationItems: ReactNode[] = []; | ||||
| 	for (let i = 0; i < pageCount; i++) { | ||||
|  | @ -83,85 +79,12 @@ export default function UserPage() { | |||
| 					</Trans> | ||||
| 				</Alert> | ||||
| 			)} | ||||
| 			<div className="grid row-gap-3"> | ||||
| 				<div className="row"> | ||||
| 					<div className="col-md-4 text-center"> | ||||
| 						<img | ||||
| 							src={user.avatar_url || defaultAvatarUrl} | ||||
| 							alt={t("user.avatar-alt", { username: user.username })} | ||||
| 							width={200} | ||||
| 							height={200} | ||||
| 							className="rounded-circle img-fluid" | ||||
| 			<BaseProfile | ||||
| 				name={user.username} | ||||
| 				avatarI18nKey={"user.avatar-alt"} | ||||
| 				profile={user} | ||||
| 				customPreferences={user.custom_preferences} | ||||
| 			/> | ||||
| 						{user.flags && user.bio && ( | ||||
| 							<div className="d-flex flex-wrap m-4"> | ||||
| 								{user.flags.map((f, i) => ( | ||||
| 									<ProfileFlag flag={f} key={i} /> | ||||
| 								))} | ||||
| 							</div> | ||||
| 						)} | ||||
| 					</div> | ||||
| 					<div className="col-md"> | ||||
| 						{user.display_name ? ( | ||||
| 							<> | ||||
| 								<h2>{user.display_name}</h2> | ||||
| 								<p className="fs-5 text-body-secondary">@{user.username}</p> | ||||
| 							</> | ||||
| 						) : ( | ||||
| 							<> | ||||
| 								<h2>@{user.username}</h2> | ||||
| 							</> | ||||
| 						)} | ||||
| 						{bio && ( | ||||
| 							<> | ||||
| 								<hr /> | ||||
| 								<p dangerouslySetInnerHTML={{ __html: bio }}></p> | ||||
| 							</> | ||||
| 						)} | ||||
| 					</div> | ||||
| 					{user.links.length > 0 && ( | ||||
| 						<div className="col-md d-flex align-items-center"> | ||||
| 							<ul className="list-unstyled"> | ||||
| 								{user.links.map((l, i) => ( | ||||
| 									<ProfileLink link={l} key={i} /> | ||||
| 								))} | ||||
| 							</ul> | ||||
| 						</div> | ||||
| 					)} | ||||
| 				</div> | ||||
| 				<div className="row row-cols-1 row-cols-sm-2 row-cols-md-3"> | ||||
| 					{user.names.length > 0 && ( | ||||
| 						<ProfileField | ||||
| 							name={t("user.heading.names")} | ||||
| 							entries={user.names} | ||||
| 							preferences={user.custom_preferences} | ||||
| 						/> | ||||
| 					)} | ||||
| 					{user.pronouns.length > 0 && ( | ||||
| 						<ProfileField | ||||
| 							name={t("user.heading.pronouns")} | ||||
| 							entries={user.pronouns} | ||||
| 							preferences={user.custom_preferences} | ||||
| 						/> | ||||
| 					)} | ||||
| 					{user.fields.map((f, i) => ( | ||||
| 						<ProfileField | ||||
| 							name={f.name} | ||||
| 							entries={f.entries} | ||||
| 							preferences={user.custom_preferences} | ||||
| 							key={i} | ||||
| 						/> | ||||
| 					))} | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			{/* If a user or member has no bio, flags are displayed in a row below the other profile info, rather than just below the avatar */} | ||||
| 			{user.flags && !user.bio && ( | ||||
| 				<div className="d-flex flex-wrap m-4"> | ||||
| 					{user.flags.map((f, i) => ( | ||||
| 						<ProfileFlag flag={f} key={i} /> | ||||
| 					))} | ||||
| 				</div> | ||||
| 			)} | ||||
| 			{(members.length > 0 || isMeUser) && ( | ||||
| 				<> | ||||
| 					<hr /> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue