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 ( | @use "bootstrap/scss/bootstrap" with ( | ||||||
| 	$color-mode-type: media-query, | 	$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"; | @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 classNames from "classnames"; | ||||||
| import { ReactNode } from "react"; | import { ReactNode } from "react"; | ||||||
| import StatusIcon from "~/components/StatusIcon"; | import StatusIcon from "~/components/StatusIcon"; | ||||||
|  | import PronounLink from "~/components/PronounLink"; | ||||||
| 
 | 
 | ||||||
| export default function StatusLine({ | export default function StatusLine({ | ||||||
| 	entry, | 	entry, | ||||||
| 	preferences, | 	preferences, | ||||||
| 	children, |  | ||||||
| }: { | }: { | ||||||
| 	entry: FieldEntry | Pronoun; | 	entry: FieldEntry | Pronoun; | ||||||
| 	preferences: Record<string, CustomPreference>; | 	preferences: Record<string, CustomPreference>; | ||||||
| 	children: ReactNode; |  | ||||||
| }) { | }) { | ||||||
| 	const mergedPrefs = Object.assign({}, defaultPreferences, preferences); | 	const mergedPrefs = Object.assign({}, defaultPreferences, preferences); | ||||||
| 	const currentPref = | 	const currentPref = | ||||||
|  | @ -28,9 +27,19 @@ export default function StatusLine({ | ||||||
| 		"fs-6": currentPref.size == PreferenceSize.Small, | 		"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 ( | 	return ( | ||||||
| 		<span className={classes}> | 		<span className={classes}> | ||||||
| 			<StatusIcon preferences={preferences} status={entry.status} /> {children} | 			<StatusIcon preferences={preferences} status={entry.status} /> {entry.value} | ||||||
| 		</span> | 		</span> | ||||||
| 	); | 	); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ import { Nav, Navbar } from "react-bootstrap"; | ||||||
| import { Link } from "@remix-run/react"; | import { Link } from "@remix-run/react"; | ||||||
| import Logo from "~/components/nav/Logo"; | import Logo from "~/components/nav/Logo"; | ||||||
| 
 | 
 | ||||||
| export default function BaseNavbar({ children }: { children?: ReactNode; }) { | export default function BaseNavbar({ children }: { children?: ReactNode }) { | ||||||
| 	return ( | 	return ( | ||||||
| 		<Navbar expand="lg" className={`mb-4 mx-2`}> | 		<Navbar expand="lg" className={`mb-4 mx-2`}> | ||||||
| 			<Navbar.Brand to="/" as={Link}> | 			<Navbar.Brand to="/" as={Link}> | ||||||
|  |  | ||||||
|  | @ -6,12 +6,7 @@ import { Nav, NavDropdown } from "react-bootstrap"; | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
| import BaseNavbar from "~/components/nav/BaseNavbar"; | import BaseNavbar from "~/components/nav/BaseNavbar"; | ||||||
| 
 | 
 | ||||||
| export default function MainNavbar({ | export default function MainNavbar({ user }: { meta: Meta; user?: User }) { | ||||||
| 	user, |  | ||||||
| }: { |  | ||||||
| 	meta: Meta; |  | ||||||
| 	user?: User; |  | ||||||
| }) { |  | ||||||
| 	const fetcher = useFetcher(); | 	const fetcher = useFetcher(); | ||||||
| 	const { t } = useTranslation(); | 	const { t } = useTranslation(); | ||||||
| 
 | 
 | ||||||
|  | @ -35,10 +30,6 @@ export default function MainNavbar({ | ||||||
| 			{t("navbar.log-in")} | 			{t("navbar.log-in")} | ||||||
| 		</Nav.Link> | 		</Nav.Link> | ||||||
| 	); | 	); | ||||||
| 	 | 
 | ||||||
| 	return ( | 	return <BaseNavbar>{userMenu}</BaseNavbar>; | ||||||
| 		<BaseNavbar> |  | ||||||
| 			{userMenu} |  | ||||||
| 		</BaseNavbar> |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -75,21 +75,18 @@ export function Layout({ children }: { children: ReactNode }) { | ||||||
| 	useChangeLanguage(locale || "en"); | 	useChangeLanguage(locale || "en"); | ||||||
| 
 | 
 | ||||||
| 	return ( | 	return ( | ||||||
| 		<html | 		<html lang={locale || "en"} dir={i18n.dir()}> | ||||||
| 			lang={locale || "en"} | 			<head> | ||||||
| 			dir={i18n.dir()} | 				<meta charSet="utf-8" /> | ||||||
| 		> | 				<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> | ||||||
| 		<head> | 				<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||||||
| 			<meta charSet="utf-8" /> | 				<MetaComponent /> | ||||||
| 			<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> | 				<Links /> | ||||||
| 			<meta name="viewport" content="width=device-width, initial-scale=1" /> | 			</head> | ||||||
| 			<MetaComponent /> | 			<body> | ||||||
| 			<Links /> | 				{children} | ||||||
| 		</head> | 				<ScrollRestoration /> | ||||||
| 		<body> | 				<Scripts /> | ||||||
| 		{children} |  | ||||||
| 		<ScrollRestoration /> |  | ||||||
| 		<Scripts /> |  | ||||||
| 			</body> | 			</body> | ||||||
| 		</html> | 		</html> | ||||||
| 	); | 	); | ||||||
|  |  | ||||||
|  | @ -7,8 +7,7 @@ import { Alert } from "react-bootstrap"; | ||||||
| import { Trans, useTranslation } from "react-i18next"; | import { Trans, useTranslation } from "react-i18next"; | ||||||
| import { renderMarkdown } from "~/lib/markdown"; | import { renderMarkdown } from "~/lib/markdown"; | ||||||
| import ProfileLink from "~/components/ProfileLink"; | import ProfileLink from "~/components/ProfileLink"; | ||||||
| import StatusLine from "~/components/StatusLine"; | import ProfileField from "~/components/ProfileField"; | ||||||
| import PronounLink from "~/components/PronounLink"; |  | ||||||
| 
 | 
 | ||||||
| export const meta: MetaFunction<typeof loader> = ({ data }) => { | export const meta: MetaFunction<typeof loader> = ({ data }) => { | ||||||
| 	const { user } = data!; | 	const { user } = data!; | ||||||
|  | @ -18,17 +17,21 @@ export const meta: MetaFunction<typeof loader> = ({ data }) => { | ||||||
| 
 | 
 | ||||||
| export const loader = async ({ request, params }: LoaderFunctionArgs) => { | export const loader = async ({ request, params }: LoaderFunctionArgs) => { | ||||||
| 	const url = new URL(request.url); | 	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!; | 	let username = params.username!; | ||||||
| 	if (!username.startsWith("@")) throw redirect(`/@${username}`); | 	if (!username.startsWith("@")) throw redirect(`/@${username}`); | ||||||
| 	username = username.substring("@".length); | 	username = username.substring("@".length); | ||||||
| 
 | 
 | ||||||
| 	const user = await serverRequest<UserWithMembers>("GET", `/users/${username}`); | 	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); | 	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() { | export default function UserPage() { | ||||||
|  | @ -90,27 +93,27 @@ export default function UserPage() { | ||||||
| 				</div> | 				</div> | ||||||
| 				<div className="row row-cols-1 row-cols-sm-2 row-cols-md-3"> | 				<div className="row row-cols-1 row-cols-sm-2 row-cols-md-3"> | ||||||
| 					{user.names.length > 0 && ( | 					{user.names.length > 0 && ( | ||||||
| 						<div className="col-md"> | 						<ProfileField | ||||||
| 							<h3>{t("user.heading.names")}</h3> | 							name={t("user.heading.names")} | ||||||
| 							<ul className="list-unstyled fs-5"> | 							entries={user.names} | ||||||
| 								{user.names.map((n, i) => ( | 							preferences={user.custom_preferences} | ||||||
| 									<StatusLine entry={n} preferences={user.custom_preferences} key={i}> | 						/> | ||||||
| 										{n.value} |  | ||||||
| 									</StatusLine> |  | ||||||
| 								))} |  | ||||||
| 							</ul> |  | ||||||
| 						</div> |  | ||||||
| 					)} | 					)} | ||||||
| 					{user.pronouns.length > 0 && ( | 					{user.pronouns.length > 0 && ( | ||||||
| 						<div className="col-md"> | 						<ProfileField | ||||||
| 							<h3>{t("user.heading.pronouns")}</h3> | 							name={t("user.heading.pronouns")} | ||||||
| 							{user.pronouns.map((p, i) => ( | 							entries={user.pronouns} | ||||||
| 								<StatusLine entry={p} preferences={user.custom_preferences} key={i}> | 							preferences={user.custom_preferences} | ||||||
| 									<PronounLink pronoun={p} /> | 						/> | ||||||
| 								</StatusLine> |  | ||||||
| 							))} |  | ||||||
| 						</div> |  | ||||||
| 					)} | 					)} | ||||||
|  | 					{user.fields.map((f, i) => ( | ||||||
|  | 						<ProfileField | ||||||
|  | 							name={f.name} | ||||||
|  | 							entries={f.entries} | ||||||
|  | 							preferences={user.custom_preferences} | ||||||
|  | 							key={i} | ||||||
|  | 						/> | ||||||
|  | 					))} | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
| 		</> | 		</> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue