Merge branch 'main' of github/Draconizations/pk-webs-svelte into feat/dashboard

This commit is contained in:
spiral
2022-05-16 23:02:18 -04:00
56 changed files with 6420 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
<script lang="ts">
import { Modal, CardHeader, CardTitle, Image, Spinner } from 'sveltestrap';
import default_avatar from '../assets/default_avatar.png';
import { toHTML } from 'discord-markdown';
import twemoji from 'twemoji';
export let item: any;
let htmlName: string;
let nameElement: any;
let settings = JSON.parse(localStorage.getItem("pk-settings"));
$: if (item.name) htmlName = toHTML(item.name);
else htmlName = "";
$: if (settings && settings.appearance.twemoji) {
if (nameElement) twemoji.parse(nameElement);
}
$: icon_url = item.avatar_url ? item.avatar_url : item.icon ? item.icon : default_avatar;
let avatarOpen = false;
const toggleAvatarModal = () => (avatarOpen = !avatarOpen);
export let loading: boolean = false;
</script>
<CardTitle style="margin-top: 0px; margin-bottom: 0px; outline: none; align-items: center;" class="d-flex justify-content-between align-middle w-100">
<div>
<div class="icon d-inline-block">
<slot name="icon" />
</div>
<span bind:this={nameElement} style="vertical-align: middle;">{@html htmlName} ({item.id})</span>
</div>
<div>
{#if loading}
<div class="d-inline-block mr-5" style="vertical-align: middle;"><Spinner color="primary" /></div>
{/if}
{#if item && (item.avatar_url || item.icon)}
<img tabindex={0} on:keyup={(event) => {if (event.key === "Enter") avatarOpen = true}} on:click={toggleAvatarModal} class="rounded-circle avatar" src={icon_url} alt="Icon" />
{:else}
<img class="rounded-circle avatar" src={default_avatar} alt="avatar (default)" />
{/if}
</div>
<Modal isOpen={avatarOpen} toggle={toggleAvatarModal}>
<div slot="external" on:click={toggleAvatarModal} style="height: 100%; max-width: 640px; width: 100%; margin-left: auto; margin-right: auto; display: flex;">
<Image style="display: block; margin: auto;" src={icon_url} thumbnail alt="Your system avatar" />
</div>
</Modal>
</CardTitle>

View File

@@ -0,0 +1,11 @@
<script lang="ts">
import {Navbar, Nav, NavItem, NavLink} from 'sveltestrap';
</script>
<Navbar color="light" light class="footer">
<Nav>
<NavItem>
<NavLink href="https://pluralkit.me/">pluralkit.me</NavLink>
</NavItem>
</Nav>
</Navbar>

View File

@@ -0,0 +1,73 @@
<script lang="ts">
import { Pagination, PaginationItem, PaginationLink } from "sveltestrap";
export let currentPage: number;
export let pageAmount: number;
export let smallPages: boolean = false;
</script>
{#if pageAmount > 1}
<Pagination size={smallPages ? "sm" : ""} class="mx-auto" arialabel="member list page navigation">
{#if currentPage !== 1}
<PaginationItem>
<PaginationLink href="#" previous on:click={(e) => {e.preventDefault(); currentPage -= 1}}></PaginationLink>
</PaginationItem>
{:else}
<PaginationItem disabled>
<PaginationLink previous></PaginationLink>
</PaginationItem>
{/if}
{#if currentPage > 2}
<PaginationItem>
<PaginationLink href="#" on:click={(e) => {e.preventDefault(); currentPage = 1}}>1</PaginationLink>
</PaginationItem>
{/if}
{#if currentPage === 4}
<PaginationItem>
<PaginationLink href="#" on:click={(e) => {e.preventDefault(); currentPage = 2}}>2</PaginationLink>
</PaginationItem>
{/if}
{#if currentPage > 4}
<PaginationItem disabled>
<PaginationLink>...</PaginationLink>
</PaginationItem>
{/if}
{#if currentPage > 1}
<PaginationItem>
<PaginationLink href="#" on:click={(e) => {e.preventDefault(); currentPage -= 1}}>{currentPage - 1}</PaginationLink>
</PaginationItem>
{/if}
<PaginationItem active>
<PaginationLink href="#">{currentPage}</PaginationLink>
</PaginationItem>
{#if currentPage < pageAmount}
<PaginationItem>
<PaginationLink href="#" on:click={(e) => {e.preventDefault(); currentPage += 1}}>{currentPage + 1}</PaginationLink>
</PaginationItem>
{/if}
{#if currentPage < pageAmount - 3}
<PaginationItem disabled>
<PaginationLink>...</PaginationLink>
</PaginationItem>
{/if}
{#if currentPage === pageAmount - 3}
<PaginationItem>
<PaginationLink href="#" on:click={(e) => {e.preventDefault(); currentPage = pageAmount - 1}}>{pageAmount - 1}</PaginationLink>
</PaginationItem>
{/if}
{#if currentPage < pageAmount - 1}
<PaginationItem>
<PaginationLink href="#" on:click={(e) => { e.preventDefault(); currentPage = pageAmount}}>{pageAmount}</PaginationLink>
</PaginationItem>
{/if}
{#if currentPage !== pageAmount}
<PaginationItem>
<PaginationLink href="#" next on:click={(e) => {e.preventDefault(); currentPage += 1}}></PaginationLink>
</PaginationItem>
{:else}
<PaginationItem disabled>
<PaginationLink next></PaginationLink>
</PaginationItem>
{/if}
</Pagination>
{/if}

View File

@@ -0,0 +1,58 @@
<script lang="ts">
import {Navbar, NavbarBrand, Nav, NavItem, NavLink, Collapse, NavbarToggler, Dropdown, DropdownItem, DropdownMenu, DropdownToggle, Button} from 'sveltestrap';
import { loggedIn } from '../stores';
import { Link, navigate } from 'svelte-navigator';
import { get } from 'svelte/store';
export let style: string;
let isOpen = false;
const toggle = () => (isOpen = !isOpen);
let loggedIn_value: boolean;
loggedIn.subscribe(value => {
loggedIn_value = value;
});
function logout() {
localStorage.removeItem("pk-token");
localStorage.removeItem("pk-user");
loggedIn.update(() => false);
navigate("/");
}
</script>
<Navbar color="light" light expand="lg" class="mb-4">
<NavbarBrand>pk-webs</NavbarBrand>
<NavbarToggler on:click={toggle}></NavbarToggler>
<Collapse {isOpen} navbar expand="lg">
<Nav class="ms-auto" navbar>
<Dropdown nav inNavbar>
<DropdownToggle color="transparent" tag="span" class="nav-link">Styles</DropdownToggle>
<DropdownMenu end>
<DropdownItem on:click={() => style = "light"}>Light</DropdownItem>
<DropdownItem on:click={() => style = "dark"}>Dark</DropdownItem>
</DropdownMenu>
</Dropdown>
{#if loggedIn_value || localStorage.getItem("pk-token")}
<Dropdown nav inNavbar>
<DropdownToggle color="transparent" tag="span" class="nav-link">Dash</DropdownToggle>
<DropdownMenu end>
<Link style="text-decoration: none;" to="/dash?tab=system"><DropdownItem>System</DropdownItem></Link>
<Link style="text-decoration: none;" to="/dash?tab=members"><DropdownItem>Members</DropdownItem></Link>
<Link style="text-decoration: none;" to="/dash?tab=groups"><DropdownItem>Groups</DropdownItem></Link>
<DropdownItem divider />
<DropdownItem on:click={logout}>Log out</DropdownItem>
</DropdownMenu>
</Dropdown>
{/if}
<NavItem>
<NavLink href="/settings">Settings</NavLink>
</NavItem>
<NavItem>
<NavLink href="/profile">Public</NavLink>
</NavItem>
</Nav>
</Collapse>
</Navbar>

View File

@@ -0,0 +1,122 @@
<script lang="ts">
import { Row, Col, Modal, Image, Button, CardBody, ModalHeader, ModalBody, ModalFooter, Spinner } from 'sveltestrap';
import moment from 'moment';
import { toHTML } from 'discord-markdown';
import Edit from './Edit.svelte';
import twemoji from 'twemoji';
import Privacy from './Privacy.svelte';
import MemberEdit from './MemberEdit.svelte';
import { Link } from 'svelte-navigator';
import { Member, Group } from '../../api/types';
export let group: Group;
let editMode: boolean = false;
let memberMode: boolean = false;
export let isPublic: boolean;
export let members: Member[] = [];
export let isMainDash = true;
export let isPage = false;
let htmlDescription: string;
$: if (group.description) {
htmlDescription = toHTML(group.description, {embed: true});
} else {
htmlDescription = "(no description)";
}
let htmlDisplayName: string;
$: if (group.display_name) htmlDisplayName = toHTML(group.display_name)
let settings = JSON.parse(localStorage.getItem("pk-settings"));
let descriptionElement: any;
let displayNameElement: any;
$: if (settings && settings.appearance.twemoji) {
if (descriptionElement) twemoji.parse(descriptionElement);
if (displayNameElement) twemoji.parse(displayNameElement);
};
let created = moment(group.created).format("MMM D, YYYY");
let bannerOpen = false;
const toggleBannerModal = () => (bannerOpen = !bannerOpen);
let privacyOpen = false;
const togglePrivacyModal = () => (privacyOpen = !privacyOpen);
</script>
<CardBody style="border-left: 4px solid #{settings && settings.appearance.color_background ? isPage ? "" : group.color : group.color }; margin: -1rem -1.25rem">
{#if !editMode && !memberMode}
<Row>
{#if group.id}
<Col xs={12} lg={4} class="mb-2">
<b>ID:</b> {group.id}
</Col>
{/if}
{#if group.name}
<Col xs={12} lg={4} class="mb-2">
<b>Name:</b> {group.name}
</Col>
{/if}
{#if group.display_name}
<Col xs={12} lg={4} class="mb-2">
<b>Display Name:</b> <span bind:this={displayNameElement}>{@html htmlDisplayName}</span>
</Col>
{/if}
{#if group.created && !isPublic}
<Col xs={12} lg={4} class="mb-2">
<b>Created:</b> {created}
</Col>
{/if}
{#if group.color}
<Col xs={12} lg={4} class="mb-2">
<b>Color:</b> {group.color}
</Col>
{/if}
{#if group.banner}
<Col xs={12} lg={3} class="mb-2">
<b>Banner:</b> <Button size="sm" color="light" on:click={toggleBannerModal}>View</Button>
<Modal isOpen={bannerOpen} toggle={toggleBannerModal}>
<div slot="external" on:click={toggleBannerModal} style="height: 100%; width: max-content; max-width: 100%; margin-left: auto; margin-right: auto; display: flex;">
<Image style="display: block; margin: auto;" src={group.banner} thumbnail alt="Your system banner" />
</div>
</Modal>
</Col>
{/if}
{#if group.privacy}
<Col xs={12} lg={4} class="mb-2">
<b>Privacy:</b> <Button size="sm" color="secondary" on:click={togglePrivacyModal}>Edit</Button>
<Modal size="lg" isOpen={privacyOpen} toggle={togglePrivacyModal}>
<ModalHeader toggle={togglePrivacyModal}>
Edit privacy
</ModalHeader>
<ModalBody>
<Privacy on:update bind:group bind:privacyOpen={privacyOpen}/>
</ModalBody>
</Modal>
</Col>
{/if}
</Row>
<div class="my-2 description" bind:this={descriptionElement}>
<b>Description:</b><br />
{@html htmlDescription && htmlDescription}
</div>
{#if (group.banner && ((settings && settings.appearance.banner_bottom) || !settings))}
<img src={group.banner} alt="your system banner" class="w-100 mb-3 rounded" style="max-height: 12em; object-fit: cover"/>
{/if}
{#if !isPublic}
<Button style="flex: 0" color="primary" on:click={() => editMode = true}>Edit</Button>
{#if isMainDash}<Button style="flex: 0" color="secondary" on:click={() => memberMode = true}>Members</Button>{/if}
{/if}
{#if !isPage}
<Link to={isPublic ? `/profile/g/${group.id}` : `/dash/g/${group.id}`}><Button style="flex: 0; {!isPublic && "float: right;"}" color="primary">View page</Button></Link>
{:else if !isPublic}
<Link to="/dash?tab=groups"><Button style="flex: 0; {!isPublic && "float: right;"}" color="primary">View system</Button></Link>
{/if}
{:else if editMode}
<Edit on:deletion on:update bind:group bind:editMode />
{:else if memberMode}
<MemberEdit on:updateMembers bind:group bind:memberMode bind:members />
{/if}
</CardBody>

View File

@@ -0,0 +1,145 @@
<script lang="ts">
import { Row, Col, Input, Button, Label, Alert, Spinner, Modal, ModalHeader, ModalBody } from 'sveltestrap';
import { createEventDispatcher } from 'svelte';
import { Group } from '../../api/types';
import api from '../../api';
import autosize from 'svelte-autosize';
const descriptions: string[] = JSON.parse(localStorage.getItem("pk-config"))?.description_templates;
let loading: boolean = false;
export let group: Group;
export let editMode: boolean;
let err: string[] = [];
let input: Group = {...group};
const dispatch = createEventDispatcher();
function update() {
dispatch('update', group);
}
function deletion() {
dispatch('deletion', group.id);
}
async function submit() {
let data = input;
err = [];
if (data.color && !/^#?[A-Fa-f0-9]{6}$/.test(input.color)) {
err.push(`"${data.color}" is not a valid color, the color must be a 6-digit hex code. (example: #ff0000)`);
} else if (data.color) {
if (data.color.startsWith("#")) {
data.color = input.color.slice(1, input.color.length);
}
}
err = err;
if (err.length > 0) return;
loading = true;
try {
let res = await api().groups(group.id).patch({data});
group = {...group, ...res};
err = [];
update();
editMode = false;
loading = false;
} catch (error) {
console.log(error);
err.push(error.message);
err = err;
loading = false;
}
}
let deleteOpen: boolean = false;
const toggleDeleteModal = () => deleteOpen = !deleteOpen;
let deleteInput: string;
let deleteErr: string;
async function submitDelete() {
deleteErr = null;
if (!deleteInput) {
deleteErr = "Please type out the group ID.";
return;
}
if (deleteInput.trim().toLowerCase() !== group.id) {
deleteErr = "This group's ID does not match the provided ID.";
return;
}
loading = true;
try {
await api().groups(group.id).delete();
deleteErr = null;
toggleDeleteModal();
loading = false;
deletion();
} catch (error) {
console.log(error);
deleteErr = error.message;
loading = false;
}
}
</script>
{#each err as error}
<Alert color="danger">{@html error}</Alert>
{/each}
<Row>
<Col xs={12} lg={4} class="mb-2">
<Label>Name:</Label>
<Input bind:value={input.name} maxlength={100} type="text" placeholder={group.name} />
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Display name:</Label>
<textarea class="form-control" style="resize: none; height: 1em" bind:value={input.display_name} maxlength={100} type="text" placeholder={group.display_name} />
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Color:</Label>
<Input bind:value={input.color} type="text" placeholder={group.color}/>
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Icon url:</Label>
<Input bind:value={input.icon} maxlength={256} type="url" placeholder={group.icon}/>
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Banner url:</Label>
<Input bind:value={input.banner} maxlength={256} type="url" placeholder={group.banner}/>
</Col>
</Row>
<div class="my-2">
<b>Description:</b><br />
{#if descriptions.length > 0 && descriptions[0].trim() != ""}
<Button size="sm" color="primary" on:click={() => input.description = descriptions[0]}>Template 1</Button>
{/if}
{#if descriptions.length > 1 && descriptions[1].trim() != ""}
<Button size="sm" color="primary" on:click={() => input.description = descriptions[1]}>Template 2</Button>
{/if}
{#if descriptions.length > 2 && descriptions[2].trim() != ""}
<Button size="sm" color="primary" on:click={() => input.description = descriptions[2]}>Template 3</Button>
{/if}
<br>
<textarea class="form-control" bind:value={input.description} maxlength={1000} use:autosize placeholder={group.description}/>
</div>
{#if !loading}<Button style="flex: 0" color="primary" on:click={submit}>Submit</Button> <Button style="flex: 0" color="secondary" on:click={() => editMode = false}>Back</Button><Button style="flex: 0; float: right;" color="danger" on:click={toggleDeleteModal}>Delete</Button>
{:else}<Button style="flex: 0" color="primary" disabled><Spinner size="sm"/></Button> <Button style="flex: 0" color="secondary" disabled>Back</Button><Button style="flex: 0; float: right;" color="danger" disabled>Delete</Button>{/if}
<Modal size="lg" isOpen={deleteOpen} toggle={toggleDeleteModal}>
<ModalHeader toggle={toggleDeleteModal}>
Delete member
</ModalHeader>
<ModalBody>
{#if deleteErr}<Alert color="danger">{deleteErr}</Alert>{/if}
<Label>If you're sure you want to delete this group, type out the group ID (<code>{group.id}</code>) below.</Label>
<Input class="mb-3" bind:value={deleteInput} maxlength={7} placeholder={group.id}></Input>
{#if !loading}<Button style="flex 0" color="danger" on:click={submitDelete}>Delete</Button> <Button style="flex: 0" color="secondary" on:click={toggleDeleteModal}>Back</Button>
{:else}<Button style="flex 0" color="danger" disabled><Spinner size="sm"/></Button> <Button style="flex: 0" color="secondary" disabled>Back</Button>
{/if}
</ModalBody>
</Modal>

View File

@@ -0,0 +1,367 @@
<script lang="ts">
import { Link } from 'svelte-navigator';
import { Card, CardHeader, CardBody, CardTitle, Alert, Accordion, AccordionItem, InputGroupText, InputGroup, Input, Label, Row, Col, Spinner, Button, Tooltip } from 'sveltestrap';
import FaUsers from 'svelte-icons/fa/FaUsers.svelte'
import { onMount } from 'svelte';
import FaSearch from 'svelte-icons/fa/FaSearch.svelte'
import { useParams } from 'svelte-navigator';
import CardsHeader from '../CardsHeader.svelte';
import ListPagination from '../ListPagination.svelte';
import Body from './Body.svelte';
import Svelecte, { addFormatter } from 'svelecte';
import FaLock from 'svelte-icons/fa/FaLock.svelte';
import NewGroup from './NewGroup.svelte';
import { Member, Group } from '../../api/types';
import api from '../../api';
export let isPublic: boolean;
export let list: Group[];
export let members: Member[];
$: memberlist = members && members.map(function(member) { return {name: member.name, shortid: member.id, id: member.uuid, display_name: member.display_name}; }).sort((a, b) => a.name.localeCompare(b.name));
let token = localStorage.getItem("pk-token");
let listLoading = true;
let err: string;
let settings = JSON.parse(localStorage.getItem("pk-settings"));
let itemsPerPageValue = settings && settings.accessibility && settings.accessibility.expandedcards ? "10" : "25";
$: itemsPerPage = parseInt(itemsPerPageValue);
let searchBy = "name";
let sortBy = "name";
let sortOrder = "ascending";
let privacyFilter = "all";
let memberSearchMode = "include";
let selectedMembers = [];
let currentPage = 1;
let params = useParams();
$: id = $params.id;
onMount(() => {
if (token || isPublic) fetchGroups();
});
async function fetchGroups() {
err = "";
listLoading = true;
try {
const res: Group[] = await api().systems(isPublic ? id : "@me").groups.get({ auth: !isPublic, query: { with_members: !isPublic } });
list = res;
listLoading = false;
} catch (error) {
console.log(error);
err = error.message;
listLoading = false;
}
}
let searchValue: string;
$: {searchValue; privacyFilter; currentPage = 1};
$: searchedList = list.filter((item) => {
if (!searchValue && searchBy !== "description" && searchBy !== "display name") return true;
switch (searchBy) {
case "name": if (item.name.toLowerCase().includes(searchValue.toLowerCase())) return true;
break;
case "display name": if (!searchValue) {
if (!item.display_name) return true;
else return false;
}
if (item.display_name && item.display_name.toLowerCase().includes(searchValue.toLowerCase())) return true;
break;
case "description": if (!searchValue) {
if (!item.description) return true;
else return false;
}
else if (item.description && item.description.toLowerCase().includes(searchValue.toLowerCase())) return true;
break;
case "ID": if (item.id.toLowerCase().includes(searchValue.toLowerCase())) return true;
break;
default: if (item.name.toLowerCase().includes(searchValue.toLowerCase())) return true;
break;
}
return false;
})
$: filteredList = searchedList.filter((item) => {
if (privacyFilter === "all") return true;
if (privacyFilter === "public" && item.privacy.visibility === "public") return true;
if (privacyFilter === "private" && item.privacy.visibility === "private") return true;
return false;
});
let sortedList = [];
$: if (filteredList) {
switch (sortBy) {
case "name": sortedList = filteredList.sort((a, b) => a.name.localeCompare(b.name));
break;
case "display name": sortedList = filteredList.sort((a, b) => {
if (a.display_name && b.display_name) return a.display_name.localeCompare(b.display_name);
else if (a.display_name && !b.display_name) return a.display_name.localeCompare(b.name);
else if (!a.display_name && b.display_name) return a.name.localeCompare(b.display_name);
else return a.name.localeCompare(b.name);
});
break;
case "creation date": sortedList = filteredList.sort((a, b) => {
if (a.created && b.created) return a.created.localeCompare(b.created);
});
break;
case "ID": sortedList = filteredList.sort((a, b) => a.id.localeCompare(b.id));
break;
default: sortedList = filteredList.sort((a, b) => a.name.localeCompare(b.name));
break;
}
}
let memberFilteredList = [];
$: memberFilteredList = sortedList.filter((item: Group) => {
if (memberSearchMode === "none") {
if (item.members && item.members.length > 0) return false;
}
if (selectedMembers.length < 1) return true;
switch (memberSearchMode) {
case "include": if (item.members && selectedMembers.some(value => item.members.includes(value))) return true;
break;
case "exclude": if (item.members && selectedMembers.every(value => !item.members.includes(value))) return true;
break;
case "match": if (item.members && selectedMembers.every(value => item.members.includes(value))) return true;
break;
default: return true;
}
return false;
});
let finalList = [];
$:{sortOrder; if (sortOrder === "descending") finalList = memberFilteredList.reverse(); else finalList = memberFilteredList;}
$: finalList = finalList;
$: indexOfLastItem = currentPage * itemsPerPage;
$: indexOfFirstItem = indexOfLastItem - itemsPerPage;
$: pageAmount = Math.ceil(finalList.length / itemsPerPage);
let slicedList = [];
$: slicedList = finalList.slice(indexOfFirstItem, indexOfLastItem);
function memberListRenderer(item: any) {
return `${item.name} (<code>${item.shortid}</code>)`;
}
addFormatter({
'member-list': memberListRenderer
});
function updateList(event: any) {
list = list.map(group => group.id !== event.detail.id ? group : event.detail)
}
function updateDelete(event: any) {
list = list.filter(group => group.id !== event.detail);
}
function addGroupToList(event: any) {
// reference types my beloathed
// the stringify/parse sequence is here so that a previous added group doesn't get overwritten
let group = JSON.parse(JSON.stringify(event.detail));
group.members = [];
list.push(group);
list = list;
}
</script>
<Card class="mb-3">
<CardHeader>
<CardTitle>
<CardTitle style="margin-top: 8px; outline: none;">
<div class="icon d-inline-block">
<FaSearch />
</div> Search groups
</CardTitle>
</CardTitle>
</CardHeader>
<CardBody>
<Row>
<Col xs={12} lg={3} class="mb-2">
<InputGroup>
<InputGroupText>Page length</InputGroupText>
<Input bind:value={itemsPerPageValue} type="select">
<option>10</option>
<option>25</option>
<option>50</option>
</Input>
</InputGroup>
</Col>
<Col xs={12} lg={3} class="mb-2">
<InputGroup>
<InputGroupText>Search by</InputGroupText>
<Input bind:value={searchBy} type="select">
<option>name</option>
<option>display name</option>
<option>description</option>
<option>ID</option>
</Input>
</InputGroup>
</Col>
<Col xs={12} lg={3} class="mb-2">
<InputGroup>
<InputGroupText>Sort by</InputGroupText>
<Input bind:value={sortBy} type="select">
<option>name</option>
<option>display name</option>
{#if !isPublic}<option>creation date</option>{/if}
<option>ID</option>
</Input>
</InputGroup>
</Col>
<Col xs={12} lg={3} class="mb-2">
<InputGroup>
<InputGroupText>Sort order</InputGroupText>
<Input bind:value={sortOrder} type="select">
<option>ascending</option>
<option>descending</option>
</Input>
</InputGroup>
</Col>
{#if !isPublic}
<Col xs={12} lg={3} class="mb-2">
<InputGroup>
<InputGroupText>Only show</InputGroupText>
<Input bind:value={privacyFilter} type="select">
<option>all</option>
<option>public</option>
<option>private</option>
</Input>
</InputGroup>
</Col>
{/if}
</Row>
{#if !isPublic}
<hr/>
<Label>Filter groups by member</Label>
<Svelecte renderer="member-list" bind:value={selectedMembers} disableHighlight options={memberlist} multiple style="margin-bottom: 0.5rem">
</Svelecte>
<span style="cursor: pointer" id="g-include" on:click={() => memberSearchMode = "include"}>{@html memberSearchMode === "include" ? "<b>include</b>" : "include"}</span>
| <span style="cursor: pointer" id="g-exclude" on:click={() => memberSearchMode = "exclude"}>{@html memberSearchMode === "exclude" ? "<b>exclude</b>" : "exclude"}</span>
| <span style="cursor: pointer" id="g-match" on:click={() => memberSearchMode = "match"}>{@html memberSearchMode === "match" ? "<b>exact match</b>" : "exact match"}</span>
| <span style="cursor: pointer" id="g-none" on:click={() => memberSearchMode = "none"}>{@html memberSearchMode === "none" ? "<b>none</b>" : "none"}</span>
<Tooltip placement="bottom" target="g-include">Includes every group with any of the members.</Tooltip>
<Tooltip placement="bottom" target="g-exclude">Excludes every group with any of the members, opposite of include.</Tooltip>
<Tooltip placement="bottom" target="g-match">Only includes groups which have all the members selected.</Tooltip>
<Tooltip placement="bottom" target="g-none">Only includes groups that have no members.</Tooltip>
{/if}
</CardBody>
</Card>
{#if listLoading && !err}
<div class="mx-auto text-center">
<Spinner class="d-inline-block" />
</div>
{:else if err}
<Row>
<Col xs={12} lg={10}>
<Alert color="danger">{err}</Alert>
</Col>
<Col xs={12} lg={2}>
<Button class="w-100 mb-3" color="primary" on:click={fetchGroups}>Refresh</Button>
</Col>
</Row>
{:else}
<Row>
<Col xs={12} lg={10} class="mb-2 mb-lg-0">
<Input class="mb-3" bind:value={searchValue} placeholder="search by {searchBy}..."/>
</Col>
<Col xs={12} lg={2}>
<Button class="w-100 mb-3" color="primary" on:click={fetchGroups}>Refresh</Button>
</Col>
</Row>
<ListPagination bind:currentPage bind:pageAmount />
{#if !isPublic}
<NewGroup on:create={addGroupToList} />
{/if}
{#if settings && settings.accessibility ? (!settings.accessibility.expandedcards && !settings.accessibility.pagelinks) : true}
<Accordion class="my-3" stayOpen>
{#each slicedList as group, index (group.id)}
{#if (!isPublic && group.privacy.visibility === "public") || isPublic}
<AccordionItem>
<CardsHeader bind:item={group} slot="header">
<FaUsers slot="icon" />
</CardsHeader>
<Body on:deletion={updateDelete} on:update={updateList} on:updateMembers={updateList} bind:members bind:group bind:isPublic={isPublic}/>
</AccordionItem>
{:else}
<AccordionItem>
<CardsHeader bind:item={group} slot="header">
<FaLock slot="icon" />
</CardsHeader>
<Body on:deletion={updateDelete} on:update={updateList} on:updateMembers={updateList} bind:members bind:group bind:isPublic={isPublic}/>
</AccordionItem>
{/if}
{/each}
</Accordion>
{:else if settings.accessibility.expandedcards}
{#each slicedList as group, index (group.id)}
{#if (!isPublic && group.privacy.visibility === "public") || isPublic}
<Card class="mb-3">
<CardHeader>
<CardsHeader item={group}>
<FaUsers slot="icon" />
</CardsHeader>
</CardHeader>
<CardBody>
<Body on:deletion={updateDelete} on:update={updateList} on:updateMembers={updateList} isPublic={isPublic} bind:members bind:group />
</CardBody>
</Card>
{:else}
<Card class="mb-3">
<CardHeader>
<CardsHeader item={group}>
<FaLock slot="icon" />
</CardsHeader>
</CardHeader>
<CardBody>
<Body on:deletion={updateDelete} on:update={updateList} on:updateMembers={updateList} isPublic={isPublic} bind:group bind:members />
</CardBody>
</Card>
{/if}
{/each}
{:else}
<div class="my-3">
{#each slicedList as group, index (group.id)}
{#if (!isPublic && group.privacy.visibility === "public") || isPublic}
<Card>
<Link class="accordion-button collapsed" style="text-decoration: none;" to={!isPublic ? `/dash/g/${group.id}` : `/profile/g/${group.id}`}>
<CardsHeader bind:item={group}>
<FaUsers slot="icon" />
</CardsHeader>
</Link>
</Card>
{:else}
<Card>
<Link class="accordion-button collapsed" style="text-decoration: none;" to={!isPublic ? `/dash/g/${group.id}` : `/profile/g/${group.id}`}>
<CardsHeader bind:item={group}>
<FaLock slot="icon" />
</CardsHeader>
</Link>
</Card>
{/if}
{/each}
</div>
{/if}
<ListPagination bind:currentPage bind:pageAmount />
{/if}

View File

@@ -0,0 +1,146 @@
<script lang="ts">
import { Row, Col, Button, Alert, ListGroup, ListGroupItem, Spinner } from 'sveltestrap';
import { createEventDispatcher } from 'svelte';
import ListPagination from "../ListPagination.svelte";
import twemoji from "twemoji";
import FaUserPlus from 'svelte-icons/fa/FaUserPlus.svelte'
import FaUserFriends from 'svelte-icons/fa/FaUserFriends.svelte'
import FaUserMinus from 'svelte-icons/fa/FaUserMinus.svelte'
import Svelecte, { addFormatter } from 'svelecte';
import { Group, Member } from '../../api/types';
import api from '../../api';
let loading: boolean = false;
let err: string;
export let group: Group;
export let memberMode: boolean = true;
export let members: Member[];
let membersInGroup: Member[];
let membersNotInGroup: Member[];
let membersInGroupSelection: any[];
let membersNotInGroupSelection: any[];
let membersToBeAdded: any[];
let membersToBeRemoved: any[];
let currentPage: number = 1;
let smallPages = true;
$: if (group.members) {
membersInGroup = members.filter(member => group.members.includes(member.uuid));
membersInGroup = membersInGroup.sort((a, b) => a.name.localeCompare(b.name));
membersNotInGroup = members.filter(member => !group.members.includes(member.uuid));
membersNotInGroup = membersNotInGroup.sort((a, b) => a.name.localeCompare(b.name));
membersInGroupSelection = membersInGroup.map(function(member) { return {name: member.name, shortid: member.id, id: member.uuid, display_name: member.display_name}; }).sort((a, b) => a.name.localeCompare(b.name));
membersNotInGroupSelection = membersNotInGroup.map(function(member) { return {name: member.name, shortid: member.id, id: member.uuid, display_name: member.display_name}; }).sort((a, b) => a.name.localeCompare(b.name));
}
$: indexOfLastItem = currentPage * 10;
$: indexOfFirstItem = indexOfLastItem - 10;
$: pageAmount = Math.ceil(membersInGroup && membersInGroup.length / 10);
$: finalMemberList = membersInGroup && membersInGroup.slice(indexOfFirstItem, indexOfLastItem);
let settings = JSON.parse(localStorage.getItem('pk-settings'));
let listGroupElements: any[] = [];
$: if (settings && settings.appearance.twemoji) {
if (listGroupElements && listGroupElements.length > 0) {
listGroupElements.forEach(element => element && twemoji.parse(element));
};
}
function memberListRenderer(item: any) {
return `${item.name} (<code>${item.shortid}</code>)`;
}
addFormatter({
'member-list': memberListRenderer
});
const dispatch = createEventDispatcher();
function update() {
dispatch("updateMembers", group)
}
async function submitAdd() {
let data = membersToBeAdded;
try {
loading = true;
await api().groups(group.id).members.add.post({data});
data.forEach(member => group.members.push(member));
update();
err = null;
membersToBeAdded = [];
loading = false;
} catch (error) {
console.log(error);
err = error.message;
loading = false;
}
}
async function submitRemove() {
let data = membersToBeRemoved;
try {
loading = true;
await api().groups(group.id).members.remove.post({data});
group.members = group.members.filter(m => !data.includes(m));
update();
err = null;
membersToBeRemoved = [];
loading = false;
} catch (error) {
console.log(error);
err = error.message;
loading = false;
}
}
</script>
{#if err}
<Alert color="danger">{err}</Alert>
{/if}
<Row>
<Col xs={12} lg={6} class="text-center mb-3">
<h5><div class="icon d-inline-block">
<FaUserFriends />
</div>Current Members</h5>
<ListPagination bind:currentPage bind:pageAmount bind:smallPages/>
{#if finalMemberList && finalMemberList.length > 0}
<ListGroup>
{#each finalMemberList as member, index (member.id)}
<ListGroupItem class="d-flex"><span bind:this={listGroupElements[index]} class="d-flex justify-content-between flex-grow-1"><span><b>{member.name}</b> (<code>{member.id}</code>)</span> <span>{member.display_name ? `${member.display_name}` : ""}</span></span></ListGroupItem>
{/each}
</ListGroup>
{:else}
<p>There are no members in this group yet.</p>
<p>You can add some in this menu!</p>
{/if}
</Col>
<Col xs={12} lg={6} class="text-center mb-3">
<h5><div class="icon d-inline-block">
<FaUserPlus />
</div>Add Members</h5>
<Svelecte renderer="member-list" disableHighlight bind:value={membersToBeAdded} options={membersNotInGroupSelection} multiple/>
{#if !loading && membersToBeAdded && membersToBeAdded.length > 0}
<Button class="w-100 mt-2" color="primary" on:click={submitAdd}>Add</Button>{:else}
<Button class="w-100 mt-2" color="primary" disabled>{#if loading}<Spinner size="sm" />{:else}Add{/if}</Button>
{/if}
<hr/>
<h5><div class="icon d-inline-block">
<FaUserMinus />
</div>Remove Members</h5>
<Svelecte renderer="member-list" disableHighlight bind:value={membersToBeRemoved} options={membersInGroupSelection} multiple/>
{#if !loading && membersToBeRemoved && membersToBeRemoved.length > 0}
<Button class="w-100 mt-2" color="primary" on:click={submitRemove}>Remove</Button>{:else}
<Button class="w-100 mt-2" color="primary" disabled>{#if loading}<Spinner size="sm" />{:else}Remove{/if}</Button>
{/if}
</Col>
</Row>
<Button style="flex: 0" color="secondary" on:click={() => memberMode = false}>Back</Button>

View File

@@ -0,0 +1,175 @@
<script lang="ts">
import { Row, Col, Input, Button, Label, Alert, Spinner, Accordion, AccordionItem, CardTitle } from 'sveltestrap';
import { Group } from '../../api/types';
import api from '../../api';
import autosize from 'svelte-autosize';
import { createEventDispatcher } from 'svelte';
import FaPlus from 'svelte-icons/fa/FaPlus.svelte';
const descriptions: string[] = JSON.parse(localStorage.getItem("pk-config"))?.description_templates;
let loading: boolean = false;
let err: string[] = [];
let message: string;
let privacyMode = false;
let defaultGroup: Group = {
privacy: {
description_privacy: "public",
metadata_privacy: "public",
list_privacy: "public",
icon_privacy: "public",
name_privacy: "public",
visibility: "public"
}
}
const dispatch = createEventDispatcher();
function create() {
dispatch('create', input);
}
let input: Group = defaultGroup;
async function submit() {
let data = input;
message = "";
err = [];
if (data.color && !/^#?[A-Fa-f0-9]{6}$/.test(input.color)) {
err.push(`"${data.color}" is not a valid color, the color must be a 6-digit hex code. (example: #ff0000)`);
} else if (data.color) {
if (data.color.startsWith("#")) {
data.color = input.color.slice(1, input.color.length);
}
}
err = err;
if (err.length > 0) return;
loading = true;
try {
let res = await api().groups().post({data});
input = res;
err = [];
create();
message = `Group ${data.name} successfully created!`
loading = false;
} catch (error) {
console.log(error);
err.push(error.message);
err = err;
loading = false;
}
}
</script>
<Accordion class="mb-3">
<AccordionItem>
<CardTitle slot="header" style="margin-top: 0px; margin-bottom: 0px; outline: none; align-items: center;" class="d-flex align-middle w-100 p-2">
<div class="icon d-inline-block">
<FaPlus/>
</div>
<span style="vertical-align: middle;">Add new Group</span>
</CardTitle>
{#if message}
<Alert color="success">{@html message}</Alert>
{/if}
{#each err as error}
<Alert color="danger">{@html error}</Alert>
{/each}
<Row>
<Col xs={12} lg={4} class="mb-2">
<Label>Name:</Label>
<Input bind:value={input.name} maxlength={100} type="text" />
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Display name:</Label>
<textarea class="form-control" style="resize: none; height: 1em" bind:value={input.display_name} maxlength={100} type="text"/>
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Color:</Label>
<Input bind:value={input.color} type="text"/>
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Icon url:</Label>
<Input bind:value={input.icon} maxlength={256} type="url"/>
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Banner url:</Label>
<Input bind:value={input.banner} maxlength={256} type="url"/>
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Privacy:</Label>
<Button class="w-100" color="secondary" on:click={() => privacyMode = !privacyMode}>Toggle privacy</Button>
</Col>
</Row>
{#if privacyMode}
<hr />
<Row>
<Col xs={12} lg={4} class="mb-3">
<Label>Description:</Label>
<Input type="select" bind:value={input.privacy.description_privacy}>
<option>public</option>
<option>private</option>
</Input>
</Col>
<Col xs={12} lg={4} class="mb-3">
<Label>Name:</Label>
<Input type="select" bind:value={input.privacy.name_privacy}>
<option>public</option>
<option>private</option>
</Input>
</Col>
<Col xs={12} lg={4} class="mb-3">
<Label>Member list:</Label>
<Input type="select" bind:value={input.privacy.list_privacy}>
<option>public</option>
<option>private</option>
</Input>
</Col>
<Col xs={12} lg={4} class="mb-3">
<Label>Icon:</Label>
<Input type="select" bind:value={input.privacy.icon_privacy}>
<option>public</option>
<option>private</option>
</Input>
</Col>
<Col xs={12} lg={4} class="mb-3">
<Label>Visibility:</Label>
<Input type="select" bind:value={input.privacy.visibility}>
<option>public</option>
<option>private</option>
</Input>
</Col>
<Col xs={12} lg={4} class="mb-3">
<Label>Metadata:</Label>
<Input type="select" bind:value={input.privacy.metadata_privacy}>
<option>public</option>
<option>private</option>
</Input>
</Col>
</Row>
<hr />
{/if}
<div class="my-2">
<b>Description:</b><br />
{#if descriptions.length > 0 && descriptions[0].trim() != ""}
<Button size="sm" color="primary" on:click={() => input.description = descriptions[0]}>Template 1</Button>
{/if}
{#if descriptions.length > 1 && descriptions[1].trim() != ""}
<Button size="sm" color="primary" on:click={() => input.description = descriptions[1]}>Template 2</Button>
{/if}
{#if descriptions.length > 2 && descriptions[2].trim() != ""}
<Button size="sm" color="primary" on:click={() => input.description = descriptions[2]}>Template 3</Button>
{/if}
<br>
<textarea class="form-control" bind:value={input.description} maxlength={1000} use:autosize />
</div>
{#if !loading && input.name}<Button style="flex: 0" color="primary" on:click={submit}>Submit</Button>
{:else if !input.name }<Button style="flex: 0" color="primary" disabled>Submit</Button>
{:else}<Button style="flex: 0" color="primary" disabled><Spinner size="sm"/></Button>{/if}
</AccordionItem>
</Accordion>

View File

@@ -0,0 +1,114 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { ModalBody, ModalHeader, Col, Row, Input, Label, ModalFooter, Button, Spinner, Alert } from "sveltestrap";
import { Group } from '../../api/types';
import api from '../../api';
export let privacyOpen: boolean;
export let group: Group;
const togglePrivacyModal = () => (privacyOpen = !privacyOpen);
let err: string;
let loading = false;
let allPrivacy: string;
$: { changePrivacy(allPrivacy)}
function changePrivacy(value: string) {
if (value) {
input.privacy.description_privacy = value;
input.privacy.list_privacy = value;
input.privacy.visibility = value;
input.privacy.icon_privacy = value;
input.privacy.name_privacy = value;
input.privacy.metadata_privacy = value;
}
}
const dispatch = createEventDispatcher();
function update() {
dispatch('update', group);
}
let input: Group = {privacy: group.privacy};
async function submit() {
let data = input;
err = null;
loading = true;
try {
let res = await api().groups(group.id).patch({data});
group = {...group, ...res};
update();
loading = false;
togglePrivacyModal();
} catch (error) {
console.log(error);
err = error.message;
err = err;
loading = false;
}
}
</script>
{#if err}
<Alert color="danger">{err}</Alert>
{/if}
<Label><b>Set all to:</b></Label>
<Input type="select" bind:value={allPrivacy}>
<option>public</option>
<option>private</option>
</Input>
<hr />
<Row>
<Col xs={12} lg={6} class="mb-3">
<Label>Description:</Label>
<Input type="select" bind:value={input.privacy.description_privacy}>
<option default={group.privacy.description_privacy === "public"}>public</option>
<option default={group.privacy.description_privacy === "private"}>private</option>
</Input>
</Col>
<Col xs={12} lg={6} class="mb-3">
<Label>Name:</Label>
<Input type="select" bind:value={input.privacy.name_privacy}>
<option default={group.privacy.name_privacy === "public"}>public</option>
<option default={group.privacy.name_privacy === "private"}>private</option>
</Input>
</Col>
<Col xs={12} lg={6} class="mb-3">
<Label>Member list:</Label>
<Input type="select" bind:value={input.privacy.list_privacy}>
<option default={group.privacy.list_privacy === "public"}>public</option>
<option default={group.privacy.list_privacy === "private"}>private</option>
</Input>
</Col>
<Col xs={12} lg={6} class="mb-3">
<Label>Icon:</Label>
<Input type="select" bind:value={input.privacy.icon_privacy}>
<option default={group.privacy.icon_privacy === "public"}>public</option>
<option default={group.privacy.icon_privacy === "private"}>private</option>
</Input>
</Col>
<Col xs={12} lg={6} class="mb-3">
<Label>Visibility:</Label>
<Input type="select" bind:value={input.privacy.visibility}>
<option default={group.privacy.visibility === "public"}>public</option>
<option default={group.privacy.visibility === "private"}>private</option>
</Input>
</Col>
<Col xs={12} lg={6} class="mb-3">
<Label>Metadata:</Label>
<Input type="select" bind:value={input.privacy.metadata_privacy}>
<option default={group.privacy.metadata_privacy === "public"}>public</option>
<option default={group.privacy.metadata_privacy === "private"}>private</option>
</Input>
</Col>
</Row>
{#if !loading}<Button style="flex: 0" color="primary" on:click={submit}>Submit</Button> <Button style="flex: 0" color="secondary" on:click={togglePrivacyModal}>Back</Button>
{:else}<Button style="flex: 0" color="primary" disabled><Spinner size="sm"/></Button> <Button style="flex: 0" color="secondary" disabled>Back</Button>
{/if}

View File

@@ -0,0 +1,162 @@
<script lang="ts">
import { Row, Col, Modal, Image, Button, CardBody, ModalHeader, ModalBody } from 'sveltestrap';
import moment from 'moment';
import { toHTML } from 'discord-markdown';
import twemoji from 'twemoji';
import GroupEdit from './GroupEdit.svelte';
import Edit from './Edit.svelte';
import Privacy from './Privacy.svelte';
import ProxyTags from './ProxyTags.svelte';
import { Member, Group } from '../../api/types';
import { Link } from 'svelte-navigator';
export let groups: Group[] = [];
export let member: Member;
export let isPublic: boolean = false;
export let isPage: boolean = false;
export let isMainDash = true;
let editMode: boolean = false;
let groupMode: boolean = false;
let htmlDescription: string;
$: if (member.description) {
htmlDescription = toHTML(member.description, {embed: true});
} else {
htmlDescription = "(no description)";
}
let htmlPronouns: string;
$: if (member.pronouns) {
htmlPronouns = toHTML(member.pronouns, {embed: true});
}
let settings = JSON.parse(localStorage.getItem("pk-settings"));
let descriptionElement: any;
let displayNameElement: any;
let pronounElement: any;
$: if (settings && settings.appearance.twemoji) {
if (descriptionElement) twemoji.parse(descriptionElement);
if (displayNameElement) twemoji.parse(displayNameElement);
if (pronounElement) twemoji.parse(pronounElement);
};
let bannerOpen = false;
const toggleBannerModal = () => (bannerOpen = !bannerOpen);
let privacyOpen = false;
const togglePrivacyModal = () => (privacyOpen = !privacyOpen);
let proxyOpen = false;
const toggleProxyModal = () => (proxyOpen = !proxyOpen);
let created = moment(member.created).format("MMM D, YYYY");
let birthday: string;
$: {member.birthday;
if (member.birthday) birthday = moment(member.birthday, "YYYY-MM-DD").format("MMM D, YYYY");
}
$: trimmedBirthday = birthday && birthday.endsWith(', 0004') ? trimmedBirthday = birthday.replace(', 0004', '') : birthday;
</script>
<CardBody style="border-left: 4px solid #{settings && settings.appearance.color_background ? isPage ? "" : member.color : member.color }; margin: -1rem -1.25rem">
{#if !editMode && !groupMode}
<Row>
{#if member.id}
<Col xs={12} lg={4} class="mb-2">
<b>ID:</b> {member.id}
</Col>
{/if}
{#if member.name}
<Col xs={12} lg={4} class="mb-2">
<b>Name:</b> {member.name}
</Col>
{/if}
{#if member.display_name}
<Col xs={12} lg={4} class="mb-2">
<b>Display Name:</b> <span bind:this={displayNameElement}>{member.display_name}</span>
</Col>
{/if}
{#if member.pronouns}
<Col xs={12} lg={4} class="mb-2">
<b>Pronouns:</b> <span bind:this={pronounElement}>{@html htmlPronouns}</span>
</Col>
{/if}
{#if member.birthday}
<Col xs={12} lg={4} class="mb-2">
<b>Birthday:</b> {trimmedBirthday}
</Col>
{/if}
{#if member.created}
<Col xs={12} lg={4} class="mb-2">
<b>Created:</b> {created}
</Col>
{/if}
{#if member.color}
<Col xs={12} lg={4} class="mb-2">
<b>Color:</b> {member.color}
</Col>
{/if}
{#if member.banner}
<Col xs={12} lg={3} class="mb-2">
<b>Banner:</b> <Button size="sm" color="secondary" on:click={toggleBannerModal}>View</Button>
<Modal isOpen={bannerOpen} toggle={toggleBannerModal}>
<div slot="external" on:click={toggleBannerModal} style="height: 100%; width: max-content; max-width: 100%; margin-left: auto; margin-right: auto; display: flex;">
<Image style="display: block; margin: auto;" src={member.banner} thumbnail alt="Your system banner" />
</div>
</Modal>
</Col>
{/if}
{#if member.privacy && !isPublic}
<Col xs={12} lg={4} class="mb-2">
<b>Privacy:</b> <Button size="sm" color="secondary" on:click={togglePrivacyModal}>Edit</Button>
<Modal size="lg" isOpen={privacyOpen} toggle={togglePrivacyModal}>
<ModalHeader toggle={togglePrivacyModal}>
Edit privacy
</ModalHeader>
<ModalBody>
<Privacy on:update bind:member bind:privacyOpen/>
</ModalBody>
</Modal>
</Col>
{/if}
{#if member.proxy_tags && !isPublic}
<Col xs={12} lg={4} class="mb-2">
<b>Proxy Tags:</b> <Button size="sm" color="secondary" on:click={toggleProxyModal}>Edit</Button>
<Modal size="lg" isOpen={proxyOpen} toggle={toggleProxyModal}>
<ModalHeader toggle={toggleProxyModal}>
Edit proxy tags
</ModalHeader>
<ModalBody>
<ProxyTags on:update bind:member bind:proxyOpen/>
</ModalBody>
</Modal>
</Col>
{/if}
</Row>
<div class="my-2 mb-3 description" bind:this={descriptionElement}>
<b>Description:</b><br />
{@html htmlDescription && htmlDescription}
</div>
{#if (member.banner && ((settings && settings.appearance.banner_bottom) || !settings))}
<img src={member.banner} alt="your system banner" class="w-100 mb-3 rounded" style="max-height: 17em; object-fit: cover"/>
{/if}
{#if !isPublic}
<Button style="flex: 0" color="primary" on:click={() => editMode = true}>Edit</Button>
{#if isMainDash}<Button style="flex: 0" color="secondary" on:click={() => groupMode = true}>Groups</Button>{/if}
{/if}
{#if !isPage}
<Link to={isPublic ? `/profile/m/${member.id}` : `/dash/m/${member.id}`}><Button style="flex: 0; {!isPublic && "float: right;"}" color="primary">View page</Button></Link>
{:else}
<Link to={isPublic ? `/profile/s/${member.system}?tab=members` : "/dash?tab=members"}><Button style="flex: 0; {!isPublic && "float: right;"}" color="primary">View system</Button></Link>
{/if}
{:else if editMode}
<Edit on:deletion on:update bind:member bind:editMode />
{:else if groupMode}
<GroupEdit on:updateGroups bind:member bind:groups bind:groupMode />
{/if}
</CardBody>

View File

@@ -0,0 +1,168 @@
<script lang="ts">
import { Row, Col, Input, Button, Label, Alert, Spinner, Modal, ModalHeader, ModalBody } from 'sveltestrap';
import { createEventDispatcher } from 'svelte';
import autosize from 'svelte-autosize';
import moment from 'moment';
import { Member } from '../../api/types'
import api from '../../api';
const descriptions: string[] = JSON.parse(localStorage.getItem("pk-config"))?.description_templates;
let loading: boolean = false;
export let member: Member;
export let editMode: boolean;
let err: string[] = [];
let input: Member = {...member};
const dispatch = createEventDispatcher();
function update() {
dispatch('update', member);
}
function deletion() {
dispatch('deletion', member.id);
}
async function submit() {
let data = input;
err = [];
if (data.color && !/^#?[A-Fa-f0-9]{6}$/.test(input.color)) {
err.push(`"${data.color}" is not a valid color, the color must be a 6-digit hex code. (example: #ff0000)`);
} else if (data.color) {
if (data.color.startsWith("#")) {
data.color = input.color.slice(1, input.color.length);
}
}
if (data.birthday) {
if (!moment(data.birthday, 'YYYY-MM-DD').isValid()) {
if (moment(data.birthday, 'MM-DD').isValid()) {
data.birthday = '0004-' + data.birthday;
} else {
err.push(`${data.birthday} is not a valid date, please use the following format: YYYY-MM-DD. (example: 2019-07-21)`);
}
}
if (data.birthday.includes('/')) {
data.birthday.replace('/', '-');
}
}
err = err;
if (err.length > 0) return;
loading = true;
try {
let res = await api().members(member.id).patch({data});
member = res;
err = [];
update();
editMode = false;
loading = false;
} catch (error) {
console.log(error);
err.push(error.message);
err = err;
loading = false;
}
}
let deleteOpen: boolean = false;
const toggleDeleteModal = () => deleteOpen = !deleteOpen;
let deleteInput: string;
let deleteErr: string;
async function submitDelete() {
deleteErr = null;
if (!deleteInput) {
deleteErr = "Please type out the member ID.";
return;
}
if (deleteInput.trim().toLowerCase() !== member.id) {
deleteErr = "This member's ID does not match the provided ID.";
return;
}
loading = true;
try {
await api().members(member.id).delete();
deleteErr = null;
toggleDeleteModal();
loading = false;
deletion();
} catch (error) {
console.log(error);
deleteErr = error.message;
loading = false;
}
}
</script>
{#each err as error}
<Alert color="danger">{@html error}</Alert>
{/each}
<Row>
<Col xs={12} lg={4} class="mb-2">
<Label>Name:</Label>
<Input bind:value={input.name} maxlength={100} type="text" placeholder={member.name} />
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Display name:</Label>
<textarea class="form-control" style="resize: none; height: 1em" bind:value={input.display_name} maxlength={100} type="text" placeholder={member.display_name} />
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Pronouns:</Label>
<textarea class="form-control" style="resize: none; height: 1em" bind:value={input.pronouns} maxlength={100} type="text" placeholder={member.pronouns} />
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Birthday:</Label>
<Input bind:value={input.birthday} maxlength={100} type="text" placeholder={member.birthday} />
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Color:</Label>
<Input bind:value={input.color} type="text" placeholder={member.color}/>
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Avatar url:</Label>
<Input bind:value={input.avatar_url} maxlength={256} type="url" placeholder={member.avatar_url}/>
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Banner url:</Label>
<Input bind:value={input.banner} maxlength={256} type="url" placeholder={member.banner}/>
</Col>
</Row>
<div class="my-2">
<b>Description:</b><br />
{#if descriptions.length > 0 && descriptions[0].trim() != ""}
<Button size="sm" color="primary" on:click={() => input.description = descriptions[0]}>Template 1</Button>
{/if}
{#if descriptions.length > 1 && descriptions[1].trim() != ""}
<Button size="sm" color="primary" on:click={() => input.description = descriptions[1]}>Template 2</Button>
{/if}
{#if descriptions.length > 2 && descriptions[2].trim() != ""}
<Button size="sm" color="primary" on:click={() => input.description = descriptions[2]}>Template 3</Button>
{/if}
<br>
<textarea class="form-control" bind:value={input.description} maxlength={1000} use:autosize placeholder={member.description}/>
</div>
{#if !loading}<Button style="flex: 0" color="primary" on:click={submit}>Submit</Button> <Button style="flex: 0" color="secondary" on:click={() => editMode = false}>Back</Button><Button style="flex: 0; float: right;" color="danger" on:click={toggleDeleteModal}>Delete</Button>
{:else}<Button style="flex: 0" color="primary" disabled><Spinner size="sm"/></Button> <Button style="flex: 0" color="secondary" disabled>Back</Button><Button style="flex: 0; float: right;" color="danger" disabled>Delete</Button>{/if}
<Modal size="lg" isOpen={deleteOpen} toggle={toggleDeleteModal}>
<ModalHeader toggle={toggleDeleteModal}>
Delete member
</ModalHeader>
<ModalBody>
{#if deleteErr}<Alert color="danger">{deleteErr}</Alert>{/if}
<Label>If you're sure you want to delete this member, type out the member ID (<code>{member.id}</code>) below.</Label>
<Input class="mb-3" bind:value={deleteInput} maxlength={7} placeholder={member.id}></Input>
{#if !loading}<Button style="flex 0" color="danger" on:click={submitDelete}>Delete</Button> <Button style="flex: 0" color="secondary" on:click={toggleDeleteModal}>Back</Button>
{:else}<Button style="flex 0" color="danger" disabled><Spinner size="sm"/></Button> <Button style="flex: 0" color="secondary" disabled>Back</Button>
{/if}
</ModalBody>
</Modal>

View File

@@ -0,0 +1,147 @@
<script lang="ts">
import { Row, Col, Button, Alert, ListGroup, ListGroupItem, Spinner } from 'sveltestrap';
import { createEventDispatcher } from 'svelte';
import ListPagination from "../ListPagination.svelte";
import twemoji from "twemoji";
import Svelecte, { addFormatter } from 'svelecte';
import { toHTML } from 'discord-markdown';
import FaFolderOpen from 'svelte-icons/fa/FaFolderOpen.svelte'
import FaFolderPlus from 'svelte-icons/fa/FaFolderPlus.svelte'
import FaFolderMinus from 'svelte-icons/fa/FaFolderMinus.svelte'
import { Member, Group } from '../../api/types';
import api from '../../api';
export let member: Member;
export let groups: Group[] = [];
let loading: boolean = false;
let err: string;
export let groupMode: boolean = true;
let groupsWithMember: Group[];
let groupsWithoutMember: Group[];
let groupsWithMemberSelection: any[];
let groupsWithoutMemberSelection: any[];
let groupsToBeAdded: any[];
let groupsToBeRemoved: any[];
let currentPage = 1;
let smallPages = true;
$: if (groups) {
groupsWithMember = groups.filter(group => group.members && group.members.includes(member.uuid));
groupsWithMember.sort((a, b) => a.name.localeCompare(b.name));
groupsWithoutMember = groups.filter(group => group.members && !group.members.includes(member.uuid));
groupsWithoutMember.sort((a, b) => a.name.localeCompare(b.name));
groupsWithMemberSelection = groupsWithMember.map(function(group) { return {name: group.name, shortid: group.id, id: group.uuid, members: group.members, display_name: group.display_name}; }).sort((a, b) => a.name.localeCompare(b.name));
groupsWithoutMemberSelection = groupsWithoutMember.map(function(group) { return {name: group.name, shortid: group.id, id: group.uuid, members: group.members, display_name: group.display_name}; }).sort((a, b) => a.name.localeCompare(b.name));
}
$: indexOfLastItem = currentPage * 10;
$: indexOfFirstItem = indexOfLastItem - 10;
$: pageAmount = Math.ceil(groupsWithMember && groupsWithMember.length / 10);
$: finalGroupsList = groupsWithMember && groupsWithMember.slice(indexOfFirstItem, indexOfLastItem);
let settings = JSON.parse(localStorage.getItem('pk-settings'));
let listGroupElements: any[] = [];
$: if (settings && settings.appearance.twemoji) {
if (listGroupElements && listGroupElements.length > 0) {
listGroupElements.forEach(element => element && twemoji.parse(element));
};
}
function groupListRenderer(item: any) {
return `${item.name} (<code>${item.shortid}</code>)`;
}
addFormatter({
'member-list': groupListRenderer
});
const dispatch = createEventDispatcher();
function updateGroups() {
dispatch("updateGroups", groups);
}
async function submitAdd() {
let data = groupsToBeAdded;
try {
loading = true;
await api().members(member.id).groups.add.post({data});
groups.forEach(group => data.includes(group.uuid) && group.members.push(member.uuid));
updateGroups();
err = null;
groupsToBeAdded = [];
loading = false;
} catch (error) {
console.log(error);
err = error.message;
loading = false;
}
}
async function submitRemove() {
let data = groupsToBeRemoved;
try {
loading = true;
await api().members(member.id).groups.remove.post({data});
groups.forEach(group => {if (data.includes(group.uuid)) group.members = group.members.filter(m => m !== member.uuid)});
updateGroups();
err = null;
groupsToBeRemoved = [];
loading = false;
} catch (error) {
console.log(error);
err = error.message;
loading = false;
}
}
</script>
{#if err}
<Alert color="danger">{err}</Alert>
{/if}
<Row>
<Col xs={12} lg={6} class="text-center mb-3">
<h5><div class="icon d-inline-block">
<FaFolderOpen />
</div>Current Groups</h5>
<ListPagination bind:currentPage bind:pageAmount bind:smallPages/>
{#if finalGroupsList && finalGroupsList.length > 0}
<ListGroup>
{#each finalGroupsList as group, index (group.id)}
<ListGroupItem class="d-flex"><span bind:this={listGroupElements[index]} class="d-flex justify-content-between flex-grow-1"><span><b>{group.name}</b> (<code>{group.id}</code>)</span> <span>{@html group.display_name ? `${toHTML(group.display_name)}` : ""}</span></span></ListGroupItem>
{/each}
</ListGroup>
{:else}
<p>This member is inside no groups.</p>
<p>You can add groups in this menu!</p>
{/if}
</Col>
<Col xs={12} lg={6} class="text-center mb-3">
<h5><div class="icon d-inline-block">
<FaFolderPlus />
</div>Add to Groups</h5>
<Svelecte renderer="member-list" disableHighlight bind:value={groupsToBeAdded} options={groupsWithoutMemberSelection} multiple/>
{#if !loading && groupsToBeAdded && groupsToBeAdded.length > 0}
<Button class="w-100 mt-2" color="primary" on:click={submitAdd}>Add</Button>{:else}
<Button class="w-100 mt-2" color="primary" disabled>{#if loading}<Spinner size="sm" />{:else}Add{/if}</Button>
{/if}
<hr/>
<h5><div class="icon d-inline-block">
<FaFolderMinus />
</div>Remove from Groups</h5>
<Svelecte renderer="member-list" disableHighlight bind:value={groupsToBeRemoved} options={groupsWithMemberSelection} multiple/>
{#if !loading && groupsToBeRemoved && groupsToBeRemoved.length > 0}
<Button class="w-100 mt-2" color="primary" on:click={submitRemove}>Remove</Button>{:else}
<Button class="w-100 mt-2" color="primary" disabled>{#if loading}<Spinner size="sm" />{:else}Remove{/if}</Button>
{/if}
</Col>
</Row>
<Button style="flex: 0" color="secondary" on:click={() => groupMode = false}>Back</Button>

View File

@@ -0,0 +1,366 @@
<script lang="ts">
import { Link } from 'svelte-navigator';
import { Card, CardHeader, CardBody, CardTitle, Alert, Accordion, AccordionItem, InputGroupText, InputGroup, Input, Row, Col, Spinner, Button, Tooltip, Label } from 'sveltestrap';
import FaUserCircle from 'svelte-icons/fa/FaUserCircle.svelte'
import { onMount } from 'svelte';
import FaSearch from 'svelte-icons/fa/FaSearch.svelte'
import { useParams } from 'svelte-navigator';
import CardsHeader from '../CardsHeader.svelte';
import ListPagination from '../ListPagination.svelte';
import Svelecte, { addFormatter } from 'svelecte';
import FaLock from 'svelte-icons/fa/FaLock.svelte';
import Body from './Body.svelte';
import NewMember from './NewMember.svelte';
import { Member, Group } from '../../api/types';
import api from '../../api';
export let isPublic: boolean;
export let list: Member[] = [];
export let groups: Group[] = [];
export let isMainDash = true;
$: grouplist = groups && groups.map(function(group) { return {name: group.name, shortid: group.id, id: group.uuid, members: group.members, display_name: group.display_name}; }).sort((a, b) => a.name.localeCompare(b.name));
let token = localStorage.getItem("pk-token");
let listLoading = true;
let err: string;
let settings = JSON.parse(localStorage.getItem("pk-settings"));
let itemsPerPageValue = settings && settings.accessibility && settings.accessibility.expandedcards ? "10" : "25";
$: itemsPerPage = parseInt(itemsPerPageValue);
let searchBy = "name";
let sortBy = "name";
let sortOrder = "ascending";
let privacyFilter = "all";
let groupSearchMode = "include";
let selectedGroups = [];
let currentPage = 1;
let params = useParams();
$: id = $params.id;
onMount(() => {
if (token || isPublic) fetchMembers();
});
async function fetchMembers() {
err = "";
listLoading = true;
try {
const res: Member[] = await api().systems(isPublic ? id : "@me").members.get({ auth: !isPublic });
list = res;
listLoading = false;
} catch (error) {
console.log(error);
err = error.message;
listLoading = false;
}
}
let searchValue: string;
$: {searchValue; privacyFilter; currentPage = 1};
$: searchedList = list.filter((item) => {
if (!searchValue && searchBy !== "description" && searchBy !== "display name") return true;
switch (searchBy) {
case "name": if (item.name.toLowerCase().includes(searchValue.toLowerCase())) return true;
break;
case "display name": if (!searchValue) {
if (!item.display_name) return true;
else return false;
}
if (item.display_name && item.display_name.toLowerCase().includes(searchValue.toLowerCase())) return true;
break;
case "description": if (!searchValue) {
if (!item.description) return true;
else return false;
}
else if (item.description && item.description.toLowerCase().includes(searchValue.toLowerCase())) return true;
break;
case "ID": if (item.id.toLowerCase().includes(searchValue.toLowerCase())) return true;
break;
default: if (item.name.toLowerCase().includes(searchValue.toLowerCase())) return true;
break;
}
return false;
})
$: filteredList = searchedList.filter((item) => {
if (privacyFilter === "all") return true;
if (privacyFilter === "public" && item.privacy.visibility === "public") return true;
if (privacyFilter === "private" && item.privacy.visibility === "private") return true;
return false;
});
let sortedList = [];
$: if (filteredList) {
switch (sortBy) {
case "name": sortedList = filteredList.sort((a, b) => a.name.localeCompare(b.name));
break;
case "display name": sortedList = filteredList.sort((a, b) => {
if (a.display_name && b.display_name) return a.display_name.localeCompare(b.display_name);
else if (a.display_name && !b.display_name) return a.display_name.localeCompare(b.name);
else if (!a.display_name && b.display_name) return a.name.localeCompare(b.display_name);
else return a.name.localeCompare(b.name);
});
break;
case "creation date": sortedList = filteredList.sort((a, b) => {
if (a.created && b.created) return a.created.localeCompare(b.created);
});
break;
case "ID": sortedList = filteredList.sort((a, b) => a.id.localeCompare(b.id));
break;
default: sortedList = filteredList.sort((a, b) => a.name.localeCompare(b.name));
break;
}
}
let memberFilteredList = [];
$: memberFilteredList = sortedList.filter((item: Member) => {
if (groupSearchMode === "none") {
if (groups.some(group => group.members && group.members.includes(item.uuid))) return false;
}
if (selectedGroups.length < 1) return true;
switch (groupSearchMode) {
case "include": if (selectedGroups.some(group => group.members && group.members.includes(item.uuid))) return true;
break;
case "exclude": if (selectedGroups.every(group => group.members && !group.members.includes(item.uuid))) return true;
break;
case "match": if (selectedGroups.every(group => group.members && group.members.includes(item.uuid))) return true;
break;
default: return true;
}
return false;
})
let finalList = [];
$:{sortOrder; if (sortOrder === "descending") finalList = memberFilteredList.reverse(); else finalList = memberFilteredList;}
$: finalList = finalList;
$: indexOfLastItem = currentPage * itemsPerPage;
$: indexOfFirstItem = indexOfLastItem - itemsPerPage;
$: pageAmount = Math.ceil(finalList.length / itemsPerPage);
$: slicedList = finalList.slice(indexOfFirstItem, indexOfLastItem);
function groupListRenderer(item: any) {
return `${item.name} (<code>${item.shortid}</code>)`;
}
addFormatter({
'member-list': groupListRenderer
});
function updateList(event: any) {
list = list.map(member => member.id !== event.detail.id ? member : event.detail);
}
function updateGroups(event: any) {
groups = event.detail;
}
function updateDelete(event: any) {
list = list.filter(member => member.id !== event.detail);
}
function addMemberToList(event: any) {
list.push(event.detail);
list = list;
}
</script>
<Card class="mb-3">
<CardHeader>
<CardTitle>
<CardTitle style="margin-top: 8px; outline: none;">
<div class="icon d-inline-block">
<FaSearch />
</div> Search members
</CardTitle>
</CardTitle>
</CardHeader>
<CardBody>
<Row>
<Col xs={12} lg={3} class="mb-2">
<InputGroup>
<InputGroupText>Page length</InputGroupText>
<Input bind:value={itemsPerPageValue} type="select">
<option>10</option>
<option>25</option>
<option>50</option>
</Input>
</InputGroup>
</Col>
<Col xs={12} lg={3} class="mb-2">
<InputGroup>
<InputGroupText>Search by</InputGroupText>
<Input bind:value={searchBy} type="select">
<option>name</option>
<option>display name</option>
<option>description</option>
<option>ID</option>
</Input>
</InputGroup>
</Col>
<Col xs={12} lg={3} class="mb-2">
<InputGroup>
<InputGroupText>Sort by</InputGroupText>
<Input bind:value={sortBy} type="select">
<option>name</option>
<option>display name</option>
{#if !isPublic}<option>creation date</option>{/if}
<option>ID</option>
</Input>
</InputGroup>
</Col>
<Col xs={12} lg={3} class="mb-2">
<InputGroup>
<InputGroupText>Sort order</InputGroupText>
<Input bind:value={sortOrder} type="select">
<option>ascending</option>
<option>descending</option>
</Input>
</InputGroup>
</Col>
{#if !isPublic}
<Col xs={12} lg={3} class="mb-2">
<InputGroup>
<InputGroupText>Only show</InputGroupText>
<Input bind:value={privacyFilter} type="select">
<option>all</option>
<option>public</option>
<option>private</option>
</Input>
</InputGroup>
</Col>
{/if}
</Row>
{#if !isPublic}
<hr/>
<Label>Filter members by group</Label>
<Svelecte disableHighlight renderer="member-list" valueAsObject bind:value={selectedGroups} options={grouplist} multiple style="margin-bottom: 0.5rem">
</Svelecte>
<span style="cursor: pointer" id="m-include" on:click={() => groupSearchMode = "include"}>{@html groupSearchMode === "include" ? "<b>include</b>" : "include"}</span>
| <span style="cursor: pointer" id="m-exclude" on:click={() => groupSearchMode = "exclude"}>{@html groupSearchMode === "exclude" ? "<b>exclude</b>" : "exclude"}</span>
| <span style="cursor: pointer" id="m-match" on:click={() => groupSearchMode = "match"}>{@html groupSearchMode === "match" ? "<b>exact match</b>" : "exact match"}</span>
| <span style="cursor: pointer" id="m-none" on:click={() => groupSearchMode = "none"}>{@html groupSearchMode === "none" ? "<b>none</b>" : "none"}</span>
<Tooltip placement="bottom" target="m-include">Includes every member who's a part of any of the groups.</Tooltip>
<Tooltip placement="bottom" target="m-exclude">Excludes every member who's a part of any of the groups, the opposite of include.</Tooltip>
<Tooltip placement="bottom" target="m-match">Only includes members who are a part of every group.</Tooltip>
<Tooltip placement="bottom" target="m-none">Only includes members that are in no groups.</Tooltip>
{/if}
</CardBody>
</Card>
{#if listLoading && !err}
<div class="mx-auto text-center">
<Spinner class="d-inline-block" />
</div>
{:else if err}
<Row>
<Col xs={12} lg={10}>
<Alert color="danger">{err}</Alert>
</Col>
<Col xs={12} lg={2}>
<Button class="w-100 mb-3" color="primary" on:click={fetchMembers}>Refresh</Button>
</Col>
</Row>
{:else}
<Row>
<Col xs={12} lg={10}>
<Input class="mb-3" bind:value={searchValue} placeholder="search by {searchBy}..."/>
</Col>
<Col xs={12} lg={2} class="mb-3 mb-lg-0">
<Button class="w-100 mb-3" color="primary" on:click={fetchMembers}>Refresh</Button>
</Col>
</Row>
<ListPagination bind:currentPage bind:pageAmount />
{#if !isPublic}
<NewMember on:create={addMemberToList} />
{/if}
{#if settings && settings.accessibility ? (!settings.accessibility.expandedcards && !settings.accessibility.pagelinks) : true}
<Accordion class="my-3" stayOpen>
{#each slicedList as member, index (member.id)}
{#if (!isPublic && member.privacy.visibility === "public") || isPublic}
<AccordionItem>
<CardsHeader bind:item={member} slot="header">
<FaUserCircle slot="icon" />
</CardsHeader>
<Body on:deletion={updateDelete} on:update={updateList} on:updateGroups={updateGroups} bind:isPublic bind:groups bind:member />
</AccordionItem>
{:else}
<AccordionItem>
<CardsHeader bind:item={member} slot="header">
<FaLock slot="icon" />
</CardsHeader>
<Body on:deletion={updateDelete} on:update={updateList} on:updateGroups={updateGroups} bind:isPublic bind:groups bind:member />
</AccordionItem>
{/if}
{/each}
</Accordion>
{:else if settings.accessibility.expandedcards}
{#each slicedList as member, index (member.id)}
{#if (!isPublic && member.privacy.visibility === "public") || isPublic}
<Card class="mb-3">
<CardHeader>
<CardsHeader bind:item={member}>
<FaUserCircle slot="icon" />
</CardsHeader>
</CardHeader>
<CardBody>
<Body on:deletion={updateDelete} on:update={updateList} on:updateGroups={updateGroups} bind:isPublic bind:groups bind:member />
</CardBody>
</Card>
{:else}
<Card class="mb-3">
<CardHeader>
<CardsHeader bind:item={member}>
<FaLock slot="icon" />
</CardsHeader>
</CardHeader>
<CardBody>
<Body on:deletion={updateDelete} on:update={updateList} on:updateGroups={updateGroups} bind:isPublic bind:groups bind:member />
</CardBody>
</Card>
{/if}
{/each}
{:else}
<div class="my-3">
{#each slicedList as member, index (member.id)}
{#if (!isPublic && member.privacy.visibility === "public") || isPublic}
<Card>
<Link class="accordion-button collapsed" style="text-decoration: none;" to={isMainDash ? `/dash/m/${member.id}` : `/profile/m/${member.id}`}>
<CardsHeader bind:item={member}>
<FaUserCircle slot="icon" />
</CardsHeader>
</Link>
</Card>
{:else}
<Card>
<Link class="accordion-button collapsed" style="text-decoration: none;" to={isMainDash ? `/dash/m/${member.id}` : `/profile/m/${member.id}`}>
<CardsHeader bind:item={member}>
<FaLock slot="icon" />
</CardsHeader>
</Link>
</Card>
{/if}
{/each}
</div>
{/if}
<ListPagination bind:currentPage bind:pageAmount />
{/if}

View File

@@ -0,0 +1,240 @@
<script lang="ts">
import { Accordion, AccordionItem, Row, Col, Input, Button, Label, Alert, Spinner, CardTitle, InputGroup } from 'sveltestrap';
import { createEventDispatcher } from 'svelte';
import autosize from 'svelte-autosize';
import moment from 'moment';
import FaPlus from 'svelte-icons/fa/FaPlus.svelte';
import { Member } from '../../api/types';
import api from '../../api';
const descriptions: string[] = JSON.parse(localStorage.getItem("pk-config"))?.description_templates;
let err: string[] = [];
let message: string;
let loading: boolean = false;
let privacyMode: boolean = false;
let proxyTagMode: boolean = false;
let defaultMember = {
privacy: {
visibility: "public",
metadata_privacy: "public",
description_privacy: "public",
pronoun_privacy: "public",
birthday_privacy: "public",
name_privacy: "public",
avatar_privacy: "public"
},
proxy_tags: [
{
prefix: "",
suffix: ""
}
]
};
let input: Member = defaultMember;
const dispatch = createEventDispatcher();
function create() {
dispatch('create', input);
}
async function submit() {
let data = input;
message = "";
err = [];
if (input.proxy_tags.some(tag => tag.prefix && tag.suffix && tag.prefix.length + tag.suffix.length + 4 > 100)) {
err.push("One of your proxy tags is too long (prefix + 'text' + suffix must be shorter than 100 characters). Please shorten this tag, or remove it.");
}
if (data.color && !/^#?[A-Fa-f0-9]{6}$/.test(input.color)) {
err.push(`"${data.color}" is not a valid color, the color must be a 6-digit hex code. (example: #ff0000)`);
} else if (data.color) {
if (data.color.startsWith("#")) {
data.color = input.color.slice(1, input.color.length);
}
}
if (data.birthday) {
if (!moment(data.birthday, 'YYYY-MM-DD').isValid()) {
if (moment(data.birthday, 'MM-DD').isValid()) {
data.birthday = '0004-' + data.birthday;
} else {
err.push(`${data.birthday} is not a valid date, please use the following format: YYYY-MM-DD. (example: 2019-07-21)`);
}
}
if (data.birthday.includes('/')) {
data.birthday.replace('/', '-');
}
}
err = err;
if (err.length > 0) return;
loading = true;
try {
let res = await api().members().post({data});
input = res;
err = [];
create();
input = defaultMember;
message = `Member ${data.name} successfully created!`
loading = false;
} catch (error) {
console.log(error);
err.push(error.message);
err = err;
loading = false;
}
}
</script>
<Accordion class="mb-3">
<AccordionItem>
<CardTitle slot="header" style="margin-top: 0px; margin-bottom: 0px; outline: none; align-items: center;" class="d-flex align-middle w-100 p-2">
<div class="icon d-inline-block">
<FaPlus/>
</div>
<span style="vertical-align: middle;">Add new Member</span>
</CardTitle>
{#if message}
<Alert color="success">{@html message}</Alert>
{/if}
{#each err as error}
<Alert color="danger">{@html error}</Alert>
{/each}
<Row>
<Col xs={12} lg={4} class="mb-2">
<Label>Name:</Label>
<Input bind:value={input.name} maxlength={100} type="text" placeholder={input.name} />
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Display name:</Label>
<textarea class="form-control" style="resize: none; height: 1em" bind:value={input.display_name} maxlength={100} type="text" placeholder={input.display_name} />
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Pronouns:</Label>
<textarea class="form-control" style="resize: none; height: 1em" bind:value={input.pronouns} maxlength={100} type="text" placeholder={input.pronouns} />
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Birthday:</Label>
<Input bind:value={input.birthday} maxlength={100} type="text" placeholder={input.birthday} />
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Color:</Label>
<Input bind:value={input.color} type="text" placeholder={input.color}/>
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Avatar url:</Label>
<Input bind:value={input.avatar_url} maxlength={256} type="url" placeholder={input.avatar_url}/>
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Banner url:</Label>
<Input bind:value={input.banner} maxlength={256} type="url" placeholder={input.banner}/>
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Privacy:</Label>
<Button class="w-100" color="secondary" on:click={() => privacyMode = !privacyMode}>Toggle privacy</Button>
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Proxy tags:</Label>
<Button class="w-100" color="secondary" on:click={() => proxyTagMode = !proxyTagMode}>Toggle proxy tags</Button>
</Col>
</Row>
{#if privacyMode}
<hr/>
<b>Privacy:</b>
<Row>
<Col xs={12} lg={4} class="mb-3">
<Label>Description:</Label>
<Input type="select" bind:value={input.privacy.description_privacy}>
<option default>public</option>
<option>private</option>
</Input>
</Col>
<Col xs={12} lg={4} class="mb-3">
<Label>Name:</Label>
<Input type="select" bind:value={input.privacy.name_privacy}>
<option default>public</option>
<option>private</option>
</Input>
</Col>
<Col xs={12} lg={4} class="mb-3">
<Label>Avatar:</Label>
<Input type="select" bind:value={input.privacy.avatar_privacy}>
<option default>public</option>
<option>private</option>
</Input>
</Col>
<Col xs={12} lg={4} class="mb-3">
<Label>Birthday:</Label>
<Input type="select" bind:value={input.privacy.birthday_privacy}>
<option default>public</option>
<option>private</option>
</Input>
</Col>
<Col xs={12} lg={4} class="mb-3">
<Label>Pronouns:</Label>
<Input type="select" bind:value={input.privacy.pronoun_privacy}>
<option default>public</option>
<option>private</option>
</Input>
</Col>
<Col xs={12} lg={4} class="mb-3">
<Label>Visibility:</Label>
<Input type="select" bind:value={input.privacy.visibility}>
<option default>public</option>
<option>private</option>
</Input>
</Col>
<Col xs={12} lg={4} class="mb-3">
<Label>Metadata:</Label>
<Input type="select" bind:value={input.privacy.metadata_privacy}>
<option default>public</option>
<option>private</option>
</Input>
</Col>
</Row>
{/if}
{#if proxyTagMode}
<hr/>
<b>Proxy tags:</b>
<Row class="mb-2">
{#each input.proxy_tags as proxyTag, index (index)}
<Col xs={12} lg={4} class="mb-2">
<InputGroup>
<Input style="resize: none; height: 1em" type="textarea" bind:value={proxyTag.prefix} />
<Input disabled value="text"/>
<Input style="resize: none; height: 1em" type="textarea" bind:value={proxyTag.suffix}/>
</InputGroup>
</Col>
{/each}
<Col xs={12} lg={4} class="mb-2">
<Button class="w-100" color="secondary" on:click={() => {input.proxy_tags.push({prefix: "", suffix: ""}); input.proxy_tags = input.proxy_tags;}}>New</Button>
</Col>
</Row>
{/if}
<hr/>
<div class="my-2">
<b>Description:</b><br />
{#if descriptions.length > 0 && descriptions[0].trim() != ""}
<Button size="sm" color="primary" on:click={() => input.description = descriptions[0]}>Template 1</Button>
{/if}
{#if descriptions.length > 1 && descriptions[1].trim() != ""}
<Button size="sm" color="primary" on:click={() => input.description = descriptions[1]}>Template 2</Button>
{/if}
{#if descriptions.length > 2 && descriptions[2].trim() != ""}
<Button size="sm" color="primary" on:click={() => input.description = descriptions[2]}>Template 3</Button>
{/if}
<br>
<textarea class="form-control" bind:value={input.description} maxlength={1000} use:autosize placeholder={input.description}/>
</div>
{#if !loading && input.name}<Button style="flex: 0" color="primary" on:click={submit}>Submit</Button>
{:else if !input.name }<Button style="flex: 0" color="primary" disabled>Submit</Button>
{:else}<Button style="flex: 0" color="primary" disabled><Spinner size="sm"/></Button>{/if}
</AccordionItem>
</Accordion>

View File

@@ -0,0 +1,122 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { Col, Row, Input, Label, Button, Alert, Spinner } from "sveltestrap";
import { Member } from '../../api/types';
import api from '../../api';
let loading: boolean;
export let privacyOpen: boolean;
export let member: Member;
const togglePrivacyModal = () => (privacyOpen = !privacyOpen);
let err: string;
let allPrivacy: string;
$: { changePrivacy(allPrivacy)}
function changePrivacy(value: string) {
if (value) {
input.privacy.description_privacy = value;
input.privacy.name_privacy = value;
input.privacy.avatar_privacy = value;
input.privacy.birthday_privacy = value;
input.privacy.pronoun_privacy = value;
input.privacy.visibility = value;
input.privacy.metadata_privacy = value;
}
}
const dispatch = createEventDispatcher();
function update() {
dispatch('update', member);
}
let input: Member = {privacy: member.privacy};
async function submit() {
let data = input;
err = null;
loading = true;
try {
let res = await api().members(member.id).patch({data});
member = res;
update();
loading = false;
togglePrivacyModal();
} catch (error) {
console.log(error);
err = error.message;
err = err;
loading = false;
}
}
</script>
{#if err}
<Alert color="danger">{err}</Alert>
{/if}
<Label><b>Set all to:</b></Label>
<Input type="select" bind:value={allPrivacy}>
<option>public</option>
<option>private</option>
</Input>
<hr />
<Row>
<Col xs={12} lg={6} class="mb-3">
<Label>Description:</Label>
<Input type="select" bind:value={input.privacy.description_privacy}>
<option default={member.privacy.description_privacy === "public"}>public</option>
<option default={member.privacy.description_privacy === "private"}>private</option>
</Input>
</Col>
<Col xs={12} lg={6} class="mb-3">
<Label>Name:</Label>
<Input type="select" bind:value={input.privacy.name_privacy}>
<option default={member.privacy.name_privacy === "public"}>public</option>
<option default={member.privacy.name_privacy === "private"}>private</option>
</Input>
</Col>
<Col xs={12} lg={6} class="mb-3">
<Label>Avatar:</Label>
<Input type="select" bind:value={input.privacy.avatar_privacy}>
<option default={member.privacy.avatar_privacy === "public"}>public</option>
<option default={member.privacy.avatar_privacy === "private"}>private</option>
</Input>
</Col>
<Col xs={12} lg={6} class="mb-3">
<Label>Birthday:</Label>
<Input type="select" bind:value={input.privacy.birthday_privacy}>
<option default={member.privacy.birthday_privacy === "public"}>public</option>
<option default={member.privacy.birthday_privacy === "private"}>private</option>
</Input>
</Col>
<Col xs={12} lg={6} class="mb-3">
<Label>Pronouns:</Label>
<Input type="select" bind:value={input.privacy.pronoun_privacy}>
<option default={member.privacy.pronoun_privacy === "public"}>public</option>
<option default={member.privacy.pronoun_privacy === "private"}>private</option>
</Input>
</Col>
<Col xs={12} lg={6} class="mb-3">
<Label>Visibility:</Label>
<Input type="select" bind:value={input.privacy.visibility}>
<option default={member.privacy.visibility === "public"}>public</option>
<option default={member.privacy.visibility === "private"}>private</option>
</Input>
</Col>
<Col xs={12} lg={6} class="mb-3">
<Label>Metadata:</Label>
<Input type="select" bind:value={input.privacy.metadata_privacy}>
<option default={member.privacy.metadata_privacy === "public"}>public</option>
<option default={member.privacy.metadata_privacy === "private"}>private</option>
</Input>
</Col>
</Row>
{#if !loading}<Button style="flex: 0" color="primary" on:click={submit}>Submit</Button> <Button style="flex: 0" color="secondary" on:click={togglePrivacyModal}>Back</Button>
{:else}<Button style="flex: 0" color="primary" disabled><Spinner size="sm"/></Button> <Button style="flex: 0" color="secondary" disabled>Back</Button>
{/if}

View File

@@ -0,0 +1,67 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { Col, Row, Input, Label, Button, Alert, Spinner, InputGroup } from "sveltestrap";
import { Member } from '../../api/types';
import api from '../../api';
let loading: boolean;
export let proxyOpen: boolean;
export let member: Member;
const toggleProxyModal = () => (proxyOpen = !proxyOpen);
let err: string;
const dispatch = createEventDispatcher();
function update() {
dispatch('update', member);
}
let input = member.proxy_tags;
async function submit() {
err = null;
if (input.some(tag => tag.prefix && tag.suffix && tag.prefix.length + tag.suffix.length + 4 > 100)) {
err = "One of your proxy tags is too long (prefix + 'text' + suffix must be shorter than 100 characters). Please shorten this tag, or remove it."
return;
}
let data: Member = {proxy_tags: input};
loading = true;
try {
let res = await api().members(member.id).patch({data});
member = res;
err = null;
update();
loading = false;
toggleProxyModal();
} catch (error) {
console.log(error);
err = error.message;
loading = false;
}
}
</script>
{#if err}
<Alert color="danger">{err}</Alert>
{/if}
<Row class="mb-2">
{#each input as proxyTag, index (index)}
<Col xs={12} lg={6} class="mb-2">
<InputGroup>
<Input style="resize: none; height: 1em" type="textarea" bind:value={proxyTag.prefix} />
<Input disabled value="text"/>
<Input style="resize: none; height: 1em" type="textarea" bind:value={proxyTag.suffix}/>
</InputGroup>
</Col>
{/each}
<Col xs={12} lg={6} class="mb-2">
<Button class="w-100" color="secondary" on:click={() => {input.push({prefix: "", suffix: ""}); input = input;}}>New</Button>
</Col>
</Row>
{#if !loading}<Button style="flex 0" color="primary" on:click={submit}>Submit</Button> <Button style="flex: 0" color="secondary" on:click={toggleProxyModal}>Back</Button>
{:else}<Button style="flex 0" color="primary" disabled><Spinner size="sm"/></Button> <Button style="flex: 0" color="secondary" disabled>Back</Button>
{/if}

View File

@@ -0,0 +1,95 @@
<script lang="ts">
export let hover;
export let shard = {
id: 1,
status: "",
ping:0,
disconnection_count:0,
last_connection:0,
last_heartbeat:0.
};
let color = "background-color: #fff";
// shard is down
// todo: check if last heartbeat is really recent, since database up/down status can get out of sync
if (shard.status != "up") color = "background-color: #000;";
// shard latency is < 250ms: OK!
else if (shard.ping < 300) color = "background-color: #00cc00;";
// shard latency is 250ms < ping < 600ms: slow, but OK
else if (shard.ping < 600) color = "background-color: #da9317;";
// shard latency is >600ms, this might be problematic
else color = "background-color: #cc0000;"
</script>
<div class="wrapper">
<div
on:click={() => hover = (hover != shard.id) ? shard.id : null}
class="shard" id={shard.id.toString()}
style={color}
>{ shard.id }</div>
{#if hover == shard.id}
<div class="more-info">
<br>
<h3>Shard { shard.id }</h3>
<br>
<span>Status: <b>{ shard.status }</b></span><br>
<span>Latency: { shard.ping }ms</span><br>
<span>Disconnection count: { shard.disconnection_count }</span><br>
<span>Last connection: { shard.last_connection }</span><br>
<span>Last heartbeat: { shard.last_heartbeat }</span><br>
<br>
</div>
{/if}
</div>
<style>
.wrapper {
height: 55px;
width: 55px;
display: block;
float: left;
}
.shard:hover {
cursor: pointer;
}
.shard {
color: #fff;
display: block;
float: left;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
justify-content: center;
z-index: 1;
height: 50px;
width: 50px;
margin-right: 5px;
margin-bottom: 5px;
border-radius: 2px;
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Old versions of Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome, Edge, Opera and Firefox */
}
.more-info {
/* display: none; */
position: absolute;
margin-top: 3em;
will-change: transform;
min-height: 150px;
width: 200px;
z-index: 2;
border-radius: 5px;
background-color: #333;
color: #fff;
opacity: 95%;
text-align: center;
}
</style>

View File

@@ -0,0 +1,94 @@
<script lang="ts">
import { Row, Col, Modal, Image, Button } from 'sveltestrap';
import moment from 'moment';
import { toHTML } from 'discord-markdown';
import twemoji from 'twemoji';
import { System } from '../../api/types';
export let user: System;
export let editMode: boolean;
export let isPublic: boolean;
let htmlDescription: string;
let htmlName: string;
if (user.description) {
htmlDescription = toHTML(user.description, {embed: true});
} else {
htmlDescription = "(no description)";
}
if (user.name) {
htmlName = toHTML(user.name);
}
let created = moment(user.created).format("MMM D, YYYY");
let bannerOpen = false;
const toggleBannerModal = () => (bannerOpen = !bannerOpen);
let settings = JSON.parse(localStorage.getItem("pk-settings"));
let descriptionElement: any;
let nameElement: any;
let tagElement: any;
$: if (settings && settings.appearance.twemoji) {
if (descriptionElement) twemoji.parse(descriptionElement);
if (nameElement) twemoji.parse(nameElement);
if (tagElement) twemoji.parse(tagElement);
}
</script>
<Row>
{#if user.id}
<Col xs={12} lg={4} class="mb-2">
<b>ID:</b> {user.id}
</Col>
{/if}
{#if user.name}
<Col xs={12} lg={4} class="mb-2">
<span bind:this={nameElement}><b>Name:</b> {@html htmlName}</span>
</Col>
{/if}
{#if user.tag}
<Col xs={12} lg={4} class="mb-2">
<span bind:this={tagElement}><b>Tag:</b> {user.tag}</span>
</Col>
{/if}
{#if user.created && !isPublic}
<Col xs={12} lg={4} class="mb-2">
<b>Created:</b> {created}
</Col>
{/if}
{#if user.timezone && !isPublic}
<Col xs={12} lg={4} class="mb-2">
<b>Timezone:</b> {user.timezone}
</Col>
{/if}
{#if user.color}
<Col xs={12} lg={4} class="mb-2">
<b>Color:</b> {user.color}
</Col>
{/if}
{#if user.banner}
<Col xs={12} lg={3} class="mb-2">
<b>Banner:</b> <Button size="sm" color="secondary" on:click={toggleBannerModal}>View</Button>
<Modal isOpen={bannerOpen} toggle={toggleBannerModal}>
<div slot="external" on:click={toggleBannerModal} style="height: 100%; width: max-content; max-width: 100%; margin-left: auto; margin-right: auto; display: flex;">
<Image style="display: block; margin: auto;" src={user.banner} thumbnail alt="Your system banner" />
</div>
</Modal>
</Col>
{/if}
</Row>
<div class="my-2 description" bind:this={descriptionElement}>
<b>Description:</b><br />
{@html htmlDescription}
</div>
{#if (user.banner && ((settings && settings.appearance.banner_bottom) || !settings))}
<img src={user.banner} alt="your system banner" class="w-100 mb-3 rounded" style="max-height: 12em; object-fit: cover"/>
{/if}
{#if !isPublic}
<Button style="flex: 0" color="primary" on:click={() => editMode = true}>Edit</Button>
{/if}

View File

@@ -0,0 +1,99 @@
<script lang="ts">
import { Row, Col, Input, Button, Label, Alert } from 'sveltestrap';
import autosize from 'svelte-autosize';
// import moment from 'moment-timezone';
import { currentUser } from '../../stores';
const descriptions: string[] = JSON.parse(localStorage.getItem("pk-config"))?.description_templates;
import { System } from '../../api/types';
import api from '../../api';
export let editMode: boolean;
export let user: System;
export let loading: boolean;
let err: string[] = [];
let input: System = {...user};
async function submit() {
let data = input;
err = [];
if (data.color && !/^#?[A-Fa-f0-9]{6}$/.test(input.color)) {
err.push(`"${data.color}" is not a valid color, the color must be a 6-digit hex code. (example: #ff0000)`);
} else if (data.color) {
if (data.color.startsWith("#")) {
data.color = input.color.slice(1, input.color.length);
}
}
/* if (data.timezone && !moment.tz.zone(data.timezone)) {
err.push(`"${data.timezone}" is not a valid timezone, check out <a target="_blank" style="color: var(--bs-body-color);" href="https://xske.github.io/tz/">this site</a> to see your current timezone!`);
} */
err = err;
if (err.length > 0) return;
loading = true;
try {
let res = await api().systems("@me").patch({data});
user = res;
currentUser.update(() => res);
err = [];
editMode = false;
loading = false;
} catch (error) {
console.log(error);
err.push(error.message);
err = err;
loading = false;
}
}
</script>
{#each err as error}
<Alert color="danger">{@html error}</Alert>
{/each}
<Row>
<Col xs={12} lg={4} class="mb-2">
<Label>Name:</Label>
<Input bind:value={input.name} maxlength={100} type="text" placeholder={user.name} />
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Tag:</Label>
<Input bind:value={input.tag} maxlength={100} type="text" placeholder={user.tag} />
</Col>
<!-- <Col xs={12} lg={4} class="mb-2">
<Label>Timezone:</Label>
<Input bind:value={input.timezone} type="text" placeholder={user.timezone} />
</Col> -->
<Col xs={12} lg={4} class="mb-2">
<Label>Color:</Label>
<Input bind:value={input.color} type="text" placeholder={user.color}/>
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Avatar url:</Label>
<Input bind:value={input.avatar_url} maxlength={256} type="url" placeholder={user.avatar_url}/>
</Col>
<Col xs={12} lg={4} class="mb-2">
<Label>Banner url:</Label>
<Input bind:value={input.banner} maxlength={256} type="url" placeholder={user.banner}/>
</Col>
</Row>
<div class="my-2">
<b>Description:</b><br />
{#if descriptions.length > 0 && descriptions[0].trim() != ""}
<Button size="sm" color="primary" on:click={() => input.description = descriptions[0]}>Template 1</Button>
{/if}
{#if descriptions.length > 1 && descriptions[1].trim() != ""}
<Button size="sm" color="primary" on:click={() => input.description = descriptions[1]}>Template 2</Button>
{/if}
{#if descriptions.length > 2 && descriptions[2].trim() != ""}
<Button size="sm" color="primary" on:click={() => input.description = descriptions[2]}>Template 3</Button>
{/if}
<br>
<textarea class="form-control" bind:value={input.description} maxlength={1000} use:autosize placeholder={user.description}/>
</div>
<Button style="flex: 0" color="primary" on:click={submit}>Submit</Button> <Button style="flex: 0" color="light" on:click={() => editMode = false}>Back</Button>

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import { Card, CardBody, CardHeader } from 'sveltestrap';
import FaAddressCard from 'svelte-icons/fa/FaAddressCard.svelte'
import CardsHeader from '../CardsHeader.svelte';
import Body from './Body.svelte';
import Privacy from './Privacy.svelte';
import Edit from './Edit.svelte';
import { System } from '../../api/types';
export let user: System;
export let isPublic = true;
let loading = false;
let editMode = false;
</script>
<Card class="mb-4">
<CardHeader>
<CardsHeader bind:item={user} bind:loading>
<FaAddressCard slot="icon" />
</CardsHeader>
</CardHeader>
<CardBody style="border-left: 4px solid #{user.color}">
{#if !editMode}
<Body bind:user bind:editMode bind:isPublic/>
{:else}
<Edit bind:user bind:editMode bind:loading />
{/if}
</CardBody>
</Card>
{#if !isPublic}
<Privacy bind:user />
{/if}

View File

@@ -0,0 +1,49 @@
<script lang="ts">
import { Card, CardHeader, CardBody, CardTitle, Row, Col, Button, Spinner } from 'sveltestrap';
import FaUserLock from 'svelte-icons/fa/FaUserLock.svelte';
import PrivacyEdit from './PrivacyEdit.svelte';
import { System } from '../../api/types';
export let user: System;
let editMode = false;
let loading: boolean;
</script>
<Card class="mb-4">
<CardHeader>
<CardTitle style="margin-top: 8px; outline: none;">
<div class="icon d-inline-block">
<FaUserLock />
</div> System privacy
{#if loading}<div class="d-inline-block mr-5" style="float: right;"><Spinner color="primary" /></div>{/if}
</CardTitle>
</CardHeader>
<CardBody style="border-left: 4px solid #{user.color}">
{#if editMode}
<PrivacyEdit bind:loading bind:user={user} bind:editMode/>
{:else}
<Row>
<Col xs={12} lg={4} class="mb-3">
<b>Description:</b> {user.privacy.description_privacy}
</Col>
<Col xs={12} lg={4} class="mb-3">
<b>Member list:</b> {user.privacy.member_list_privacy}
</Col>
<Col xs={12} lg={4} class="mb-3">
<b>Group list:</b> {user.privacy.group_list_privacy}
</Col>
<Col xs={12} lg={4} class="mb-3">
<b>Current front:</b> {user.privacy.front_privacy}
</Col>
<Col xs={12} lg={4} class="mb-3">
<b>Front history:</b> {user.privacy.front_history_privacy}
</Col>
</Row>
<Button style="flex: 0" color="primary" on:click={() => editMode = true}>Edit</Button>
<Button style="flex: 0" color="secondary" on:click={() => window.location.href = window.location.origin+"/dash/bulk-member-privacy"}>Bulk member privacy</Button>
<Button style="flex: 0" color="secondary" on:click={() => window.location.href = window.location.origin+"/dash/bulk-group-privacy"}>Bulk group privacy</Button>
{/if}
</CardBody>
</Card>

View File

@@ -0,0 +1,96 @@
<script lang="ts">
import { Input, Row, Col, Button, Label, Alert } from 'sveltestrap';
import { currentUser } from '../../stores';
import { System } from '../../api/types';
import api from '../../api';
export let loading = false;
export let user: System;
export let editMode: boolean;
let err: string;
let input: System = {privacy: user.privacy};
async function submit() {
let data = input;
err = null;
loading = true;
try {
let res = await api().systems("@me").patch({data});
user = res;
currentUser.update(() => res);
editMode = false;
loading = false;
} catch (error) {
console.log(error);
err = error.message;
err = err;
loading = false;
}
}
let allPrivacy: string;
$: { changePrivacy(allPrivacy)}
function changePrivacy(value: string) {
if (value) {
input.privacy.description_privacy = value;
input.privacy.member_list_privacy = value;
input.privacy.group_list_privacy = value;
input.privacy.front_privacy = value;
input.privacy.front_history_privacy = value;
}
}
</script>
{#if err}
<Alert color="danger">{err}</Alert>
{/if}
<Label><b>Set all to:</b></Label>
<Input type="select" bind:value={allPrivacy}>
<option>public</option>
<option>private</option>
</Input>
<hr />
<Row>
<Col xs={12} lg={4} class="mb-3">
<Label>Description:</Label>
<Input type="select" bind:value={input.privacy.description_privacy}>
<option default={user.privacy.description_privacy === "public"}>public</option>
<option default={user.privacy.description_privacy === "private"}>private</option>
</Input>
</Col>
<Col xs={12} lg={4} class="mb-3">
<Label>Member list:</Label>
<Input type="select" bind:value={input.privacy.member_list_privacy}>
<option default={user.privacy.member_list_privacy === "public"}>public</option>
<option default={user.privacy.member_list_privacy === "private"}>private</option>
</Input>
</Col>
<Col xs={12} lg={4} class="mb-3">
<Label>Group list:</Label>
<Input type="select" bind:value={input.privacy.group_list_privacy}>
<option default={user.privacy.group_list_privacy === "public"}>public</option>
<option default={user.privacy.group_list_privacy === "private"}>private</option>
</Input>
</Col>
<Col xs={12} lg={4} class="mb-3">
<Label>Current front:</Label>
<Input type="select" bind:value={input.privacy.front_privacy}>
<option default={user.privacy.front_privacy === "public"}>public</option>
<option default={user.privacy.front_privacy === "private"}>private</option>
</Input>
</Col>
<Col xs={12} lg={4} class="mb-3">
<Label>Front history:</Label>
<Input type="select" bind:value={input.privacy.front_history_privacy}>
<option default={user.privacy.front_history_privacy === "public"}>public</option>
<option default={user.privacy.front_history_privacy === "private"}>private</option>
</Input>
</Col>
</Row>
<Button style="flex: 0" color="primary" on:click={submit}>Submit</Button> <Button style="flex: 0" color="secondary" on:click={() => editMode = false}>Back</Button>