you know what let's just change frontend framework again
This commit is contained in:
		
							parent
							
								
									c8cd483d20
								
							
						
					
					
						commit
						0d47f1fb01
					
				
					 115 changed files with 4407 additions and 10824 deletions
				
			
		|  | @ -0,0 +1,16 @@ | |||
| <script lang="ts"> | ||||
| 	import { t } from "$lib/i18n"; | ||||
| 
 | ||||
| 	type Props = { memberName?: string; editLink: string }; | ||||
| 	let { memberName, editLink }: Props = $props(); | ||||
| </script> | ||||
| 
 | ||||
| <div class="alert alert-secondary"> | ||||
| 	{#if memberName} | ||||
| 		{$t("profile.edit-member-profile-notice", { memberName })} | ||||
| 	{:else} | ||||
| 		{$t("profile.edit-user-profile-notice")} | ||||
| 	{/if} | ||||
| 	<br /> | ||||
| 	<a href={editLink}>{$t("profile.edit-profile-link")}</a> | ||||
| </div> | ||||
|  | @ -0,0 +1,26 @@ | |||
| <script lang="ts"> | ||||
| 	import type { CustomPreference, Member, User } from "$api/models"; | ||||
| 	import ProfileField from "./field/ProfileField.svelte"; | ||||
| 	import { t } from "$lib/i18n"; | ||||
| 
 | ||||
| 	type Props = { profile: User | Member; allPreferences: Record<string, CustomPreference> }; | ||||
| 	let { profile, allPreferences }: Props = $props(); | ||||
| </script> | ||||
| 
 | ||||
| <div class="row row-cols-1 row-cols-sm-2 row-cols-md-3"> | ||||
| 	{#if profile.names.length > 0} | ||||
| 		<ProfileField name={$t("profile.names-header")} entries={profile.names} {allPreferences} /> | ||||
| 	{/if} | ||||
| 	{#if profile.pronouns.length > 0} | ||||
| 		<ProfileField | ||||
| 			name={$t("profile.pronouns-header")} | ||||
| 			entries={profile.pronouns} | ||||
| 			{allPreferences} | ||||
| 		/> | ||||
| 	{/if} | ||||
| 	{#each profile.fields as field} | ||||
| 		{#if field.entries.length > 0} | ||||
| 			<ProfileField name={field.name} entries={field.entries} {allPreferences} /> | ||||
| 		{/if} | ||||
| 	{/each} | ||||
| </div> | ||||
|  | @ -0,0 +1,24 @@ | |||
| <script lang="ts"> | ||||
| 	import type { PrideFlag } from "$api/models/user"; | ||||
| 	import { Tooltip } from "@sveltestrap/sveltestrap"; | ||||
| 
 | ||||
| 	type Props = { flag: PrideFlag }; | ||||
| 	let { flag }: Props = $props(); | ||||
| 
 | ||||
| 	// svelte-ignore non_reactive_update | ||||
| 	let elem: HTMLImageElement; | ||||
| </script> | ||||
| 
 | ||||
| <span class="mx-2 my-1"> | ||||
| 	<Tooltip target={elem} aria-hidden placement="top">{flag.description ?? flag.name}</Tooltip> | ||||
| 	<img bind:this={elem} class="flag" src={flag.image_url} alt={flag.description ?? flag.name} /> | ||||
| 	{flag.name} | ||||
| </span> | ||||
| 
 | ||||
| <style> | ||||
| 	.flag { | ||||
| 		height: 1.5rem; | ||||
| 		max-width: 200px; | ||||
| 		border-radius: 3px; | ||||
| 	} | ||||
| </style> | ||||
|  | @ -0,0 +1,69 @@ | |||
| <script lang="ts"> | ||||
| 	import type { User, Member } from "$api/models"; | ||||
| 	import { t } from "$lib/i18n"; | ||||
| 	import { renderMarkdown } from "$lib/markdown"; | ||||
| 	import ProfileLink from "./ProfileLink.svelte"; | ||||
| 	import ProfileFlag from "./ProfileFlag.svelte"; | ||||
| 	import Avatar from "$components/Avatar.svelte"; | ||||
| 
 | ||||
| 	type Props = { | ||||
| 		name: string; | ||||
| 		profile: User | Member; | ||||
| 		lazyLoadAvatar?: boolean; | ||||
| 	}; | ||||
| 
 | ||||
| 	let { name, profile, lazyLoadAvatar }: Props = $props(); | ||||
| 
 | ||||
| 	// renderMarkdown sanitizes the output HTML for us | ||||
| 	let bio = $derived(renderMarkdown(profile.bio)); | ||||
| </script> | ||||
| 
 | ||||
| <div class="grid row-gap-3"> | ||||
| 	<div class="row"> | ||||
| 		<div class="col-md-4 text-center"> | ||||
| 			<Avatar | ||||
| 				url={profile.avatar_url} | ||||
| 				alt={$t("avatar-tooltip", { name })} | ||||
| 				lazyLoad={lazyLoadAvatar} | ||||
| 			/> | ||||
| 			<!-- Flags show up below the avatar if the profile has a bio, otherwise they show up below the row entirely --> | ||||
| 			{#if profile.flags && profile.bio} | ||||
| 				<div class="d-flex flex-wrap m-4"> | ||||
| 					{#each profile.flags as flag} | ||||
| 						<ProfileFlag {flag} /> | ||||
| 					{/each} | ||||
| 				</div> | ||||
| 			{/if} | ||||
| 		</div> | ||||
| 		<div class="col-md"> | ||||
| 			{#if profile.display_name} | ||||
| 				<div> | ||||
| 					<h2>{profile.display_name}</h2> | ||||
| 					<p class="fs-5 text-body-secondary">{name}</p> | ||||
| 				</div> | ||||
| 			{:else} | ||||
| 				<h2>{name}</h2> | ||||
| 			{/if} | ||||
| 			{#if bio} | ||||
| 				<hr /> | ||||
| 				<p>{@html bio}</p> | ||||
| 			{/if} | ||||
| 		</div> | ||||
| 		{#if profile.links.length > 0} | ||||
| 			<div class="col-md d-flex align-items-center"> | ||||
| 				<ul class="list-unstyled"> | ||||
| 					{#each profile.links as link} | ||||
| 						<ProfileLink {link} /> | ||||
| 					{/each} | ||||
| 				</ul> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| </div> | ||||
| {#if profile.flags && !profile.bio} | ||||
| 	<div class="d-flex flex-wrap m-4"> | ||||
| 		{#each profile.flags as flag} | ||||
| 			<ProfileFlag {flag} /> | ||||
| 		{/each} | ||||
| 	</div> | ||||
| {/if} | ||||
|  | @ -0,0 +1,33 @@ | |||
| <script lang="ts"> | ||||
| 	import { Icon } from "@sveltestrap/sveltestrap"; | ||||
| 
 | ||||
| 	type Props = { link: string }; | ||||
| 	let { link }: Props = $props(); | ||||
| 
 | ||||
| 	const prettifyLink = (raw: string) => { | ||||
| 		let out = raw; | ||||
| 		if (raw.startsWith("https://")) out = raw.substring("https://".length); | ||||
| 		else if (raw.startsWith("http://")) out = raw.substring("http://".length); | ||||
| 
 | ||||
| 		if (raw.endsWith("/")) out = raw.substring(0, raw.length - 1); | ||||
| 
 | ||||
| 		return out; | ||||
| 	}; | ||||
| 
 | ||||
| 	let isLink = $derived(link.startsWith("http://") || link.startsWith("https://")); | ||||
| 	let displayLink = $derived(prettifyLink(link)); | ||||
| </script> | ||||
| 
 | ||||
| {#if isLink} | ||||
| 	<a href={link} class="text-decoration-none" rel="me nofollow noreferrer" target="_blank"> | ||||
| 		<li class="py-2 py-lg-0"> | ||||
| 			<Icon name="globe" aria-hidden class="text-body" /> | ||||
| 			<span class="text-decoration-underline">{displayLink}</span> | ||||
| 		</li> | ||||
| 	</a> | ||||
| {:else} | ||||
| 	<li class="py-2 py-lg-0"> | ||||
| 		<Icon name="globe" aria-hidden /> | ||||
| 		<span>{displayLink}</span> | ||||
| 	</li> | ||||
| {/if} | ||||
|  | @ -0,0 +1,30 @@ | |||
| <script lang="ts"> | ||||
| 	import type { CustomPreference, FieldEntry, Pronoun } from "$api/models"; | ||||
| 	import ProfileFieldEntry from "./ProfileFieldEntry.svelte"; | ||||
| 	import PronounLink from "./PronounLink.svelte"; | ||||
| 
 | ||||
| 	type Props = { | ||||
| 		name: string; | ||||
| 		entries: Array<FieldEntry | Pronoun>; | ||||
| 		allPreferences: Record<string, CustomPreference>; | ||||
| 		isCol?: boolean; | ||||
| 	}; | ||||
| 	let { name, entries, allPreferences, isCol }: Props = $props(); | ||||
| </script> | ||||
| 
 | ||||
| <div class:col={isCol === false}> | ||||
| 	<h3>{name}</h3> | ||||
| 	<ul class="list-unstyled fs-5"> | ||||
| 		{#each entries as entry} | ||||
| 			<li> | ||||
| 				<ProfileFieldEntry status={entry.status} {allPreferences}> | ||||
| 					{#if "display_text" in entry} | ||||
| 						<PronounLink pronouns={entry} /> | ||||
| 					{:else} | ||||
| 						{entry.value} | ||||
| 					{/if} | ||||
| 				</ProfileFieldEntry> | ||||
| 			</li> | ||||
| 		{/each} | ||||
| 	</ul> | ||||
| </div> | ||||
|  | @ -0,0 +1,28 @@ | |||
| <script lang="ts"> | ||||
| 	import { defaultPreferences, PreferenceSize, type CustomPreference } from "$api/models"; | ||||
| 	import StatusIcon from "$components/StatusIcon.svelte"; | ||||
| 	import type { Snippet } from "svelte"; | ||||
| 
 | ||||
| 	type Props = { | ||||
| 		status: string; | ||||
| 		allPreferences: Record<string, CustomPreference>; | ||||
| 		children: Snippet; | ||||
| 	}; | ||||
| 	let { status, allPreferences, children }: Props = $props(); | ||||
| 
 | ||||
| 	let preference = $derived( | ||||
| 		status in allPreferences ? allPreferences[status] : defaultPreferences.missing, | ||||
| 	); | ||||
| 
 | ||||
| 	let elemType = $derived(preference.size === PreferenceSize.Large ? "strong" : "span"); | ||||
| </script> | ||||
| 
 | ||||
| <svelte:element | ||||
| 	this={elemType} | ||||
| 	class:text-muted={preference.muted} | ||||
| 	class:fs-5={preference.size === PreferenceSize.Large} | ||||
| 	class:fs-6={preference.size === PreferenceSize.Small} | ||||
| > | ||||
| 	<StatusIcon {preference} /> | ||||
| 	{@render children?.()} | ||||
| </svelte:element> | ||||
|  | @ -0,0 +1,41 @@ | |||
| <script lang="ts"> | ||||
| 	import type { Pronoun } from "$api/models/user"; | ||||
| 
 | ||||
| 	type Props = { pronouns: Pronoun }; | ||||
| 	let { pronouns }: Props = $props(); | ||||
| 
 | ||||
| 	// TODO: this entire component is only made with English pronouns in mind. | ||||
| 	// It's gonna need a major rework to work with other languages. | ||||
| 	const updatePronouns = (pronouns: Pronoun) => { | ||||
| 		if (pronouns.display_text) { | ||||
| 			return pronouns.display_text; | ||||
| 		} else { | ||||
| 			const split = pronouns.value.split("/"); | ||||
| 			if (split.length === 5) return split.splice(0, 2).join("/"); | ||||
| 			return pronouns.value; | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	const linkPronouns = (pronouns: Pronoun) => { | ||||
| 		const linkBase = pronouns.value | ||||
| 			.split("/") | ||||
| 			.map((snippet) => encodeURIComponent(snippet)) | ||||
| 			.join("/"); | ||||
| 
 | ||||
| 		if (pronouns.display_text) { | ||||
| 			return `${linkBase},${encodeURIComponent(pronouns.display_text)}`; | ||||
| 		} | ||||
| 		return linkBase; | ||||
| 	}; | ||||
| 
 | ||||
| 	let pronounText = $derived(updatePronouns(pronouns)); | ||||
| 
 | ||||
| 	let link = $derived(linkPronouns(pronouns)); | ||||
| 	let shouldLink = $derived(pronouns.value.split("/").length === 5); | ||||
| </script> | ||||
| 
 | ||||
| {#if shouldLink} | ||||
| 	<a class="text-reset" href="/pronouns/{link}">{pronounText}</a> | ||||
| {:else} | ||||
| 	{pronounText} | ||||
| {/if} | ||||
|  | @ -0,0 +1,49 @@ | |||
| <script lang="ts"> | ||||
| 	import type { CustomPreference, PartialMember } from "$api/models"; | ||||
| 	import Avatar from "$components/Avatar.svelte"; | ||||
| 	import { t } from "$lib/i18n"; | ||||
| 
 | ||||
| 	type Props = { | ||||
| 		username: string; | ||||
| 		member: PartialMember; | ||||
| 		allPreferences: Record<string, CustomPreference>; | ||||
| 	}; | ||||
| 	let { username, member, allPreferences }: Props = $props(); | ||||
| 
 | ||||
| 	const getPronouns = (member: PartialMember) => { | ||||
| 		const filteredPronouns = member.pronouns.filter( | ||||
| 			(entry) => (allPreferences[entry.status] || { favourite: false }).favourite, | ||||
| 		); | ||||
| 		if (filteredPronouns.length === 0) { | ||||
| 			return undefined; | ||||
| 		} | ||||
| 		return filteredPronouns | ||||
| 			.map((pronouns) => { | ||||
| 				if (pronouns.display_text) { | ||||
| 					return pronouns.display_text; | ||||
| 				} else { | ||||
| 					const split = pronouns.value.split("/"); | ||||
| 					if (split.length === 5) return split.splice(0, 2).join("/"); | ||||
| 					return pronouns.value; | ||||
| 				} | ||||
| 			}) | ||||
| 			.join(", "); | ||||
| 	}; | ||||
| 
 | ||||
| 	let pronouns = $derived(getPronouns(member)); | ||||
| </script> | ||||
| 
 | ||||
| <div> | ||||
| 	<a href="/@{username}/{member.name}"> | ||||
| 		<Avatar url={member.avatar_url} lazyLoad alt={$t("avatar-tooltip", { name: member.name })} /> | ||||
| 	</a> | ||||
| 	<p class="m-2"> | ||||
| 		<a class="text-reset fs-5 text-break" href="/@{username}/{member.name}"> | ||||
| 			{member.name} | ||||
| 		</a> | ||||
| 		{#if pronouns} | ||||
| 			<br /> | ||||
| 			{pronouns} | ||||
| 		{/if} | ||||
| 	</p> | ||||
| </div> | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue