feat: paginate member list, add create member button
This commit is contained in:
parent
9bfabcc1f1
commit
3678f5a3e8
6 changed files with 120 additions and 34 deletions
15
frontend/src/lib/components/ErrorAlert.svelte
Normal file
15
frontend/src/lib/components/ErrorAlert.svelte
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { APIError } from "$lib/api/entities";
|
||||||
|
import { Alert } from "sveltestrap";
|
||||||
|
|
||||||
|
export let error: APIError;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Alert color="danger" fade={false}>
|
||||||
|
<h4 class="alert-heading">An error occurred</h4>
|
||||||
|
<b>{error.code}:</b>
|
||||||
|
{error.message}
|
||||||
|
{#if error.details}
|
||||||
|
({error.details})
|
||||||
|
{/if}
|
||||||
|
</Alert>
|
|
@ -4,15 +4,34 @@
|
||||||
|
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
import { Alert, Icon } from "sveltestrap";
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Icon,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
} from "sveltestrap";
|
||||||
import FieldCard from "$lib/components/FieldCard.svelte";
|
import FieldCard from "$lib/components/FieldCard.svelte";
|
||||||
import StatusIcon from "$lib/components/StatusIcon.svelte";
|
import StatusIcon from "$lib/components/StatusIcon.svelte";
|
||||||
import PronounLink from "$lib/components/PronounLink.svelte";
|
import PronounLink from "$lib/components/PronounLink.svelte";
|
||||||
import PartialMemberCard from "$lib/components/PartialMemberCard.svelte";
|
import PartialMemberCard from "$lib/components/PartialMemberCard.svelte";
|
||||||
import FallbackImage from "$lib/components/FallbackImage.svelte";
|
import FallbackImage from "$lib/components/FallbackImage.svelte";
|
||||||
import { userStore } from "$lib/store";
|
import { userStore } from "$lib/store";
|
||||||
import { pronounDisplay, userAvatars, WordStatus, type Member } from "$lib/api/entities";
|
import {
|
||||||
|
MAX_MEMBERS,
|
||||||
|
pronounDisplay,
|
||||||
|
userAvatars,
|
||||||
|
WordStatus,
|
||||||
|
type APIError,
|
||||||
|
type Member,
|
||||||
|
type PartialMember,
|
||||||
|
} from "$lib/api/entities";
|
||||||
import { PUBLIC_BASE_URL } from "$env/static/public";
|
import { PUBLIC_BASE_URL } from "$env/static/public";
|
||||||
|
import { apiFetchClient } from "$lib/api/fetch";
|
||||||
|
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
|
@ -20,8 +39,44 @@
|
||||||
$: bio = data.bio ? sanitizeHtml(marked.parse(data.bio)) : null;
|
$: bio = data.bio ? sanitizeHtml(marked.parse(data.bio)) : null;
|
||||||
|
|
||||||
let memberPage: number = 0;
|
let memberPage: number = 0;
|
||||||
let memberSlice: Member[] = [];
|
let memberSlice: PartialMember[] = [];
|
||||||
$: member = data.members.slice(memberPage * 20, memberPage + 1 * 20);
|
$: memberSlice = data.members.slice(memberPage * 20, (memberPage + 1) * 20);
|
||||||
|
const totalPages = Math.floor(data.members.length / 20) + 1;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
const createMember = async () => {
|
||||||
|
try {
|
||||||
|
const member = await apiFetchClient<Member>("/members", "POST", {
|
||||||
|
name: newMemberName,
|
||||||
|
});
|
||||||
|
|
||||||
|
newMemberName = "";
|
||||||
|
newMemberError = null;
|
||||||
|
data.members = [...data.members, member];
|
||||||
|
|
||||||
|
toggleModal();
|
||||||
|
} catch (e) {
|
||||||
|
newMemberError = e as APIError;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const favNames = data.names.filter((entry) => entry.status === WordStatus.Favourite);
|
const favNames = data.names.filter((entry) => entry.status === WordStatus.Favourite);
|
||||||
const favPronouns = data.pronouns.filter((entry) => entry.status === WordStatus.Favourite);
|
const favPronouns = data.pronouns.filter((entry) => entry.status === WordStatus.Favourite);
|
||||||
|
@ -97,15 +152,49 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<hr />
|
<hr />
|
||||||
<h2>Members</h2>
|
<h2>
|
||||||
|
Members
|
||||||
|
{#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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 text-center">
|
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 text-center">
|
||||||
{#each data.members as member}
|
{#each memberSlice as member}
|
||||||
<PartialMemberCard user={data} {member} />
|
<PartialMemberCard user={data} {member} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
<Modal header="Create member" isOpen={modalOpen} toggle={toggleModal}>
|
||||||
|
<ModalBody>
|
||||||
|
<Input bind:value={newMemberName} />
|
||||||
|
{#if newMemberError}
|
||||||
|
<ErrorAlert error={newMemberError} />
|
||||||
|
{/if}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button color="primary" on:click={createMember} disabled={newMemberName.length === 0}
|
||||||
|
>Create member</Button
|
||||||
|
>
|
||||||
|
<Button color="secondary" on:click={toggleModal}>Cancel</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import { apiFetch } from "$lib/api/fetch";
|
import { apiFetch } from "$lib/api/fetch";
|
||||||
import { userStore } from "$lib/store";
|
import { userStore } from "$lib/store";
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
|
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
|
||||||
|
|
||||||
interface SignupResponse {
|
interface SignupResponse {
|
||||||
user: MeUser;
|
user: MeUser;
|
||||||
|
@ -74,11 +75,7 @@
|
||||||
<h1>Log in with Discord</h1>
|
<h1>Log in with Discord</h1>
|
||||||
|
|
||||||
{#if data.error}
|
{#if data.error}
|
||||||
<Alert color="danger" fade={false}>
|
<ErrorAlert error={data.error} />
|
||||||
<h4 class="alert-heading">An error occurred</h4>
|
|
||||||
<b>{data.error.code}:</b>
|
|
||||||
{data.error.message}
|
|
||||||
</Alert>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if data.ticket}
|
{#if data.ticket}
|
||||||
<form on:submit|preventDefault={signupForm}>
|
<form on:submit|preventDefault={signupForm}>
|
||||||
|
@ -126,10 +123,7 @@
|
||||||
</Alert>
|
</Alert>
|
||||||
{/if}
|
{/if}
|
||||||
{#if deleteError}
|
{#if deleteError}
|
||||||
<Alert color="danger" fade={false}>
|
<ErrorAlert error={deleteError} />
|
||||||
<h4 class="alert-heading">An error occurred</h4>
|
|
||||||
<b>{deleteError.code}</b>: {deleteError.message}
|
|
||||||
</Alert>
|
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
Loading...
|
Loading...
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
import EditableField from "../EditableField.svelte";
|
import EditableField from "../EditableField.svelte";
|
||||||
import EditableName from "../EditableName.svelte";
|
import EditableName from "../EditableName.svelte";
|
||||||
import EditablePronouns from "../EditablePronouns.svelte";
|
import EditablePronouns from "../EditablePronouns.svelte";
|
||||||
|
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
|
||||||
|
|
||||||
const MAX_AVATAR_BYTES = 1_000_000;
|
const MAX_AVATAR_BYTES = 1_000_000;
|
||||||
|
|
||||||
|
@ -250,13 +251,7 @@
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<Alert color="danger" fade={false}>
|
<ErrorAlert {error} />
|
||||||
<h4 class="alert-header">An error occurred</h4>
|
|
||||||
<p>
|
|
||||||
<b>{error.code}</b>: {error.message}
|
|
||||||
{#if error.details}{error.details}{/if}
|
|
||||||
</p>
|
|
||||||
</Alert>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !$userStore}
|
{#if !$userStore}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { type MeUser, userAvatars, type APIError, MAX_MEMBERS } from "$lib/api/entities";
|
import { type MeUser, userAvatars, type APIError, MAX_MEMBERS } from "$lib/api/entities";
|
||||||
import { apiFetchClient } from "$lib/api/fetch";
|
import { apiFetchClient } from "$lib/api/fetch";
|
||||||
|
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
|
||||||
import FallbackImage from "$lib/components/FallbackImage.svelte";
|
import FallbackImage from "$lib/components/FallbackImage.svelte";
|
||||||
import { userStore } from "$lib/store";
|
import { userStore } from "$lib/store";
|
||||||
import {
|
import {
|
||||||
|
@ -77,10 +78,7 @@
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if error}
|
{#if error}
|
||||||
<Alert color="danger" fade={false}>
|
<ErrorAlert {error} />
|
||||||
<h5 class="alert-heading">An error occurred</h5>
|
|
||||||
<b>{error.code}</b>: {error.message}
|
|
||||||
</Alert>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
|
@ -137,10 +135,7 @@
|
||||||
<input type="text" class="form-control" bind:value={deleteUsername} />
|
<input type="text" class="form-control" bind:value={deleteUsername} />
|
||||||
</p>
|
</p>
|
||||||
{#if deleteError}
|
{#if deleteError}
|
||||||
<Alert color="danger" fade={false}>
|
<ErrorAlert error={deleteError} />
|
||||||
<h5 class="alert-heading">An error occurred</h5>
|
|
||||||
<b>{deleteError.code}</b>: {deleteError.message}
|
|
||||||
</Alert>
|
|
||||||
{/if}
|
{/if}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { APIError, Invite } from "$lib/api/entities";
|
import type { APIError, Invite } from "$lib/api/entities";
|
||||||
import { apiFetchClient } from "$lib/api/fetch";
|
import { apiFetchClient } from "$lib/api/fetch";
|
||||||
|
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
|
||||||
import { Alert, Button, Modal, Table } from "sveltestrap";
|
import { Alert, Button, Modal, Table } from "sveltestrap";
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
|
@ -54,10 +55,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
{#if error}
|
{#if error}
|
||||||
<Alert color="danger" fade={false}>
|
<ErrorAlert {error} />
|
||||||
<h4 class="alert-heading">An error occurred</h4>
|
|
||||||
<b>{error.code}</b>: {error.message}
|
|
||||||
</Alert>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue