empty branch
This commit is contained in:
parent
bec88e14cd
commit
9eeda0706e
25
.gitignore
vendored
25
.gitignore
vendored
@ -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
21
LICENSE
@ -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.
|
@ -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.
|
38400
package-lock.json
generated
38400
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
75
package.json
75
package.json
@ -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"
|
||||
}
|
||||
}
|
@ -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>
|
@ -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.
|
@ -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 |
67
src/App.js
67
src/App.js
@ -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>
|
||||
);
|
||||
}
|
329
src/App.scss
329
src/App.scss
@ -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;
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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>
|
||||
|
||||
)
|
||||
}
|
@ -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></>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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} /> </> }
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
@ -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>
|
||||
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
const API_URL = "https://api.pluralkit.me/v1/";
|
||||
|
||||
export default API_URL
|
@ -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
|
||||
};
|
@ -1,2 +0,0 @@
|
||||
import { createBrowserHistory } from "history";
|
||||
export default createBrowserHistory();
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
@ -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;
|
@ -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}
|
||||
</>
|
||||
)
|
||||
}
|
@ -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;
|
@ -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}
|
||||
</>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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;
|
@ -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 |
@ -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;
|
||||
}
|
13
src/index.js
13
src/index.js
@ -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')
|
||||
);
|
@ -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 |
@ -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;
|
Loading…
x
Reference in New Issue
Block a user