Merge branch 'main' of github/Draconizations/pk-webs-svelte into feat/dashboard
This commit is contained in:
104
dashboard/src/pages/BulkGroupPrivacy.svelte
Normal file
104
dashboard/src/pages/BulkGroupPrivacy.svelte
Normal file
@@ -0,0 +1,104 @@
|
||||
<script lang="ts">
|
||||
import { Container, Row, Col, Card, CardHeader, CardBody, CardTitle, Alert, Label, Input, Button, Spinner } from 'sveltestrap';
|
||||
import FaUserLock from 'svelte-icons/fa/FaUserLock.svelte';
|
||||
|
||||
import api from '../api';
|
||||
import { GroupPrivacy, System } from '../api/types';
|
||||
const user: System = JSON.parse(localStorage.getItem("pk-user"));
|
||||
|
||||
// const capitalize = (str: string) => str[0].toUpperCase() + str.substr(1);
|
||||
|
||||
let loading = false;
|
||||
let err = "";
|
||||
let success = false;
|
||||
|
||||
// kinda hacked together from typescript's Required<T> type
|
||||
const privacy: { [P in keyof GroupPrivacy]-?: string; } = {
|
||||
description_privacy: "no change",
|
||||
name_privacy: "no change",
|
||||
list_privacy: "no change",
|
||||
icon_privacy: "no change",
|
||||
visibility: "no change",
|
||||
metadata_privacy: "no change",
|
||||
};
|
||||
|
||||
const privacyNames: { [P in keyof GroupPrivacy]-?: string; } = {
|
||||
name_privacy: "Name",
|
||||
description_privacy: "Description",
|
||||
icon_privacy: "Icon",
|
||||
list_privacy: "Member list",
|
||||
metadata_privacy: "Metadata",
|
||||
visibility: "Visbility",
|
||||
};
|
||||
|
||||
async function submit() {
|
||||
success = false;
|
||||
loading = true;
|
||||
const dataArray = Object.entries(privacy).filter(([, value]) => value === "no change" ? false : true);
|
||||
const data = Object.fromEntries(dataArray);
|
||||
try {
|
||||
await api().private.bulk_privacy.group.post({ data });
|
||||
success = true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
err = error.message;
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
|
||||
function changeAll(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
Object.keys(privacy).forEach(x => privacy[x] = target.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<Row>
|
||||
<Col class="mx-auto" xs={12} lg={11} xl={10}>
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle style="margin-top: 8px; outline: none;">
|
||||
<div class="icon d-inline-block">
|
||||
<FaUserLock />
|
||||
</div> Bulk group privacy
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody style="border-left: 4px solid #{user.color}">
|
||||
{#if err}
|
||||
<Alert color="danger">{err}</Alert>
|
||||
{/if}
|
||||
{#if success}
|
||||
<Alert color="success">Group privacy updated!</Alert>
|
||||
{/if}
|
||||
<Label><b>Set all to:</b></Label>
|
||||
<Input type="select" on:change={(e) => changeAll(e)}>
|
||||
<option>no change</option>
|
||||
<option>public</option>
|
||||
<option>private</option>
|
||||
</Input>
|
||||
<hr/>
|
||||
<Row>
|
||||
{#each Object.keys(privacy) as x}
|
||||
<Col xs={12} lg={6} class="mb-3">
|
||||
<Label>{privacyNames[x]}:</Label>
|
||||
<Input type="select" bind:value={privacy[x]}>
|
||||
<option default>no change</option>
|
||||
<option>public</option>
|
||||
<option>private</option>
|
||||
</Input>
|
||||
</Col>
|
||||
{/each}
|
||||
</Row>
|
||||
|
||||
<Button color="primary" on:click={submit} bind:disabled={loading}>
|
||||
{#if loading}
|
||||
<Spinner />
|
||||
{:else}
|
||||
Submit
|
||||
{/if}
|
||||
</Button>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
106
dashboard/src/pages/BulkMemberPrivacy.svelte
Normal file
106
dashboard/src/pages/BulkMemberPrivacy.svelte
Normal file
@@ -0,0 +1,106 @@
|
||||
<script lang="ts">
|
||||
import { Container, Row, Col, Card, CardHeader, CardBody, CardTitle, Label, Input, Button, Spinner, Alert } from 'sveltestrap';
|
||||
import FaUserLock from 'svelte-icons/fa/FaUserLock.svelte';
|
||||
|
||||
import api from '../api';
|
||||
import { MemberPrivacy, System } from '../api/types';
|
||||
import Member from './Member.svelte';
|
||||
const user: System = JSON.parse(localStorage.getItem("pk-user"));
|
||||
|
||||
// const capitalize = (str: string) => str[0].toUpperCase() + str.substr(1);
|
||||
|
||||
let loading = false;
|
||||
let err = "";
|
||||
let success = false;
|
||||
|
||||
const privacy: { [P in keyof MemberPrivacy]-?: string; } = {
|
||||
description_privacy: "no change",
|
||||
name_privacy: "no change",
|
||||
avatar_privacy: "no change",
|
||||
birthday_privacy: "no change",
|
||||
pronoun_privacy: "no change",
|
||||
visibility: "no change",
|
||||
metadata_privacy: "no change",
|
||||
};
|
||||
|
||||
const privacyNames: { [P in keyof MemberPrivacy]-?: string; } = {
|
||||
avatar_privacy: "Avatar",
|
||||
birthday_privacy: "Birthday",
|
||||
description_privacy: "Description",
|
||||
metadata_privacy: "Metadata",
|
||||
name_privacy: "Name",
|
||||
pronoun_privacy: "Pronouns",
|
||||
visibility: "Visibility",
|
||||
};
|
||||
|
||||
async function submit() {
|
||||
success = false;
|
||||
loading = true;
|
||||
const dataArray = Object.entries(privacy).filter(([, value]) => value === "no change" ? false : true);
|
||||
const data = Object.fromEntries(dataArray);
|
||||
try {
|
||||
await api().private.bulk_privacy.member.post({ data });
|
||||
success = true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
err = error.message;
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
|
||||
function changeAll(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
Object.keys(privacy).forEach(x => privacy[x] = target.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<Row>
|
||||
<Col class="mx-auto" xs={12} lg={11} xl={10}>
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle style="margin-top: 8px; outline: none;">
|
||||
<div class="icon d-inline-block">
|
||||
<FaUserLock />
|
||||
</div> Bulk member privacy
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody style="border-left: 4px solid #{user.color}">
|
||||
{#if err}
|
||||
<Alert color="danger">{err}</Alert>
|
||||
{/if}
|
||||
{#if success}
|
||||
<Alert color="success">Member privacy updated!</Alert>
|
||||
{/if}
|
||||
<Label><b>Set all to:</b></Label>
|
||||
<Input type="select" on:change={(e) => changeAll(e)}>
|
||||
<option>no change</option>
|
||||
<option>public</option>
|
||||
<option>private</option>
|
||||
</Input>
|
||||
<hr/>
|
||||
<Row>
|
||||
{#each Object.keys(privacy) as x}
|
||||
<Col xs={12} lg={6} class="mb-3">
|
||||
<Label>{privacyNames[x]}:</Label>
|
||||
<Input type="select" bind:value={privacy[x]}>
|
||||
<option default>no change</option>
|
||||
<option>public</option>
|
||||
<option>private</option>
|
||||
</Input>
|
||||
</Col>
|
||||
{/each}
|
||||
</Row>
|
||||
|
||||
<Button color="primary" on:click={submit} bind:disabled={loading}>
|
||||
{#if loading}
|
||||
<Spinner />
|
||||
{:else}
|
||||
Submit
|
||||
{/if}
|
||||
</Button>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
98
dashboard/src/pages/Dash.svelte
Normal file
98
dashboard/src/pages/Dash.svelte
Normal file
@@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import { Container, Col, Row, TabContent, TabPane } from 'sveltestrap';
|
||||
import { navigate, useLocation } from "svelte-navigator";
|
||||
import { currentUser, loggedIn } from '../stores';
|
||||
|
||||
import SystemMain from '../lib/system/Main.svelte';
|
||||
import MemberList from '../lib/member/List.svelte';
|
||||
import GroupList from '../lib/group/List.svelte';
|
||||
|
||||
import { System } from '../api/types';
|
||||
import api from '../api';
|
||||
|
||||
let isPublic = false;
|
||||
|
||||
// get the state from the navigator so that we know which tab to start on
|
||||
let location = useLocation();
|
||||
let params = $location.search && new URLSearchParams($location.search);
|
||||
let tabPane: string;
|
||||
if (params) {
|
||||
tabPane = params.get("tab");
|
||||
}
|
||||
|
||||
if (!tabPane) {
|
||||
tabPane = "system";
|
||||
}
|
||||
|
||||
// subscribe to the cached user in the store
|
||||
let current;
|
||||
currentUser.subscribe(value => {
|
||||
current = value;
|
||||
});
|
||||
|
||||
// if there is no cached user, get the user from localstorage
|
||||
let user: System = current ?? JSON.parse(localStorage.getItem("pk-user"));
|
||||
// since the user in localstorage can be outdated, fetch the user from the api again
|
||||
if (!current) {
|
||||
login(localStorage.getItem("pk-token"));
|
||||
}
|
||||
|
||||
// if there's no user, and there's no token, assume the login failed and send us back to the homepage.
|
||||
if (!localStorage.getItem("pk-token") && !user) {
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
let settings = JSON.parse(localStorage.getItem("pk-settings"));
|
||||
|
||||
// just the login function
|
||||
async function login(token: string) {
|
||||
try {
|
||||
if (!token) {
|
||||
throw new Error("Token cannot be empty.")
|
||||
}
|
||||
const res: System = await api().systems("@me").get({ token });
|
||||
localStorage.setItem("pk-token", token);
|
||||
localStorage.setItem("pk-user", JSON.stringify(res));
|
||||
loggedIn.update(() => true);
|
||||
currentUser.update(() => res);
|
||||
user = res;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
localStorage.removeItem("pk-token");
|
||||
localStorage.removeItem("pk-user");
|
||||
currentUser.update(() => null);
|
||||
navigate("/");
|
||||
}
|
||||
}
|
||||
|
||||
// some values that get passed from the member to the group components and vice versa
|
||||
let members = [];
|
||||
let groups = [];
|
||||
|
||||
</script>
|
||||
|
||||
<!-- display the banner if there's a banner set, and if the current settings allow for it-->
|
||||
{#if user && user.banner && ((settings && settings.appearance.banner_top) || !settings)}
|
||||
<div class="banner" style="background-image: url({user.banner})" />
|
||||
{/if}
|
||||
<Container>
|
||||
<Row>
|
||||
<Col class="mx-auto" xs={12} lg={11} xl={10}>
|
||||
<TabContent class="mt-3">
|
||||
<TabPane tabId="system" tab="System" active={tabPane === "system"}>
|
||||
<SystemMain bind:user={user} bind:isPublic />
|
||||
</TabPane>
|
||||
<TabPane tabId="members" tab="Members" active={tabPane === "members"}>
|
||||
<MemberList bind:groups={groups} bind:list={members} bind:isPublic />
|
||||
</TabPane>
|
||||
<TabPane tabId="groups" tab="Groups" active={tabPane === "groups"}>
|
||||
<GroupList bind:members={members} bind:list={groups} bind:isPublic />
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
<svelte:head>
|
||||
<title>pk-webs | dash</title>
|
||||
</svelte:head>
|
||||
34
dashboard/src/pages/DiscordLogin.svelte
Normal file
34
dashboard/src/pages/DiscordLogin.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { Container, Row, Col } from 'sveltestrap';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import api from '../api';
|
||||
|
||||
let text = "Loading...";
|
||||
|
||||
onMount(async () =>
|
||||
{
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const paramkeys = [...params.keys()];
|
||||
if (paramkeys.includes("code"))
|
||||
{
|
||||
const res = await api().private.discord.callback.post({ data: { code: params.get("code"), redirect_domain: window.location.origin } });
|
||||
localStorage.setItem("pk-token", res.token);
|
||||
localStorage.setItem("pk-user", JSON.stringify(res.system));
|
||||
localStorage.setItem("pk-config", JSON.stringify(res.config));
|
||||
window.location.href = window.location.origin;
|
||||
}
|
||||
else
|
||||
{
|
||||
text = "Error: " + params.get("error_description");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<Row>
|
||||
<Col class="mx-auto" xs={12} lg={11} xl={10}>
|
||||
{text}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
238
dashboard/src/pages/Group.svelte
Normal file
238
dashboard/src/pages/Group.svelte
Normal file
@@ -0,0 +1,238 @@
|
||||
<script lang="ts">
|
||||
import { Container, Row, Col, Alert, Spinner, Card, CardHeader, CardBody, Accordion, AccordionItem, CardTitle } from "sveltestrap";
|
||||
import Body from '../lib/group/Body.svelte';
|
||||
import MemberBody from '../lib/member/Body.svelte';
|
||||
import { useParams, Link } from 'svelte-navigator';
|
||||
import { onMount } from 'svelte';
|
||||
import api from "../api";
|
||||
import { Member, Group } from "../api/types";
|
||||
import CardsHeader from "../lib/CardsHeader.svelte";
|
||||
import FaUsers from 'svelte-icons/fa/FaUsers.svelte';
|
||||
import FaList from 'svelte-icons/fa/FaList.svelte';
|
||||
import FaUserCircle from 'svelte-icons/fa/FaUserCircle.svelte';
|
||||
import ListPagination from '../lib/ListPagination.svelte';
|
||||
import FaLock from 'svelte-icons/fa/FaLock.svelte'
|
||||
|
||||
let loading = true;
|
||||
let memberLoading = false;
|
||||
let params = useParams();
|
||||
let err = "";
|
||||
let memberErr = "";
|
||||
let group: Group;
|
||||
let members: Member[] = [];
|
||||
let systemMembers: Group[] = [];
|
||||
let isMainDash = false;
|
||||
let isDeleted = false;
|
||||
let notOwnSystem = false;
|
||||
|
||||
const isPage = true;
|
||||
export let isPublic = true;
|
||||
let settings = JSON.parse(localStorage.getItem("pk-settings"));
|
||||
|
||||
let currentPage = 1;
|
||||
let itemsPerPage = settings && settings.accessibility && settings.accessibility.expandedcards ? 5 : 10;
|
||||
|
||||
$: indexOfLastItem = currentPage * itemsPerPage;
|
||||
$: indexOfFirstItem = indexOfLastItem - itemsPerPage;
|
||||
$: pageAmount = Math.ceil(members.length / itemsPerPage);
|
||||
|
||||
$: orderedMembers = members.sort((a, b) => a.name.localeCompare(b.name));
|
||||
$: slicedMembers = orderedMembers.slice(indexOfFirstItem, indexOfLastItem);
|
||||
|
||||
onMount(() => {
|
||||
fetchGroup();
|
||||
});
|
||||
|
||||
async function fetchGroup() {
|
||||
try {
|
||||
group = await api().groups($params.id).get({auth: !isPublic});
|
||||
if (!isPublic && !group.privacy) {
|
||||
notOwnSystem = true;
|
||||
throw new Error("Group is not from own system.");
|
||||
}
|
||||
err = "";
|
||||
loading = false;
|
||||
memberLoading = true;
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
fetchMembers();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
err = error.message;
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchMembers() {
|
||||
try {
|
||||
members = await api().groups($params.id).members().get({auth: !isPublic});
|
||||
group.members = members.map(function(member) {return member.uuid});
|
||||
if (!isPublic) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
systemMembers = await api().systems("@me").members.get({ auth: true });
|
||||
}
|
||||
memberErr = "";
|
||||
memberLoading = false;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
memberErr = error.message;
|
||||
memberLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateMembers() {
|
||||
memberLoading = true;
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
fetchMembers();
|
||||
}
|
||||
|
||||
function updateDelete() {
|
||||
isDeleted = true;
|
||||
}
|
||||
|
||||
function updateMemberList(event: any) {
|
||||
members = members.map(member => member.id !== event.detail.id ? member : event.detail);
|
||||
systemMembers = systemMembers.map(member => member.id !== event.detail.id ? member : event.detail);
|
||||
}
|
||||
|
||||
function deleteMemberFromList(event: any) {
|
||||
members = members.filter(member => member.id !== event.detail);
|
||||
systemMembers = systemMembers.filter(member => member.id !== event.detail);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if settings && settings.appearance.color_background && !notOwnSystem}
|
||||
<div class="background" style="background-color: {group && `#${group.color}`}"></div>
|
||||
{/if}
|
||||
{#if group && group.banner && settings && settings.appearance.banner_top && !notOwnSystem}
|
||||
<div class="banner" style="background-image: url({group.banner})" />
|
||||
{/if}
|
||||
<Container>
|
||||
<Row>
|
||||
<Col class="mx-auto" xs={12} lg={11} xl={10}>
|
||||
{#if isDeleted}
|
||||
<Alert color="success">Group has been successfully deleted. <Link to="/dash">Return to dash</Link></Alert>
|
||||
{:else}
|
||||
{#if isPublic}
|
||||
<Alert color="info">You are currently <b>viewing</b> a group.</Alert>
|
||||
{/if}
|
||||
{#if notOwnSystem}
|
||||
<Alert color="danger">This group does not belong to your system, did you mean to look up <Link to={`/profile/g/${group.id}`}>it's public page</Link>?</Alert>
|
||||
{:else if err}
|
||||
<Alert color="danger">{@html err}</Alert>
|
||||
{:else if loading}
|
||||
<Spinner/>
|
||||
{:else if group && group.id}
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<CardsHeader bind:item={group}>
|
||||
<FaUsers slot="icon" />
|
||||
</CardsHeader>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Body on:deletion={updateDelete} on:updateMembers={updateMembers} bind:members={systemMembers} bind:group={group} isPage={isPage} isPublic={isPublic}/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/if}
|
||||
{#if memberLoading}
|
||||
<Alert color="primary"><Spinner size="sm" /> Fetching members...</Alert>
|
||||
{:else if memberErr}
|
||||
<Alert color="danger">{memberErr}</Alert>
|
||||
{:else if members && members.length > 0}
|
||||
<Card class="mb-2">
|
||||
<CardHeader>
|
||||
<CardTitle style="margin-top: 8px; outline: none;">
|
||||
<div class="icon d-inline-block">
|
||||
<FaList />
|
||||
</div> Group list
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<ListPagination bind:currentPage bind:pageAmount />
|
||||
{#if settings && settings.accessibility ? (!settings.accessibility.expandedcards && !settings.accessibility.pagelinks) : true}
|
||||
<Accordion class="mb-3" stayOpen>
|
||||
{#each slicedMembers as member, index (member.id)}
|
||||
{#if (!isPublic && member.privacy.visibility === "public") || isPublic}
|
||||
<AccordionItem>
|
||||
<CardsHeader bind:item={member} slot="header">
|
||||
<FaUserCircle slot="icon" />
|
||||
</CardsHeader>
|
||||
<MemberBody on:update={updateMemberList} isMainDash={isMainDash} on:deletion={deleteMemberFromList} bind:member bind:isPublic={isPublic}/>
|
||||
</AccordionItem>
|
||||
{:else}
|
||||
<AccordionItem>
|
||||
<CardsHeader bind:item={member} slot="header">
|
||||
<FaLock slot="icon" />
|
||||
</CardsHeader>
|
||||
<MemberBody on:update={updateMemberList} isMainDash={isMainDash} on:deletion={deleteMemberFromList} bind:member bind:isPublic={isPublic}/>
|
||||
</AccordionItem>
|
||||
{/if}
|
||||
{/each}
|
||||
</Accordion>
|
||||
{:else if settings.accessibility.expandedcards}
|
||||
{#each slicedMembers 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>
|
||||
<MemberBody on:update={updateMemberList} isMainDash={isMainDash} on:deletion={deleteMemberFromList} bind:member bind:isPublic={isPublic}/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{:else}
|
||||
<Card class="mb-3">
|
||||
<CardHeader>
|
||||
<CardsHeader bind:item={member}>
|
||||
<FaLock slot="icon" />
|
||||
</CardsHeader>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<MemberBody on:update={updateMemberList} isMainDash={isMainDash} on:deletion={deleteMemberFromList} bind:member bind:isPublic={isPublic}/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="my-3">
|
||||
{#each slicedMembers as member, index (member.id)}
|
||||
{#if (!isPublic && member.privacy.visibility === "public") || isPublic}
|
||||
<Card>
|
||||
<Link class="accordion-button collapsed" style="text-decoration: none;" to={!isPublic ? `/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={!isPublic ? `/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}
|
||||
{/if}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
<style>
|
||||
.background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-height: 100%;
|
||||
z-index: -30;
|
||||
}
|
||||
</style>
|
||||
|
||||
137
dashboard/src/pages/Home.svelte
Normal file
137
dashboard/src/pages/Home.svelte
Normal file
@@ -0,0 +1,137 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Container, Card, CardHeader, CardBody, CardTitle, Col, Row, Spinner, Input, Button, Label, Alert } from 'sveltestrap';
|
||||
import FaLockOpen from 'svelte-icons/fa/FaLockOpen.svelte';
|
||||
import { loggedIn, currentUser } from '../stores';
|
||||
import { Link } from 'svelte-navigator';
|
||||
import twemoji from 'twemoji';
|
||||
import { toHTML } from 'discord-markdown';
|
||||
|
||||
import { System } from '../api/types';
|
||||
import api from '../api';
|
||||
|
||||
let loading = false;
|
||||
let err: string;
|
||||
let token: string;
|
||||
|
||||
let isLoggedIn: boolean;
|
||||
let user;
|
||||
|
||||
loggedIn.subscribe(value => {
|
||||
isLoggedIn = value;
|
||||
});
|
||||
|
||||
currentUser.subscribe(value => {
|
||||
user = value;
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
if (localStorage.getItem("pk-token")) {
|
||||
login(localStorage.getItem("pk-token"));
|
||||
}
|
||||
});
|
||||
|
||||
async function login(token: string) {
|
||||
loading = true;
|
||||
try {
|
||||
if (!token) {
|
||||
throw new Error("Token cannot be empty.")
|
||||
}
|
||||
const res: System = await api().systems("@me").get({ token });
|
||||
localStorage.setItem("pk-token", token);
|
||||
localStorage.setItem("pk-user", JSON.stringify(res));
|
||||
const settings = await api().systems("@me").settings.get({ token });
|
||||
localStorage.setItem("pk-config", JSON.stringify(settings));
|
||||
err = null;
|
||||
loggedIn.update(() => true);
|
||||
currentUser.update(() => res);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
localStorage.removeItem("pk-token");
|
||||
localStorage.removeItem("pk-user");
|
||||
currentUser.update(() => null);
|
||||
err = error.message;
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
|
||||
|
||||
function logout() {
|
||||
token = null;
|
||||
localStorage.removeItem("pk-token");
|
||||
localStorage.removeItem("pk-user");
|
||||
loggedIn.update(() => false);
|
||||
currentUser.update(() => null);
|
||||
}
|
||||
|
||||
let settings = JSON.parse(localStorage.getItem("pk-settings"));
|
||||
let welcomeElement: any;
|
||||
let htmlName: string;
|
||||
$: if (user && user.name) {
|
||||
htmlName = toHTML(user.name);
|
||||
}
|
||||
$: if (settings && settings.appearance.twemoji) {
|
||||
if (welcomeElement) twemoji.parse(welcomeElement);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<Row>
|
||||
<Col class="mx-auto" xs={12} lg={11} xl={10}>
|
||||
{#if err}
|
||||
<Alert color="danger" >{err}</Alert>
|
||||
{/if}
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle style="margin-top: 8px; outline: none;">
|
||||
<div class="icon d-inline-block">
|
||||
<FaLockOpen />
|
||||
</div>Log in {#if loading} <div style="float: right"><Spinner color="primary" /></div> {/if}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{#if loading}
|
||||
verifying login...
|
||||
{:else if isLoggedIn}
|
||||
{#if user && user.name}
|
||||
<p bind:this={welcomeElement}>Welcome, <b>{@html htmlName}</b>!</p>
|
||||
{:else}
|
||||
<p>Welcome!</p>
|
||||
{/if}
|
||||
<Link to="/dash"><Button style="float: left;" color='primary'>Go to dash</Button></Link><Button style="float: right;" color='danger' on:click={logout}>Log out</Button>
|
||||
{:else}
|
||||
<Row>
|
||||
<Label>Enter your token here. You can get this by using <b>pk;token</b></Label>
|
||||
<Col xs={12} md={10}>
|
||||
<Input class="mb-2 mb-md-0" type="text" bind:value={token}/>
|
||||
</Col>
|
||||
<Col xs={12} md={2}>
|
||||
<Button style="width: 100%" color="primary" on:click={() => login(token)}>Submit</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<br>
|
||||
<Row>
|
||||
<Label>Or, you can</Label>
|
||||
<Col xs={12}>
|
||||
<Button color="dark" on:click={() => window.location.href = `https://discord.com/api/oauth2/authorize?client_id=${localStorage.isBeta ? "912009351160541225" : "466378653216014359"}&redirect_uri=${encodeURIComponent(window.location.origin + "/login/discord")}&response_type=code&scope=guilds%20identify`}>
|
||||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.54 0c1.356 0 2.46 1.104 2.46 2.472v21.528l-2.58-2.28-1.452-1.344-1.536-1.428.636 2.22h-13.608c-1.356 0-2.46-1.104-2.46-2.472v-16.224c0-1.368 1.104-2.472 2.46-2.472h16.08zm-4.632 15.672c2.652-.084 3.672-1.824 3.672-1.824 0-3.864-1.728-6.996-1.728-6.996-1.728-1.296-3.372-1.26-3.372-1.26l-.168.192c2.04.624 2.988 1.524 2.988 1.524-1.248-.684-2.472-1.02-3.612-1.152-.864-.096-1.692-.072-2.424.024l-.204.024c-.42.036-1.44.192-2.724.756-.444.204-.708.348-.708.348s.996-.948 3.156-1.572l-.12-.144s-1.644-.036-3.372 1.26c0 0-1.728 3.132-1.728 6.996 0 0 1.008 1.74 3.66 1.824 0 0 .444-.54.804-.996-1.524-.456-2.1-1.416-2.1-1.416l.336.204.048.036.047.027.014.006.047.027c.3.168.6.3.876.408.492.192 1.08.384 1.764.516.9.168 1.956.228 3.108.012.564-.096 1.14-.264 1.74-.516.42-.156.888-.384 1.38-.708 0 0-.6.984-2.172 1.428.36.456.792.972.792.972zm-5.58-5.604c-.684 0-1.224.6-1.224 1.332 0 .732.552 1.332 1.224 1.332.684 0 1.224-.6 1.224-1.332.012-.732-.54-1.332-1.224-1.332zm4.38 0c-.684 0-1.224.6-1.224 1.332 0 .732.552 1.332 1.224 1.332.684 0 1.224-.6 1.224-1.332 0-.732-.54-1.332-1.224-1.332z"/>
|
||||
</svg>
|
||||
Login with Discord
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
||||
{#if isLoggedIn}
|
||||
<Card class="mb-4">
|
||||
<CardBody>
|
||||
Some cool stuff will go here.
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/if}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
243
dashboard/src/pages/Member.svelte
Normal file
243
dashboard/src/pages/Member.svelte
Normal file
@@ -0,0 +1,243 @@
|
||||
<script lang="ts">
|
||||
import { Container, Row, Col, Alert, Spinner, Card, CardHeader, CardBody, Accordion, AccordionItem, CardTitle } from "sveltestrap";
|
||||
import Body from '../lib/member/Body.svelte';
|
||||
import GroupBody from '../lib/group/Body.svelte';
|
||||
import { useParams, Link } from 'svelte-navigator';
|
||||
import { onMount } from 'svelte';
|
||||
import api from "../api";
|
||||
import { Member, Group } from "../api/types";
|
||||
import CardsHeader from "../lib/CardsHeader.svelte";
|
||||
import FaAddressCard from 'svelte-icons/fa/FaAddressCard.svelte'
|
||||
import FaUsers from 'svelte-icons/fa/FaUsers.svelte'
|
||||
import FaLock from 'svelte-icons/fa/FaLock.svelte'
|
||||
import FaList from 'svelte-icons/fa/FaList.svelte'
|
||||
import ListPagination from '../lib/ListPagination.svelte';
|
||||
|
||||
let loading = true;
|
||||
let groupLoading = false;
|
||||
let params = useParams();
|
||||
let err = "";
|
||||
let groupErr = "";
|
||||
let member: Member;
|
||||
let groups: Group[] = [];
|
||||
let systemGroups: Group[] = [];
|
||||
let systemMembers: Member[] = [];
|
||||
let isMainDash = false;
|
||||
let isDeleted = false;
|
||||
let notOwnSystem = false;
|
||||
|
||||
const isPage = true;
|
||||
export let isPublic = true;
|
||||
let settings = JSON.parse(localStorage.getItem("pk-settings"));
|
||||
|
||||
let currentPage = 1;
|
||||
let itemsPerPage = settings && settings.accessibility && settings.accessibility.expandedcards ? 5 : 10;
|
||||
|
||||
$: indexOfLastItem = currentPage * itemsPerPage;
|
||||
$: indexOfFirstItem = indexOfLastItem - itemsPerPage;
|
||||
$: pageAmount = Math.ceil(groups.length / itemsPerPage);
|
||||
|
||||
$: orderedGroups = groups.sort((a, b) => a.name.localeCompare(b.name));
|
||||
$: slicedGroups = orderedGroups.slice(indexOfFirstItem, indexOfLastItem);
|
||||
|
||||
onMount(() => {
|
||||
fetchMember();
|
||||
});
|
||||
|
||||
async function fetchMember() {
|
||||
try {
|
||||
member = await api().members($params.id).get({auth: !isPublic});
|
||||
if (!isPublic && !member.privacy) {
|
||||
notOwnSystem = true;
|
||||
throw new Error("Member is not from own system.");
|
||||
}
|
||||
err = "";
|
||||
loading = false;
|
||||
groupLoading = true;
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
fetchGroups();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
err = error.message;
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchGroups() {
|
||||
try {
|
||||
groups = await api().members($params.id).groups().get({auth: !isPublic, query: { with_members: !isPublic } });
|
||||
if (!isPublic) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
systemGroups = await api().systems("@me").groups.get({ auth: true, query: { with_members: true } });
|
||||
}
|
||||
groupErr = "";
|
||||
groupLoading = false;
|
||||
// we can't use with_members from a group list from a member endpoint yet, but I'm leaving this in in case we do
|
||||
// (this is needed for editing a group member list from the member page)
|
||||
/* if (!isPublic) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
systemMembers = await api().systems("@me").members.get({auth: true});
|
||||
} */
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
groupErr = error.message;
|
||||
groupLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateGroups() {
|
||||
groupLoading = true;
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
fetchGroups();
|
||||
}
|
||||
|
||||
function updateDelete() {
|
||||
isDeleted = true;
|
||||
}
|
||||
|
||||
function updateGroupList(event: any) {
|
||||
groups = groups.map(group => group.id !== event.detail.id ? group : event.detail);
|
||||
systemGroups = systemGroups.map(group => group.id !== event.detail.id ? group : event.detail);
|
||||
}
|
||||
|
||||
function deleteGroupFromList(event: any) {
|
||||
groups = groups.filter(group => group.id !== event.detail);
|
||||
systemGroups = systemGroups.filter(group => group.id !== event.detail);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if settings && settings.appearance.color_background && !notOwnSystem}
|
||||
<div class="background" style="background-color: {member && `#${member.color}`}"></div>
|
||||
{/if}
|
||||
{#if member && member.banner && settings && settings.appearance.banner_top && !notOwnSystem}
|
||||
<div class="banner" style="background-image: url({member.banner})" />
|
||||
{/if}
|
||||
<Container>
|
||||
<Row>
|
||||
<Col class="mx-auto" xs={12} lg={11} xl={10}>
|
||||
{#if isDeleted}
|
||||
<Alert color="success">Member has been successfully deleted. <Link to="/dash">Return to dash</Link></Alert>
|
||||
{:else}
|
||||
{#if isPublic}
|
||||
<Alert color="info">You are currently <b>viewing</b> a member.</Alert>
|
||||
{/if}
|
||||
{#if notOwnSystem}
|
||||
<Alert color="danger">This member does not belong to your system, did you mean to look up <Link to={`/profile/m/${member.id}`}>their public page</Link>?</Alert>
|
||||
{:else if err}
|
||||
<Alert color="danger">{@html err}</Alert>
|
||||
{:else if loading}
|
||||
<Spinner/>
|
||||
{:else if member && member.id}
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<CardsHeader bind:item={member}>
|
||||
<FaAddressCard slot="icon" />
|
||||
</CardsHeader>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Body on:deletion={updateDelete} on:updateGroups={updateGroups} bind:groups={systemGroups} bind:member={member} isPage={isPage} isPublic={isPublic}/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/if}
|
||||
{#if groupLoading}
|
||||
<Alert color="primary"><Spinner size="sm" /> Fetching groups...</Alert>
|
||||
{:else if groupErr}
|
||||
<Alert color="danger">{groupErr}</Alert>
|
||||
{:else if groups && groups.length > 0}
|
||||
<Card class="mb-2">
|
||||
<CardHeader>
|
||||
<CardTitle style="margin-top: 8px; outline: none;">
|
||||
<div class="icon d-inline-block">
|
||||
<FaList />
|
||||
</div> Member groups
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<ListPagination bind:currentPage bind:pageAmount />
|
||||
{#if settings && settings.accessibility ? (!settings.accessibility.expandedcards && !settings.accessibility.pagelinks) : true}
|
||||
<Accordion class="mb-3" stayOpen>
|
||||
{#each slicedGroups as group, index (group.id)}
|
||||
{#if (!isPublic && group.privacy.visibility === "public") || isPublic}
|
||||
<AccordionItem>
|
||||
<CardsHeader bind:item={group} slot="header">
|
||||
<FaUsers slot="icon" />
|
||||
</CardsHeader>
|
||||
<GroupBody bind:members={systemMembers} on:update={updateGroupList} isMainDash={isMainDash} on:deletion={deleteGroupFromList} bind:group bind:isPublic={isPublic}/>
|
||||
</AccordionItem>
|
||||
{:else}
|
||||
<AccordionItem>
|
||||
<CardsHeader bind:item={group} slot="header">
|
||||
<FaLock slot="icon" />
|
||||
</CardsHeader>
|
||||
<GroupBody bind:members={systemMembers} on:update={updateGroupList} isMainDash={isMainDash} on:deletion={deleteGroupFromList} bind:group bind:isPublic={isPublic}/>
|
||||
</AccordionItem>
|
||||
{/if}
|
||||
{/each}
|
||||
</Accordion>
|
||||
{:else if settings.accessibility.expandedcards}
|
||||
{#each slicedGroups as group, index (group.id)}
|
||||
{#if (!isPublic && group.privacy.visibility === "public") || isPublic}
|
||||
<Card class="mb-3">
|
||||
<CardHeader>
|
||||
<CardsHeader bind:item={group} >
|
||||
<FaUsers slot="icon" />
|
||||
</CardsHeader>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<GroupBody bind:members={systemMembers} on:update={updateGroupList} isMainDash={isMainDash} on:deletion={deleteGroupFromList} bind:group bind:isPublic={isPublic}/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{:else}
|
||||
<Card class="mb-3">
|
||||
<CardHeader>
|
||||
<CardsHeader bind:item={group} >
|
||||
<FaLock slot="icon" />
|
||||
</CardsHeader>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<GroupBody bind:members={systemMembers} on:update={updateGroupList} isMainDash={isMainDash} on:deletion={deleteGroupFromList} bind:group bind:isPublic={isPublic}/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="my-3">
|
||||
{#each slicedGroups 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}>
|
||||
<FaUsers slot="icon" />
|
||||
</CardsHeader>
|
||||
</Link>
|
||||
</Card>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<ListPagination bind:currentPage bind:pageAmount />
|
||||
{/if}
|
||||
{/if}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
<style>
|
||||
.background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-height: 100%;
|
||||
z-index: -30;
|
||||
}
|
||||
</style>
|
||||
19
dashboard/src/pages/PageNotFound.svelte
Normal file
19
dashboard/src/pages/PageNotFound.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Container, Card, CardHeader, CardBody, Row, Col, CardTitle } from 'sveltestrap';
|
||||
import { Link } from 'svelte-navigator';
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<Row>
|
||||
<Col class="mx-auto" xs={12} lg={11} xl={10}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle style="margin-top: 8px; outline: none;">404. Page not found</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
Looks like this page doesn't exist. <Link to="/">Go home</Link>?
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
92
dashboard/src/pages/Public.svelte
Normal file
92
dashboard/src/pages/Public.svelte
Normal file
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import { Container, Row, Col, Card, CardHeader, CardTitle, CardBody, Input, Button } from 'sveltestrap';
|
||||
import { Link, navigate } from 'svelte-navigator';
|
||||
import FaRocket from 'svelte-icons/fa/FaRocket.svelte';
|
||||
import FaStar from 'svelte-icons/fa/FaStar.svelte'
|
||||
import FaMoon from 'svelte-icons/fa/FaMoon.svelte'
|
||||
|
||||
let sysInput: string = "";
|
||||
let memberInput: string = "";
|
||||
let groupInput: string = "";
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<Row>
|
||||
<Col class="mx-auto" xs={12} lg={11} xl={12}>
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle style="margin-top: 8px; outline: none;">
|
||||
<div class="icon d-inline-block">
|
||||
<FaRocket />
|
||||
</div>System Profile
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
Submit a <b>system ID</b> to view that system's profile.
|
||||
<Row>
|
||||
<Col xs={12} lg={9} class="my-2">
|
||||
<Input on:keyup={(event) => {if (event.key === "Enter" && sysInput !== "") navigate(`/profile/s/${sysInput.toLowerCase().trim()}`)}} bind:value={sysInput} />
|
||||
</Col>
|
||||
<Col xs={12} lg={3} class="my-2 d-flex">
|
||||
{#if sysInput !== ""}
|
||||
<Link style="text-decoration: none; flex: 1 0 auto" to="/profile/s/{sysInput.toLowerCase().trim()}"><Button class="w-100" color="primary">View</Button></Link>
|
||||
{:else}
|
||||
<Button class="w-100" disabled color="primary">View</Button>
|
||||
{/if}
|
||||
</Col>
|
||||
</Row>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle style="margin-top: 8px; outline: none;">
|
||||
<div class="icon d-inline-block">
|
||||
<FaStar />
|
||||
</div>Member Card
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
Submit a <b>member ID</b> to view that member's profile.
|
||||
<Row>
|
||||
<Col xs={12} lg={9} class="my-2">
|
||||
<Input on:keyup={(event) => {if (event.key === "Enter" && memberInput !== "") navigate(`/profile/m/${memberInput.toLowerCase().trim()}`)}} bind:value={memberInput} />
|
||||
</Col>
|
||||
<Col xs={12} lg={3} class="my-2 d-flex">
|
||||
{#if memberInput !== ""}
|
||||
<Link style="text-decoration: none; flex: 1 0 auto" to="/profile/m/{memberInput.toLowerCase().trim()}"><Button class="w-100" color="primary">View</Button></Link>
|
||||
{:else}
|
||||
<Button class="w-100" disabled color="primary">View</Button>
|
||||
{/if}
|
||||
</Col>
|
||||
</Row>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle style="margin-top: 8px; outline: none;">
|
||||
<div class="icon d-inline-block">
|
||||
<FaMoon />
|
||||
</div>Group Card
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
Submit a <b>group ID</b> to view that group's profile.
|
||||
<Row>
|
||||
<Col xs={12} lg={9} class="my-2">
|
||||
<Input on:keyup={(event) => {if (event.key === "Enter" && groupInput !== "") navigate(`/profile/g/${groupInput.toLowerCase().trim()}`)}} bind:value={groupInput} />
|
||||
</Col>
|
||||
<Col xs={12} lg={3} class="my-2 d-flex">
|
||||
{#if groupInput !== ""}
|
||||
<Link style="text-decoration: none; flex: 1 0 auto" to="/profile/g/{groupInput.toLowerCase().trim()}"><Button class="w-100" color="primary">View</Button></Link>
|
||||
{:else}
|
||||
<Button class="w-100" disabled color="primary">View</Button>
|
||||
{/if}
|
||||
</Col>
|
||||
</Row>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
134
dashboard/src/pages/Settings.svelte
Normal file
134
dashboard/src/pages/Settings.svelte
Normal file
@@ -0,0 +1,134 @@
|
||||
<script lang="ts">
|
||||
import { Card, CardHeader, CardBody, Container, Row, Col, CardTitle, Tooltip, Button } from 'sveltestrap';
|
||||
import Toggle from 'svelte-toggle';
|
||||
import autosize from 'svelte-autosize';
|
||||
import FaCogs from 'svelte-icons/fa/FaCogs.svelte'
|
||||
import { Config } from '../api/types';
|
||||
import api from '../api';
|
||||
|
||||
let savedSettings = JSON.parse(localStorage.getItem("pk-settings"));
|
||||
let apiConfig: Config = JSON.parse(localStorage.getItem("pk-config"));
|
||||
|
||||
let settings = {
|
||||
appearance: {
|
||||
banner_top: false,
|
||||
banner_bottom: true,
|
||||
gradient_background: false,
|
||||
color_background: false,
|
||||
twemoji: false
|
||||
},
|
||||
accessibility: {
|
||||
opendyslexic: false,
|
||||
pagelinks: false,
|
||||
expandedcards: false
|
||||
}
|
||||
};
|
||||
|
||||
if (savedSettings) {
|
||||
settings = {...settings, ...savedSettings}
|
||||
};
|
||||
|
||||
let descriptions = apiConfig.description_templates;
|
||||
|
||||
async function saveDescriptionTemplates() {
|
||||
const res = await api().systems("@me").settings.patch({ data: { description_templates: descriptions } });
|
||||
localStorage.setItem("pk-config", JSON.stringify(res));
|
||||
}
|
||||
|
||||
function toggleOpenDyslexic() {
|
||||
if (settings.accessibility.opendyslexic) document.getElementById("app").classList.add("dyslexic");
|
||||
else document.getElementById("app").classList.remove("dyslexic");
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<Row>
|
||||
<Col class="mx-auto" xs={12} lg={11} xl={10}>
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle style="margin-top: 8px; outline: none;">
|
||||
<div class="icon d-inline-block">
|
||||
<FaCogs />
|
||||
</div>Personal settings
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<p>These settings are saved in your localstorage. This means that you have to reapply these every time you visit in a different browser, or clear your browser's cookies.</p>
|
||||
<h4>Appearance</h4>
|
||||
<hr/>
|
||||
<Row class="mb-3">
|
||||
<Col xs={12} lg={4} class="mb-2">
|
||||
<span id="s-bannertop">Show banners in the background?</span> <Toggle hideLabel style="display: inline" label="Remove banner from background" toggled={settings.appearance.banner_top} on:toggle={() => {settings.appearance.banner_top = !settings.appearance.banner_top; localStorage.setItem("pk-settings", JSON.stringify(settings));}}/>
|
||||
<Tooltip target="s-bannertop" placement="bottom">If enabled, shows banners from the top of the system, member and group pages.</Tooltip>
|
||||
</Col>
|
||||
<Col xs={12} lg={4} class="mb-2">
|
||||
<span id="s-bannerbottom">Show banners at the bottom of cards?</span> <Toggle hideLabel style="display: inline" label="Remove banner from bottom" toggled={settings.appearance.banner_bottom} on:toggle={() => {settings.appearance.banner_bottom = !settings.appearance.banner_bottom; localStorage.setItem("pk-settings", JSON.stringify(settings));}}/>
|
||||
<Tooltip target="s-bannerbottom" placement="bottom">If enabled, shows banners at the bottom of the system, member and group cards.</Tooltip>
|
||||
</Col>
|
||||
<Col xs={12} lg={4} class="mb-2">
|
||||
<span id="s-twemoji">Use twemoji?</span> <Toggle hideLabel style="display: inline" label="Convert to twemoji" toggled={settings.appearance.twemoji} on:toggle={() => {settings.appearance.twemoji = !settings.appearance.twemoji; localStorage.setItem("pk-settings", JSON.stringify(settings));}}/>
|
||||
<Tooltip target="s-twemoji" placement="bottom">If enabled, converts all emojis into twemoji.</Tooltip>
|
||||
</Col>
|
||||
<Col xs={12} lg={4} class="mb-2">
|
||||
<span id="s-colorbackground">Colored background?</span> <Toggle hideLabel style="display: inline" label="Member color as background" toggled={settings.appearance.color_background} on:toggle={() => {settings.appearance.color_background = !settings.appearance.color_background; localStorage.setItem("pk-settings", JSON.stringify(settings));}}/>
|
||||
<Tooltip target="s-colorbackground" placement="bottom">If enabled, turns the background on member pages into the member's color.</Tooltip>
|
||||
</Col>
|
||||
</Row>
|
||||
<h4>Accessibility</h4>
|
||||
<hr/>
|
||||
<Row>
|
||||
<!-- <Col xs={12} lg={4} class="mb-2">
|
||||
<span id="s-opendyslexic">Use the opendyslexic font?</span> <Toggle hideLabel style="display: inline" label="Use the opendyslexic font" toggled={settings.accessibility.opendyslexic} on:toggle={() => {settings.accessibility.opendyslexic = !settings.accessibility.opendyslexic; localStorage.setItem("pk-settings", JSON.stringify(settings)); toggleOpenDyslexic();}}/>
|
||||
<Tooltip target="s-opendyslexic" placement="bottom">If enabled, uses the opendyslexic font as it's main font.</Tooltip>
|
||||
</Col> -->
|
||||
<Col xs={12} lg={4} class="mb-2">
|
||||
<span id="s-expandedcards">Expand cards by default?</span> <Toggle hideLabel style="display: inline" label="Expand cards by default" toggled={settings.accessibility.expandedcards} on:toggle={() => {settings.accessibility.expandedcards = !settings.accessibility.expandedcards; localStorage.setItem("pk-settings", JSON.stringify(settings));}}/>
|
||||
<Tooltip target="s-expandedcards" placement="bottom">If enabled, lists will be expanded by default (overrides page links).</Tooltip>
|
||||
</Col>
|
||||
<Col xs={12} lg={4} class="mb-2">
|
||||
<span id="s-pagelinks">Use page links instead of cards?</span> <Toggle hideLabel style="display: inline" label="Use page links" toggled={settings.accessibility.pagelinks} on:toggle={() => {settings.accessibility.pagelinks= !settings.accessibility.pagelinks; localStorage.setItem("pk-settings", JSON.stringify(settings));}}/>
|
||||
<Tooltip target="s-pagelinks" placement="bottom">If enabled, the list items will not expand, but instead link to the corresponding page.</Tooltip>
|
||||
</Col>
|
||||
</Row>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col class="mx-auto" xs={12} lg={11} xl={10}>
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle style="margin-top: 8px; outline: none;">
|
||||
<div class="icon d-inline-block">
|
||||
<FaCogs />
|
||||
</div>Templates
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<p>Templates allow you to quickly set up a member description with a specific layout. Put in the template in one of the below fields, and access it whenever you create or edit a member. You can set up to 3 templates.</p>
|
||||
<b>Template 1</b>
|
||||
<textarea class="form-control" bind:value={descriptions[0]} maxlength={1000} use:autosize placeholder={descriptions[0]}/>
|
||||
<br>
|
||||
<b>Template 2</b>
|
||||
<textarea class="form-control" bind:value={descriptions[1]} maxlength={1000} use:autosize placeholder={descriptions[1]}/>
|
||||
<br>
|
||||
<b>Template 3</b>
|
||||
<textarea class="form-control" bind:value={descriptions[2]} maxlength={1000} use:autosize placeholder={descriptions[2]}/>
|
||||
<br>
|
||||
<Button on:click={saveDescriptionTemplates}>Save</Button>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
<svelte:head>
|
||||
<title>pk-webs | settings</title>
|
||||
</svelte:head>
|
||||
|
||||
<style>
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
</style>
|
||||
87
dashboard/src/pages/profiles/Main.svelte
Normal file
87
dashboard/src/pages/profiles/Main.svelte
Normal file
@@ -0,0 +1,87 @@
|
||||
<script lang="ts">
|
||||
import { Container, Col, Row, TabContent, TabPane, Alert, Spinner } from 'sveltestrap';
|
||||
import { useParams, useLocation } from "svelte-navigator";
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import SystemMain from '../../lib/system/Main.svelte';
|
||||
import MemberList from '../../lib/member/List.svelte';
|
||||
import GroupList from '../../lib/group/List.svelte';
|
||||
|
||||
import { System } from '../../api/types';
|
||||
import api from '../../api';
|
||||
|
||||
let user: System = {};
|
||||
let settings = JSON.parse(localStorage.getItem("pk-settings"));
|
||||
|
||||
let members = [];
|
||||
let groups = [];
|
||||
|
||||
let params = useParams();
|
||||
$: id = $params.id;
|
||||
|
||||
let location = useLocation();
|
||||
let urlParams = $location.search && new URLSearchParams($location.search);
|
||||
let tabPane: string;
|
||||
if (urlParams) {
|
||||
tabPane = urlParams.get("tab");
|
||||
}
|
||||
|
||||
if (!tabPane) {
|
||||
tabPane = "system";
|
||||
}
|
||||
|
||||
let err: string;
|
||||
|
||||
let title = "system"
|
||||
|
||||
onMount(() => {
|
||||
getSystem();
|
||||
})
|
||||
|
||||
async function getSystem() {
|
||||
try {
|
||||
let res: System = await api().systems(id).get();
|
||||
user = res;
|
||||
title = user.name ? user.name : "system";
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
err = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<!-- display the banner if there's a banner set, and if the current settings allow for it-->
|
||||
{#if user && user.banner && ((settings && settings.appearance.banner_top) || !settings)}
|
||||
<div class="banner" style="background-image: url({user.banner})" />
|
||||
{/if}
|
||||
<Container>
|
||||
<Row>
|
||||
<Col class="mx-auto" xs={12} lg={11} xl={10}>
|
||||
{#if !user.id && !err}
|
||||
<div class="mx-auto text-center">
|
||||
<Spinner class="d-inline-block" />
|
||||
</div>
|
||||
{:else if err}
|
||||
<Alert color="danger">{err}</Alert>
|
||||
{:else}
|
||||
<Alert color="info">You are currently <b>viewing</b> a system.</Alert>
|
||||
<TabContent class="mt-3">
|
||||
<TabPane tabId="system" tab="System" active={tabPane === "system"}>
|
||||
<SystemMain bind:user isPublic={true} />
|
||||
</TabPane>
|
||||
<TabPane tabId="members" tab="Members" active={tabPane === "members"}>
|
||||
<MemberList bind:list={members} isPublic={true} />
|
||||
</TabPane>
|
||||
<TabPane tabId="groups" tab="Groups" active={tabPane === "groups"}>
|
||||
<GroupList bind:members={members} bind:list={groups} isPublic={true}/>
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
{/if}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
<svelte:head>
|
||||
<title>pk-webs | {title}</title>
|
||||
</svelte:head>
|
||||
174
dashboard/src/pages/status.svelte
Normal file
174
dashboard/src/pages/status.svelte
Normal file
@@ -0,0 +1,174 @@
|
||||
<script lang="ts">
|
||||
import { Container, Row, Col, Card, CardHeader, CardTitle, CardBody, Input, Button } from 'sveltestrap';
|
||||
import FaInfoCircle from 'svelte-icons/fa/FaInfoCircle.svelte'
|
||||
import ShardItem from '../lib/shard.svelte';
|
||||
|
||||
import api from '../api';
|
||||
|
||||
let hover = null;
|
||||
|
||||
let message = "Loading...";
|
||||
let shards = [];
|
||||
let clusters = {};
|
||||
let pingAverage = "";
|
||||
let currentCommitMsg = "";
|
||||
|
||||
let foundShard = {
|
||||
id: 1,
|
||||
status: 1,
|
||||
ping:"",
|
||||
disconnection_count:0,
|
||||
last_connection:0,
|
||||
last_heartbeat:0.
|
||||
};
|
||||
foundShard = null;
|
||||
|
||||
let findShardInput = "";
|
||||
let valid = false;
|
||||
|
||||
const get = async () => {
|
||||
const pkdata = await api().private.meta.get();
|
||||
let data = pkdata.shards.sort((x, y) => (x.id > y.id) ? 1 : -1);
|
||||
let pings = 0;
|
||||
data = data.map(shard => {
|
||||
pings += shard.ping;
|
||||
shard.last_connection = new Date(Number(shard.last_connection) * 1000).toUTCString().match(/([0-9][0-9]:[0-9][0-9]:[0-9][0-9])/)?.shift()
|
||||
shard.last_heartbeat = new Date(Number(shard.last_heartbeat) * 1000).toUTCString().match(/([0-9][0-9]:[0-9][0-9]:[0-9][0-9])/)?.shift()
|
||||
return shard;
|
||||
});
|
||||
|
||||
currentCommitMsg = `Current Git commit: <a href="https://github.com/xSke/PluralKit/commit/${pkdata.version}">${pkdata.version.slice(0,7)}</a>`;
|
||||
|
||||
if (data[0].cluster_id === 0) {
|
||||
let clusterData = {};
|
||||
data.forEach(shard => {
|
||||
if (clusterData[shard.cluster_id] === undefined) clusterData[shard.cluster_id] = [];
|
||||
clusterData[shard.cluster_id].push(shard);
|
||||
});
|
||||
clusters = clusterData;
|
||||
}
|
||||
|
||||
shards = data;
|
||||
pingAverage = Math.trunc(pings / shards.length).toString();
|
||||
|
||||
message = "";
|
||||
};
|
||||
|
||||
get();
|
||||
setTimeout(get, 30 * 1000);
|
||||
|
||||
// javascript wants everything to be BigInts
|
||||
const getShardID = (guild_id: string, num_shards: number) => guild_id == "" ? -1 : (BigInt(guild_id) >> BigInt(22)) % BigInt(num_shards);
|
||||
|
||||
let shardInfoMsg = "";
|
||||
|
||||
let shardInfoHandler = (_: Event) => {
|
||||
if (findShardInput == "" || !findShardInput) {
|
||||
valid = false;
|
||||
foundShard = null;
|
||||
shardInfoMsg = "";
|
||||
return;
|
||||
};
|
||||
var match = findShardInput.match(/https:\/\/[\w+]?discord[app]?.com\/channels\/(\d+)\/\d+\/\d+/);
|
||||
if (match != null) {
|
||||
console.log("match", match)
|
||||
foundShard = shards[Number(getShardID(match[1], shards.length))];
|
||||
valid = true;
|
||||
shardInfoMsg = "";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var shard = getShardID(findShardInput, shards.length);
|
||||
if (shard == -1) {
|
||||
valid = false;
|
||||
foundShard == null;
|
||||
shardInfoMsg = "Invalid server ID";
|
||||
return;
|
||||
}
|
||||
foundShard = shards[Number(shard)];
|
||||
valid = true;
|
||||
shardInfoMsg = "";
|
||||
} catch(e) {
|
||||
valid = false;
|
||||
shardInfoMsg = "Invalid server ID";
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<Container fluid>
|
||||
<Row>
|
||||
<Col class="mx-auto" xs={12} lg={11} xl={10}>
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle style="margin-top: 8px; outline: none;">
|
||||
<div class="icon d-inline-block">
|
||||
<FaInfoCircle />
|
||||
</div>
|
||||
Bot status
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<span>{@html currentCommitMsg}</span>
|
||||
<br>
|
||||
<noscript>Please enable JavaScript to view this page!</noscript>
|
||||
|
||||
{ shards.length } shards ({ shards.filter(x => x.status == "up").length } up) <br>
|
||||
Average latency: { pingAverage }ms
|
||||
<br><br>
|
||||
All times in UTC. More statistics available at <a href="https://stats.pluralkit.me">https://stats.pluralkit.me</a>
|
||||
<br><br>
|
||||
<details>
|
||||
<summary><b>Find my shard</b></summary>
|
||||
<br>
|
||||
Enter a server ID or a message link to find the shard currently assigned to your server:
|
||||
<br>
|
||||
<input bind:value={findShardInput} on:input={shardInfoHandler} />
|
||||
<br><br>
|
||||
<span>{ shardInfoMsg }</span>
|
||||
{#if valid}
|
||||
<h3>Your shard is: Shard { foundShard.id }</h3>
|
||||
<br>
|
||||
<span>Status: <b>{ foundShard.status }</b></span><br>
|
||||
<span>Latency: { foundShard.ping }ms</span><br>
|
||||
<span>Disconnection count: { foundShard.disconnection_count }</span><br>
|
||||
<span>Last connection: { foundShard.last_connection }</span><br>
|
||||
<span>Last heartbeat: { foundShard.last_heartbeat }</span><br>
|
||||
{/if}
|
||||
</details>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
{#if Object.keys(clusters).length == 0 && shards.length > 0}
|
||||
<Row>
|
||||
<Col class="mx-auto" xs={12} lg={11} xl={10}>
|
||||
<Card class="mb-4">
|
||||
<CardBody>
|
||||
<span>{ message }</span>
|
||||
{#each shards as shard}
|
||||
<ShardItem shard={shard} bind:hover={hover} />
|
||||
{/each}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
{/if}
|
||||
{#each Object.keys(clusters) as key}
|
||||
<Row>
|
||||
<Col class="mx-auto" xs={12} lg={11} xl={10}>
|
||||
<Card class="mb-4">
|
||||
<CardBody>
|
||||
<CardTitle style="margin-top: 8px; outline: none;">
|
||||
Cluster {key}
|
||||
</CardTitle>
|
||||
<br>
|
||||
{#each clusters[key] as shard}
|
||||
<ShardItem shard={shard} bind:hover={hover} />
|
||||
{/each}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
{/each}
|
||||
</Container>
|
||||
Reference in New Issue
Block a user