feat(frontend): show user profile fields
This commit is contained in:
		
							parent
							
								
									4ba28bbfde
								
							
						
					
					
						commit
						4732451040
					
				
					 7 changed files with 95 additions and 55 deletions
				
			
		|  | @ -1,6 +1,21 @@ | |||
| @use "bootstrap/scss/bootstrap" with ( | ||||
| 	$color-mode-type: media-query, | ||||
| 	$font-family-sans-serif: ("FiraGO", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"), | ||||
| 	$font-family-sans-serif: ( | ||||
| 		"FiraGO", | ||||
| 		system-ui, | ||||
| 		-apple-system, | ||||
| 		"Segoe UI", | ||||
| 		Roboto, | ||||
| 		"Helvetica Neue", | ||||
| 		"Noto Sans", | ||||
| 		"Liberation Sans", | ||||
| 		Arial, | ||||
| 		sans-serif, | ||||
| 		"Apple Color Emoji", | ||||
| 		"Segoe UI Emoji", | ||||
| 		"Segoe UI Symbol", | ||||
| 		"Noto Color Emoji", | ||||
| 	) | ||||
| ); | ||||
| 
 | ||||
| @import "@fontsource/firago/400.css"; | ||||
|  |  | |||
							
								
								
									
										25
									
								
								Foxnouns.Frontend/app/components/ProfileField.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								Foxnouns.Frontend/app/components/ProfileField.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | |||
| import { CustomPreference, FieldEntry, Pronoun } from "~/lib/api/user"; | ||||
| import StatusLine from "~/components/StatusLine"; | ||||
| 
 | ||||
| export default function ProfileField({ | ||||
| 	name, | ||||
| 	entries, | ||||
| 	preferences, | ||||
| }: { | ||||
| 	name: string; | ||||
| 	entries: Array<FieldEntry | Pronoun>; | ||||
| 	preferences: Record<string, CustomPreference>; | ||||
| }) { | ||||
| 	return ( | ||||
| 		<div className="col"> | ||||
| 			<h3>{name}</h3> | ||||
| 			<ul className="list-unstyled fs-5"> | ||||
| 				{entries.map((e, i) => ( | ||||
| 					<li key={i}> | ||||
| 						<StatusLine entry={e} preferences={preferences} /> | ||||
| 					</li> | ||||
| 				))} | ||||
| 			</ul> | ||||
| 		</div> | ||||
| 	); | ||||
| } | ||||
|  | @ -8,15 +8,14 @@ import { | |||
| import classNames from "classnames"; | ||||
| import { ReactNode } from "react"; | ||||
| import StatusIcon from "~/components/StatusIcon"; | ||||
| import PronounLink from "~/components/PronounLink"; | ||||
| 
 | ||||
| export default function StatusLine({ | ||||
| 	entry, | ||||
| 	preferences, | ||||
| 	children, | ||||
| }: { | ||||
| 	entry: FieldEntry | Pronoun; | ||||
| 	preferences: Record<string, CustomPreference>; | ||||
| 	children: ReactNode; | ||||
| }) { | ||||
| 	const mergedPrefs = Object.assign({}, defaultPreferences, preferences); | ||||
| 	const currentPref = | ||||
|  | @ -28,9 +27,19 @@ export default function StatusLine({ | |||
| 		"fs-6": currentPref.size == PreferenceSize.Small, | ||||
| 	}); | ||||
| 
 | ||||
| 	if ("display_text" in entry) { | ||||
| 		const pronoun = entry as Pronoun; | ||||
| 		return ( | ||||
| 			<span className={classes}> | ||||
| 				<StatusIcon preferences={preferences} status={entry.status} />{" "} | ||||
| 				<PronounLink pronoun={pronoun} /> | ||||
| 			</span> | ||||
| 		); | ||||
| 	} | ||||
| 
 | ||||
| 	return ( | ||||
| 		<span className={classes}> | ||||
| 			<StatusIcon preferences={preferences} status={entry.status} /> {children} | ||||
| 			<StatusIcon preferences={preferences} status={entry.status} /> {entry.value} | ||||
| 		</span> | ||||
| 	); | ||||
| } | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import { Nav, Navbar } from "react-bootstrap"; | |||
| import { Link } from "@remix-run/react"; | ||||
| import Logo from "~/components/nav/Logo"; | ||||
| 
 | ||||
| export default function BaseNavbar({ children }: { children?: ReactNode; }) { | ||||
| export default function BaseNavbar({ children }: { children?: ReactNode }) { | ||||
| 	return ( | ||||
| 		<Navbar expand="lg" className={`mb-4 mx-2`}> | ||||
| 			<Navbar.Brand to="/" as={Link}> | ||||
|  |  | |||
|  | @ -6,12 +6,7 @@ import { Nav, NavDropdown } from "react-bootstrap"; | |||
| import { useTranslation } from "react-i18next"; | ||||
| import BaseNavbar from "~/components/nav/BaseNavbar"; | ||||
| 
 | ||||
| export default function MainNavbar({ | ||||
| 	user, | ||||
| }: { | ||||
| 	meta: Meta; | ||||
| 	user?: User; | ||||
| }) { | ||||
| export default function MainNavbar({ user }: { meta: Meta; user?: User }) { | ||||
| 	const fetcher = useFetcher(); | ||||
| 	const { t } = useTranslation(); | ||||
| 
 | ||||
|  | @ -35,10 +30,6 @@ export default function MainNavbar({ | |||
| 			{t("navbar.log-in")} | ||||
| 		</Nav.Link> | ||||
| 	); | ||||
| 	 | ||||
| 	return ( | ||||
| 		<BaseNavbar> | ||||
| 			{userMenu} | ||||
| 		</BaseNavbar> | ||||
| 	); | ||||
| 
 | ||||
| 	return <BaseNavbar>{userMenu}</BaseNavbar>; | ||||
| } | ||||
|  |  | |||
|  | @ -75,21 +75,18 @@ export function Layout({ children }: { children: ReactNode }) { | |||
| 	useChangeLanguage(locale || "en"); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<html | ||||
| 			lang={locale || "en"} | ||||
| 			dir={i18n.dir()} | ||||
| 		> | ||||
| 		<head> | ||||
| 			<meta charSet="utf-8" /> | ||||
| 			<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> | ||||
| 			<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||||
| 			<MetaComponent /> | ||||
| 			<Links /> | ||||
| 		</head> | ||||
| 		<body> | ||||
| 		{children} | ||||
| 		<ScrollRestoration /> | ||||
| 		<Scripts /> | ||||
| 		<html lang={locale || "en"} dir={i18n.dir()}> | ||||
| 			<head> | ||||
| 				<meta charSet="utf-8" /> | ||||
| 				<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> | ||||
| 				<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||||
| 				<MetaComponent /> | ||||
| 				<Links /> | ||||
| 			</head> | ||||
| 			<body> | ||||
| 				{children} | ||||
| 				<ScrollRestoration /> | ||||
| 				<Scripts /> | ||||
| 			</body> | ||||
| 		</html> | ||||
| 	); | ||||
|  |  | |||
|  | @ -7,8 +7,7 @@ import { Alert } from "react-bootstrap"; | |||
| import { Trans, useTranslation } from "react-i18next"; | ||||
| import { renderMarkdown } from "~/lib/markdown"; | ||||
| import ProfileLink from "~/components/ProfileLink"; | ||||
| import StatusLine from "~/components/StatusLine"; | ||||
| import PronounLink from "~/components/PronounLink"; | ||||
| import ProfileField from "~/components/ProfileField"; | ||||
| 
 | ||||
| export const meta: MetaFunction<typeof loader> = ({ data }) => { | ||||
| 	const { user } = data!; | ||||
|  | @ -18,17 +17,21 @@ export const meta: MetaFunction<typeof loader> = ({ data }) => { | |||
| 
 | ||||
| export const loader = async ({ request, params }: LoaderFunctionArgs) => { | ||||
| 	const url = new URL(request.url); | ||||
| 	const memberPage = parseInt(url.searchParams.get("page") ?? "0", 10); | ||||
| 	let memberPage = parseInt(url.searchParams.get("page") ?? "0", 10); | ||||
| 
 | ||||
| 	let username = params.username!; | ||||
| 	if (!username.startsWith("@")) throw redirect(`/@${username}`); | ||||
| 	username = username.substring("@".length); | ||||
| 
 | ||||
| 	const user = await serverRequest<UserWithMembers>("GET", `/users/${username}`); | ||||
| 	const pageCount = Math.ceil(user.members.length / 20); | ||||
| 	let members = user.members.slice(memberPage * 20, (memberPage + 1) * 20); | ||||
| 	if (members.length === 0) members = user.members.slice(0, 20); | ||||
| 	if (members.length === 0) { | ||||
| 		members = user.members.slice(0, 20); | ||||
| 		memberPage = 0; | ||||
| 	} | ||||
| 
 | ||||
| 	return json({ user, members }); | ||||
| 	return json({ user, members, currentPage: memberPage, pageCount }); | ||||
| }; | ||||
| 
 | ||||
| export default function UserPage() { | ||||
|  | @ -90,27 +93,27 @@ export default function UserPage() { | |||
| 				</div> | ||||
| 				<div className="row row-cols-1 row-cols-sm-2 row-cols-md-3"> | ||||
| 					{user.names.length > 0 && ( | ||||
| 						<div className="col-md"> | ||||
| 							<h3>{t("user.heading.names")}</h3> | ||||
| 							<ul className="list-unstyled fs-5"> | ||||
| 								{user.names.map((n, i) => ( | ||||
| 									<StatusLine entry={n} preferences={user.custom_preferences} key={i}> | ||||
| 										{n.value} | ||||
| 									</StatusLine> | ||||
| 								))} | ||||
| 							</ul> | ||||
| 						</div> | ||||
| 						<ProfileField | ||||
| 							name={t("user.heading.names")} | ||||
| 							entries={user.names} | ||||
| 							preferences={user.custom_preferences} | ||||
| 						/> | ||||
| 					)} | ||||
| 					{user.pronouns.length > 0 && ( | ||||
| 						<div className="col-md"> | ||||
| 							<h3>{t("user.heading.pronouns")}</h3> | ||||
| 							{user.pronouns.map((p, i) => ( | ||||
| 								<StatusLine entry={p} preferences={user.custom_preferences} key={i}> | ||||
| 									<PronounLink pronoun={p} /> | ||||
| 								</StatusLine> | ||||
| 							))} | ||||
| 						</div> | ||||
| 						<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> | ||||
| 		</> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue