empty branch

This commit is contained in:
Spectralitree 2021-12-09 12:57:01 +01:00
parent bec88e14cd
commit 9eeda0706e
42 changed files with 0 additions and 55772 deletions

25
.gitignore vendored
View File

@ -1,25 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.eslintcache

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 Spectralitree
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,7 +0,0 @@
# pk-webs
A web interface for [pluralkit](https://pluralkit.me/).
Check it out at https://spectralitree.github.io/pk-webs/
### Please note:
This is a work in progress. The code is super messy, many features are not added yet, and I cannot guarantee that this is completely bugless. Feel free to open an issue if you notice anything wrong.

0
heeo.txt Normal file
View File

38400
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,75 +0,0 @@
{
"homepage": "https://pk-webs.spectralitree.com",
"name": "pk-webs",
"version": "0.1.2",
"private": true,
"dependencies": {
"@hookform/resolvers": "^1.1.2",
"@material-ui/core": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.57",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"autosize": "^4.0.2",
"bootstrap": "^4.5.3",
"discord-markdown": "^2.4.1",
"moment": "^2.29.1",
"moment-timezone": "^0.5.32",
"node-fetch": "^2.6.1",
"react": "^17.0.1",
"react-bootstrap": "^1.4.0",
"react-dom": "^17.0.1",
"react-hook-form": "^7.0.7",
"react-icons": "^4.1.0",
"react-lazyload": "^3.1.0",
"react-moment": "^1.0.0",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.1",
"react-toggle": "^4.1.1",
"reactjs-popup": "^2.0.4",
"sass": "^1.43.4",
"twemoji": "^13.1.0",
"web-vitals": "^0.2.4",
"yup": "^0.32.8"
},
"scripts": {
"predeploy": "npm run build",
"deploy": "gh-pages -d build",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"gh-pages": "^3.1.0"
},
"description": "A web interface for [pluralkit](https://pluralkit.me/).\r Check it out at https://spectralitree.github.io/pk-webs/",
"main": "index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/Spectralitree/pk-webs.git"
},
"author": "Spectralitree",
"license": "ISC",
"bugs": {
"url": "https://github.com/Spectralitree/pk-webs/issues"
}
}

View File

@ -1,40 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Single Page Apps for GitHub Pages</title>
<script type="text/javascript">
// Single Page Apps for GitHub Pages
// MIT License
// https://github.com/rafgraph/spa-github-pages
// This script takes the current url and converts the path and query
// string into just a query string, and then redirects the browser
// to the new url with only a query string and hash fragment,
// e.g. https://www.foo.tld/one/two?a=b&c=d#qwe, becomes
// https://www.foo.tld/?/one/two&a=b~and~c=d#qwe
// Note: this 404.html file must be at least 512 bytes for it to work
// with Internet Explorer (it is currently > 512 bytes)
// If you're creating a Project Pages site and NOT using a custom domain,
// then set pathSegmentsToKeep to 1 (enterprise users may need to set it to > 1).
// This way the code will only replace the route part of the path, and not
// the real directory in which the app resides, for example:
// https://username.github.io/repo-name/one/two?a=b&c=d#qwe becomes
// https://username.github.io/repo-name/?/one/two&a=b~and~c=d#qwe
// Otherwise, leave pathSegmentsToKeep as 0.
var pathSegmentsToKeep = 0;
var l = window.location;
l.replace(
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
l.pathname.split('/').slice(0, 1 + pathSegmentsToKeep).join('/') + '/?/' +
l.pathname.slice(1).split('/').slice(pathSegmentsToKeep).join('/').replace(/&/g, '~and~') +
(l.search ? '&' + l.search.slice(1).replace(/&/g, '~and~') : '') +
l.hash
);
</script>
</head>
<body>
</body>
</html>

View File

@ -1,12 +0,0 @@
# pk-webs
A web interface for [pluralkit](https://pluralkit.me/).
Check it out at https://spectralitree.github.io/pk-webs/
### Please note:
This is a work in progress. The code is super messy, many features are not added yet, and I cannot guarantee that this is completely bugless. Feel free to open an issue if you notice anything amiss.
### Why does this exist?
You might be wondering this, considering https://pk.greysdawn.com/ already exists and functions just fine. My answer to that is: it didnt suit my exact needs. I also just wanted to see if I could write my own interface for pluralkit, since I'm new to programming and wanted to test my skills.
If you prefer greysdawn, that's perfectly fine! This is meant to be an alternative, not a competitor. If you miss any features from greysdawn, chances are they haven't been added yet, they are unable to be added, or I just haven't considered them! Again, feel free to open an issue.

View File

@ -1,67 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/myriad.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="application-name"
content="pk-webs"
/>
<meta
name="description"
content="A web interface for pluralkit."
/>
<meta content="A web interface for pluralkit." property="og:title">
<meta content="Edit your system and member data using your browser, or link to your public system list as a way to introduce yourself." property="og:description">
<meta content="pk-webs" property="og:site_name">
<meta content='%PUBLIC_URL%/myriad.png' property='og:image'>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>pk-webs</title>
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"
crossorigin="anonymous" />
<script type="text/javascript">
// Single Page Apps for GitHub Pages
// MIT License
// https://github.com/rafgraph/spa-github-pages
// This script checks to see if a redirect is present in the query string,
// converts it back into the correct url and adds it to the
// browser's history using window.history.replaceState(...),
// which won't cause the browser to attempt to load the new url.
// When the single page app is loaded further down in this file,
// the correct url will be waiting in the browser's history for
// the single page app to route accordingly.
(function(l) {
if (l.search[1] === '/' ) {
var decoded = l.search.slice(1).split('&').map(function(s) {
return s.replace(/~and~/g, '&')
}).join('?');
window.history.replaceState(null, null,
l.pathname.slice(0, -1) + decoded + l.hash
);
}
}(window.location))
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

View File

@ -1,67 +0,0 @@
import React, { useState, useCallback, useEffect } from 'react';
import { Router, Switch, Route, Redirect } from 'react-router-dom';
import * as BS from 'react-bootstrap'
import './App.scss';
import 'bootstrap/dist/css/bootstrap.min.css';
import "react-toggle/style.css"
import Dash from './Pages/Dash.js'
import history from "./History.js"
import Footer from './Components/Footer.js'
import Public from './Pages/Public.js'
import Home from './Pages/Home.js'
import Settings from './Pages/Settings.js'
import Template from './Pages/Template.js'
import Navbar from './Components/Navbar.js'
export default function App() {
const [isLoading, setIsLoading] = useState(false);
const [isSubmit, setIsSubmit] = useState(false);
const [isInvalid, setIsInvalid] = useState(false);
const [, updateState] = useState();
const forceUpdate = useCallback(() => updateState({}), []);
useEffect(() => {
if (localStorage.getItem("pk-darkmode")) {
document.body.classList.add('dark-mode')
}
else {
document.body.classList.remove('dark-mode')
}
forceUpdate();
}, []);
return (
<div className={ `contents ${localStorage.getItem('opendyslexic') ? "opendyslexic" : ""}`}>
<Router history={history}>
<Navbar forceUpdate={forceUpdate} setIsSubmit={setIsSubmit} />
<div className="content">
<BS.Container>
<Switch>
<Route path="/dash">
{ !localStorage.getItem('token') || isInvalid ? <Redirect to="/"/> : <Dash />
}
</Route>
<Route exact path="/">
<Home forceUpdate={forceUpdate} isLoading={isLoading} setIsLoading={setIsLoading} isSubmit={isSubmit} setIsSubmit={setIsSubmit} isInvalid={isInvalid} setIsInvalid={setIsInvalid}/>
</Route>
<Route path="/profile">
<Public />
</Route>
<Route path="/settings">
<Settings forceUpdate={forceUpdate}/>
</Route>
<Route path="/template">
<Template/>
</Route>
</Switch>
</BS.Container>
</div>
<Footer />
</Router>
</div>
);
}

View File

@ -1,329 +0,0 @@
@import "~bootstrap/scss/bootstrap";
@font-face {
font-family: 'openDyslexic';
src: url('https://dl.dropboxusercontent.com/s/qfpakpjedhsrdb9/OpenDyslexicAlta-Regular.ttf');
}
#root {
z-index: -5;
}
html, body {
position: relative;
min-height: 100vh;
}
.contents {
display: flex;
flex-flow: column;
flex: 1;
min-height: 100vh;
}
.content {
flex: 1;
}
.opendyslexic {
font-family: "OpenDyslexic";
}
.dark-mode {
background-color: $gray-800;
}
.card-header {
background-color: $gray-200 !important;
opacity: 1 !important;
}
.dark-mode .card {
color: $white;
background-color: $gray-800;
border: 1px solid #191c1f !important;
}
.card-body {
background-color: $white;
}
.dark-mode .card-body {
color: $white;
background-color: $gray-800;
}
.dark-mode .card-header {
color: $gray-100 !important;
background-color: $gray-900 !important;
}
.navbar {
background-color: $gray-100;
flex: 0 0 auto;
z-index: 5;
}
.dark-mode .navbar {
color: $gray-100;
background-color: $gray-900;
}
.dark-mode .navbar-brand {
color: $gray-100 !important;
}
.dark-mode .form-label {
color: $white !important;
}
.dark-mode .form-control {
color: $gray-200;
border-color: #191c1f;
background-color: $gray-900;
}
.dark-mode .form-control:disabled {
color: $gray-600;
border-color: #191c1f;
background-color: $gray-900;
}
.dark-mode .form-control:focus {
color: $gray-200;
border-color: #191c1f;
background-color: $gray-900;
}
.dark-mode .btn-light {
background-color: $gray-900;
border-color: #191c1f;
color: $gray-100;
}
.dark-mode .btn-light:focus {
background-color: $gray-900;
border-color: #191c1f;
color: $gray-100;
}
.card-header .btn-link {
color: $gray-900;
}
.card-header .btn-link:focus {
color: $gray-900;
text-decoration: none;
}
.card-header .btn-link:hover {
color: $gray-900;
text-decoration: none;
}
.card-header .btn-link {
font-size: 1.25rem !important;
font-weight: 500;
}
.dark-mode .btn-link {
color: $white;
}
.dark-mode .btn-link:focus {
color: $white;
text-decoration: none;
}
.dark-mode .btn-link:hover {
color: $white;
text-decoration: none;
}
.dark-mode .alert-danger {
color: $white;
background-color: $danger;
border: $gray-900;
}
.dark-mode .alert-primary {
color: $white;
background-color: $primary;
border: $gray-900;
}
.dark-mode .alert-light {
color: $white;
background-color: $gray-900;
border: #191c1f;
}
code {
color: black !important;
background-color: $gray-300;
padding: 3px 7px;
border-radius: 3px;
margin: 3px;
display: inline-block;
}
.dark-mode code {
color: white !important;
background-color: $gray-900;
}
pre {
margin: 2px !important;
}
code.hljs {
display: block;
border: 1px solid rgba(0, 0, 0, 0.125);
margin: 3px 3px 7px 3px;
}
.d-spoiler {
color: $gray-300 !important;
background-color: $gray-300;
padding: 3px 5px;
border-radius: 3px;
margin: 3px;
display: inline-block;
transition-delay: 6000s;
}
.d-spoiler::selection {
color: $gray-300;
}
.dark-mode .d-spoiler {
color: $gray-900 !important;
background-color: $gray-900;
padding: 3px 5px;
border-radius: 3px;
margin: 3px;
display: inline-block;
transition-delay: 6000s;
}
.dark-mode .d-spoiler::selection {
color: $gray-900;
}
.d-spoiler:active {
color: $gray-900 !important;
background-color: $gray-200;
transition-delay: 0s;
transition: background-color 0.2s ease;
}
.dark-mode .d-spoiler:active {
color: $gray-200 !important;
background-color: $gray-700;
transition-delay: 0s;
transition: background-color 0.2s ease;
}
blockquote {
border-left: 6px solid $gray-200;
padding: 2px 5px 2px 15px;
margin: 7px !important;
margin-left: 9px !important;
}
.dark-mode blockquote {
border-left: 6px solid $gray-700;
}
.popup-content {
background: none !important;
border: none !important;
width: 100% !important;
max-height: 100vh;
overflow-y: auto;
}
.popup-overlay {
background: rgba(0, 0, 0, 0.7) !important;
}
.dark-mode .nav-link {
color: $white !important;
}
.footer {
width: 100% !important;
flex: 0 0 auto;
}
.dark-mode .page-item:not(.active) .page-link {
background-color: $gray-900 !important;
color: $white !important;
border-color: #191c1f !important;
}
.toggles {
position: absolute !important;
right: 50%;
margin-right: -100px; ;
}
.emoji, .d-emoji {
width: 1.25em;
height: auto;
}
.backdrop {
position:fixed;
padding:0;
margin:0;
top:0;
left:0;
width: 100%;
height: 100%;
opacity: 1;
}
.backdrop-overlay {
position:fixed;
padding:0;
margin:0;
top:0;
left:0;
width: 100%;
height: 100%;
background: linear-gradient(to bottom, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
}
.dark-mode .backdrop-overlay {
background: linear-gradient(to bottom, rgba(52, 58, 64, 1) 0%, rgba(52, 58, 64, 0) 100%);
}
textarea {
resize: none !important;
}
.banner {
position: absolute;
top: 3.125rem;
left: 0;
width: 100%;
height: 30vh;
background-size: cover;
background-position: center;
box-shadow: inset 0 -3px 10px -10px #000000, inset 0 10px 10px -10px #000000;
}
@media only screen and (min-width: 992px) {
.banner {
height: 50vh;
}
}
.banner-content {
max-height: 650px !important;
}

View File

@ -1,25 +0,0 @@
import * as BS from 'react-bootstrap'
export default function Navigation() {
return (
<BS.Navbar className="mt-4 align-items-center justify-content-between footer">
<BS.Nav>
<BS.Nav.Item>
<BS.Nav.Link rel="noreferrer" target="_blank" href="https://pluralkit.me/">
Pluralkit.me
</BS.Nav.Link>
</BS.Nav.Item>
<BS.Nav.Item>
<BS.Nav.Link rel="noreferrer" target="_blank" href="https://github.com/Spectralitree/pk-webs">
Github
</BS.Nav.Link>
</BS.Nav.Item>
<BS.Nav.Item>
<BS.Nav.Link rel="noreferrer" target="_blank" href="https://ko-fi.com/spectralitree">
Ko-fi
</BS.Nav.Link>
</BS.Nav.Item>
</BS.Nav>
</BS.Navbar>
)
}

View File

@ -1,8 +0,0 @@
import React from 'react';
import * as BS from 'react-bootstrap'
export default function Loading() {
return (
<BS.Container fluid className="text-center"><BS.Spinner animation="border" /></BS.Container>
)
}

View File

@ -1,56 +0,0 @@
import React from 'react';
import * as BS from 'react-bootstrap'
import Toggle from 'react-toggle'
import { FaSun, FaMoon } from "react-icons/fa";
import history from "../History.js";
const Navbar = ({ setIsSubmit, forceUpdate}) => {
function toggleDarkMode() {
if (localStorage.getItem("pk-darkmode")) {
localStorage.removeItem("pk-darkmode");
document.body.classList.remove('dark-mode')
}
else {
localStorage.setItem("pk-darkmode", "true");
document.body.classList.add('dark-mode')
}
forceUpdate();
};
function logOut() {
setIsSubmit(false);
localStorage.removeItem("token");
localStorage.removeItem("user");
history.push("/");
forceUpdate();
};
return(
<BS.Navbar className="mb-5 align-items-center">
<BS.Navbar.Brand href="/">
pk-webs
</BS.Navbar.Brand>
<BS.NavDropdown id="menu" className="mr-auto" title="Menu">
{/* for some reason just using react router's link elements doesn't work here, maybe look into that */}
<BS.NavDropdown.Item onClick={() => history.push('/dash')} >Dash</BS.NavDropdown.Item>
<BS.NavDropdown.Item onClick={() => history.push('/settings')} >Settings</BS.NavDropdown.Item>
<BS.NavDropdown.Item onClick={() => history.push('/template')}>Templates</BS.NavDropdown.Item>
<BS.NavDropdown.Item onClick={() => history.push('/profile')}>Public profile</BS.NavDropdown.Item>
{ localStorage.getItem('token') ? <><hr className="my-1"/><BS.NavDropdown.Item onClick={() => logOut()}>Log out</BS.NavDropdown.Item></> : "" }
</BS.NavDropdown>
<BS.Nav className="mr-2 d-flex align-items-center row">
<Toggle
defaultChecked={true}
icons={false}
onClick={() => toggleDarkMode()} />
{localStorage.getItem("pk-darkmode") ? <FaMoon className="m-1"/> : <FaSun className="m-1"/>}
</BS.Nav>
</BS.Navbar>
);
}
export default Navbar;

View File

@ -1,155 +0,0 @@
import React, { useState } from "react";
import { useForm } from "react-hook-form";
import * as BS from "react-bootstrap";
import moment from "moment";
import "moment-timezone";
import API_URL from "../../../Constants/constants.js";
const EditSystem = ({
name,
tag,
timezone,
avatar,
banner,
editDesc,
setEditMode,
setErrorAlert,
user,
setUser,
setErrorMessage
}) => {
const [invalidTimezone, setInvalidTimezone] = useState(false);
const { register: registerEdit, handleSubmit: handleSubmitEdit } = useForm();
const submitEdit = (data) => {
if (data.tz) {
if (!moment.tz.zone(data.tz)) {
setInvalidTimezone(true);
return;
}
}
fetch(`${API_URL}s`, {
method: "PATCH",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
Authorization: localStorage.getItem("token"),
},
})
.then((res) => {
if (!res.ok)
throw new Error('HTTP Status ' + res.status)
return res.json();
})
.then(() => {
setUser((prevState) => {
return { ...prevState, ...data };
});
localStorage.setItem("user", JSON.stringify(user));
setEditMode(false);
})
.catch((error) => {
console.log(error);
setErrorMessage(error.message);
if (error.message === 'HTTP Status 401') {
setErrorMessage("Your token is invalid, please log out and enter a new token.")
};
if (error.message === 'HTTP Status 500') {
setErrorMessage("500: Internal server error.")
}
setErrorAlert(true);
});
};
return (
<BS.Form onSubmit={handleSubmitEdit(submitEdit)}>
<BS.Form.Text className="mb-4">
<b>Note:</b> if you refresh the page, the old data might show up again,
this is due to the bot caching data.
<br />
Try editing a member to clear the cache, or wait a few minutes before
refreshing.
</BS.Form.Text>
<BS.Form.Row>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Name:</BS.Form.Label>
<BS.Form.Control
name="name"
{...registerEdit("name")}
defaultValue={name}
/>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Tag:</BS.Form.Label>
<BS.Form.Control
name="tag"
{...registerEdit("tag")}
defaultValue={tag}
/>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Timezone:</BS.Form.Label>
<BS.Form.Control
name="tz"
{...registerEdit("tz")}
defaultValue={timezone}
required
/>
{invalidTimezone ? (
<BS.Form.Text>
Please enter a valid
<a
href="https://xske.github.io/tz/"
rel="noreferrer"
target="_blank"
>
timezone
</a>
</BS.Form.Text>
) : (
""
)}
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Avatar url:</BS.Form.Label>
<BS.Form.Control
type="url"
name="avatar_url"
{...registerEdit("avatar_url")}
defaultValue={avatar}
/>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Banner url:</BS.Form.Label>
<BS.Form.Control
type="url"
name="banner"
{...registerEdit("banner")}
defaultValue={banner}
/>
</BS.Col>
</BS.Form.Row>
<BS.Form.Group className="mt-3">
<BS.Form.Label>Description:</BS.Form.Label>
<BS.Form.Control
maxLength="1000"
as="textarea"
name="description"
{...registerEdit("description")}
defaultValue={editDesc}
/>
</BS.Form.Group>
<BS.Button variant="light" onClick={() => setEditMode(false)}>
Cancel
</BS.Button>{" "}
<BS.Button variant="primary" type="submit">
Submit
</BS.Button>
</BS.Form>
);
};
export default EditSystem;

View File

@ -1,118 +0,0 @@
import React from "react";
import { useForm } from "react-hook-form";
import * as BS from "react-bootstrap";
import API_URL from "../../../Constants/constants.js";
const EditSystemPrivacy = ({
setErrorAlert,
setUser,
user,
setPrivacyEdit,
setErrorMessage
}) => {
const { register: registerPrivacy, handleSubmit: handleSubmitPrivacy } =
useForm();
// submit privacy stuffs
const submitPrivacy = (data) => {
fetch(`${API_URL}s`, {
method: "PATCH",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
Authorization: localStorage.getItem("token"),
},
})
.then((res) => {
if (!res.ok)
throw new Error('HTTP Status ' + res.status)
return res.json();
})
.then(() => {
setUser((prevState) => {
return { ...prevState, ...data };
});
localStorage.setItem("user", JSON.stringify(user));
setPrivacyEdit(false);
})
.catch((error) => {
console.log(error);
setErrorMessage(error.message);
if (error.message === 'HTTP Status 401') {
setErrorMessage("Your token is invalid, please log out and enter a new token.")
};
if (error.message === 'HTTP Status 500') {
setErrorMessage("500: Internal server error.")
}
setErrorAlert(true);
});
};
return (
<BS.Form onSubmit={handleSubmitPrivacy(submitPrivacy)}>
<hr />
<h5>Editing privacy settings</h5>
<BS.Form.Row className="mb-3 mb-lg-0">
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Description:</BS.Form.Label>
<BS.Form.Control
name="description_privacy"
defaultValue={user.description_privacy}
as="select"
{...registerPrivacy("description_privacy")}
>
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Member list:</BS.Form.Label>
<BS.Form.Control
name="member_list_privacy"
defaultValue={user.member_list_privacy}
as="select"
{...registerPrivacy("member_list_privacy")}
>
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Front:</BS.Form.Label>
<BS.Form.Control
name="front_privacy"
as="select"
defaultValue={user.front_privacy}
{...registerPrivacy("front_privacy")}
>
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Front history:</BS.Form.Label>
<BS.Form.Control
name="front_history_privacy"
defaultValue={user.front_history_privacy}
as="select"
{...registerPrivacy("front_history_privacy")}
>
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
</BS.Form.Row>
<BS.Button variant="light" onClick={() => setPrivacyEdit(false)}>
Cancel
</BS.Button>{" "}
<BS.Button variant="primary" type="submit">
Submit
</BS.Button>
<hr />
</BS.Form>
);
};
export default EditSystemPrivacy;

View File

@ -1,469 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Link } from "react-router-dom";
import * as BS from 'react-bootstrap'
import { useForm } from "react-hook-form";
import moment from 'moment';
import Popup from 'reactjs-popup';
import 'reactjs-popup/dist/index.css';
import autosize from 'autosize';
import LazyLoad from 'react-lazyload';
import twemoji from 'twemoji';
import API_URL from "../../Constants/constants.js";
import defaultAvatar from '../../default_discord_avatar.png'
import { FaLink, FaLock, FaTrashAlt } from "react-icons/fa";
export default function MemberCard(props) {
const system = JSON.parse(localStorage.getItem('user'));
const sysID = system.id;
const [member, setMember] = useState(props.member);
const [ displayName, setDisplayName ] = useState("");
const [ birthday, setBirthday ] = useState("");
const [ birthdate, setBirthdate ] = useState("");
const [ created, setCreated ] = useState("");
const [ pronouns, setPronouns ] = useState("");
const [ editPronouns, setEditPronouns ] = useState("");
const [ avatar, setAvatar ] = useState("");
const [ banner, setBanner ] = useState("");
const [ color, setColor ] = useState("");
const [ desc, setDesc ] = useState("");
const [ editDesc, setEditDesc ] = useState("");
const [ proxyTags, setProxyTags ] = useState(member.proxy_tags);
const [ editMode, setEditMode ] = useState(false);
const [ privacyEdit, setprivacyEdit ] = useState(false);
const [ privacyView, setPrivacyView ] = useState(false);
const [ proxyView, setProxyView ] = useState(false);
const [ proxyEdit, setproxyEdit ] = useState(false);
const [open, setOpen] = useState(false);
const closeModal = () => setOpen(false);
const [ errorAlert, setErrorAlert ] = useState(false);
const [ wrongID, setWrongID ] = useState(false);
const [ memberDeleted, setMemberDeleted ] = useState(false);
const [ errorMessage, setErrorMessage ] = useState("");
const {
register: registerEdit,
handleSubmit: handleSubmitEdit,
setValue
} = useForm();
const {
register: registerPrivacy,
handleSubmit: handleSubmitPrivacy
} = useForm();
const {
register: registerDelete,
handleSubmit: handleSubmitDelete
} = useForm();
const {
register: registerProxy,
handleSubmit: handleSubmitProxy,
} = useForm();
useEffect(() => {
autosize(document.querySelectorAll('textarea'));
})
useEffect(() => {
const { toHTML } = require('../../Functions/discord-parser.js');
if (member.display_name) {
setDisplayName(member.display_name)
} else setDisplayName('')
if (member.birthday) {
setBirthdate(member.birthday)
if (member.birthday.startsWith('0004-')) {
var bdaymoment = moment(member.birthday, 'YYYY-MM-DD').format('MMM D');
setBirthday(bdaymoment);
} else {
var birthdaymoment = moment(member.birthday, 'YYYY-MM-DD').format('MMM D, YYYY');
setBirthday(birthdaymoment);
}
} else { setBirthday('');
setBirthdate('');
}
var createdmoment = moment(member.created).format('MMM D, YYYY');
setCreated(createdmoment);
if (member.pronouns) {
setPronouns(toHTML(member.pronouns));
setEditPronouns(member.pronouns);
} else { setPronouns('');
setEditPronouns('');
}
if (member.avatar_url) {
var avatarsmall = member.avatar_url.replace('&format=jpeg', '');
setAvatar(avatarsmall.replace('?width=256&height=256', ''))
} else setAvatar('')
if (member.color) {
setColor(member.color);
} else setColor('');
if (member.banner) {
setBanner(member.banner);
} else setBanner("");
if (member.description) {
setDesc(toHTML(member.description));
setEditDesc(member.description);
} else { setDesc("(no description)");
setEditDesc("");
}
}, [member.description, member.color, member.birthday, member.display_name, member.pronouns, member.avatar_url, member.proxy_tags, member.created, member.banner]);
function submitEditPatch(data) {
fetch(`${API_URL}m/${member.id}`,{
method: 'PATCH',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
'Authorization': localStorage.getItem("token")
}}).then (res => {
if (!res.ok) {
throw new Error('HTTP Status ' + res.status);
}
return res.json();
}
).then (data => {
setMember(prevState => {return {...prevState, ...data}});
setErrorAlert(false);
setEditMode(false);
}
).catch (error => {
console.log(error);
setErrorMessage(error.message);
if (error.message === 'HTTP Status 401') {
setErrorMessage("401: Your token is invalid, please log out and enter a new token.")
};
if (error.message === 'HTTP Status 500') {
setErrorMessage("500: Internal server error.")
}
setErrorAlert(true);
});
}
const submitEdit = data => {
props.edit(Object.assign(member, data));
submitEditPatch(data);
}
const submitPrivacy = data => {
props.edit(Object.assign(member, data));
submitEditPatch(data);
}
const deleteMember = data => {
if (data.memberID !== member.id) {
setWrongID(true);
} else {
fetch(`${API_URL}m/${member.id}`,{
method: 'DELETE',
headers: {
'Authorization': localStorage.getItem("token")
}}).then (() => {
setErrorAlert(false);
setMemberDeleted(true);
})
.catch (error => {
console.error(error);
setErrorAlert(true);
})
}
}
function addProxyField() {
setProxyTags(oldTags => [...oldTags, {prefix: '', suffix: ''}] )
}
function resetProxyFields() {
setproxyEdit(false);
setProxyTags(member.proxy_tags);
}
const submitProxy = data => {
const newdata = {proxy_tags: data.proxy_tags.filter(tag => !(tag.prefix === "" && tag.suffix === ""))}
props.edit(Object.assign(member, newdata));
fetch(`${API_URL}m/${member.id}`,{
method: 'PATCH',
body: JSON.stringify(newdata),
headers: {
'Content-Type': 'application/json',
'Authorization': localStorage.getItem("token")
}}).then (res => res.json()
).then (data => {
setMember(prevState => {return {...prevState, ...data}});
setProxyTags(data.proxy_tags);
setErrorAlert(false);
setproxyEdit(false);
}
).catch (error => {
console.error(error);
setErrorAlert(true);
});
}
function copyLink() {
var link = `https://pk-webs.spectralitree.com/profile/${sysID}/${member.id}`
var textField = document.createElement('textarea')
textField.innerText = link
document.body.appendChild(textField);
textField.select();
textField.setSelectionRange(0, 99999);
document.execCommand('copy');
document.body.removeChild(textField);
}
function renderCard() {
return (
<BS.Card.Body style={{borderLeft: `5px solid #${color}` }}>
{ errorAlert ? <BS.Alert variant="danger">{errorMessage}</BS.Alert> : "" }
{ editMode ?
<>
<BS.Form id='Edit' onSubmit={handleSubmitEdit(submitEdit)}>
<BS.Form.Row>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Name:</BS.Form.Label>
<BS.Form.Control name="name" {...registerEdit("name")} defaultValue={member.name} />
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Display name: </BS.Form.Label>
<BS.Form.Control name="display_name" {...registerEdit("display_name")} defaultValue={displayName} />
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Birthday:</BS.Form.Label>
<BS.Form.Control pattern="^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$" name="birthday" {...registerEdit("birthday")} defaultValue={birthdate}/>
<BS.Form.Text>(YYYY-MM-DD)</BS.Form.Text>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Pronouns:</BS.Form.Label>
<BS.Form.Control as="textarea" rows="1" maxLength="100" name="pronouns" {...registerEdit("pronouns")} defaultValue={editPronouns} />
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Avatar url:</BS.Form.Label>
<BS.Form.Control type="url" name="avatar_url" {...registerEdit("avatar_url")} defaultValue={avatar} />
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Banner url:</BS.Form.Label>
<BS.Form.Control type="url" name="banner" {...registerEdit("banner")} defaultValue={banner} />
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Color:</BS.Form.Label>
<BS.Form.Control pattern="[A-Fa-f0-9]{6}" name="color" {...registerEdit("color")} defaultValue={color} />
<BS.Form.Text>(hexcode)</BS.Form.Text>
</BS.Col>
</BS.Form.Row>
<BS.Form.Group className="mt-3">
<BS.Form.Label>Description:</BS.Form.Label><br/>
{ localStorage.getItem('template1') ? <BS.Button className="mb-2" size="sm" variant="primary" onClick={() => setValue('description', localStorage.getItem('template1'))}>Template 1</BS.Button> : ""} { localStorage.getItem('template2') ? <BS.Button className="mb-2" size="sm" variant="primary" onClick={() => setValue('description', localStorage.getItem('template2'))}>Template 2</BS.Button> : ""} { localStorage.getItem('template3') ? <BS.Button className="mb-2" size="sm" variant="primary" onClick={() => setValue('description', localStorage.getItem('template3'))}>Template 3</BS.Button> : ""}
<BS.Form.Control maxLength="1000" as="textarea" name="description" {...registerEdit("description")} defaultValue={editDesc}/>
</BS.Form.Group>
<BS.Button variant="light" onClick={() => setEditMode(false)}>Cancel</BS.Button> <BS.Button variant="primary" type="submit">Submit</BS.Button> <BS.Button variant="danger" className="float-right" onClick={() => setOpen(o => !o)}>Delete</BS.Button>
</BS.Form>
<Popup open={open} position="top-center" modal>
<BS.Container>
<BS.Card>
<BS.Card.Header>
<h5><FaTrashAlt className="mr-3"/> Are you sure you want to delete {member.name}?</h5>
</BS.Card.Header>
<BS.Card.Body>
{ wrongID ? <BS.Alert variant="danger">Incorrect ID, please check the spelling.</BS.Alert> : "" }
<p>If you're sure you want to delete this member, please enter the member ID ({member.id}) below.</p>
<BS.Form id='Delete' onSubmit={handleSubmitDelete(deleteMember)}>
<BS.Form.Label>Member ID:</BS.Form.Label>
<BS.Form.Control className="mb-4" name="memberID" {...registerDelete("memberID", {required: true})} placeholder={member.id} />
<BS.Button variant="danger" type="submit">Delete</BS.Button> <BS.Button variant="light" className="float-right" onClick={closeModal}>Cancel</BS.Button>
</BS.Form>
</BS.Card.Body>
</BS.Card>
</BS.Container>
</Popup></>
:
<>
<BS.Row>
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>ID:</b> {member.id}</BS.Col>
{ member.display_name ?
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>Display name:</b> { localStorage.getItem("twemoji") ? <span dangerouslySetInnerHTML={{__html: twemoji.parse(displayName)}}></span> : <span dangerouslySetInnerHTML={{__html: displayName}}></span>}</BS.Col> : "" }
{ member.birthday ? <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Birthday:</b> {birthday}</BS.Col> : "" }
{ member.pronouns ? <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Pronouns:</b> { localStorage.getItem("twemoji") ? <span dangerouslySetInnerHTML={{__html: twemoji.parse(pronouns)}}></span> : <span dangerouslySetInnerHTML={{__html: pronouns}}></span>}</BS.Col> : "" }
{ member.color ? <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Color:</b> {color}</BS.Col> : "" }
{ privacyView ? "" : proxyView ? "" : <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Privacy:</b> <BS.Button variant="light" size="sm" onClick={() => setPrivacyView(true)}>View</BS.Button></BS.Col> }
{ privacyView ? "" : proxyView ? "" : <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Proxy tags:</b> <BS.Button variant="light" size="sm" onClick={() => setProxyView(true)}>View</BS.Button></BS.Col> }
{ privacyView || proxyView || !member.banner ? "" :
<BS.Col className="mb-lg-3" xs={12} lg={3}>
<b>Banner:</b>{" "}
<Popup
trigger={
<BS.Button
variant="light"
size="sm"
>
View
</BS.Button>
}
className="banner"
modal
>
{(close) => (
<div className="text-center w-100" onClick={() => close()}>
<div className="m-auto" style={{maxWidth: '100%'}}>
<BS.Image src={`${banner}`} style={{maxWidth: 'auto', maxHeight: '640px'}} thumbnail />
</div>
</div>
)}
</Popup>
</BS.Col>
}
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>Created:</b> {created}</BS.Col>
</BS.Row>
{ privacyEdit ? <BS.Form id='Privacy' onSubmit={handleSubmitPrivacy(submitPrivacy)}>
<hr/>
<h5>Editing privacy settings</h5>
<BS.Form.Row>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Visibility:</BS.Form.Label>
<BS.Form.Control name="visibility" defaultValue={member.visibility} as="select" {...registerPrivacy("visibility")}>
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Name:</BS.Form.Label>
<BS.Form.Control name="name_privacy" defaultValue={member.name_privacy} as="select" {...registerPrivacy("name_privacy")}>
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Description:</BS.Form.Label>
<BS.Form.Control name="description_privacy" defaultValue={member.description_privacy} as="select" {...registerPrivacy("description_privacy")}>
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Avatar:</BS.Form.Label>
<BS.Form.Control name="avatar_privacy" defaultValue={member.avatar_privacy} as="select" {...registerPrivacy("avatar_privacy")}>
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Birthday:</BS.Form.Label>
<BS.Form.Control name="birthday_privacy" defaultValue={member.birthday_privacy} as="select" {...registerPrivacy("birthday_privacy")}>
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Pronouns:</BS.Form.Label>
<BS.Form.Control name="pronoun_privacy" defaultValue={member.pronoun_privacy} as="select" {...registerPrivacy("pronoun_privacy")}>
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-3" xs={12} lg={3}>
<BS.Form.Label>Meta:</BS.Form.Label>
<BS.Form.Control name="metadata_privacy" defaultValue={member.metadata_privacy} as="select" {...registerPrivacy("metadata_privacy")}>
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
</BS.Form.Row>
<BS.Button variant="light" onClick={() => setprivacyEdit(false)}>Cancel</BS.Button> <BS.Button variant="primary" type="submit">Submit</BS.Button>
<hr/>
</BS.Form> : privacyView ? <><hr/>
<h5>Viewing privacy settings</h5>
<BS.Row>
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>Visibility:</b> {member.visibility}</BS.Col>
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>Name: </b>{member.name_privacy}</BS.Col>
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>Description:</b> {member.description_privacy}</BS.Col>
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>Avatar:</b> {member.avatar_privacy}</BS.Col>
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>Birthday:</b> {member.birthday_privacy}</BS.Col>
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>Pronouns:</b> {member.pronoun_privacy}</BS.Col>
<BS.Col className="mb-3" xs={12} lg={3}><b>Meta:</b> {member.metadata_privacy}</BS.Col>
</BS.Row>
<BS.Button variant="light" onClick={() => setPrivacyView(false)}>Exit</BS.Button> <BS.Button variant="primary" onClick={() => setprivacyEdit(true)}>Edit</BS.Button>
<hr/></> : "" }
{ proxyEdit ?
<><hr/>
<h5>Editing proxy tags</h5>
<BS.Form onSubmit={handleSubmitProxy(submitProxy)}>
<BS.Form.Row>
{ proxyTags.map((item, index) => (
<BS.Col key={index} className="mb-lg-2" xs={12} lg={6}>
<BS.Form.Row>
<BS.InputGroup className="ml-1 mr-1 mb-1">
<BS.Form.Control as="textarea" rows="1" name={`proxy_tags[${index}].prefix`} defaultValue={item.prefix} {...registerProxy(`proxy_tags[${index}].prefix`)}/>
<BS.Form.Control style={{flex: '0 0 3.5em'}} as="textarea" rows="1" disabled placeholder='text'/>
<BS.Form.Control as="textarea" rows="1" name={`proxy_tags[${index}].suffix`} defaultValue={item.suffix} {...registerProxy(`proxy_tags[${index}].suffix`)}/>
</BS.InputGroup>
</BS.Form.Row>
</BS.Col>
))} <BS.Col className="mb-2" xs={12} lg={3}><BS.Button block variant="light" onClick={() => addProxyField()}>Add new</BS.Button></BS.Col>
</BS.Form.Row>
<BS.Button variant="light" onClick={() => resetProxyFields()}>Exit</BS.Button> <BS.Button variant="primary" type="submit">Submit</BS.Button>
</BS.Form><hr/></> : proxyView ?
<><hr/>
<h5>Viewing proxy tags</h5>
<BS.Row className="mb-2">
{ proxyTags.length === 0 ? <BS.Col className="mb-lg-2"><b>No proxy tags set.</b></BS.Col> : proxyTags.map((proxytag, index) => <BS.Col key={index} className="mb-lg-2" xs={12} lg={2}> <code>{proxytag.prefix}text{proxytag.suffix}</code></BS.Col> )}
</BS.Row>
<BS.Button variant="light" onClick={() => setProxyView(false)}>Exit</BS.Button> <BS.Button variant="primary" onClick={() => setproxyEdit(true)}>Edit</BS.Button>
<hr/></> : "" }
<p><b>Description:</b></p>
{ localStorage.getItem("twemoji") ? <p dangerouslySetInnerHTML={{__html: twemoji.parse(desc)}}></p> : <p dangerouslySetInnerHTML={{__html: desc}}></p>}
{ !member.banner || !localStorage.getItem("bottombanners") ? "" :
<BS.Image rounded className="mb-2" style={{width: '100%', maxHeight: '15rem', objectFit: 'cover'}} src={banner}/>
}
{ proxyView ? "" : privacyEdit ? "" : privacyView ? "" : <><BS.Button variant="light" onClick={() => setEditMode(true)}>Edit</BS.Button> <Link to={`dash/${member.id}`}><BS.Button variant="primary" className="float-right">View page</BS.Button></Link></> }
</> } </BS.Card.Body>
)
}
return (
memberDeleted ? <BS.Card.Header className="d-flex align-items-center justify-content-between"><BS.Button variant="link" className="float-left"><FaTrashAlt className="mr-4"/>Member Deleted</BS.Button></BS.Card.Header> :
<LazyLoad offset={100}>
<BS.Card.Header className="d-flex align-items-center justify-content-between">
<div> { member.visibility === 'public' ? <BS.OverlayTrigger placement="left" overlay={
<BS.Tooltip>
Copy public link
</BS.Tooltip>
}><BS.Button variant="link" onClick={() => copyLink()}><FaLink style={{fontSize: '1.25rem'}}/></BS.Button></BS.OverlayTrigger> :
<BS.Button variant="link"><FaLock style={{fontSize: '1.25rem'}} /></BS.Button> }
{ localStorage.getItem('pagesonly') ?
<Link to={`dash/${member.id}`}><BS.Button variant="link" className="float-left"><b>{member.name}</b> ({member.id})</BS.Button></Link>
: <BS.Accordion.Toggle as={BS.Button} variant="link" eventKey={member.id}> <b>{member.name}</b> ({member.id})</BS.Accordion.Toggle>}</div>
{ member.avatar_url ? <Popup trigger={<BS.Image src={`${member.avatar_url}`} style={{width: 50, height: 50}} tabIndex="0" className="float-right" roundedCircle />} className="avatar" modal>
{close => (
<div className="text-center w-100 m-0" onClick={() => close()}>
<div className="m-auto" style={{maxWidth: '640px'}}>
<BS.Image src={`${avatar}`} style={{'maxWidth': '100%', height: 'auto'}} thumbnail />
</div>
</div>
)}
</Popup> :
<BS.Image src={defaultAvatar} style={{width: 50, height: 50}} tabIndex="0" className="float-right" roundedCircle />}
</BS.Card.Header>
{localStorage.getItem("expandcards") ? renderCard() : <BS.Accordion.Collapse eventKey={member.id}>
{renderCard()}
</BS.Accordion.Collapse>}
</LazyLoad>
)
}

View File

@ -1,463 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import * as BS from 'react-bootstrap'
import { useForm } from "react-hook-form";
import moment from 'moment';
import Popup from 'reactjs-popup';
import 'reactjs-popup/dist/index.css';
import autosize from 'autosize';
import twemoji from 'twemoji';
import API_URL from "../../Constants/constants.js";
import history from "../../History.js";
import defaultAvatar from '../../default_discord_avatar.png'
import { FaLink, FaLock, FaTrashAlt } from "react-icons/fa";
export default function MemberPage(props) {
const [ member, setMember] = useState(props.member);
const system = JSON.parse(localStorage.getItem('user'));
const sysID = system.id;
const [ displayName, setDisplayName ] = useState("");
const [ birthday, setBirthday ] = useState("");
const [ birthdate, setBirthdate ] = useState("");
const [ created, setCreated ] = useState("");
const [ pronouns, setPronouns ] = useState("");
const [ editPronouns, setEditPronouns ] = useState("");
const [ avatar, setAvatar ] = useState("");
const [ banner, setBanner ] = useState("");
const [ color, setColor ] = useState("");
const [ desc, setDesc ] = useState("");
const [ editDesc, setEditDesc ] = useState("");
const [ proxyTags, setProxyTags ] = useState(member.proxy_tags);
const [ editMode, setEditMode ] = useState(false);
const [ privacyMode, setPrivacyMode ] = useState(false);
const [ privacyView, setPrivacyView ] = useState(false);
const [ proxyView, setProxyView ] = useState(false);
const [ proxyMode, setProxyMode ] = useState(false);
const [open, setOpen] = useState(false);
const closeModal = () => setOpen(false);
const [ errorAlert, setErrorAlert ] = useState(false);
const [ wrongID, setWrongID ] = useState(false);
const [ memberDeleted, setMemberDeleted ] = useState(false);
const [ errorMessage, setErrorMessage ] = useState("");
const {
register: registerEdit,
handleSubmit: handleSubmitEdit,
setValue
} = useForm();
const {
register: registerPrivacy,
handleSubmit: handleSubmitPrivacy
} = useForm();
const {
register: registerDelete,
handleSubmit: handleSubmitDelete
} = useForm();
const {
register: registerProxy,
handleSubmit: handleSubmitProxy,
} = useForm();
useEffect(() => {
autosize(document.querySelectorAll('textarea'));
})
useEffect(() => {
const { toHTML } = require('../../Functions/discord-parser.js');
if (member.display_name) {
setDisplayName(member.display_name)
} else setDisplayName('')
if (member.birthday) {
setBirthdate(member.birthday)
if (member.birthday.startsWith('0004-')) {
var bdaymoment = moment(member.birthday, 'YYYY-MM-DD').format('MMM D');
setBirthday(bdaymoment);
} else {
var birthdaymoment = moment(member.birthday, 'YYYY-MM-DD').format('MMM D, YYYY');
setBirthday(birthdaymoment);
}
} else { setBirthday('');
setBirthdate('');
}
var createdmoment = moment(member.created).format('MMM D, YYYY');
setCreated(createdmoment);
if (member.pronouns) {
setPronouns(toHTML(member.pronouns));
setEditPronouns(member.pronouns);
} else { setPronouns('');
setEditPronouns('');
}
if (member.avatar_url) {
var avatarsmall = member.avatar_url.replace('&format=jpeg', '');
setAvatar(avatarsmall.replace('?width=256&height=256', ''))
} else setAvatar('')
if (member.banner) {
setBanner(member.banner);
} else setBanner("");
if (member.color) {
setColor(member.color);
} else setColor('');
if (member.description) {
setDesc(toHTML(member.description));
setEditDesc(member.description);
} else { setDesc("(no description)");
setEditDesc("");
}
}, [member.description, member.color, member.birthday, member.display_name, member.pronouns, member.avatar_url, member.proxy_tags, member.created, member.banner]);
function submitEditPatch(data) {
fetch(`${API_URL}m/${member.id}`,{
method: 'PATCH',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
'Authorization': localStorage.getItem("token")
}}).then (res => {
if (!res.ok) {
throw new Error('HTTP Status ' + res.status);
}
return res.json();
}
).then (data => {
setMember(prevState => {return {...prevState, ...data}});
setErrorAlert(false);
setEditMode(false);
}
).catch (error => {
console.log(error);
setErrorMessage(error.message);
if (error.message === 'HTTP Status 401') {
setErrorMessage("Your token is invalid, please log out and enter a new token.")
};
if (error.message === 'HTTP Status 500') {
setErrorMessage("500: Internal server error.")
}
setErrorAlert(true);
});
}
const submitEdit = data => {
props.edit(Object.assign(member, data));
submitEditPatch(data);
}
const submitPrivacy = data => {
props.edit(Object.assign(member, data));
submitEditPatch(data);
}
const deleteMember = data => {
if (data.memberID !== member.id) {
setWrongID(true);
} else {
fetch(`${API_URL}m/${member.id}`,{
method: 'DELETE',
headers: {
'Authorization': localStorage.getItem("token")
}}).then (() => {
setErrorAlert(false);
setMemberDeleted(true);
})
.catch (error => {
console.error(error);
setErrorAlert(true);
})
}
}
function addProxyField() {
setProxyTags(oldTags => [...oldTags, {prefix: '', suffix: ''}] )
}
function resetProxyFields() {
setProxyMode(false);
setProxyTags(member.proxy_tags);
}
const submitProxy = data => {
const newdata = {proxy_tags: data.proxy_tags.filter(tag => !(tag.prefix === "" && tag.suffix === ""))}
props.edit(Object.assign(member, newdata));
fetch(`${API_URL}m/${member.id}`,{
method: 'PATCH',
body: JSON.stringify(newdata),
headers: {
'Content-Type': 'application/json',
'Authorization': localStorage.getItem("token")
}}).then (res => res.json()
).then (data => {
setMember(prevState => {return {...prevState, ...data}});
setProxyTags(data.proxy_tags);
setErrorAlert(false)
setProxyMode(false);
}
).catch (error => {
console.error(error);
setErrorAlert(true);
});
}
function copyLink() {
var link = `https://pk-webs.spectralitree.com/profile/${sysID}/${member.id}`
var textField = document.createElement('textarea')
textField.innerText = link
document.body.appendChild(textField);
textField.select();
textField.setSelectionRange(0, 99999);
document.execCommand('copy');
document.body.removeChild(textField);
}
return (
memberDeleted ? <BS.Card className="mb-5"><BS.Card.Header className="d-flex align-items-center justify-content-between"><BS.Button variant="link" className="float-left"><FaTrashAlt className="mr-4"/>Member Deleted</BS.Button></BS.Card.Header>
<BS.Card.Body>
Member successfully deleted, click the button below to go back to the dash.
<BS.Button variant="primary" className="float-right" onClick={() => history.push("/dash/reload")}>Back</BS.Button>
</BS.Card.Body></BS.Card> :
<>
{ localStorage.getItem('colorbg') && member.color ? "" : <><div className="backdrop" style={{backgroundColor: `#${color}`}}/>
{ !localStorage.getItem('fullbg') ? <div className="backdrop-overlay"/> : "" }</> }
{ member.banner && !localStorage.getItem("hidebanners") ? <div className="banner" style={{backgroundImage: `url(${banner})`}} alt=""/> : ""}
<BS.Card className="mb-5">
<BS.Card.Header className="d-flex align-items-center justify-content-between">
<div> { member.visibility === 'public' ? <BS.OverlayTrigger placement="left" overlay={
<BS.Tooltip>
Copy public link
</BS.Tooltip>
}><BS.Button variant="link" onClick={() => copyLink()}><FaLink style={{fontSize: '1.25rem'}}/></BS.Button></BS.OverlayTrigger> : <BS.Button variant="link"><FaLock style={{fontSize: '1.25rem'}} /></BS.Button> }<BS.Button variant="link" ><b>{member.name}</b> ({member.id})</BS.Button> </div>
{ member.avatar_url ? <Popup trigger={<BS.Image src={`${member.avatar_url}`} style={{width: 50, height: 50}} tabIndex="0" className="float-right" roundedCircle />} className="avatar" modal>
{close => (
<div className="text-center w-100 m-0" onClick={() => close()}>
<div className="m-auto" style={{maxWidth: '640px'}}>
<BS.Image src={`${avatar}`} style={{'maxWidth': '100%', height: 'auto'}} thumbnail />
</div>
</div>
)}
</Popup> :
<BS.Image src={defaultAvatar} style={{width: 50, height: 50}} tabIndex="0" className="float-right" roundedCircle />}
</BS.Card.Header>
<BS.Card.Body style={{ borderLeft: localStorage.getItem('colorbg') ? `5px solid #${color}` : ''}}>
{ errorAlert ? <BS.Alert variant="danger">Something went wrong, please try logging in and out again.</BS.Alert> : "" }
{ editMode ?
<>
<BS.Form id='Edit' onSubmit={handleSubmitEdit(submitEdit)}>
<BS.Form.Row>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Name:</BS.Form.Label>
<BS.Form.Control name="name" {...registerEdit("name")} defaultValue={member.name} />
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Display name: </BS.Form.Label>
<BS.Form.Control name="display_name" {...registerEdit("display_name")} defaultValue={displayName} />
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Birthday:</BS.Form.Label>
<BS.Form.Control pattern="^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$" name="birthday" {...registerEdit("birthday")} defaultValue={birthdate}/>
<BS.Form.Text>(YYYY-MM-DD)</BS.Form.Text>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Pronouns:</BS.Form.Label>
<BS.Form.Control as="textarea" rows="1" maxLength="100" name="pronouns" {...registerEdit("pronouns")} defaultValue={editPronouns} />
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Avatar url:</BS.Form.Label>
<BS.Form.Control type="url" name="avatar_url" {...registerEdit("avatar_url")} defaultValue={avatar} />
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Banner url:</BS.Form.Label>
<BS.Form.Control type="url" name="banner" {...registerEdit("banner")} defaultValue={banner} />
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Color:</BS.Form.Label>
<BS.Form.Control pattern="[A-Fa-f0-9]{6}" name="color" {...registerEdit("color")} defaultValue={color} />
<BS.Form.Text>(hexcode)</BS.Form.Text>
</BS.Col>
</BS.Form.Row>
<BS.Form.Group className="mt-3">
<BS.Form.Label>Description:</BS.Form.Label><br/>
{ localStorage.getItem('template1') ? <BS.Button className="mb-2" size="sm" variant="primary" onClick={() => setValue('description', localStorage.getItem('template1'))}>Template 1</BS.Button> : ""} { localStorage.getItem('template2') ? <BS.Button className="mb-2" size="sm" variant="primary" onClick={() => setValue('description', localStorage.getItem('template2'))}>Template 2</BS.Button> : ""} { localStorage.getItem('template3') ? <BS.Button className="mb-2" size="sm" variant="primary" onClick={() => setValue('description', localStorage.getItem('template3'))}>Template 3</BS.Button> : ""}
<BS.Form.Control maxLength="1000" as="textarea" name="description" {...registerEdit("description")} defaultValue={editDesc}/>
</BS.Form.Group>
<BS.Button variant="light" onClick={() => setEditMode(false)}>Cancel</BS.Button> <BS.Button variant="primary" type="submit">Submit</BS.Button> <BS.Button variant="danger" className="float-right" onClick={() => setOpen(o => !o)}>Delete</BS.Button>
</BS.Form>
<Popup open={open} position="top-center" modal>
<BS.Container>
<BS.Card>
<BS.Card.Header>
<h5><FaTrashAlt className="mr-3"/> Are you sure you want to delete {member.name}?</h5>
</BS.Card.Header>
<BS.Card.Body>
{ wrongID ? <BS.Alert variant="danger">Incorrect ID, please check the spelling.</BS.Alert> : "" }
<p>If you're sure you want to delete this member, please enter the member ID ({member.id}) below.</p>
<BS.Form id='Delete' onSubmit={handleSubmitDelete(deleteMember)}>
<BS.Form.Label>Member ID:</BS.Form.Label>
<BS.Form.Control className="mb-4" name="memberID" {...registerDelete("memberID", {required: true})} placeholder={member.id} />
<BS.Button variant="danger" type="submit">Delete</BS.Button> <BS.Button variant="light" className="float-right" onClick={closeModal}>Cancel</BS.Button>
</BS.Form>
</BS.Card.Body>
</BS.Card>
</BS.Container>
</Popup></>
:
<>
<BS.Row>
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>ID:</b> {member.id}</BS.Col>
{ member.display_name ? <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Display name:</b> { localStorage.getItem("twemoji") ? <span dangerouslySetInnerHTML={{__html: twemoji.parse(displayName)}}></span> : <span dangerouslySetInnerHTML={{__html: displayName}}></span>}</BS.Col> : "" }
{ member.birthday ? <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Birthday:</b> {birthday}</BS.Col> : "" }
{ member.pronouns ? <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Pronouns:</b> { localStorage.getItem("twemoji") ? <span dangerouslySetInnerHTML={{__html: twemoji.parse(pronouns)}}></span> : <span dangerouslySetInnerHTML={{__html: pronouns}}></span>}</BS.Col> : "" }
{ member.color ? <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Color:</b> {color}</BS.Col> : "" }
{ privacyView ? "" : proxyView ? "" : <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Privacy:</b> <BS.Button variant="light" size="sm" onClick={() => setPrivacyView(true)}>View</BS.Button></BS.Col> }
{ privacyView ? "" : proxyView ? "" : <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Proxy tags:</b> <BS.Button variant="light" size="sm" onClick={() => setProxyView(true)}>View</BS.Button></BS.Col> }
{ privacyView || proxyView || !member.banner ? "" :
<BS.Col className="mb-lg-3" xs={12} lg={3}>
<b>Banner:</b>{" "}
<Popup
trigger={
<BS.Button
variant="light"
size="sm"
>
View
</BS.Button>
}
className="banner"
modal
>
{(close) => (
<div className="text-center w-100" onClick={() => close()}>
<div className="m-auto" style={{maxWidth: '100%'}}>
<BS.Image src={`${banner}`} style={{maxWidth: 'auto', maxHeight: '640px'}} thumbnail />
</div>
</div>
)}
</Popup>
</BS.Col>
}
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>Created:</b> {created}</BS.Col>
</BS.Row>
{ privacyMode ? <BS.Form id='Privacy' onSubmit={handleSubmitPrivacy(submitPrivacy)}>
<hr/>
<h5>Editing privacy settings</h5>
<BS.Form.Row>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Visibility:</BS.Form.Label>
<BS.Form.Control name="visibility" defaultValue={member.visibility} as="select" {...registerPrivacy("visibility")}>
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Name:</BS.Form.Label>
<BS.Form.Control name="name_privacy" defaultValue={member.name_privacy} as="select" {...registerPrivacy("name_privacy")}>
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Description:</BS.Form.Label>
<BS.Form.Control name="description_privacy" defaultValue={member.description_privacy} as="select" {...registerPrivacy("description_privacy")}>
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Avatar:</BS.Form.Label>
<BS.Form.Control name="avatar_privacy" defaultValue={member.avatar_privacy} as="select" {...registerPrivacy("avatar_privacy")}>
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Birthday:</BS.Form.Label>
<BS.Form.Control name="birthday_privacy" defaultValue={member.birthday_privacy} as="select" {...registerPrivacy("birthday_privacy")}>
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Pronouns:</BS.Form.Label>
<BS.Form.Control name="pronoun_privacy" defaultValue={member.pronoun_privacy} as="select" {...registerPrivacy("pronoun_privacy")}>
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-3" xs={12} lg={3}>
<BS.Form.Label>Meta:</BS.Form.Label>
<BS.Form.Control name="metadata_privacy" defaultValue={member.metadata_privacy} as="select" {...registerPrivacy("metadata_privacy")}>
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
</BS.Form.Row>
<BS.Button variant="light" onClick={() => setPrivacyMode(false)}>Cancel</BS.Button> <BS.Button variant="primary" type="submit">Submit</BS.Button>
<hr/>
</BS.Form> : privacyView ? <><hr/>
<h5>Viewing privacy settings</h5>
<BS.Row>
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>Visibility:</b> {member.visibility}</BS.Col>
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>Name: </b>{member.name_privacy}</BS.Col>
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>Description:</b> {member.description_privacy}</BS.Col>
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>Avatar:</b> {member.avatar_privacy}</BS.Col>
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>Birthday:</b> {member.birthday_privacy}</BS.Col>
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>Pronouns:</b> {member.pronoun_privacy}</BS.Col>
<BS.Col className="mb-3" xs={12} lg={3}><b>Meta:</b> {member.metadata_privacy}</BS.Col>
</BS.Row>
<BS.Button variant="light" onClick={() => setPrivacyView(false)}>Exit</BS.Button> <BS.Button variant="primary" onClick={() => setPrivacyMode(true)}>Edit</BS.Button>
<hr/></> : "" }
{ proxyMode ?
<><hr/>
<h5>Editing proxy tags</h5>
<BS.Form onSubmit={handleSubmitProxy(submitProxy)}>
<BS.Form.Row>
{ proxyTags.map((item, index) => (
<BS.Col key={index} className="mb-lg-2" xs={12} lg={2}>
<BS.Form.Row>
<BS.InputGroup className="ml-1 mr-1 mb-1">
<BS.Form.Control as="textarea" rows="1" name={`proxy_tags[${index}].prefix`} defaultValue={item.prefix} {...registerProxy(`proxy_tags[${index}].prefix`)}/>
<BS.Form.Control style={{flex: '0 0 3.5em'}} as="textarea" rows="1" disabled placeholder='text'/>
<BS.Form.Control as="textarea" rows="1" name={`proxy_tags[${index}].suffix`} defaultValue={item.suffix} {...registerProxy(`proxy_tags[${index}].suffix`)}/>
</BS.InputGroup>
</BS.Form.Row>
</BS.Col>
))} <BS.Col className="mb-2" xs={12} lg={3}><BS.Button block variant="light" onClick={() => addProxyField()}>Add new</BS.Button></BS.Col>
</BS.Form.Row>
<BS.Button variant="light" onClick={() => resetProxyFields()}>Exit</BS.Button> <BS.Button variant="primary" type="submit">Submit</BS.Button>
</BS.Form><hr/></> : proxyView ?
<><hr/>
<h5>Viewing proxy tags</h5>
<BS.Row className="mb-2">
{ proxyTags.length === 0 ? <BS.Col className="mb-lg-2"><b>No proxy tags set.</b></BS.Col> : proxyTags.map((proxytag, index) => <BS.Col key={index} className="mb-lg-2" xs={12} lg={2}> <code>{proxytag.prefix}text{proxytag.suffix}</code></BS.Col> )}
</BS.Row>
<BS.Button variant="light" onClick={() => setProxyView(false)}>Exit</BS.Button> <BS.Button variant="primary" onClick={() => setProxyMode(true)}>Edit</BS.Button>
<hr/></> : "" }
<p><b>Description:</b></p>
{ localStorage.getItem("twemoji") ? <p dangerouslySetInnerHTML={{__html: twemoji.parse(desc)}}></p> : <p dangerouslySetInnerHTML={{__html: desc}}></p>}
{ !member.banner || !localStorage.getItem("bottombanners") ? "" :
<BS.Image rounded className="mb-2" style={{width: '100%', maxHeight: '15rem', objectFit: 'cover'}} src={banner}/>
}
{ proxyView ? "" : privacyMode ? "" : privacyView ? "" : <><BS.Button variant="light" onClick={() => setEditMode(true)}>Edit</BS.Button> <Link to="/dash" ><BS.Button variant="primary" className="float-right">Back</BS.Button></Link></>}
</> } </BS.Card.Body></BS.Card></>
)
}

