2023-03-11 01:36:30 +01:00
|
|
|
<script lang="ts">
|
|
|
|
import type { PageData } from "./$types";
|
2023-03-12 05:04:15 +01:00
|
|
|
|
2023-03-14 16:43:31 +01:00
|
|
|
import {
|
|
|
|
Alert,
|
|
|
|
Button,
|
|
|
|
ButtonGroup,
|
|
|
|
Icon,
|
|
|
|
Input,
|
|
|
|
Modal,
|
|
|
|
ModalBody,
|
|
|
|
ModalFooter,
|
|
|
|
} from "sveltestrap";
|
2023-03-12 05:04:15 +01:00
|
|
|
import FieldCard from "$lib/components/FieldCard.svelte";
|
2023-03-11 16:52:48 +01:00
|
|
|
import PronounLink from "$lib/components/PronounLink.svelte";
|
|
|
|
import PartialMemberCard from "$lib/components/PartialMemberCard.svelte";
|
|
|
|
import FallbackImage from "$lib/components/FallbackImage.svelte";
|
2023-03-12 15:59:20 +01:00
|
|
|
import { userStore } from "$lib/store";
|
2023-03-14 16:43:31 +01:00
|
|
|
import {
|
|
|
|
MAX_MEMBERS,
|
|
|
|
pronounDisplay,
|
|
|
|
userAvatars,
|
|
|
|
type APIError,
|
|
|
|
type Member,
|
|
|
|
type PartialMember,
|
2023-04-20 09:12:44 +02:00
|
|
|
type CustomPreferences,
|
|
|
|
type FieldEntry,
|
|
|
|
type Pronoun,
|
2023-03-14 16:43:31 +01:00
|
|
|
} from "$lib/api/entities";
|
2023-03-14 03:01:26 +01:00
|
|
|
import { PUBLIC_BASE_URL } from "$env/static/public";
|
2023-03-14 16:43:31 +01:00
|
|
|
import { apiFetchClient } from "$lib/api/fetch";
|
|
|
|
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
|
2023-03-14 17:06:35 +01:00
|
|
|
import { goto } from "$app/navigation";
|
2023-04-02 23:08:44 +02:00
|
|
|
import { renderMarkdown } from "$lib/utils";
|
2023-03-23 15:20:07 +01:00
|
|
|
import ReportButton from "./ReportButton.svelte";
|
2023-03-26 00:07:51 +01:00
|
|
|
import ProfileLink from "./ProfileLink.svelte";
|
2023-03-30 23:10:13 +02:00
|
|
|
import { memberNameRegex } from "$lib/api/regex";
|
|
|
|
import StatusLine from "$lib/components/StatusLine.svelte";
|
2023-04-20 09:12:44 +02:00
|
|
|
import defaultPreferences from "$lib/api/default_preferences";
|
2023-03-11 01:36:30 +01:00
|
|
|
|
|
|
|
export let data: PageData;
|
|
|
|
|
|
|
|
let bio: string | null;
|
2023-03-23 10:05:17 +01:00
|
|
|
$: bio = renderMarkdown(data.bio);
|
2023-03-14 03:01:26 +01:00
|
|
|
|
2023-04-20 09:29:47 +02:00
|
|
|
let memberPage = 0;
|
2023-03-14 16:43:31 +01:00
|
|
|
let memberSlice: PartialMember[] = [];
|
|
|
|
$: memberSlice = data.members.slice(memberPage * 20, (memberPage + 1) * 20);
|
2023-04-03 23:32:34 +02:00
|
|
|
const totalPages = Math.ceil(data.members.length / 20);
|
2023-03-14 16:43:31 +01:00
|
|
|
|
|
|
|
const prevPage = () => {
|
|
|
|
if (memberPage === 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
memberPage = memberPage - 1;
|
|
|
|
};
|
|
|
|
|
|
|
|
const nextPage = () => {
|
|
|
|
if ((memberPage + 1) * 20 > data.members.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
memberPage = memberPage + 1;
|
|
|
|
};
|
|
|
|
|
|
|
|
let modalOpen = false;
|
|
|
|
let toggleModal = () => (modalOpen = !modalOpen);
|
|
|
|
let newMemberName = "";
|
|
|
|
let newMemberError: APIError | null = null;
|
2023-03-27 01:23:04 +02:00
|
|
|
let memberNameValid = true;
|
|
|
|
$: memberNameValid = memberNameRegex.test(newMemberName);
|
2023-03-14 16:43:31 +01:00
|
|
|
|
|
|
|
const createMember = async () => {
|
|
|
|
try {
|
|
|
|
const member = await apiFetchClient<Member>("/members", "POST", {
|
|
|
|
name: newMemberName,
|
|
|
|
});
|
|
|
|
|
|
|
|
newMemberName = "";
|
|
|
|
newMemberError = null;
|
|
|
|
data.members = [...data.members, member];
|
|
|
|
|
|
|
|
toggleModal();
|
2023-03-14 17:06:35 +01:00
|
|
|
|
|
|
|
goto(`/@${data.name}/${member.name}`);
|
2023-03-14 16:43:31 +01:00
|
|
|
} catch (e) {
|
|
|
|
newMemberError = e as APIError;
|
|
|
|
}
|
|
|
|
};
|
2023-03-14 14:27:54 +01:00
|
|
|
|
2023-04-20 09:12:44 +02:00
|
|
|
let mergedPreferences: CustomPreferences;
|
|
|
|
$: mergedPreferences = Object.assign(defaultPreferences, data.custom_preferences);
|
|
|
|
|
|
|
|
let favNames: FieldEntry[];
|
|
|
|
$: favNames = data.names.filter(
|
|
|
|
(entry) => (mergedPreferences[entry.status] || { favourite: false }).favourite,
|
|
|
|
);
|
|
|
|
let favPronouns: Pronoun[];
|
|
|
|
$: favPronouns = data.pronouns.filter(
|
|
|
|
(entry) => (mergedPreferences[entry.status] || { favourite: false }).favourite,
|
|
|
|
);
|
2023-03-27 00:44:55 +02:00
|
|
|
|
|
|
|
let profileEmpty = false;
|
|
|
|
$: profileEmpty =
|
|
|
|
data.names.length === 0 &&
|
|
|
|
data.pronouns.length === 0 &&
|
|
|
|
data.fields.length === 0 &&
|
|
|
|
(!data.bio || data.bio.length === 0);
|
2023-03-11 01:36:30 +01:00
|
|
|
</script>
|
|
|
|
|
|
|
|
<div class="container">
|
2023-03-12 15:59:20 +01:00
|
|
|
{#if $userStore && $userStore.id === data.id}
|
|
|
|
<Alert color="secondary" fade={false}>
|
|
|
|
You are currently viewing your <strong>public</strong> profile.
|
|
|
|
<br /><a href="/edit/profile">Edit your profile</a>
|
|
|
|
</Alert>
|
|
|
|
{/if}
|
|
|
|
<div class="grid row-gap-3">
|
2023-03-11 01:36:30 +01:00
|
|
|
<div class="row">
|
2023-03-14 17:19:10 +01:00
|
|
|
<div class="col-md-4 text-center">
|
2023-03-13 02:04:09 +01:00
|
|
|
<FallbackImage width={200} urls={userAvatars(data)} alt="Avatar for @{data.name}" />
|
2023-03-11 16:52:48 +01:00
|
|
|
</div>
|
2023-03-12 15:59:20 +01:00
|
|
|
<div class="col-md">
|
2023-03-11 01:36:30 +01:00
|
|
|
{#if data.display_name}
|
2023-03-12 15:59:20 +01:00
|
|
|
<div>
|
|
|
|
<h2>{data.display_name}</h2>
|
2023-03-27 15:48:03 +02:00
|
|
|
<p class="fs-5 text-body-secondary">@{data.name}</p>
|
2023-03-12 15:59:20 +01:00
|
|
|
</div>
|
2023-03-11 01:36:30 +01:00
|
|
|
{:else}
|
|
|
|
<h2>@{data.name}</h2>
|
|
|
|
{/if}
|
2023-03-27 00:44:55 +02:00
|
|
|
{#if profileEmpty && $userStore?.id === data.id}
|
|
|
|
<hr />
|
|
|
|
<p>
|
|
|
|
<em>
|
|
|
|
Your profile is empty! You can customize it by going to the <a href="/edit/profile"
|
|
|
|
>edit profile</a
|
|
|
|
> page.</em
|
|
|
|
> <span class="text-muted">(only you can see this)</span>
|
|
|
|
</p>
|
|
|
|
{:else if bio}
|
2023-03-12 15:59:20 +01:00
|
|
|
<hr />
|
2023-03-11 01:36:30 +01:00
|
|
|
<p>{@html bio}</p>
|
|
|
|
{/if}
|
2023-03-12 15:59:20 +01:00
|
|
|
</div>
|
|
|
|
{#if data.links.length > 0}
|
|
|
|
<div class="col-md d-flex align-items-center">
|
2023-03-12 05:04:15 +01:00
|
|
|
<ul class="list-unstyled">
|
2023-03-11 01:36:30 +01:00
|
|
|
{#each data.links as link}
|
2023-03-27 23:46:06 +02:00
|
|
|
<ProfileLink {link} />
|
2023-03-11 01:36:30 +01:00
|
|
|
{/each}
|
|
|
|
</ul>
|
2023-03-12 15:59:20 +01:00
|
|
|
</div>
|
|
|
|
{/if}
|
2023-03-11 01:36:30 +01:00
|
|
|
</div>
|
2023-03-12 15:59:20 +01:00
|
|
|
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3">
|
|
|
|
{#if data.names.length > 0}
|
|
|
|
<div class="col-md">
|
2023-03-14 14:27:54 +01:00
|
|
|
<h3>Names</h3>
|
|
|
|
<ul class="list-unstyled fs-5">
|
2023-03-12 15:59:20 +01:00
|
|
|
{#each data.names as name}
|
2023-04-20 09:12:44 +02:00
|
|
|
<li>
|
|
|
|
<StatusLine preferences={data.custom_preferences} status={name.status}
|
|
|
|
>{name.value}</StatusLine
|
|
|
|
>
|
|
|
|
</li>
|
2023-03-12 15:59:20 +01:00
|
|
|
{/each}
|
|
|
|
</ul>
|
|
|
|
</div>
|
|
|
|
{/if}
|
|
|
|
{#if data.pronouns.length > 0}
|
|
|
|
<div class="col-md">
|
2023-03-14 14:27:54 +01:00
|
|
|
<h3>Pronouns</h3>
|
|
|
|
<ul class="list-unstyled fs-5">
|
2023-03-12 15:59:20 +01:00
|
|
|
{#each data.pronouns as pronouns}
|
2023-04-20 09:12:44 +02:00
|
|
|
<li>
|
|
|
|
<StatusLine preferences={data.custom_preferences} status={pronouns.status}
|
|
|
|
><PronounLink {pronouns} /></StatusLine
|
|
|
|
>
|
|
|
|
</li>
|
2023-03-12 15:59:20 +01:00
|
|
|
{/each}
|
|
|
|
</ul>
|
|
|
|
</div>
|
|
|
|
{/if}
|
|
|
|
{#each data.fields as field}
|
|
|
|
<div class="col">
|
2023-04-19 12:24:34 +02:00
|
|
|
<FieldCard preferences={data.custom_preferences} {field} />
|
2023-03-12 15:59:20 +01:00
|
|
|
</div>
|
|
|
|
{/each}
|
|
|
|
</div>
|
2023-03-23 15:20:07 +01:00
|
|
|
{#if $userStore && $userStore.id !== data.id}
|
|
|
|
<div class="row">
|
|
|
|
<ReportButton subject="user" reportUrl="/users/{data.id}/reports" />
|
|
|
|
</div>
|
|
|
|
{/if}
|
2023-03-14 17:11:09 +01:00
|
|
|
{#if data.members.length > 0 || ($userStore && $userStore.id === data.id)}
|
2023-03-12 15:59:20 +01:00
|
|
|
<div class="row">
|
|
|
|
<div class="col">
|
|
|
|
<hr />
|
2023-03-14 16:43:31 +01:00
|
|
|
<h2>
|
2023-04-02 01:29:19 +02:00
|
|
|
{data.member_title || "Members"}
|
2023-03-14 16:43:31 +01:00
|
|
|
{#if $userStore && $userStore.id === data.id}
|
|
|
|
<Button
|
|
|
|
color="success"
|
|
|
|
disabled={data.members.length >= MAX_MEMBERS}
|
|
|
|
on:click={toggleModal}><Icon name="person-plus-fill" /> Create member</Button
|
|
|
|
>
|
|
|
|
{/if}
|
|
|
|
{#if totalPages > 1}
|
|
|
|
<ButtonGroup>
|
|
|
|
<Button on:click={prevPage} disabled={memberPage === 0}
|
|
|
|
><Icon name="chevron-left" /> Previous page</Button
|
|
|
|
>
|
|
|
|
<Button disabled>Page {memberPage + 1}/{totalPages}</Button>
|
|
|
|
<Button on:click={nextPage} disabled={memberPage === totalPages - 1}
|
|
|
|
>Next page <Icon name="chevron-right" /></Button
|
|
|
|
>
|
|
|
|
</ButtonGroup>
|
|
|
|
{/if}
|
|
|
|
</h2>
|
2023-03-12 15:59:20 +01:00
|
|
|
</div>
|
2023-03-12 04:25:53 +01:00
|
|
|
</div>
|
2023-03-27 00:44:55 +02:00
|
|
|
{#if data.members.length > 0}
|
|
|
|
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 text-center">
|
|
|
|
{#each memberSlice as member}
|
|
|
|
<PartialMemberCard user={data} {member} />
|
|
|
|
{/each}
|
|
|
|
</div>
|
|
|
|
{:else}
|
|
|
|
<div>
|
|
|
|
<p>
|
|
|
|
You don't have any members yet.
|
|
|
|
<br />
|
2023-03-27 01:23:04 +02:00
|
|
|
Members are sub-profiles that can have their own avatar, names, pronouns, and preferred terms.
|
2023-04-02 23:08:44 +02:00
|
|
|
<br />
|
|
|
|
If you were expecting to see members here, check your
|
|
|
|
<a href="/settings/members">list of hidden members</a>.
|
2023-03-27 01:23:04 +02:00
|
|
|
<span class="text-muted">(only you can see this)</span>
|
2023-03-27 00:44:55 +02:00
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
{/if}
|
2023-03-12 04:25:53 +01:00
|
|
|
{/if}
|
2023-03-14 16:43:31 +01:00
|
|
|
<Modal header="Create member" isOpen={modalOpen} toggle={toggleModal}>
|
|
|
|
<ModalBody>
|
|
|
|
<Input bind:value={newMemberName} />
|
2023-03-27 01:23:04 +02:00
|
|
|
<p class="text-muted my-2">
|
|
|
|
<Icon name="info-circle-fill" aria-label="Info" /> Your members must have distinct names. Member
|
|
|
|
names must be 100 characters long at most, and cannot contain the following characters: @ ?
|
|
|
|
! # / \ [ ] " ' $ % & ( ) + < = > ^ | ~ ` and ,
|
|
|
|
</p>
|
2023-03-14 16:43:31 +01:00
|
|
|
{#if newMemberError}
|
|
|
|
<ErrorAlert error={newMemberError} />
|
|
|
|
{/if}
|
|
|
|
</ModalBody>
|
|
|
|
<ModalFooter>
|
2023-03-27 01:23:04 +02:00
|
|
|
{#if !memberNameValid && newMemberName.length > 0}
|
|
|
|
<span class="text-danger-emphasis mb-2">That member name is not valid.</span>
|
|
|
|
{/if}
|
|
|
|
<Button color="primary" on:click={createMember} disabled={!memberNameValid}
|
2023-03-14 16:43:31 +01:00
|
|
|
>Create member</Button
|
|
|
|
>
|
|
|
|
<Button color="secondary" on:click={toggleModal}>Cancel</Button>
|
|
|
|
</ModalFooter>
|
|
|
|
</Modal>
|
2023-03-12 04:25:53 +01:00
|
|
|
</div>
|
2023-03-11 01:36:30 +01:00
|
|
|
</div>
|
2023-03-14 14:27:54 +01:00
|
|
|
|
|
|
|
<svelte:head>
|
|
|
|
<title>@{data.name} - pronouns.cc</title>
|
|
|
|
|
|
|
|
<meta
|
|
|
|
property="og:title"
|
|
|
|
content={data.display_name ? `${data.display_name} (@${data.name})` : `@${data.name}`}
|
|
|
|
/>
|
|
|
|
<meta property="og:url" content="{PUBLIC_BASE_URL}/@{data.name}" />
|
2023-03-28 00:52:44 +02:00
|
|
|
<meta name="description" content="@{data.name} on pronouns.cc" />
|
2023-04-08 14:48:03 +02:00
|
|
|
<meta name="robots" content="noindex" />
|
2023-03-14 14:27:54 +01:00
|
|
|
|
|
|
|
{#if data.avatar}
|
|
|
|
<meta property="og:image" content={userAvatars(data)[0]} />
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
{#if favNames.length !== 0 && favPronouns.length !== 0}
|
|
|
|
<meta
|
|
|
|
property="og:description"
|
|
|
|
content="@{data.name} goes by {favNames.map((x) => x.value).join(', ')} and uses {favPronouns
|
|
|
|
.map((x) => pronounDisplay(x))
|
|
|
|
.join(', ')} pronouns."
|
|
|
|
/>
|
|
|
|
{:else if favNames.length !== 0}
|
|
|
|
<meta
|
|
|
|
property="og:description"
|
|
|
|
content="@{data.name} goes by {favNames.map((x) => x.value).join(', ')}."
|
|
|
|
/>
|
|
|
|
{:else if favPronouns.length !== 0}
|
|
|
|
<meta
|
|
|
|
property="og:description"
|
|
|
|
content="@{data.name} uses {favPronouns.map((x) => pronounDisplay(x)).join(', ')} pronouns."
|
|
|
|
/>
|
|
|
|
{:else if data.bio && data.bio !== ""}
|
|
|
|
<meta
|
|
|
|
property="og:description"
|
2023-03-14 22:26:21 +01:00
|
|
|
content="{data.bio.slice(0, 500)}{data.bio.length > 500 ? '…' : ''}"
|
2023-03-14 14:27:54 +01:00
|
|
|
/>
|
|
|
|
{:else}
|
|
|
|
<meta property="og:description" content="@{data.name} on pronouns.cc" />
|
|
|
|
{/if}
|
|
|
|
</svelte:head>
|