Add member adding

This commit is contained in:
Spectralitree 2020-12-12 19:19:22 +01:00
parent e421222002
commit 71c85a26e2
13 changed files with 642 additions and 308 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,71 +1,12 @@
# Getting Started with Create React App
# pk-webs
A web interface for [pluralkit](https://pluralkit.me/).
Check it out at https://spectralitree.github.io/pk-webs/
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
### 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.
In the project directory, you can run:
### 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.
### `yarn start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `yarn test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `yarn build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `yarn build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
"# pk-web"
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.

5
package-lock.json generated
View File

@ -12407,6 +12407,11 @@
"prop-types": "^15.6.2"
}
},
"reactjs-popup": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/reactjs-popup/-/reactjs-popup-2.0.4.tgz",
"integrity": "sha512-G5jTXL2JkClKAYAdqedf+K9QvbNsFWvdbrXW1/vWiyanuCU/d7DtQzQux+uKOz2HeNVRsFQHvs7abs0Z7VLAhg=="
},
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",

View File

@ -23,6 +23,7 @@
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.1",
"react-toggle": "^4.1.1",
"reactjs-popup": "^2.0.4",
"use-dark-mode": "^2.3.1",
"web-vitals": "^0.2.4",
"yup": "^0.32.8"

12
public/README.md Normal file
View File

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

View File

@ -12,6 +12,7 @@ import Dash from './Components/Dash.js'
import history from "./History.js";
import Loading from "./Components/Loading.js";
import Navigation from "./Components/Navigation.js";
import Footer from './Components/Footer.js'
import API_URL from "./Constants/constants.js";
@ -62,6 +63,7 @@ export default function App() {
return (
<Router history={history} basename="/pk-webs">
<Navigation/>
<div class="filler">
<BS.Container>
<Switch>
<Route path="/pk-webs/dash" >
@ -98,6 +100,8 @@ export default function App() {
</Route>
</Switch>
</BS.Container>
</div>
<Footer />
</Router>
);
}

21
src/Components/Footer.js Normal file
View File

@ -0,0 +1,21 @@
import react from 'react'
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>
</BS.Navbar>
)
}

View File

@ -1,11 +0,0 @@
import React from 'react';
import * as BS from 'react-bootstrap';
export default function Home() {
return (
<BS.Jumbotron>
<h2>Hello, world!</h2>
</BS.Jumbotron>
);
}

View File

