Compare commits

..

25 Commits

Author SHA1 Message Date
adc4f163b7 Resolved lint issues 2022-05-08 09:20:36 +00:00
4b0014ba83 Fix infinite repaint loop in DraftPreview 2022-05-08 09:17:41 +00:00
e1e49b06da Removed all class components 2022-05-08 09:03:21 +00:00
9fcd193a87 Functional component for usersearch 2022-05-08 08:49:51 +00:00
fb5559158f Fixed bug on project settings page 2022-05-08 08:29:50 +00:00
e4bdadba6e Addressed issue with wideBody determination on project page 2022-05-08 08:18:27 +00:00
080d604a17 Additional functional requirements and fixed lint issues 2022-05-07 15:55:37 +00:00
a31dca4006 Resolved lint errors 2022-05-07 15:47:59 +00:00
2a3330e38d More functional components 2022-05-07 15:44:38 +00:00
80ce9beef5 Warp as functional component 2022-05-07 15:33:25 +00:00
4161397d56 Made DraftPreview into a functional component 2022-05-07 15:17:59 +00:00
ce81d99cdf Changed Tieups into functional component 2022-05-07 14:40:34 +00:00
ce32c85b64 Moved Weft to use functional component 2022-05-07 09:21:57 +00:00
8f8e799323 Resolved all lint issues 2022-05-06 22:53:40 +00:00
c9a5593fe0 Moved all routing to App.js 2022-05-06 22:49:00 +00:00
0575d795ea Moved group nav to App.js 2022-05-06 22:39:23 +00:00
5220c1a484 Moving all routing into App.js 2022-05-06 22:28:37 +00:00
99e4a01ef2 Modernised additional components 2022-05-06 21:43:52 +00:00
8b482d6761 Removed un-needed files. Modernised additional components 2022-05-06 21:33:59 +00:00
9fcb902ee2 More components updated 2022-05-06 20:28:22 +00:00
cb67d7afe0 Additional updates 2022-05-06 20:03:55 +00:00
e100a8520c Updated react-redux 2022-05-06 19:09:25 +00:00
c2fbd272fd Fixed lint issues 2022-05-06 18:48:54 +00:00
5970422d58 Removed all usage of withRouter 2022-05-06 18:22:53 +00:00
1124e0af52 Initial changes 2022-05-06 18:13:43 +00:00
49 changed files with 2680 additions and 2840 deletions

View File

@ -1,4 +1,5 @@
REACT_APP_API_URL="http://localhost:2001" REACT_APP_API_URL="http://localhost:2001"
REACT_APP_IMAGINARY_URL="http://localhost:9000" REACT_APP_IMAGINARY_URL="http://localhost:9000"
REACT_APP_SENTRY_DSN=""
REACT_APP_SOURCE_REPO_URL="https://git.wilw.dev/wilw/treadl" REACT_APP_SOURCE_REPO_URL="https://git.wilw.dev/wilw/treadl"
REACT_APP_SUPPORT_ROOT="https://git.wilw.dev/wilw/treadl/wiki/" REACT_APP_SUPPORT_ROOT="https://git.wilw.dev/wilw/treadl/wiki/"

View File

@ -18,8 +18,9 @@
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-helmet": "^6.0.0", "react-helmet": "^6.0.0",
"react-joyride": "^2.4.0", "react-joyride": "^2.4.0",
"react-redux": "^7.2.0", "react-markdown": "^8.0.3",
"react-router-dom": "^5.1.2", "react-redux": "^8.0.1",
"react-router-dom": "^6.3.0",
"react-scripts": "3.4.1", "react-scripts": "3.4.1",
"react-toastify": "^4.4.0", "react-toastify": "^4.4.0",
"redux": "^4.0.0", "redux": "^4.0.0",

View File

@ -1,55 +1,79 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Switch, Route, Link, withRouter } from 'react-router-dom'; import { Routes, Route, Link } from 'react-router-dom';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { connect } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { ToastContainer, toast } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
import { Grid, Divider, Icon, Container } from 'semantic-ui-react'; import { Grid, Divider, Icon, Container } from 'semantic-ui-react';
import api from 'api'; import api from 'api';
import actions from 'actions'; import actions from 'actions';
import utils from 'utils/utils.js';
import NavBar from 'components/includes/NavBar'; import NavBar from 'components/includes/NavBar';
import logo from 'images/logo/main.png'; import logo from 'images/logo/main.png';
import MarketingHome from './marketing/Home.js'; import MarketingHome from './marketing/Home.js';
import MarketingPricing from './marketing/Pricing.js';
import PrivacyPolicy from './marketing/PrivacyPolicy'; import PrivacyPolicy from './marketing/PrivacyPolicy';
import TermsOfUse from './marketing/TermsOfUse'; import TermsOfUse from './marketing/TermsOfUse';
import Login from './Login.js'; import Login from './Login.js';
import ForgottenPassword from './ForgottenPassword'; import ForgottenPassword from './ForgottenPassword';
import ResetPassword from './ResetPassword'; import ResetPassword from './ResetPassword';
import Home from './main/Home.js'; import Home from './main/Home.js';
import Profile from './main/users/Profile.js'; import Profile from './main/users/Profile.js';
import ProfileEdit from './main/users/EditProfile';
import ProfileProjects from './main/users/ProfileProjects';
import NewProject from './main/projects/New.js'; import NewProject from './main/projects/New.js';
import Project from './main/projects/Project.js'; import Project from './main/projects/Project.js';
import ProjectObjects from './main/projects/ProjectObjects.js';
import ProjectSettings from './main/projects/Settings.js';
import ObjectDraft from './main/projects/objects/Draft.js';
import ObjectList from './main/projects/ObjectList.js';
import Settings from './main/settings/Settings.js'; import Settings from './main/settings/Settings.js';
import SettingsIdentity from './main/settings/Identity';
import SettingsNotification from './main/settings/Notification';
import SettingsAccount from './main/settings/Account';
import NewGroup from './main/groups/New.js'; import NewGroup from './main/groups/New.js';
import Group from './main/groups/Group.js'; import Group from './main/groups/Group.js';
import GroupFeed from './main/groups/Feed.js';
import GroupMembers from './main/groups/Members.js';
import GroupProjects from './main/groups/Projects.js';
import GroupSettings from './main/groups/Settings.js';
import Root from './main/root'; import Root from './main/root';
//import Docs from './docs';
function App() {
const dispatch = useDispatch();
function App({ user, groups, syncedToDrift, driftReady, onOpenRegister, onCloseAuthentication, isAuthenticating, isAuthenticatingType, isAuthenticated, onLoginSuccess, onLogout, onReceiveProjects, onReceiveInvitations, onReceiveGroups, onDriftReady, onDriftSynced, helpModalOpen, openHelpModal, searchTerm, updateSearchTerm, searchPopupOpen, openSearchPopup, searchResults, updateSearchResults, searching, updateSearching, history }) { const { isAuthenticated, isAuthenticating, isAuthenticatingType, user, driftReady, syncedToDrift } = useSelector(state => {
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const { isAuthenticated, isAuthenticating, isAuthenticatingType } = state.auth;
const { driftReady, syncedToDrift } = state.users;
return { isAuthenticated, isAuthenticating, isAuthenticatingType, user, driftReady, syncedToDrift };
});
const loggedInUserId = user?._id; const loggedInUserId = user?._id;
useEffect(() => { useEffect(() => {
api.auth.autoLogin(onLoginSuccess); api.auth.autoLogin(token => dispatch(actions.auth.receiveLogin(token)));
}, [onLoginSuccess]); }, [dispatch]);
useEffect(() => { useEffect(() => {
if (!loggedInUserId) return; if (!loggedInUserId) return;
api.users.getMyProjects(onReceiveProjects); api.users.getMyProjects(p => dispatch(actions.projects.receiveProjects(p)));
api.groups.getMine(onReceiveGroups); api.groups.getMine(g => dispatch(actions.groups.receiveGroups(g)));
api.invitations.get(({ invitations, sentInvitations}) => { api.invitations.get(({ invitations, sentInvitations}) => {
onReceiveInvitations(invitations.concat(sentInvitations)); dispatch(actions.invitations.receiveInvitations(invitations.concat(sentInvitations)));
}); });
}, [loggedInUserId, onReceiveProjects, onReceiveGroups, onReceiveInvitations]); }, [dispatch, loggedInUserId]);
useEffect(() => { useEffect(() => {
window.drift && window.drift.on('ready', () => { window.drift && window.drift.on('ready', () => {
onDriftReady(); dispatch(actions.users.initDrift());
}); });
}, [onDriftReady]); }, [dispatch]);
useEffect(() => { useEffect(() => {
if (user && driftReady && !syncedToDrift && window.drift) { if (user && driftReady && !syncedToDrift && window.drift) {
@ -58,35 +82,52 @@ function App({ user, groups, syncedToDrift, driftReady, onOpenRegister, onCloseA
username: user.username, username: user.username,
createdAt: user.createdAt, createdAt: user.createdAt,
}); });
onDriftSynced(); dispatch(actions.users.syncDrift(null));
} }
}, [user, driftReady, syncedToDrift, onDriftSynced]); }, [dispatch, user, driftReady, syncedToDrift]);
return ( return (
<div style={{display: 'flex', flexDirection: 'column', minHeight: '100vh'}}> <div style={{display: 'flex', flexDirection: 'column', minHeight: '100vh'}}>
<Helmet defaultTitle={'Treadl'} titleTemplate={`%s | Treadl`} /> <Helmet defaultTitle={'Treadl'} titleTemplate={`%s | Treadl`} />
<NavBar /> <NavBar />
<div style={{ flex: '1 0 0' }}> <div style={{ flex: '1 0 0' }}>
<Switch> <Routes>
<Route exact path="/" render={props => (isAuthenticated <Route end path="/" element={isAuthenticated
? <Home {...props} /> ? <Home />
: <MarketingHome {...props} onRegisterClicked={onOpenRegister} />) : <MarketingHome onRegisterClicked={() => dispatch(actions.auth.openRegister())} />
} /> } />
<Route path="/pricing" render={props => <MarketingPricing {...props} onRegisterClicked={onOpenRegister} />} /> <Route path="/privacy" element={<PrivacyPolicy />} />
<Route path="/privacy" component={PrivacyPolicy} /> <Route path="/terms-of-use" element={<TermsOfUse />} />
<Route path="/terms-of-use" component={TermsOfUse} /> <Route path="/password/forgotten" element={<ForgottenPassword />} />
<Route path="/password/forgotten" component={ForgottenPassword} /> <Route path="/password/reset" element={<ResetPassword />} />
<Route path="/password/reset" component={ResetPassword} /> <Route path="/settings" element={<Settings />}>
<Route path="/settings" component={Settings} /> <Route path='identity' element={<SettingsIdentity />} />
<Route path="/projects/new" component={NewProject} /> <Route path='notifications' element={<SettingsNotification />} />
<Route path="/groups/new" component={NewGroup} /> <Route path='account' element={<SettingsAccount />} />
<Route path="/groups/:id" component={Group} /> <Route path='' element={<SettingsIdentity />} />
<Route path='/root' component={Root} /> </Route>
<Route path="/:username/edit" component={Profile} /> <Route path="/projects/new" element={<NewProject />} />
<Route path="/:username/:projectPath" component={Project} /> <Route path="/groups/new" element={<NewGroup />} />
<Route path="/:username" component={Profile} /> <Route path="/groups/:id" element={<Group />}>
</Switch> <Route path='feed' element={<GroupFeed />} />
<Login open={isAuthenticating} authType={isAuthenticatingType} onClose={onCloseAuthentication} /> <Route path='members' element={<GroupMembers />} />
<Route path='projects' element={<GroupProjects />} />
<Route path='settings' element={<GroupSettings />} />
<Route path='' end element={<GroupFeed />} />
</Route>
<Route path='/root' element={<Root />} />
<Route path='/:username/:projectPath' element={<Project />}>
<Route path="settings" element={<ProjectSettings />} />
<Route path=":objectId/edit" element={<ObjectDraft />} />
<Route path=":objectId" element={<ProjectObjects />} />
<Route path='' element={<ObjectList />} />
</Route>
<Route path="/:username" element={<Profile />}>
<Route path="edit" element={<ProfileEdit />} />
<Route path='' element={<ProfileProjects />} />
</Route>
</Routes>
<Login open={isAuthenticating} authType={isAuthenticatingType} onClose={() => dispatch(actions.auth.closeAuthentication())} />
<ToastContainer position={toast.POSITION.BOTTOM_CENTER} hideProgressBar/> <ToastContainer position={toast.POSITION.BOTTOM_CENTER} hideProgressBar/>
<Divider hidden section /> <Divider hidden section />
</div> </div>
@ -131,34 +172,4 @@ function App({ user, groups, syncedToDrift, driftReady, onOpenRegister, onCloseA
); );
} }
const mapStateToProps = (state) => { export default App;
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
const { isAuthenticated, isAuthenticating, isAuthenticatingType } = state.auth;
const { driftReady, syncedToDrift } = state.users;
const { helpModalOpen, searchPopupOpen, searchTerm, searchResults, searching } = state.app;
return { isAuthenticated, isAuthenticating, isAuthenticatingType, user, groups, driftReady, syncedToDrift, helpModalOpen, searchPopupOpen, searchTerm, searchResults, searching };
};
const mapDispatchToProps = dispatch => ({
onOpenRegister: () => dispatch(actions.auth.openRegister()),
onCloseAuthentication: () => dispatch(actions.auth.closeAuthentication()),
onLoginSuccess: token => dispatch(actions.auth.receiveLogin(token)),
onLogout: () => dispatch(actions.auth.logout()),
onReceiveProjects: p => dispatch(actions.projects.receiveProjects(p)),
onReceiveGroups: g => dispatch(actions.groups.receiveGroups(g)),
onReceiveInvitations: i => dispatch(actions.invitations.receiveInvitations(i)),
onDriftSynced: (s) => dispatch(actions.users.syncDrift(s)),
onDriftReady: () => dispatch(actions.users.initDrift()),
openHelpModal: o => dispatch(actions.app.openHelpModal(o)),
openSearchPopup: o => dispatch(actions.app.openSearchPopup(o)),
updateSearchTerm: t => dispatch(actions.app.updateSearchTerm(t)),
updateSearchResults: r => dispatch(actions.app.updateSearchResults(r)),
updateSearching: s => dispatch(actions.app.updateSearching(s)),
});
const AppContainer = withRouter(connect(
mapStateToProps,
mapDispatchToProps,
)(App));
export default AppContainer;

View File

@ -1,31 +1,28 @@
import React, { Component } from 'react'; import React, { useState } from 'react';
import { import {
Card, Input, Divider, Button, Card, Input, Divider, Button,
} from 'semantic-ui-react'; } from 'semantic-ui-react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { connect } from 'react-redux';
import api from 'api'; import api from 'api';
class ForgottenPassword extends Component { function ForgottenPassword() {
constructor(props) { const [email, setEmail] = useState('');
super(props); const [loading, setLoading] = useState(false);
this.state = { email: '', loading: false }; const navigate = useNavigate();
}
sendEmail = () => { const sendEmail = () => {
this.setState({ loading: true }); setLoading(true);
api.auth.sendPasswordResetEmail(this.state.email, () => { api.auth.sendPasswordResetEmail(email, () => {
this.setState({ loading: false }); setLoading(false);
toast.info('If your account exists, a password email has been sent'); toast.info('If your account exists, a password email has been sent');
this.props.history.push('/'); navigate('/');
}, (err) => { }, (err) => {
this.setState({ loading: false }); setLoading(false);
toast.error(err.message); toast.error(err.message);
}); });
} };
render() {
const { email, loading } = this.state;
return ( return (
<Card.Group centered style={{ marginTop: 50 }}> <Card.Group centered style={{ marginTop: 50 }}>
<Card raised color="yellow"> <Card raised color="yellow">
@ -33,24 +30,15 @@ class ForgottenPassword extends Component {
<Card.Header>Forgotten your password?</Card.Header> <Card.Header>Forgotten your password?</Card.Header>
<Card.Meta>Type your email address below, and we'll send you a password-reset email.</Card.Meta> <Card.Meta>Type your email address below, and we'll send you a password-reset email.</Card.Meta>
<Divider hidden /> <Divider hidden />
<Input fluid type="email" value={email} onChange={e => this.setState({ email: e.target.value })} placeholder="mary@example.com" autoFocus /> <Input fluid type="email" value={email} onChange={e => setEmail(e.target.value)} placeholder="mary@example.com" autoFocus />
</Card.Content> </Card.Content>
<Card.Content extra textAlign="right"> <Card.Content extra textAlign="right">
<Button basic onClick={this.props.history.goBack} content="Cancel" /> <Button basic onClick={() => navigate('/')} content="Cancel" />
<Button color="teal" content="Send email" onClick={this.sendEmail} loading={loading} /> <Button color="teal" content="Send email" onClick={sendEmail} loading={loading} />
</Card.Content> </Card.Content>
</Card> </Card>
</Card.Group> </Card.Group>
); );
}
} }
const mapStateToProps = state => ({ }); export default ForgottenPassword;
const mapDispatchToProps = dispatch => ({
});
const ForgottenPasswordContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(ForgottenPassword);
export default ForgottenPasswordContainer;

View File

@ -1,65 +1,64 @@
import React, { Component } from 'react'; import React, { useState } from 'react';
import { import {
Message, Modal, Grid, Form, Input, Button, Message, Modal, Grid, Form, Input, Button,
} from 'semantic-ui-react'; } from 'semantic-ui-react';
import { connect } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { Link, withRouter } from 'react-router-dom'; import { Link } from 'react-router-dom';
import actions from 'actions'; import actions from 'actions';
import { api } from 'api'; import { api } from 'api';
import ReadingImage from 'images/reading.png'; import ReadingImage from 'images/reading.png';
class Login extends Component { function Login({ open, authType, onClose }) {
constructor(props) { const [username, setUsername] = useState('');
super(props); const [email, setEmail] = useState('');
this.state = { const [password, setPassword] = useState('');
registering: false, username: '', email: '', password: '', loading: false, const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const { error } = useSelector(state => {
const { loading, error } = state.auth;
return { loading, error };
});
const login = () => {
setLoading(true);
api.auth.login(email, password, () => dispatch(actions.auth.requestLogin()), (data) => {
setLoading(false);
setPassword('');
setEmail('');
setUsername('');
dispatch(actions.auth.receiveLogin(data));
onClose();
}, (err) => {
dispatch(actions.auth.loginError(err));
setLoading(false);
});
}; };
}
login = () => { const register = () => {
const { email, password } = this.state; setLoading(true);
this.setState({ loading: true }); api.auth.register(username, email, password, () => dispatch(actions.auth.requestLogin()), (data) => {
api.auth.login(email, password, this.props.onLoginStart, (data) => { setLoading(false);
this.setState({ loading: false, password: '', email: '', username: '' }); setPassword('');
this.props.onLoginSuccess(data); setEmail('');
this.props.onClose(); setUsername('');
dispatch(actions.auth.receiveLogin(data));
onClose();
}, (err) => { }, (err) => {
this.props.onLoginFailure(err); dispatch(actions.auth.loginError(err));
this.setState({ loading: false }); setLoading(false);
}); });
} };
register = () => {
const { username, email, password } = this.state;
this.setState({ loading: true });
api.auth.register(username, email, password, this.props.onLoginStart, (data) => {
this.setState({ loading: false, password: '', email: '', username: '' });
this.props.onLoginSuccess(data);
this.props.onClose();
}, (err) => {
this.props.onLoginFailure(err);
this.setState({ loading: false });
});
}
handleChange = (event) => {
const update = {};
update[event.target.name] = event.target.value;
this.setState(update);
}
render() {
const { loading } = this.state;
return ( return (
<div> <div>
{this.props.authType === 'register' {authType === 'register'
&& ( && (
<Modal dimmer="inverted" open={this.props.open} onClose={this.props.onClose}> <Modal dimmer="inverted" open={open} onClose={onClose}>
<Modal.Header> <Modal.Header>
<span role="img" aria-label="wave">👋</span> Welcome! <span role="img" aria-label="wave">👋</span> Welcome!
<Button floated="right" onClick={this.props.onClose} basic content="Close" /> <Button floated="right" onClick={onClose} basic content="Close" />
</Modal.Header> </Modal.Header>
<Modal.Content> <Modal.Content>
<Grid stackable> <Grid stackable>
@ -101,25 +100,25 @@ class Login extends Component {
</strong> Sign-up to see what it's about, and we'd love to hear your feedback and work with you as we grow. </strong> Sign-up to see what it's about, and we'd love to hear your feedback and work with you as we grow.
</Message> </Message>
{this.props.error && (<div className="ui warning message">{this.props.error.message}</div>)} {error && (<div className="ui warning message">{error.message}</div>)}
<Form onSubmit={this.register}> <Form onSubmit={register}>
<Form.Field> <Form.Field>
<label>Pick a username <label>Pick a username
<small> (you can use letters, numbers and underscores)</small> <small> (you can use letters, numbers and underscores)</small>
</label> </label>
<Input autoFocus size="large" fluid name="username" type="text" value={this.state.username} onChange={event => this.handleChange(event)} /> <Input autoFocus size="large" fluid name="username" type="text" value={username} onChange={e => setUsername(e.target.value)} />
</Form.Field> </Form.Field>
<Form.Field> <Form.Field>
<label>Email address <label>Email address
<small> (for password resets &amp; other important things)</small> <small> (for password resets &amp; other important things)</small>
</label> </label>
<Input size="large" fluid name="email" type="email" value={this.state.email} onChange={event => this.handleChange(event)} /> <Input size="large" fluid name="email" type="email" value={email} onChange={e => setEmail(e.target.value)} />
</Form.Field> </Form.Field>
<Form.Field> <Form.Field>
<label>Choose a strong password <label>Choose a strong password
<small> (at least 6 characters)</small> <small> (at least 6 characters)</small>
</label> </label>
<Input size="large" fluid name="password" type="password" value={this.state.password} onChange={event => this.handleChange(event)} /> <Input size="large" fluid name="password" type="password" value={password} onChange={e => setPassword(e.target.value)} />
</Form.Field> </Form.Field>
<div className="ui hidden divider" /> <div className="ui hidden divider" />
<p style={{ fontSize: 11, color: 'rgb(180,180,180)' }}>By signing-up, you agree to the Treadl <a href="/privacy" target="_blank">Privacy Policy</a> and <a href="terms-of-use" target="_blank">Terms of Use</a>.</p> <p style={{ fontSize: 11, color: 'rgb(180,180,180)' }}>By signing-up, you agree to the Treadl <a href="/privacy" target="_blank">Privacy Policy</a> and <a href="terms-of-use" target="_blank">Terms of Use</a>.</p>
@ -133,12 +132,12 @@ class Login extends Component {
</Modal> </Modal>
) )
} }
{this.props.authType === 'login' {authType === 'login'
&& ( && (
<Modal dimmer="inverted" open={this.props.open} onClose={this.props.onClose}> <Modal dimmer="inverted" open={open} onClose={onClose}>
<Modal.Header> <Modal.Header>
<span role="img" aria-label="Peace"> </span> <span role="img" aria-label="Peace"> </span>
Welcome back <Button floated="right" onClick={this.props.onClose} basic content="Close" /> Welcome back <Button floated="right" onClick={onClose} basic content="Close" />
</Modal.Header> </Modal.Header>
<Modal.Content> <Modal.Content>
<Grid stackable> <Grid stackable>
@ -148,17 +147,17 @@ Welcome back <Button floated="right" onClick={this.props.onClose} basic content=
</Grid.Column> </Grid.Column>
<Grid.Column computer={8}> <Grid.Column computer={8}>
{this.props.error && (<div className="ui warning message">{this.props.error.message}</div>)} {error && (<div className="ui warning message">{error.message}</div>)}
<Form onSubmit={this.login}> <Form onSubmit={login}>
<Form.Field> <Form.Field>
<label>Your email address or username</label> <label>Your email address or username</label>
<Input autoFocus size="large" fluid name="email" type="text" value={this.state.email} onChange={event => this.handleChange(event)} placeholder='Email or username' /> <Input autoFocus size="large" fluid name="email" type="text" value={email} onChange={e => setEmail(e.target.value)} placeholder='Email or username' />
</Form.Field> </Form.Field>
<Form.Field> <Form.Field>
<label>Password <label>Password
<Link to="/password/forgotten" style={{ float: 'right' }} onClick={this.props.onClose}>Forgotten your password?</Link> <Link to="/password/forgotten" style={{ float: 'right' }} onClick={onClose}>Forgotten your password?</Link>
</label> </label>
<Input size="large" fluid name="password" type="password" value={this.state.password} onChange={event => this.handleChange(event)} placeholder='Password' /> <Input size="large" fluid name="password" type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder='Password' />
</Form.Field> </Form.Field>
<div className="ui hidden divider" /> <div className="ui hidden divider" />
<Form.Button type='submit' size="large" color="teal" fluid loading={loading}>Login</Form.Button> <Form.Button type='submit' size="large" color="teal" fluid loading={loading}>Login</Form.Button>
@ -171,23 +170,6 @@ Welcome back <Button floated="right" onClick={this.props.onClose} basic content=
} }
</div> </div>
); );
}
} }
const mapStateToProps = (state) => { export default Login;
const { auth } = state;
const { loading, isAuthenticated, error } = auth;
return { loading, isAuthenticated, error };
};
const mapDispatchToProps = dispatch => ({
onLoginStart: () => dispatch(actions.auth.requestLogin()),
onLoginSuccess: token => dispatch(actions.auth.receiveLogin(token)),
onLoginFailure: error => dispatch(actions.auth.loginError(error)),
});
const LoginContainer = withRouter(connect(
mapStateToProps,
mapDispatchToProps,
)(Login));
export default LoginContainer;

View File

@ -1,33 +1,30 @@
import React, { Component } from 'react'; import React, { useState } from 'react';
import { import {
Card, Input, Divider, Button, Card, Input, Divider, Button,
} from 'semantic-ui-react'; } from 'semantic-ui-react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { connect } from 'react-redux';
import api from 'api'; import api from 'api';
class ResetPassword extends Component { function ResetPassword() {
constructor(props) { const [loading, setLoading] = useState(false);
super(props); const [password, setPassword] = useState('');
this.state = { password: '', loading: false }; const navigate = useNavigate();
}
resetPassword = () => { const resetPassword = () => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const token = params.get('token'); const token = params.get('token');
this.setState({ loading: true }); setLoading(true);
api.auth.updatePasswordWithToken(token, this.state.password, () => { api.auth.updatePasswordWithToken(token, password, () => {
this.setState({ loading: false }); setLoading(false);
toast.info('Password changed successfully.'); toast.info('Password changed successfully.');
this.props.history.push('/'); navigate('/');
}, (err) => { }, (err) => {
this.setState({ loading: false }); setLoading(false);
toast.error(err.message); toast.error(err.message);
}); });
} };
render() {
const { password, loading } = this.state;
return ( return (
<Card.Group centered style={{ marginTop: 50 }}> <Card.Group centered style={{ marginTop: 50 }}>
<Card raised color="yellow"> <Card raised color="yellow">
@ -35,23 +32,14 @@ class ResetPassword extends Component {
<Card.Header>Enter a new password</Card.Header> <Card.Header>Enter a new password</Card.Header>
<Card.Meta>Enter a new password below.</Card.Meta> <Card.Meta>Enter a new password below.</Card.Meta>
<Divider hidden /> <Divider hidden />
<Input fluid type="password" value={password} onChange={e => this.setState({ password: e.target.value })} autoFocus /> <Input fluid type="password" value={password} onChange={e => setPassword(e.target.value)} autoFocus />
</Card.Content> </Card.Content>
<Card.Content extra textAlign="right"> <Card.Content extra textAlign="right">
<Button color="teal" content="Change password" onClick={this.resetPassword} loading={loading} /> <Button color="teal" content="Change password" onClick={resetPassword} loading={loading} />
</Card.Content> </Card.Content>
</Card> </Card>
</Card.Group> </Card.Group>
); );
}
} }
const mapStateToProps = state => ({ }); export default ResetPassword;
const mapDispatchToProps = dispatch => ({
});
const ResetPasswordContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(ResetPassword);
export default ResetPasswordContainer;

View File

@ -0,0 +1,29 @@
import React, { useState } from 'react';
import { Container } from 'semantic-ui-react';
import { Switch, Route, Link } from 'react-router-dom';
import ReactMarkdown from 'react-markdown';
import stuff from './stuff.md';
function Docs({ }) {
const [markdown, setMarkdown] = useState();
const getDoc = async key => {
const markdownFile = await fetch(stuff);//.then(res => res.text()).then(text => this.setState({ markdown: text }));
const markdown = await markdownFile.text();
console.log(markdown);
setMarkdown(markdown);
};
return (
<Container>
<h1>Treadl documentation</h1>
<ReactMarkdown># Hello, *world*!</ReactMarkdown>
<ReactMarkdown>{stuff}</ReactMarkdown>
<Switch>
<Route path="/docs/project" component={<ReactMarkdown>{markdown}</ReactMarkdown>} />
</Switch>
</Container>
);
};
export default Docs;

View File

@ -0,0 +1,6 @@
# Hello
This is some content
* and here
* there

View File

@ -1,76 +1,72 @@
import React, { Component } from 'react'; import React, { useState, useRef } from 'react';
import { Button } from 'semantic-ui-react'; import { Button } from 'semantic-ui-react';
import api from 'api'; import api from 'api';
class FileChooser extends Component {
constructor(props) {
super(props);
this.state = { isUploading: false };
}
startUpload = () => { function FileChooser({ onUploadStart, onUploadFinish, onError, onComplete, content, trigger, accept, forType, forObject }) {
this.setState({ isUploading: true }); const [isUploading, setIsUploading] = useState(false);
this.props.onUploadStart && this.props.onUploadStart(); const inputRef = useRef();
}
finishUpload = () => { const startUpload = () => {
this.setState({ isUploading: false }); setIsUploading(true);
this.props.onUploadFinish && this.props.onUploadFinish(); onUploadStart && onUploadStart();
} };
chooseFile = () => this.refs.fileInput.click() const finishUpload = () => {
setIsUploading(false);
onUploadFinish && onUploadFinish();
};
handleFileChosen = (e) => { const chooseFile = () => inputRef.current.click();
const handleFileChosen = (e) => {
const file = e.target.files && e.target.files[0]; const file = e.target.files && e.target.files[0];
if (file) { if (file) {
this.startUpload(); startUpload();
const fileName = file.name.replace(/[^a-zA-Z0-9_.]/g, '_'); const fileName = file.name.replace(/[^a-zA-Z0-9_.]/g, '_');
if (this.props.forType === 'project' && fileName.toLowerCase().indexOf('.wif') > -1) { if (forType === 'project' && fileName.toLowerCase().indexOf('.wif') > -1) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e2) => { reader.onload = (e2) => {
this.finishUpload(); finishUpload();
this.props.onComplete({ wif: e2.target.result, type: 'pattern' }); onComplete({ wif: e2.target.result, type: 'pattern' });
}; };
reader.readAsText(file); reader.readAsText(file);
} else { } else {
api.uploads.generateFileUploadRequest({ api.uploads.generateFileUploadRequest({
forType: this.props.forType, forId: this.props.for._id, name: fileName, size: file.size, type: file.type, forType: forType, forId: forObject._id, name: fileName, size: file.size, type: file.type,
}, async (response) => { }, async (response) => {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open('PUT', response.signedRequest); xhr.open('PUT', response.signedRequest);
xhr.setRequestHeader('Content-Type', file.type); xhr.setRequestHeader('Content-Type', file.type);
xhr.onreadystatechange = () => { xhr.onreadystatechange = () => {
if (xhr.readyState === 4) { if (xhr.readyState === 4) {
this.finishUpload(); finishUpload();
if (xhr.status === 200) { if (xhr.status === 200) {
// We pass back the original file name so it can be displayed nicely // We pass back the original file name so it can be displayed nicely
this.props.onComplete({ storedName: response.fileName, name: file.name, type: 'file' }); onComplete({ storedName: response.fileName, name: file.name, type: 'file' });
} else if (this.props.onError) { } else if (onError) {
this.finishUpload(); finishUpload();
this.props.onError('Unable to upload file'); onError('Unable to upload file');
} }
} }
}; };
xhr.send(file); xhr.send(file);
}, (err) => { }, (err) => {
this.finishUpload(); finishUpload();
if (this.props.onError) this.props.onError(err.message || 'Unable to upload file'); if (onError) onError(err.message || 'Unable to upload file');
}); });
} }
} }
} }
render() {
const { content, trigger, accept } = this.props;
return ( return (
<React.Fragment> <React.Fragment>
<input type="file" style={{ display: 'none' }} ref="fileInput" onChange={this.handleFileChosen} accept={accept || '*'} /> <input type="file" style={{ display: 'none' }} ref={inputRef} onChange={handleFileChosen} accept={accept || '*'} />
{trigger {trigger
? React.cloneElement(trigger, { loading: this.state.isUploading, onClick: this.chooseFile }) ? React.cloneElement(trigger, { loading: isUploading, onClick: chooseFile })
: <Button size="small" color="blue" icon="file" fluid content={content || 'Choose a file'} loading={this.state.isUploading} onClick={this.chooseFile} /> : <Button size="small" color="blue" icon="file" fluid content={content || 'Choose a file'} loading={isUploading} onClick={chooseFile} />
} }
</React.Fragment> </React.Fragment>
); );
}
} }
export default FileChooser; export default FileChooser;

View File

@ -1,6 +1,6 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Link, withRouter } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { connect } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import { Loader, List, Popup, Modal, Grid, Icon, Button, Container, Dropdown } from 'semantic-ui-react'; import { Loader, List, Popup, Modal, Grid, Icon, Button, Container, Dropdown } from 'semantic-ui-react';
import api from 'api'; import api from 'api';
@ -74,22 +74,31 @@ const SearchBar = styled.div`
} }
`; `;
function NavBar({ user, groups, onOpenLogin, onOpenRegister, isAuthenticated, onLogout, onDriftSynced, helpModalOpen, openHelpModal, searchTerm, updateSearchTerm, searchPopupOpen, openSearchPopup, searchResults, updateSearchResults, searching, updateSearching, history }) { function NavBar() {
const dispatch = useDispatch();
const { isAuthenticated, user, groups, helpModalOpen, searchPopupOpen, searchTerm, searchResults, searching } = useSelector(state => {
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
const { isAuthenticated } = state.auth;
const { helpModalOpen, searchPopupOpen, searchTerm, searchResults, searching } = state.app;
return { isAuthenticated, user, groups, helpModalOpen, searchPopupOpen, searchTerm, searchResults, searching };
});
const navigate = useNavigate();
useEffect(() => { useEffect(() => {
openSearchPopup(false); dispatch(actions.app.openSearchPopup(false));
}, [history.location.pathname, openSearchPopup]); }, [dispatch]);
const logout = () => api.auth.logout(() => { const logout = () => api.auth.logout(() => {
onLogout(); dispatch(actions.auth.logout());
onDriftSynced(false); dispatch(actions.users.syncDrift(false))
if (window.drift) window.drift.reset(); if (window.drift) window.drift.reset();
history.push('/'); navigate('/');
}); });
const search = () => { const search = () => {
updateSearching(true); dispatch(actions.app.updateSearching(true));
api.search.all(searchTerm, updateSearchResults); api.search.all(searchTerm, r => dispatch(actions.app.updateSearchResults(r)));
}; };
return ( return (
@ -99,8 +108,9 @@ function NavBar({ user, groups, onOpenLogin, onOpenRegister, isAuthenticated, on
{isAuthenticated {isAuthenticated
? ( ? (
<div className='nav-links'> <div className='nav-links'>
<Popup basic on='focus' open={searchPopupOpen} onOpen={e => openSearchPopup(true)} onClose={e => openSearchPopup(false)} <Popup basic on='focus' open={searchPopupOpen}
trigger={<SearchBar><input placeholder='Click to search...' value={searchTerm} onChange={e => updateSearchTerm(e.target.value)} onKeyDown={e => e.keyCode === 13 && search()} /></SearchBar>} onOpen={e => dispatch(actions.app.openSearchPopup(true))} onClose={e => dispatch(actions.app.openSearchPopup(false))}
trigger={<SearchBar><input placeholder='Click to search...' value={searchTerm} onChange={e => dispatch(actions.app.updateSearchTerm(e.target.value))} onKeyDown={e => e.keyCode === 13 && search()} /></SearchBar>}
content={<div style={{width: 300}} className='joyride-search'> content={<div style={{width: 300}} className='joyride-search'>
{!searchResults?.users && !searchResults?.groups ? {!searchResults?.users && !searchResults?.groups ?
<small> <small>
@ -171,7 +181,7 @@ function NavBar({ user, groups, onOpenLogin, onOpenRegister, isAuthenticated, on
} }
<span className='above-mobile'> <span className='above-mobile'>
<Button size='small' icon='help' basic inverted onClick={e => openHelpModal(true)}/> <Button size='small' icon='help' basic inverted onClick={e => dispatch(actions.app.openHelpModal(true))}/>
</span> </span>
<Dropdown direction="left" pointing="top right" icon={null} style={{marginLeft: 10}} <Dropdown direction="left" pointing="top right" icon={null} style={{marginLeft: 10}}
@ -213,20 +223,20 @@ function NavBar({ user, groups, onOpenLogin, onOpenRegister, isAuthenticated, on
trigger=<Button basic inverted icon="bars" /> trigger=<Button basic inverted icon="bars" />
> >
<Dropdown.Menu direction="left"> <Dropdown.Menu direction="left">
<Dropdown.Item onClick={onOpenLogin}>Login</Dropdown.Item> <Dropdown.Item onClick={() => dispatch(actions.auth.openLogin())}>Login</Dropdown.Item>
</Dropdown.Menu> </Dropdown.Menu>
</Dropdown> </Dropdown>
</span> </span>
<span className="above-mobile"> <span className="above-mobile">
<Button inverted basic onClick={onOpenLogin}>Login</Button> <Button inverted basic onClick={() => dispatch(actions.auth.openLogin())}>Login</Button>
</span> </span>
<Button color="teal" onClick={onOpenRegister}> <Button color="teal" onClick={() => dispatch(actions.auth.openRegister())}>
<span role="img" aria-label="wave">👋</span> Sign-up <span role="img" aria-label="wave">👋</span> Sign-up
</Button> </Button>
</div> </div>
) )
} }
<Modal open={helpModalOpen} onClose={e => openHelpModal(false)}> <Modal open={helpModalOpen} onClose={e => dispatch(actions.app.openHelpModal(false))}>
<Modal.Header>Welcome to Treadl!</Modal.Header> <Modal.Header>Welcome to Treadl!</Modal.Header>
<Modal.Content> <Modal.Content>
<h3>Introduction</h3> <h3>Introduction</h3>
@ -247,7 +257,7 @@ function NavBar({ user, groups, onOpenLogin, onOpenRegister, isAuthenticated, on
<p>If you have any comments or feedback please tell us by emailing <a href="mailto:hello@treadl.com">hello@treadl.com</a>!</p> <p>If you have any comments or feedback please tell us by emailing <a href="mailto:hello@treadl.com">hello@treadl.com</a>!</p>
</Modal.Content> </Modal.Content>
<Modal.Actions> <Modal.Actions>
<Button onClick={e => openHelpModal(false)} color='teal' icon='check' content='OK' /> <Button onClick={e => dispatch(actions.app.openHelpModal(false))} color='teal' icon='check' content='OK' />
</Modal.Actions> </Modal.Actions>
</Modal> </Modal>
</Container> </Container>
@ -255,29 +265,4 @@ function NavBar({ user, groups, onOpenLogin, onOpenRegister, isAuthenticated, on
); );
} }
const mapStateToProps = (state) => { export default NavBar;
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
const { isAuthenticated } = state.auth;
const { helpModalOpen, searchPopupOpen, searchTerm, searchResults, searching } = state.app;
return { isAuthenticated, user, groups, helpModalOpen, searchPopupOpen, searchTerm, searchResults, searching };
};
const mapDispatchToProps = dispatch => ({
onOpenLogin: () => dispatch(actions.auth.openLogin()),
onOpenRegister: () => dispatch(actions.auth.openRegister()),
onLogout: () => dispatch(actions.auth.logout()),
onReceiveUser: user => dispatch(actions.users.receive(user)),
onDriftSynced: (s) => dispatch(actions.users.syncDrift(s)),
openHelpModal: o => dispatch(actions.app.openHelpModal(o)),
openSearchPopup: o => dispatch(actions.app.openSearchPopup(o)),
updateSearchTerm: t => dispatch(actions.app.updateSearchTerm(t)),
updateSearchResults: r => dispatch(actions.app.updateSearchResults(r)),
updateSearching: s => dispatch(actions.app.updateSearching(s)),
});
const NavBarContainer = withRouter(connect(
mapStateToProps,
mapDispatchToProps,
)(NavBar));
export default NavBarContainer;

View File

@ -84,7 +84,7 @@ const NewFeedMessage = connect(
> >
<Dropdown.Menu> <Dropdown.Menu>
<FileChooser <FileChooser
forType={forType} for={forObj} forType={forType} forObject={forObj}
trigger=<Dropdown.Item icon="upload" content="Upload a file from your computer" /> trigger=<Dropdown.Item icon="upload" content="Upload a file from your computer" />
onUploadStart={e => updateAttachmentUploading(true) } onUploadStart={e => updateAttachmentUploading(true) }
onUploadFinish={e => updateAttachmentUploading(false) } onUploadFinish={e => updateAttachmentUploading(false) }

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react'; import React, { useState } from 'react';
import { connect } from 'react-redux'; import { useDispatch } from 'react-redux';
import { withRouter } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Button, Dropdown } from 'semantic-ui-react'; import { Button, Dropdown } from 'semantic-ui-react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import actions from 'actions'; import actions from 'actions';
@ -8,37 +8,33 @@ import api from 'api';
import FileChooser from 'components/includes/FileChooser'; import FileChooser from 'components/includes/FileChooser';
class ObjectCreator extends Component { function ObjectCreator({ project, onCreateObject, onError, fluid }) {
constructor(props) { const [isUploading, setIsUploading] = useState(false);
super(props); const navigate = useNavigate();
this.state = { isUploading: false }; const dispatch = useDispatch();
}
createNewPattern = () => { const createNewPattern = () => {
api.projects.createObject(this.props.project.fullName, { name: 'Untitled pattern', type: 'pattern' }, (object) => { api.projects.createObject(project.fullName, { name: 'Untitled pattern', type: 'pattern' }, (object) => {
this.props.onCreateObject(object); dispatch(actions.objects.create(object));
this.props.history.push(`/${this.props.project.fullName}/${object._id}/edit`); navigate(`/${project.fullName}/${object._id}/edit`);
}, err => this.setState({ loading: false })); });
} };
fileUploaded = (file) => { const fileUploaded = (file) => {
this.setState({ isUploading: true }); setIsUploading(true);
api.projects.createObject(this.props.project.fullName, { api.projects.createObject(project.fullName, {
name: file.name, storedName: file.storedName, type: file.type, wif: file.wif, name: file.name, storedName: file.storedName, type: file.type, wif: file.wif,
}, (object) => { }, (object) => {
this.setState({ isUploading: false }); setIsUploading(false);
this.props.onCreateObject(object); dispatch(actions.objects.create(object));
this.props.history.push(`/${this.props.project.fullName}/${object._id}`); navigate(`/${project.fullName}/${object._id}`);
}, (err) => { }, (err) => {
toast.error(err.message); toast.error(err.message);
this.setState({ isUploading: false }); setIsUploading(false);
this.props.onError && this.props.onError(err); onError && onError(err);
}); });
} };
render() {
const { project, fluid } = this.props;
const { isUploading } = this.state;
return ( return (
<Dropdown <Dropdown
fluid={!!fluid} fluid={!!fluid}
@ -46,35 +42,27 @@ class ObjectCreator extends Component {
trigger=<Button color="teal" fluid content="Add something" icon="plus" loading={isUploading} /> trigger=<Button color="teal" fluid content="Add something" icon="plus" loading={isUploading} />
> >
<Dropdown.Menu> <Dropdown.Menu>
<Dropdown.Item onClick={this.createNewPattern} icon="pencil" content="Create a new weaving pattern" /> <Dropdown.Item onClick={createNewPattern} icon="pencil" content="Create a new weaving pattern" />
<FileChooser <FileChooser
forType="project" forType="project"
for={project} forObject={project}
trigger=<Dropdown.Item icon="upload" content="Import a WIF file" /> trigger=<Dropdown.Item icon="upload" content="Import a WIF file" />
accept=".wif" accept=".wif"
onUploadStart={e => this.setState({ isUploading: true })} onUploadStart={e => setIsUploading(true)}
onUploadFinish={e => this.setState({ isUploading: false })} onUploadFinish={e => setIsUploading(false)}
onComplete={this.fileUploaded} onComplete={fileUploaded}
/> />
<FileChooser <FileChooser
forType="project" forType="project"
for={project} forObject={project}
trigger=<Dropdown.Item icon="cloud upload" content="Upload an image or a file" /> trigger=<Dropdown.Item icon="cloud upload" content="Upload an image or a file" />
onUploadStart={e => this.setState({ isUploading: true })} onUploadStart={e => setIsUploading(true)}
onUploadFinish={e => this.setState({ isUploading: false })} onUploadFinish={e => setIsUploading(false)}
onComplete={this.fileUploaded} onComplete={fileUploaded}
/> />
</Dropdown.Menu> </Dropdown.Menu>
</Dropdown> </Dropdown>
); );
}
} }
const mapDispatchToProps = dispatch => ({
onCreateObject: name => dispatch(actions.objects.create(name)),
onSelectObject: id => dispatch(actions.objects.select(id)),
});
const ObjectCreatorContainer = withRouter(connect(
null, mapDispatchToProps,
)(ObjectCreator));
export default ObjectCreatorContainer; export default ObjectCreator;

View File

@ -1,16 +1,20 @@
import React, { Component } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import pell from 'pell'; import pell from 'pell';
class RichText extends Component { function RichText({ value, onChange }) {
ensureHTTP(url) { const [completedInit, setCompletedInit] = useState(false);
const textboxRef = useRef();
const ensureHTTP = (url) => {
if (url.trim().toLowerCase().indexOf('http') !== 0) return `http://${url}`; if (url.trim().toLowerCase().indexOf('http') !== 0) return `http://${url}`;
return url; return url;
} };
componentDidMount() { useEffect(() => {
if (completedInit) return;
pell.init({ pell.init({
element: this.refs.textbox, element: textboxRef.current,
onChange: this.props.onChange, onChange: onChange,
actions: [ actions: [
{ {
icon: '<i class="italic icon"></i>', icon: '<i class="italic icon"></i>',
@ -54,7 +58,7 @@ class RichText extends Component {
title: 'Insert an image using a direct URL link', title: 'Insert an image using a direct URL link',
result: () => { result: () => {
const url = window.prompt('Enter the image URL'); // eslint-disable-line no-alert const url = window.prompt('Enter the image URL'); // eslint-disable-line no-alert
if (url) pell.exec('insertImage', this.ensureHTTP(url)); if (url) pell.exec('insertImage', ensureHTTP(url));
}, },
}, },
{ {
@ -63,17 +67,16 @@ class RichText extends Component {
title: 'Add hyperlink to selected text', title: 'Add hyperlink to selected text',
result: () => { result: () => {
const url = window.prompt('Enter the link URL'); // eslint-disable-line no-alert const url = window.prompt('Enter the link URL'); // eslint-disable-line no-alert
if (url) pell.exec('createLink', this.ensureHTTP(url)); if (url) pell.exec('createLink', ensureHTTP(url));
}, },
}, },
], ],
defaultParagraphSeparator: 'p', defaultParagraphSeparator: 'p',
}).content.innerHTML = this.props.value || ''; }).content.innerHTML = value || '';
} setCompletedInit(true);
}, [completedInit, value, onChange]);
render() { return <div ref={textboxRef} />;
return <div ref="textbox" />;
}
} }
export default RichText; export default RichText;