View File

@ -1,448 +0,0 @@
import React, { useEffect, useState, useCallback } from 'react';
import { useRouteMatch, Switch, Route } from "react-router-dom";
import * as BS from 'react-bootstrap'
import Popup from 'reactjs-popup';
import 'reactjs-popup/dist/index.css';
import { useForm } from "react-hook-form";
import MemberCard from './MemberCard.js'
import MemberPages from '../../Pages/MemberPages.js'
import Loading from "../Loading.js";
import API_URL from "../../Constants/constants.js";
import { FaPlus } from "react-icons/fa";
export default function Memberlist() {
const { path } = useRouteMatch();
const user = JSON.parse(localStorage.getItem('user'));
const userId = user.id;
const [isLoading, setIsLoading ] = useState(false);
const [isError, setIsError ] = useState(false);
const [errorAlert, setErrorAlert ] = useState(false);
const [proxyView, setProxyView] = useState(false);
const [privacyView, setPrivacyView] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [membersPerPage, setMembersPerPage] = useState(25);
const [open, setOpen] = useState(false);
const closeModal = () => setOpen(false);
const [members, setMembers ] = useState([]);
const [searchBy, setSearchBy] = useState('name')
const [privacyFilter, setPrivacyFilter] = useState('all')
const [sortBy, setSortBy] = useState('name')
const [sortOrder, setSortOrder] = useState('ascending')
const [value, setValue] = useState('');
const [proxyTags, setProxyTags] = useState([{
prefix: "", suffix: ""
}]);
const {register, handleSubmit, setValue: setTemplate} = useForm();
const fetchMembers = useCallback( () => {
setIsLoading(true);
setIsError(false);
setMembersPerPage(localStorage.getItem("expandcards") ? 10 : 25);
fetch(`${API_URL}s/${userId}/members`,{
method: 'GET',
headers: {
'Authorization': localStorage.getItem("token")
}}).then ( res => res.json()
).then (data => {
setMembers(data)
setIsLoading(false);
})
.catch (error => {
console.log(error);
setIsError(true);
setIsLoading(false);
})
}, [userId])
useEffect(() => {
fetchMembers();
}, [fetchMembers])
const indexOfLastMember = currentPage * membersPerPage;
const indexOfFirstMember = indexOfLastMember - membersPerPage;
let Members = members.map(member => {
if (member.display_name) {
return {...member, displayName: member.display_name}
} return {...member, displayName: member.name}
})
let Members1 = Members.map(member => {
if (member.description) {
return {...member, desc: member.description}
} return {...member, desc: "(no description)"}
})
const currentMembers = Members1.filter(member => {
if (!value & privacyFilter === 'all') return true;
if (privacyFilter === 'private') {
if (member.visibility !== 'private') {
return false;
}
} else if (privacyFilter === 'public') {
if (member.visibility !== 'public') {
return false;
}
}
if (searchBy === 'name') {
if (member.name.toLowerCase().includes(value.toLowerCase())) {
return true;
}
return false;
} else if (searchBy === 'display name') {
if (member.displayName.toLowerCase().includes(value.toLowerCase())) {
return true;
}
return false
} else if (searchBy === 'description') {
if (member.desc.toLowerCase().includes(value.toLowerCase())) {
return true;
}
return false;
} else if (searchBy === 'ID') {
if (member.id.toLowerCase().includes(value.toLowerCase())) {
return true;
}
return false;
}
return false;
})
const active = currentPage;
const pageAmount = Math.ceil(currentMembers.length / membersPerPage);
var sortMembers = currentMembers;
if (sortBy === 'name') {
if (sortOrder === 'descending') {
sortMembers = currentMembers.sort((a, b) => a.name.localeCompare(b.name)).reverse().slice(indexOfFirstMember, indexOfLastMember);
} else sortMembers = currentMembers.sort((a, b) => a.name.localeCompare(b.name)).slice(indexOfFirstMember, indexOfLastMember);
}
else if (sortBy === 'display name') {
if (sortOrder === 'descending') {
sortMembers = currentMembers.sort((a, b) => a.displayName.localeCompare(b.displayName)).reverse().slice(indexOfFirstMember, indexOfLastMember);
} else sortMembers = currentMembers.sort((a, b) => a.displayName.localeCompare(b.displayName)).slice(indexOfFirstMember, indexOfLastMember);
}
else if (sortBy === 'ID') {
if (sortOrder === 'descending') {
sortMembers = currentMembers.sort((a, b) => a.id.localeCompare(b.id)).reverse().slice(indexOfFirstMember, indexOfLastMember);
} else sortMembers = currentMembers.sort((a, b) => a.id.localeCompare(b.id)).slice(indexOfFirstMember, indexOfLastMember);
}
else if (sortBy === 'date created') {
if (sortOrder === 'descending') {
sortMembers = currentMembers.sort((a, b) => a.created.localeCompare(b.created)).reverse().slice(indexOfFirstMember, indexOfLastMember);
} else sortMembers = currentMembers.sort((a, b) => a.created.localeCompare(b.created)).slice(indexOfFirstMember, indexOfLastMember);
}
const memberList = sortMembers.map((member) => <BS.Card key={member.id} className={localStorage.getItem("expandcards") ? "mb-3" : ""}>
<MemberCard
member={member}
edit={memberEdit => setMembers(members.map(member => member.id === memberEdit.id ? Object.assign(member, memberEdit) : member))}
/>
</BS.Card>
);
function addProxyField() {
setProxyTags(oldTags => [...oldTags, {prefix: '', suffix: ''}] )
}
const submitMember = data => {
setIsLoading(true);
const newdata = data.proxy_tags ? {...data, proxy_tags: data.proxy_tags.filter(tag => !(tag.prefix === "" && tag.suffix === ""))} : data
fetch(`${API_URL}m/`,{
method: 'POST',
body: JSON.stringify(newdata),
headers: {
'Content-Type': 'application/json',
'Authorization': localStorage.getItem("token")
}}).then (res => res.json()
).then (data => {
setErrorAlert(false);
closeModal();
fetchMembers();
}
).catch (error => {
console.error(error);
setErrorAlert(true);
});
}
return (
<Switch>
<Route exact path={path}>
<>
<BS.Row className="mb-lg-3 justfiy-content-md-center">
<BS.Col xs={12} lg={3}>
<BS.Form>
<BS.InputGroup className="mb-3">
<BS.Form.Control disabled placeholder='Page length:'/>
<BS.Form.Control as="select" defaultValue={localStorage.getItem("expandcards") ? 10 : 25} onChange={e => {
setMembersPerPage(e.target.value);
setCurrentPage(1);
}}>
<option>10</option>
<option>25</option>
<option>50</option>
<option>100</option>
</BS.Form.Control>
</BS.InputGroup>
</BS.Form>
</BS.Col>
<BS.Col xs={12} lg={3}>
<BS.Form>
<BS.InputGroup className="mb-3">
<BS.Form.Control disabled placeholder='Search by:'/>
<BS.Form.Control as="select" defaultValue={searchBy} onChange={e => {
setSearchBy(e.target.value)
}}>
<option>name</option>
<option>display name</option>
<option>description</option>
<option>ID</option>
</BS.Form.Control>
</BS.InputGroup>
</BS.Form>
</BS.Col>
<BS.Col xs={12} lg={3}>
<BS.Form>
<BS.InputGroup className="mb-3">
<BS.Form.Control disabled placeholder='Sort by:'/>
<BS.Form.Control as="select" defaultValue={sortBy} onChange={e => {
setSortBy(e.target.value)
}}>
<option>name</option>
<option>display name</option>
<option>ID</option>
<option>date created</option>
</BS.Form.Control>
</BS.InputGroup>
</BS.Form>
</BS.Col>
<BS.Col xs={12} lg={3}>
<BS.Form>
<BS.InputGroup className="mb-3">
<BS.Form.Control disabled placeholder='Sort order:'/>
<BS.Form.Control as="select" defaultValue={sortOrder} onChange={e => {
setSortOrder(e.target.value)
}}>
<option>ascending</option>
<option>descending</option>
</BS.Form.Control>
</BS.InputGroup>
</BS.Form>
</BS.Col>
</BS.Row>
<BS.Row className="justify-content-md-center">
<BS.Col xs={12} lg={3}>
<BS.Form>
<BS.InputGroup className="mb-3">
<BS.Form.Control disabled placeholder='Only show:'/>
<BS.Form.Control as="select" defaultValue={privacyFilter} onChange={e => {
setPrivacyFilter(e.target.value)
}}>
<option>all</option>
<option>private</option>
<option>public</option>
</BS.Form.Control>
</BS.InputGroup>
</BS.Form>
</BS.Col>
<BS.Col className="mb-3" xs={12} lg={7}>
<BS.Form>
<BS.Form.Control value={value} onChange={e => {setValue(e.target.value); setCurrentPage(1);}} placeholder={`Search by ${searchBy}`}/>
</BS.Form>
</BS.Col>
<BS.Col className="mb-3" xs={12} lg={2}>
<BS.Button type="primary" className="m-0" block onClick={() => fetchMembers()}>Refresh</BS.Button>
</BS.Col>
</BS.Row>
<BS.Row className="justify-content-md-center">
<BS.Pagination className="ml-auto mr-auto">
{ currentPage === 1 ? <BS.Pagination.Prev disabled/> : <BS.Pagination.Prev onClick={() => setCurrentPage(currentPage - 1)} />}
{ currentPage < 3 ? "" : <BS.Pagination.Item onClick={() => setCurrentPage(1)} active={1 === active}>{1}</BS.Pagination.Item>}
{ currentPage < 4 ? "" : currentPage < 5 ? <BS.Pagination.Item onClick={() => setCurrentPage(2)} active={2 === active}>{2}</BS.Pagination.Item> : <BS.Pagination.Ellipsis disabled />}
{ currentPage > 1 ? <BS.Pagination.Item onClick={() => setCurrentPage(currentPage - 1)}>{currentPage - 1}</BS.Pagination.Item> : "" }
<BS.Pagination.Item onClick={() => setCurrentPage(currentPage)} active={currentPage === active}>{currentPage}</BS.Pagination.Item>
{ currentPage < pageAmount ? <BS.Pagination.Item onClick={() => setCurrentPage(currentPage + 1)}>{currentPage + 1}</BS.Pagination.Item> : "" }
{ currentPage > pageAmount - 3 ? "" : currentPage === pageAmount - 3 ? <BS.Pagination.Item onClick={() => setCurrentPage(pageAmount - 1)} active={pageAmount - 1 === active}>{pageAmount - 1}</BS.Pagination.Item> : <BS.Pagination.Ellipsis disabled />}
{ currentPage > pageAmount - 2 ? "" : <BS.Pagination.Item onClick={() => setCurrentPage(pageAmount)} active={pageAmount === active}>{pageAmount}</BS.Pagination.Item>}
{ currentPage === pageAmount ? <BS.Pagination.Next disabled /> :<BS.Pagination.Next onClick={() => setCurrentPage(currentPage + 1)} />}
</BS.Pagination>
</BS.Row>
{ isLoading ? <Loading /> : isError ?
<BS.Alert variant="danger">Error fetching members.</BS.Alert> :
<>
<BS.Card className="w-100">
<BS.Card.Header className="d-flex align-items-center justify-content-between">
<BS.Button variant="link" className="float-left" onClick={() => setOpen(o => !o)}><FaPlus className="mr-4"/>Add Member</BS.Button>
<Popup open={open} position="top-center" modal>
<BS.Container>
<BS.Card>
<BS.Card.Header>
<h5><FaPlus className="mr-3"/> Add member </h5>
</BS.Card.Header>
<BS.Card.Body>
{ errorAlert ? <BS.Alert variant="danger">Something went wrong, please try logging in and out again.</BS.Alert> : "" }
<BS.Form onSubmit={handleSubmit(submitMember)}>
<BS.Form.Text>
</BS.Form.Text>
<BS.Form.Row>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Name:</BS.Form.Label>
<BS.Form.Control name="name"{...register("name")} defaultValue={''} required/>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Display name: </BS.Form.Label>
<BS.Form.Control name="display_name" {...register("display_name")} defaultValue={''} />
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Birthday:</BS.Form.Label>
<BS.Form.Control pattern="^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$" name="birthday" {...register("birthday")} defaultValue={''}/>
<BS.Form.Text>(YYYY-MM-DD)</BS.Form.Text>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Pronouns:</BS.Form.Label>
<BS.Form.Control as="textarea" rows="1" name="pronouns" {...register("pronouns")} defaultValue={''} />
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Avatar url:</BS.Form.Label>
<BS.Form.Control type="url" name="avatar_url" {...register("avatar_url")} defaultValue={''} />
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Banner url:</BS.Form.Label>
<BS.Form.Control type="url" name="banner" {...register("banner")} defaultValue={''} />
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Color:</BS.Form.Label>
<BS.Form.Control pattern="[A-Fa-f0-9]{6}" name="color" {...register("color")} defaultValue={''} />
<BS.Form.Text>(hexcode)</BS.Form.Text>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={2}>
<BS.Form.Label>Proxy tags:</BS.Form.Label>
<BS.Button variant="primary" block onClick={() => setProxyView(view => !view)}> { proxyView ? "Hide" : "Show" }</BS.Button>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={2}>
<BS.Form.Label>Privacy settings:</BS.Form.Label>
<BS.Button variant="primary" block onClick={() => setPrivacyView(view => !view)}> { privacyView ? "Hide" : "Show" }</BS.Button>
</BS.Col>
</BS.Form.Row>
<hr/>
{ proxyView ? <>
<h5>Proxy Tags</h5>
<BS.Form.Row>
{ proxyTags.map((item, index) => (
<BS.Col key={index} className="mb-lg-2" xs={12} lg={6}>
<BS.Form.Row>
<BS.InputGroup className="ml-1 mr-1 mb-1">
<BS.Form.Control as="textarea" rows="1" name={`proxy_tags[${index}].prefix`} defaultValue={item.prefix} {...register(`proxy_tags[${index}].prefix`)}/>
<BS.Form.Control style={{flex: '0 0 3.5em'}} as="textarea" rows="1" disabled placeholder='text'/>
<BS.Form.Control as="textarea" rows="1" name={`proxy_tags[${index}].suffix`} defaultValue={item.suffix} {...register(`proxy_tags[${index}].suffix`)}/>
</BS.InputGroup>
</BS.Form.Row>
</BS.Col>
))} <BS.Col className="mb-lg-2" xs={12} lg={2}><BS.Button block variant="light" onClick={() => addProxyField()}>Add new</BS.Button></BS.Col>
</BS.Form.Row>
<hr/></> : "" }
{ privacyView ? <><h5>privacy settings</h5>
<BS.Form.Row>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Visibility:</BS.Form.Label>
<BS.Form.Control name="visibility" as="select" {...register("visibility")} >
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Name:</BS.Form.Label>
<BS.Form.Control name="name_privacy" as="select" {...register("name_privacy")} >
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Description:</BS.Form.Label>
<BS.Form.Control name="description_privacy" as="select" {...register("description_privacy")} >
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Birthday:</BS.Form.Label>
<BS.Form.Control name="birthday_privacy" as="select" {...register("birthday_privacy")} >
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Pronouns:</BS.Form.Label>
<BS.Form.Control name="pronoun_privacy" as="select" {...register("pronoun_privacy")} >
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-3" xs={12} lg={3}>
<BS.Form.Label>Meta:</BS.Form.Label>
<BS.Form.Control name="metadata_privacy" as="select" {...register("metadata_privacy")} >
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
</BS.Form.Row>
<hr/></> : "" }
<BS.Form.Group className="mt-3">
<BS.Form.Label>Description:</BS.Form.Label><br/>
{ localStorage.getItem('template1') ? <BS.Button className="mb-2" size="sm" variant="primary" onClick={() => setTemplate('description', localStorage.getItem('template1'))}>Template 1</BS.Button> : ""} { localStorage.getItem('template2') ? <BS.Button className="mb-2" size="sm" variant="primary" onClick={() => setTemplate('description', localStorage.getItem('template2'))}>Template 2</BS.Button> : ""} { localStorage.getItem('template3') ? <BS.Button className="mb-2" size="sm" variant="primary" onClick={() => setTemplate('description', localStorage.getItem('template3'))}>Template 3</BS.Button> : ""}
<BS.Form.Control maxLength="1000" as="textarea" rows="7" name="description" {...register("description")} defaultValue={''}/>
</BS.Form.Group>
<BS.Button variant="primary" type="submit">Submit</BS.Button> <BS.Button variant="light" className="float-right" onClick={closeModal}>Cancel</BS.Button>
</BS.Form>
</BS.Card.Body>
</BS.Card>
</BS.Container>
</Popup>
</BS.Card.Header>
</BS.Card>
<BS.Accordion className="mb-3 mt-3 w-100" defaultActiveKey="0">
{memberList}
</BS.Accordion>
<BS.Row className="justify-content-md-center">
<BS.Pagination className="ml-auto mr-auto">
{ currentPage === 1 ? <BS.Pagination.Prev disabled/> : <BS.Pagination.Prev onClick={() => setCurrentPage(currentPage - 1)} />}
{ currentPage < 3 ? "" : <BS.Pagination.Item onClick={() => setCurrentPage(1)} active={1 === active}>{1}</BS.Pagination.Item>}
{ currentPage < 4 ? "" : currentPage < 5 ? <BS.Pagination.Item onClick={() => setCurrentPage(2)} active={2 === active}>{2}</BS.Pagination.Item> : <BS.Pagination.Ellipsis disabled />}
{ currentPage > 1 ? <BS.Pagination.Item onClick={() => setCurrentPage(currentPage - 1)}>{currentPage - 1}</BS.Pagination.Item> : "" }
<BS.Pagination.Item onClick={() => setCurrentPage(currentPage)} active={currentPage === active}>{currentPage}</BS.Pagination.Item>
{ currentPage < pageAmount ? <BS.Pagination.Item onClick={() => setCurrentPage(currentPage + 1)}>{currentPage + 1}</BS.Pagination.Item> : "" }
{ currentPage > pageAmount - 3 ? "" : currentPage === pageAmount - 3 ? <BS.Pagination.Item onClick={() => setCurrentPage(pageAmount - 1)} active={pageAmount - 1 === active}>{pageAmount - 1}</BS.Pagination.Item> : <BS.Pagination.Ellipsis disabled />}
{ currentPage > pageAmount - 2 ? "" : <BS.Pagination.Item onClick={() => setCurrentPage(pageAmount)} active={pageAmount === active}>{pageAmount}</BS.Pagination.Item>}
{ currentPage === pageAmount ? <BS.Pagination.Next disabled /> :<BS.Pagination.Next onClick={() => setCurrentPage(currentPage + 1)} />}
</BS.Pagination>
</BS.Row>
</>
}
</>
</Route>
<Route path={`${path}/:memberID`}>
{ isLoading ? <Loading/> :
<MemberPages members={members}
edit={memberEdit => setMembers(members.map(member => member.id === memberEdit.id ? Object.assign(member, memberEdit) : member))}/>}
</Route>
</Switch>
)
}

View File

@ -1,278 +0,0 @@
import React, { useState, useEffect } from "react";
import * as BS from "react-bootstrap";
import { useRouteMatch } from "react-router-dom";
import autosize from "autosize";
import "moment-timezone";
import Popup from "reactjs-popup";
import twemoji from 'twemoji';
import history from "../../History.js";
import defaultAvatar from "../../default_discord_avatar.png";
import { FaAddressCard } from "react-icons/fa";
import EditSystem from "./Edit/EditSystem.js";
import EditSystemPrivacy from "./Edit/EditSystemPrivacy.js";
export default function System() {
// match the url, if there's a member ID there, don't render this component at all
const match = useRouteMatch("/dash/:memberID");
// get the user from the localstorage
const [user, setUser] = useState(JSON.parse(localStorage.getItem("user")));
// bunch of useState stuff, used in the useEffect() hook below
const [name, setName] = useState("");
const [tag, setTag] = useState("");
const [timezone, setTimezone] = useState("");
const [avatar, setAvatar] = useState("");
const [banner, setBanner] = useState("");
const [desc, setDesc] = useState("");
const [editDesc, setEditDesc] = useState("");
// more useState, this time to actually handle state
// TODO: name them something more intuitive maybe?
const [editMode, setEditMode] = useState(false);
const [privacyEdit, setPrivacyEdit] = useState(false);
const [privacyView, setPrivacyView] = useState(false);
const [errorAlert, setErrorAlert] = useState(false);
const [ errorMessage, setErrorMessage ] = useState("");
// this useEffect does a couple of things after the user is gotten from localstorage
useEffect(() => {
// first require the discord markdown parser
const { toHTML } = require("../../Functions/discord-parser.js");
// check if there's a name object in user, if it's null, set name to a blank string, otherwise set name to user.name
if (user.name) {
setName(user.name);
} else setName("");
// same as above, but with the user tag instead
if (user.tag) {
setTag(user.tag);
} else setTag("");
// same as above but with timezone
if (user.tz) {
setTimezone(user.tz);
} else setTimezone("");
// also trims the avatar url so that 1. pngs won't be converted to jpegs and 2. won't be resized to 256x256
if (user.avatar_url) {
var avatarsmall = user.avatar_url.replace("&format=jpeg", "");
setAvatar(avatarsmall.replace("?width=256&height=256", ""));
} else setAvatar("");
if (user.banner) {
setBanner(user.banner);
} else setBanner("");
// same as above, but with descriptions
// two description variables! one is just the plain description, the other is parsed and converted to html
if (user.description) {
setDesc(toHTML(user.description));
setEditDesc(user.description);
} else {
setDesc("(no description)");
setEditDesc("");
}
}, [user.description, user.tag, user.avatar_url, user.tz, user.name, user.banner]);
// this just resizes the textarea when filled with larger amounts of text
useEffect(() => {
autosize(document.querySelector("textarea"));
});
if (match) return null;
return (
<>
{ user.banner && !localStorage.getItem("hidebanners") ? <div className="banner" style={{backgroundImage: `url(${user.banner})`}} alt=""/> : ""}
<BS.Card className="mb-3 mt-3 w-100">
<BS.Card.Header className="d-flex align-items-center justify-content-between">
<BS.Card.Title className="float-left">
<FaAddressCard className="mr-4 float-left" /> {name} ({user.id})
</BS.Card.Title>
{user.avatar_url ? (
<Popup
trigger={
<BS.Image
src={`${user.avatar_url}`}
style={{ width: 50, height: 50 }}
tabIndex="0"
className="float-right"
roundedCircle
/>
}
className="avatar"
modal
>
{(close) => (
<div className="text-center w-100 m-0" onClick={() => close()}>
<div className="m-auto" style={{maxWidth: '640px'}}>
<BS.Image src={`${avatar}`} style={{'maxWidth': '100%', height: 'auto'}} thumbnail />
</div>
</div>
)}
</Popup>
) : (
<BS.Image
src={defaultAvatar}
style={{ width: 50, height: 50 }}
className="float-right"
roundedCircle
/>
)}
</BS.Card.Header>
<BS.Card.Body>
{errorAlert ? (
<BS.Alert variant="danger">
{errorMessage}
</BS.Alert>
) : (
""
)}
{editMode ? (
<EditSystem
editDesc={editDesc}
name={name}
tag={tag}
timezone={timezone}
avatar={avatar}
banner={banner}
setErrorAlert={setErrorAlert}
user={user}
setUser={setUser}
setEditMode={setEditMode}
setErrorMessage={setErrorMessage}
/>
) : (
<>
<BS.Row>
<BS.Col className="mb-lg-3" xs={12} lg={3}>
<b>ID:</b> {user.id}
</BS.Col>
<BS.Col className="mb-lg-3" xs={12} lg={3}>
<b>Tag:</b> {tag}
</BS.Col>
<BS.Col className="mb-lg-3" xs={12} lg={3}>
<b>Timezone:</b> {timezone}
</BS.Col>
{privacyView ? (
""
) : (
<>
<BS.Col className="mb-lg-3" xs={12} lg={3}>
<b>Privacy:</b>{" "}
<BS.Button
variant="light"
size="sm"
onClick={() => setPrivacyView(true)}
>
View
</BS.Button>
</BS.Col>
{user.banner ?
<BS.Col className="mb-lg-3" xs={12} lg={3}>
<b>Banner:</b>{" "}
<Popup
trigger={
<BS.Button
variant="light"
size="sm"
>
View
</BS.Button>
}
className="banner"
modal
>
{(close) => (
<div className="text-center w-100" onClick={() => close()}>
<div className="m-auto" style={{maxWidth: '100%'}}>
<BS.Image src={`${banner}`} style={{maxWidth: 'auto', maxHeight: '640px'}} thumbnail />
</div>
</div>
)}
</Popup>
</BS.Col>
: "" }
</>
)}
</BS.Row>
{privacyEdit ? (
<EditSystemPrivacy
setErrorAlert={setErrorAlert}
setUser={setUser}
user={user}
setPrivacyEdit={setPrivacyEdit}
setErrorMessage={setErrorMessage}
/>
) : privacyView ? (
<>
<hr />
<h5>Viewing privacy settings</h5>
<BS.Row>
<BS.Col className="mb-lg-3" xs={12} lg={3}>
<b>Description:</b> {user.description_privacy}
</BS.Col>
<BS.Col className="mb-lg-3" xs={12} lg={3}>
<b>Member list: </b>
{user.member_list_privacy}
</BS.Col>
<BS.Col className="mb-lg-3" xs={12} lg={3}>
<b>Front:</b> {user.front_privacy}
</BS.Col>
<BS.Col className="mb-lg-3" xs={12} lg={3}>
<b>Front history:</b> {user.front_history_privacy}
</BS.Col>
</BS.Row>
<BS.Button
variant="light"
onClick={() => setPrivacyView(false)}
>
Exit
</BS.Button>{" "}
<BS.Button
variant="primary"
onClick={() => setPrivacyEdit(true)}
>
Edit
</BS.Button>
<hr />
</>
) : (
""
)}
<p>
<b>Description:</b>
</p>
{ localStorage.getItem("twemoji") ? <p dangerouslySetInnerHTML={{__html: twemoji.parse(desc)}}></p> : <p dangerouslySetInnerHTML={{__html: desc}}></p>}
{ !user.banner || !localStorage.getItem("bottombanners") ? "" :
<BS.Image rounded className="mb-2" style={{width: '100%', maxHeight: '15rem', objectFit: 'cover'}} src={banner}/>
}
{privacyEdit ? (
""
) : privacyView ? (
""
) : (
<>
<BS.Button variant="light" onClick={() => setEditMode(true)}>
Edit
</BS.Button>
<BS.Button
variant="primary"
className="float-right"
onClick={() => history.push(`/profile/${user.id}`)}
>
Profile
</BS.Button>
</>
)}
</>
)}
</BS.Card.Body>
</BS.Card>
</>
);
}

View File

@ -1,134 +0,0 @@
import React, { useEffect, useState } from 'react';
import { useRouteMatch } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import * as BS from 'react-bootstrap';
import Popup from 'reactjs-popup';
import twemoji from 'twemoji';
import { FaAddressCard } from "react-icons/fa";
import defaultAvatar from '../../default_discord_avatar.png'
import Loading from "../Loading.js";
import API_URL from "../../Constants/constants.js";
import ProfileList from "./ProfileList.js";
export default function Profile () {
const match = useRouteMatch("/profile/:sysID/:memberID");
const { sysID } = useParams();
const [ system, setSystem ] = useState('');
const [ name, setName ] = useState('');
const [ tag, setTag ] = useState("");
const [ timezone, setTimezone ] = useState("");
const [ desc, setDesc ] = useState("");
const [ avatar, setAvatar ] = useState('');
const [ banner, setBanner ] = useState("");
const [ isLoading, setIsLoading ] = useState(true);
const [ isError, setIsError ] = useState(false);
useEffect (() => {
fetch(`${API_URL}s/${sysID}`,{
method: 'GET'
}).then ( res => res.json()
).then (data => {
setSystem(data);
setIsLoading(false);
})
.catch (error => {
console.log(error);
setIsError(true);
setIsLoading(false);
})
}, [sysID])
useEffect(() => {
const { toHTML } = require('../../Functions/discord-parser.js');
if (system.name) {
setName(system.name);
} else setName('');
if (system.avatar_url) {
var avatarsmall = system.avatar_url.replace('&format=jpeg', '');
setAvatar(avatarsmall.replace('?width=256&height=256', ''))
} else setAvatar('')
if (system.banner) {
setBanner(system.banner);
} else setBanner("");
if (system.tag) {
setTag(system.tag);
} else setTag('');
if (system.tz) {
setTimezone(system.tz);
} else setTimezone('');
if (system.description) {
setDesc(toHTML(system.description));
} else setDesc("(no description)");
}, [system.description, system.tag, system.avatar_url, system.tz, system.name, system.banner]);
return (match ? <ProfileList sysID={sysID} /> :
<>{ isLoading ? <Loading /> : isError ? <BS.Alert variant="danger">Something went wrong, either the system doesn't exist, or there was an error fetching data.</BS.Alert> :
<>{ system.banner && !localStorage.getItem("hidebanners") ? <div className="banner" style={{backgroundImage: `url(${banner})`}} alt=""/> : ""}
<BS.Alert variant="primary" >You are currently <b>viewing</b> a system.</BS.Alert>
<BS.Card className="mb-3 mt-3 w-100" >
<BS.Card.Header className="d-flex align-items-center justify-content-between">
<BS.Card.Title className="float-left"><FaAddressCard className="mr-4 float-left" /> {name} ({system.id})</BS.Card.Title>
{ system.avatar_url ? <Popup trigger={<BS.Image src={`${system.avatar_url}`} style={{width: 50, height: 50}} tabIndex="0" className="float-right" roundedCircle />} className="avatar" modal>
{close => (
<div className="text-center w-100 m-0" onClick={() => close()}>
<div className="m-auto" style={{maxWidth: '640px'}}>
<BS.Image src={`${avatar}`} style={{'maxWidth': '100%', height: 'auto'}} thumbnail />
</div>
</div>
)}
</Popup> :
<BS.Image src={defaultAvatar} style={{width: 50, height: 50}} className="float-right" roundedCircle />}
</BS.Card.Header>
<BS.Card.Body>
<BS.Row>
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>ID:</b> {system.id}</BS.Col>
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>Tag:</b> {tag}</BS.Col>
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>Timezone:</b> {timezone}</BS.Col>
{system.banner ?
<BS.Col className="mb-lg-3" xs={12} lg={3}>
<b>Banner:</b>{" "}
<Popup
trigger={
<BS.Button
variant="light"
size="sm"
>
View
</BS.Button>
}
className="banner"
modal
>
{(close) => (
<div className="text-center w-100" onClick={() => close()}>
<div className="m-auto" style={{maxWidth: '100%'}}>
<BS.Image src={`${banner}`} style={{maxWidth: 'auto', maxHeight: '640px'}} thumbnail />
</div>
</div>
)}
</Popup>
</BS.Col>
: "" }
</BS.Row>
<p><b>Description:</b></p>
{ localStorage.getItem("twemoji") ? <p dangerouslySetInnerHTML={{__html: twemoji.parse(desc)}}></p> : <p dangerouslySetInnerHTML={{__html: desc}}></p>}
{ !system.banner || !localStorage.getItem("bottombanners") ? "" :
<BS.Image rounded className="mb-2" style={{width: '100%', maxHeight: '15rem', objectFit: 'cover'}} src={banner}/>
}
</BS.Card.Body>
</BS.Card>
<ProfileList sysID={sysID} /> </> }
</>
)
}