@ -1,18 +1,17 @@
import React, { useEffect, useState } from 'react';
import * as BS from 'react-bootstrap'
import { useForm, Controller } from "react-hook-form";
import autosize from 'autosize';
import { useForm } from "react-hook-form";
import moment from 'moment';
import Popup from 'reactjs-popup';
import 'reactjs-popup/dist/index.css';
import API_URL from "../Constants/constants.js";
import defaultAvatar from '../default_discord_avatar.png'
import { FaUser } from "react-icons/fa";
import { FaUser, FaTrashAlt } from "react-icons/fa";
export default function MemberCard(props) {
const { register, handleSubmit, control } = useForm();
const [member, setMember] = useState(props.member);
const [ displayName, setDisplayName ] = useState("");
@ -23,13 +22,40 @@ export default function MemberCard(props) {
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 {
register: registerEdit,
handleSubmit: handleSubmitEdit
} = useForm();
const {
register: registerPrivacy,
handleSubmit: handleSubmitPrivacy
} = useForm();
const {
register: registerDelete,
handleSubmit: handleSubmitDelete
} = useForm();
const {
register: registerProxy,
handleSubmit: handleSubmitProxy,
} = useForm();
useEffect(() => {
const { toHTML } = require('../Functions/discord-parser.js');
@ -70,12 +96,7 @@ export default function MemberCard(props) {
} else { setDesc("(no description)");
setEditDesc("");
}
}, [member.description, member.color, member.birthday, member.display_name, member.pronouns, member.avatar_url]);
useEffect(() => {
autosize(document.querySelector('textarea'));
})
}, [member.description, member.color, member.birthday, member.display_name, member.pronouns, member.avatar_url, member.proxy_tags]);
const submitEdit = data => {
fetch(`${API_URL}m/${member.id}`,{
@ -85,7 +106,11 @@ export default function MemberCard(props) {
'Content-Type': 'application/json',
'Authorization': JSON.stringify(localStorage.getItem("token")).slice(1, -1)
}}).then (res => res.json()
).then (data => { setMember(prevState => {return {...prevState, ...data}}); setEditMode(false)}
).then (data => {
setMember(prevState => {return {...prevState, ...data}});
setErrorAlert(false);
setEditMode(false);
}
).catch (error => {
console.error(error);
setErrorAlert(true);
@ -100,15 +125,70 @@ export default function MemberCard(props) {
'Content-Type': 'application/json',
'Authorization': JSON.stringify(localStorage.getItem("token")).slice(1, -1)
}}).then (res => res.json()
).then (data => { setMember(prevState => {return {...prevState, ...data}}); setPrivacyMode(false)}
).then (data => {
setMember(prevState => {return {...prevState, ...data}});
setErrorAlert(false);
setPrivacyMode(false)
}
).catch (error => {
console.error(error);
setErrorAlert(true);
})
}
const deleteMember = data => {
if (data.memberID !== member.id) {
setWrongID(true);
} else {
fetch(`${API_URL}m/${member.id}`,{
method: 'DELETE',
headers: {
'Authorization': JSON.stringify(localStorage.getItem("token")).slice(1, -1)
}}).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 === ""))}
fetch(`${API_URL}m/${member.id}`,{
method: 'PATCH',
body: JSON.stringify(newdata),
headers: {
'Content-Type': 'application/json',
'Authorization': JSON.stringify(localStorage.getItem("token")).slice(1, -1)
}}).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);
});
}
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> : <>
<BS.Card.Header className="d-flex align-items-center justify-content-between">
<BS.Accordion.Toggle as={BS.Button} variant="link" eventKey={member.id} className="float-left"><FaUser className="mr-4" /> <b>{member.name}</b> ({member.id})</BS.Accordion.Toggle>
{ member.avatar_url ? <BS.Image src={`${member.avatar_url}`} style={{width: 50, height: 50}} className="float-right" roundedCircle /> :
@ -118,94 +198,114 @@ export default function MemberCard(props) {
<BS.Card.Body style={{borderLeft: `5px solid #${color}` }}>
{ errorAlert ? <BS.Alert variant="danger">Something went wrong, please try logging in and out again.</BS.Alert> : "" }
{ editMode ?
<BS.Form onSubmit={handleSubmit(submitEdit)}>
<>
<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>
<Controller as={<BS.Form.Control />} name="name" control={control} defaultValue={member.name} />
<BS.Form.Control name="name" ref={registerEdit} defaultValue={member.name} />
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>AKA: </BS.Form.Label>
<Controller as={<BS.Form.Control />} name="display_name" control={control} defaultValue={displayName} />
<BS.Form.Label>Display name: </BS.Form.Label>
<BS.Form.Control name="display_name" ref={registerEdit} defaultValue={displayName} />
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Birthday:</BS.Form.Label>
<Controller as={<BS.Form.Control pattern="^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$"/>} name="birthday" control={control} defaultValue={birthdate}/>
<BS.Form.Control pattern="^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$" name="birthday" ref={registerEdit} 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>
<Controller as={<BS.Form.Control/>} name="pronouns" control={control} defaultValue={pronouns} />
<BS.Form.Control name="pronouns" ref={registerEdit} defaultValue={pronouns} />
</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"/>} name="avatar_url" control={control} defaultValue={avatar} />
<BS.Form.Control type="url" name="avatar_url" ref={registerEdit} defaultValue={avatar} />
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Color:</BS.Form.Label>
<Controller as={<BS.Form.Control pattern="[A-Fa-f0-9]{6}"/>} name="color" control={control} defaultValue={color} />
<BS.Form.Control pattern="[A-Fa-f0-9]{6}" name="color" ref={registerEdit} 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>
<Controller as={<BS.Form.Control maxLength="1000" as="textarea" />} name="description" control={control} defaultValue={editDesc}/>
<BS.Form.Control maxLength="1000" as="textarea" name="description" ref={registerEdit} 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.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" ref={registerDelete({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>AKA: </b>{displayName}</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> {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> }
{ 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> }
</BS.Row>
{ privacyMode ? <BS.Form onSubmit={handleSubmit(submitPrivacy)}>
{ 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" as="select" ref={register}>
<BS.Form.Control name="visibility" as="select" ref={registerPrivacy}>
<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}>
<BS.Form.Control name="name_privacy" as="select" ref={registerPrivacy}>
<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}>
<BS.Form.Control name="description_privacy" as="select" ref={registerPrivacy}>
<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}>
<BS.Form.Control name="birthday_privacy" as="select" ref={registerPrivacy}>
<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}>
<BS.Form.Control name="pronoun_privacy" as="select" ref={registerPrivacy}>
<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" ref={register}>
<BS.Form.Control name="metadata_privacy" as="select" ref={registerPrivacy}>
<option>public</option>
<option>private</option>
</BS.Form.Control>
@ -223,11 +323,37 @@ export default function MemberCard(props) {
<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>
<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={item.id} className="mb-lg-2" xs={12} lg={2}>
<BS.Form.Row>
<BS.InputGroup className="ml-1 mr-1 mb-1">
<BS.Form.Control name={`proxy_tags[${index}].prefix`} defaultValue={item.prefix} ref={registerProxy}/>
<BS.Form.Control disabled placeholder='text'/>
<BS.Form.Control name={`proxy_tags[${index}].suffix`} defaultValue={item.suffix} ref={registerProxy}/>
</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>
<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) => <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> <BS.Button variant="primary" onClick={() => setProxyMode(true)}>Edit</BS.Button>
<hr/></> : "" }
<p><b>Description:</b></p>
<p dangerouslySetInnerHTML={{__html: desc}}></p>
{ privacyMode ? "" : privacyView ? "" : <BS.Button variant="light" onClick={() => setEditMode(true)}>Edit</BS.Button>}
{ proxyView ? "" : privacyMode ? "" : privacyView ? "" : <BS.Button variant="light" onClick={() => setEditMode(true)}>Edit</BS.Button>}
</> } </BS.Card.Body>
</BS.Accordion.Collapse>
</>