View File

@ -1,16 +1,15 @@
import React, { Component } from 'react'; import React from 'react';
import sanitizeHtml from 'sanitize-html'; import sanitizeHtml from 'sanitize-html';
class RichTextViewer extends Component { function RichTextViewer({ content, style, className }) {
render() { if (!content) return null;
if (!this.props.content) return null;
return ( return (
<p <p
style={Object.assign({}, { breakLines: 'pre-line'}, this.props.style)} style={Object.assign({}, { breakLines: 'pre-line'}, style)}
className={this.props.className} className={className}
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: __html:
sanitizeHtml(this.props.content, { sanitizeHtml(content, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'u']), allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'u']),
allowedAttributes: { allowedAttributes: {
a: ['href', 'name', 'target'], a: ['href', 'name', 'target'],
@ -20,7 +19,6 @@ class RichTextViewer extends Component {
}} }}
/> />
); );
}
} }
export default RichTextViewer; export default RichTextViewer;

View File

@ -1,58 +1,55 @@
import React, { Component } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { Menu, Popup, Input } from 'semantic-ui-react'; import { Menu, Popup, Input } from 'semantic-ui-react';
import { connect } from 'react-redux'
import api from 'api'; import api from 'api';
class UserSearch extends Component { function UserSearch({ onSelected, fluid }) {
const [open, setOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [oldSearchTerm, setOldSearchTerm] = useState('');
const [searchResults, setSearchResults] = useState();
const [searching, setSearching] = useState(false);
constructor(props) { const updateSearchTerm = e => {
super(props); setSearching(true);
this.state = { open: false, searchTerm: '', oldSearchTerm: '', searchResults: null, searching: false }; setSearchTerm(e.target.value);
} };
componentDidMount() {
this.searcher = setInterval(() => {
if (this.state.searchTerm !== this.state.oldSearchTerm) this.search();
}, 500);
}
componentWillUnmount() {
clearInterval(this.searcher);
}
updateSearchTerm = e => { const search = useCallback(() => {
this.setState({ searching: true, searchTerm: e.target.value }) if (searchTerm === oldSearchTerm) return;
} setOpen(false);
search = () => { setSearching(true);
this.setState({ open: false, searching: true, oldSearchTerm: this.state.searchTerm }); setOldSearchTerm(searchTerm);
if (!this.state.searchTerm) return this.setState({ searching: false }); if (!searchTerm) return setSearching(false);
api.search.users(this.state.searchTerm, searchResults => this.setState({ open: true, searching: false, searchResults }), err => this.setState({searching: false})); api.search.users(searchTerm, searchResults => {
} setOpen(true);
setSearching(false);
setSearchResults(searchResults);
}, () => setSearching(false));
}, [oldSearchTerm, searchTerm]);
useEffect(() => {
const searcher = setInterval(() => search(), 500);
return () => clearInterval(searcher);
}, [search]);
inputClicked = () => { const inputClicked = () => {
if (this.state.searchResults) this.setState({ open: true }); if (searchResults) setOpen(true);
} };
onSelected = user => { const choose = user => {
this.props.onSelected && this.props.onSelected(user); onSelected && onSelected(user);
} };
render() {
const { searchResults, searching, open } = this.state;
const { fluid } = this.props;
return ( return (
<Popup hoverable position='bottom left' open={open} onClose={e => this.setState({ open: false })} <Popup hoverable position='bottom left' open={open} onClose={e => setOpen(false)}
trigger={<Input fluid={fluid} icon='search' iconPosition='left' placeholder='Search for a username...' onChange={this.updateSearchTerm} loading={searching} onClick={this.inputClicked}/>} trigger={<Input fluid={fluid} icon='search' iconPosition='left' placeholder='Search for a username...' onChange={updateSearchTerm} loading={searching} onClick={inputClicked}/>}
content={( content={(
<Menu borderless vertical> <Menu borderless vertical>
{searchResults && searchResults.map(r => {searchResults && searchResults.map(r =>
<Menu.Item key={r._id} as='a' icon='user' content={r.username} onClick={e => this.onSelected(r)} image={r.avatarUrl}/> <Menu.Item key={r._id} as='a' icon='user' content={r.username} onClick={e => choose(r)} image={r.avatarUrl}/>
)} )}
</Menu> </Menu>
)} )}
/> />
); );
}
} }
export default UserSearch;
const UserSearchContainer = connect(null, null)(UserSearch);
export default UserSearchContainer;

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Loader, Divider, Button, Message, Container, Segment, Grid, Card, Icon, List } from 'semantic-ui-react'; import { Loader, Divider, Button, Message, Container, Segment, Grid, Card, Icon, List } from 'semantic-ui-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { connect } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import actions from 'actions'; import actions from 'actions';
import api from 'api'; import api from 'api';
@ -13,29 +13,37 @@ import HelpLink from 'components/includes/HelpLink';
import ProjectCard from 'components/includes/ProjectCard'; import ProjectCard from 'components/includes/ProjectCard';
import Tour from 'components/includes/Tour'; import Tour from 'components/includes/Tour';
function Home({ user, groups, projects, invitations, loadingProjects, onReceiveProjects, onReceiveInvitations, onDismissInvitation, onReceiveGroup, onJoinGroup }) { function Home() {
const [runJoyride, setRunJoyride] = useState(false); const [runJoyride, setRunJoyride] = useState(false);
const dispatch = useDispatch();
const { user, projects, groups, invitations, loadingProjects } = useSelector(state => {
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
const invitations = state.invitations.invitations.filter(i => i.recipient === user?._id);
const projects = state.projects.projects.filter(p => p.user === user?._id);
return { user, projects, groups, invitations, loadingProjects: state.projects.loading };
});
useEffect(() => { useEffect(() => {
api.invitations.get(({ invitations, sentInvitations}) => { api.invitations.get(({ invitations, sentInvitations}) => {
onReceiveInvitations(invitations.concat(sentInvitations)); dispatch(actions.invitations.receiveInvitations(invitations.concat(sentInvitations)));
}); });
}, [onReceiveInvitations]); }, [dispatch]);
useEffect(() => { useEffect(() => {
api.users.getMyProjects(onReceiveProjects); api.users.getMyProjects(p => dispatch(actions.projects.receiveProjects(p)));
setTimeout(() => setTimeout(() =>
setRunJoyride(true), 2000); setRunJoyride(true), 2000);
}, [onReceiveProjects]); }, [dispatch]);
const declineInvite = (invite) => { const declineInvite = (invite) => {
api.invitations.decline(invite._id, () => onDismissInvitation(invite._id), err => toast.error(err.message)); api.invitations.decline(invite._id, () => dispatch(actions.invitations.dismiss(invite._id)), err => toast.error(err.message));
} }
const acceptInvite = (invite) => { const acceptInvite = (invite) => {
api.invitations.accept(invite._id, (result) => { api.invitations.accept(invite._id, (result) => {
onDismissInvitation(invite._id); dispatch(actions.invitations.dismiss(invite._id));
if (result.group) { if (result.group) {
onReceiveGroup(result.group); dispatch(actions.groups.receiveGroup(result.group));
onJoinGroup(user._id, result.group._id); dispatch(actions.users.joinGroup(user._id, result.group._id));
} }
}, err => toast.error(err.message)); }, err => toast.error(err.message));
} }
@ -170,23 +178,4 @@ function Home({ user, groups, projects, invitations, loadingProjects, onReceiveP
); );
} }
const mapStateToProps = state => { export default Home;
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
const invitations = state.invitations.invitations.filter(i => i.recipient === user?._id);
const projects = state.projects.projects.filter(p => p.user === user?._id);
return { user, projects, groups, invitations, loadingProjects: state.projects.loading };
}
const mapDispatchToProps = dispatch => ({
onReceiveGroup: group => dispatch(actions.groups.receiveGroup(group)),
onJoinGroup: (userId, groupId) => dispatch(actions.users.joinGroup(userId, groupId)),
onReceiveProjects: p => dispatch(actions.projects.receiveProjects(p)),
onDismissInvitation: id => dispatch(actions.invitations.dismiss(id)),
onReceiveInvitations: i => dispatch(actions.invitations.receiveInvitations(i)),
});
const HomeContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(Home);
export default HomeContainer;

View File

