diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index 60d4499..d5df79d 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -71,6 +71,7 @@ public static class DatabaseQueryExtensions { member = await context.Members .Include(m => m.User) + .Include(m => m.ProfileFlags) .Where(m => !m.User.Deleted) .FirstOrDefaultAsync(m => m.Id == snowflake && m.UserId == userId, ct); if (member != null) return member; @@ -78,6 +79,7 @@ public static class DatabaseQueryExtensions member = await context.Members .Include(m => m.User) + .Include(m => m.ProfileFlags) .Where(m => !m.User.Deleted) .FirstOrDefaultAsync(m => m.Name == memberRef && m.UserId == userId, ct); if (member != null) return member; diff --git a/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs b/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs index 6b6da6d..c28b58e 100644 --- a/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs +++ b/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs @@ -1,5 +1,6 @@ using System.Net; using Foxnouns.Backend.Utils; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Newtonsoft.Json; namespace Foxnouns.Backend.Middleware; diff --git a/Foxnouns.Backend/Services/MemberRendererService.cs b/Foxnouns.Backend/Services/MemberRendererService.cs index 0fdaa53..9ebea49 100644 --- a/Foxnouns.Backend/Services/MemberRendererService.cs +++ b/Foxnouns.Backend/Services/MemberRendererService.cs @@ -24,13 +24,14 @@ public class MemberRendererService(DatabaseContext db, Config config) return members.Select(m => RenderPartialMember(m, renderUnlisted)); } - public MemberResponse RenderMember(Member member, Token? token) + public MemberResponse RenderMember(Member member, Token? token = null) { var renderUnlisted = token?.UserId == member.UserId && token.HasScope("user.read_hidden"); return new MemberResponse( member.Id, member.Sid, member.Name, member.DisplayName, member.Bio, AvatarUrlFor(member), member.Links, member.Names, member.Pronouns, member.Fields, + member.ProfileFlags.Select(f => RenderPrideFlag(f.PrideFlag)), RenderPartialUser(member.User), renderUnlisted ? member.Unlisted : null); } @@ -50,6 +51,9 @@ public class MemberRendererService(DatabaseContext db, Config config) private string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp"; + private UserRendererService.PrideFlagResponse RenderPrideFlag(PrideFlag flag) => + new(flag.Id, ImageUrlFor(flag), flag.Name, flag.Description); + public record PartialMember( Snowflake Id, string Sid, @@ -73,6 +77,7 @@ public class MemberRendererService(DatabaseContext db, Config config) IEnumerable Names, IEnumerable Pronouns, IEnumerable Fields, + IEnumerable Flags, UserRendererService.PartialUser User, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Unlisted); diff --git a/Foxnouns.Frontend/app/app.scss b/Foxnouns.Frontend/app/app.scss index 45a8ee5..0d30b1e 100644 --- a/Foxnouns.Frontend/app/app.scss +++ b/Foxnouns.Frontend/app/app.scss @@ -21,3 +21,9 @@ @import "@fontsource/firago/400.css"; @import "@fontsource/firago/400-italic.css"; @import "@fontsource/firago/700.css"; + +.pride-flag { + height: 1.5rem; + max-width: 200px; + border-radius: 3px; +} diff --git a/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx b/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx new file mode 100644 index 0000000..4017877 --- /dev/null +++ b/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx @@ -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; +}; + +export default function BaseProfile({ + name, + avatarI18nKey, + fullName, + profile, + customPreferences, +}: Props) { + const { t } = useTranslation(); + const bio = renderMarkdown(profile.bio); + + return ( + <> +
+
+
+ {t(avatarI18nKey, + {profile.flags && profile.bio && ( +
+ {profile.flags.map((f, i) => ( + + ))} +
+ )} +
+
+ {profile.display_name ? ( + <> +

{profile.display_name}

+

{fullName || `@${name}`}

+ + ) : ( + <> +

{fullName || `@${name}`}

+ + )} + {bio && ( + <> +
+

+ + )} +
+ {profile.links.length > 0 && ( +
+
    + {profile.links.map((l, i) => ( + + ))} +
+
+ )} +
+
+ {profile.names.length > 0 && ( + + )} + {profile.pronouns.length > 0 && ( + + )} + {profile.fields.map((f, i) => ( + + ))} +
+
+ {/* 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 && ( +
+ {profile.flags.map((f, i) => ( + + ))} +
+ )} + + ); +} diff --git a/Foxnouns.Frontend/app/components/ProfileField.tsx b/Foxnouns.Frontend/app/components/profile/ProfileField.tsx similarity index 89% rename from Foxnouns.Frontend/app/components/ProfileField.tsx rename to Foxnouns.Frontend/app/components/profile/ProfileField.tsx index ed5577d..92d8a46 100644 --- a/Foxnouns.Frontend/app/components/ProfileField.tsx +++ b/Foxnouns.Frontend/app/components/profile/ProfileField.tsx @@ -1,5 +1,5 @@ import { CustomPreference, FieldEntry, Pronoun } from "~/lib/api/user"; -import StatusLine from "~/components/StatusLine"; +import StatusLine from "~/components/profile/StatusLine"; export default function ProfileField({ name, diff --git a/Foxnouns.Frontend/app/components/profile/ProfileFlag.tsx b/Foxnouns.Frontend/app/components/profile/ProfileFlag.tsx new file mode 100644 index 0000000..756783d --- /dev/null +++ b/Foxnouns.Frontend/app/components/profile/ProfileFlag.tsx @@ -0,0 +1,28 @@ +import type { PrideFlag } from "~/lib/api/user"; +import { OverlayTrigger, Tooltip } from "react-bootstrap"; + +export default function ProfileFlag({ flag }: { flag: PrideFlag }) { + return ( + + + {flag.description ?? flag.name} + + } + > + + {flag.description + + {" "} + {flag.name} + + ); +} diff --git a/Foxnouns.Frontend/app/components/ProfileLink.tsx b/Foxnouns.Frontend/app/components/profile/ProfileLink.tsx similarity index 100% rename from Foxnouns.Frontend/app/components/ProfileLink.tsx rename to Foxnouns.Frontend/app/components/profile/ProfileLink.tsx diff --git a/Foxnouns.Frontend/app/components/PronounLink.tsx b/Foxnouns.Frontend/app/components/profile/PronounLink.tsx similarity index 100% rename from Foxnouns.Frontend/app/components/PronounLink.tsx rename to Foxnouns.Frontend/app/components/profile/PronounLink.tsx diff --git a/Foxnouns.Frontend/app/components/StatusIcon.tsx b/Foxnouns.Frontend/app/components/profile/StatusIcon.tsx similarity index 100% rename from Foxnouns.Frontend/app/components/StatusIcon.tsx rename to Foxnouns.Frontend/app/components/profile/StatusIcon.tsx diff --git a/Foxnouns.Frontend/app/components/StatusLine.tsx b/Foxnouns.Frontend/app/components/profile/StatusLine.tsx similarity index 89% rename from Foxnouns.Frontend/app/components/StatusLine.tsx rename to Foxnouns.Frontend/app/components/profile/StatusLine.tsx index 704df75..3729e8a 100644 --- a/Foxnouns.Frontend/app/components/StatusLine.tsx +++ b/Foxnouns.Frontend/app/components/profile/StatusLine.tsx @@ -7,8 +7,8 @@ import { Pronoun, } from "~/lib/api/user"; import classNames from "classnames"; -import StatusIcon from "~/components/StatusIcon"; -import PronounLink from "~/components/PronounLink"; +import StatusIcon from "~/components/profile/StatusIcon"; +import PronounLink from "~/components/profile/PronounLink"; export default function StatusLine({ entry, diff --git a/Foxnouns.Frontend/app/lib/api/member.ts b/Foxnouns.Frontend/app/lib/api/member.ts new file mode 100644 index 0000000..7827f0a --- /dev/null +++ b/Foxnouns.Frontend/app/lib/api/member.ts @@ -0,0 +1,7 @@ +import { Field, PartialMember, PrideFlag } from "~/lib/api/user"; + +export type Member = PartialMember & { + fields: Field[]; + flags: PrideFlag[]; + links: string[]; +}; diff --git a/Foxnouns.Frontend/app/lib/api/user.ts b/Foxnouns.Frontend/app/lib/api/user.ts index 5dc969c..0b6f375 100644 --- a/Foxnouns.Frontend/app/lib/api/user.ts +++ b/Foxnouns.Frontend/app/lib/api/user.ts @@ -13,6 +13,7 @@ export type User = PartialUser & { names: FieldEntry[]; pronouns: Pronoun[]; fields: Field[]; + flags: PrideFlag[]; }; export type UserWithMembers = User & { members: PartialMember[] }; @@ -50,6 +51,13 @@ export type Field = { entries: FieldEntry[]; }; +export type PrideFlag = { + id: string; + image_url: string; + name: string; + description: string | null; +}; + export type CustomPreference = { icon: string; tooltip: string; diff --git a/Foxnouns.Frontend/app/routes/$username/route.tsx b/Foxnouns.Frontend/app/routes/$username/route.tsx index c19b5e7..48aee01 100644 --- a/Foxnouns.Frontend/app/routes/$username/route.tsx +++ b/Foxnouns.Frontend/app/routes/$username/route.tsx @@ -6,12 +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/ProfileLink"; -import ProfileField from "~/components/ProfileField"; import { PersonPlusFill } from "react-bootstrap-icons"; -import { defaultAvatarUrl } from "~/lib/utils"; import MemberCard from "~/routes/$username/MemberCard"; import { ReactNode } from "react"; +import BaseProfile from "~/components/profile/BaseProfile"; export const meta: MetaFunction = ({ data }) => { const { user } = data!; @@ -44,7 +42,6 @@ export default function UserPage() { const { meUser } = useRouteLoaderData("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++) { @@ -82,70 +79,12 @@ export default function UserPage() { )} -
-
-
- {t("user.avatar-alt", -
-
- {user.display_name ? ( - <> -

{user.display_name}

-

@{user.username}

- - ) : ( - <> -

@{user.username}

- - )} - {bio && ( - <> -
-

- - )} -
- {user.links.length > 0 && ( -
-
    - {user.links.map((l, i) => ( - - ))} -
-
- )} -
-
- {user.names.length > 0 && ( - - )} - {user.pronouns.length > 0 && ( - - )} - {user.fields.map((f, i) => ( - - ))} -
-
+ {(members.length > 0 || isMeUser) && ( <>