add member editing

This commit is contained in:
Spectralitree 2020-12-11 00:53:43 +01:00
parent f526a6c40f
commit d30136fba2
9 changed files with 290 additions and 48 deletions

File diff suppressed because one or more lines are too long

54
package-lock.json generated
View File

@ -1299,6 +1299,11 @@
"@hapi/hoek": "^8.3.0"
}
},
"@hookform/resolvers": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-1.1.2.tgz",
"integrity": "sha512-I3kYtdj1QX9VkNq4GtC2ZNfoBV13GpoxZCsoA9F3CyAmkQmiznJt6HG8Oc9/R3PLqPrw98FiIzyVs/rG0FnGAA=="
},
"@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@ -2068,6 +2073,11 @@
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4="
},
"@types/lodash": {
"version": "4.14.165",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.165.tgz",
"integrity": "sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg=="
},
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@ -2864,6 +2874,11 @@
"postcss-value-parser": "^4.1.0"
}
},
"autosize": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/autosize/-/autosize-4.0.2.tgz",
"integrity": "sha512-jnSyH2d+qdfPGpWlcuhGiHmqBJ6g3X+8T+iRwFrHPLVcdoGJE/x6Qicm6aDHfTsbgZKxyV8UU/YB2p4cjKDRRA=="
},
"aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@ -9699,6 +9714,11 @@
"minimist": "^1.2.5"
}
},
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@ -9746,6 +9766,11 @@
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
},
"nanoclone": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz",
"integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA=="
},
"nanoid": {
"version": "3.1.20",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz",
@ -11873,6 +11898,11 @@
"warning": "^4.0.0"
}
},
"property-expr": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz",
"integrity": "sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg=="
},
"proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
@ -12213,6 +12243,11 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"react-moment": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/react-moment/-/react-moment-1.0.0.tgz",
"integrity": "sha512-J4iIiwUT4oZcL7cp2U7naQKbQtqvmzGXXBMg/DLj+Pi7n9EW0VhBRx/1aJ1Tp2poCqTCAPoadLEoUIkReGnNNg=="
},
"react-overlays": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-4.1.1.tgz",
@ -14698,6 +14733,11 @@
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"toposort": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
"integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA="
},
"tough-cookie": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz",
@ -16688,6 +16728,20 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
},
"yup": {
"version": "0.32.8",
"resolved": "https://registry.npmjs.org/yup/-/yup-0.32.8.tgz",
"integrity": "sha512-SZulv5FIZ9d5H99EN5tRCRPXL0eyoYxWIP1AacCrjC9d4DfP13J1dROdKGfpfRHT3eQB6/ikBl5jG21smAfCkA==",
"requires": {
"@babel/runtime": "^7.10.5",
"@types/lodash": "^4.14.165",
"lodash": "^4.17.20",
"lodash-es": "^4.17.11",
"nanoclone": "^0.2.1",
"property-expr": "^2.0.4",
"toposort": "^2.0.2"
}
}
}
}

View File

@ -4,22 +4,27 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@hookform/resolvers": "^1.1.2",
"@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",
"node-fetch": "^2.6.1",
"node-sass": "^4.14.1",
"react": "^17.0.1",
"react-bootstrap": "^1.4.0",
"react-dom": "^17.0.1",
"react-icons": "^4.1.0",
"react-moment": "^1.0.0",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.1",
"react-toggle": "^4.1.1",
"use-dark-mode": "^2.3.1",
"web-vitals": "^0.2.4"
"web-vitals": "^0.2.4",
"yup": "^0.32.8"
},
"scripts": {
"predeploy": "npm run build",

View File

@ -2,35 +2,26 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="public/favicon.ico" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="A web interface for pluralkit."
name="A web interface for pluralkit."
content="(This is still a work in progress)"
/>
<!--
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/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>pk-webs</title>
<link
rel="stylesheet"
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"
crossorigin="anonymous"
/>
crossorigin="anonymous" />
<script type="text/javascript">
// Single Page Apps for GitHub Pages
// MIT License
@ -59,16 +50,6 @@
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -39,7 +39,7 @@ export default function App() {
setIsLoading(true);
fetch(`${API_URL}s/`,{
method: 'get',
method: 'GET',
headers: {
'Authorization': JSON.stringify(localStorage.getItem("token")).slice(1, -1)
}}).then ( res => res.json()

View File

@ -1,29 +1,112 @@
import React, { useEffect, useState } from 'react';
import * as BS from 'react-bootstrap'
import { useForm, Controller } from "react-hook-form";
import autosize from 'autosize';
import moment from 'moment';
import Loading from "./Loading.js";
import API_URL from "../Constants/constants.js";
import defaultAvatar from '../default_discord_avatar.png'
import { FaUser } from "react-icons/fa";
export default function MemberCard(props) {
const { toHTML } = require('../Functions/discord-parser.js');
const { register, handleSubmit, errors, control } = useForm();
const [ desc, setDesc ] = useState("");
const [member, setMember] = useState(props.member);
const [ displayName, setDisplayName ] = useState("");
const [ birthday, setBirthday ] = useState("");
const [ birthdate, setBirthdate ] = useState("");
const [ pronouns, setPronouns ] = useState("");
const [ avatar, setAvatar ] = useState("");
const [ color, setColor ] = useState("");
const [ desc, setDesc ] = useState("");
const [ editDesc, setEditDesc ] = useState("");
const member = props.member;
const [ editMode, setEditMode ] = useState(false);
const [ privacyMode, setPrivacyMode ] = useState(false);
const [ privacyView, setPrivacyView ] = useState(false);
useEffect(() => {
if (member.description) {
setDesc(toHTML(member.description));
} else setDesc("(no description)");
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 bday = member.birthday.replace('0004-','');
var bdaymoment = moment(bday, 'MM DD').format('MMM D');
setBirthday(bdaymoment);
} else {
var birthdaymoment = moment(bday, 'YYYY MM DD').format('MMM D, YYYY');
setBirthday(birthdaymoment);
}
} else { setBirthday('');
setBirthdate('');
}
if (member.pronouns) {
setPronouns(member.pronouns)
} else setPronouns('')
if (member.avatar_url) {
setAvatar(member.avatar_url)
} else setAvatar('')
if (member.color) {
setColor(member.color);
}
else setColor('transparent');
} else { setColor('');
}
}, [member.description, member.color]);
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]);
useEffect(() => {
autosize(document.querySelector('textarea'));
})
const submitEdit = data => {
fetch(`${API_URL}m/${member.id}`,{
method: 'PATCH',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
'Authorization': JSON.stringify(localStorage.getItem("token")).slice(1, -1)
}}).then (res => res.json()
).then (data => setMember(data), setEditMode(false)
).catch (error => {
console.error(error);
})
}
const submitPrivacy = data => {
fetch(`${API_URL}m/${member.id}`,{
method: 'PATCH',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
'Authorization': JSON.stringify(localStorage.getItem("token")).slice(1, -1)
}}).then (res => res.json()
).then (data => setMember(data),
setPrivacyMode(false)
).catch (error => {
console.error(error);
})
}
return (
<>
@ -34,16 +117,118 @@ export default function MemberCard(props) {
</BS.Accordion.Toggle>
<BS.Accordion.Collapse eventKey={member.id}>
<BS.Card.Body style={{borderLeft: `5px solid #${color}` }}>
{ editMode ?
<BS.Form onSubmit={handleSubmit(submitEdit)}>
<BS.Form.Row>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Name:</BS.Form.Label>
<Controller as={<BS.Form.Control placeholder={member.name}/>} name="name" control={control}/>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>AKA: </BS.Form.Label>
<Controller as={<BS.Form.Control placeholder={displayName}/>} name="display_name" control={control}/>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Birthday:</BS.Form.Label>
<Controller as={<BS.Form.Control placeholder={birthdate} pattern="/^\d{4}(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$/g"/>} name="birthday" control={control}/>
<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>
<Controller as={<BS.Form.Control placeholder={pronouns}/>} name="pronouns" control={control}/>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Avatar url:</BS.Form.Label>
<Controller as={<BS.Form.Control type="url" placeholder={avatar}/>} name="avatar_url" control={control} />
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Color:</BS.Form.Label>
<Controller as={<BS.Form.Control placeholder={color} pattern="/[A-Fa-f0-9]{6}$/g"/>} name="color" control={control}/>
<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>
<Controller as={<BS.Form.Control maxLength="1000" as="textarea" />} name="description" control={control} 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>
:
<>
<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>AKA: </b>{member.display_name}</BS.Col> : "" }
{ member.birthday ? <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Birthday:</b> {member.birthday}</BS.Col> : "" }
{ member.pronouns ? <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Pronouns:</b> {member.pronouns}</BS.Col> : "" }
{ member.color ? <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Color:</b> #{member.color}</BS.Col> : "" }
{ member.display_name ? <BS.Col className="mb-lg-3" xs={12} lg={3}><b>AKA: </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> {pronouns}</BS.Col> : "" }
{ member.color ? <BS.Col className="mb-lg-3" xs={12} lg={3}><b>Color:</b> {color}</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> }
</BS.Row>
{ privacyMode ? <BS.Form onSubmit={handleSubmit(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" as="select" ref={register}>
<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" ref={register}>
<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" ref={register}>
<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" ref={register}>
<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" ref={register}>
<option>public</option>
<option>private</option>
</BS.Form.Control>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Meta:</BS.Form.Label>
<BS.Form.Control name="metadata_privacy" as="select" ref={register}>
<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>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-lg-3" xs={12} lg={3}><b>Meta:</b> {member.metadata_privacy}</BS.Col>
</BS.Row>
<BS.Button variant="light" onClick={() => setPrivacyView(false)}>Cancel</BS.Button> <BS.Button variant="primary" onClick={() => setPrivacyMode(true)}>Edit</BS.Button>
<hr/></> : "" }
<p><b>Description:</b></p>
<p dangerouslySetInnerHTML={{__html: desc}}></p>
</BS.Card.Body>
{ privacyMode ? "" : privacyView ? "" : <BS.Button variant="light" onClick={() => setEditMode(true)}>Edit</BS.Button>}
</> } </BS.Card.Body>
</BS.Accordion.Collapse>
</>
)

View File

@ -1,4 +1,3 @@
import react from 'react';
import * as BS from 'react-bootstrap'
import useDarkMode from 'use-dark-mode';
import Toggle from 'react-toggle'

View File

@ -6,12 +6,12 @@ import defaultAvatar from '../default_discord_avatar.png'
export default function System(props) {
const { toHTML } = require('../Functions/discord-parser.js');
const [ desc, setDesc ] = useState("");
const user = JSON.parse(localStorage.getItem("user"));
useEffect(() => {
const { toHTML } = require('../Functions/discord-parser.js');
if (user.description) {
setDesc(toHTML(user.description));
} else setDesc("(no description)");

View File

@ -23,11 +23,11 @@
transition: background-color 0.3s ease;
}
.navbar-light {
.navbar {
background-color: $gray-100;
}
.dark-mode .navbar-light {
.dark-mode .navbar {
color: $gray-100;
background-color: $gray-900;
transition: background-color 0.3s ease;
@ -44,12 +44,30 @@
transition: background-color 0.3s ease;
}
.dark-mode .form-control:disabled {
color: $gray-200;
border-color: #191c1f;
background-color: $gray-900;
transition: background-color 0.3s ease;
}
.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;
}
.dark-mode .alert-danger {
color: $white;