View File

@ -1,10 +1,16 @@
import React, { useEffect, useState } from 'react';
import * as BS from 'react-bootstrap'
import Popup from 'reactjs-popup';
import 'reactjs-popup/dist/index.css';
import { useForm } from "react-hook-form";
import autosize from 'autosize';
import MemberCard from './MemberCard.js'
import Loading from "./Loading.js";
import API_URL from "../Constants/constants.js";
import { FaPlus } from "react-icons/fa";
export default function Memberlist(props) {
const user = JSON.parse(localStorage.getItem('user'));
@ -12,30 +18,79 @@ export default function Memberlist(props) {
const [isLoading, setIsLoading ] = useState(false);
const [isError, setIsError ] = useState(false);
const [members, setMembers ] = useState([]);
const [value, setValue] = useState('')
const [errorAlert, setErrorAlert ] = useState(false);
const [proxyView, setProxyView] = useState(false);
const [privacyView, setPrivacyView] = useState(false);
const [open, setOpen] = useState(false);
const closeModal = () => setOpen(false);
const [members, setMembers ] = useState([]);
const [newMember, setNewMember ] = useState('');
const [value, setValue] = useState('');
const [proxyTags, setProxyTags] = useState([{
prefix: "", suffix: ""
}]);
const {register, handleSubmit} = useForm();
useEffect(() => {
setIsLoading(true);
setIsError(false);
fetchMembers();
}, [userId, newMember])
fetch(`${API_URL}s/${userId}/members`,{
method: 'get',
useEffect(() => {
autosize(document.querySelector('textarea'));
})
function fetchMembers() {
setIsLoading(true);
setIsError(false);
fetch(`${API_URL}s/${userId}/members`,{
method: 'get',
headers: {
'Authorization': JSON.stringify(localStorage.getItem("token")).slice(1, -1)
}}).then ( res => res.json()
).then (data => {
{ newMember ? setMembers([...data, newMember]) : setMembers(data)}
setIsLoading(false);
})
.catch (error => {
console.log(error);
setIsError(true);
setIsLoading(false);
})
}
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
console.log(newdata);
fetch(`${API_URL}m/`,{
method: 'POST',
body: JSON.stringify(newdata),
headers: {
'Content-Type': 'application/json',
'Authorization': JSON.stringify(localStorage.getItem("token")).slice(1, -1)
}}).then ( res => res.json()
}}).then (res => res.json()
).then (data => {
setMembers(data);
setIsLoading(false);
})
.catch (error => {
console.log(error);
setIsError(true);
setIsLoading(false);
})
}, [userId])
setNewMember(data);
setErrorAlert(false);
closeModal();
}
).catch (error => {
console.error(error);
setErrorAlert(true);
});
}
const memberList = members.filter(member => {
if (!value) return true;
@ -55,13 +110,141 @@ export default function Memberlist(props) {
{ isLoading ? <Loading /> : isError ?
<BS.Alert variant="danger">Error fetching members.</BS.Alert> :
<>
<BS.Row className="justify-content-md-center">
<BS.Row noGutters="true" className="justify-content-md-center">
<BS.Col xs={12} lg={4}>
<BS.Form inline>
<BS.Form.Control className="w-100" value={value} onChange={e => setValue(e.target.value)} placeholder="Search"/>
<BS.Form.Control value={value} className="w-100" onChange={e => setValue(e.target.value)} placeholder="Search"/>
</BS.Form>
</BS.Col>
<BS.Col xs={12} lg={2}>
<BS.Button type="primary" className="ml-2" block onClick={() => fetchMembers()}>Refresh</BS.Button>
</BS.Col>
</BS.Row>
<BS.Card className="mt-3 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" ref={register()} 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" ref={register} 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" ref={register} 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 name="pronouns" ref={register} 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" ref={register} 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" ref={register} 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={2}>
<BS.Form.Row>
<BS.InputGroup className="ml-1 mr-1 mb-1">
<BS.Form.Control name={`proxy_tags[${index}].prefix`} defaultValue={item.prefix} ref={register}/>
<BS.Form.Control disabled placeholder='text'/>
<BS.Form.Control name={`proxy_tags[${index}].suffix`} defaultValue={item.suffix} ref={register}/>
</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" 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-3" 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>
<hr/></> : "" }
<BS.Form.Group className="mt-3">
<BS.Form.Label>Description:</BS.Form.Label>
<BS.Form.Control maxLength="1000" as="textarea" rows="7" name="description" ref={register} 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>

View File

@ -17,7 +17,7 @@ export default function Navigation() {
}
return (
<BS.Navbar className="mb-3 align-items-center justify-content-between">
<BS.Navbar className="mb-5 align-items-center justify-content-between">
<BS.Navbar.Brand>
pk-web
</BS.Navbar.Brand>

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import * as BS from 'react-bootstrap'
import { useForm, Controller } from "react-hook-form";
import { useForm } from "react-hook-form";
import autosize from 'autosize';
import moment from 'moment';
import 'moment-timezone';
@ -12,7 +12,16 @@ import { FaAddressCard } from "react-icons/fa";
export default function System(props) {
const { register, handleSubmit,control } = useForm();
const {
register: registerEdit,
handleSubmit: handleSubmitEdit
} = useForm();
const {
register: registerPrivacy,
handleSubmit: handleSubmitPrivacy
} = useForm();
const [ user, setUser ] = useState(JSON.parse(localStorage.getItem('user')));
@ -104,30 +113,30 @@ const submitPrivacy = data => {
<BS.Card.Body>
{ errorAlert ? <BS.Alert variant="danger">Something went wrong, please try logging in and out again.</BS.Alert> : "" }
{ editMode ?
<BS.Form onSubmit={handleSubmit(submitEdit)}>
<BS.Form onSubmit={handleSubmitEdit(submitEdit)}>
{/* <BS.Form.Text className='mb-4'>Note: changes here may take a while to be reflected on the bot. This is due to the bot caching data.<br/> Try editing a member after to make the changes show up.</BS.Form.Text> */}
<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/>} name="name" control={control} defaultValue={user.name}/>
<BS.Form.Control name="name" ref={registerEdit} defaultValue={user.name}/>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Tag:</BS.Form.Label>
<Controller as={<BS.Form.Control/>} name="y" control={control} defaultValue={tag}/>
<BS.Form.Control name="tag" ref={registerEdit} defaultValue={tag}/>
</BS.Col>
<BS.Col className="mb-lg-2" xs={12} lg={3}>
<BS.Form.Label>Timezone:</BS.Form.Label>
<Controller as={<BS.Form.Control/>} name="tz" control={control} defaultValue={timezone}/>
<BS.Form.Control name="tz" ref={registerEdit} defaultValue={timezone}/>
{ 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>
<Controller as={<BS.Form.Control/>} name="avatar_url" control={control} defaultValue={avatar}/>
<BS.Form.Control name="avatar_url" ref={registerEdit} defaultValue={avatar}/>
</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.Control maxLength="1000" as="textarea" name="description" ref={registerEdit} 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> :
@ -137,34 +146,34 @@ const submitPrivacy = data => {
<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> }
</BS.Row>
{ privacyMode ? <BS.Form onSubmit={handleSubmit(submitPrivacy)}>
{ privacyMode ? <BS.Form 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>Description:</BS.Form.Label>
<BS.Form.Control name="description_privacy" as="select" ref={register}>
<BS.Form.Control name="description_privacy" as="select" ref={registerPrivacy}>
<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" as="select" ref={register}>
<BS.Form.Control name="member_list_privacy" as="select" ref={registerPrivacy}>
<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" ref={register}>
<BS.Form.Control name="front_privacy" as="select" ref={registerPrivacy}>
<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" as="select" ref={register}>
<BS.Form.Control name="front_history_privacy" as="select" ref={registerPrivacy}>
<option>public</option>
<option>private</option>
</BS.Form.Control>

View File

@ -1,191 +1,234 @@
html {
position: relative;
min-height: 100vh;
}
body {
padding-bottom: 60px;
}
.filler {
flex: 1 1 auto;
}
.dark-mode {
background-color: $gray-800;
transition: background-color 0.3s ease;
}
background-color: $gray-800;
transition: background-color 0.3s ease;
}
.dark-mode .card {
color: $white;
background-color: $gray-800;
border: 1px solid #191c1f !important;
}
.dark-mode .card {
color: $white;
background-color: $gray-800;
border: 1px solid #191c1f !important;
}
.dark-mode .card-body {
color: $white;
background-color: $gray-800;
transition: background-color 0.3s ease;
}
.dark-mode .card-body {
color: $white;
background-color: $gray-800;
transition: background-color 0.3s ease;
}
.dark-mode .card-header {
color: $gray-100;
background-color: $gray-900;
transition: background-color 0.3s ease;
}
.dark-mode .card-header {
color: $gray-100;
background-color: $gray-900;
transition: background-color 0.3s ease;
}
.navbar {
background-color: $gray-100;
}
.navbar {
background-color: $gray-100;
}
.dark-mode .navbar {
color: $gray-100;
background-color: $gray-900;
transition: background-color 0.3s ease;
}
.dark-mode .navbar {
color: $gray-100;
background-color: $gray-900;
transition: background-color 0.3s ease;
}
.dark-mode .navbar-brand {
color: $gray-100 !important;
}
.dark-mode .navbar-brand {
color: $gray-100 !important;
}
.dark-mode .form-control {
color: $gray-200;
border-color: #191c1f;
background-color: $gray-900;
transition: background-color 0.3s ease;
}
.dark-mode .form-label {
color: $white !important;
}
.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 {
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 .form-control:disabled {
color: $gray-600;
border-color: #191c1f;
background-color: $gray-900;
transition: background-color 0.3s ease;
}
.dark-mode .btn-light {
background-color: $gray-900;
border-color: #191c1f;
color: $gray-100;
}
.dark-mode .form-control:focus {
color: $gray-200;
border-color: #191c1f;
background-color: $gray-900;
}
.dark-mode .btn-light:focus {
background-color: $gray-900;
border-color: #191c1f;
color: $gray-100;
}
.dark-mode .btn-light {
background-color: $gray-900;
border-color: #191c1f;
color: $gray-100;
}
.card-header .btn-link {
color: $gray-900;
}
.dark-mode .btn-light:focus {
background-color: $gray-900;
border-color: #191c1f;
color: $gray-100;
}
.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 {
color: $gray-900;
}
.card-header .btn-link {
font-size: 1.25rem !important;
font-weight: 500;
}
.card-header .btn-link:focus {
color: $gray-900;
text-decoration: none;
}
.dark-mode .btn-link {
color: $white;
}
.card-header .btn-link:hover {
color: $gray-900;
text-decoration: none;
}
.dark-mode .btn-link:focus {
color: $white;
text-decoration: none;
}
.card-header .btn-link {
font-size: 1.25rem !important;
font-weight: 500;
}
.dark-mode .btn-link:hover {
color: $white;
text-decoration: none;
}
.dark-mode .btn-link {
color: $white;
}
.dark-mode .alert-danger {
color: $white;
background-color: $danger;
border: $gray-900;
transition: background-color 0.3s ease;
}
.dark-mode .btn-link:focus {
color: $white;
text-decoration: none;
}
code {
color: black !important;
background-color: $gray-300;
padding: 5px 7px;
border-radius: 3px;
margin: 3px;
display: inline-block;
}
.dark-mode .btn-link:hover {
color: $white;
text-decoration: none;
}
.dark-mode code{
color: white !important;
background-color: $gray-900;
transition: background-color 0.3s ease;
}
.dark-mode .alert-danger {
color: $white;
background-color: $danger;
border: $gray-900;
transition: background-color 0.3s ease;
}
pre {
margin: 2px !important;
}
code.hljs {
display: block;
border: 1px solid rgba(0, 0, 0, 0.125);
margin: 3px 3px 7px 3px;
}
.dark-mode .alert-light {
color: $white;
background-color: $gray-900;
border: #191c1f;
transition: background-color 0.3s ease;
}
.d-spoiler {
color: $gray-300 !important;
background-color: $gray-300;
padding: 3px 5px;
border-radius: 3px;
margin: 3px;
display: inline-block;
transition-delay: 6000s;
}
code {
color: black !important;
background-color: $gray-300;
padding: 5px 7px;
border-radius: 3px;
margin: 3px;
display: inline-block;
}
.d-spoiler::selection {
color: $gray-300;
}
.dark-mode code {
color: white !important;
background-color: $gray-900;
transition: background-color 0.3s ease;
}
.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;
}
pre {
margin: 2px !important;
}
.dark-mode .d-spoiler::selection {
color: $gray-900;
}
code.hljs {
display: block;
border: 1px solid rgba(0, 0, 0, 0.125);
margin: 3px 3px 7px 3px;
}
.d-spoiler:active {
color: $gray-900 !important;
background-color: $gray-200;
transition-delay: 0s;
transition: background-color 0.2s ease;
}
.d-spoiler {
color: $gray-300 !important;
background-color: $gray-300;
padding: 3px 5px;
border-radius: 3px;
margin: 3px;
display: inline-block;
transition-delay: 6000s;
}
.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;
}
.d-spoiler::selection {
color: $gray-300;
}
.dark-mode blockquote {
border-left: 6px solid $gray-700;
}
.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;
}
@import "~bootstrap/scss/bootstrap";
.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 {
position: absolute !important;
width: 100% !important;
bottom: 0px !important;
}
@import "~bootstrap/scss/bootstrap";