View File

@ -1,167 +0,0 @@
import React, { useEffect, useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import * as BS from 'react-bootstrap'
import moment from 'moment';
import Popup from 'reactjs-popup';
import 'reactjs-popup/dist/index.css';
import autosize from 'autosize';
import LazyLoad from 'react-lazyload';
import twemoji from 'twemoji';
import defaultAvatar from '../../default_discord_avatar.png'
import { FaLink } from "react-icons/fa";
export default function MemberCard(props) {
const { sysID } = useParams();
const member = props.member;
const [ avatar, setAvatar ] = useState('')
const [ displayName, setDisplayName ] = useState("");
const [ birthday, setBirthday ] = useState("");
const [ pronouns, setPronouns ] = useState("");
const [ banner, setBanner ] = useState("");
const [ color, setColor ] = useState("");
const [ desc, setDesc ] = useState("");
const proxyTags = member.proxy_tags;
const [ proxyView, setProxyView ] = useState(false);
useEffect(() => {
autosize(document.querySelector('textarea'));
})
useEffect(() => {
const { toHTML } = require('../../Functions/discord-parser.js');
if (member.display_name) {
setDisplayName(member.display_name)
} else setDisplayName('')
if (member.birthday) {
if (member.birthday.startsWith('0004-')) {
var bdaymoment = moment(member.birthday, 'YYYY-MM-DD').format('MMM D');
setBirthday(bdaymoment);
} else {
var birthdaymoment = moment(member.birthday, 'YYYY-MM-DD').format('MMM D, YYYY');
setBirthday(birthdaymoment);
}
} else { setBirthday('');
}
if (member.avatar_url) {
var avatarsmall = member.avatar_url.replace('&format=jpeg', '');
setAvatar(avatarsmall.replace('?width=256&height=256', ''))
} else setAvatar('')
if (member.pronouns) {
setPronouns(toHTML(member.pronouns))
} else setPronouns('')
if (member.color) {
setColor(member.color);
} else setColor('');
if (member.banner) {
setBanner(member.banner);
} else setBanner("");
if (member.description) {
setDesc(toHTML(member.description));
} else setDesc("(no description)");
}, [member.description, member.color, member.birthday, member.display_name, member.pronouns, member.avatar_url, member.proxy_tags, member.banner]);
function copyLink() {
var link = `https://pk-webs.spectralitree.com/profile/${sysID}/${member.id}`
var textField = document.createElement('textarea')
textField.innerText = link
document.body.appendChild(textField);
textField.select();
textField.setSelectionRange(0, 99999);
document.execCommand('copy');
document.body.removeChild(textField);
}
function renderCard() {
return (
<BS.Card.Body style={{borderLeft: `5px solid #${color}` }}>
<BS.Row>
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>ID:</b> {member.id}</BS.Col>
{ member.display_name ? <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Display name:</b> {displayName}</BS.Col> : "" }
{ member.birthday ? <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Birthday:</b> {birthday}</BS.Col> : "" }
{ member.pronouns ? <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Pronouns:</b> <span dangerouslySetInnerHTML={{__html: pronouns}}></span></BS.Col> : "" }
{ member.color ? <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Color:</b> {color}</BS.Col> : "" }
{ proxyView ? "" : <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Proxy tags:</b> <BS.Button variant="light" size="sm" onClick={() => setProxyView(true)}>View</BS.Button></BS.Col> }
{ proxyView || !member.banner ? "" :
<BS.Col className="mb-lg-3" xs={12} lg={3}>
<b>Banner:</b>{" "}
<Popup
trigger={
<BS.Button
variant="light"
size="sm"
>
View
</BS.Button>
}
className="banner"
modal
>
{(close) => (
<div className="text-center w-100" onClick={() => close()}>
<div className="m-auto" style={{maxWidth: '100%'}}>
<BS.Image src={`${banner}`} style={{maxWidth: 'auto', maxHeight: '640px'}} thumbnail />
</div>
</div>
)}
</Popup>
</BS.Col>
}
</BS.Row>
{ proxyView ? <><hr/>
<h5>Viewing proxy tags</h5>
<BS.Row className="mb-2">
{ proxyTags.length === 0 ? <BS.Col className="mb-lg-2"><b>No proxy tags set.</b></BS.Col> : proxyTags.map((proxytag) => <BS.Col key={proxytag.index} className="mb-lg-2" xs={12} lg={2}> <code>{proxytag.prefix}text{proxytag.suffix}</code></BS.Col> )}
</BS.Row>
<BS.Button variant="light" onClick={() => setProxyView(false)}>Exit</BS.Button>
<hr/></> : "" }
<p><b>Description:</b></p>
{ localStorage.getItem("twemoji") ? <p dangerouslySetInnerHTML={{__html: twemoji.parse(desc)}}></p> : <p dangerouslySetInnerHTML={{__html: desc}}></p>}
{ !member.banner || !localStorage.getItem("bottombanners") ? "" :
<BS.Image rounded className="mb-2" style={{width: '100%', maxHeight: '15rem', objectFit: 'cover'}} src={banner}/>
}
<BS.Row><BS.Col><Link to={`${sysID}/${member.id}`}><BS.Button variant="primary" className="float-right">View page</BS.Button></Link></BS.Col></BS.Row> </BS.Card.Body>
)
}
return (
<LazyLoad offset={100}>
<BS.Card.Header className="d-flex align-items-center justify-content-between">
<div> <BS.OverlayTrigger placement="left" overlay={
<BS.Tooltip>
Copy link
</BS.Tooltip>
}><BS.Button variant="link" onClick={() => copyLink()}><FaLink style={{fontSize: '1.25rem'}}/></BS.Button></BS.OverlayTrigger>
{ localStorage.getItem('pagesonly') ?
<Link to={`${sysID}/${member.id}`}><BS.Button variant="link"> <b>{member.name}</b> ({member.id})</BS.Button></Link>
: <BS.Accordion.Toggle as={BS.Button} variant="link" eventKey={member.id} > <b>{member.name}</b> ({member.id})</BS.Accordion.Toggle>}</div>
{ member.avatar_url ? <Popup trigger={<BS.Image src={`${member.avatar_url}`} style={{width: 50, height: 50}} tabIndex="0" className="float-right" roundedCircle />} className="avatar" modal>
{close => (
<div className="text-center w-100 m-0" onClick={() => close()}>
<div className="m-auto" style={{maxWidth: '640px'}}>
<BS.Image src={`${avatar}`} style={{'maxWidth': '100%', height: 'auto'}} thumbnail />
</div>
</div>
)}
</Popup> :
<BS.Image src={defaultAvatar} style={{width: 50, height: 50}} tabIndex="0" className="float-right" roundedCircle />}
</BS.Card.Header>
{localStorage.getItem("expandcards") ? renderCard() : <BS.Accordion.Collapse eventKey={member.id}>
{renderCard()}
</BS.Accordion.Collapse>}
</LazyLoad>
)
}

View File

@ -1,247 +0,0 @@
import React, { useEffect, useState, useCallback } from 'react';
import { Switch, Route, useParams, useRouteMatch } from 'react-router-dom';
import * as BS from 'react-bootstrap'
import 'reactjs-popup/dist/index.css';
import ProfileCard from './ProfileCard.js'
import ProfilePages from '../../Pages/ProfilePages.js'
import Loading from "../Loading.js";
import API_URL from "../../Constants/constants.js";
export default function Memberlist() {
const { path } = useRouteMatch();
const { sysID } = useParams();
const [isLoading, setIsLoading ] = useState(false);
const [isError, setIsError ] = useState(false);
const [isForbidden, setIsForbidden ] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [membersPerPage, setMembersPerPage] = useState(25);
const [members, setMembers ] = useState([]);
const [searchBy, setSearchBy] = useState('name')
const [sortBy, setSortBy] = useState('name')
const [sortOrder, setSortOrder] = useState('ascending')
const [value, setValue] = useState('');
const fetchMembers = useCallback( () => {
setIsLoading(true);
setIsError(false);
setMembersPerPage(localStorage.getItem("expandcards") ? 10 : 25);
fetch(`${API_URL}s/${sysID}/members`,{
method: 'GET',
}).then ( res => {
if (res.status === 403) {
throw new Error('Access denied!');
}
return res.json()
}
).then (data => {
setMembers(data)
setIsLoading(false);
})
.catch (error => {
if (error.message === 'Access denied!') {
setIsForbidden(true);
} else {
console.log(error);
setIsError(true);
}
setIsLoading(false);
})
}, [sysID])
useEffect(() => {
fetchMembers();
}, [fetchMembers])
const indexOfLastMember = currentPage * membersPerPage;
const indexOfFirstMember = indexOfLastMember - membersPerPage;
let Members = members.map(member => {
if (member.display_name) {
return {...member, displayName: member.display_name}
} return {...member, displayName: member.name}
})
let Members1 = Members.map(member => {
if (member.description) {
return {...member, desc: member.description}
} return {...member, desc: "(no description)"}
})
const currentMembers = Members1.filter(member => {
if (!value) return true;
if (searchBy === 'name') {
if (member.name.toLowerCase().includes(value.toLowerCase())) {
return true;
}
return false;
} else if (searchBy === 'display name') {
if (member.displayName.toLowerCase().includes(value.toLowerCase())) {
return true;
}
return false
} else if (searchBy === 'description') {
if (member.desc.toLowerCase().includes(value.toLowerCase())) {
return true;
}
return false;
} else if (searchBy === 'ID') {
if (member.id.toLowerCase().includes(value.toLowerCase())) {
return true;
}
return false;
}
return false;
})
const active = currentPage;
const pageAmount = Math.ceil(currentMembers.length / membersPerPage);
var sortMembers = currentMembers;
if (sortBy === 'name') {
if (sortOrder === 'descending') {
sortMembers = currentMembers.sort((a, b) => a.name.localeCompare(b.name)).reverse().slice(indexOfFirstMember, indexOfLastMember);
} else sortMembers = currentMembers.sort((a, b) => a.name.localeCompare(b.name)).slice(indexOfFirstMember, indexOfLastMember);
}
else if (sortBy === 'display name') {
if (sortOrder === 'descending') {
sortMembers = currentMembers.sort((a, b) => a.displayName.localeCompare(b.displayName)).reverse().slice(indexOfFirstMember, indexOfLastMember);
} else sortMembers = currentMembers.sort((a, b) => a.displayName.localeCompare(b.displayName)).slice(indexOfFirstMember, indexOfLastMember);
}
else if (sortBy === 'ID') {
if (sortOrder === 'descending') {
sortMembers = currentMembers.sort((a, b) => a.id.localeCompare(b.id)).reverse().slice(indexOfFirstMember, indexOfLastMember);
} else sortMembers = currentMembers.sort((a, b) => a.id.localeCompare(b.id)).slice(indexOfFirstMember, indexOfLastMember);
}
const memberList = sortMembers.map((member) => <BS.Card key={member.id} className={localStorage.getItem("expandcards") ? "mb-3" : ""}>
<ProfileCard
member={member}
/>
</BS.Card>
);
return (
<Switch>
<Route exact path={path}>
<>
<BS.Row className="mb-lg-3 justfiy-content-md-center">
<BS.Col xs={12} lg={3}>
<BS.Form>
<BS.InputGroup className="mb-3">
<BS.Form.Control disabled placeholder='Page length:'/>
<BS.Form.Control as="select" defaultValue={localStorage.getItem("expandcards") ? 10 : 25} onChange={e => {
setMembersPerPage(e.target.value);
setCurrentPage(1);
}}>
<option>10</option>
<option>25</option>
<option>50</option>
<option>100</option>
</BS.Form.Control>
</BS.InputGroup>
</BS.Form>
</BS.Col>
<BS.Col xs={12} lg={3}>
<BS.Form>
<BS.InputGroup className="mb-3">
<BS.Form.Control disabled placeholder='Search by:'/>
<BS.Form.Control as="select" defaultValue={searchBy} onChange={e => {
setSearchBy(e.target.value)
}}>
<option>name</option>
<option>display name</option>
<option>description</option>
<option>ID</option>
</BS.Form.Control>
</BS.InputGroup>
</BS.Form>
</BS.Col>
<BS.Col xs={12} lg={3}>
<BS.Form>
<BS.InputGroup className="mb-3">
<BS.Form.Control disabled placeholder='Sort by:'/>
<BS.Form.Control as="select" defaultValue={sortBy} onChange={e => {
setSortBy(e.target.value)
}}>
<option>name</option>
<option>display name</option>
<option>ID</option>
</BS.Form.Control>
</BS.InputGroup>
</BS.Form>
</BS.Col>
<BS.Col xs={12} lg={3}>
<BS.Form>
<BS.InputGroup className="mb-3">
<BS.Form.Control disabled placeholder='Sort order:'/>
<BS.Form.Control as="select" defaultValue={sortOrder} onChange={e => {
setSortOrder(e.target.value)
}}>
<option>ascending</option>
<option>descending</option>
</BS.Form.Control>
</BS.InputGroup>
</BS.Form>
</BS.Col>
</BS.Row>
<BS.Row className="justify-content-md-center">
<BS.Col className="mb-3" xs={12} lg={7}>
<BS.Form>
<BS.Form.Control value={value} onChange={e => {setValue(e.target.value); setCurrentPage(1);}} placeholder={`Search by ${searchBy}`}/>
</BS.Form>
</BS.Col>
<BS.Col className="mb-3" xs={12} lg={2}>
<BS.Button type="primary" className="m-0" block onClick={() => fetchMembers()}>Refresh</BS.Button>
</BS.Col>
</BS.Row>
<BS.Row className="justify-content-md-center">
<BS.Pagination className="ml-auto mr-auto">
{ currentPage === 1 ? <BS.Pagination.Prev disabled/> : <BS.Pagination.Prev onClick={() => setCurrentPage(currentPage - 1)} />}
{ currentPage < 3 ? "" : <BS.Pagination.Item onClick={() => setCurrentPage(1)} active={1 === active}>{1}</BS.Pagination.Item>}
{ currentPage < 4 ? "" : currentPage < 5 ? <BS.Pagination.Item onClick={() => setCurrentPage(2)} active={2 === active}>{2}</BS.Pagination.Item> : <BS.Pagination.Ellipsis disabled />}
{ currentPage > 1 ? <BS.Pagination.Item onClick={() => setCurrentPage(currentPage - 1)}>{currentPage - 1}</BS.Pagination.Item> : "" }
<BS.Pagination.Item onClick={() => setCurrentPage(currentPage)} active={currentPage === active}>{currentPage}</BS.Pagination.Item>
{ currentPage < pageAmount ? <BS.Pagination.Item onClick={() => setCurrentPage(currentPage + 1)}>{currentPage + 1}</BS.Pagination.Item> : "" }
{ currentPage > pageAmount - 3 ? "" : currentPage === pageAmount - 3 ? <BS.Pagination.Item onClick={() => setCurrentPage(pageAmount - 1)} active={pageAmount - 1 === active}>{pageAmount - 1}</BS.Pagination.Item> : <BS.Pagination.Ellipsis disabled />}
{ currentPage > pageAmount - 2 ? "" : <BS.Pagination.Item onClick={() => setCurrentPage(pageAmount)} active={pageAmount === active}>{pageAmount}</BS.Pagination.Item>}
{ currentPage === pageAmount ? <BS.Pagination.Next disabled /> :<BS.Pagination.Next onClick={() => setCurrentPage(currentPage + 1)} />}
</BS.Pagination>
</BS.Row>
{ isLoading ? <Loading /> : isError ?
<BS.Alert variant="danger">Error fetching members.</BS.Alert> : isForbidden ? <BS.Alert variant="danger">Member list is private.</BS.Alert> :
<>
<BS.Accordion className="mb-3 mt-3 w-100" defaultActiveKey="0">
{memberList}
</BS.Accordion>
<BS.Row className="justify-content-md-center">
<BS.Pagination className="ml-auto mr-auto">
{ currentPage === 1 ? <BS.Pagination.Prev disabled/> : <BS.Pagination.Prev onClick={() => setCurrentPage(currentPage - 1)} />}
{ currentPage < 3 ? "" : <BS.Pagination.Item onClick={() => setCurrentPage(1)} active={1 === active}>{1}</BS.Pagination.Item>}
{ currentPage < 4 ? "" : currentPage < 5 ? <BS.Pagination.Item onClick={() => setCurrentPage(2)} active={2 === active}>{2}</BS.Pagination.Item> : <BS.Pagination.Ellipsis disabled />}
{ currentPage > 1 ? <BS.Pagination.Item onClick={() => setCurrentPage(currentPage - 1)}>{currentPage - 1}</BS.Pagination.Item> : "" }
<BS.Pagination.Item onClick={() => setCurrentPage(currentPage)} active={currentPage === active}>{currentPage}</BS.Pagination.Item>
{ currentPage < pageAmount ? <BS.Pagination.Item onClick={() => setCurrentPage(currentPage + 1)}>{currentPage + 1}</BS.Pagination.Item> : "" }
{ currentPage > pageAmount - 3 ? "" : currentPage === pageAmount - 3 ? <BS.Pagination.Item onClick={() => setCurrentPage(pageAmount - 1)} active={pageAmount - 1 === active}>{pageAmount - 1}</BS.Pagination.Item> : <BS.Pagination.Ellipsis disabled />}
{ currentPage > pageAmount - 2 ? "" : <BS.Pagination.Item onClick={() => setCurrentPage(pageAmount)} active={pageAmount === active}>{pageAmount}</BS.Pagination.Item>}
{ currentPage === pageAmount ? <BS.Pagination.Next disabled /> :<BS.Pagination.Next onClick={() => setCurrentPage(currentPage + 1)} />}
</BS.Pagination>
</BS.Row>
</>
}
</>
</Route>
<Route path={`/profile/${sysID}/:memberID`}>
{ isLoading ? <Loading/> :
<ProfilePages members={members}/>}
</Route>
</Switch>
)
}

View File

@ -1,163 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import * as BS from 'react-bootstrap'
import moment from 'moment';
import Popup from 'reactjs-popup';
import 'reactjs-popup/dist/index.css';
import autosize from 'autosize';
import twemoji from 'twemoji';
import defaultAvatar from '../../default_discord_avatar.png'
import { FaLink } from "react-icons/fa";
export default function ProfilePage(props) {
const location = useLocation();
const member = props.member;
const [ avatar, setAvatar ] = useState('')
const [ banner, setBanner ] = useState("");
const [ displayName, setDisplayName ] = useState("");
const [ birthday, setBirthday ] = useState("");
const [ pronouns, setPronouns ] = useState("");
const [ color, setColor ] = useState("");
const [ desc, setDesc ] = useState("");
const proxyTags = member.proxy_tags;
const [ proxyView, setProxyView ] = useState(false);
useEffect(() => {
autosize(document.querySelector('textarea'));
})
useEffect(() => {
const { toHTML } = require('../../Functions/discord-parser.js');
if (member.display_name) {
setDisplayName(member.display_name)
} else setDisplayName('')
if (member.birthday) {
if (member.birthday.startsWith('0004-')) {
var bdaymoment = moment(member.birthday, 'YYYY-MM-DD').format('MMM D');
setBirthday(bdaymoment);
} else {
var birthdaymoment = moment(member.birthday, 'YYYY-MM-DD').format('MMM D, YYYY');
setBirthday(birthdaymoment);
}
} else { setBirthday('');
}
if (member.avatar_url) {
var avatarsmall = member.avatar_url.replace('&format=jpeg', '');
setAvatar(avatarsmall.replace('?width=256&height=256', ''))
} else setAvatar('')
if (member.pronouns) {
setPronouns(toHTML(member.pronouns))
} else setPronouns('')
if (member.color) {
setColor(member.color);
} else setColor('');
if (member.banner) {
setBanner(member.banner);
} else setBanner("");
if (member.description) {
setDesc(toHTML(member.description));
} else setDesc("(no description)");
}, [member.description, member.color, member.birthday, member.display_name, member.pronouns, member.avatar_url, member.proxy_tags, member.banner]);
function copyLink() {
var link = `https://spectralitree.github.io${location.pathname}`
var textField = document.createElement('textarea')
textField.innerText = link
document.body.appendChild(textField);
textField.select();
textField.setSelectionRange(0, 99999);
document.execCommand('copy');
document.body.removeChild(textField);
}
return (
<>
{ localStorage.getItem('colorbg') && member.color ? "" : <><div className="backdrop" style={{backgroundColor: `#${color}`}}/>
{ !localStorage.getItem('fullbg') ? <div className="backdrop-overlay"/> : "" }</> }
{ member.banner && !localStorage.getItem("hidebanners") ? <div className="banner" style={{backgroundImage: `url(${banner})`}} alt=""/> : ""}
<BS.Alert variant="primary" >You are currently <b>viewing</b> a member.</BS.Alert>
<BS.Card className="mb-5">
<BS.Card.Header className="d-flex align-items-center justify-content-between">
<div> <BS.OverlayTrigger placement="left" overlay={
<BS.Tooltip>
Copy link
</BS.Tooltip>
}><BS.Button variant="link" onClick={() => copyLink()}><FaLink style={{fontSize: '1.25rem'}}/></BS.Button></BS.OverlayTrigger>
<BS.Button variant="link" ><b>{member.name}</b> ({member.id})</BS.Button></div>
{ member.avatar_url ? <Popup trigger={<BS.Image src={`${member.avatar_url}`} style={{width: 50, height: 50}} tabIndex="0" className="float-right" roundedCircle />} className="avatar" modal>
{close => (
<div className="text-center w-100 m-0" onClick={() => close()}>
<div className="m-auto" style={{maxWidth: '640px'}}>
<BS.Image src={`${avatar}`} style={{'maxWidth': '100%', height: 'auto'}} thumbnail />
</div>
</div>
)}
</Popup> :
<BS.Image src={defaultAvatar} style={{width: 50, height: 50}} tabIndex="0" className="float-right" roundedCircle />}
</BS.Card.Header>
<BS.Card.Body style={{ borderLeft: localStorage.getItem('colorbg') ? `5px solid #${color}` : ''}}>
<BS.Row>
<BS.Col className="mb-lg-3" xs={12} lg={3}><b>ID:</b> {member.id}</BS.Col>
{ member.display_name ? <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Display name:</b> { localStorage.getItem("twemoji") ? <span dangerouslySetInnerHTML={{__html: twemoji.parse(displayName)}}></span> : <span dangerouslySetInnerHTML={{__html: displayName}}></span>}</BS.Col> : "" }
{ member.birthday ? <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Birthday:</b> {birthday}</BS.Col> : "" }
{ member.pronouns ? <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Pronouns:</b> { localStorage.getItem("twemoji") ? <span dangerouslySetInnerHTML={{__html: twemoji.parse(pronouns)}}></span> : <span dangerouslySetInnerHTML={{__html: pronouns}}></span>}</BS.Col> : "" }
{ member.color ? <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Color:</b> {color}</BS.Col> : "" }
{ proxyView ? "" : <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Proxy tags:</b> <BS.Button variant="light" size="sm" onClick={() => setProxyView(true)}>View</BS.Button></BS.Col> }
{ proxyView || !member.banner ? "" :
<BS.Col className="mb-lg-3" xs={12} lg={3}>
<b>Banner:</b>{" "}
<Popup
trigger={
<BS.Button
variant="light"
size="sm"
>
View
</BS.Button>
}
className="banner"
modal
>
{(close) => (
<div className="text-center w-100" onClick={() => close()}>
<div className="m-auto" style={{maxWidth: '100%'}}>
<BS.Image src={`${banner}`} style={{maxWidth: 'auto', maxHeight: '640px'}} thumbnail />
</div>
</div>
)}
</Popup>
</BS.Col>
}
</BS.Row>
{ proxyView ? <><hr/>
<h5>Viewing proxy tags</h5>
<BS.Row className="mb-2">
{ proxyTags.length === 0 ? <BS.Col className="mb-lg-2"><b>No proxy tags set.</b></BS.Col> : proxyTags.map((proxytag) => <BS.Col key={proxytag.index} className="mb-lg-2" xs={12} lg={2}> <code>{proxytag.prefix}text{proxytag.suffix}</code></BS.Col> )}
</BS.Row>
<BS.Button variant="light" onClick={() => setProxyView(false)}>Exit</BS.Button>
<hr/></> : "" }
<p><b>Description:</b></p>
{ localStorage.getItem("twemoji") ? <p dangerouslySetInnerHTML={{__html: twemoji.parse(desc)}}></p> : <p dangerouslySetInnerHTML={{__html: desc}}></p>}
{ !member.banner || !localStorage.getItem("bottombanners") ? "" :
<BS.Image rounded className="mb-2" style={{width: '100%', maxHeight: '15rem', objectFit: 'cover'}} src={banner}/>
}
{ props.list ? <BS.Row><BS.Col><Link to={location.pathname.substring(0, location.pathname.lastIndexOf('/'))}><BS.Button variant="primary" className="float-right">Back</BS.Button></Link></BS.Col></BS.Row> : ""}
</BS.Card.Body>
</BS.Card>
</>
)
}

View File

@ -1,3 +0,0 @@
const API_URL = "https://api.pluralkit.me/v1/";
export default API_URL

View File

@ -1,327 +0,0 @@
const markdown = require('simple-markdown');
const highlight = require('highlight.js');
function htmlTag(tagName, content, attributes, isClosed = true, state = { }) {
if (typeof isClosed === 'object') {
state = isClosed;
isClosed = true;
}
if (!attributes)
attributes = { };
if (attributes.class && state.cssModuleNames)
attributes.class = attributes.class.split(' ').map(cl => state.cssModuleNames[cl] || cl).join(' ');
let attributeString = '';
for (let attr in attributes) {
// Removes falsy attributes
if (Object.prototype.hasOwnProperty.call(attributes, attr) && attributes[attr])
attributeString += ` ${markdown.sanitizeText(attr)}="${markdown.sanitizeText(attributes[attr])}"`;
}
let unclosedTag = `<${tagName}${attributeString}>`;
if (isClosed)
return unclosedTag + content + `</${tagName}>`;
return unclosedTag;
}
markdown.htmlTag = htmlTag;
const rules = {
blockQuote: Object.assign({ }, markdown.defaultRules.blockQuote, {
match: function(source, state, prevSource) {
return !/^$|\n *$/.test(prevSource) || state.inQuote ? null : /^( *>>> ([\s\S]*))|^( *> [^\n]*(\n *> [^\n]*)*\n?)/.exec(source);
},
parse: function(capture, parse, state) {
const all = capture[0];
const isBlock = Boolean(/^ *>>> ?/.exec(all));
const removeSyntaxRegex = isBlock ? /^ *>>> ?/ : /^ *> ?/gm;
const content = all.replace(removeSyntaxRegex, '');
return {
content: parse(content, Object.assign({ }, state, { inQuote: true })),
type: 'blockQuote'
}
}
}),
codeBlock: Object.assign({ }, markdown.defaultRules.codeBlock, {
match: markdown.inlineRegex(/^```(([a-z0-9-]+?)\n+)?\n*([^]+?)\n*```/i),
parse: function(capture, parse, state) {
return {
lang: (capture[2] || '').trim(),
content: capture[3] || '',
inQuote: state.inQuote || false
};
},
html: (node, output, state) => {
let code;
if (node.lang && highlight.getLanguage(node.lang))
code = highlight.highlight(node.lang, node.content, true); // Discord seems to set ignoreIllegals: true
if (code && state.cssModuleNames) // Replace classes in hljs output
code.value = code.value.replace(/<span class="([a-z0-9-_ ]+)">/gi, (str, m) =>
str.replace(m, m.split(' ').map(cl => state.cssModuleNames[cl] || cl).join(' ')));
return htmlTag('pre', htmlTag(
'code', code ? code.value : markdown.sanitizeText(node.content), { class: `hljs${code ? ' ' + code.language : ''}` }, state
), null, state);
}
}),
newline: markdown.defaultRules.newline,
escape: markdown.defaultRules.escape,
autolink: Object.assign({ }, markdown.defaultRules.autolink, {
parse: capture => {
return {
content: [{
type: 'text',
content: capture[1]
}],
target: capture[1]
};
},
html: (node, output, state) => {
return htmlTag('a', output(node.content, state), { href: markdown.sanitizeUrl(node.target) }, state);
}
}),
url: Object.assign({ }, markdown.defaultRules.url, {
parse: capture => {
return {
content: [{
type: 'text',
content: capture[1]
}],
target: capture[1]
}
},
html: (node, output, state) => {
return htmlTag('a', output(node.content, state), { href: markdown.sanitizeUrl(node.target) }, state);
}
}),
em: Object.assign({ }, markdown.defaultRules.em, {
parse: function(capture, parse, state) {
const parsed = markdown.defaultRules.em.parse(capture, parse, Object.assign({ }, state, { inEmphasis: true }));
return state.inEmphasis ? parsed.content : parsed;
},
}),
strong: markdown.defaultRules.strong,
u: markdown.defaultRules.u,
strike: Object.assign({ }, markdown.defaultRules.del, {
match: markdown.inlineRegex(/^~~([\s\S]+?)~~(?!_)/),
}),
inlineCode: Object.assign({ }, markdown.defaultRules.inlineCode, {
match: source => markdown.defaultRules.inlineCode.match.regex.exec(source),
html: function(node, output, state) {
return htmlTag('code', markdown.sanitizeText(node.content.trim()), null, state);
}
}),
text: Object.assign({ }, markdown.defaultRules.text, {
match: source => /^[\s\S]+?(?=[^0-9A-Za-z\s\u00c0-\uffff-]|\n\n|\n|\w+:\S|$)/.exec(source),
html: function(node, output, state) {
if (state.escapeHTML)
return markdown.sanitizeText(node.content);
return node.content;
}
}),
emoticon: {
order: markdown.defaultRules.text.order,
match: source => /^(¯\\_\(\)_\/¯)/.exec(source),
parse: function(capture) {
return {
type: 'text',
content: capture[1]
};
},
html: function(node, output, state) {
return output(node.content, state);
},
},
br: Object.assign({ }, markdown.defaultRules.br, {
match: markdown.anyScopeRegex(/^\n/),
}),
spoiler: {
order: 0,
match: source => /^\|\|([\s\S]+?)\|\|/.exec(source),
parse: function(capture, parse, state) {
return {
content: parse(capture[1], state)
};
},
html: function(node, output, state) {
return htmlTag('a', output(node.content, state), { class: 'd-spoiler' }, state);
}
},
link: Object.assign({ }, markdown.defaultRules.link, {
html: (node, output, state) => {
return htmlTag('a', output(node.content, state), { href: markdown.sanitizeUrl(node.target), rel: "noreferrer", target: "_blank" }, state);
}
})
};
const discordCallbackDefaults = {
user: node => '@' + markdown.sanitizeText(node.id),
channel: node => '#' + markdown.sanitizeText(node.id),
role: node => '&' + markdown.sanitizeText(node.id),
everyone: () => '@everyone',
here: () => '@here'
};
const rulesDiscord = {
discordUser: {
order: markdown.defaultRules.strong.order,
match: source => /^<@!?([0-9]*)>/.exec(source),
parse: function(capture) {
return {
id: capture[1]
};
},
html: function(node, output, state) {
return htmlTag('span', state.discordCallback.user(node), { class: 'd-mention d-user' }, state);
}
},
discordChannel: {
order: markdown.defaultRules.strong.order,
match: source => /^<#?([0-9]*)>/.exec(source),
parse: function(capture) {
return {
id: capture[1]
};
},
html: function(node, output, state) {
return htmlTag('span', state.discordCallback.channel(node), { class: 'd-mention d-channel' }, state);
}
},
discordRole: {
order: markdown.defaultRules.strong.order,
match: source => /^<@&([0-9]*)>/.exec(source),
parse: function(capture) {
return {
id: capture[1]
};
},
html: function(node, output, state) {
return htmlTag('span', state.discordCallback.role(node), { class: 'd-mention d-role' }, state);
}
},
discordEmoji: {
order: markdown.defaultRules.strong.order,
match: source => /^<(a?):(\w+):(\d+)>/.exec(source),
parse: function(capture) {
return {
animated: capture[1] === "a",
name: capture[2],
id: capture[3],
};
},
html: function(node, output, state) {
return htmlTag('img', '', {
class: `d-emoji${node.animated ? ' d-emoji-animated' : ''}`,
src: `https://cdn.discordapp.com/emojis/${node.id}.${node.animated ? 'gif' : 'png'}`,
alt: `:${node.name}:`
}, false, state);
}
},
discordEveryone: {
order: markdown.defaultRules.strong.order,
match: source => /^@everyone/.exec(source),
parse: function() {
return { };
},
html: function(node, output, state) {
return htmlTag('span', state.discordCallback.everyone(node), { class: 'd-mention d-user' }, state);
}
},
discordHere: {
order: markdown.defaultRules.strong.order,
match: source => /^@here/.exec(source),
parse: function() {
return { };
},
html: function(node, output, state) {
return htmlTag('span', state.discordCallback.here(node), { class: 'd-mention d-user' }, state);
}
}
};
Object.assign(rules, rulesDiscord);
const rulesDiscordOnly = Object.assign({ }, rulesDiscord, {
text: Object.assign({ }, markdown.defaultRules.text, {
match: source => /^[\s\S]+?(?=[^0-9A-Za-z\s\u00c0-\uffff-]|\n\n|\n|\w+:\S|$)/.exec(source),
html: function(node, output, state) {
if (state.escapeHTML)
return markdown.sanitizeText(node.content);
return node.content;
}
})
});
const rulesEmbed = Object.assign({ }, rules, {
link: markdown.defaultRules.link
});
const parser = markdown.parserFor(rules);
const htmlOutput = markdown.htmlFor(markdown.ruleOutput(rules, 'html'));
const parserDiscord = markdown.parserFor(rulesDiscordOnly);
const htmlOutputDiscord = markdown.htmlFor(markdown.ruleOutput(rulesDiscordOnly, 'html'));
const parserEmbed = markdown.parserFor(rulesEmbed);
const htmlOutputEmbed = markdown.htmlFor(markdown.ruleOutput(rulesEmbed, 'html'));
/**
* Parse markdown and return the HTML output
* @param {String} source Source markdown content
* @param {Object} [options] Options for the parser
* @param {Boolean} [options.embed=false] Parse as embed content
* @param {Boolean} [options.escapeHTML=true] Escape HTML in the output
* @param {Boolean} [options.discordOnly=false] Only parse Discord-specific stuff (such as mentions)
* @param {Object} [options.discordCallback] Provide custom handling for mentions and emojis
* @param {Object} [options.cssModuleNames] An object mapping css classes to css module classes
*/
function toHTML(source, options, customParser, customHtmlOutput) {
if ((customParser || customHtmlOutput) && (!customParser || !customHtmlOutput))
throw new Error('You must pass both a custom parser and custom htmlOutput function, not just one');
options = Object.assign({
embed: false,
escapeHTML: true,
discordOnly: false,
discordCallback: { }
}, options || { });
let _parser = parser;
let _htmlOutput = htmlOutput;
if (customParser) {
_parser = customParser;
_htmlOutput = customHtmlOutput;
} else if (options.discordOnly) {
_parser = parserDiscord;
_htmlOutput = htmlOutputDiscord;
} else if (options.embed) {
_parser = parserEmbed;
_htmlOutput = htmlOutputEmbed;
}
const state = {
inline: true,
inQuote: false,
inEmphasis: false,
escapeHTML: options.escapeHTML,
cssModuleNames: options.cssModuleNames || null,
discordCallback: Object.assign({ }, discordCallbackDefaults, options.discordCallback)
};
return _htmlOutput(_parser(source, state), state);
}
module.exports = {
parser: source => parser(source, { inline: true }),
htmlOutput,
toHTML,
rules,
rulesDiscordOnly,
rulesEmbed,
markdownEngine: markdown,
htmlTag
};

View File

@ -1,2 +0,0 @@
import { createBrowserHistory } from "history";
export default createBrowserHistory();

View File

@ -1,13 +0,0 @@
import React from 'react';
import System from '../Components/Private/System.js'
import Memberlist from '../Components/Private/Memberlist.js'
export default function Dash(props) {
return (<>
<System />
<Memberlist />
</>
);
}

View File

@ -1,211 +0,0 @@
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import * as fetch from 'node-fetch';
import Loading from "../Components/Loading";
import * as BS from "react-bootstrap";
import history from "../History.js";
import { FaLockOpen, FaHome } from "react-icons/fa";
import API_URL from "../Constants/constants.js";
const Home = ({isInvalid, setIsInvalid, isLoading, setIsLoading, isSubmit, setIsSubmit, forceUpdate }) => {
const { register, handleSubmit } = useForm();
const [ errorMessage, setErrorMessage ] = useState("");
// submit login form, add the token to the localstorage
const onSubmit = (data) => {
localStorage.setItem("token", data.pkToken);
logIn();
};
function logIn() {
// make sure the token is not set to invalid and add a funny little spinner to indicate loading
setIsInvalid(false);
setIsLoading(true);
// then fetch the system data with the token stored in localstorage
fetch(`${API_URL}s/`, {
method: "GET",
headers: {
Authorization: localStorage.getItem("token"),
},
})
// put all the system data in localstorage
.then((res) => {
if (!res.ok)
throw new Error('HTTP Status ' + res.status);
return res.json();
})
.then((data) => {
localStorage.setItem("user", JSON.stringify(data));
setIsSubmit(true);
setIsLoading(false);
history.push("/dash");
})
// remove the token and user data from localstorage if there's an error, also set the token as invalid
.catch((error) => {
console.log(error);
setIsInvalid(true);
setErrorMessage(error.message);
if (error.message === "HTTP Status 401")
setErrorMessage("Your token is invalid.")
localStorage.removeItem("token");
localStorage.removeItem("user");
setIsLoading(false);
});
}
// Logout function
function logOut() {
setIsSubmit(false);
localStorage.removeItem("token");
localStorage.removeItem("user");
history.push("/");
forceUpdate();
};
// when the homepage loads, check if there's a token, if there is, check if it's still valid
// removing the dependency array causes a rerender loop, so just ignore ESlint here
useEffect(() => {
if (localStorage.getItem('token')) {
checkLogIn();
}
}, []);
// very similar to LogIn(), only difference is that it doesn't push the user afterwards
// TODO: useless double code that probably could be refactored somehow
function checkLogIn() {
setIsInvalid(false);
setIsLoading(true);
fetch(`${API_URL}s/`,{
method: 'GET',
headers: {
'Authorization': localStorage.getItem("token")
}}).then ( res => res.json()
).then (data => {
localStorage.setItem('user', JSON.stringify(data));
setIsSubmit(true);
setIsLoading(false);
})
.catch (error => {
console.log(error);
setIsInvalid(true);
localStorage.removeItem('token');
localStorage.removeItem('user');
setIsLoading(false);
})
};
return (
<>
{/* if the page is loading, just show the loading component */}
{isLoading ? (
<Loading />
) : (
<BS.Card className="mb-3 mt-3">
<BS.Card.Header className="d-flex align-items-center justify-content-between">
<BS.Card.Title>
<FaLockOpen className="mr-3" />
Login
</BS.Card.Title>
</BS.Card.Header>
<BS.Card.Body>
{/* if the login form has been submitted, and there's no user object, show a generic error */}
{isSubmit && !localStorage.getItem("user") ? (
<BS.Alert variant="danger">
Something went wrong, please try again.
</BS.Alert>
) : (
""
)}
{/* if the inserted token is invalid, show invalid error!
this also shows if the token used in checkLogIn() is invalid */}
{isInvalid ? (
<BS.Alert variant="danger">{errorMessage}</BS.Alert>
) : (
""
)}
{ // if there's a user object in localstorage, and there's a token in localstorage, the user is logged in already
localStorage.getItem("user") && localStorage.getItem("token") ? (
<>
<p>
You are logged in already, click here to continue to the dash.
</p>
<BS.Button
variant="primary"
onClick={() => history.push("/dash")}
>
Continue to dash
</BS.Button>
<BS.Button style={{float: 'right'}} variant="danger"
onClick={() => logOut()}
>Log out
</BS.Button>
</>
) : (
// otherwise, show login form
<BS.Form onSubmit={handleSubmit(onSubmit)}>
<BS.Form.Row>
<BS.Col className="mb-1" xs={12} lg={10}>
<BS.Form.Label>
Enter your token here. You can get your token by using{" "}
<b>"pk;token"</b>.
</BS.Form.Label>
</BS.Col>
</BS.Form.Row>
<BS.Form.Row>
<BS.Col xs={12} lg={10}>
<BS.Form.Control
required
name="pkToken"
type="text"
{...register("pkToken")}
placeholder="token"
/>
</BS.Col>
<BS.Col>
<BS.Button variant="primary" type="submit" block>
Submit
</BS.Button>
</BS.Col>
</BS.Form.Row>
</BS.Form>
)}
</BS.Card.Body>
</BS.Card>
)}
<BS.Card>
<BS.Card.Header>
<BS.Card.Title>
<FaHome className="mr-3" />
Welcome!
</BS.Card.Title>
</BS.Card.Header>
<BS.Card.Body>
<p>Pk-webs is a web interface for PluralKit, it lets you edit your system and members using PluralKit's API, without having to use commands on discord.</p>
<blockquote>
<p>This website is an ongoing project and will be sporadically updated. If you have any issues or questions, join <a href='https://discord.gg/PczBt78'>PluralKit's support server</a> and ping us (Fulmine#1917). Since this project is unofficial, the actual pluralkit devs will not be able to help you with everything.</p>
</blockquote>
<hr/>
<h5>FAQ:</h5>
Will groups be added to this website?
<blockquote>In the future, yes! Groups are not in the API as of now, which is what this site depends on. When APIv2 comes out, they will eventually be added here too.</blockquote>
Will switch history/editing switches be added to this website?
<blockquote>Probably when APIv2 comes out, right now the API is very limited in how it can handle switches. And we'd rather add everything in one go.</blockquote>
What about bulk editing?
<blockquote>Probably not. Bulk commands are planned for the main bot, and we're not sure how to fit them into the main site at the moment.</blockquote>
Can you add [other feature]?
<blockquote>Depends on the feature. Ping us in the support server and we'll let you know if it's feasible or not. If it's accessibility related, chances are good that we'll add it.</blockquote>
Can i contribute to this?
<blockquote>Yeah sure! The code is open source on <a href='https://github.com/Spectralitree/pk-webs/'>github</a>. Be warned though that we're currently in the middle of cleaning it all up and adding comments to everything, so some sections are quite messy still.</blockquote>
</BS.Card.Body>
</BS.Card>
</>
);
};
export default Home;

View File

@ -1,23 +0,0 @@
import React from 'react';
import * as BS from 'react-bootstrap';
import { useParams } from "react-router-dom";
import MemberPage from '../Components/Private/MemberPage.js'
export default function MemberPages(props) {
const { memberID } = useParams();
const memberpages = props.members.filter((member) => member.id === memberID)
const memberpage = memberpages.map((member) => <MemberPage key={member.id} member={member} edit={props.edit}/>)
const noMatch = memberpages.length === 0;
if (noMatch) return (
<BS.Alert variant="danger">You do not have a member with the ID '{memberID}' in your system. Please check the ID again.</BS.Alert>
)
return (
<>
{memberpage}
</>
)
}

View File

@ -1,54 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import * as BS from 'react-bootstrap';
import Loading from '../Components/Loading';
import API_URL from '../Constants/constants';
import ProfilePage from '../Components/Public/ProfilePage';
const MemberProfile = () => {
const { memberID } = useParams();
const [isLoading, setIsLoading ] = useState(false);
const [isError, setIsError ] = useState(false);
const [isForbidden, setIsForbidden ] = useState(false);
const [ member, setMember ] = useState({});
const fetchMember = useCallback( () => {
setIsLoading(true);
setIsError(false);
fetch(`${API_URL}m/${memberID}`,{
method: 'GET',
}).then ( res => {
if (res.status === 403) {
throw new Error('Access denied!');
}
return res.json()
}
).then (data => {
setMember(data)
setIsLoading(false);
})
.catch (error => {
if (error.message === 'Access denied!') {
setIsForbidden(true);
} else {
console.log(error);
setIsError(true);
}
setIsLoading(false);
})
}, [memberID])
useEffect(() => {
fetchMember();
}, [fetchMember])
return (
isLoading ? <Loading /> : isError ?
<BS.Alert variant="danger">Error fetching member.</BS.Alert> : isForbidden ? <BS.Alert variant="danger">This member is private.</BS.Alert> : <ProfilePage member={member} list={false}/>
);
}
export default MemberProfile;

View File

@ -1,22 +0,0 @@
import React from 'react';
import * as BS from 'react-bootstrap';
import { useParams } from "react-router-dom";
import ProfilePage from '../Components/Public/ProfilePage.js'
export default function MemberPages(props) {
const { memberID } = useParams();
const memberpages = props.members.filter((member) => member.id === memberID)
const memberpage = memberpages.map((member) => <ProfilePage key={member.id} member={member} list={true}/>)
const noMatch = memberpages.length === 0;
if (noMatch) return (
<BS.Alert variant="danger">This system does not have a member with the ID '{memberID}', or the member's visibility is set to private.</BS.Alert>
)
return (
<>
{memberpage}
</>
)
}

View File

@ -1,74 +0,0 @@
import React from 'react';
import * as BS from 'react-bootstrap';
import { FaStar } from "react-icons/fa";
import { useForm } from "react-hook-form";
import history from "../History.js";
import { Switch, Route, useRouteMatch } from 'react-router-dom';
import Profile from '../Components/Public/Profile.js'
import MemberProfile from '../Pages/MemberProfile.js'
export default function Public () {
const { path, url } = useRouteMatch();
const { register: registerSys, handleSubmit: handleSys } = useForm();
const submitSysID = (data) => {
history.push(`${url}/${data.sysID}`);
}
const { register: registerMember, handleSubmit: handleMember } = useForm();
const submitMemberID = (data) => {
history.push(`${url}/m/${data.memberID}`);
}
return (
<Switch>
<Route exact path={path}>
<BS.Card className="mb-3">
<BS.Card.Header>
<BS.Card.Title><FaStar className="mr-3" />Profile</BS.Card.Title>
</BS.Card.Header>
<BS.Card.Body>
<BS.Form onSubmit={handleSys(submitSysID)}>
<BS.Form.Label>
Submit a <b>system ID</b> to view to that system's profile.
</BS.Form.Label>
<BS.Form.Row>
<BS.Col className="mb-1" xs={12} lg={10}>
<BS.Form.Control name="sysID" {...registerSys("sysID")} defaultValue="" placeholder="Enter system ID here..."/>
</BS.Col>
<BS.Col>
<BS.Button variant="primary" type="submit" block >Submit</BS.Button>
</BS.Col>
</BS.Form.Row>
</BS.Form>
</BS.Card.Body>
</BS.Card>
<BS.Card>
<BS.Card.Body>
<BS.Form onSubmit={handleMember(submitMemberID)}>
<BS.Form.Label>
Alternatively, submit a <b>member ID</b> to view that member.
</BS.Form.Label>
<BS.Form.Row>
<BS.Col className="mb-1" xs={12} lg={10}>
<BS.Form.Control name="memberID" {...registerMember("memberID")} defaultValue="" placeholder="Enter member ID here..." />
</BS.Col>
<BS.Col>
<BS.Button variant="primary" type="submit" block >Submit</BS.Button>
</BS.Col>
</BS.Form.Row>
</BS.Form>
</BS.Card.Body>
</BS.Card>
</Route>
<Route path={`${path}/m/:memberID`}>
<MemberProfile />
</Route>
<Route path={`${path}/:sysID`}>
<Profile />
</Route>
</Switch>
)
}

View File

@ -1,256 +0,0 @@
import React from "react";
import * as BS from "react-bootstrap";
import { FaCog } from "react-icons/fa";
import Toggle from "react-toggle";
const Settings = ({ forceUpdate }) => {
// this all should be pretty self-explanatory, might add comments later tho
return (
<BS.Card>
<BS.Card.Header className="d-flex align-items-center justify-content-between">
<BS.Card.Title>
<FaCog className="mr-3" />
Settings
</BS.Card.Title>
</BS.Card.Header>
<BS.Card.Body>
<p>
Change how you view and use pk-webs here, changes will be saved after
refreshing. You will have to apply them again in different browsers
and on different devices.
</p>
<hr />
<BS.Row>
<BS.Col
xs={12}
lg={4}
className="mx-1 mb-4 d-flex align-items-center row"
>
{localStorage.getItem("opendyslexic") ? (
<Toggle
className="mr-2"
defaultChecked={true}
icons={false}
onChange={() => {
localStorage.removeItem("opendyslexic");
forceUpdate();
}}
/>
) : (
<Toggle
className="mr-2"
defaultChecked={false}
icons={false}
onChange={() => {
localStorage.setItem("opendyslexic", "true");
forceUpdate();
}}
/>
)}
Use opendyslexic?
</BS.Col>
<BS.Col
xs={12}
lg={4}
className="mx-1 mb-4 d-flex align-items-center row"
>
{localStorage.getItem("twemoji") ? (
<Toggle
className="mr-2"
defaultChecked={true}
icons={false}
onChange={() => {
localStorage.removeItem("twemoji");
forceUpdate();
}}
/>
) : (
<Toggle
className="mr-2"
defaultChecked={false}
icons={false}
onChange={() => {
localStorage.setItem("twemoji", "true");
forceUpdate();
}}
/>
)}
Use twemoji?
</BS.Col>
<BS.Col
xs={12}
lg={4}
className="mx-1 mb-4 d-flex align-items-center row"
>
{localStorage.getItem("pagesonly") ? (
<Toggle
className="mr-2"
defaultChecked={true}
icons={false}
onChange={() => {
localStorage.removeItem("pagesonly");
forceUpdate();
}}
/>
) : (
<Toggle
className="mr-2"
defaultChecked={false}
icons={false}
onChange={() => {
localStorage.setItem("pagesonly", "true");
forceUpdate();
}}
/>
)}
Use only member pages?
</BS.Col>
<BS.Col
xs={12}
lg={4}
className="mx-1 mb-4 d-flex align-items-center row"
>
{localStorage.getItem("fullbg") ? (
<Toggle
className="mr-2"
defaultChecked={true}
icons={false}
onChange={() => {
localStorage.removeItem("fullbg");
forceUpdate();
}}
/>
) : (
<Toggle
className="mr-2"
defaultChecked={false}
icons={false}
onChange={() => {
localStorage.setItem("fullbg", "true");
forceUpdate();
}}
/>
)}
Remove gradient from background color?
</BS.Col>
<BS.Col
xs={12}
lg={4}
className="mx-1 mb-4 d-flex align-items-center row"
>
{localStorage.getItem("colorbg") ? (
<Toggle
className="mr-2"
defaultChecked={true}
icons={false}
onChange={() => {
localStorage.removeItem("colorbg");
forceUpdate();
}}
/>
) : (
<Toggle
className="mr-2"
defaultChecked={false}
icons={false}
onChange={() => {
localStorage.setItem("colorbg", "false");
forceUpdate();
}}
/>
)}
Hide colored backgrounds altogether?
</BS.Col>
<BS.Col
xs={12}
lg={4}
className="mx-1 mb-4 d-flex align-items-center row"
>
{localStorage.getItem("expandcards") ? (
<Toggle
className="mr-2"
defaultChecked={true}
icons={false}
onChange={() => {
localStorage.removeItem("expandcards");
forceUpdate();
}}
/>
) : (
<Toggle
className="mr-2"
defaultChecked={false}
icons={false}
onChange={() => {
localStorage.setItem("expandcards", "true");
forceUpdate();
}}
/>
)}
Expand member cards on default?
</BS.Col>
<BS.Col
xs={12}
lg={4}
className="mx-1 mb-4 d-flex align-items-center row"
>
{localStorage.getItem("hidebanners") ? (
<Toggle
className="mr-2"
defaultChecked={true}
icons={false}
onChange={() => {
localStorage.removeItem("hidebanners");
forceUpdate();
}}
/>
) : (
<Toggle
className="mr-2"
defaultChecked={false}
icons={false}
onChange={() => {
localStorage.setItem("hidebanners", "true");
forceUpdate();
}}
/>
)}
Hide banners at the top?
</BS.Col>
<BS.Col
xs={12}
lg={4}
className="mx-1 mb-4 d-flex align-items-center row"
>
{localStorage.getItem("bottombanners") ? (
<Toggle
className="mr-2"
defaultChecked={true}
icons={false}
onChange={() => {
localStorage.removeItem("bottombanners");
forceUpdate();
}}
/>
) : (
<Toggle
className="mr-2"
defaultChecked={false}
icons={false}
onChange={() => {
localStorage.setItem("bottombanners", "true");
forceUpdate();
}}
/>
)}
Show banners at the bottom?
</BS.Col>
</BS.Row>
</BS.Card.Body>
</BS.Card>
);
};
export default Settings;

View File

@ -1,68 +0,0 @@
import { useEffect, useState } from 'react';
import * as BS from 'react-bootstrap';
import { FaFileAlt } from 'react-icons/fa';
import { useForm } from 'react-hook-form';
import autosize from 'autosize';
const Template = () => {
const [isSubmit, setIsSubmit] = useState(false);
const { register, handleSubmit } = useForm();
var template1 = "";
var template2 = "";
var template3 = "";
if (localStorage.getItem('template1') !== null) template1 = localStorage.getItem('template1');
if (localStorage.getItem('template2') !== null) template2 = localStorage.getItem('template2');
if (localStorage.getItem('template3') !== null) template3 = localStorage.getItem('template3');
const onSubmit = data => {
localStorage.setItem('template1', data.template1);
localStorage.setItem('template2', data.template2);
localStorage.setItem('template3', data.template3);
setIsSubmit(true);
};
useEffect(() => {
autosize(document.querySelectorAll('textarea'));
});
return (
<>
<BS.Card className="mb-3">
<BS.Card.Header>
<BS.Card.Title>
<FaFileAlt className="mr-3" />
Templates
</BS.Card.Title>
</BS.Card.Header>
<BS.Card.Body>
<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>
<p><b>Note:</b> just like the settings, these templates are saved in your localstorage, which means you will have to set them again on every different device/browser you use. If you clear your local storage, the templates will also be cleared!</p>
</BS.Card.Body>
</BS.Card>
<BS.Card>
<BS.Card.Body>
<BS.Form onSubmit={handleSubmit(onSubmit)}>
<BS.Form.Group className="mt-3">
<BS.Form.Label><b>Template 1.</b></BS.Form.Label>
<BS.Form.Control maxLength="1000" as="textarea" name="template1" {...register("template1")} defaultValue={template1}/>
</BS.Form.Group>
<BS.Form.Group className="mt-3">
<BS.Form.Label><b>Template 2.</b></BS.Form.Label>
<BS.Form.Control maxLength="1000" as="textarea" name="template2" {...register("template2")} defaultValue={template2}/>
</BS.Form.Group>
<BS.Form.Group className="mt-3">
<BS.Form.Label><b>Template 3.</b></BS.Form.Label>
<BS.Form.Control maxLength="1000" as="textarea" name="template3" {...register("template3")} defaultValue={template3}/>
</BS.Form.Group>
<BS.Button variant="primary" className="float-left mr-2" type="submit">Submit</BS.Button> { isSubmit? <p style={{opacity: 0.7}}>Templates saved!</p> : ""}
</BS.Form>
</BS.Card.Body>
</BS.Card>
</>
);
}
export default Template;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -1,13 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Router } from "react-router-dom";
import history from "./History.js";
import './index.css';
import App from './App';
ReactDOM.render(
<Router history={history}>
<App />
</Router>,
document.getElementById('root')
);

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,13 +0,0 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

12905
yarn.lock

File diff suppressed because it is too large Load Diff