@ -1,7 +1,7 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Loader, Button, Segment } from 'semantic-ui-react'; import { Loader, Button, Segment } from 'semantic-ui-react';
import { withRouter } from 'react-router-dom'; import { useSelector, useDispatch } from 'react-redux';
import { connect } from 'react-redux'; import { useParams } from 'react-router-dom';
import utils from 'utils/utils.js'; import utils from 'utils/utils.js';
import actions from 'actions'; import actions from 'actions';
import api from 'api'; import api from 'api';
@ -10,16 +10,29 @@ import FeedMessage from 'components/includes/FeedMessage';
import NewFeedMessage from 'components/includes/NewFeedMessage'; import NewFeedMessage from 'components/includes/NewFeedMessage';
import MessagesImage from 'images/messages.png'; import MessagesImage from 'images/messages.png';
function Feed({ user, group, entries, onReceiveEntry, onDeleteEntry, newEntry, onJoinGroup, replyingTo, updateReplyingTo, loadingEntries, updateLoadingEntries, match }) { function Feed() {
const dispatch = useDispatch();
const { id } = useParams();
const { user, group, entries, replyingTo, loadingEntries } = useSelector(state => {
const group = state.groups.groups.filter(g => g._id === id)[0];
const entries = state.groups.entries.filter(e => e.group === id).sort((a, b) => {
const aDate = new Date(a.createdAt);
const bDate = new Date(b.createdAt);
return aDate < bDate;
});
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const { replyingTo } = state.posts;
return { user, group, entries, replyingTo, loadingEntries: state.groups.loadingEntries };
});
const myGroups = user?.groups || []; const myGroups = user?.groups || [];
useEffect(() => { useEffect(() => {
updateLoadingEntries(true); dispatch(actions.groups.updateLoadingEntries(true));
api.groups.getEntries(match.params.id, entries => { api.groups.getEntries(id, entries => {
updateLoadingEntries(false); dispatch(actions.groups.updateLoadingEntries(false));
entries.forEach(e => onReceiveEntry(e)); entries.forEach(e => dispatch(actions.groups.receiveEntry(e)));
}); });
}, [match.params.id, myGroups.length, onReceiveEntry, updateLoadingEntries]); }, [dispatch, id, myGroups.length]);
const mainEntries = entries && entries.filter(e => !e.inReplyTo); const mainEntries = entries && entries.filter(e => !e.inReplyTo);
@ -27,9 +40,9 @@ function Feed({ user, group, entries, onReceiveEntry, onDeleteEntry, newEntry, o
<div> <div>
{utils.isInGroup(user, group._id) && <> {utils.isInGroup(user, group._id) && <>
{replyingTo ? {replyingTo ?
<Button style={{marginBottom: 20}} color='teal' content='Write a new post' onClick={() => updateReplyingTo(null)} /> <Button style={{marginBottom: 20}} color='teal' content='Write a new post' onClick={() => dispatch(actions.posts.updateReplyingTo(null))} />
: :
<NewFeedMessage user={user} group={group} forType='group' onPosted={onReceiveEntry}/> <NewFeedMessage user={user} group={group} forType='group' onPosted={e => dispatch(actions.groups.receiveEntry(e))}/>
} }
{loadingEntries && !mainEntries?.length && {loadingEntries && !mainEntries?.length &&
<div style={{textAlign:'center'}}> <div style={{textAlign:'center'}}>
@ -45,35 +58,11 @@ function Feed({ user, group, entries, onReceiveEntry, onDeleteEntry, newEntry, o
</Segment> </Segment>
} }
{mainEntries?.map(e => {mainEntries?.map(e =>
<FeedMessage key={e._id} user={user} forType='group' group={group} post={e} replies={entries.filter(r => r.inReplyTo === e._id)} onDeleted={onDeleteEntry} onReplyPosted={onReceiveEntry} /> <FeedMessage key={e._id} user={user} forType='group' group={group} post={e} replies={entries.filter(r => r.inReplyTo === e._id)} onDeleted={id => dispatch(actions.groups.deleteEntry(id))} onReplyPosted={e => dispatch(actions.groups.receiveEntry(e))} />
)} )}
</>} </>}
</div> </div>
) )
} }
const mapStateToProps = (state, ownProps) => { export default Feed;
const { id } = ownProps.match.params;
const group = state.groups.groups.filter(g => g._id === id)[0];
const entries = state.groups.entries.filter(e => e.group === id).sort((a, b) => {
const aDate = new Date(a.createdAt);
const bDate = new Date(b.createdAt);
return aDate < bDate;
});
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const projects = state.projects.projects.filter(p => p.user === (user && user._id));
const { replyingTo } = state.posts;
return { user, group, loading: state.groups.loading, errorMessage: state.groups.errorMessage, projects, newEntry: state.groups.newEntry, entries, replyingTo, loadingEntries: state.groups.loadingEntries };
};
const mapDispatchToProps = dispatch => ({
onReceiveEntry: entry => dispatch(actions.groups.receiveEntry(entry)),
onDeleteEntry: id => dispatch(actions.groups.deleteEntry(id)),
onJoinGroup: (userId, groupId) => dispatch(actions.users.joinGroup(userId, groupId)),
updateReplyingTo: entryId => dispatch(actions.posts.updateReplyingTo(entryId)),
updateLoadingEntries: l => dispatch(actions.groups.updateLoadingEntries(l)),
});
const FeedContainer = withRouter(connect(
mapStateToProps, mapDispatchToProps,
)(Feed));
export default FeedContainer;

View File

@ -1,8 +1,8 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Segment, Loader, Menu, Message, Container, Button, Icon, Grid, Card } from 'semantic-ui-react'; import { Segment, Loader, Menu, Message, Container, Button, Icon, Grid, Card } from 'semantic-ui-react';
import { Switch, Route, Link, withRouter } from 'react-router-dom'; import { Outlet, Link, useParams } from 'react-router-dom';
import { connect } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import utils from 'utils/utils.js'; import utils from 'utils/utils.js';
import actions from 'actions'; import actions from 'actions';
@ -10,53 +10,62 @@ import api from 'api';
import UserChip from 'components/includes/UserChip'; import UserChip from 'components/includes/UserChip';
import HelpLink from 'components/includes/HelpLink'; import HelpLink from 'components/includes/HelpLink';
import Feed from './Feed.js';
import Members from './Members.js';
import Projects from './Projects.js';
import Settings from './Settings.js';
function Group({ user, group, requests, myRequests, loading, errorMessage, onReceiveGroup, onRequest, onRequestFailed, onJoinGroup, onLeaveGroup, onSubsUpdated, onReceiveInvitations, invitations, onDismissInvitation, match }) { function Group() {
const { id } = useParams();
const dispatch = useDispatch();
const { user, group, loading, errorMessage, requests, myRequests, invitations } = useSelector(state => {
let group;
state.groups.groups.forEach((g) => {
if (g._id === id) group = g;
});
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const requests = state.invitations.invitations.filter(i => i.recipientGroup === group?._id);
const myRequests = state.invitations.invitations.filter(i => i.recipientGroup === group?._id && i.user === user?._id);
const invitations = state.invitations.invitations.filter(i => i.recipient === user?._id && i.typeId === group?._id);
return { user, group, loading: state.groups.loading, errorMessage: state.groups.errorMessage, requests, myRequests, invitations };
});
useEffect(() => { useEffect(() => {
onRequest(); dispatch(actions.groups.request());
api.groups.get(match.params.id, onReceiveGroup, onRequestFailed); api.groups.get(id, g => dispatch(actions.groups.receiveGroup(g)), err => dispatch(actions.groups.requestFailed(err)));
}, [match.params.id, onRequest, onReceiveGroup, onRequestFailed]); }, [dispatch, id]);
const join = () => { const join = () => {
if (!user) return toast.warning('Please login or sign-up first'); if (!user) return toast.warning('Please login or sign-up first');
api.groups.createMember(match.params.id, user._id, () => { api.groups.createMember(id, user._id, () => {
onJoinGroup(user._id, match.params.id); dispatch(actions.users.joingGroup(user._id, id));
}, err => toast.error(err.message)); }, err => toast.error(err.message));
} }
const leave = () => { const leave = () => {
utils.confirm('Really leave this group?', 'You may not be able to re-join the group yourself.').then(() => { utils.confirm('Really leave this group?', 'You may not be able to re-join the group yourself.').then(() => {
api.groups.deleteMember(match.params.id, user._id, () => { api.groups.deleteMember(id, user._id, () => {
onLeaveGroup(user._id, match.params.id); dispatch(actions.users.leaveGroup(user._id, id));
}, err => toast.error(err.message)); }, err => toast.error(err.message));
}, () => {}); }, () => {});
} }
const toggleEmailSub = (key, enable) => { const toggleEmailSub = (key, enable) => {
if (enable) if (enable)
api.users.createEmailSubscription(user.username, key, ({ subscriptions }) => onSubsUpdated(user._id, subscriptions), err => toast.error(err.message)); api.users.createEmailSubscription(user.username, key, ({ subscriptions }) => dispatch(actions.users.updateSubscriptions(user._id, subscriptions)), err => toast.error(err.message));
else else
api.users.deleteEmailSubscription(user.username, key, ({ subscriptions }) => onSubsUpdated(user._id, subscriptions), err => toast.error(err.message)); api.users.deleteEmailSubscription(user.username, key, ({ subscriptions }) => dispatch(actions.users.updateSubscriptions(user._id, subscriptions)), err => toast.error(err.message));
} }
const requestToJoin = () => { const requestToJoin = () => {
api.groups.createJoinRequest(group._id, invitation => { api.groups.createJoinRequest(group._id, invitation => {
toast.success('Request to join sent'); toast.success('Request to join sent');
onReceiveInvitations([invitation]); dispatch(actions.invitations.receiveInvitations([invitation]));
}, err => toast.error(err.message)); }, err => toast.error(err.message));
} }
const declineInvite = (invite) => { const declineInvite = (invite) => {
api.invitations.decline(invite._id, () => onDismissInvitation(invite._id), err => toast.error(err.message)); api.invitations.decline(invite._id, () => dispatch(actions.invitations.dismiss(invite._id)), err => toast.error(err.message));
} }
const acceptInvite = (invite) => { const acceptInvite = (invite) => {
api.invitations.accept(invite._id, (result) => { api.invitations.accept(invite._id, (result) => {
onDismissInvitation(invite._id); dispatch(actions.invitations.dismiss(invite._id));
if (result.group) { if (result.group) {
onJoinGroup(user._id, result.group._id); dispatch(actions.users.joinGroup(user._id, result.group._id));
} }
}, err => toast.error(err.message)); }, err => toast.error(err.message));
} }
@ -168,13 +177,7 @@ function Group({ user, group, requests, myRequests, loading, errorMessage, onRec
} }
</Segment> </Segment>
} }
<Switch> <Outlet />
<Route path='/groups/:id' exact render={() => <Feed group={group} />} />
<Route path='/groups/:id/feed' render={() => <Feed group={group} />} />
<Route path='/groups/:id/members' render={() => <Members group={group} />} />
<Route path='/groups/:id/projects' render={() => <Projects group={group} />} />
<Route path='/groups/:id/settings' render={() => <Settings group={group} />} />
</Switch>
</> </>
: :
<Message>Please login to view or join this group.</Message> <Message>Please login to view or join this group.</Message>
@ -187,31 +190,4 @@ function Group({ user, group, requests, myRequests, loading, errorMessage, onRec
); );
} }
const mapStateToProps = (state, ownProps) => { export default Group;
const { id } = ownProps.match.params;
let group;
state.groups.groups.forEach((g) => {
if (g._id === id) group = g;
});
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const requests = state.invitations.invitations.filter(i => i.recipientGroup === group?._id);
const myRequests = state.invitations.invitations.filter(i => i.recipientGroup === group?._id && i.user === user?._id);
const invitations = state.invitations.invitations.filter(i => i.recipient === user?._id && i.typeId === group?._id);
return { user, group, loading: state.groups.loading, errorMessage: state.groups.errorMessage, requests, myRequests, invitations };
};
const mapDispatchToProps = dispatch => ({
onRequest: () => dispatch(actions.groups.request()),
onRequestFailed: err => dispatch(actions.groups.requestFailed(err)),
onReceiveGroup: group => dispatch(actions.groups.receiveGroup(group)),
onUpdateGroup: (id, update) => dispatch(actions.groups.updateGroup(id, update)),
onJoinGroup: (userId, groupId) => dispatch(actions.users.joinGroup(userId, groupId)),
onLeaveGroup: (userId, groupId) => dispatch(actions.users.leaveGroup(userId, groupId)),
onSubsUpdated: (id, subs) => dispatch(actions.users.updateSubscriptions(id, subs)),
onReceiveInvitations: i => dispatch(actions.invitations.receiveInvitations(i)),
onDismissInvitation: id => dispatch(actions.invitations.dismiss(id)),
});
const GroupContainer = withRouter(connect(
mapStateToProps, mapDispatchToProps,
)(Group));
export default GroupContainer;

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { Grid, Table, Button, Input, Label, Header, Loader, Segment, Dropdown, Card } from 'semantic-ui-react'; import { Grid, Table, Button, Input, Label, Header, Loader, Segment, Dropdown, Card } from 'semantic-ui-react';
import { withRouter } from 'react-router-dom'; import { useSelector, useDispatch } from 'react-redux';
import { connect } from 'react-redux'; import { useParams } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import moment from 'moment'; import moment from 'moment';
import utils from 'utils/utils.js'; import utils from 'utils/utils.js';
@ -12,9 +12,19 @@ import UserChip from 'components/includes/UserChip';
import HelpLink from 'components/includes/HelpLink'; import HelpLink from 'components/includes/HelpLink';
import UserSearch from 'components/includes/UserSearch'; import UserSearch from 'components/includes/UserSearch';
function Members({ user, group, members, requests, loading, onReceiveUser, onJoinGroup, onLeaveGroup, onUpdateGroupLoading, onDismissInvitation }) { function Members() {
const [invitations, setInvitations] = useState([]); const [invitations, setInvitations] = useState([]);
const joinLinkRef = useRef(null); const joinLinkRef = useRef(null);
const { id } = useParams();
const dispatch = useDispatch();
const { user, group, members, loading, requests } = useSelector(state => {
const group = state.groups.groups.filter(g => g._id === id)[0];
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const members = state.users.users.filter(u => utils.isInGroup(u, id));
const requests = state.invitations.invitations.filter(i => i.recipientGroup === group?._id);
return { user, group, members, loading: state.groups.loading, requests };
});
useEffect(() => { useEffect(() => {
if (utils.isGroupAdmin(user, group)) { if (utils.isGroupAdmin(user, group)) {
@ -22,15 +32,15 @@ function Members({ user, group, members, requests, loading, onReceiveUser, onJoi
} }
}, [user, group]); }, [user, group]);
useEffect(() => { useEffect(() => {
onUpdateGroupLoading(true); dispatch(actions.users.request(true));
api.groups.getMembers(group._id, members => { api.groups.getMembers(group._id, members => {
members.forEach(onReceiveUser); members.forEach(u => dispatch(actions.users.receive(u)));
onUpdateGroupLoading(false); dispatch(actions.users.request(false));
}, err => { }, err => {
toast.error(err.message); toast.error(err.message);
onUpdateGroupLoading(false); dispatch(actions.users.request(false));
}); });
}, [group, onReceiveUser, onUpdateGroupLoading]); }, [dispatch, group]);
const copyLink = () => { const copyLink = () => {
joinLinkRef.current.select(); joinLinkRef.current.select();
@ -40,7 +50,7 @@ function Members({ user, group, members, requests, loading, onReceiveUser, onJoi
const kickUser = (id) => { const kickUser = (id) => {
utils.confirm('Really kick this user?').then(() => { utils.confirm('Really kick this user?').then(() => {
api.groups.deleteMember(group._id, id, () => { api.groups.deleteMember(group._id, id, () => {
onLeaveGroup(id, group._id); dispatch(actions.users.leaveGroup(id, group._id));
}, err => toast.error(err.message)); }, err => toast.error(err.message));
}, () => {}); }, () => {});
} }
@ -60,14 +70,14 @@ function Members({ user, group, members, requests, loading, onReceiveUser, onJoi
} }
const approveRequest = (invite) => { const approveRequest = (invite) => {
api.invitations.accept(invite._id, (result) => { api.invitations.accept(invite._id, (result) => {
onDismissInvitation(invite._id); dispatch(actions.invitations.dismiss(invite._id))
onReceiveUser(invite.invitedBy); dispatch(actions.users.receive(invite.invitedBy));
onJoinGroup(invite.user, group._id); dispatch(actions.users.joinGroup(invite.user, group._id));
toast.success(`${invite.invitedBy.username} is now a member`); toast.success(`${invite.invitedBy.username} is now a member`);
}, err => toast.error(err.message)); }, err => toast.error(err.message));
} }
const declineRequest = (request) => { const declineRequest = (request) => {
api.invitations.decline(request._id, () => onDismissInvitation(request._id), api.invitations.decline(request._id, () => dispatch(actions.invitations.dismiss(request._id)),
err => toast.error(err.message)); err => toast.error(err.message));
} }
@ -164,23 +174,4 @@ function Members({ user, group, members, requests, loading, onReceiveUser, onJoi
) )
} }
const mapStateToProps = (state, ownProps) => { export default Members;
const { id } = ownProps.match.params;
const group = state.groups.groups.filter(g => g._id === id)[0];
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const members = state.users.users.filter(u => utils.isInGroup(u, id));
const requests = state.invitations.invitations.filter(i => i.recipientGroup === group?._id);
return { user, group, members, loading: state.groups.loading, errorMessage: state.groups.errorMessage, requests };
};
const mapDispatchToProps = dispatch => ({
onDismissInvitation: id => dispatch(actions.invitations.dismiss(id)),
onReceiveUser: user => dispatch(actions.users.receive(user)),
onJoinGroup: (userId, groupId) => dispatch(actions.users.joinGroup(userId, groupId)),
onLeaveGroup: (userId, groupId) => dispatch(actions.users.leaveGroup(userId, groupId)),
onUpdateGroupLoading: l => dispatch(actions.users.request(l)),
});
const MembersContainer = withRouter(connect(
mapStateToProps, mapDispatchToProps,
)(Members));
export default MembersContainer;

View File

@ -2,23 +2,30 @@ import React from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Icon, Form, Grid, Input, Checkbox, Button, Divider } from 'semantic-ui-react'; import { Icon, Form, Grid, Input, Checkbox, Button, Divider } from 'semantic-ui-react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { withRouter } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { connect } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import actions from 'actions'; import actions from 'actions';
import api from 'api'; import api from 'api';
import HelpLink from 'components/includes/HelpLink'; import HelpLink from 'components/includes/HelpLink';
function NewGroup({ user, newGroupName, newGroupDescription, newGroupClosed, onUpdateGroupName, onUpdateGroupDescription, onUpdateGroupClosed, onReceiveGroup, loading, onUpdateGroupLoading, history }) { function NewGroup() {
const navigate = useNavigate();
const dispatch = useDispatch();
const { newGroupName, newGroupDescription, newGroupClosed, loading } = useSelector(state => {
const { loading, newGroupName, newGroupDescription, newGroupClosed } = state.groups;
return { newGroupName, newGroupDescription, newGroupClosed, loading };
});
const createGroup = () => { const createGroup = () => {
onUpdateGroupLoading(true); dispatch(actions.groups.request(true));
api.groups.create({ name: newGroupName, description: newGroupDescription, closed: newGroupClosed }, (group) => { api.groups.create({ name: newGroupName, description: newGroupDescription, closed: newGroupClosed }, (group) => {
onReceiveGroup(group); dispatch(actions.groups.receiveGroup(group));
onUpdateGroupLoading(false); dispatch(actions.groups.request(false));
history.push(`/groups/${group._id}/members`); navigate(`/groups/${group._id}/members`);
}, (err) => { }, (err) => {
onUpdateGroupLoading(false); dispatch(actions.groups.request(false));
toast.error(err.message); toast.error(err.message);
}); });
} }
@ -38,12 +45,12 @@ function NewGroup({ user, newGroupName, newGroupDescription, newGroupClosed, onU
<h3>About your group</h3> <h3>About your group</h3>
<p>Give your group a short name. You can change this whenever you like.</p> <p>Give your group a short name. You can change this whenever you like.</p>
<Input autoFocus type="text" fluid onChange={e => onUpdateGroupName(e.target.value)} value={newGroupName} /> <Input autoFocus type="text" fluid onChange={e => dispatch(actions.groups.updateNewGroupName(e.target.value))} value={newGroupName} />
<Divider hidden /> <Divider hidden />
<p><strong>Optional:</strong> Write a short description to describe your group to others.</p> <p><strong>Optional:</strong> Write a short description to describe your group to others.</p>
<Form><Form.TextArea placeholder="Group description (optional)..." value={newGroupDescription} onChange={e => onUpdateGroupDescription(e.target.value)} /></Form> <Form><Form.TextArea placeholder="Group description (optional)..." value={newGroupDescription} onChange={e => dispatch(actions.groups.updateNewGroupDescription(e.target.value))} /></Form>
<Checkbox style={{marginTop: 40}} toggle checked={newGroupClosed} label="Make this group a closed group" onChange={(e,c) => onUpdateGroupClosed(c.checked)} /> <Checkbox style={{marginTop: 40}} toggle checked={newGroupClosed} label="Make this group a closed group" onChange={(e,c) => dispatch(actions.groups.updateNewGroupClosed(c.checked))} />
<div style={{ marginLeft: 63, color: 'rgb(150,150,150)' }}> <div style={{ marginLeft: 63, color: 'rgb(150,150,150)' }}>
<p>Closed groups are more restrictive and new members must be invited or approved to join. Members can join non-closed groups without being invited.</p> <p>Closed groups are more restrictive and new members must be invited or approved to join. Members can join non-closed groups without being invited.</p>
</div> </div>
@ -52,7 +59,7 @@ function NewGroup({ user, newGroupName, newGroupDescription, newGroupClosed, onU
<p>You can add and invite others to join your group after you've created it.</p> <p>You can add and invite others to join your group after you've created it.</p>
<div style={{textAlign: 'right'}}> <div style={{textAlign: 'right'}}>
<Button basic onClick={history.goBack}>Cancel</Button> <Button basic onClick={() => navigate(-1)}>Cancel</Button>
<Button color="teal" icon="check" content="Create group" onClick={createGroup} loading={loading} /> <Button color="teal" icon="check" content="Create group" onClick={createGroup} loading={loading} />
</div> </div>
</Grid.Column> </Grid.Column>
@ -60,19 +67,4 @@ function NewGroup({ user, newGroupName, newGroupDescription, newGroupClosed, onU
); );
} }
const mapStateToProps = state => { export default NewGroup;
const { loading, newGroupName, newGroupDescription, newGroupClosed } = state.groups;
return { user: state.users.users.filter(u => state.auth.currentUserId === u._id)[0], newGroupName, newGroupDescription, newGroupClosed, loading };
};
const mapDispatchToProps = dispatch => ({
onUpdateGroupLoading: l => dispatch(actions.groups.request(l)),
onReceiveGroup: group => dispatch(actions.groups.receiveGroup(group)),
onUpdateGroupName: n => dispatch(actions.groups.updateNewGroupName(n)),
onUpdateGroupDescription: d => dispatch(actions.groups.updateNewGroupDescription(d)),
onUpdateGroupClosed: c => dispatch(actions.groups.updateNewGroupClosed(c)),
});
const NewGroupContainer = withRouter(connect(
mapStateToProps, mapDispatchToProps,
)(NewGroup));
export default NewGroupContainer;

View File

@ -1,16 +1,29 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Input, Divider, Loader, Segment, Card, Dropdown, Button } from 'semantic-ui-react'; import { Input, Divider, Loader, Segment, Card, Dropdown, Button } from 'semantic-ui-react';
import { withRouter } from 'react-router-dom'; import { useSelector, useDispatch } from 'react-redux';
import { connect } from 'react-redux'; import { useParams } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import actions from 'actions'; import actions from 'actions';
import api from 'api'; import api from 'api';
import ProjectCard from 'components/includes/ProjectCard'; import ProjectCard from 'components/includes/ProjectCard';
function Projects({ group, myProjects, onReceiveProject, projectFilter, updateProjectFilter }) { function Projects() {
const [loadingProjects, setLoadingProjects] = useState(false); const [loadingProjects, setLoadingProjects] = useState(false);
const [projects, setProjects] = useState([]); const [projects, setProjects] = useState([]);
const dispatch = useDispatch();
const { id } = useParams();
const { group, myProjects, projectFilter } = useSelector(state => {
let group;
state.groups.groups.forEach((g) => {
if (g._id === id) group = g;
});
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const myProjects = state.projects.projects.filter(p => p.user === user?._id);
const projectFilter = state.groups.projectFilter;
return { group, myProjects, projectFilter };
});
useEffect(() => { useEffect(() => {
setLoadingProjects(true); setLoadingProjects(true);
@ -29,7 +42,7 @@ function Projects({ group, myProjects, onReceiveProject, projectFilter, updatePr
if (index > -1) groupVisibility.splice(index, 1); if (index > -1) groupVisibility.splice(index, 1);
else groupVisibility.push(group._id); else groupVisibility.push(group._id);
api.projects.update(project.fullName, { groupVisibility }, updatedProject => { api.projects.update(project.fullName, { groupVisibility }, updatedProject => {
onReceiveProject(updatedProject); dispatch(actions.projects.receiveProject(updatedProject));
const existingIndex = projects.map(p => p._id).indexOf(updatedProject._id); const existingIndex = projects.map(p => p._id).indexOf(updatedProject._id);
const newProjects = Object.assign([], projects); const newProjects = Object.assign([], projects);
if (index > -1 && existingIndex > -1) newProjects.splice(existingIndex, 1); if (index > -1 && existingIndex > -1) newProjects.splice(existingIndex, 1);
@ -66,7 +79,7 @@ function Projects({ group, myProjects, onReceiveProject, projectFilter, updatePr
<AddProject style={{float:'right'}} /> <AddProject style={{float:'right'}} />
</>} </>}
{projects?.length > 0 && {projects?.length > 0 &&
<Input autoFocus style={{float:'right', marginRight: 5}} size='small' icon='search' value={projectFilter} onChange={e => updateProjectFilter(e.target.value)} placeholder='Filter projects...' /> <Input autoFocus style={{float:'right', marginRight: 5}} size='small' icon='search' value={projectFilter} onChange={e => dispatch(actions.groups.updateProjectFilter(e.target.value))} placeholder='Filter projects...' />
} }
<Divider hidden clearing /> <Divider hidden clearing />
@ -88,23 +101,4 @@ function Projects({ group, myProjects, onReceiveProject, projectFilter, updatePr
) )
} }
const mapStateToProps = (state, ownProps) => { export default Projects;
const { id } = ownProps.match.params;
let group;
state.groups.groups.forEach((g) => {
if (g._id === id) group = g;
});
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const myProjects = state.projects.projects.filter(p => p.user === user?._id);
const projectFilter = state.groups.projectFilter;
return { user, group, myProjects, projectFilter, loading: state.groups.loading, errorMessage: state.groups.errorMessage };
};
const mapDispatchToProps = dispatch => ({
onReceiveProject: project => dispatch(actions.projects.receiveProject(project)),
updateProjectFilter: f => dispatch(actions.groups.updateProjectFilter(f)),
});
const ProjectsContainer = withRouter(connect(
mapStateToProps, mapDispatchToProps,
)(Projects));
export default ProjectsContainer;

View File

@ -1,31 +1,40 @@
import React from 'react'; import React from 'react';
import { Header, Button, Divider, Segment, Form, } from 'semantic-ui-react'; import { Header, Button, Divider, Segment, Form, } from 'semantic-ui-react';
import { withRouter } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { connect } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import utils from 'utils/utils.js'; import utils from 'utils/utils.js';
import actions from 'actions'; import actions from 'actions';
import api from 'api'; import api from 'api';
function Settings({ user, group, loading, onUpdateGroupLoading, onUpdateGroup, onDeleteGroup, history }) { function Settings() {
const navigate = useNavigate();
const dispatch = useDispatch();
const { id } = useParams();
const { loading, group } = useSelector(state => {
const { loading } = state.groups;
const group = state.groups.groups.filter(g => g._id === id)[0];
return { loading, group };
});
const saveGroup = () => { const saveGroup = () => {
onUpdateGroupLoading(true); dispatch(actions.groups.request(true));
const { _id, name, description, closed } = group; const { _id, name, description, closed } = group;
api.groups.update(_id, { name, description, closed }, () => { api.groups.update(_id, { name, description, closed }, () => {
onUpdateGroupLoading(false); dispatch(actions.groups.request(false));
toast.info('Group updated'); toast.info('Group updated');
}, err => { }, err => {
toast.error(err.message); toast.error(err.message);
onUpdateGroupLoading(false); dispatch(actions.groups.request(false));
}); });
} }
const deleteGroup = () => { const deleteGroup = () => {
utils.confirm('Really delete this group?', 'You\'ll lose all entries in the group feed and anything else you\'ve added to it.').then(() => { utils.confirm('Really delete this group?', 'You\'ll lose all entries in the group feed and anything else you\'ve added to it.').then(() => {
api.groups.delete(group._id, () => { api.groups.delete(group._id, () => {
toast.info('Group deleted'); toast.info('Group deleted');
onDeleteGroup(group._id); dispatch(actions.groups.deleteGroup(group._id));
history.push(`/`); navigate(`/`);
}, err => toast.error(err.message)); }, err => toast.error(err.message));
}, () => {}); }, () => {});
} }
@ -35,9 +44,9 @@ function Settings({ user, group, loading, onUpdateGroupLoading, onUpdateGroup, o
<Segment color='blue'> <Segment color='blue'>
<Header>About this group</Header> <Header>About this group</Header>
<Form> <Form>
<Form.Input label='Group name' value={group.name} onChange={e => onUpdateGroup(group._id, { name: e.target.value })} /> <Form.Input label='Group name' value={group.name} onChange={e => dispatch(actions.groups.updateGroup(group._id, { name: e.target.value }))} />
<Form.TextArea label='Group description' value={group.description} onChange={e => onUpdateGroup(group._id, { description: e.target.value })}/> <Form.TextArea label='Group description' value={group.description} onChange={e => dispatch(actions.groups.updateGroup(group._id, { description: e.target.value }))}/>
<Form.Checkbox toggle checked={group.closed} label="Closed group" onChange={(e,c) => onUpdateGroup(group._id, { closed: c.checked })} /> <Form.Checkbox toggle checked={group.closed} label="Closed group" onChange={(e,c) => dispatch(actions.groups.updateGroup(group._id, { closed: c.checked }))} />
<div style={{ marginLeft: 63, color: 'rgb(150,150,150)' }}> <div style={{ marginLeft: 63, color: 'rgb(150,150,150)' }}>
<p>Closed groups are more restrictive and new members must be invited or approved to join. Members can join non-closed groups without being invited.</p> <p>Closed groups are more restrictive and new members must be invited or approved to join. Members can join non-closed groups without being invited.</p>
</div> </div>
@ -56,17 +65,4 @@ function Settings({ user, group, loading, onUpdateGroupLoading, onUpdateGroup, o
) )
} }
const mapStateToProps = state => { export default Settings;
const { loading } = state.groups;
return { loading };
};
const mapDispatchToProps = dispatch => ({
onUpdateGroup: (id, update) => dispatch(actions.groups.updateGroup(id, update)),
onUpdateGroupLoading: l => dispatch(actions.groups.request(l)),
onDeleteGroup: id => dispatch(actions.groups.deleteGroup(id)),
});
const SettingsContainer = withRouter(connect(
mapStateToProps, mapDispatchToProps,
)(Settings));
export default SettingsContainer;

View File

@ -1,9 +1,9 @@
import React, { Component } from 'react'; import React, { useState } from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Icon, Form, Message, Grid, Input, Button, Divider } from 'semantic-ui-react'; import { Icon, Form, Message, Grid, Input, Button, Divider } from 'semantic-ui-react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { withRouter } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { connect } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import utils from 'utils/utils.js'; import utils from 'utils/utils.js';
import actions from 'actions'; import actions from 'actions';
import api from 'api'; import api from 'api';
@ -11,44 +11,48 @@ import api from 'api';
import UserChip from 'components/includes/UserChip'; import UserChip from 'components/includes/UserChip';
import HelpLink from 'components/includes/HelpLink'; import HelpLink from 'components/includes/HelpLink';
class NewProject extends Component { function NewProject() {
constructor(props) { const [name, setName] = useState('My new project');
super(props); const [description, setDescription] = useState('');
this.state = { const [visibility, setVisibility] = useState('public');
name: 'My new project', description: '', visibility: 'public', openSource: true, groupVisibility: [], error: '', loading: false, const [openSource, setOpenSource] = useState(true);
}; const [groupVisibility, setGroupVisibility] = useState([]);
} const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const dispatch = useDispatch();
updateName = (event) => { const { user, groups } = useSelector(state => {
this.setState({ name: event.target.value }); const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
} const groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
return { user, groups };
changeVisibility = (event, r) => {
this.setState({ visibility: r.checked ? 'private' : 'public' });
}
changeOpenSource = (event, c) => {
this.setState({ openSource: c.checked });
}
createProject = () => {
this.setState({ loading: true });
const { name, description, visibility, openSource, groupVisibility } = this.state;
api.projects.create({ name, description, visibility, openSource, groupVisibility }, (project) => {
this.props.onReceiveProject(project);
this.setState({ loading: false });
this.props.history.push(`/${this.props.user.username}/${project.path}`);
}, (err) => {
this.setState({ loading: false });
toast.error(err.message);
}); });
}
render() { const updateName = (event) => {
const { setName(event.target.value);
name, description, visibility, openSource, groupVisibility };
} = this.state;
const { user, groups } = this.props; const changeVisibility = (event, r) => {
setVisibility(r.checked ? 'private' : 'public');
};
const changeOpenSource = (event, c) => {
setOpenSource(c.checked);
};
const createProject = () => {
setLoading(true);
api.projects.create({ name, description, visibility, openSource, groupVisibility }, (project) => {
dispatch(actions.projects.receiveProject(project));
setLoading(false);
navigate(`/${user.username}/${project.path}`);
}, (err) => {
setLoading(false);
toast.error(err.message);
setError(err.message);
});
};
return ( return (
<Grid stackable centered> <Grid stackable centered>
<Helmet title={'Create Project'} /> <Helmet title={'Create Project'} />
@ -66,17 +70,17 @@ Create a new project
<h3>About your project</h3> <h3>About your project</h3>
<p>Give your project a short name. You can always rename it later on.</p> <p>Give your project a short name. You can always rename it later on.</p>
<Input autoFocus type="text" fluid onChange={this.updateName} value={name} label={<UserChip user={user} style={{ marginRight: 10, marginTop: 4 }} />} /> <Input autoFocus type="text" fluid onChange={updateName} value={name} label={<UserChip user={user} style={{ marginRight: 10, marginTop: 4 }} />} />
<Divider hidden /> <Divider hidden />
<p>Write a project description (optional).</p> <p>Write a project description (optional).</p>
<Form><Form.TextArea placeholder="Project description (optional)..." value={description} onChange={e => this.setState({ description: e.target.value })} /></Form> <Form><Form.TextArea placeholder="Project description (optional)..." value={description} onChange={e => setDescription(e.target.value)} /></Form>
<Divider section /> <Divider section />
<h3>Project visibility</h3> <h3>Project visibility</h3>
<Form> <Form>
<Form.Checkbox label='This is a private project' checked={visibility === 'private'} onChange={this.changeVisibility} /> <Form.Checkbox label='This is a private project' checked={visibility === 'private'} onChange={changeVisibility} />
<p style={{color:'rgb(150,150,150)'}}><Icon name='info' /> Private projects are not searchable and can't be seen by others, unless you give them access.</p> <p style={{color:'rgb(150,150,150)'}}><Icon name='info' /> Private projects are not searchable and can't be seen by others, unless you give them access.</p>
<Form.Checkbox label='This project is "open-source"' checked={openSource} onChange={this.changeOpenSource} /> <Form.Checkbox label='This project is "open-source"' checked={openSource} onChange={changeOpenSource} />
<p style={{color:'rgb(150,150,150)'}}><Icon name='info' /> Open-source projects allow other people to download any pattern source files they contain in WIF format.</p> <p style={{color:'rgb(150,150,150)'}}><Icon name='info' /> Open-source projects allow other people to download any pattern source files they contain in WIF format.</p>
<Divider hidden /> <Divider hidden />
@ -89,7 +93,7 @@ Create a new project
label='Make this project always visible to members of these groups' label='Make this project always visible to members of these groups'
value={groupVisibility} value={groupVisibility}
options={groups.map(g => ({ key: g._id, value: g._id, text: g.name }))} options={groups.map(g => ({ key: g._id, value: g._id, text: g.name }))}
onChange={(e, s) => this.setState({ groupVisibility: s.value })} onChange={(e, s) => setGroupVisibility(s.value)}
/> />
</> </>
} }
@ -97,27 +101,13 @@ Create a new project
<Divider section /> <Divider section />
{this.state.error && <Message color="orange" content={this.state.error} />} {error && <Message color="orange" content={error} />}
<div style={{textAlign:'right'}}> <div style={{textAlign:'right'}}>
<Button basic onClick={this.props.history.goBack}>Cancel</Button> <Button basic onClick={() => navigate(-1)}>Cancel</Button>
<Button color="teal" icon="check" content="Create project" onClick={this.createProject} loading={this.state.loading} /> <Button color="teal" icon="check" content="Create project" onClick={createProject} loading={loading} />
</div> </div>
</Grid.Column> </Grid.Column>
</Grid> </Grid>
); );
}
} }
export default NewProject;
const mapStateToProps = state => {
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
return { user, groups };
};
const mapDispatchToProps = dispatch => ({
onReceiveProject: project => dispatch(actions.projects.receiveProject(project)),
});
const NewProjectContainer = withRouter(connect(
mapStateToProps, mapDispatchToProps,
)(NewProject));
export default NewProjectContainer;

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Segment, Label, Input, Icon, Card, Loader } from 'semantic-ui-react'; import { Segment, Label, Input, Icon, Card, Loader } from 'semantic-ui-react';
import { Link, withRouter } from 'react-router-dom'; import { Link, useParams } from 'react-router-dom';
import { connect } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import utils from 'utils/utils.js'; import utils from 'utils/utils.js';
import actions from 'actions'; import actions from 'actions';
@ -22,17 +22,31 @@ const CompactObject = styled(Link)`
} }
`; `;
function ObjectList({ user, objects, currentObject, project, fullProjectPath, onReceiveObjects, compact }) { function ObjectList({ compact }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [objectFilter, setObjectFilter] = useState(''); const [objectFilter, setObjectFilter] = useState('');
const { username, projectPath, objectId } = useParams();
const dispatch = useDispatch();
const { user, project, objects, currentObject, fullProjectPath } = useSelector(state => {
const project = state.projects.projects.filter(p => p.path === projectPath && p.owner && p.owner.username === username)[0];
const objects = [];
let currentObject;
state.objects.objects.forEach((d) => {
if (d.project === project._id) objects.push(d);
if (d._id === objectId) currentObject = d;
});
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
return { user, project, objects, currentObject, fullProjectPath: `${username}/${projectPath}` };
});
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
api.projects.getObjects(fullProjectPath, projects => { api.projects.getObjects(fullProjectPath, o => {
onReceiveObjects(projects); dispatch(actions.objects.receiveMultiple(o));
setLoading(false); setLoading(false);
}, err => setLoading(false)); }, err => setLoading(false));
}, [fullProjectPath, onReceiveObjects]); }, [fullProjectPath, dispatch]);
let filteredObjects = objects; let filteredObjects = objects;
if (objectFilter) { if (objectFilter) {
@ -157,26 +171,4 @@ function ObjectList({ user, objects, currentObject, project, fullProjectPath, on
); );
} }
export default ObjectList;
const mapStateToProps = (state, ownProps) => {
const { username, projectPath, objectId } = ownProps.match.params;
const project = state.projects.projects.filter(p => p.path === projectPath && p.owner && p.owner.username === username)[0];
const objects = [];
let currentObject;
state.objects.objects.forEach((d) => {
if (d.project === project._id) objects.push(d);
if (d._id === objectId) currentObject = d;
});
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
return { user, project, objects, currentObject, fullProjectPath: `${username}/${projectPath}` };
};
const mapDispatchToProps = dispatch => ({
onReceiveObjects: objects => dispatch(actions.objects.receiveMultiple(objects)),
onEditObject: (id, field, value) => dispatch(actions.objects.update(id, field, value)),
onDeleteObject: id => dispatch(actions.objects.delete(id)),
});
const ObjectListContainer = withRouter(connect(
mapStateToProps, mapDispatchToProps,
)(ObjectList));
export default ObjectListContainer;

View File

@ -1,9 +1,9 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Input, Message, Icon, Button, Dropdown } from 'semantic-ui-react'; import { Input, Message, Icon, Button, Dropdown } from 'semantic-ui-react';
import { connect } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { Link, withRouter } from 'react-router-dom'; import { Link, useNavigate, useParams } from 'react-router-dom';
import moment from 'moment'; import moment from 'moment';
import utils from 'utils/utils.js'; import utils from 'utils/utils.js';
import actions from 'actions'; import actions from 'actions';
@ -15,23 +15,42 @@ import DraftPreview from './objects/DraftPreview';
import NewFeedMessage from 'components/includes/NewFeedMessage'; import NewFeedMessage from 'components/includes/NewFeedMessage';
import FeedMessage from 'components/includes/FeedMessage'; import FeedMessage from 'components/includes/FeedMessage';
function ObjectViewer({ user, myProjects, project, fullProjectPath, onEditObject, onDeleteObject, object, comments, history, onReceiveComment, onDeleteComment }) { function ObjectViewer() {
const [editingName, setEditingName] = useState(false); const [editingName, setEditingName] = useState(false);
const [editingDescription, setEditingDescription] = useState(false); const [editingDescription, setEditingDescription] = useState(false);
const [downloading, setDownloading] = useState(false); const [downloading, setDownloading] = useState(false);
const objectId = object?._id; const navigate = useNavigate();
const { username, projectPath, objectId } = useParams();
const dispatch = useDispatch();
const { user, myProjects, project, fullProjectPath, object, comments } = useSelector(state => {
const project = state.projects.projects.filter(p => p.path === projectPath && p.owner && p.owner.username === username)[0];
const objects = [];
state.objects.objects.forEach((d) => {
if (d.project === project._id) objects.push(d);
});
const object = objects.filter(o => o._id === objectId)[0];
const comments = state.objects.comments?.filter(c => c.object === object?._id).sort((a, b) => {
const aDate = new Date(a.createdAt);
const bDate = new Date(b.createdAt);
return aDate < bDate;
});
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const myProjects = state.projects.projects.filter(p => p.owner?.username === user?.username);
return { user, myProjects, project, fullProjectPath: `${username}/${projectPath}`, object, comments };
});
useEffect(() => { useEffect(() => {
if (!objectId) return; if (!objectId) return;
api.objects.getComments(objectId, data => { api.objects.getComments(objectId, data => {
data.comments.forEach(onReceiveComment); data.comments.forEach(c => dispatch(actions.objects.receiveComment(c)));
}); });
}, [objectId, onReceiveComment]); }, [dispatch, objectId]);
const deleteObject = (object) => { const deleteObject = (object) => {
utils.confirm('Delete object', 'Really delete this object? This cannot be undone.').then(() => { utils.confirm('Delete object', 'Really delete this object? This cannot be undone.').then(() => {
history.push(`/${fullProjectPath}`); navigate(`/${fullProjectPath}`);
api.objects.delete(object._id, () => onDeleteObject(object._id), err => toast.error(err.message)); api.objects.delete(object._id, () => dispatch(actions.objects.delete(object._id)), err => toast.error(err.message));
}, () => {}); }, () => {});
} }
@ -146,7 +165,7 @@ function ObjectViewer({ user, myProjects, project, fullProjectPath, onEditObject
{editingName ? {editingName ?
<div style={{marginBottom: 5}}> <div style={{marginBottom: 5}}>
<Input autoFocus size="small" value={object.name} style={{marginBottom: 10}} <Input autoFocus size="small" value={object.name} style={{marginBottom: 10}}
onChange={e => onEditObject(object._id, 'name', e.target.value)} onChange={e => dispatch(actions.objects.update(object._id, 'name', e.target.value))}
action=<Button icon="check" primary onClick={e => saveObjectName(object)} /> action=<Button icon="check" primary onClick={e => saveObjectName(object)} />
/> />
</div> </div>
@ -166,7 +185,7 @@ function ObjectViewer({ user, myProjects, project, fullProjectPath, onEditObject
<div style={{marginTop: 20, marginBottom: 20, padding: 10, border: '1px solid rgb(240,240,240)'}}> <div style={{marginTop: 20, marginBottom: 20, padding: 10, border: '1px solid rgb(240,240,240)'}}>
{object.type === 'pattern' && {object.type === 'pattern' &&
<div style={{maxHeight: 400, overflowY: 'scroll'}}> <div style={{maxHeight: 400, overflowY: 'scroll'}}>
<DraftPreview object={object} onImageLoaded={i => onEditObject(object._id, 'patternImage', i)}/> <DraftPreview object={object} />
</div> </div>
} }
{object.isImage && {object.isImage &&
@ -182,7 +201,7 @@ function ObjectViewer({ user, myProjects, project, fullProjectPath, onEditObject
{editingDescription ? {editingDescription ?
<div style={{padding: 10, border: '1px solid rgb(230,230,230)'}}> <div style={{padding: 10, border: '1px solid rgb(230,230,230)'}}>
<RichText value={object.description} onChange={t => onEditObject(object._id, 'description', t)} /> <RichText value={object.description} onChange={t => dispatch(actions.objects.update(object._id, 'description', t))} />
<Button primary size="small" icon="check" content="Save description" onClick={e => saveObjectDescription(object)} /> <Button primary size="small" icon="check" content="Save description" onClick={e => saveObjectDescription(object)} />
</div> </div>
: :
@ -202,42 +221,14 @@ function ObjectViewer({ user, myProjects, project, fullProjectPath, onEditObject
} }
</h3> </h3>
{user ? {user ?
<NewFeedMessage noAttachments user={user} forType='object' object={object} onPosted={onReceiveComment} placeholder='Write a new comment...'/> <NewFeedMessage noAttachments user={user} forType='object' object={object} onPosted={c => dispatch(actions.objects.receiveComment(c))} placeholder='Write a new comment...'/>
: <Message size='small'>Please login to write your own comments.</Message> : <Message size='small'>Please login to write your own comments.</Message>
} }
{comments?.map(c => {comments?.map(c =>
<FeedMessage key={c._id} user={user} forType='object' object={object} post={c} replies={[]} onDeleted={onDeleteComment} /> <FeedMessage key={c._id} user={user} forType='object' object={object} post={c} replies={[]} onDeleted={id => dispatch(actions.objects.deleteComment(id))} />
)} )}
</div> </div>
</> </>
); );
} }
export default ObjectViewer;
const mapStateToProps = (state, ownProps) => {
const { username, projectPath, objectId } = ownProps.match.params;
const project = state.projects.projects.filter(p => p.path === projectPath && p.owner && p.owner.username === username)[0];
const objects = [];
state.objects.objects.forEach((d) => {
if (d.project === project._id) objects.push(d);
});
const object = objects.filter(o => o._id === objectId)[0];
const comments = state.objects.comments?.filter(c => c.object === object?._id).sort((a, b) => {
const aDate = new Date(a.createdAt);
const bDate = new Date(b.createdAt);
return aDate < bDate;
});
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const myProjects = state.projects.projects.filter(p => p.owner?.username === user?.username);
return { user, myProjects, project, objects, fullProjectPath: `${username}/${projectPath}`, object, comments };
};
const mapDispatchToProps = dispatch => ({
onEditObject: (id, field, value) => dispatch(actions.objects.update(id, field, value)),
onDeleteObject: id => dispatch(actions.objects.delete(id)),
onReceiveComment: c => dispatch(actions.objects.receiveComment(c)),
onDeleteComment: id => dispatch(actions.objects.deleteComment(id)),
});
const ObjectViewerContainer = withRouter(connect(
mapStateToProps, mapDispatchToProps,
)(ObjectViewer));
export default ObjectViewerContainer;

View File

@ -1,8 +1,8 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Message, Form, TextArea, Container, Button, Icon, Grid, Card } from 'semantic-ui-react'; import { Message, Form, TextArea, Container, Button, Icon, Grid, Card } from 'semantic-ui-react';
import { Switch, Route, Link, withRouter } from 'react-router-dom'; import { Outlet, Link, useParams, useLocation } from 'react-router-dom';
import { connect } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import utils from 'utils/utils.js'; import utils from 'utils/utils.js';
import actions from 'actions'; import actions from 'actions';
import api from 'api'; import api from 'api';
@ -11,24 +11,28 @@ import UserChip from 'components/includes/UserChip';
import HelpLink from 'components/includes/HelpLink'; import HelpLink from 'components/includes/HelpLink';
import ObjectCreator from 'components/includes/ObjectCreator'; import ObjectCreator from 'components/includes/ObjectCreator';
import FormattedMessage from 'components/includes/FormattedMessage'; import FormattedMessage from 'components/includes/FormattedMessage';
import Draft from './objects/Draft.js';
import ObjectList from './ObjectList.js';
import ProjectObjects from './ProjectObjects.js';
import ProjectSettings from './Settings.js';
function Project({ user, project, fullName, errorMessage, editingDescription, onUpdateProject, onReceiveProject, onEditDescription, onRequest, onRequestFailed, match, history }) { function Project() {
const [descriptionExpanded, setDescriptionExpanded] = useState(false); const [descriptionExpanded, setDescriptionExpanded] = useState(false);
const { username, projectPath } = useParams();
const dispatch = useDispatch();
const location = useLocation();
const { user, project, fullName, errorMessage, editingDescription } = useSelector(state => {
const project = state.projects.projects.filter(p => p.path === projectPath && p.owner && p.owner.username === username)[0];
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
return { user, project, fullName: `${username}/${projectPath}`, errorMessage: state.projects.errorMessage, editingDescription: state.projects.editingDescription };
});
useEffect(() => { useEffect(() => {
onRequest(); dispatch(actions.projects.request());
api.projects.get(fullName, onReceiveProject, onRequestFailed); api.projects.get(fullName, p => dispatch(actions.projects.receiveProject(p)), err => dispatch(actions.projects.requestFailed(err)));
}, [user, onRequest, fullName, onReceiveProject, onRequestFailed]); }, [user, dispatch, fullName]);
const wideBody = () => !match.isExact const wideBody = () => !location.pathname.toLowerCase().endsWith(fullName.toLowerCase());
const saveDescription = () => { const saveDescription = () => {
onEditDescription(false); dispatch(actions.projects.editDescription(false));
api.projects.update(fullName, { description: project.description }, onReceiveProject); api.projects.update(fullName, { description: project.description }, p => dispatch(actions.projects.receiveProject(p)));
} }
const getDescription = () => { const getDescription = () => {
@ -50,11 +54,11 @@ function Project({ user, project, fullName, errorMessage, editingDescription, on
{project {project
&& ( && (
<div> <div>
{history.location?.state?.prevPath && {/*history.location?.state?.prevPath &&
<div style={{marginBottom:15}}> <div style={{marginBottom:15}}>
<Button basic secondary onClick={e => history.goBack()} icon='arrow left' content='Go back' /> <Button basic secondary onClick={e => history.goBack()} icon='arrow left' content='Go back' />
</div> </div>
} */}
{wideBody() && project.owner && {wideBody() && project.owner &&
<> <>
@ -85,7 +89,7 @@ function Project({ user, project, fullName, errorMessage, editingDescription, on
{editingDescription {editingDescription
? ( ? (
<Form> <Form>
<TextArea style={{ marginBottom: '5px' }} placeholder="Describe this project..." value={project.description} onChange={e => onUpdateProject(project._id, { description: e.target.value })} /> <TextArea style={{ marginBottom: '5px' }} placeholder="Describe this project..." value={project.description} onChange={e => dispatch(actions.projects.updateProject(project._id, { description: e.target.value }))} />
<Button size="tiny" color="green" fluid onClick={saveDescription}>Save description</Button> <Button size="tiny" color="green" fluid onClick={saveDescription}>Save description</Button>
</Form> </Form>
) )
@ -100,7 +104,7 @@ function Project({ user, project, fullName, errorMessage, editingDescription, on
</div> </div>
} }
{utils.canEditProject(user, project) && ( {utils.canEditProject(user, project) && (
<Button size='mini' fluid className="right floated" onClick={e => onEditDescription(true)}> <Button size='mini' fluid className="right floated" onClick={e => dispatch(actions.projects.editDescription(true))}>
<Icon name="pencil" /> {project.description ? 'Edit' : 'Add a project'} description <Icon name="pencil" /> {project.description ? 'Edit' : 'Add a project'} description
</Button> </Button>
) )
@ -121,14 +125,7 @@ function Project({ user, project, fullName, errorMessage, editingDescription, on
)} )}
<Grid.Column computer={wideBody() ? 16 : 12} tablet={wideBody() ? 16 : 10}> <Grid.Column computer={wideBody() ? 16 : 12} tablet={wideBody() ? 16 : 10}>
{project && ( {project && <Outlet />}
<Switch>
<Route path="/:username/:projectPath/settings" component={ProjectSettings} />
<Route path="/:username/:projectPath/:objectId/edit" component={Draft} />
<Route path="/:username/:projectPath/:objectId" component={ProjectObjects} />
<Route path="/:username/:projectPath" component={ObjectList} />
</Switch>
) }
</Grid.Column> </Grid.Column>
</Grid> </Grid>
@ -140,22 +137,4 @@ function Project({ user, project, fullName, errorMessage, editingDescription, on
); );
} }
const mapStateToProps = (state, ownProps) => { export default Project;
const { username, projectPath } = ownProps.match.params;
const project = state.projects.projects.filter(p => p.path === projectPath && p.owner && p.owner.username === username)[0];
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
return { user, project, fullName: `${username}/${projectPath}`, loading: state.projects.loading, errorMessage: state.projects.errorMessage, editingDescription: state.projects.editingDescription };
};
const mapDispatchToProps = dispatch => ({
onRequest: () => dispatch(actions.projects.request()),
onRequestFailed: err => dispatch(actions.projects.requestFailed(err)),
onEditDescription: e => dispatch(actions.projects.editDescription(e)),
onReceiveProject: project => dispatch(actions.projects.receiveProject(project)),
onSelectObject: id => dispatch(actions.objects.select(id)),
onUpdateProject: (id, update) => dispatch(actions.projects.updateProject(id, update)),
});
const ProjectContainer = withRouter(connect(
mapStateToProps, mapDispatchToProps,
)(Project));
export default ProjectContainer;

View File

@ -1,57 +1,63 @@
import React, { Component } from 'react'; import React, { useState } from 'react';
import { Form, Input, Divider, Segment, Button, Icon } from 'semantic-ui-react'; import { Form, Input, Divider, Segment, Button, Icon } from 'semantic-ui-react';
import { withRouter } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { connect } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import utils from 'utils/utils.js'; import utils from 'utils/utils.js';
import actions from 'actions'; import actions from 'actions';
import api from 'api'; import api from 'api';
import HelpLink from 'components/includes/HelpLink'; import HelpLink from 'components/includes/HelpLink';
class ProjectSettings extends Component {
constructor(props) {
super(props);
this.state = { name: props.project.name, visibility: props.project.visibility || 'public', openSource: props.project.openSource, groupVisibility: props.project.groupVisibility || [] };
}
changeVisibility = (event, r) => { function ProjectSettings() {
this.setState({ visibility: r.checked ? 'private' : 'public' }); const { username, projectPath } = useParams();
} const { groups, project, fullProjectPath } = useSelector(state => {
const project = state.projects.projects.filter(p => p.path === projectPath && p.owner && p.owner.username === username)[0];
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
return { groups, project, fullProjectPath: `${username}/${projectPath}` };
});
const [name, setName] = useState(project.name);
const [visibility, setVisibility] = useState(project.visibility || 'public');
const [groupVisibility, setGroupVisibility] = useState(project.groupVisibility || []);
const [openSource, setOpenSource] = useState(project.openSource || true);
const navigate = useNavigate();
const dispatch = useDispatch();
changeOpenSource = (event, c) => { const changeVisibility = (event, r) => {
this.setState({ openSource: c.checked }); setVisibility(r.checked ? 'private' : 'public');
} };
saveName = () => { const changeOpenSource = (event, c) => {
setOpenSource(c.checked);
};
const saveName = () => {
utils.confirm('Update name', 'Really update this project\'s name? If you\'ve shared this project\'s link with someone, they may no longer be able to find it.').then(() => { utils.confirm('Update name', 'Really update this project\'s name? If you\'ve shared this project\'s link with someone, they may no longer be able to find it.').then(() => {
api.projects.update(this.props.fullProjectPath, { name: this.state.name }, (project) => { api.projects.update(fullProjectPath, { name: name }, (project) => {
this.props.onReceiveProject(project); dispatch(actions.projects.receiveProject(project));
this.props.history.push(`/${project.owner.username}/${project.path}`); navigate(`/${project.owner.username}/${project.path}`);
}, err => toast.error(err.message)); }, err => toast.error(err.message));
}, () => {}); });
} };
saveVisibility = () => { const saveVisibility = () => {
api.projects.update(this.props.project.fullName, { visibility: this.state.visibility, openSource: this.state.openSource, groupVisibility: this.state.groupVisibility }, (project) => { api.projects.update(project.fullName, { visibility, openSource, groupVisibility }, (p) => {
this.props.onReceiveProject(project); dispatch(actions.projects.receiveProject(p));
toast.info('Visibility saved'); toast.info('Visibility saved');
}, err => toast.error(err.message)); }, err => toast.error(err.message));
} };
deleteProject = () => { const deleteProject = () => {
utils.confirm('Delete project', 'Really delete this project? All files and patterns it contains will also be deleted. This action cannot be undone.').then(() => { utils.confirm('Delete project', 'Really delete this project? All files and patterns it contains will also be deleted. This action cannot be undone.').then(() => {
api.projects.delete(this.props.fullProjectPath, () => { api.projects.delete(fullProjectPath, () => {
this.props.onDeleteProject(this.props.project._id); dispatch(actions.projects.deleteProject(project._id));
toast.info('🗑️ Project deleted'); toast.info('🗑️ Project deleted');
this.props.history.push('/'); navigate('/');
this.setState({ nameError: '' });
}, err => toast.error(err.message)); }, err => toast.error(err.message));
}, () => {}); });
} }
render() {
const { name, visibility, openSource } = this.state;
const { groups } = this.props;
return ( return (
<div> <div>
<h2>Project settings</h2> <h2>Project settings</h2>
@ -67,8 +73,8 @@ class ProjectSettings extends Component {
<p>Changing the name of your project will also change its URL. If you have previously shared a link to your project, then visitors may not be able to find it if the name is changed.</p> <p>Changing the name of your project will also change its URL. If you have previously shared a link to your project, then visitors may not be able to find it if the name is changed.</p>
<Input <Input
type="text" value={name} type="text" value={name}
action=<Button color="teal" content="Update" onClick={this.saveName} /> action=<Button color="teal" content="Update" onClick={saveName} />
onChange={e => this.setState({ name: e.target.value })} onChange={e => setName(e.target.value)}
/> />
</Segment> </Segment>
@ -77,9 +83,9 @@ class ProjectSettings extends Component {
<Segment color="yellow"> <Segment color="yellow">
<h3>Project visibility</h3> <h3>Project visibility</h3>
<Form> <Form>
<Form.Checkbox label='This is a private project' checked={visibility === 'private'} onChange={this.changeVisibility} /> <Form.Checkbox label='This is a private project' checked={visibility === 'private'} onChange={changeVisibility} />
<p style={{color:'rgb(150,150,150)'}}><Icon name='info' /> Private projects are not searchable and can't be seen by others, unless you give them access.</p> <p style={{color:'rgb(150,150,150)'}}><Icon name='info' /> Private projects are not searchable and can't be seen by others, unless you give them access.</p>
<Form.Checkbox label='This project is "open-source"' checked={openSource} onChange={this.changeOpenSource} /> <Form.Checkbox label='This project is "open-source"' checked={openSource} onChange={changeOpenSource} />
<p style={{color:'rgb(150,150,150)'}}><Icon name='info' /> Open-source projects allow other people to download any pattern source files they contain in WIF format.</p> <p style={{color:'rgb(150,150,150)'}}><Icon name='info' /> Open-source projects allow other people to download any pattern source files they contain in WIF format.</p>
<Divider hidden /> <Divider hidden />
@ -90,16 +96,16 @@ class ProjectSettings extends Component {
<Form.Select multiple <Form.Select multiple
label='Make this project always visible to members of these groups' label='Make this project always visible to members of these groups'
value={this.state.groupVisibility} value={groupVisibility}
options={groups.map(g => ({ key: g._id, value: g._id, text: g.name }))} options={groups.map(g => ({ key: g._id, value: g._id, text: g.name }))}
onChange={(e, s) => this.setState({ groupVisibility: s.value })} onChange={(e, s) => setGroupVisibility(s.value)}
/> />
</> </>
} }
</Form> </Form>
<Divider hidden /> <Divider hidden />
<Button color="teal" content="Save visibility" onClick={this.saveVisibility} /> <Button color="teal" content="Save visibility" onClick={saveVisibility} />
</Segment> </Segment>
<Divider hidden section /> <Divider hidden section />
@ -107,28 +113,10 @@ class ProjectSettings extends Component {
<Segment color="red"> <Segment color="red">
<h3>Delete project</h3> <h3>Delete project</h3>
<p>Immediately and irreversibly delete this project, along with all of its contents.</p> <p>Immediately and irreversibly delete this project, along with all of its contents.</p>
<Button icon="trash" content="Delete project" color="red" onClick={this.deleteProject} /> <Button icon="trash" content="Delete project" color="red" onClick={deleteProject} />
</Segment> </Segment>
</div> </div>
); );
}
} }
export default ProjectSettings;
const mapStateToProps = (state, ownProps) => {
const { username, projectPath } = ownProps.match.params;
const project = state.projects.projects.filter(p => p.path === projectPath && p.owner && p.owner.username === username)[0];
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
return { user, groups, project, fullProjectPath: `${username}/${projectPath}` };
};
const mapDispatchToProps = dispatch => ({
onDeleteProject: id => dispatch(actions.projects.deleteProject(id)),
onReceiveProject: project => dispatch(actions.projects.receiveProject(project)),
onUpdateProject: (id, update) => dispatch(actions.projects.updateProject(id, update)),
});
const ProjectSettingsContainer = withRouter(connect(
mapStateToProps, mapDispatchToProps,
)(ProjectSettings));
export default ProjectSettingsContainer;

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react'; import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { withRouter, Prompt } from 'react-router-dom'; import { useSelector, useDispatch } from 'react-redux';
import { connect } from 'react-redux'; import { useParams } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import styled from 'styled-components'; import styled from 'styled-components';
import ElementPan from 'components/includes/ElementPan'; import ElementPan from 'components/includes/ElementPan';
@ -25,67 +25,63 @@ export const StyledPattern = styled.div`
margin:20px; margin:20px;
`; `;
class Draft extends Component { function Draft() {
constructor(props) { const [unsaved, setUnsaved] = useState(false);
super(props); const [saving, setSaving] = useState(false);
this.state = { unsaved: false, saving: false }; const [object, setObject] = useState();
} const [pattern, setPattern] = useState();
const [name] = useState();
const { objectId } = useParams();
const dispatch = useDispatch();
const { editor } = useSelector(state => ({ editor: state.objects.editor }));
componentDidMount() { useEffect(() => {
api.objects.get(this.props.match.params.objectId, (object) => { api.objects.get(objectId, (o) => {
if (!object.pattern.baseSize) object.pattern.baseSize = 10; if (!o.pattern.baseSize) o.pattern.baseSize = 10;
this.setState(object); setObject(o);
setPattern(o.pattern);
}); });
} }, [objectId]);
updateObject = (update) => { const updateObject = (update) => {
this.setState(Object.assign({}, this.state, update)); setObject(Object.assign({}, object, update));
this.setState({ unsaved: true }); setUnsaved(true);
} };
updatePattern = (update) => { const updatePattern = (update) => {
const newPattern = Object.assign({}, this.state.pattern, update); const newPattern = Object.assign({}, pattern, update);
this.setState(Object.assign({}, this.state, { pattern: newPattern })); setPattern(Object.assign({}, pattern, newPattern));
this.setState({ unsaved: true }); setUnsaved(true);
} };
saveObject = () => { const saveObject = () => {
this.setState({ saving: true }); setSaving(true);
const canvas = document.getElementsByClassName('drawdown')[0]; const canvas = document.getElementsByClassName('drawdown')[0];
const object = Object.assign({}, this.state); const newObject = Object.assign({}, object);
object.preview = canvas.toDataURL(); newObject.preview = canvas.toDataURL();
api.objects.update(this.props.match.params.objectId, object, (o) => { api.objects.update(objectId, newObject, (o) => {
toast.success('Pattern saved'); toast.success('Pattern saved');
this.props.onReceiveObject(o); dispatch(actions.objects.receive(o));
this.setState({ unsaved: false, saving: false }); setUnsaved(false);
setSaving(false);
}, (err) => { }, (err) => {
toast.error(err.message); toast.error(err.message);
this.setState({ saving: false }); setSaving(false);
}); });
} };
rerunTour = () => { if (!pattern) return null;
const { warp, weft, tieups, baseSize } = pattern;
}
render() {
if (!this.state.pattern) return null;
const { unsaved, saving } = this.state;
const { warp, weft, tieups, baseSize } = this.state.pattern;
const cellStyle = { width: `${baseSize || 10}px`, height: `${baseSize || 10}px` }; const cellStyle = { width: `${baseSize || 10}px`, height: `${baseSize || 10}px` };
return ( return (
<div> <div>
<Helmet title={`${this.state?.name || 'Weaving Draft'}`} /> <Helmet title={`${name || 'Weaving Draft'}`} />
<Prompt
when={unsaved ? true : false}
message='You have unsaved changes. Are you sure you want to leave tnis page?'
/>
<Tour id='pattern' run={true} /> <Tour id='pattern' run={true} />
<div style={{display: 'flex'}}> <div style={{display: 'flex'}}>
<div style={{flex: 1, overflow: 'hidden'}}> <div style={{flex: 1, overflow: 'hidden'}}>
<ElementPan <ElementPan
disabled={!(this.props.editor && this.props.editor.tool === 'pan')} disabled={!(editor?.tool === 'pan')}
startX={5000} startX={5000}
startY={0} startY={0}
> >
@ -96,9 +92,9 @@ class Draft extends Component {
}} }}
> >
<Warp baseSize={baseSize} cellStyle={cellStyle} warp={warp} weft={weft} updatePattern={this.updatePattern} /> <Warp baseSize={baseSize} cellStyle={cellStyle} warp={warp} weft={weft} updatePattern={updatePattern} />
<Weft cellStyle={cellStyle} warp={warp} weft={weft} baseSize={baseSize} updatePattern={this.updatePattern} /> <Weft cellStyle={cellStyle} warp={warp} weft={weft} baseSize={baseSize} updatePattern={updatePattern} />
<Tieups cellStyle={cellStyle} warp={warp} weft={weft} tieups={tieups} updatePattern={this.updatePattern} baseSize={baseSize}/> <Tieups cellStyle={cellStyle} warp={warp} weft={weft} tieups={tieups} updatePattern={updatePattern} baseSize={baseSize}/>
<Drawdown warp={warp} weft={weft} tieups={tieups} baseSize={baseSize} /> <Drawdown warp={warp} weft={weft} tieups={tieups} baseSize={baseSize} />
</StyledPattern> </StyledPattern>
@ -108,21 +104,12 @@ class Draft extends Component {
<div style={{width: 300, marginLeft: 20}}> <div style={{width: 300, marginLeft: 20}}>
<HelpLink className='joyride-help' link={`${process.env.REACT_APP_SUPPORT_ROOT}Editing-patterns#using-the-pattern-editor`} marginBottom/> <HelpLink className='joyride-help' link={`${process.env.REACT_APP_SUPPORT_ROOT}Editing-patterns#using-the-pattern-editor`} marginBottom/>
<ReRunTour id='pattern' /> <ReRunTour id='pattern' />
<Tools warp={warp} weft={weft} object={this.state} pattern={this.state.pattern} updateObject={this.updateObject} updatePattern={this.updatePattern} saveObject={this.saveObject} baseSize={baseSize} unsaved={unsaved} saving={saving}/> <Tools warp={warp} weft={weft} object={object} pattern={pattern} updateObject={updateObject} updatePattern={updatePattern} saveObject={saveObject} baseSize={baseSize} unsaved={unsaved} saving={saving}/>
</div> </div>
</div> </div>
</div> </div>
); );
}
} }
const mapStateToProps = (state, ownProps) => ({ editor: state.objects.editor }); export default Draft;
const mapDispatchToProps = dispatch => ({
onReceiveObject: object => dispatch(actions.objects.receive(object)),
});
const DraftContainer = withRouter(connect(
mapStateToProps, mapDispatchToProps,
)(Draft));
export default DraftContainer;

View File

@ -1,52 +0,0 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import actions from 'actions'; import api from 'api'; import Warp from './Warp.js'; import Weft from './Weft.js';
import Tieups from './Tieups.js';
import Drawdown from './Drawdown.js';
class DraftExport extends Component {
constructor(props) {
super(props);
this.state = { };
}
componentDidMount() {
this.props.onEditorUpdated({ view: 'colour' }); // interlacement, warp, weft
api.objects.get(this.props.match.params.id, (object) => {
if (object.pattern && object.pattern.warp) this.setState(object);
});
}
render() {
if (!this.state.pattern) return null;
const { warp, weft, tieups } = this.state.pattern;
if (!warp || !weft || !tieups) return null;
const baseSize = 6;
const cellStyle = { width: `${baseSize || 10}px`, height: `${baseSize || 10}px` };
return (
<div className="pattern"
style={{
margin: '0px 0px',
width: `${warp.threads * baseSize + weft.treadles * baseSize + 20}px`,
height: `${warp.shafts * baseSize + weft.threads * baseSize + 20}px`,
display: this.props.hidden ? 'none' : 'inherit',
}}
>
<Drawdown warp={warp} weft={weft} tieups={tieups} baseSize={baseSize} />
<Warp baseSize={baseSize} cellStyle={cellStyle} warp={warp} weft={weft} updatePattern={() => {}} />
<Weft cellStyle={cellStyle} warp={warp} weft={weft} baseSize={baseSize} updatePattern={() => {}} />
<Tieups cellStyle={cellStyle} warp={warp} weft={weft} tieups={tieups} baseSize={baseSize} updatePattern={() => {}} />
</div>
);
}
}
const mapDispatchToProps = dispatch => ({
onEditorUpdated: editor => dispatch(actions.objects.updateEditor(editor)),
onEditObject: (id, field, value) => dispatch(actions.objects.update(id, field, value)),
});
const DraftExportContainer = connect(
null, mapDispatchToProps,
)(DraftExport);
export default DraftExportContainer;

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react'; import React, { useEffect, useState, useCallback } from 'react';
import { connect } from 'react-redux'; import { useDispatch } from 'react-redux';
import { Loader } from 'semantic-ui-react'; import { Loader } from 'semantic-ui-react';
import actions from 'actions'; import actions from 'actions';
import api from 'api'; import api from 'api';
@ -11,40 +11,40 @@ import Weft from './Weft.js';
import Tieups from './Tieups.js'; import Tieups from './Tieups.js';
import Drawdown from './Drawdown.js'; import Drawdown from './Drawdown.js';
class DraftPreview extends Component { function DraftPreview({ object }) {
constructor(props) { const [loading, setLoading] = useState(false);
super(props); const [pattern, setPattern] = useState();
this.state = { loading: false }; const dispatch = useDispatch();
} const objectId = object?._id;
componentDidMount() { const generatePreview = useCallback(() => {
this.props.onEditorUpdated({ tool: 'pan' });
this.setState({ loading: true });
api.objects.get(this.props.object._id, (object) => {
this.setState({ loading: false });
if (object.pattern && object.pattern.warp) {
this.setState(object, () => {
if (this.props.onImageLoaded) this.unifyCanvas();
});
}
if (!object.preview) {
setTimeout(() => { setTimeout(() => {
const c = document.getElementsByClassName('drawdown')[0]; const c = document.getElementsByClassName('drawdown')[0];
const preview = c && c.toDataURL(); const preview = c?.toDataURL();
if (preview) { if (preview) {
api.objects.update(object._id, { preview }, () => { api.objects.update(objectId, { preview }, () => {
this.props.onEditObject(object._id, 'preview', preview); dispatch(actions.objects.update(objectId, 'preview', preview));
}); });
} }
}, 1000); }, 1000);
} }, [dispatch, objectId]);
}, err => this.setState({ loading: false }));
}
unifyCanvas() { useEffect(() => {
dispatch(actions.objects.updateEditor({ tool: 'pan' }));
setLoading(true);
api.objects.get(objectId, (o) => {
setLoading(false);
if (o.pattern && o.pattern.warp) {
setPattern(o.pattern);
if (!o.preview) generatePreview();
}
}, err => setLoading(false));
}, [dispatch, objectId, generatePreview]);
const unifyCanvas = useCallback(() => {
if (!pattern) return;
const { warp, weft } = pattern;
setTimeout(() => { setTimeout(() => {
const id = this.props.object._id;
const { warp, weft } = this.state.pattern;
const baseSize = 6; const baseSize = 6;
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
@ -53,12 +53,12 @@ class DraftPreview extends Component {
ctx.fillStyle = 'white'; ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'black'; ctx.fillStyle = 'black';
const warpCanvas = document.querySelector(`.preview-${id} .warp-threads`); const warpCanvas = document.querySelector(`.preview-${objectId} .warp-threads`);
const warpColourwayCanvas = document.querySelector(`.preview-${id} .warp-colourway`); const warpColourwayCanvas = document.querySelector(`.preview-${objectId} .warp-colourway`);
const weftCanvas = document.querySelector(`.preview-${id} .weft-threads`); const weftCanvas = document.querySelector(`.preview-${objectId} .weft-threads`);
const weftColourwayCanvas = document.querySelector(`.preview-${id} .weft-colourway`); const weftColourwayCanvas = document.querySelector(`.preview-${objectId} .weft-colourway`);
const drawdownCanvas = document.querySelector(`.preview-${id} .drawdown`); const drawdownCanvas = document.querySelector(`.preview-${objectId} .drawdown`);
const tieupsCanvas = document.querySelector(`.preview-${id} .tieups`); const tieupsCanvas = document.querySelector(`.preview-${objectId} .tieups`);
if (warpCanvas) { if (warpCanvas) {
ctx.drawImage(warpColourwayCanvas, canvas.width - warpCanvas.width - weft.treadles * baseSize - 20, 0); ctx.drawImage(warpColourwayCanvas, canvas.width - warpCanvas.width - weft.treadles * baseSize - 20, 0);
ctx.drawImage(warpCanvas, canvas.width - warpCanvas.width - weft.treadles * baseSize - 20, 10); ctx.drawImage(warpCanvas, canvas.width - warpCanvas.width - weft.treadles * baseSize - 20, 10);
@ -66,16 +66,18 @@ class DraftPreview extends Component {
ctx.drawImage(weftColourwayCanvas, canvas.width - 10, warp.shafts * baseSize + 20); ctx.drawImage(weftColourwayCanvas, canvas.width - 10, warp.shafts * baseSize + 20);
ctx.drawImage(tieupsCanvas, canvas.width - weft.treadles * baseSize - 10, 10); ctx.drawImage(tieupsCanvas, canvas.width - weft.treadles * baseSize - 10, 10);
ctx.drawImage(drawdownCanvas, canvas.width - drawdownCanvas.width - weft.treadles * baseSize - 20, warp.shafts * baseSize + 20); ctx.drawImage(drawdownCanvas, canvas.width - drawdownCanvas.width - weft.treadles * baseSize - 20, warp.shafts * baseSize + 20);
dispatch(actions.objects.update(objectId, 'patternImage', canvas.toDataURL()));
this.props.onImageLoaded(canvas.toDataURL());
} }
}, 500); }, 500);
} }, [dispatch, objectId, pattern]);
render() { useEffect(() => {
if (this.state.loading) return <Loader active />; unifyCanvas();
if (!this.state.pattern) return null; }, [unifyCanvas])
const { warp, weft, tieups } = this.state.pattern;
if (loading) return <Loader active />;
if (!pattern) return null;
const { warp, weft, tieups } = pattern;
if (!warp || !weft || !tieups) return null; if (!warp || !weft || !tieups) return null;
const baseSize = 6; const baseSize = 6;
const cellStyle = { width: `${baseSize || 10}px`, height: `${baseSize || 10}px` }; const cellStyle = { width: `${baseSize || 10}px`, height: `${baseSize || 10}px` };
@ -85,7 +87,7 @@ class DraftPreview extends Component {
startY={0} startY={0}
> >
<StyledPattern <StyledPattern
className={`pattern preview-${this.props.object._id}`} className={`pattern preview-${objectId}`}
style={{ style={{
width: '2000px', width: '2000px',
height: '1000px', height: '1000px',
@ -98,15 +100,6 @@ class DraftPreview extends Component {
</StyledPattern> </StyledPattern>
</ElementPan> </ElementPan>
); );
}
} }
const mapDispatchToProps = dispatch => ({ export default DraftPreview;
onEditorUpdated: editor => dispatch(actions.objects.updateEditor(editor)),
onEditObject: (id, field, value) => dispatch(actions.objects.update(id, field, value)),
});
const DraftPreviewContainer = connect(
null, mapDispatchToProps,
)(DraftPreview);
export default DraftPreviewContainer;

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react'; import React, { useEffect, useRef } from 'react';
import { connect } from 'react-redux'; import { useSelector } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import utils from 'utils/utils'; import utils from 'utils/utils';
@ -12,23 +12,17 @@ const StyledDrawdown = styled.canvas`
width: ${props => props.warp.threads * props.baseSize}px; width: ${props => props.warp.threads * props.baseSize}px;
`; `;
class Drawdown extends Component { // Cache
constructor(props) { const squares = {};
super(props);
this.squares = {};
}
componentDidMount() { function Drawdown({ baseSize, warp, weft, tieups }) {
this.paintDrawdown(); const drawdownRef = useRef();
} useEffect(() => paintDrawdown());
const { editor } = useSelector(state => ({ editor: state.objects.editor }));
componentDidUpdate(prevProps, prevState) { const getSquare = (thread, size, colour) => {
this.paintDrawdown(prevProps); const { view } = editor;
} if (squares[view] && squares[view][thread] && squares[view][thread][size] && squares[view][thread][size][colour]) return squares[view][thread][size][colour];
getSquare(thread, size, colour) {
const { view } = this.props.editor;
if (this.squares[view] && this.squares[view][thread] && this.squares[view][thread][size] && this.squares[view][thread][size][colour]) return this.squares[view][thread][size][colour];
const m_canvas = document.createElement('canvas'); const m_canvas = document.createElement('canvas');
m_canvas.width = size; m_canvas.width = size;
m_canvas.height = size; m_canvas.height = size;
@ -43,7 +37,7 @@ class Drawdown extends Component {
if (view === 'colour' || view === 'interlacement') { if (view === 'colour' || view === 'interlacement') {
mc.fillStyle = colour; mc.fillStyle = colour;
mc.fillRect(0, 0, size, size); mc.fillRect(0, 0, size, size);
if (this.props.editor.view === 'interlacement') { if (editor.view === 'interlacement') {
if (thread === 'warp') { if (thread === 'warp') {
const grd = mc.createLinearGradient(0, 0, size, 0); const grd = mc.createLinearGradient(0, 0, size, 0);
grd.addColorStop(0.1, 'rgba(0,0,0,0.3)'); grd.addColorStop(0.1, 'rgba(0,0,0,0.3)');
@ -63,19 +57,16 @@ class Drawdown extends Component {
} }
} }
if (!this.squares[view]) this.squares[view] = {}; if (!squares[view]) squares[view] = {};
if (!this.squares[view][thread]) this.squares[view][thread] = {}; if (!squares[view][thread]) squares[view][thread] = {};
if (!this.squares[view][thread][size]) this.squares[view][thread][size] = {}; if (!squares[view][thread][size]) squares[view][thread][size] = {};
this.squares[view][thread][size][colour] = m_canvas; squares[view][thread][size][colour] = m_canvas;
return m_canvas; return m_canvas;
} };
paintDrawdown(prevProps) { const paintDrawdown = () => {
const canvas = this.refs.drawdown; const canvas = drawdownRef.current;
const ctx = canvas.getContext('2d', { alpha: false }); const ctx = canvas.getContext('2d', { alpha: false });
const {
baseSize, warp, weft, tieups,
} = this.props;
for (let tread = 0; tread < weft.threads; tread++) { for (let tread = 0; tread < weft.threads; tread++) {
for (let thread = 0; thread < warp.threads; thread++) { for (let thread = 0; thread < warp.threads; thread++) {
@ -83,40 +74,25 @@ class Drawdown extends Component {
const shaft = warp.threading[thread].shaft; const shaft = warp.threading[thread].shaft;
const tieup = tieups[treadle - 1]; const tieup = tieups[treadle - 1];
const proceed = true; const proceed = true;
/* if (prevProps) {
const prevTreadle = prevProps.weft.treadling[tread].treadle;
const prevShaft = prevProps.warp.threading[thread].shaft;
const prevTieup = prevProps.tieups[prevTreadle - 1];
const prevBaseSize = prevProps.baseSize;
proceed = prevTreadle !== treadle || prevShaft !== shaft || baseSize !== prevBaseSize || prevTieup !== tieup;
} */
if (proceed) { if (proceed) {
const weftColour = utils.rgb(weft.treadling[tread].colour || weft.defaultColour); const weftColour = utils.rgb(weft.treadling[tread].colour || weft.defaultColour);
const warpColour = utils.rgb(warp.threading[thread].colour || warp.defaultColour); const warpColour = utils.rgb(warp.threading[thread].colour || warp.defaultColour);
const threadType = tieup && tieup.filter(t => t <= warp.shafts).indexOf(shaft) > -1 ? 'warp' : 'weft'; const threadType = tieup && tieup.filter(t => t <= warp.shafts).indexOf(shaft) > -1 ? 'warp' : 'weft';
const square = this.getSquare(threadType, baseSize, threadType === 'warp' ? warpColour : weftColour); const square = getSquare(threadType, baseSize, threadType === 'warp' ? warpColour : weftColour);
ctx.drawImage(square, canvas.width - (baseSize * (thread + 1)), tread * baseSize); ctx.drawImage(square, canvas.width - (baseSize * (thread + 1)), tread * baseSize);
} }
} }
} }
} };
render() {
const { warp, weft, baseSize } = this.props;
return ( return (
<StyledDrawdown ref="drawdown" className="drawdown joyride-drawdown" <StyledDrawdown ref={drawdownRef} className="drawdown joyride-drawdown"
width={warp.threads * baseSize} width={warp.threads * baseSize}
height={weft.threads * baseSize} height={weft.threads * baseSize}
weft={weft} warp={warp} baseSize={baseSize} weft={weft} warp={warp} baseSize={baseSize}
/> />
); );
}
} }
const mapStateToProps = (state, ownProps) => ({ editor: state.objects.editor }); export default Drawdown;
const DrawdownContainer = connect(
mapStateToProps,
)(Drawdown);
export default DrawdownContainer;

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { useEffect, useRef } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
const StyledTieups = styled.canvas` const StyledTieups = styled.canvas`
@ -7,46 +7,40 @@ const StyledTieups = styled.canvas`
right:10px; right:10px;
`; `;
class Tieups extends Component { function Tieups({ cellStyle, warp, weft, tieups, updatePattern, baseSize }) {
useEffect(() => paintTieups());
const tieupRef = useRef(null);
componentDidUpdate(prevProps, prevState) { const fillUpTo = (t, limit) => {
this.paintTieups(); let i = t.length;
}
componentDidMount() {
this.paintTieups();
}
fillUpTo = (tieups, limit) => {
let i = tieups.length;
while (i <= limit) { while (i <= limit) {
tieups.push([]); t.push([]);
i++; i++;
} }
} };
getTieupShaft = (event) => { const getTieupShaft = (event) => {
const rect = event.currentTarget.getBoundingClientRect(); const rect = event.currentTarget.getBoundingClientRect();
const y = event.clientY - rect.top; const y = event.clientY - rect.top;
const x = 0 - (event.clientX - rect.right); const x = 0 - (event.clientX - rect.right);
const shaft = this.props.warp.shafts - parseInt(y / this.props.baseSize); const shaft = warp.shafts - parseInt(y / baseSize);
const tieup = this.props.weft.treadles - parseInt(x / this.props.baseSize) - 1; const tieup = weft.treadles - parseInt(x / baseSize) - 1;
return { tieup, shaft }; return { tieup, shaft };
} };
click = (event) => { const click = (event) => {
const { tieup, shaft } = this.getTieupShaft(event); const { tieup, shaft } = getTieupShaft(event);
const tieups = Object.assign([], this.props.tieups); const newTieups = Object.assign([], tieups);
if (tieup >= tieups.length) this.fillUpTo(tieups, tieup); if (tieup >= tieups.length) fillUpTo(newTieups, tieup);
if (tieups[tieup] !== undefined) { if (tieups[tieup] !== undefined) {
if (tieups[tieup].indexOf(shaft) === -1) tieups[tieup].push(shaft); if (tieups[tieup].indexOf(shaft) === -1) newTieups[tieup].push(shaft);
else tieups[tieup].splice(tieups[tieup].indexOf(shaft)); else newTieups[tieup].splice(tieups[tieup].indexOf(shaft));
}
this.props.updatePattern({ tieups });
} }
updatePattern({ tieups: newTieups });
};
paintTieups() { const paintTieups = () => {
const canvas = this.refs.tieups; const canvas = tieupRef.current;
const ctx = canvas.getContext('2d');// , { alpha: false }); const ctx = canvas.getContext('2d');// , { alpha: false });
const { baseSize, tieups } = this.props;
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
@ -68,12 +62,9 @@ class Tieups extends Component {
} }
} }
render() {
const { warp, weft, baseSize } = this.props;
return ( return (
<StyledTieups ref='tieups' className='tieups joyride-tieups' width={weft.treadles * baseSize} height= {warp.shafts * baseSize} style={{width: weft.treadles * baseSize, height: warp.shafts * baseSize}} onClick={this.click}/> <StyledTieups ref={tieupRef} className='tieups joyride-tieups' width={weft.treadles * baseSize} height= {warp.shafts * baseSize} style={{width: weft.treadles * baseSize, height: warp.shafts * baseSize}} onClick={click}/>
); );
}
} }
export default Tieups; export default Tieups;

View File

@ -1,9 +1,9 @@
import React, { Component } from 'react'; import React, { useState } from 'react';
import { import {
Confirm, Select, Segment, Accordion, Grid, Icon, Input, Button, Confirm, Select, Segment, Accordion, Grid, Icon, Input, Button,
} from 'semantic-ui-react'; } from 'semantic-ui-react';
import { connect } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { withRouter } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import Slider from 'rc-slider'; import Slider from 'rc-slider';
import styled from 'styled-components'; import styled from 'styled-components';
@ -35,69 +35,78 @@ const ColourSquare = styled.div`
} }
`; `;
class Tools extends Component { function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updatePattern, updateObject, saveObject }) {
state = { colours: [], activeDrawers: ['properties', 'drawing', 'palette'], view: 'interlacement' } const [activeDrawers, setActiveDrawers] = useState(['properties', 'drawing', 'palette']);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const navigate = useNavigate();
const dispatch = useDispatch();
const { objectId, username, projectPath } = useParams();
enableTool = (tool) => { const { project, editor } = useSelector(state => {
this.props.onEditorUpdated({ tool, colour: this.props.editor.colour }); let project = {};
} state.projects.projects.forEach((p) => {
if (p.path === projectPath && p.owner && p.owner.username === username) project = p;
});
return { project, editor: state.objects.editor };
});
setColour = (colour) => { const enableTool = (tool) => {
this.props.onEditorUpdated({ tool: 'colour', colour }); dispatch(actions.objects.updateEditor({ tool, colour: editor.colour }));
} };
setView = (view) => { const setColour = (colour) => {
this.props.onEditorUpdated({ view }); dispatch(actions.objects.updateEditor({ tool: 'colour', colour }));
} };
setName = (event) => { const setEditorView = (view) => {
this.props.updateObject({ name: event.target.value }); dispatch(actions.objects.updateEditor({ view }));
} };
setShafts = (event) => { const setName = (event) => {
const warp = { ...this.props.warp, shafts: parseInt(event.target.value, 10) || 1 }; updateObject({ name: event.target.value });
this.props.updatePattern({ warp }); };
}
setTreadles = (event) => { const setShafts = (event) => {
const weft = { ...this.props.weft, treadles: parseInt(event.target.value, 10) || 1 }; updatePattern({ warp: { ...warp, shafts: parseInt(event.target.value, 10) || 1 } });
this.props.updatePattern({ weft }); };
}
onZoomChange = zoom => this.props.updatePattern({ baseSize: zoom || 10 }) const setTreadles = (event) => {
updatePattern({ weft: { ...weft, treadles: parseInt(event.target.value, 10) || 1 } });
};
drawerIsActive = drawer => this.state.activeDrawers.indexOf(drawer) > -1 const onZoomChange = zoom => updatePattern({ baseSize: zoom || 10 });
activateDrawer = (drawer) => { const drawerIsActive = drawer => activeDrawers.indexOf(drawer) > -1;
const index = this.state.activeDrawers.indexOf(drawer);
const drawers = this.state.activeDrawers; const activateDrawer = (drawer) => {
const index = activeDrawers.indexOf(drawer);
const drawers = activeDrawers;
if (index === -1) { if (index === -1) {
drawers.push(drawer); drawers.push(drawer);
} else { } else {
drawers.splice(index, 1); drawers.splice(index, 1);
} }
this.setState({ activeDrawers: drawers }); setActiveDrawers(drawers);
} };
deleteObject = () => { const deleteObject = () => {
api.objects.delete(this.props.match.params.objectId, () => { api.objects.delete(objectId, () => {
toast('🗑️ Pattern deleted'); toast('🗑️ Pattern deleted');
this.props.onObjectDeleted(this.props.match.params.objectId); dispatch(actions.objects.delete(objectId));
this.props.history.push(`/${this.props.project.fullName}`); navigate(`/${project.fullName}`);
}, err => console.log(err)); }, err => console.log(err));
} }
revertChanges = () => { const revertChanges = () => {
const sure = window.confirm('Really revert your changes to your last save point?\n\nAny updates to your pattern since you last saved will be lost.') const sure = window.confirm('Really revert your changes to your last save point?\n\nAny updates to your pattern since you last saved will be lost.')
if (sure) { if (sure) {
window.location.reload(); window.location.reload();
} }
} };
changeWidth = () => { const changeWidth = () => {
const newWidth = parseInt(window.prompt('Enter a new width for your pattern.\n\nIMPORTANT: If your new width is less than the current width, then any shafts selected beyond the new width will be lost.')); const newWidth = parseInt(window.prompt('Enter a new width for your pattern.\n\nIMPORTANT: If your new width is less than the current width, then any shafts selected beyond the new width will be lost.'));
if (!newWidth) return; if (!newWidth) return;
const { warp } = this.props;
if (newWidth > warp.threading.length) { if (newWidth > warp.threading.length) {
let i = warp.threading.length; let i = warp.threading.length;
while (i < newWidth) { while (i < newWidth) {
@ -110,14 +119,13 @@ class Tools extends Component {
warp.threading.splice(newWidth); warp.threading.splice(newWidth);
warp.threads = warp.threading.length; warp.threads = warp.threading.length;
} }
this.props.updatePattern({ warp }); updatePattern({ warp });
this.props.onEditorUpdated(); dispatch(actions.objects.updateEditor());
this.props.onEditorUpdated({ tool: 'pan' }); dispatch(actions.objects.updateEditor( { tool: 'pan' }));
} };
changeHeight = () => { const changeHeight = () => {
const newHeight = parseInt(window.prompt('Enter a new height for your pattern.\n\nIMPORTANT: If your new height is less than the current height, then any treadles selected beyond the new height will be lost.')); const newHeight = parseInt(window.prompt('Enter a new height for your pattern.\n\nIMPORTANT: If your new height is less than the current height, then any treadles selected beyond the new height will be lost.'));
if (!newHeight) return; if (!newHeight) return;
const { weft } = this.props;
if (newHeight > weft.treadling.length) { if (newHeight > weft.treadling.length) {
let i = weft.treadling.length; let i = weft.treadling.length;
while (i < newHeight) { while (i < newHeight) {
@ -130,32 +138,30 @@ class Tools extends Component {
weft.treadling.splice(newHeight); weft.treadling.splice(newHeight);
weft.threads = weft.treadling.length; weft.threads = weft.treadling.length;
} }
this.props.updatePattern({ weft }); updatePattern({ weft });
this.props.onEditorUpdated(); dispatch(actions.objects.updateEditor());
} };
render() {
const { warp, weft, editor, unsaved, saving } = this.props;
return ( return (
<div className="pattern-toolbox joyride-tools"> <div className="pattern-toolbox joyride-tools">
{unsaved && {unsaved &&
<Segment attached="top"> <Segment attached="top">
<Button fluid color="teal" icon="save" content="Save pattern" onClick={() => this.props.saveObject(this.refs.canvas)} loading={saving}/> <Button fluid color="teal" icon="save" content="Save pattern" onClick={() => saveObject(/*this.refs.canvas*/)} loading={saving}/>
<Button style={{marginTop: 5}} fluid icon='refresh' content='Undo changes' onClick={this.revertChanges} /> <Button style={{marginTop: 5}} fluid icon='refresh' content='Undo changes' onClick={revertChanges} />
</Segment> </Segment>
} }
<Segment attached> <Segment attached>
<Accordion fluid> <Accordion fluid>
<Accordion.Title active={this.drawerIsActive('view')} onClick={e => this.activateDrawer('view')}> <Accordion.Title active={drawerIsActive('view')} onClick={e => activateDrawer('view')}>
<Icon name="dropdown" /> View <Icon name="dropdown" /> View
</Accordion.Title> </Accordion.Title>
<Accordion.Content active={this.drawerIsActive('view')}> <Accordion.Content active={drawerIsActive('view')}>
<small>Drawdown view</small> <small>Drawdown view</small>
<Select <Select
size="tiny" size="tiny"
fluid fluid
value={editor.view} value={editor.view}
onChange={(e, s) => this.setView(s.value)} onChange={(e, s) => setEditorView(s.value)}
style={{ fontSize: '11px' }} style={{ fontSize: '11px' }}
options={[ options={[
{ key: 1, value: 'interlacement', text: 'Interlacement' }, { key: 1, value: 'interlacement', text: 'Interlacement' },
@ -166,98 +172,81 @@ class Tools extends Component {
/> />
<div style={{ marginTop: '5px' }} /> <div style={{ marginTop: '5px' }} />
<small>Zoom</small> <small>Zoom</small>
<Slider defaultValue={this.props.baseSize} min={5} max={13} step={1} onAfterChange={this.onZoomChange} /> <Slider defaultValue={baseSize} min={5} max={13} step={1} onAfterChange={onZoomChange} />
</Accordion.Content> </Accordion.Content>
<Accordion.Title active={this.drawerIsActive('properties')} onClick={e => this.activateDrawer('properties')}> <Accordion.Title active={drawerIsActive('properties')} onClick={e => activateDrawer('properties')}>
<Icon name="dropdown" /> Properties <Icon name="dropdown" /> Properties
</Accordion.Title> </Accordion.Title>
<Accordion.Content active={this.drawerIsActive('properties')}> <Accordion.Content active={drawerIsActive('properties')}>
<small>Name</small> <small>Name</small>
<Input type="text" size="small" fluid style={{ marginBottom: '5px' }} value={this.props.object.name} onChange={this.setName} /> <Input type="text" size="small" fluid style={{ marginBottom: '5px' }} value={object.name} onChange={setName} />
<Grid columns={2}> <Grid columns={2}>
<Grid.Row className='joyride-threads'> <Grid.Row className='joyride-threads'>
<Grid.Column> <Grid.Column>
<small>Shafts</small> <small>Shafts</small>
<Input fluid type="number" value={warp.shafts} onKeyDown={e => false} onChange={this.setShafts} size="mini" /> <Input fluid type="number" value={warp.shafts} onKeyDown={e => false} onChange={setShafts} size="mini" />
</Grid.Column> </Grid.Column>
<Grid.Column> <Grid.Column>
<small>Treadles</small> <small>Treadles</small>
<Input fluid type="number" value={weft.treadles} onKeyDown={e => false} onChange={this.setTreadles} size="mini" /> <Input fluid type="number" value={weft.treadles} onKeyDown={e => false} onChange={setTreadles} size="mini" />
</Grid.Column> </Grid.Column>
</Grid.Row> </Grid.Row>
<Grid.Row style={{paddingTop: 0}}> <Grid.Row style={{paddingTop: 0}}>
<Grid.Column> <Grid.Column>
<small>Width</small> <small>Width</small>
<Input fluid readOnly value={warp.threading?.length || 0} size="mini" <Input fluid readOnly value={warp.threading?.length || 0} size="mini"
action={{icon: 'edit', onClick: this.changeWidth}} action={{icon: 'edit', onClick: changeWidth}}
/> />
</Grid.Column> </Grid.Column>
<Grid.Column> <Grid.Column>
<small>Height</small> <small>Height</small>
<Input fluid readOnly value={weft.treadling?.length || 0} size="mini" <Input fluid readOnly value={weft.treadling?.length || 0} size="mini"
action={{icon: 'edit', onClick: this.changeHeight}} action={{icon: 'edit', onClick: changeHeight}}
/> />
</Grid.Column> </Grid.Column>
</Grid.Row> </Grid.Row>
</Grid> </Grid>
</Accordion.Content> </Accordion.Content>
<Accordion.Title active={this.drawerIsActive('drawing')} onClick={e => this.activateDrawer('drawing')}> <Accordion.Title active={drawerIsActive('drawing')} onClick={e => activateDrawer('drawing')}>
<Icon name="dropdown" /> Tools <Icon name="dropdown" /> Tools
</Accordion.Title> </Accordion.Title>
<Accordion.Content active={this.drawerIsActive('drawing')}> <Accordion.Content active={drawerIsActive('drawing')}>
<Button.Group fluid> <Button.Group fluid>
<Button className='joyride-pan' data-tooltip="Pan (drag to move) pattern" color={this.props.editor.tool === 'pan' && 'blue'} size="tiny" icon onClick={() => this.enableTool('pan')}><Icon name="move" /></Button> <Button className='joyride-pan' data-tooltip="Pan (drag to move) pattern" color={editor.tool === 'pan' && 'blue'} size="tiny" icon onClick={() => enableTool('pan')}><Icon name="move" /></Button>
<Button className='joyride-colour' data-tooltip="Paint selected colour" color={this.props.editor.tool === 'colour' && 'blue'} size="tiny" icon onClick={() => this.enableTool('colour')}><Icon name="paint brush" /></Button> <Button className='joyride-colour' data-tooltip="Paint selected colour" color={editor.tool === 'colour' && 'blue'} size="tiny" icon onClick={() => enableTool('colour')}><Icon name="paint brush" /></Button>
<Button className='joyride-straight' data-tooltip="Straight draw" color={this.props.editor.tool === 'straight' && 'blue'} size="tiny" icon onClick={() => this.enableTool('straight')}>/ /</Button> <Button className='joyride-straight' data-tooltip="Straight draw" color={editor.tool === 'straight' && 'blue'} size="tiny" icon onClick={() => enableTool('straight')}>/ /</Button>
<Button className='joyride-point' data-tooltip="Point draw" color={this.props.editor.tool === 'point' && 'blue'} size="tiny" icon onClick={() => this.enableTool('point')}><Icon name="chevron up" /></Button> <Button className='joyride-point' data-tooltip="Point draw" color={editor.tool === 'point' && 'blue'} size="tiny" icon onClick={() => enableTool('point')}><Icon name="chevron up" /></Button>
</Button.Group> </Button.Group>
</Accordion.Content> </Accordion.Content>
<Accordion.Title active={this.drawerIsActive('palette')} onClick={e => this.activateDrawer('palette')}> <Accordion.Title active={drawerIsActive('palette')} onClick={e => activateDrawer('palette')}>
<Icon name="dropdown" /> Palette <Icon name="dropdown" /> Palette
<ColourSquare colour={utils.rgb(this.props.editor.colour)} style={{top: 4, marginLeft: 10}}/> <ColourSquare colour={utils.rgb(editor.colour)} style={{top: 4, marginLeft: 10}}/>
</Accordion.Title> </Accordion.Title>
<Accordion.Content active={this.drawerIsActive('palette')}> <Accordion.Content active={drawerIsActive('palette')}>
{this.props.pattern.colours && this.props.pattern.colours.map(colour => {pattern.colours && pattern.colours.map(colour =>
<ColourSquare key={colour} colour={utils.rgb(colour)} onClick={() => this.setColour(colour)} /> <ColourSquare key={colour} colour={utils.rgb(colour)} onClick={() => setColour(colour)} />
)} )}
</Accordion.Content> </Accordion.Content>
<Accordion.Title active={this.drawerIsActive('advanced')} onClick={e => this.activateDrawer('advanced')}> <Accordion.Title active={drawerIsActive('advanced')} onClick={e => activateDrawer('advanced')}>
<Icon name="dropdown" /> Advanced <Icon name="dropdown" /> Advanced
</Accordion.Title> </Accordion.Title>
<Accordion.Content active={this.drawerIsActive('advanced')}> <Accordion.Content active={drawerIsActive('advanced')}>
<Button size="small" basic color="red" fluid onClick={e => this.setState({ deleteModalOpen: true })}>Delete pattern</Button> <Button size="small" basic color="red" fluid onClick={e => setDeleteModalOpen(true)}>Delete pattern</Button>
<Confirm <Confirm
open={this.state.deleteModalOpen} open={deleteModalOpen}
content="Really delete this pattern?" content="Really delete this pattern?"
onCancel={e => this.setState({ deleteModalOpen: false })} onCancel={e => setDeleteModalOpen(false)}
onConfirm={this.deleteObject} onConfirm={deleteObject}
/> />
</Accordion.Content> </Accordion.Content>
</Accordion> </Accordion>
</Segment> </Segment>
</div> </div>
); );
}
} }
const mapStateToProps = (state, ownProps) => { export default Tools;
const { username, projectPath } = ownProps.match.params;
let project = {};
state.projects.projects.forEach((p) => {
if (p.path === projectPath && p.owner && p.owner.username === username) project = p;
});
return { project, fullName: `${username}/${projectPath}`, editor: state.objects.editor };
};
const mapDispatchToProps = dispatch => ({
onEditorUpdated: editor => dispatch(actions.objects.updateEditor(editor)),
onObjectDeleted: id => dispatch(actions.objects.delete(id)),
});
const ToolsContainer = withRouter(connect(
mapStateToProps, mapDispatchToProps,
)(Tools));
export default ToolsContainer;

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { connect } from 'react-redux'; import { useSelector } from 'react-redux';
import utils from 'utils/utils.js'; import utils from 'utils/utils.js';
@ -17,157 +17,154 @@ const StyledWarp = styled.div`
} }
`; `;
class Warp extends Component { const squares = {};
constructor(props) { const markers = {};
super(props);
this.squares = {};
this.markers = {};
}
componentDidUpdate(prevProps, prevState) {
this.paintDrawdown();
}
componentDidMount() {
this.paintDrawdown();
}
getThreadShaft = (event) => { function Warp({ baseSize, cellStyle, warp, weft, updatePattern }) {
const [draggingColourway, setDraggingColourway] = useState(false);
const [dragging, setDragging] = useState(false);
const [startShaft, setStartShaft] = useState();
const [startThread, setStartThread] = useState();
const { editor } = useSelector(state => ({ editor: state.objects.editor }));
useEffect(() => paintDrawdown());
const warpRef = useRef(null);
const colourwayRef = useRef(null);
const getThreadShaft = (event) => {
const rect = event.currentTarget.getBoundingClientRect(); const rect = event.currentTarget.getBoundingClientRect();
const y = event.clientY - rect.top; const y = event.clientY - rect.top;
const x = 0 - (event.clientX - rect.right); const x = 0 - (event.clientX - rect.right);
const shaft = this.props.warp.shafts - parseInt(y / this.props.baseSize); const shaft = warp.shafts - parseInt(y / baseSize);
const thread = parseInt(x / this.props.baseSize); const thread = parseInt(x / baseSize);
return { shaft, thread }; return { shaft, thread };
} };
mouseClickColourway = event => { const mouseClickColourway = event => {
const warp = Object.assign({}, this.props.warp); const newWarp = Object.assign({}, warp);
const { thread } = this.getThreadShaft(event); const { thread } = getThreadShaft(event);
if (thread >= warp.threading.length) this.fillUpTo(warp, thread); if (thread >= warp.threading.length) fillUpTo(newWarp, thread);
warp.threading[thread].colour = this.props.colour; newWarp.threading[thread].colour = editor.colour;
this.props.updatePattern({ warp }); updatePattern({ warp: newWarp });
} };
mouseDownColourway = event => { const mouseDownColourway = event => {
event.preventDefault(); event.preventDefault();
this.draggingColourway = true; setDraggingColourway(true);
} };
mouseUpColourway = event => this.draggingColourway = false; const mouseUpColourway = event => setDraggingColourway(false);
mouseMoveColourway = (event) => { const mouseMoveColourway = (event) => {
if (this.draggingColourway) { if (draggingColourway) {
const warp = Object.assign({}, this.props.warp); const newWarp = Object.assign({}, warp);
const { thread } = this.getThreadShaft(event); const { thread } = getThreadShaft(event);
if (thread >= warp.threading.length) this.fillUpTo(warp, thread); if (thread >= warp.threading.length) fillUpTo(newWarp, thread);
warp.threading[thread].colour = this.props.colour; newWarp.threading[thread].colour = editor.colour;
this.props.updatePattern({ warp }); updatePattern({ warp: newWarp });
}
} }
};
mouseUp = event => this.dragging = false; const mouseUp = event => setDragging(false);
mouseDown = (event) => { const mouseDown = (event) => {
event.preventDefault(); event.preventDefault();
const { shaft, thread } = this.getThreadShaft(event); const { shaft, thread } = getThreadShaft(event);
this.startShaft = shaft; setStartShaft(shaft);
this.startThread = thread; setStartThread(thread);
this.dragging = true; setDragging(true);
} };
mouseMove = (event) => { const mouseMove = (event) => {
if (this.dragging && this.props.tool) { if (dragging && editor.tool) {
const warp = Object.assign({}, this.props.warp); const newWarp = Object.assign({}, warp);
const { shaft, thread } = this.getThreadShaft(event); const { shaft, thread } = getThreadShaft(event);
let lX = this.startThread; let hX = thread; let lY = this.startShaft; let let lX = startThread; let hX = thread; let lY = startShaft; let hY = shaft;
hY = shaft; let xDirection = 1; let yDirection = 1;
let xDirection = 1; let if (thread < startThread) {
yDirection = 1;
if (thread < this.startThread) {
lX = thread; lX = thread;
hX = this.startThread; hX = startThread;
xDirection = -1; xDirection = -1;
} }
if (shaft < this.startShaft) { if (shaft < startShaft) {
lY = shaft; lY = shaft;
hY = this.startShaft; hY = startShaft;
yDirection = -1; yDirection = -1;
} }
let x = xDirection > 0 ? lX : hX; let x = xDirection > 0 ? lX : hX;
let y = yDirection > 0 ? lY : hY; let y = yDirection > 0 ? lY : hY;
if (this.props.tool === 'colour') { if (editor.tool === 'colour') {
if (thread >= warp.threading.length) this.fillUpTo(warp, thread); if (thread >= warp.threading.length) fillUpTo(newWarp, thread);
warp.threading[thread].colour = this.props.colour; newWarp.threading[thread].colour = editor.colour;
} }
if (this.props.tool === 'straight') { if (editor.tool === 'straight') {
while (x <= hX && x >= lX) { while (x <= hX && x >= lX) {
if (x >= warp.threading.length || warp.threading.length - x < 5) this.fillUpTo(warp, x + 5); if (x >= warp.threading.length || warp.threading.length - x < 5) fillUpTo(newWarp, x + 5);
warp.threading[x].shaft = y; newWarp.threading[x].shaft = y;
x += xDirection; x += xDirection;
y += yDirection; y += yDirection;
if (y > hY || y < lY) y = yDirection > 0 ? lY : hY; if (y > hY || y < lY) y = yDirection > 0 ? lY : hY;
} }
} }
if (this.props.tool === 'point') { if (editor.tool === 'point') {
while (x <= hX && x >= lX) { while (x <= hX && x >= lX) {
if (x >= warp.threading.length || warp.threading.length - x < 5) this.fillUpTo(warp, x + 5); if (x >= warp.threading.length || warp.threading.length - x < 5) fillUpTo(newWarp, x + 5);
warp.threading[x].shaft = y; newWarp.threading[x].shaft = y;
x += xDirection; x += xDirection;
y += yDirection; y += yDirection;
if (y > hY || y <= lY) yDirection = 0 - yDirection; if (y > hY || y <= lY) yDirection = 0 - yDirection;
} }
} }
this.props.updatePattern({ warp }); updatePattern({ warp: newWarp });
} }
} };
click = (event) => { const click = (event) => {
if (this.props.tool === 'point' || this.props.tool === 'straight') { if (editor.tool === 'point' || editor.tool === 'straight') {
const { thread, shaft } = this.getThreadShaft(event); const { thread, shaft } = getThreadShaft(event);
const warp = Object.assign({}, this.props.warp); const newWarp = Object.assign({}, warp);
if (thread > warp.threading.length || warp.threading.length - thread < 5) this.fillUpTo(warp, thread + 5); if (thread > warp.threading.length || warp.threading.length - thread < 5) fillUpTo(newWarp, thread + 5);
const warpThread = warp.threading[thread]; const warpThread = newWarp.threading[thread];
warpThread.shaft = warpThread.shaft === shaft ? 0 : shaft; warpThread.shaft = warpThread.shaft === shaft ? 0 : shaft;
this.props.updatePattern({ warp }); updatePattern({ warp: newWarp });
}
} }
};
fillUpTo = (warp, limit) => { const fillUpTo = (w, limit) => {
let i = warp.threading.length; let i = warp.threading.length;
while (i <= limit) { while (i <= limit) {
warp.threading.push({ shaft: 0 }); w.threading.push({ shaft: 0 });
warp.threads++; w.threads++;
i++; i++;
} }
} };
const getMarker = (size) => {
getMarker(size) { if (markers[size]) return markers[size];
if (this.markers[size]) return this.markers[size];
const m_canvas = document.createElement('canvas'); const m_canvas = document.createElement('canvas');
m_canvas.width = this.props.baseSize; m_canvas.width = baseSize;
m_canvas.height = this.props.baseSize; m_canvas.height = baseSize;
const mc = m_canvas.getContext('2d'); const mc = m_canvas.getContext('2d');
mc.fillStyle = 'black'; mc.fillStyle = 'black';
mc.fillRect(0, 0, this.props.baseSize, this.props.baseSize); mc.fillRect(0, 0, baseSize, baseSize);
this.markers[size] = m_canvas; markers[size] = m_canvas;
return m_canvas; return m_canvas;
} };
getSquare(size, colour) { const getSquare = (size, colour) => {
if (this.squares[size] && this.squares[size][colour]) return this.squares[size][colour]; if (squares[size] && squares[size][colour]) return squares[size][colour];
const m_canvas = document.createElement('canvas'); const m_canvas = document.createElement('canvas');
m_canvas.width = this.props.baseSize; m_canvas.width = baseSize;
m_canvas.height = 10; m_canvas.height = 10;
const mc = m_canvas.getContext('2d'); const mc = m_canvas.getContext('2d');
mc.fillStyle = utils.rgb(colour); mc.fillStyle = utils.rgb(colour);
mc.fillRect(0, 0, this.props.baseSize, 10); mc.fillRect(0, 0, baseSize, 10);
if (!this.squares[size]) this.squares[size] = {}; if (!squares[size]) squares[size] = {};
this.squares[size][colour] = m_canvas; squares[size][colour] = m_canvas;
return m_canvas; return m_canvas;
} };
paintDrawdown() { const paintDrawdown = () => {
const canvas = this.refs.warp; const canvas = warpRef.current;
const colourway = this.refs.colourway; const colourway = colourwayRef.current;
const ctx = canvas.getContext('2d');// , { alpha: false }); const ctx = canvas.getContext('2d');// , { alpha: false });
const ctx2 = colourway.getContext('2d');// , { alpha: false }); const ctx2 = colourway.getContext('2d');// , { alpha: false });
const { baseSize, warp } = this.props;
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
@ -184,52 +181,40 @@ class Warp extends Component {
for (let thread = 0; thread < warp.threads; thread++) { for (let thread = 0; thread < warp.threads; thread++) {
const shaft = warp.threading[thread].shaft; const shaft = warp.threading[thread].shaft;
const marker = this.getMarker(baseSize); const marker = getMarker(baseSize);
ctx.drawImage(marker, canvas.width - ((thread + 1) * this.props.baseSize), canvas.height - (shaft * this.props.baseSize)); ctx.drawImage(marker, canvas.width - ((thread + 1) * baseSize), canvas.height - (shaft * baseSize));
const colourSquare = this.getSquare(baseSize, warp.threading[thread].colour || warp.defaultColour); const colourSquare = getSquare(baseSize, warp.threading[thread].colour || warp.defaultColour);
ctx2.drawImage(colourSquare, canvas.width - ((thread + 1) * this.props.baseSize), 0); ctx2.drawImage(colourSquare, canvas.width - ((thread + 1) * baseSize), 0);
}
} }
};
render() {
const { warp, weft, baseSize } = this.props;
return ( return (
<StyledWarp treadles={weft.treadles} shafts={warp.shafts} baseSize={baseSize}> <StyledWarp treadles={weft.treadles} shafts={warp.shafts} baseSize={baseSize}>
<canvas className='warp-colourway joyride-warpColourway' ref="colourway" width={warp.threading.length * baseSize} height={10} <canvas className='warp-colourway joyride-warpColourway' ref={colourwayRef} width={warp.threading.length * baseSize} height={10}
style={{ style={{
position: 'absolute', top: 0, right: 0, height: 10, width: warp.threading.length * baseSize, position: 'absolute', top: 0, right: 0, height: 10, width: warp.threading.length * baseSize,
}} }}
onClick={this.mouseClickColourway} onClick={mouseClickColourway}
onMouseDown={this.mouseDownColourway} onMouseDown={mouseDownColourway}
onMouseMove={this.mouseMoveColourway} onMouseMove={mouseMoveColourway}
onMouseUp={this.mouseUpColourway} onMouseUp={mouseUpColourway}
onMouseLeave={this.mouseUpColourway} onMouseLeave={mouseUpColourway}
/> />
<canvas className='warp-threads joyride-warp' ref="warp" width={warp.threading.length * baseSize} height={warp.shafts * baseSize} <canvas className='warp-threads joyride-warp' ref={warpRef} width={warp.threading.length * baseSize} height={warp.shafts * baseSize}
style={{ style={{
position: 'absolute', top: 10, right: 0, position: 'absolute', top: 10, right: 0,
height: warp.shafts * baseSize, height: warp.shafts * baseSize,
width: warp.threading.length * baseSize, borderRadius: 4, width: warp.threading.length * baseSize, borderRadius: 4,
boxShadow: '0px 0px 10px rgba(0,0,0,0.15)', boxShadow: '0px 0px 10px rgba(0,0,0,0.15)',
}} }}
onClick={this.click} onClick={click}
onMouseDown={this.mouseDown} onMouseDown={mouseDown}
onMouseMove={this.mouseMove} onMouseMove={mouseMove}
onMouseUp={this.mouseUp} onMouseUp={mouseUp}
onMouseLeave={this.mouseUp} onMouseLeave={mouseUp}
/> />
</StyledWarp> </StyledWarp>
); );
}
} }
const mapStateToProps = (state, ownProps) => state.objects.editor; export default Warp;
const mapDispatchToProps = dispatch => ({
});
const WarpContainer = connect(
mapStateToProps, mapDispatchToProps,
)(Warp);
export default WarpContainer;

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { connect } from 'react-redux'; import { useSelector } from 'react-redux';
import utils from 'utils/utils.js'; import utils from 'utils/utils.js';
const StyledWeft = styled.div` const StyledWeft = styled.div`
@ -18,172 +18,156 @@ const StyledWeft = styled.div`
} }
`; `;
class Weft extends Component { // Cache
constructor(props) { const squares = {};
super(props); const markers = {};
this.squares = {};
this.markers = {};
}
componentDidUpdate(prevProps, prevState) {
this.paintDrawdown();
}
componentDidMount() {
this.paintDrawdown();
}
toggleWeft = (treadle, threadCount) => { function Weft({ cellStyle, warp, weft, baseSize, updatePattern }) {
const weft = Object.assign({}, this.props.weft); const [draggingColourway, setDraggingColourway] = useState(false);
const thread = weft.treadling[threadCount]; const [dragging, setDragging] = useState(false);
thread.treadle = thread.treadle === treadle ? 0 : treadle; const [startTreadle, setStartTreadle] = useState();
this.props.updatePattern({ weft }); const [startThread, setStartThread] = useState();
}
changeWeftColour = (threadIndex) => { const { editor } = useSelector(state => ({ editor: state.objects.editor }));
const weft = Object.assign({}, this.props.weft); useEffect(() => paintDrawdown());
const colour = this.props.colour; const weftRef = useRef(null);
if (colour) { const colourwayRef = useRef(null);
weft.treadling[threadIndex].colour = colour;
this.props.updatePattern({ weft });
}
}
getThreadTreadle = (event) => { const getThreadTreadle = (event) => {
const rect = event.currentTarget.getBoundingClientRect(); const rect = event.currentTarget.getBoundingClientRect();
const y = event.clientY - rect.top; const y = event.clientY - rect.top;
const x = (event.clientX - rect.left); const x = (event.clientX - rect.left);
const thread = parseInt(y / this.props.baseSize) + 1; const thread = parseInt(y / baseSize) + 1;
const treadle = parseInt(x / this.props.baseSize); const treadle = parseInt(x / baseSize);
return { treadle, thread }; return { treadle, thread };
} };
mouseClickColourway = event => { const mouseClickColourway = event => {
const weft = Object.assign({}, this.props.weft); const newWeft = Object.assign({}, weft);
const { thread } = this.getThreadTreadle(event); const { thread } = getThreadTreadle(event);
if (thread >= weft.treadling.length) this.fillUpTo(weft, thread); if (thread >= weft.treadling.length) fillUpTo(newWeft, thread);
weft.treadling[thread - 1].colour = this.props.colour; newWeft.treadling[thread - 1].colour = editor.colour;
this.props.updatePattern({ weft }); updatePattern({ weft: newWeft });
} };
mouseDownColourway = event => { const mouseDownColourway = event => {
event.preventDefault(); event.preventDefault();
this.draggingColourway = true; setDraggingColourway(true);
} };
mouseUpColourway = event => this.draggingColourway = false; const mouseUpColourway = event => setDraggingColourway(false);
mouseMoveColourway = (event) => { const mouseMoveColourway = (event) => {
if (this.draggingColourway) { if (draggingColourway) {
const weft = Object.assign({}, this.props.weft); const newWeft = Object.assign({}, weft);
const { thread } = this.getThreadTreadle(event); const { thread } = getThreadTreadle(event);
if (thread >= weft.treadling.length) this.fillUpTo(weft, thread); if (thread >= weft.treadling.length) fillUpTo(newWeft, thread);
weft.treadling[thread - 1].colour = this.props.colour; newWeft.treadling[thread - 1].colour = editor.colour;
this.props.updatePattern({ weft }); updatePattern({ weft: newWeft });
}
} }
};
mouseUp = event => this.dragging = false; const mouseUp = event => setDragging(false);
mouseDown = (event) => { const mouseDown = (event) => {
event.preventDefault(); event.preventDefault();
const { treadle, thread } = this.getThreadTreadle(event); const { treadle, thread } = getThreadTreadle(event);
this.startTreadle = treadle; setStartTreadle(treadle);
this.startThread = thread; setStartThread(thread);
this.dragging = true; setDragging(true);
} };
mouseMove = (event) => { const mouseMove = (event) => {
if (this.dragging && this.props.tool) { if (dragging && editor.tool) {
const weft = Object.assign({}, this.props.weft); const newWeft = Object.assign({}, weft);
const { treadle, thread } = this.getThreadTreadle(event); const { treadle, thread } = getThreadTreadle(event);
let lX = this.startTreadle; let hX = treadle; let lY = this.startThread; let let lX = startTreadle; let hX = treadle; let lY = startThread; let hY = thread;
hY = thread;
let xDirection = 1; let let xDirection = 1; let
yDirection = 1; yDirection = 1;
if (treadle < this.startTreadle) { if (treadle < startTreadle) {
lX = treadle; lX = treadle;
hX = this.startTreadle; hX = startTreadle;
xDirection = -1; xDirection = -1;
} }
if (thread < this.startThread) { if (thread < startThread) {
lY = thread; lY = thread;
hY = this.startThread; hY = startThread;
yDirection = -1; yDirection = -1;
} }
let x = xDirection > 0 ? lX : hX; let x = xDirection > 0 ? lX : hX;
let y = yDirection > 0 ? lY : hY; let y = yDirection > 0 ? lY : hY;
if (this.props.tool === 'colour') { if (editor.tool === 'colour') {
if ((thread - 1) >= weft.treadling.length) this.fillUpTo(weft, (thread - 1)); if ((thread - 1) >= weft.treadling.length) fillUpTo(newWeft, (thread - 1));
weft.treadling[thread - 1].colour = this.props.colour; newWeft.treadling[thread - 1].colour = editor.colour;
} }
if (this.props.tool === 'straight') { if (editor.tool === 'straight') {
while (y <= hY && y >= lY) { while (y <= hY && y >= lY) {
if ((y - 1) >= weft.treadling.length || weft.treadling.length - y - 1 < 5) this.fillUpTo(weft, (y + 5)); if ((y - 1) >= weft.treadling.length || weft.treadling.length - y - 1 < 5) fillUpTo(newWeft, (y + 5));
weft.treadling[y - 1].treadle = x + 1; newWeft.treadling[y - 1].treadle = x + 1;
x += xDirection; x += xDirection;
y += yDirection; y += yDirection;
if (x > hX || x < lX) x = xDirection > 0 ? lX : hX; if (x > hX || x < lX) x = xDirection > 0 ? lX : hX;
} }
} }
if (this.props.tool === 'point') { if (editor.tool === 'point') {
while (y <= hY && y >= lY) { while (y <= hY && y >= lY) {
if ((y - 1) >= weft.treadling.length || weft.treadling.length - y -1 < 5) this.fillUpTo(weft, y + 5); if ((y - 1) >= weft.treadling.length || weft.treadling.length - y -1 < 5) fillUpTo(newWeft, y + 5);
weft.treadling[y - 1].treadle = x + 1; newWeft.treadling[y - 1].treadle = x + 1;
x += xDirection; x += xDirection;
y += yDirection; y += yDirection;
if (x > hX || x <= lX) xDirection = 0 - xDirection; if (x > hX || x <= lX) xDirection = 0 - xDirection;
} }
} }
this.props.updatePattern({ weft }); updatePattern({ weft: newWeft });
} }
} };
click = (event) => { const click = (event) => {
if (this.props.tool === 'point' || this.props.tool === 'straight') { if (editor.tool === 'point' || editor.tool === 'straight') {
let { thread, treadle } = this.getThreadTreadle(event); let { thread, treadle } = getThreadTreadle(event);
treadle += 1; treadle += 1;
const weft = Object.assign({}, this.props.weft); const newWeft = Object.assign({}, weft);
if (thread >= weft.treadling.length || weft.treadling.length - thread < 5) this.fillUpTo(weft, thread + 5); if (thread >= newWeft.treadling.length || newWeft.treadling.length - thread < 5) fillUpTo(newWeft, thread + 5);
const weftThread = weft.treadling[thread - 1]; const weftThread = newWeft.treadling[thread - 1];
weftThread.treadle = weftThread.treadle === treadle ? 0 : treadle; weftThread.treadle = weftThread.treadle === treadle ? 0 : treadle;
this.props.updatePattern({ weft }); updatePattern({ weft: newWeft });
}
} }
};
fillUpTo = (weft, limit) => { const fillUpTo = (weft, limit) => {
let i = weft.treadling.length; let i = weft.treadling.length;
while (i <= limit) { while (i <= limit) {
weft.treadling.push({ treadle: 0 }); weft.treadling.push({ treadle: 0 });
weft.threads++; weft.threads++;
i++; i++;
} }
} };
getMarker(size) { const getMarker = (size) => {
if (this.markers[size]) return this.markers[size]; if (markers[size]) return markers[size];
const m_canvas = document.createElement('canvas'); const m_canvas = document.createElement('canvas');
m_canvas.width = this.props.baseSize; m_canvas.width = baseSize;
m_canvas.height = this.props.baseSize; m_canvas.height = baseSize;
const mc = m_canvas.getContext('2d'); const mc = m_canvas.getContext('2d');
mc.fillStyle = 'black'; mc.fillStyle = 'black';
mc.fillRect(0, 0, this.props.baseSize, this.props.baseSize); mc.fillRect(0, 0, baseSize, baseSize);
this.markers[size] = m_canvas; markers[size] = m_canvas;
return m_canvas; return m_canvas;
} };
getSquare(size, colour) { const getSquare = (size, colour) => {
if (this.squares[size] && this.squares[size][colour]) return this.squares[size][colour]; if (squares[size] && squares[size][colour]) return squares[size][colour];
const m_canvas = document.createElement('canvas'); const m_canvas = document.createElement('canvas');
m_canvas.width = 10; m_canvas.width = 10;
m_canvas.height = this.props.baseSize; m_canvas.height = baseSize;
const mc = m_canvas.getContext('2d'); const mc = m_canvas.getContext('2d');
mc.fillStyle = utils.rgb(colour); mc.fillStyle = utils.rgb(colour);
mc.fillRect(0, 0, 10, this.props.baseSize); mc.fillRect(0, 0, 10, baseSize);
if (!this.squares[size]) this.squares[size] = {}; if (!squares[size]) squares[size] = {};
this.squares[size][colour] = m_canvas; squares[size][colour] = m_canvas;
return m_canvas; return m_canvas;
} };
paintDrawdown() { const paintDrawdown = () => {
const canvas = this.refs.weft; const canvas = weftRef.current;
const colourway = this.refs.colourway; const colourway = colourwayRef.current;
const ctx = canvas.getContext('2d');// , { alpha: false }); const ctx = canvas.getContext('2d');// , { alpha: false });
const ctx2 = colourway.getContext('2d');// , { alpha: false }); const ctx2 = colourway.getContext('2d');// , { alpha: false });
const { baseSize, weft } = this.props;
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
@ -200,49 +184,37 @@ class Weft extends Component {
for (let thread = 0; thread < weft.threads; thread++) { for (let thread = 0; thread < weft.threads; thread++) {
const treadle = weft.treadling[thread].treadle; const treadle = weft.treadling[thread].treadle;
const marker = this.getMarker(baseSize); const marker = getMarker(baseSize);
ctx.drawImage(marker, ((treadle - 1) * this.props.baseSize), ((thread) * this.props.baseSize)); ctx.drawImage(marker, ((treadle - 1) * baseSize), ((thread) * baseSize));
const colourSquare = this.getSquare(baseSize, weft.treadling[thread].colour || weft.defaultColour); const colourSquare = getSquare(baseSize, weft.treadling[thread].colour || weft.defaultColour);
ctx2.drawImage(colourSquare, 0, (thread * this.props.baseSize)); ctx2.drawImage(colourSquare, 0, (thread * baseSize));
} }
} }
render() {
const { warp, weft, baseSize } = this.props;
return ( return (
<StyledWeft baseSize={baseSize} treadles={weft.treadles} shafts={warp.shafts} threads={weft.threads}> <StyledWeft baseSize={baseSize} treadles={weft.treadles} shafts={warp.shafts} threads={weft.threads}>
<canvas className='weft-colourway' ref="colourway" width={10} height={weft.threads * baseSize} <canvas className='weft-colourway' ref={colourwayRef} width={10} height={weft.threads * baseSize}
style={{ position: 'absolute', top: 0, right: 0, width: 10, height: weft.threads * baseSize}} style={{ position: 'absolute', top: 0, right: 0, width: 10, height: weft.threads * baseSize}}
onClick={this.mouseClickColourway} onClick={mouseClickColourway}
onMouseDown={this.mouseDownColourway} onMouseDown={mouseDownColourway}
onMouseMove={this.mouseMoveColourway} onMouseMove={mouseMoveColourway}
onMouseUp={this.mouseUpColourway} onMouseUp={mouseUpColourway}
onMouseLeave={this.mouseUpColourway} onMouseLeave={mouseUpColourway}
/> />
<canvas className='weft-threads joyride-weft' ref="weft" width={weft.treadles * baseSize} height={weft.threads * baseSize} <canvas className='weft-threads joyride-weft' ref={weftRef} width={weft.treadles * baseSize} height={weft.threads * baseSize}
style={{ style={{
position: 'absolute', position: 'absolute',
top: 0, right: 10, height: weft.threads * baseSize, width: weft.treadles * baseSize, top: 0, right: 10, height: weft.threads * baseSize, width: weft.treadles * baseSize,
borderRadius: 4, boxShadow: '0px 0px 10px rgba(0,0,0,0.15)', borderRadius: 4, boxShadow: '0px 0px 10px rgba(0,0,0,0.15)',
}} }}
onClick={this.click} onClick={click}
onMouseDown={this.mouseDown} onMouseDown={mouseDown}
onMouseMove={this.mouseMove} onMouseMove={mouseMove}
onMouseUp={this.mouseUp} onMouseUp={mouseUp}
onMouseLeave={this.mouseUp} onMouseLeave={mouseUp}
/> />
</StyledWeft> </StyledWeft>
); );
}
} }
const mapStateToProps = (state, ownProps) => state.objects.editor; export default Weft;
const mapDispatchToProps = dispatch => ({
});
const WeftContainer = connect(
mapStateToProps, mapDispatchToProps,
)(Weft);
export default WeftContainer;

View File

@ -1,13 +1,18 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Table, Container } from 'semantic-ui-react'; import { Table, Container } from 'semantic-ui-react';
import { Link, withRouter } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { connect } from 'react-redux'; import { useSelector } from 'react-redux';
import moment from 'moment'; import moment from 'moment';
import api from 'api'; import api from 'api';
function Root({ user }) { function Root() {
const [users, setUsers] = useState([]); const [users, setUsers] = useState([]);
const { user } = useSelector(state => {
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
return { user };
});
useEffect(() => { useEffect(() => {
if (!(user?.roles?.indexOf('root') > -1)) return; if (!(user?.roles?.indexOf('root') > -1)) return;
api.root.getUsers(({ users}) => { api.root.getUsers(({ users}) => {
@ -60,15 +65,4 @@ function Root({ user }) {
); );
} }
const mapStateToProps = (state, ownProps) => { export default Root;
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
return { user };
};
const mapDispatchToProps = dispatch => ({
});
const RootContainer = withRouter(connect(
mapStateToProps, mapDispatchToProps,
)(Root));
export default RootContainer;

View File

@ -1,22 +1,29 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Message, Form, Divider, Segment, Icon } from 'semantic-ui-react'; import { Message, Form, Divider, Segment, Icon } from 'semantic-ui-react';
import { withRouter } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { connect } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import actions from 'actions'; import actions from 'actions';
import api from 'api'; import api from 'api';
function AccountSettings({ user, onLogout, history, onReceiveUser }) { function AccountSettings() {
const [newEmail, setNewEmail] = useState(''); const [newEmail, setNewEmail] = useState('');
const [existingPassword, setExistingPassword] = useState(''); const [existingPassword, setExistingPassword] = useState('');
const [newPassword, setNewPassword] = useState(''); const [newPassword, setNewPassword] = useState('');
const [deletePassword, setDeletePassword] = useState(''); const [deletePassword, setDeletePassword] = useState('');
const navigate = useNavigate();
const dispatch = useDispatch();
const { user } = useSelector(state => {
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
return { user };
});
const updateEmail = () => { const updateEmail = () => {
api.accounts.updateEmail(newEmail, data => { api.accounts.updateEmail(newEmail, data => {
setNewEmail(''); setNewEmail('');
onReceiveUser(Object.assign({}, user, { email: data.email })); dispatch(actions.users.receive(Object.assign({}, user, { email: data.email })));
}, err => toast.error(err.message)); }, err => toast.error(err.message));
} }
@ -32,8 +39,8 @@ function AccountSettings({ user, onLogout, history, onReceiveUser }) {
const confirm = window.confirm('Really delete your account?'); const confirm = window.confirm('Really delete your account?');
if (!confirm) return; if (!confirm) return;
api.accounts.delete(deletePassword, () => { api.accounts.delete(deletePassword, () => {
api.auth.logout(onLogout); api.auth.logout(() => dispatch(actions.auth.logout()));
history.push('/'); navigate('/');
toast.info('Sorry to see you go'); toast.info('Sorry to see you go');
}, err => toast.error(err.message)); }, err => toast.error(err.message));
} }
@ -81,16 +88,4 @@ function AccountSettings({ user, onLogout, history, onReceiveUser }) {
); );
} }
const mapStateToProps = (state, ownProps) => { export default AccountSettings;
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
return { user };
};
const mapDispatchToProps = dispatch => ({
onReceiveUser: user => dispatch(actions.users.receive(user)),
onLogout: () => dispatch(actions.auth.logout()),
});
const AccountSettingsContainer = withRouter(connect(
mapStateToProps, mapDispatchToProps,
)(AccountSettings));
export default AccountSettingsContainer;

View File

@ -1,214 +0,0 @@
import React, { Component } from 'react';
import { Loader, Dropdown, Modal, Icon, Message, Divider, Segment, Button, Card } from 'semantic-ui-react';
import { withRouter } from 'react-router-dom';
import { toast } from 'react-toastify';
import { connect } from 'react-redux';
import utils from 'utils/utils.js';
import actions from 'actions';
import api from 'api';
class BillingSettings extends Component {
constructor(props) {
super(props);
this.state = { card: null, planId: null };
}
componentDidMount() {
api.billing.get(({ planId, card }) => this.setState({ planId, card }));
}
getCardIcon = (brand) => {
if (brand === 'Visa') return 'visa';
if (brand === 'MasterCard') return 'mastercard';
if (brand === 'American Express') return 'amex';
return 'credit card';
}
loadStripe = () => {
this.setState({ showCardModal: true });
this.stripe = window.Stripe(process.env.REACT_APP_STRIPE_KEY);
const elements = this.stripe.elements(); this.card = elements.create('card', {
style: {
base: {
color: '#32325d',
lineHeight: '25px',
fontFamily: '"Lato", sans-serif',
fontSize: '20px',
'::placeholder': {
color: 'rgb(180,180,180)',
},
},
invalid: {
color: '#fa755a',
iconColor: '#fa755a',
},
},
});
setTimeout(() => this.card.mount('#stripe-card'), 500);
}
saveCard = () => {
this.setState({ cardLoading: true });
this.stripe.createToken(this.card).then((result) => {
if (result.error) {
this.setState({ cardLoading: false });
} else {
api.billing.updateCard(result, ({ card }) => {
this.setState({ showCardModal: false, cardLoading: false, card });
toast.info('Card updated successfully');
}, (err) => {
toast.error(err.message);
this.setState({ cardLoading: false });
});
}
});
}
deleteCard = () => {
utils.confirm('Really delete this payment card?', 'We recommend keeping a card on your account so you don\'t lose access to your projects when your plan is due for renewal.').then(() => {
this.setState({ cardLoading: true });
api.billing.deleteCard(() => {
this.setState({ card: null, cardLoading: false });
toast.info('Payment card deleted');
}, (err) => {
toast.error(err.message);
this.setState({ cardLoading: false });
});
}, () => {});
}
selectPlan = (planId) => {
const text = planId === 'free'
? 'Switching to the free plan will cancel your current subscription. Your account will be updated right away and you will not be charged again until you re-subscribe to a paid plan. Please note that you will lose access to paid-for features and may lose access to private projects.'
: 'Thank you for changing your plan! Your account will be updated right away. We will charge your card now, but if you are switching from another paid plan the payment will be prorated.';
utils.confirm('Changing your plan', `${text} Do you want to continue?`).then(() => {
this.setState({ planLoading: true });
api.billing.selectPlan(planId, (response) => {
this.setState({ planId: response.planId, planLoading: false });
this.props.onUpdatePlan(response.planId);
toast.info('Your plan was updated successfully');
}, (err) => {
this.setState({ planLoading: false });
toast.error(err.message);
});
}, () => {});
}
render() {
const { showCardModal, cardLoading, planLoading, planId, card } = this.state;
return (
<div>
<Segment>
<h3>Plan</h3>
<Card.Group stackable centered itemsPerRow={2} style={{ marginTop: 30, marginBottom: 30 }}>
{utils.plans.map(p => (
<Card key={p.id}>
<Card.Content>
<Card.Header textAlign="center">{p.name}</Card.Header>
<Card.Meta textAlign="center">{p.description}</Card.Meta>
<Card.Meta textAlign="center">
<strong>
{p.price} per month
</strong>
</Card.Meta>
<Divider hidden />
{p.features.map((f, i) => (
<p key={i}>
<Icon style={{ color: 'green' }} name="check circle outline" /> {f}
</p>
))}
</Card.Content>
<Card.Content extra>
{p.id === (planId || 'free')
? <Button color="teal" fluid icon="check" content="Selected" loading={planLoading} />
: <Button color="teal" basic fluid content="Activate" onClick={e => this.selectPlan(p.id)} loading={planLoading} />
}
</Card.Content>
</Card>
))}
</Card.Group>
</Segment>
<Segment>
<h3>Payment method</h3>
{card
? (
<Card color="green">
<Loader active={cardLoading} />
<Card.Content>
<h2><Icon name={`cc ${this.getCardIcon(card.brand)}`} /></h2>
<h3 style={{fontFamily: 'Monospace'}}>
**** **** **** {card.last4}
</h3>
</Card.Content>
<Card.Content extra>
<span style={{fontFamily: 'Monospace'}}>Expires {card.exp_month}/{card.exp_year}</span>
<Dropdown text="Options" style={{ float: 'right' }}>
<Dropdown.Menu>
<Dropdown.Item content="Update card details" icon="credit card" onClick={this.loadStripe} />
<Dropdown.Item content="Delete card" icon="trash" onClick={this.deleteCard} />
</Dropdown.Menu>
</Dropdown>
</Card.Content>
</Card>
)
: (
<Message>
<Message.Header>
<Icon name="credit card" /> You don't currently have a payment method setup
</Message.Header>
<p>To sign-up for a paid plan, or to ensure your access to your Treadl projects is un-interrupted, please add a payment card to your account.</p>
<h4>
<Icon name="visa" />
<Icon name="mastercard" />
<Icon name="amex" />
<Icon name="diners club" />
</h4>
<Button color="yellow" icon="credit card" content="Add a payment card" onClick={this.loadStripe} />
</Message>
)
}
<Modal open={showCardModal}>
<Modal.Header>Add a card to your account</Modal.Header>
<Modal.Content image>
<Modal.Description>
<Message>
<Message.Content>
<Message.Header>
<Icon name="lock" /> Secure payments
</Message.Header>
<p>We use Stripe <Icon name="stripe" /> to handle payments. Treadl never sees or stores your full card details.</p>
</Message.Content>
</Message>
<Divider hidden />
<p><strong>Your card details</strong></p>
<div id="stripe-card" />
<Divider hidden />
<p>Adding a card does not mean you will be charged right away. We just use it to collect payment when you're on a paid-for plan.</p>
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button basic content="Cancel" onClick={e => this.setState({ showCardModal: false })} />
<Button color="teal" content="Save card to your account" onClick={this.saveCard} loading={cardLoading} />
</Modal.Actions>
</Modal>
</Segment>
</div>
);
}
}
const mapStateToProps = (state, ownProps) => {
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
return { user };
};
const mapDispatchToProps = dispatch => ({
onUpdatePlan: plan => dispatch(actions.auth.updatePlan(plan)),
});
const BillingSettingsContainer = withRouter(connect(
mapStateToProps, mapDispatchToProps,
)(BillingSettings));
export default BillingSettingsContainer;

View File

@ -1,27 +1,27 @@
import React, { Component } from 'react'; import React, { useState } from 'react';
import { Message, Input, Segment, Button } from 'semantic-ui-react'; import { Message, Input, Segment, Button } from 'semantic-ui-react';
import { withRouter } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { connect } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import actions from 'actions'; import actions from 'actions';
import api from 'api'; import api from 'api';
class IdentitySettings extends Component { function IdentitySettings() {
constructor(props) { const [newUsername, setNewUsername] = useState('');
super(props); const dispatch = useDispatch();
this.state = { newUsername: '' }; const { user } = useSelector(state => {
} const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
return { user };
});
updateUsername = () => { const updateUsername = () => {
api.users.update(this.props.user.username, { username: this.state.newUsername }, (user) => { api.users.update(user.username, { username: newUsername }, (user) => {
this.props.onUpdateUsername(this.props.user._id, this.state.newUsername); dispatch(actions.users.updateUsername(user._id, newUsername));
toast.info('Username updated'); toast.info('Username updated');
this.setState({ newUsername: '' }); setNewUsername('');
}, err => toast.error(err.message)); }, err => toast.error(err.message));
} };
render() {
return ( return (
<Segment raised color="blue"> <Segment raised color="blue">
<h3>Username</h3> <h3>Username</h3>
@ -33,24 +33,12 @@ class IdentitySettings extends Component {
<p>If you change your username, your old one will become available to be taken by somebody else.</p> <p>If you change your username, your old one will become available to be taken by somebody else.</p>
</Message> </Message>
<Input <Input
type="text" value={this.state.newUsername} type="text" value={newUsername}
onChange={e => this.setState({ newUsername: e.target.value })} onChange={e => setNewUsername(e.target.value)}
action=<Button color="yellow" content="Set new username" onClick={this.updateUsername} /> action=<Button color="yellow" content="Set new username" onClick={updateUsername} />
/> />
</Segment> </Segment>
); );
}
} }
const mapStateToProps = (state, ownProps) => { export default IdentitySettings;
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
return { user };
};
const mapDispatchToProps = dispatch => ({
onUpdateUsername: (id, username) => dispatch(actions.users.updateUsername(id, username)),
});
const IdentitySettingsContainer = withRouter(connect(
mapStateToProps, mapDispatchToProps,
)(IdentitySettings));
export default IdentitySettingsContainer;

View File

@ -1,9 +1,7 @@
import React, { Component } from 'react'; import React from 'react';
import { Label, Table, Checkbox, Divider, Segment } from 'semantic-ui-react'; import { Label, Table, Checkbox, Divider, Segment } from 'semantic-ui-react';
import { withRouter } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { connect } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import actions from 'actions'; import actions from 'actions';
import api from 'api'; import api from 'api';
import utils from 'utils/utils.js'; import utils from 'utils/utils.js';
@ -16,19 +14,22 @@ const subs = [
{ key: 'projects.commented', text: 'Someone comments on one of your projects'}, { key: 'projects.commented', text: 'Someone comments on one of your projects'},
]; ];
class NotificationSettings extends Component { function NotificationSettings() {
const dispatch = useDispatch();
const { user, groups } = useSelector(state => {
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
return { user, groups };
});
hasEmailSub = (key) => utils.hasSubscription(this.props.user, key); const hasEmailSub = (key) => utils.hasSubscription(user, key);
toggleEmailSub = (key, enable) => { const toggleEmailSub = (key, enable) => {
const { user, onSubsUpdated } = this.props;
if (enable) if (enable)
api.users.createEmailSubscription(user.username, key, ({ subscriptions }) => onSubsUpdated(user._id, subscriptions), err => toast.error(err.message)); api.users.createEmailSubscription(user.username, key, ({ subscriptions }) => dispatch(actions.users.updateSubscriptions(user._id, subscriptions)), err => toast.error(err.message));
else else
api.users.deleteEmailSubscription(user.username, key, ({ subscriptions }) => onSubsUpdated(user._id, subscriptions), err => toast.error(err.message)); api.users.deleteEmailSubscription(user.username, key, ({ subscriptions }) => dispatch(actions.users.updateSubscriptions(user._id, subscriptions)), err => toast.error(err.message));
} };
render() {
const { groups } = this.props;
return ( return (
<Segment raised color="blue"> <Segment raised color="blue">
<h3>Email preferences</h3> <h3>Email preferences</h3>
@ -52,32 +53,19 @@ class NotificationSettings extends Component {
{subs.map(s => {subs.map(s =>
<Table.Row key={s.key}> <Table.Row key={s.key}>
<Table.Cell>{s.text}</Table.Cell> <Table.Cell>{s.text}</Table.Cell>
<Table.Cell><Checkbox toggle onChange={(e, c) => this.toggleEmailSub(s.key, c.checked)} checked={this.hasEmailSub(s.key)}/></Table.Cell> <Table.Cell><Checkbox toggle onChange={(e, c) => toggleEmailSub(s.key, c.checked)} checked={hasEmailSub(s.key)}/></Table.Cell>
</Table.Row> </Table.Row>
)} )}
{groups.map(g => {groups.map(g =>
<Table.Row key={g._id}> <Table.Row key={g._id}>
<Table.Cell>Someone writes in the Notice Board of {g.name}</Table.Cell> <Table.Cell>Someone writes in the Notice Board of {g.name}</Table.Cell>
<Table.Cell><Checkbox toggle onChange={(e, c) => this.toggleEmailSub(`groupFeed-${g._id}`, c.checked)} checked={this.hasEmailSub(`groupFeed-${g._id}`)}/></Table.Cell> <Table.Cell><Checkbox toggle onChange={(e, c) => toggleEmailSub(`groupFeed-${g._id}`, c.checked)} checked={hasEmailSub(`groupFeed-${g._id}`)}/></Table.Cell>
</Table.Row> </Table.Row>
)} )}
</Table.Body> </Table.Body>
</Table> </Table>
</Segment> </Segment>
); );
}
} }
const mapStateToProps = (state, ownProps) => { export default NotificationSettings;
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
return { user, groups };
};
const mapDispatchToProps = dispatch => ({
onSubsUpdated: (id, subs) => dispatch(actions.users.updateSubscriptions(id, subs)),
});
const NotificationSettingsContainer = withRouter(connect(
mapStateToProps, mapDispatchToProps,
)(NotificationSettings));
export default NotificationSettingsContainer;

View File

@ -1,16 +1,8 @@
import React, { Component } from 'react'; import React from 'react';
import { import { Divider, Container, Grid, Menu } from 'semantic-ui-react';
Divider, Container, Grid, Menu, import { Outlet, NavLink } from 'react-router-dom';
} from 'semantic-ui-react';
import { Link, Switch, Route } from 'react-router-dom';
import Identity from './Identity'; function Settings() {
import Notification from './Notification';
import Billing from './Billing';
import Account from './Account';
class Settings extends Component {
render() {
return ( return (
<Container style={{ marginTop: '40px' }}> <Container style={{ marginTop: '40px' }}>
<h2>Manage your account</h2> <h2>Manage your account</h2>
@ -18,26 +10,18 @@ class Settings extends Component {
<Grid stackable> <Grid stackable>
<Grid.Column computer={4}> <Grid.Column computer={4}>
<Menu fluid vertical tabular> <Menu fluid vertical tabular>
<Menu.Item as={Link} to="/settings/identity" name="identity" active={this.props.location.pathname === '/settings/identity' || this.props.location.pathname === '/settings'} icon="user secret" /> <Menu.Item as={NavLink} to="/settings/identity" name="identity" icon="user secret" />
<Menu.Item as={Link} to='/settings/notifications' content='Notifications' icon='envelope' active={this.props.location.pathname === '/settings/notifications'} /> <Menu.Item as={NavLink} to='/settings/notifications' content='Notifications' icon='envelope' />
{/*<Menu.Item as={Link} to="/settings/billing" content="Plan &amp; billing" icon="credit card" active={this.props.location.pathname === '/settings/billing'} />*/} <Menu.Item as={NavLink} to="/settings/account" name="Account" icon="key" />
<Menu.Item as={Link} to="/settings/account" name="Account" active={this.props.location.pathname === '/settings/account'} icon="key" />
</Menu> </Menu>
</Grid.Column> </Grid.Column>
<Grid.Column stretched width={12}> <Grid.Column stretched width={12}>
<Switch> <Outlet />
<Route path="/settings/notifications" component={Notification} />
<Route path="/settings/account" component={Account} />
<Route path="/settings/billing" component={Billing} />
<Route path="/settings" component={Identity} />
</Switch>
</Grid.Column> </Grid.Column>
</Grid> </Grid>
</Container> </Container>
); );
}
} }
export default Settings; export default Settings;

View File

@ -1,39 +1,45 @@
import React, { Component } from 'react'; import React from 'react';
import { import {
Icon, Segment, Form, Button, Divider, Icon, Segment, Form, Button, Divider,
} from 'semantic-ui-react'; } from 'semantic-ui-react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { Link } from 'react-router-dom'; import { Link, useParams } from 'react-router-dom';
import { connect } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import utils from 'utils/utils'; import utils from 'utils/utils';
import actions from 'actions'; import actions from 'actions';
import api from 'api'; import api from 'api';
import FileChooser from 'components/includes/FileChooser'; import FileChooser from 'components/includes/FileChooser';
class EditProfile extends Component { function EditProfile() {
updatePicture = (avatar) => { const { username } = useParams();
api.users.update(this.props.profileUser.username, { avatar }, this.props.onReceiveUser); const dispatch = useDispatch();
} const { profileUser } = useSelector(state => {
const users = state.users.users;
const profileUser = users.filter(u => username === u.username)[0];
return { profileUser };
});
updateProfile = () => { const updatePicture = (avatar) => {
const { bio, location, website } = this.props.profileUser; api.users.update(profileUser.username, { avatar }, u => dispatch(actions.users.receive(u)));
api.users.update(this.props.profileUser.username, { bio, location, website }, (user) => { };
this.props.onReceiveUser(user);
const updateProfile = () => {
const { bio, location, website } = profileUser;
api.users.update(profileUser.username, { bio, location, website }, (u) => {
dispatch(actions.users.receive(u));
toast.info('Profile saved'); toast.info('Profile saved');
}, err => toast.error(err.message)); }, err => toast.error(err.message));
} };
updateSocial = () => { const updateSocial = () => {
const { twitter, facebook, linkedIn, instagram } = this.props.profileUser; const { twitter, facebook, linkedIn, instagram } = profileUser;
api.users.update(this.props.profileUser.username, { twitter, facebook, linkedIn, instagram }, (user) => { api.users.update(profileUser.username, { twitter, facebook, linkedIn, instagram }, (u) => {
this.props.onReceiveUser(user); dispatch(actions.users.receive(u));
toast.info('Profile saved'); toast.info('Profile saved');
}, err => toast.error(err.message)); }, err => toast.error(err.message));
} };
render() {
const { profileUser, onUserEdited } = this.props;
return ( return (
<div> <div>
<Link to={`/${profileUser.username}`} className="ui basic button"> <Link to={`/${profileUser.username}`} className="ui basic button">
@ -47,9 +53,9 @@ class EditProfile extends Component {
<h5>Profile picture</h5> <h5>Profile picture</h5>
<FileChooser <FileChooser
forType="user" for={profileUser} forType="user" forObject={profileUser}
trigger=<Button basic color="yellow" icon="image" content="Choose an image" /> trigger=<Button basic color="yellow" icon="image" content="Choose an image" />
accept="image/*" onComplete={f => this.updatePicture(f.storedName)} accept="image/*" onComplete={f => updatePicture(f.storedName)}
/> />
<h4>Or choose one of ours:</h4> <h4>Or choose one of ours:</h4>
{utils.defaultAvatars().map(a => ( {utils.defaultAvatars().map(a => (
@ -58,7 +64,7 @@ class EditProfile extends Component {
style={{ style={{
width: 40, height: 40, margin: 4, cursor: 'pointer', width: 40, height: 40, margin: 4, cursor: 'pointer',
}} }}
onClick={e => this.updatePicture(a.key)} onClick={e => updatePicture(a.key)}
/> />
))} ))}
<Divider hidden /> <Divider hidden />
@ -67,19 +73,19 @@ class EditProfile extends Component {
<Form.TextArea <Form.TextArea
label="Bio" placeholder="Write a bit about yourself..." label="Bio" placeholder="Write a bit about yourself..."
value={profileUser.bio} value={profileUser.bio}
onChange={e => onUserEdited(profileUser._id, { bio: e.target.value })} onChange={e => dispatch(actions.users.update(profileUser._id, { bio: e.target.value }))}
/> />
<Form.Input <Form.Input
label="Location" placeholder="Where in the world are you?" icon="map pin" iconPosition="left" label="Location" placeholder="Where in the world are you?" icon="map pin" iconPosition="left"
value={profileUser.location} value={profileUser.location}
onChange={e => onUserEdited(profileUser._id, { location: e.target.value })} onChange={e => dispatch(actions.users.update(profileUser._id, { location: e.target.value }))}
/> />
<Form.Input <Form.Input
label="Website or URL" placeholder="https://yourwebsite.com" icon="globe" iconPosition="left" label="Website or URL" placeholder="https://yourwebsite.com" icon="globe" iconPosition="left"
value={profileUser.website} value={profileUser.website}
onChange={e => onUserEdited(profileUser._id, { website: e.target.value })} onChange={e => dispatch(actions.users.update(profileUser._id, { website: e.target.value }))}
/> />
<Form.Button color="yellow" content="Save profile" onClick={this.updateProfile} /> <Form.Button color="yellow" content="Save profile" onClick={updateProfile} />
</Form> </Form>
</Segment> </Segment>
@ -90,44 +96,28 @@ class EditProfile extends Component {
<Form.Input <Form.Input
label="Twitter" placeholder="@username" icon="twitter" iconPosition="left" label="Twitter" placeholder="@username" icon="twitter" iconPosition="left"
value={profileUser.twitter} value={profileUser.twitter}
onChange={e => onUserEdited(profileUser._id, { twitter: e.target.value })} onChange={e => dispatch(actions.users.update(profileUser._id, { twitter: e.target.value }))}
/> />
<Form.Input <Form.Input
label="Facebook" placeholder="username" icon="facebook" iconPosition="left" label="Facebook" placeholder="username" icon="facebook" iconPosition="left"
value={profileUser.facebook} value={profileUser.facebook}
onChange={e => onUserEdited(profileUser._id, { facebook: e.target.value })} onChange={e => dispatch(actions.users.update(profileUser._id, { facebook: e.target.value }))}
/> />
<Form.Input <Form.Input
label="Instagram" placeholder="username" icon="instagram" iconPosition="left" label="Instagram" placeholder="username" icon="instagram" iconPosition="left"
value={profileUser.instagram} value={profileUser.instagram}
onChange={e => onUserEdited(profileUser._id, { instagram: e.target.value })} onChange={e => dispatch(actions.users.update(profileUser._id, { instagram: e.target.value }))}
/> />
<Form.Input <Form.Input
label="LinkedIn" placeholder="username" icon="linkedin" iconPosition="left" label="LinkedIn" placeholder="username" icon="linkedin" iconPosition="left"
value={profileUser.linkedIn} value={profileUser.linkedIn}
onChange={e => onUserEdited(profileUser._id, { linkedIn: e.target.value })} onChange={e => dispatch(actions.users.update(profileUser._id, { linkedIn: e.target.value }))}
/> />
<Form.Button color="yellow" content="Save social profiles" onClick={this.updateSocial} /> <Form.Button color="yellow" content="Save social profiles" onClick={updateSocial} />
</Form> </Form>
</Segment> </Segment>
</div>); </div>);
}
} }
const mapStateToProps = (state, ownProps) => { export default EditProfile;
const users = state.users.users;
const profileUser = users.filter(u => ownProps.match.params.username === u.username)[0];
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
return { user, profileUser };
};
const mapDispatchToProps = dispatch => ({
onUserEdited: (id, data) => dispatch(actions.users.update(id, data)),
onReceiveUser: user => dispatch(actions.users.receive(user)),
});
const EditProfileContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(EditProfile);
export default EditProfileContainer;

View File

@ -1,48 +1,44 @@
import React, { Component } from 'react'; import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Loader, Icon, List, Container, Card, Grid, Message } from 'semantic-ui-react'; import { Loader, Icon, List, Container, Card, Grid, Message } from 'semantic-ui-react';
import { Link, Switch, Route } from 'react-router-dom'; import { Link, Outlet, useParams } from 'react-router-dom';
import { connect } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import moment from 'moment'; import moment from 'moment';
import utils from 'utils/utils'; import utils from 'utils/utils';
import actions from 'actions'; import actions from 'actions';
import api from 'api'; import api from 'api';
import EditProfile from './EditProfile';
import ProfileProjects from './ProfileProjects';
import BlurrableImage from 'components/includes/BlurrableImage'; import BlurrableImage from 'components/includes/BlurrableImage';
class Profile extends Component { function Profile() {
constructor(props) { const [loading, setLoading] = useState(false);
super(props); const dispatch = useDispatch();
this.state = { loading: false }; const { username } = useParams();
}
componentDidMount() { const { user, profileUser, errorMessage } = useSelector(state => {
this.setState({ loading: true }); const users = state.users.users;
api.users.get(this.props.match.params.username, user => { const profileUser = users.filter(u => username === u.username)[0];
this.props.onReceiveUser(user); const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
this.setState({ loading: false }); return { user, profileUser, errorMessage: state.users.errorMessage };
}, err => {
this.props.onRequestFailed(err);
this.setState({ loading: false });
}); });
}
componentDidUpdate(prevProps) { useEffect(() => {
if (this.props.match.params.username !== prevProps.match.params.username) { setLoading(true);
api.users.get(this.props.match.params.username, this.props.onReceiveUser); api.users.get(username, user => {
} dispatch(actions.users.receive(user));
} setLoading(false);
}, err => {
dispatch(actions.users.requestFailed(err));
setLoading(false);
});
}, [dispatch, username])
render() {
const { loading } = this.state;
const { user, profileUser, errorMessage } = this.props;
return ( return (
<Container style={{ marginTop: '40px' }}> <Container style={{ marginTop: '40px' }}>
<Helmet title={profileUser?.username ? `${profileUser.username}'s Profile` : 'Profile'} /> <Helmet title={profileUser?.username ? `${profileUser.username}'s Profile` : 'Profile'} />
{loading && !profileUser && {loading && !profileUser &&
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<h4>Loading {this.props.match.params.username}'s profile...</h4> <h4>Loading {username}'s profile...</h4>
<Loader active inline="centered" /> <Loader active inline="centered" />
</div> </div>
} }
@ -166,33 +162,13 @@ class Profile extends Component {
</Grid.Column> </Grid.Column>
<Grid.Column computer={11}> <Grid.Column computer={11}>
<Switch> <Outlet />
<Route path="/:username/edit" component={EditProfile} />
<Route component={ProfileProjects} />
</Switch>
</Grid.Column> </Grid.Column>
</Grid> </Grid>
) )
} }
</Container> </Container>
); );
}
} }
const mapStateToProps = (state, ownProps) => { export default Profile;
const users = state.users.users;
const profileUser = users.filter(u => ownProps.match.params.username === u.username)[0];
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
return { user, profileUser, loading: state.users.loading, errorMessage: state.users.errorMessage };
};
const mapDispatchToProps = dispatch => ({
onRequestUser: () => dispatch(actions.users.request()),
onRequestFailed: err => dispatch(actions.users.requestFailed(err)),
onReceiveUser: user => dispatch(actions.users.receive(user)),
});
const ProfileContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(Profile);
export default ProfileContainer;

View File

@ -1,12 +1,18 @@
import React, { Component } from 'react'; import React from 'react';
import { Icon, Divider, Card, Message } from 'semantic-ui-react'; import { Icon, Divider, Card, Message } from 'semantic-ui-react';
import { connect } from 'react-redux'; import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import ProjectCard from 'components/includes/ProjectCard'; import ProjectCard from 'components/includes/ProjectCard';
class ProfileProjects extends Component { function ProfileProjects() {
render() { const { username } = useParams();
const { profileUser } = this.props; const { profileUser } = useSelector(state => {
const users = state.users.users;
const profileUser = users.filter(u => u.username === username)[0];
return { profileUser };
});
return ( return (
<div> <div>
<h3><Icon name='book' /> {profileUser.username}'s projects</h3> <h3><Icon name='book' /> {profileUser.username}'s projects</h3>
@ -26,17 +32,6 @@ class ProfileProjects extends Component {
) )
} }
</div>); </div>);
}
} }
const mapStateToProps = (state, ownProps) => { export default ProfileProjects;
const users = state.users.users;
const profileUser = users.filter(u => u.username === ownProps.match.params.username)[0];
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
return { user, profileUser };
};
const ProfileProjectsContainer = connect(
mapStateToProps,
)(ProfileProjects);
export default ProfileProjectsContainer;

View File

@ -1,93 +0,0 @@
import React, { Component } from 'react';
import { Message, Card, Icon, Divider, Button, Container } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import api from 'api';
class Pricing extends Component {
constructor(props) {
super(props);
this.state = { plans: [] };
}
componentDidMount() {
api.billing.getPlans(plans => this.setState({ plans }));
}
render() {
const { user } = this.props;
return (
<Container style={{ marginTop: 30 }}>
<h1>Pricing</h1>
<p>Creating a Treadl account is free, but you can choose to pay for extra features. We only require card details when you decide to subscribe to a paid plan.</p>
<Card.Group itemsPerRow={2}>
<Card>
<Card.Content>
<Card.Header>How come it's free?</Card.Header>
<p>Treadl offers several key features to free account-holders. For many people, the free account will be enough, but others may want to subscribe to a paid plan for extra features.</p>
</Card.Content>
</Card>
<Card>
<Card.Content>
<Card.Header>You own your data</Card.Header>
<p>We will never claim any ownership of your own content that you create using Treadl or upload to your Treadl account - even for free accounts.</p>
</Card.Content>
</Card>
</Card.Group>
<Divider section />
<h2>Plans</h2>
<p>You can select and subscribe to a plan after you've created a free account.</p>
{user &&
<Message>
<p>{user.username}, you can update your plan at any time by visiting your billing settings page.</p>
<Button as={Link} to='/settings/billing' color='blue' icon='settings' content='Billing settings' />
</Message>
}
<Card.Group stackable centered itemsPerRow={3} style={{ marginTop: 30, marginBottom: 30 }}>
{this.state.plans.map(p => (
<Card key={p.id}>
<Card.Content textAlign="center">
<Card.Header>{p.name}</Card.Header>
<Card.Meta>{p.description}</Card.Meta>
<Card.Meta><strong>{p.price} per month</strong>
</Card.Meta>
</Card.Content>
<Card.Content>
{p.features.map((f, i) => (
<p key={i}>
<Icon style={{ color: 'green' }} name="check circle outline" /> {f}
</p>
))}
</Card.Content>
{!user &&
<Card.Content extra>
<Button fluid size="small" color="teal" content="Create your account" onClick={this.props.onRegisterClicked} />
</Card.Content>
}
</Card>
))}
</Card.Group>
{!user &&
<div>
<h2>Interested to find out more?</h2>
<p>Create your free account today to see what it's about.</p>
<Button color="teal" content="Sign-up" onClick={this.props.onRegisterClicked} />
</div>
}
</Container>
);
}
}
const mapStateToProps = (state, ownProps) => {
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
return { user };
};
const PricingContainer = connect(
mapStateToProps,
)(Pricing);
export default PricingContainer;

View File

@ -1,9 +1,8 @@
import React, { Component } from 'react'; import React from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Container } from 'semantic-ui-react'; import { Container } from 'semantic-ui-react';
class PrivacyPolicy extends Component { function PrivacyPolicy() {
render() {
return ( return (
<Container style={{ marginTop: 50, marginBottom: 50 }}> <Container style={{ marginTop: 50, marginBottom: 50 }}>
<Helmet title='Privacy Policy' /> <Helmet title='Privacy Policy' />
@ -72,7 +71,6 @@ class PrivacyPolicy extends Component {
<p>We do not use personal data for automated decision making, and does not use such data for profiling users. Additionally, any processing done for analytics and reporting is done on an entirely anonymous basis. For more information or if you have any concerns, please email us.</p> <p>We do not use personal data for automated decision making, and does not use such data for profiling users. Additionally, any processing done for analytics and reporting is done on an entirely anonymous basis. For more information or if you have any concerns, please email us.</p>
</Container> </Container>
); );
}
} }
export default PrivacyPolicy; export default PrivacyPolicy;

View File

@ -1,9 +1,8 @@
import React, { Component } from 'react'; import React from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Container } from 'semantic-ui-react'; import { Container } from 'semantic-ui-react';
class TermsOfUse extends Component { function TermsOfUse() {
render() {
return ( return (
<Container style={{ marginTop: 50, marginBottom: 50 }}> <Container style={{ marginTop: 50, marginBottom: 50 }}>
<Helmet title='Terms of Use' /> <Helmet title='Terms of Use' />
@ -38,7 +37,6 @@ class TermsOfUse extends Component {
</Container> </Container>
); );
}
} }
export default TermsOfUse; export default TermsOfUse;

View File

@ -1,19 +1,17 @@
import 'react-app-polyfill/ie9'; import 'react-app-polyfill/ie9';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Switch, Route, Router } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { createStore } from 'redux'; import { createStore } from 'redux';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing'; import { BrowserTracing } from '@sentry/tracing';
import createBrowserHistory from 'history/createBrowserHistory';
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
import 'pell/dist/pell.min.css'; import 'pell/dist/pell.min.css';
import './index.css'; import './index.css';
import reducers from './reducers'; import reducers from './reducers';
import App from './components/App.js'; import App from './components/App.js';
import DraftExport from 'components/main/projects/objects/DraftExport';
import * as serviceWorker from './registerServiceWorker'; import * as serviceWorker from './registerServiceWorker';
export const store = createStore(reducers); export const store = createStore(reducers);
@ -28,12 +26,9 @@ if (process.env.REACT_APP_SENTRY_DSN) {
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>
<Router history={createBrowserHistory()}> <BrowserRouter>
<Switch> <App />
<Route path="/objects/:id/export" component={DraftExport} /> </BrowserRouter>
<Route component={App} />
</Switch>
</Router>
</Provider>, </Provider>,
document.getElementById('root'), document.getElementById('root'),
); );

View File

@ -1110,13 +1110,20 @@
dependencies: dependencies:
regenerator-runtime "^0.13.4" regenerator-runtime "^0.13.4"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4": "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4":
version "7.11.2" version "7.11.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
dependencies: dependencies:
regenerator-runtime "^0.13.4" regenerator-runtime "^0.13.4"
"@babel/runtime@^7.12.1", "@babel/runtime@^7.7.6":
version "7.17.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72"
integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.10.4", "@babel/template@^7.4.0", "@babel/template@^7.8.6": "@babel/template@^7.10.4", "@babel/template@^7.4.0", "@babel/template@^7.8.6":
version "7.10.4" version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
@ -1629,6 +1636,13 @@
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
"@types/debug@^4.0.0":
version "4.1.7"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==
dependencies:
"@types/ms" "*"
"@types/eslint-visitor-keys@^1.0.0": "@types/eslint-visitor-keys@^1.0.0":
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
@ -1642,6 +1656,21 @@
"@types/minimatch" "*" "@types/minimatch" "*"
"@types/node" "*" "@types/node" "*"
"@types/hast@^2.0.0":
version "2.3.4"
resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc"
integrity sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==
dependencies:
"@types/unist" "*"
"@types/hoist-non-react-statics@^3.3.1":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
dependencies:
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
@ -1667,11 +1696,28 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd"
integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==
"@types/mdast@^3.0.0":
version "3.0.10"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af"
integrity sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==
dependencies:
"@types/unist" "*"
"@types/mdurl@^1.0.0":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9"
integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==
"@types/minimatch@*": "@types/minimatch@*":
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
"@types/ms@*":
version "0.7.31"
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
"@types/node@*": "@types/node@*":
version "14.6.0" version "14.6.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.0.tgz#7d4411bf5157339337d7cff864d9ff45f177b499" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.0.tgz#7d4411bf5157339337d7cff864d9ff45f177b499"
@ -1682,16 +1728,45 @@
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
"@types/prop-types@*", "@types/prop-types@^15.0.0":
version "15.7.5"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
"@types/q@^1.5.1": "@types/q@^1.5.1":
version "1.5.4" version "1.5.4"
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
"@types/react@*":
version "18.0.8"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.8.tgz#a051eb380a9fbcaa404550543c58e1cf5ce4ab87"
integrity sha512-+j2hk9BzCOrrOSJASi5XiOyBbERk9jG5O73Ya4M0env5Ixi6vUNli4qy994AINcEF+1IEHISYFfIT4zwr++LKw==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/scheduler@*":
version "0.16.2"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
"@types/stack-utils@^1.0.1": "@types/stack-utils@^1.0.1":
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
"@types/unist@*", "@types/unist@^2.0.0":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
"@types/use-sync-external-store@^0.0.3":
version "0.0.3"
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43"
integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==
"@types/yargs-parser@*": "@types/yargs-parser@*":
version "15.0.0" version "15.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d"
@ -2454,6 +2529,11 @@ babylon@^6.18.0:
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==
bail@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d"
integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==
balanced-match@^1.0.0: balanced-match@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@ -2911,6 +2991,11 @@ chalk@^4.1.0:
ansi-styles "^4.1.0" ansi-styles "^4.1.0"
supports-color "^7.1.0" supports-color "^7.1.0"
character-entities@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.1.tgz#98724833e1e27990dee0bd0f2b8a859c3476aac7"
integrity sha512-OzmutCf2Kmc+6DrFrrPS8/tDh2+DpnrfzdICHWhcVC9eOd0N1PXmQEE1a8iM4IziIAG+8tmTq3K+oo0ubH6RRQ==
chardet@^0.7.0: chardet@^0.7.0:
version "0.7.0" version "0.7.0"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
@ -3136,6 +3221,11 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
dependencies: dependencies:
delayed-stream "~1.0.0" delayed-stream "~1.0.0"
comma-separated-tokens@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz#d4c25abb679b7751c880be623c1179780fe1dd98"
integrity sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg==
commander@^2.11.0, commander@^2.20.0: commander@^2.11.0, commander@^2.20.0:
version "2.20.3" version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@ -3659,6 +3749,11 @@ cssstyle@^1.0.0, cssstyle@^1.1.1:
dependencies: dependencies:
cssom "0.3.x" cssom "0.3.x"
csstype@^3.0.2:
version "3.0.11"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33"
integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==
cyclist@^1.0.1: cyclist@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
@ -3707,6 +3802,13 @@ debug@^3.1.1, debug@^3.2.5:
dependencies: dependencies:
ms "^2.1.1" ms "^2.1.1"
debug@^4.0.0:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"
debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
@ -3719,6 +3821,13 @@ decamelize@^1.2.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
decode-named-character-reference@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.1.tgz#57b2bd9112659cacbc449d3577d7dadb8e1f3d1b"
integrity sha512-YV/0HQHreRwKb7uBopyIkLG17jG6Sv2qUchk9qSoVJ2f+flwRsPNBO0hAnjt6mTNYUT+vw9Gy2ihXg4sUWPi2w==
dependencies:
character-entities "^2.0.0"
decode-uri-component@^0.2.0: decode-uri-component@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
@ -3811,6 +3920,11 @@ depd@~1.1.2:
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
dequal@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d"
integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==
des.js@^1.0.0: des.js@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
@ -3847,6 +3961,11 @@ diff-sequences@^24.9.0:
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5"
integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==
diff@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
diffie-hellman@^5.0.0: diffie-hellman@^5.0.0:
version "5.0.3" version "5.0.3"
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
@ -4594,7 +4713,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
assign-symbols "^1.0.0" assign-symbols "^1.0.0"
is-extendable "^1.0.1" is-extendable "^1.0.1"
extend@~3.0.2: extend@^3.0.0, extend@~3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
@ -5229,6 +5348,11 @@ hash.js@^1.0.0, hash.js@^1.0.3:
inherits "^2.0.3" inherits "^2.0.3"
minimalistic-assert "^1.0.1" minimalistic-assert "^1.0.1"
hast-util-whitespace@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz#4fc1086467cc1ef5ba20673cb6b03cec3a970f1c"
integrity sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg==
he@^1.2.0: he@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
@ -5239,7 +5363,7 @@ hex-color-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
history@^4.7.2, history@^4.9.0: history@^4.7.2:
version "4.10.1" version "4.10.1"
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
@ -5251,6 +5375,13 @@ history@^4.7.2, history@^4.9.0:
tiny-warning "^1.0.0" tiny-warning "^1.0.0"
value-equal "^1.0.1" value-equal "^1.0.1"
history@^5.2.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b"
integrity sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==
dependencies:
"@babel/runtime" "^7.7.6"
hmac-drbg@^1.0.0: hmac-drbg@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@ -5260,7 +5391,7 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0" minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1" minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2" version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@ -5568,6 +5699,11 @@ ini@^1.3.5:
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
inline-style-parser@0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1"
integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==
inquirer@7.0.4: inquirer@7.0.4:
version "7.0.4" version "7.0.4"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.4.tgz#99af5bde47153abca23f5c7fc30db247f39da703" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.4.tgz#99af5bde47153abca23f5c7fc30db247f39da703"
@ -5708,6 +5844,11 @@ is-buffer@^1.0.2, is-buffer@^1.1.5:
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
is-buffer@^2.0.0:
version "2.0.5"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191"
integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==
is-callable@^1.1.4, is-callable@^1.2.0: is-callable@^1.1.4, is-callable@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb"
@ -5883,6 +6024,11 @@ is-plain-obj@^1.0.0:
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
is-plain-obj@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.0.0.tgz#06c0999fd7574edf5a906ba5644ad0feb3a84d22"
integrity sha512-NXRbBtUdBioI73y/HmOhogw/U5msYPC9DAtGkJXeFcFWSFZw0mCUsPxk/snTuJHzNKA8kLBK4rH97RMB1BfCXw==
is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
@ -5958,11 +6104,6 @@ is-wsl@^2.1.1:
dependencies: dependencies:
is-docker "^2.0.0" is-docker "^2.0.0"
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@ -6646,6 +6787,11 @@ kleur@^3.0.3:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
kleur@^4.0.3:
version "4.1.4"
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.4.tgz#8c202987d7e577766d039a8cd461934c01cda04d"
integrity sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==
last-call-webpack-plugin@^3.0.0: last-call-webpack-plugin@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555" resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555"
@ -6820,7 +6966,7 @@ loglevel@^1.6.6:
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171"
integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA== integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA==
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@ -6896,6 +7042,54 @@ md5.js@^1.3.4:
inherits "^2.0.1" inherits "^2.0.1"
safe-buffer "^5.1.2" safe-buffer "^5.1.2"
mdast-util-definitions@^5.0.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.0.tgz#b6d10ef00a3c4cf191e8d9a5fa58d7f4a366f817"
integrity sha512-5hcR7FL2EuZ4q6lLMUK5w4lHT2H3vqL9quPvYZ/Ku5iifrirfMHiGdhxdXMUbUkDmz5I+TYMd7nbaxUhbQkfpQ==
dependencies:
"@types/mdast" "^3.0.0"
"@types/unist" "^2.0.0"
unist-util-visit "^3.0.0"
mdast-util-from-markdown@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.2.0.tgz#84df2924ccc6c995dec1e2368b2b208ad0a76268"
integrity sha512-iZJyyvKD1+K7QX1b5jXdE7Sc5dtoTry1vzV28UZZe8Z1xVnB/czKntJ7ZAkG0tANqRnBF6p3p7GpU1y19DTf2Q==
dependencies:
"@types/mdast" "^3.0.0"
"@types/unist" "^2.0.0"
decode-named-character-reference "^1.0.0"
mdast-util-to-string "^3.1.0"
micromark "^3.0.0"
micromark-util-decode-numeric-character-reference "^1.0.0"
micromark-util-decode-string "^1.0.0"
micromark-util-normalize-identifier "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
unist-util-stringify-position "^3.0.0"
uvu "^0.5.0"
mdast-util-to-hast@^12.1.0:
version "12.1.1"
resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.1.1.tgz#89a2bb405eaf3b05eb8bf45157678f35eef5dbca"
integrity sha512-qE09zD6ylVP14jV4mjLIhDBOrpFdShHZcEsYvvKGABlr9mGbV7mTlRWdoFxL/EYSTNDiC9GZXy7y8Shgb9Dtzw==
dependencies:
"@types/hast" "^2.0.0"
"@types/mdast" "^3.0.0"
"@types/mdurl" "^1.0.0"
mdast-util-definitions "^5.0.0"
mdurl "^1.0.0"
micromark-util-sanitize-uri "^1.0.0"
unist-builder "^3.0.0"
unist-util-generated "^2.0.0"
unist-util-position "^4.0.0"
unist-util-visit "^4.0.0"
mdast-util-to-string@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz#56c506d065fbf769515235e577b5a261552d56e9"
integrity sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==
mdn-data@2.0.4: mdn-data@2.0.4:
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
@ -6906,6 +7100,11 @@ mdn-data@2.0.6:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978"
integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA== integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==
mdurl@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
media-typer@0.3.0: media-typer@0.3.0:
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@ -6970,6 +7169,201 @@ microevent.ts@~0.1.1:
resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0" resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0"
integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g== integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==
micromark-core-commonmark@^1.0.1:
version "1.0.6"
resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz#edff4c72e5993d93724a3c206970f5a15b0585ad"
integrity sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==
dependencies:
decode-named-character-reference "^1.0.0"
micromark-factory-destination "^1.0.0"
micromark-factory-label "^1.0.0"
micromark-factory-space "^1.0.0"
micromark-factory-title "^1.0.0"
micromark-factory-whitespace "^1.0.0"
micromark-util-character "^1.0.0"
micromark-util-chunked "^1.0.0"
micromark-util-classify-character "^1.0.0"
micromark-util-html-tag-name "^1.0.0"
micromark-util-normalize-identifier "^1.0.0"
micromark-util-resolve-all "^1.0.0"
micromark-util-subtokenize "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.1"
uvu "^0.5.0"
micromark-factory-destination@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz#fef1cb59ad4997c496f887b6977aa3034a5a277e"
integrity sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==
dependencies:
micromark-util-character "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
micromark-factory-label@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz#6be2551fa8d13542fcbbac478258fb7a20047137"
integrity sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==
dependencies:
micromark-util-character "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
uvu "^0.5.0"
micromark-factory-space@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz#cebff49968f2b9616c0fcb239e96685cb9497633"
integrity sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==
dependencies:
micromark-util-character "^1.0.0"
micromark-util-types "^1.0.0"
micromark-factory-title@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz#7e09287c3748ff1693930f176e1c4a328382494f"
integrity sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==
dependencies:
micromark-factory-space "^1.0.0"
micromark-util-character "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
uvu "^0.5.0"
micromark-factory-whitespace@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz#e991e043ad376c1ba52f4e49858ce0794678621c"
integrity sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==
dependencies:
micromark-factory-space "^1.0.0"
micromark-util-character "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
micromark-util-character@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.1.0.tgz#d97c54d5742a0d9611a68ca0cd4124331f264d86"
integrity sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==
dependencies:
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
micromark-util-chunked@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz#5b40d83f3d53b84c4c6bce30ed4257e9a4c79d06"
integrity sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==
dependencies:
micromark-util-symbol "^1.0.0"
micromark-util-classify-character@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz#cbd7b447cb79ee6997dd274a46fc4eb806460a20"
integrity sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==
dependencies:
micromark-util-character "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
micromark-util-combine-extensions@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz#91418e1e74fb893e3628b8d496085639124ff3d5"
integrity sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==
dependencies:
micromark-util-chunked "^1.0.0"
micromark-util-types "^1.0.0"
micromark-util-decode-numeric-character-reference@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz#dcc85f13b5bd93ff8d2868c3dba28039d490b946"
integrity sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==
dependencies:
micromark-util-symbol "^1.0.0"
micromark-util-decode-string@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz#942252ab7a76dec2dbf089cc32505ee2bc3acf02"
integrity sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==
dependencies:
decode-named-character-reference "^1.0.0"
micromark-util-character "^1.0.0"
micromark-util-decode-numeric-character-reference "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-encode@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz#2c1c22d3800870ad770ece5686ebca5920353383"
integrity sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==
micromark-util-html-tag-name@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.0.0.tgz#75737e92fef50af0c6212bd309bc5cb8dbd489ed"
integrity sha512-NenEKIshW2ZI/ERv9HtFNsrn3llSPZtY337LID/24WeLqMzeZhBEE6BQ0vS2ZBjshm5n40chKtJ3qjAbVV8S0g==
micromark-util-normalize-identifier@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz#4a3539cb8db954bbec5203952bfe8cedadae7828"
integrity sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==
dependencies:
micromark-util-symbol "^1.0.0"
micromark-util-resolve-all@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz#a7c363f49a0162e931960c44f3127ab58f031d88"
integrity sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==
dependencies:
micromark-util-types "^1.0.0"
micromark-util-sanitize-uri@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.0.0.tgz#27dc875397cd15102274c6c6da5585d34d4f12b2"
integrity sha512-cCxvBKlmac4rxCGx6ejlIviRaMKZc0fWm5HdCHEeDWRSkn44l6NdYVRyU+0nT1XC72EQJMZV8IPHF+jTr56lAg==
dependencies:
micromark-util-character "^1.0.0"
micromark-util-encode "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-subtokenize@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz#ff6f1af6ac836f8bfdbf9b02f40431760ad89105"
integrity sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==
dependencies:
micromark-util-chunked "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
uvu "^0.5.0"
micromark-util-symbol@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz#b90344db62042ce454f351cf0bebcc0a6da4920e"
integrity sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==
micromark-util-types@^1.0.0, micromark-util-types@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.0.2.tgz#f4220fdb319205812f99c40f8c87a9be83eded20"
integrity sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==
micromark@^3.0.0:
version "3.0.10"
resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.0.10.tgz#1eac156f0399d42736458a14b0ca2d86190b457c"
integrity sha512-ryTDy6UUunOXy2HPjelppgJ2sNfcPz1pLlMdA6Rz9jPzhLikWXv/irpWV/I2jd68Uhmny7hHxAlAhk4+vWggpg==
dependencies:
"@types/debug" "^4.0.0"
debug "^4.0.0"
decode-named-character-reference "^1.0.0"
micromark-core-commonmark "^1.0.1"
micromark-factory-space "^1.0.0"
micromark-util-character "^1.0.0"
micromark-util-chunked "^1.0.0"
micromark-util-combine-extensions "^1.0.0"
micromark-util-decode-numeric-character-reference "^1.0.0"
micromark-util-encode "^1.0.0"
micromark-util-normalize-identifier "^1.0.0"
micromark-util-resolve-all "^1.0.0"
micromark-util-sanitize-uri "^1.0.0"
micromark-util-subtokenize "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.1"
uvu "^0.5.0"
micromatch@^3.1.10, micromatch@^3.1.4: micromatch@^3.1.10, micromatch@^3.1.4:
version "3.1.10" version "3.1.10"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
@ -7024,14 +7418,6 @@ mimic-fn@^2.0.0, mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
mini-create-react-context@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz#df60501c83151db69e28eac0ef08b4002efab040"
integrity sha512-b0TytUgFSbgFJGzJqXPKCFCBWigAjpjo+Fl7Vf7ZbKRDptszpppKxXH6DRXEABZ/gcEQczeb0iZ7JvL8e8jjCA==
dependencies:
"@babel/runtime" "^7.5.5"
tiny-warning "^1.0.3"
mini-css-extract-plugin@0.9.0: mini-css-extract-plugin@0.9.0:
version "0.9.0" version "0.9.0"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz#47f2cf07aa165ab35733b1fc97d4c46c0564339e" resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz#47f2cf07aa165ab35733b1fc97d4c46c0564339e"
@ -7148,6 +7534,11 @@ move-concurrently@^1.0.1:
rimraf "^2.5.4" rimraf "^2.5.4"
run-queue "^1.0.3" run-queue "^1.0.3"
mri@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==
ms@2.0.0: ms@2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@ -7158,7 +7549,7 @@ ms@2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
ms@^2.1.1: ms@2.1.2, ms@^2.1.1:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
@ -7823,13 +8214,6 @@ path-to-regexp@0.1.7:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
dependencies:
isarray "0.0.1"
path-type@^2.0.0: path-type@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
@ -8720,6 +9104,20 @@ prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, pr
object-assign "^4.1.1" object-assign "^4.1.1"
react-is "^16.8.1" react-is "^16.8.1"
prop-types@^15.0.0:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
dependencies:
loose-envify "^1.4.0"
object-assign "^4.1.1"
react-is "^16.13.1"
property-information@^6.0.0:
version "6.1.1"
resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.1.1.tgz#5ca85510a3019726cb9afed4197b7b8ac5926a22"
integrity sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w==
proxy-addr@~2.0.5: proxy-addr@~2.0.5:
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
@ -9035,11 +9433,16 @@ react-helmet@^6.0.0:
react-fast-compare "^3.1.1" react-fast-compare "^3.1.1"
react-side-effect "^2.1.0" react-side-effect "^2.1.0"
react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.6.3, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0: react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.3, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-is@^18.0.0:
version "18.1.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67"
integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==
react-joyride@^2.4.0: react-joyride@^2.4.0:
version "2.4.0" version "2.4.0"
resolved "https://registry.yarnpkg.com/react-joyride/-/react-joyride-2.4.0.tgz#273a99fea4804a48155e7cc7bae308dcbc8cb725" resolved "https://registry.yarnpkg.com/react-joyride/-/react-joyride-2.4.0.tgz#273a99fea4804a48155e7cc7bae308dcbc8cb725"
@ -9061,6 +9464,27 @@ react-lifecycles-compat@^3.0.4:
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-markdown@^8.0.3:
version "8.0.3"
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.3.tgz#e8aba0d2f5a1b2124d476ee1fff9448a2f57e4b3"
integrity sha512-We36SfqaKoVNpN1QqsZwWSv/OZt5J15LNgTLWynwAN5b265hrQrsjMtlRNwUvS+YyR3yDM8HpTNc4pK9H/Gc0A==
dependencies:
"@types/hast" "^2.0.0"
"@types/prop-types" "^15.0.0"
"@types/unist" "^2.0.0"
comma-separated-tokens "^2.0.0"
hast-util-whitespace "^2.0.0"
prop-types "^15.0.0"
property-information "^6.0.0"
react-is "^18.0.0"
remark-parse "^10.0.0"
remark-rehype "^10.0.0"
space-separated-tokens "^2.0.0"
style-to-object "^0.3.0"
unified "^10.0.0"
unist-util-visit "^4.0.0"
vfile "^5.0.0"
react-popper@^1.3.4: react-popper@^1.3.4:
version "1.3.7" version "1.3.7"
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.7.tgz#f6a3471362ef1f0d10a4963673789de1baca2324" resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.7.tgz#f6a3471362ef1f0d10a4963673789de1baca2324"
@ -9079,45 +9503,32 @@ react-proptype-conditional-require@^1.0.4:
resolved "https://registry.yarnpkg.com/react-proptype-conditional-require/-/react-proptype-conditional-require-1.0.4.tgz#69c2d5741e6df5e08f230f36bbc2944ee1222555" resolved "https://registry.yarnpkg.com/react-proptype-conditional-require/-/react-proptype-conditional-require-1.0.4.tgz#69c2d5741e6df5e08f230f36bbc2944ee1222555"
integrity sha1-acLVdB5t9eCPIw82u8KUTuEiJVU= integrity sha1-acLVdB5t9eCPIw82u8KUTuEiJVU=
react-redux@^7.2.0: react-redux@^8.0.1:
version "7.2.1" version "8.0.1"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.1.tgz#8dedf784901014db2feca1ab633864dee68ad985" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.1.tgz#2bc029f5ada9b443107914c373a2750f6bc0f40c"
integrity sha512-T+VfD/bvgGTUA74iW9d2i5THrDQWbweXP0AVNI8tNd1Rk5ch1rnMiJkDD67ejw7YBKM4+REvcvqRuWJb7BLuEg== integrity sha512-LMZMsPY4DYdZfLJgd7i79n5Kps5N9XVLCJJeWAaPYTV+Eah2zTuBjTxKtNEbjiyitbq80/eIkm55CYSLqAub3w==
dependencies: dependencies:
"@babel/runtime" "^7.5.5" "@babel/runtime" "^7.12.1"
hoist-non-react-statics "^3.3.0" "@types/hoist-non-react-statics" "^3.3.1"
loose-envify "^1.4.0" "@types/use-sync-external-store" "^0.0.3"
prop-types "^15.7.2" hoist-non-react-statics "^3.3.2"
react-is "^16.9.0" react-is "^18.0.0"
use-sync-external-store "^1.0.0"
react-router-dom@^5.1.2: react-router-dom@^6.3.0:
version "5.2.0" version "6.3.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.3.0.tgz#a0216da813454e521905b5fa55e0e5176123f43d"
integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA== integrity sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==
dependencies: dependencies:
"@babel/runtime" "^7.1.2" history "^5.2.0"
history "^4.9.0" react-router "6.3.0"
loose-envify "^1.3.1"
prop-types "^15.6.2"
react-router "5.2.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-router@5.2.0: react-router@6.3.0:
version "5.2.0" version "6.3.0"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557"
integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw== integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==
dependencies: dependencies:
"@babel/runtime" "^7.1.2" history "^5.2.0"
history "^4.9.0"
hoist-non-react-statics "^3.1.0"
loose-envify "^1.3.1"
mini-create-react-context "^0.4.0"
path-to-regexp "^1.7.0"
prop-types "^15.6.2"
react-is "^16.6.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-scripts@3.4.1: react-scripts@3.4.1:
version "3.4.1" version "3.4.1"
@ -9395,6 +9806,25 @@ relateurl@^0.2.7:
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
remark-parse@^10.0.0:
version "10.0.1"
resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.1.tgz#6f60ae53edbf0cf38ea223fe643db64d112e0775"
integrity sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==
dependencies:
"@types/mdast" "^3.0.0"
mdast-util-from-markdown "^1.0.0"
unified "^10.0.0"
remark-rehype@^10.0.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-10.1.0.tgz#32dc99d2034c27ecaf2e0150d22a6dcccd9a6279"
integrity sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==
dependencies:
"@types/hast" "^2.0.0"
"@types/mdast" "^3.0.0"
mdast-util-to-hast "^12.1.0"
unified "^10.0.0"
remove-trailing-separator@^1.0.1: remove-trailing-separator@^1.0.1:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@ -9632,6 +10062,13 @@ rxjs@^6.5.3, rxjs@^6.6.0:
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
sade@^1.7.3:
version "1.8.1"
resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701"
integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==
dependencies:
mri "^1.1.0"
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2" version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@ -10085,6 +10522,11 @@ source-map@^0.5.0, source-map@^0.5.6:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
space-separated-tokens@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz#43193cec4fb858a2ce934b7f98b7f2c18107098b"
integrity sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw==
spdx-correct@^3.0.0: spdx-correct@^3.0.0:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
@ -10402,6 +10844,13 @@ style-loader@0.23.1:
loader-utils "^1.1.0" loader-utils "^1.1.0"
schema-utils "^1.0.0" schema-utils "^1.0.0"
style-to-object@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46"
integrity sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==
dependencies:
inline-style-parser "0.1.1"
styled-components@^5.1.0: styled-components@^5.1.0:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.1.1.tgz#96dfb02a8025794960863b9e8e365e3b6be5518d" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.1.1.tgz#96dfb02a8025794960863b9e8e365e3b6be5518d"
@ -10596,7 +11045,7 @@ tiny-invariant@^1.0.2:
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
tiny-warning@^1.0.0, tiny-warning@^1.0.3: tiny-warning@^1.0.0:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
@ -10691,6 +11140,11 @@ tree-changes@^0.9.0:
"@gilbarbara/deep-equal" "^0.1.0" "@gilbarbara/deep-equal" "^0.1.0"
is-lite "^0.8.1" is-lite "^0.8.1"
trough@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876"
integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==
ts-pnp@1.1.6: ts-pnp@1.1.6:
version "1.1.6" version "1.1.6"
resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.1.6.tgz#389a24396d425a0d3162e96d2b4638900fdc289a" resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.1.6.tgz#389a24396d425a0d3162e96d2b4638900fdc289a"
@ -10803,6 +11257,19 @@ unicode-property-aliases-ecmascript@^1.0.4:
resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4" resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4"
integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg== integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==
unified@^10.0.0:
version "10.1.2"
resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df"
integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==
dependencies:
"@types/unist" "^2.0.0"
bail "^2.0.0"
extend "^3.0.0"
is-buffer "^2.0.0"
is-plain-obj "^4.0.0"
trough "^2.0.0"
vfile "^5.0.0"
union-value@^1.0.0: union-value@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
@ -10837,6 +11304,71 @@ unique-slug@^2.0.0:
dependencies: dependencies:
imurmurhash "^0.1.4" imurmurhash "^0.1.4"
unist-builder@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-3.0.0.tgz#728baca4767c0e784e1e64bb44b5a5a753021a04"
integrity sha512-GFxmfEAa0vi9i5sd0R2kcrI9ks0r82NasRq5QHh2ysGngrc6GiqD5CDf1FjPenY4vApmFASBIIlk/jj5J5YbmQ==
dependencies:
"@types/unist" "^2.0.0"
unist-util-generated@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.0.tgz#86fafb77eb6ce9bfa6b663c3f5ad4f8e56a60113"
integrity sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw==
unist-util-is@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.1.1.tgz#e8aece0b102fa9bc097b0fef8f870c496d4a6236"
integrity sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ==
unist-util-position@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-4.0.3.tgz#5290547b014f6222dff95c48d5c3c13a88fadd07"
integrity sha512-p/5EMGIa1qwbXjA+QgcBXaPWjSnZfQ2Sc3yBEEfgPwsEmJd8Qh+DSk3LGnmOM4S1bY2C0AjmMnB8RuEYxpPwXQ==
dependencies:
"@types/unist" "^2.0.0"
unist-util-stringify-position@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.2.tgz#5c6aa07c90b1deffd9153be170dce628a869a447"
integrity sha512-7A6eiDCs9UtjcwZOcCpM4aPII3bAAGv13E96IkawkOAW0OhH+yRxtY0lzo8KiHpzEMfH7Q+FizUmwp8Iqy5EWg==
dependencies:
"@types/unist" "^2.0.0"
unist-util-visit-parents@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz#e83559a4ad7e6048a46b1bdb22614f2f3f4724f2"
integrity sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==
dependencies:
"@types/unist" "^2.0.0"
unist-util-is "^5.0.0"
unist-util-visit-parents@^5.0.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.0.tgz#44bbc5d25f2411e7dfc5cecff12de43296aa8521"
integrity sha512-y+QVLcY5eR/YVpqDsLf/xh9R3Q2Y4HxkZTp7ViLDU6WtJCEcPmRzW1gpdWDCDIqIlhuPDXOgttqPlykrHYDekg==
dependencies:
"@types/unist" "^2.0.0"
unist-util-is "^5.0.0"
unist-util-visit@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-3.1.0.tgz#9420d285e1aee938c7d9acbafc8e160186dbaf7b"
integrity sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==
dependencies:
"@types/unist" "^2.0.0"
unist-util-is "^5.0.0"
unist-util-visit-parents "^4.0.0"
unist-util-visit@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.0.tgz#f41e407a9e94da31594e6b1c9811c51ab0b3d8f5"
integrity sha512-n7lyhFKJfVZ9MnKtqbsqkQEk5P1KShj0+//V7mAcoI6bpbUjh3C/OG8HVD+pBihfh6Ovl01m8dkcv9HNqYajmQ==
dependencies:
"@types/unist" "^2.0.0"
unist-util-is "^5.0.0"
unist-util-visit-parents "^5.0.0"
universalify@^0.1.0: universalify@^0.1.0:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
@ -10902,6 +11434,11 @@ url@^0.11.0:
punycode "1.3.2" punycode "1.3.2"
querystring "0.2.0" querystring "0.2.0"
use-sync-external-store@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz#3343c3fe7f7e404db70f8c687adf5c1652d34e82"
integrity sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ==
use@^3.1.0: use@^3.1.0:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
@ -10959,6 +11496,16 @@ uuid@^3.0.1, uuid@^3.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uvu@^0.5.0:
version "0.5.3"
resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.3.tgz#3d83c5bc1230f153451877bfc7f4aea2392219ae"
integrity sha512-brFwqA3FXzilmtnIyJ+CxdkInkY/i4ErvP7uV0DnUVxQcQ55reuHphorpF+tZoVHK2MniZ/VJzI7zJQoc9T9Yw==
dependencies:
dequal "^2.0.0"
diff "^5.0.0"
kleur "^4.0.3"
sade "^1.7.3"
v8-compile-cache@^2.0.3: v8-compile-cache@^2.0.3:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745"
@ -10996,6 +11543,24 @@ verror@1.10.0:
core-util-is "1.0.2" core-util-is "1.0.2"
extsprintf "^1.2.0" extsprintf "^1.2.0"
vfile-message@^3.0.0:
version "3.1.2"
resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.2.tgz#a2908f64d9e557315ec9d7ea3a910f658ac05f7d"
integrity sha512-QjSNP6Yxzyycd4SVOtmKKyTsSvClqBPJcd00Z0zuPj3hOIjg0rUPG6DbFGPvUKRgYyaIWLPKpuEclcuvb3H8qA==
dependencies:
"@types/unist" "^2.0.0"
unist-util-stringify-position "^3.0.0"
vfile@^5.0.0:
version "5.3.2"
resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.2.tgz#b499fbc50197ea50ad3749e9b60beb16ca5b7c54"
integrity sha512-w0PLIugRY3Crkgw89TeMvHCzqCs/zpreR31hl4D92y6SOE07+bfJe+dK5Q2akwS+i/c801kzjoOr9gMcTe6IAA==
dependencies:
"@types/unist" "^2.0.0"
is-buffer "^2.0.0"
unist-util-stringify-position "^3.0.0"
vfile-message "^3.0.0"
vm-browserify@^1.0.1: vm-browserify@^1.0.1:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"