Compare commits
25 Commits
919382f4e7
...
adc4f163b7
Author | SHA1 | Date | |
---|---|---|---|
adc4f163b7 | |||
4b0014ba83 | |||
e1e49b06da | |||
9fcd193a87 | |||
fb5559158f | |||
e4bdadba6e | |||
080d604a17 | |||
a31dca4006 | |||
2a3330e38d | |||
80ce9beef5 | |||
4161397d56 | |||
ce81d99cdf | |||
ce32c85b64 | |||
8f8e799323 | |||
c9a5593fe0 | |||
0575d795ea | |||
5220c1a484 | |||
99e4a01ef2 | |||
8b482d6761 | |||
9fcb902ee2 | |||
cb67d7afe0 | |||
e100a8520c | |||
c2fbd272fd | |||
5970422d58 | |||
1124e0af52 |
@ -1,4 +1,5 @@
|
||||
REACT_APP_API_URL="http://localhost:2001"
|
||||
REACT_APP_IMAGINARY_URL="http://localhost:9000"
|
||||
REACT_APP_SENTRY_DSN=""
|
||||
REACT_APP_SOURCE_REPO_URL="https://git.wilw.dev/wilw/treadl"
|
||||
REACT_APP_SUPPORT_ROOT="https://git.wilw.dev/wilw/treadl/wiki/"
|
@ -18,8 +18,9 @@
|
||||
"react-dom": "^16.13.1",
|
||||
"react-helmet": "^6.0.0",
|
||||
"react-joyride": "^2.4.0",
|
||||
"react-redux": "^7.2.0",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-redux": "^8.0.1",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-scripts": "3.4.1",
|
||||
"react-toastify": "^4.4.0",
|
||||
"redux": "^4.0.0",
|
||||
|
@ -1,55 +1,79 @@
|
||||
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 { connect } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import { Grid, Divider, Icon, Container } from 'semantic-ui-react';
|
||||
|
||||
import api from 'api';
|
||||
import actions from 'actions';
|
||||
import utils from 'utils/utils.js';
|
||||
import NavBar from 'components/includes/NavBar';
|
||||
|
||||
import logo from 'images/logo/main.png';
|
||||
|
||||
import MarketingHome from './marketing/Home.js';
|
||||
import MarketingPricing from './marketing/Pricing.js';
|
||||
import PrivacyPolicy from './marketing/PrivacyPolicy';
|
||||
import TermsOfUse from './marketing/TermsOfUse';
|
||||
|
||||
import Login from './Login.js';
|
||||
import ForgottenPassword from './ForgottenPassword';
|
||||
import ResetPassword from './ResetPassword';
|
||||
|
||||
import Home from './main/Home.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 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 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 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 Docs from './docs';
|
||||
|
||||
|
||||
|
||||
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 }) {
|
||||
function App() {
|
||||
const dispatch = useDispatch();
|
||||
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;
|
||||
|
||||
useEffect(() => {
|
||||
api.auth.autoLogin(onLoginSuccess);
|
||||
}, [onLoginSuccess]);
|
||||
api.auth.autoLogin(token => dispatch(actions.auth.receiveLogin(token)));
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loggedInUserId) return;
|
||||
api.users.getMyProjects(onReceiveProjects);
|
||||
api.groups.getMine(onReceiveGroups);
|
||||
api.users.getMyProjects(p => dispatch(actions.projects.receiveProjects(p)));
|
||||
api.groups.getMine(g => dispatch(actions.groups.receiveGroups(g)));
|
||||
api.invitations.get(({ invitations, sentInvitations}) => {
|
||||
onReceiveInvitations(invitations.concat(sentInvitations));
|
||||
dispatch(actions.invitations.receiveInvitations(invitations.concat(sentInvitations)));
|
||||
});
|
||||
}, [loggedInUserId, onReceiveProjects, onReceiveGroups, onReceiveInvitations]);
|
||||
}, [dispatch, loggedInUserId]);
|
||||
|
||||
useEffect(() => {
|
||||
window.drift && window.drift.on('ready', () => {
|
||||
onDriftReady();
|
||||
dispatch(actions.users.initDrift());
|
||||
});
|
||||
}, [onDriftReady]);
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user && driftReady && !syncedToDrift && window.drift) {
|
||||
@ -58,35 +82,52 @@ function App({ user, groups, syncedToDrift, driftReady, onOpenRegister, onCloseA
|
||||
username: user.username,
|
||||
createdAt: user.createdAt,
|
||||
});
|
||||
onDriftSynced();
|
||||
dispatch(actions.users.syncDrift(null));
|
||||
}
|
||||
}, [user, driftReady, syncedToDrift, onDriftSynced]);
|
||||
}, [dispatch, user, driftReady, syncedToDrift]);
|
||||
|
||||
return (
|
||||
<div style={{display: 'flex', flexDirection: 'column', minHeight: '100vh'}}>
|
||||
<Helmet defaultTitle={'Treadl'} titleTemplate={`%s | Treadl`} />
|
||||
<NavBar />
|
||||
<div style={{ flex: '1 0 0' }}>
|
||||
<Switch>
|
||||
<Route exact path="/" render={props => (isAuthenticated
|
||||
? <Home {...props} />
|
||||
: <MarketingHome {...props} onRegisterClicked={onOpenRegister} />)
|
||||
<Routes>
|
||||
<Route end path="/" element={isAuthenticated
|
||||
? <Home />
|
||||
: <MarketingHome onRegisterClicked={() => dispatch(actions.auth.openRegister())} />
|
||||
} />
|
||||
<Route path="/pricing" render={props => <MarketingPricing {...props} onRegisterClicked={onOpenRegister} />} />
|
||||
<Route path="/privacy" component={PrivacyPolicy} />
|
||||
<Route path="/terms-of-use" component={TermsOfUse} />
|
||||
<Route path="/password/forgotten" component={ForgottenPassword} />
|
||||
<Route path="/password/reset" component={ResetPassword} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
<Route path="/projects/new" component={NewProject} />
|
||||
<Route path="/groups/new" component={NewGroup} />
|
||||
<Route path="/groups/:id" component={Group} />
|
||||
<Route path='/root' component={Root} />
|
||||
<Route path="/:username/edit" component={Profile} />
|
||||
<Route path="/:username/:projectPath" component={Project} />
|
||||
<Route path="/:username" component={Profile} />
|
||||
</Switch>
|
||||
<Login open={isAuthenticating} authType={isAuthenticatingType} onClose={onCloseAuthentication} />
|
||||
<Route path="/privacy" element={<PrivacyPolicy />} />
|
||||
<Route path="/terms-of-use" element={<TermsOfUse />} />
|
||||
<Route path="/password/forgotten" element={<ForgottenPassword />} />
|
||||
<Route path="/password/reset" element={<ResetPassword />} />
|
||||
<Route path="/settings" element={<Settings />}>
|
||||
<Route path='identity' element={<SettingsIdentity />} />
|
||||
<Route path='notifications' element={<SettingsNotification />} />
|
||||
<Route path='account' element={<SettingsAccount />} />
|
||||
<Route path='' element={<SettingsIdentity />} />
|
||||
</Route>
|
||||
<Route path="/projects/new" element={<NewProject />} />
|
||||
<Route path="/groups/new" element={<NewGroup />} />
|
||||
<Route path="/groups/:id" element={<Group />}>
|
||||
<Route path='feed' element={<GroupFeed />} />
|
||||
<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/>
|
||||
<Divider hidden section />
|
||||
</div>
|
||||
@ -131,34 +172,4 @@ function App({ user, groups, syncedToDrift, driftReady, onOpenRegister, onCloseA
|
||||
);
|
||||
}
|
||||
|
||||
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));
|
||||
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;
|
||||
export default App;
|
||||
|
@ -1,31 +1,28 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Card, Input, Divider, Button,
|
||||
} from 'semantic-ui-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import { connect } from 'react-redux';
|
||||
import api from 'api';
|
||||
|
||||
class ForgottenPassword extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { email: '', loading: false };
|
||||
}
|
||||
function ForgottenPassword() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
sendEmail = () => {
|
||||
this.setState({ loading: true });
|
||||
api.auth.sendPasswordResetEmail(this.state.email, () => {
|
||||
this.setState({ loading: false });
|
||||
const sendEmail = () => {
|
||||
setLoading(true);
|
||||
api.auth.sendPasswordResetEmail(email, () => {
|
||||
setLoading(false);
|
||||
toast.info('If your account exists, a password email has been sent');
|
||||
this.props.history.push('/');
|
||||
navigate('/');
|
||||
}, (err) => {
|
||||
this.setState({ loading: false });
|
||||
setLoading(false);
|
||||
toast.error(err.message);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { email, loading } = this.state;
|
||||
return (
|
||||
<Card.Group centered style={{ marginTop: 50 }}>
|
||||
<Card raised color="yellow">
|
||||
@ -33,24 +30,15 @@ class ForgottenPassword extends Component {
|
||||
<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>
|
||||
<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 extra textAlign="right">
|
||||
<Button basic onClick={this.props.history.goBack} content="Cancel" />
|
||||
<Button color="teal" content="Send email" onClick={this.sendEmail} loading={loading} />
|
||||
<Button basic onClick={() => navigate('/')} content="Cancel" />
|
||||
<Button color="teal" content="Send email" onClick={sendEmail} loading={loading} />
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</Card.Group>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({ });
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
});
|
||||
const ForgottenPasswordContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(ForgottenPassword);
|
||||
|
||||
export default ForgottenPasswordContainer;
|
||||
export default ForgottenPassword;
|
||||
|
@ -1,65 +1,64 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Message, Modal, Grid, Form, Input, Button,
|
||||
} from 'semantic-ui-react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import actions from 'actions';
|
||||
import { api } from 'api';
|
||||
|
||||
import ReadingImage from 'images/reading.png';
|
||||
|
||||
class Login extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
registering: false, username: '', email: '', password: '', loading: false,
|
||||
function Login({ open, authType, onClose }) {
|
||||
const [username, setUsername] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
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 { email, password } = this.state;
|
||||
this.setState({ loading: true });
|
||||
api.auth.login(email, password, this.props.onLoginStart, (data) => {
|
||||
this.setState({ loading: false, password: '', email: '', username: '' });
|
||||
this.props.onLoginSuccess(data);
|
||||
this.props.onClose();
|
||||
const register = () => {
|
||||
setLoading(true);
|
||||
api.auth.register(username, email, password, () => dispatch(actions.auth.requestLogin()), (data) => {
|
||||
setLoading(false);
|
||||
setPassword('');
|
||||
setEmail('');
|
||||
setUsername('');
|
||||
dispatch(actions.auth.receiveLogin(data));
|
||||
onClose();
|
||||
}, (err) => {
|
||||
this.props.onLoginFailure(err);
|
||||
this.setState({ loading: false });
|
||||
dispatch(actions.auth.loginError(err));
|
||||
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 (
|
||||
<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>
|
||||
<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.Content>
|
||||
<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.
|
||||
</Message>
|
||||
|
||||
{this.props.error && (<div className="ui warning message">{this.props.error.message}</div>)}
|
||||
<Form onSubmit={this.register}>
|
||||
{error && (<div className="ui warning message">{error.message}</div>)}
|
||||
<Form onSubmit={register}>
|
||||
<Form.Field>
|
||||
<label>Pick a username
|
||||
<small> (you can use letters, numbers and underscores)</small>
|
||||
</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>
|
||||
<label>Email address
|
||||
<small> (for password resets & other important things)</small>
|
||||
</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>
|
||||
<label>Choose a strong password
|
||||
<small> (at least 6 characters)</small>
|
||||
</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>
|
||||
<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>
|
||||
@ -133,12 +132,12 @@ class Login extends Component {
|
||||
</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>
|
||||
<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.Content>
|
||||
<Grid stackable>
|
||||
@ -148,17 +147,17 @@ Welcome back <Button floated="right" onClick={this.props.onClose} basic content=
|
||||
</Grid.Column>
|
||||
|
||||
<Grid.Column computer={8}>
|
||||
{this.props.error && (<div className="ui warning message">{this.props.error.message}</div>)}
|
||||
<Form onSubmit={this.login}>
|
||||
{error && (<div className="ui warning message">{error.message}</div>)}
|
||||
<Form onSubmit={login}>
|
||||
<Form.Field>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<div className="ui hidden divider" />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
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;
|
||||
export default Login;
|
||||
|
@ -1,33 +1,30 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Card, Input, Divider, Button,
|
||||
} from 'semantic-ui-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import { connect } from 'react-redux';
|
||||
import api from 'api';
|
||||
|
||||
class ResetPassword extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { password: '', loading: false };
|
||||
}
|
||||
function ResetPassword() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [password, setPassword] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
resetPassword = () => {
|
||||
const resetPassword = () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const token = params.get('token');
|
||||
this.setState({ loading: true });
|
||||
api.auth.updatePasswordWithToken(token, this.state.password, () => {
|
||||
this.setState({ loading: false });
|
||||
setLoading(true);
|
||||
api.auth.updatePasswordWithToken(token, password, () => {
|
||||
setLoading(false);
|
||||
toast.info('Password changed successfully.');
|
||||
this.props.history.push('/');
|
||||
navigate('/');
|
||||
}, (err) => {
|
||||
this.setState({ loading: false });
|
||||
setLoading(false);
|
||||
toast.error(err.message);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { password, loading } = this.state;
|
||||
return (
|
||||
<Card.Group centered style={{ marginTop: 50 }}>
|
||||
<Card raised color="yellow">
|
||||
@ -35,23 +32,14 @@ class ResetPassword extends Component {
|
||||
<Card.Header>Enter a new password</Card.Header>
|
||||
<Card.Meta>Enter a new password below.</Card.Meta>
|
||||
<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 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>
|
||||
</Card.Group>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({ });
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
});
|
||||
const ResetPasswordContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(ResetPassword);
|
||||
|
||||
export default ResetPasswordContainer;
|
||||
export default ResetPassword;
|
||||
|
29
web/src/components/docs/index.js
Normal file
29
web/src/components/docs/index.js
Normal 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;
|
6
web/src/components/docs/stuff.md
Normal file
6
web/src/components/docs/stuff.md
Normal file
@ -0,0 +1,6 @@
|
||||
# Hello
|
||||
|
||||
This is some content
|
||||
|
||||
* and here
|
||||
* there
|
@ -1,76 +1,72 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Button } from 'semantic-ui-react';
|
||||
import api from 'api';
|
||||
class FileChooser extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isUploading: false };
|
||||
}
|
||||
|
||||
startUpload = () => {
|
||||
this.setState({ isUploading: true });
|
||||
this.props.onUploadStart && this.props.onUploadStart();
|
||||
}
|
||||
function FileChooser({ onUploadStart, onUploadFinish, onError, onComplete, content, trigger, accept, forType, forObject }) {
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const inputRef = useRef();
|
||||
|
||||
finishUpload = () => {
|
||||
this.setState({ isUploading: false });
|
||||
this.props.onUploadFinish && this.props.onUploadFinish();
|
||||
}
|
||||
const startUpload = () => {
|
||||
setIsUploading(true);
|
||||
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];
|
||||
if (file) {
|
||||
this.startUpload();
|
||||
startUpload();
|
||||
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();
|
||||
reader.onload = (e2) => {
|
||||
this.finishUpload();
|
||||
this.props.onComplete({ wif: e2.target.result, type: 'pattern' });
|
||||
finishUpload();
|
||||
onComplete({ wif: e2.target.result, type: 'pattern' });
|
||||
};
|
||||
reader.readAsText(file);
|
||||
} else {
|
||||
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) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('PUT', response.signedRequest);
|
||||
xhr.setRequestHeader('Content-Type', file.type);
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === 4) {
|
||||
this.finishUpload();
|
||||
finishUpload();
|
||||
if (xhr.status === 200) {
|
||||
// We pass back the original file name so it can be displayed nicely
|
||||
this.props.onComplete({ storedName: response.fileName, name: file.name, type: 'file' });
|
||||
} else if (this.props.onError) {
|
||||
this.finishUpload();
|
||||
this.props.onError('Unable to upload file');
|
||||
onComplete({ storedName: response.fileName, name: file.name, type: 'file' });
|
||||
} else if (onError) {
|
||||
finishUpload();
|
||||
onError('Unable to upload file');
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send(file);
|
||||
}, (err) => {
|
||||
this.finishUpload();
|
||||
if (this.props.onError) this.props.onError(err.message || 'Unable to upload file');
|
||||
finishUpload();
|
||||
if (onError) onError(err.message || 'Unable to upload file');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { content, trigger, accept } = this.props;
|
||||
return (
|
||||
<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
|
||||
? React.cloneElement(trigger, { loading: this.state.isUploading, onClick: this.chooseFile })
|
||||
: <Button size="small" color="blue" icon="file" fluid content={content || 'Choose a file'} 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={isUploading} onClick={chooseFile} />
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FileChooser;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import { Loader, List, Popup, Modal, Grid, Icon, Button, Container, Dropdown } from 'semantic-ui-react';
|
||||
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(() => {
|
||||
openSearchPopup(false);
|
||||
}, [history.location.pathname, openSearchPopup]);
|
||||
dispatch(actions.app.openSearchPopup(false));
|
||||
}, [dispatch]);
|
||||
|
||||
const logout = () => api.auth.logout(() => {
|
||||
onLogout();
|
||||
onDriftSynced(false);
|
||||
dispatch(actions.auth.logout());
|
||||
dispatch(actions.users.syncDrift(false))
|
||||
if (window.drift) window.drift.reset();
|
||||
history.push('/');
|
||||
navigate('/');
|
||||
});
|
||||
|
||||
const search = () => {
|
||||
updateSearching(true);
|
||||
api.search.all(searchTerm, updateSearchResults);
|
||||
dispatch(actions.app.updateSearching(true));
|
||||
api.search.all(searchTerm, r => dispatch(actions.app.updateSearchResults(r)));
|
||||
};
|
||||
|
||||
return (
|
||||
@ -99,8 +108,9 @@ function NavBar({ user, groups, onOpenLogin, onOpenRegister, isAuthenticated, on
|
||||
{isAuthenticated
|
||||
? (
|
||||
<div className='nav-links'>
|
||||
<Popup basic on='focus' open={searchPopupOpen} onOpen={e => openSearchPopup(true)} onClose={e => openSearchPopup(false)}
|
||||
trigger={<SearchBar><input placeholder='Click to search...' value={searchTerm} onChange={e => updateSearchTerm(e.target.value)} onKeyDown={e => e.keyCode === 13 && search()} /></SearchBar>}
|
||||
<Popup basic on='focus' open={searchPopupOpen}
|
||||
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'>
|
||||
{!searchResults?.users && !searchResults?.groups ?
|
||||
<small>
|
||||
@ -171,7 +181,7 @@ function NavBar({ user, groups, onOpenLogin, onOpenRegister, isAuthenticated, on
|
||||
}
|
||||
|
||||
<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>
|
||||
|
||||
<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" />
|
||||
>
|
||||
<Dropdown.Menu direction="left">
|
||||
<Dropdown.Item onClick={onOpenLogin}>Login</Dropdown.Item>
|
||||
<Dropdown.Item onClick={() => dispatch(actions.auth.openLogin())}>Login</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</span>
|
||||
<span className="above-mobile">
|
||||
<Button inverted basic onClick={onOpenLogin}>Login</Button>
|
||||
<Button inverted basic onClick={() => dispatch(actions.auth.openLogin())}>Login</Button>
|
||||
</span>
|
||||
<Button color="teal" onClick={onOpenRegister}>
|
||||
<Button color="teal" onClick={() => dispatch(actions.auth.openRegister())}>
|
||||
<span role="img" aria-label="wave">👋</span> Sign-up
|
||||
</Button>
|
||||
</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.Content>
|
||||
<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>
|
||||
</Modal.Content>
|
||||
<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>
|
||||
</Container>
|
||||
@ -255,29 +265,4 @@ function NavBar({ user, groups, onOpenLogin, onOpenRegister, isAuthenticated, on
|
||||
);
|
||||
}
|
||||
|
||||
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));
|
||||
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;
|
||||
export default NavBar;
|
||||
|
@ -84,7 +84,7 @@ const NewFeedMessage = connect(
|
||||
>
|
||||
<Dropdown.Menu>
|
||||
<FileChooser
|
||||
forType={forType} for={forObj}
|
||||
forType={forType} forObject={forObj}
|
||||
trigger=<Dropdown.Item icon="upload" content="Upload a file from your computer" />
|
||||
onUploadStart={e => updateAttachmentUploading(true) }
|
||||
onUploadFinish={e => updateAttachmentUploading(false) }
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import React, { useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button, Dropdown } from 'semantic-ui-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import actions from 'actions';
|
||||
@ -8,37 +8,33 @@ import api from 'api';
|
||||
|
||||
import FileChooser from 'components/includes/FileChooser';
|
||||
|
||||
class ObjectCreator extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isUploading: false };
|
||||
}
|
||||
function ObjectCreator({ project, onCreateObject, onError, fluid }) {
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
createNewPattern = () => {
|
||||
api.projects.createObject(this.props.project.fullName, { name: 'Untitled pattern', type: 'pattern' }, (object) => {
|
||||
this.props.onCreateObject(object);
|
||||
this.props.history.push(`/${this.props.project.fullName}/${object._id}/edit`);
|
||||
}, err => this.setState({ loading: false }));
|
||||
}
|
||||
const createNewPattern = () => {
|
||||
api.projects.createObject(project.fullName, { name: 'Untitled pattern', type: 'pattern' }, (object) => {
|
||||
dispatch(actions.objects.create(object));
|
||||
navigate(`/${project.fullName}/${object._id}/edit`);
|
||||
});
|
||||
};
|
||||
|
||||
fileUploaded = (file) => {
|
||||
this.setState({ isUploading: true });
|
||||
api.projects.createObject(this.props.project.fullName, {
|
||||
const fileUploaded = (file) => {
|
||||
setIsUploading(true);
|
||||
api.projects.createObject(project.fullName, {
|
||||
name: file.name, storedName: file.storedName, type: file.type, wif: file.wif,
|
||||
}, (object) => {
|
||||
this.setState({ isUploading: false });
|
||||
this.props.onCreateObject(object);
|
||||
this.props.history.push(`/${this.props.project.fullName}/${object._id}`);
|
||||
setIsUploading(false);
|
||||
dispatch(actions.objects.create(object));
|
||||
navigate(`/${project.fullName}/${object._id}`);
|
||||
}, (err) => {
|
||||
toast.error(err.message);
|
||||
this.setState({ isUploading: false });
|
||||
this.props.onError && this.props.onError(err);
|
||||
setIsUploading(false);
|
||||
onError && onError(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { project, fluid } = this.props;
|
||||
const { isUploading } = this.state;
|
||||
return (
|
||||
<Dropdown
|
||||
fluid={!!fluid}
|
||||
@ -46,35 +42,27 @@ class ObjectCreator extends Component {
|
||||
trigger=<Button color="teal" fluid content="Add something" icon="plus" loading={isUploading} />
|
||||
>
|
||||
<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
|
||||
forType="project"
|
||||
for={project}
|
||||
forObject={project}
|
||||
trigger=<Dropdown.Item icon="upload" content="Import a WIF file" />
|
||||
accept=".wif"
|
||||
onUploadStart={e => this.setState({ isUploading: true })}
|
||||
onUploadFinish={e => this.setState({ isUploading: false })}
|
||||
onComplete={this.fileUploaded}
|
||||
onUploadStart={e => setIsUploading(true)}
|
||||
onUploadFinish={e => setIsUploading(false)}
|
||||
onComplete={fileUploaded}
|
||||
/>
|
||||
<FileChooser
|
||||
forType="project"
|
||||
for={project}
|
||||
forObject={project}
|
||||
trigger=<Dropdown.Item icon="cloud upload" content="Upload an image or a file" />
|
||||
onUploadStart={e => this.setState({ isUploading: true })}
|
||||
onUploadFinish={e => this.setState({ isUploading: false })}
|
||||
onComplete={this.fileUploaded}
|
||||
onUploadStart={e => setIsUploading(true)}
|
||||
onUploadFinish={e => setIsUploading(false)}
|
||||
onComplete={fileUploaded}
|
||||
/>
|
||||
</Dropdown.Menu>
|
||||
</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;
|
||||
|
@ -1,16 +1,20 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import pell from 'pell';
|
||||
|
||||
class RichText extends Component {
|
||||
ensureHTTP(url) {
|
||||
function RichText({ value, onChange }) {
|
||||
const [completedInit, setCompletedInit] = useState(false);
|
||||
const textboxRef = useRef();
|
||||
|
||||
const ensureHTTP = (url) => {
|
||||
if (url.trim().toLowerCase().indexOf('http') !== 0) return `http://${url}`;
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
if (completedInit) return;
|
||||
pell.init({
|
||||
element: this.refs.textbox,
|
||||
onChange: this.props.onChange,
|
||||
element: textboxRef.current,
|
||||
onChange: onChange,
|
||||
actions: [
|
||||
{
|
||||
icon: '<i class="italic icon"></i>',
|
||||
@ -54,7 +58,7 @@ class RichText extends Component {
|
||||
title: 'Insert an image using a direct URL link',
|
||||
result: () => {
|
||||
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',
|
||||
result: () => {
|
||||
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',
|
||||
}).content.innerHTML = this.props.value || '';
|
||||
}
|
||||
}).content.innerHTML = value || '';
|
||||
setCompletedInit(true);
|
||||
}, [completedInit, value, onChange]);
|
||||
|
||||
render() {
|
||||
return <div ref="textbox" />;
|
||||
}
|
||||
return <div ref={textboxRef} />;
|
||||
}
|
||||
|
||||
export default RichText;
|
||||
|
@ -1,16 +1,15 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
|
||||
class RichTextViewer extends Component {
|
||||
render() {
|
||||
if (!this.props.content) return null;
|
||||
function RichTextViewer({ content, style, className }) {
|
||||
if (!content) return null;
|
||||
return (
|
||||
<p
|
||||
style={Object.assign({}, { breakLines: 'pre-line'}, this.props.style)}
|
||||
className={this.props.className}
|
||||
style={Object.assign({}, { breakLines: 'pre-line'}, style)}
|
||||
className={className}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
sanitizeHtml(this.props.content, {
|
||||
sanitizeHtml(content, {
|
||||
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'u']),
|
||||
allowedAttributes: {
|
||||
a: ['href', 'name', 'target'],
|
||||
@ -20,7 +19,6 @@ class RichTextViewer extends Component {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RichTextViewer;
|
||||
|
@ -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 { connect } from 'react-redux'
|
||||
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) {
|
||||
super(props);
|
||||
this.state = { open: false, searchTerm: '', oldSearchTerm: '', searchResults: null, searching: false };
|
||||
}
|
||||
componentDidMount() {
|
||||
this.searcher = setInterval(() => {
|
||||
if (this.state.searchTerm !== this.state.oldSearchTerm) this.search();
|
||||
}, 500);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.searcher);
|
||||
}
|
||||
const updateSearchTerm = e => {
|
||||
setSearching(true);
|
||||
setSearchTerm(e.target.value);
|
||||
};
|
||||
|
||||
updateSearchTerm = e => {
|
||||
this.setState({ searching: true, searchTerm: e.target.value })
|
||||
}
|
||||
search = () => {
|
||||
this.setState({ open: false, searching: true, oldSearchTerm: this.state.searchTerm });
|
||||
if (!this.state.searchTerm) return this.setState({ searching: false });
|
||||
api.search.users(this.state.searchTerm, searchResults => this.setState({ open: true, searching: false, searchResults }), err => this.setState({searching: false}));
|
||||
}
|
||||
const search = useCallback(() => {
|
||||
if (searchTerm === oldSearchTerm) return;
|
||||
setOpen(false);
|
||||
setSearching(true);
|
||||
setOldSearchTerm(searchTerm);
|
||||
if (!searchTerm) return setSearching(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 = () => {
|
||||
if (this.state.searchResults) this.setState({ open: true });
|
||||
}
|
||||
onSelected = user => {
|
||||
this.props.onSelected && this.props.onSelected(user);
|
||||
}
|
||||
const inputClicked = () => {
|
||||
if (searchResults) setOpen(true);
|
||||
};
|
||||
const choose = user => {
|
||||
onSelected && onSelected(user);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { searchResults, searching, open } = this.state;
|
||||
const { fluid } = this.props;
|
||||
return (
|
||||
<Popup hoverable position='bottom left' open={open} onClose={e => this.setState({ open: false })}
|
||||
trigger={<Input fluid={fluid} icon='search' iconPosition='left' placeholder='Search for a username...' onChange={this.updateSearchTerm} loading={searching} onClick={this.inputClicked}/>}
|
||||
<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={updateSearchTerm} loading={searching} onClick={inputClicked}/>}
|
||||
content={(
|
||||
<Menu borderless vertical>
|
||||
{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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const UserSearchContainer = connect(null, null)(UserSearch);
|
||||
|
||||
export default UserSearchContainer;
|
||||
export default UserSearch;
|
||||
|
@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Loader, Divider, Button, Message, Container, Segment, Grid, Card, Icon, List } from 'semantic-ui-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { toast } from 'react-toastify';
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
@ -13,29 +13,37 @@ import HelpLink from 'components/includes/HelpLink';
|
||||
import ProjectCard from 'components/includes/ProjectCard';
|
||||
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 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(() => {
|
||||
api.invitations.get(({ invitations, sentInvitations}) => {
|
||||
onReceiveInvitations(invitations.concat(sentInvitations));
|
||||
dispatch(actions.invitations.receiveInvitations(invitations.concat(sentInvitations)));
|
||||
});
|
||||
}, [onReceiveInvitations]);
|
||||
}, [dispatch]);
|
||||
useEffect(() => {
|
||||
api.users.getMyProjects(onReceiveProjects);
|
||||
api.users.getMyProjects(p => dispatch(actions.projects.receiveProjects(p)));
|
||||
setTimeout(() =>
|
||||
setRunJoyride(true), 2000);
|
||||
}, [onReceiveProjects]);
|
||||
}, [dispatch]);
|
||||
|
||||
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) => {
|
||||
api.invitations.accept(invite._id, (result) => {
|
||||
onDismissInvitation(invite._id);
|
||||
dispatch(actions.invitations.dismiss(invite._id));
|
||||
if (result.group) {
|
||||
onReceiveGroup(result.group);
|
||||
onJoinGroup(user._id, result.group._id);
|
||||
dispatch(actions.groups.receiveGroup(result.group));
|
||||
dispatch(actions.users.joinGroup(user._id, result.group._id));
|
||||
}
|
||||
}, err => toast.error(err.message));
|
||||
}
|
||||
@ -170,23 +178,4 @@ function Home({ user, groups, projects, invitations, loadingProjects, onReceiveP
|
||||
);
|
||||
}
|
||||
|
||||
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));
|
||||
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;
|
||||
export default Home;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Loader, Button, Segment } from 'semantic-ui-react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import utils from 'utils/utils.js';
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
@ -10,16 +10,29 @@ import FeedMessage from 'components/includes/FeedMessage';
|
||||
import NewFeedMessage from 'components/includes/NewFeedMessage';
|
||||
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 || [];
|
||||
|
||||
useEffect(() => {
|
||||
updateLoadingEntries(true);
|
||||
api.groups.getEntries(match.params.id, entries => {
|
||||
updateLoadingEntries(false);
|
||||
entries.forEach(e => onReceiveEntry(e));
|
||||
dispatch(actions.groups.updateLoadingEntries(true));
|
||||
api.groups.getEntries(id, entries => {
|
||||
dispatch(actions.groups.updateLoadingEntries(false));
|
||||
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);
|
||||
|
||||
@ -27,9 +40,9 @@ function Feed({ user, group, entries, onReceiveEntry, onDeleteEntry, newEntry, o
|
||||
<div>
|
||||
{utils.isInGroup(user, group._id) && <>
|
||||
{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 &&
|
||||
<div style={{textAlign:'center'}}>
|
||||
@ -45,35 +58,11 @@ function Feed({ user, group, entries, onReceiveEntry, onDeleteEntry, newEntry, o
|
||||
</Segment>
|
||||
}
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
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;
|
||||
export default Feed;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Segment, Loader, Menu, Message, Container, Button, Icon, Grid, Card } from 'semantic-ui-react';
|
||||
import { Switch, Route, Link, withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { Outlet, Link, useParams } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { toast } from 'react-toastify';
|
||||
import utils from 'utils/utils.js';
|
||||
import actions from 'actions';
|
||||
@ -10,53 +10,62 @@ import api from 'api';
|
||||
|
||||
import UserChip from 'components/includes/UserChip';
|
||||
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(() => {
|
||||
onRequest();
|
||||
api.groups.get(match.params.id, onReceiveGroup, onRequestFailed);
|
||||
}, [match.params.id, onRequest, onReceiveGroup, onRequestFailed]);
|
||||
dispatch(actions.groups.request());
|
||||
api.groups.get(id, g => dispatch(actions.groups.receiveGroup(g)), err => dispatch(actions.groups.requestFailed(err)));
|
||||
}, [dispatch, id]);
|
||||
|
||||
const join = () => {
|
||||
if (!user) return toast.warning('Please login or sign-up first');
|
||||
api.groups.createMember(match.params.id, user._id, () => {
|
||||
onJoinGroup(user._id, match.params.id);
|
||||
api.groups.createMember(id, user._id, () => {
|
||||
dispatch(actions.users.joingGroup(user._id, id));
|
||||
}, err => toast.error(err.message));
|
||||
}
|
||||
const leave = () => {
|
||||
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, () => {
|
||||
onLeaveGroup(user._id, match.params.id);
|
||||
api.groups.deleteMember(id, user._id, () => {
|
||||
dispatch(actions.users.leaveGroup(user._id, id));
|
||||
}, err => toast.error(err.message));
|
||||
}, () => {});
|
||||
}
|
||||
const toggleEmailSub = (key, 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
|
||||
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 = () => {
|
||||
api.groups.createJoinRequest(group._id, invitation => {
|
||||
toast.success('Request to join sent');
|
||||
onReceiveInvitations([invitation]);
|
||||
dispatch(actions.invitations.receiveInvitations([invitation]));
|
||||
}, err => toast.error(err.message));
|
||||
}
|
||||
|
||||
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) => {
|
||||
api.invitations.accept(invite._id, (result) => {
|
||||
onDismissInvitation(invite._id);
|
||||
dispatch(actions.invitations.dismiss(invite._id));
|
||||
if (result.group) {
|
||||
onJoinGroup(user._id, result.group._id);
|
||||
dispatch(actions.users.joinGroup(user._id, result.group._id));
|
||||
}
|
||||
}, err => toast.error(err.message));
|
||||
}
|
||||
@ -168,13 +177,7 @@ function Group({ user, group, requests, myRequests, loading, errorMessage, onRec
|
||||
}
|
||||
</Segment>
|
||||
}
|
||||
<Switch>
|
||||
<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>
|
||||
<Outlet />
|
||||
</>
|
||||
:
|
||||
<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) => {
|
||||
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;
|
||||
export default Group;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Grid, Table, Button, Input, Label, Header, Loader, Segment, Dropdown, Card } from 'semantic-ui-react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import moment from 'moment';
|
||||
import utils from 'utils/utils.js';
|
||||
@ -12,9 +12,19 @@ import UserChip from 'components/includes/UserChip';
|
||||
import HelpLink from 'components/includes/HelpLink';
|
||||
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 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(() => {
|
||||
if (utils.isGroupAdmin(user, group)) {
|
||||
@ -22,15 +32,15 @@ function Members({ user, group, members, requests, loading, onReceiveUser, onJoi
|
||||
}
|
||||
}, [user, group]);
|
||||
useEffect(() => {
|
||||
onUpdateGroupLoading(true);
|
||||
dispatch(actions.users.request(true));
|
||||
api.groups.getMembers(group._id, members => {
|
||||
members.forEach(onReceiveUser);
|
||||
onUpdateGroupLoading(false);
|
||||
members.forEach(u => dispatch(actions.users.receive(u)));
|
||||
dispatch(actions.users.request(false));
|
||||
}, err => {
|
||||
toast.error(err.message);
|
||||
onUpdateGroupLoading(false);
|
||||
dispatch(actions.users.request(false));
|
||||
});
|
||||
}, [group, onReceiveUser, onUpdateGroupLoading]);
|
||||
}, [dispatch, group]);
|
||||
|
||||
const copyLink = () => {
|
||||
joinLinkRef.current.select();
|
||||
@ -40,7 +50,7 @@ function Members({ user, group, members, requests, loading, onReceiveUser, onJoi
|
||||
const kickUser = (id) => {
|
||||
utils.confirm('Really kick this user?').then(() => {
|
||||
api.groups.deleteMember(group._id, id, () => {
|
||||
onLeaveGroup(id, group._id);
|
||||
dispatch(actions.users.leaveGroup(id, group._id));
|
||||
}, err => toast.error(err.message));
|
||||
}, () => {});
|
||||
}
|
||||
@ -60,14 +70,14 @@ function Members({ user, group, members, requests, loading, onReceiveUser, onJoi
|
||||
}
|
||||
const approveRequest = (invite) => {
|
||||
api.invitations.accept(invite._id, (result) => {
|
||||
onDismissInvitation(invite._id);
|
||||
onReceiveUser(invite.invitedBy);
|
||||
onJoinGroup(invite.user, group._id);
|
||||
dispatch(actions.invitations.dismiss(invite._id))
|
||||
dispatch(actions.users.receive(invite.invitedBy));
|
||||
dispatch(actions.users.joinGroup(invite.user, group._id));
|
||||
toast.success(`${invite.invitedBy.username} is now a member`);
|
||||
}, err => toast.error(err.message));
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
@ -164,23 +174,4 @@ function Members({ user, group, members, requests, loading, onReceiveUser, onJoi
|
||||
)
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
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;
|
||||
export default Members;
|
||||
|
@ -2,23 +2,30 @@ import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Icon, Form, Grid, Input, Checkbox, Button, Divider } from 'semantic-ui-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
|
||||
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 = () => {
|
||||
onUpdateGroupLoading(true);
|
||||
dispatch(actions.groups.request(true));
|
||||
api.groups.create({ name: newGroupName, description: newGroupDescription, closed: newGroupClosed }, (group) => {
|
||||
onReceiveGroup(group);
|
||||
onUpdateGroupLoading(false);
|
||||
history.push(`/groups/${group._id}/members`);
|
||||
dispatch(actions.groups.receiveGroup(group));
|
||||
dispatch(actions.groups.request(false));
|
||||
navigate(`/groups/${group._id}/members`);
|
||||
}, (err) => {
|
||||
onUpdateGroupLoading(false);
|
||||
dispatch(actions.groups.request(false));
|
||||
toast.error(err.message);
|
||||
});
|
||||
}
|
||||
@ -38,12 +45,12 @@ function NewGroup({ user, newGroupName, newGroupDescription, newGroupClosed, onU
|
||||
|
||||
<h3>About your group</h3>
|
||||
<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 />
|
||||
<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)' }}>
|
||||
<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>
|
||||
@ -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>
|
||||
|
||||
<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} />
|
||||
</div>
|
||||
</Grid.Column>
|
||||
@ -60,19 +67,4 @@ function NewGroup({ user, newGroupName, newGroupDescription, newGroupClosed, onU
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
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;
|
||||
export default NewGroup;
|
||||
|
@ -1,16 +1,29 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Input, Divider, Loader, Segment, Card, Dropdown, Button } from 'semantic-ui-react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
|
||||
import ProjectCard from 'components/includes/ProjectCard';
|
||||
|
||||
function Projects({ group, myProjects, onReceiveProject, projectFilter, updateProjectFilter }) {
|
||||
function Projects() {
|
||||
const [loadingProjects, setLoadingProjects] = useState(false);
|
||||
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(() => {
|
||||
setLoadingProjects(true);
|
||||
@ -29,7 +42,7 @@ function Projects({ group, myProjects, onReceiveProject, projectFilter, updatePr
|
||||
if (index > -1) groupVisibility.splice(index, 1);
|
||||
else groupVisibility.push(group._id);
|
||||
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 newProjects = Object.assign([], projects);
|
||||
if (index > -1 && existingIndex > -1) newProjects.splice(existingIndex, 1);
|
||||
@ -66,7 +79,7 @@ function Projects({ group, myProjects, onReceiveProject, projectFilter, updatePr
|
||||
<AddProject style={{float:'right'}} />
|
||||
</>}
|
||||
{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 />
|
||||
|
||||
@ -88,23 +101,4 @@ function Projects({ group, myProjects, onReceiveProject, projectFilter, updatePr
|
||||
)
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
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;
|
||||
export default Projects;
|
||||
|
@ -1,31 +1,40 @@
|
||||
import React from 'react';
|
||||
import { Header, Button, Divider, Segment, Form, } from 'semantic-ui-react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { toast } from 'react-toastify';
|
||||
import utils from 'utils/utils.js';
|
||||
import actions from 'actions';
|
||||
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 = () => {
|
||||
onUpdateGroupLoading(true);
|
||||
dispatch(actions.groups.request(true));
|
||||
const { _id, name, description, closed } = group;
|
||||
api.groups.update(_id, { name, description, closed }, () => {
|
||||
onUpdateGroupLoading(false);
|
||||
dispatch(actions.groups.request(false));
|
||||
toast.info('Group updated');
|
||||
}, err => {
|
||||
toast.error(err.message);
|
||||
onUpdateGroupLoading(false);
|
||||
dispatch(actions.groups.request(false));
|
||||
});
|
||||
}
|
||||
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(() => {
|
||||
api.groups.delete(group._id, () => {
|
||||
toast.info('Group deleted');
|
||||
onDeleteGroup(group._id);
|
||||
history.push(`/`);
|
||||
dispatch(actions.groups.deleteGroup(group._id));
|
||||
navigate(`/`);
|
||||
}, err => toast.error(err.message));
|
||||
}, () => {});
|
||||
}
|
||||
@ -35,9 +44,9 @@ function Settings({ user, group, loading, onUpdateGroupLoading, onUpdateGroup, o
|
||||
<Segment color='blue'>
|
||||
<Header>About this group</Header>
|
||||
<Form>
|
||||
<Form.Input label='Group name' value={group.name} onChange={e => onUpdateGroup(group._id, { name: e.target.value })} />
|
||||
<Form.TextArea label='Group description' value={group.description} onChange={e => onUpdateGroup(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.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 => dispatch(actions.groups.updateGroup(group._id, { description: e.target.value }))}/>
|
||||
<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)' }}>
|
||||
<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>
|
||||
@ -56,17 +65,4 @@ function Settings({ user, group, loading, onUpdateGroupLoading, onUpdateGroup, o
|
||||
)
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
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;
|
||||
export default Settings;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Icon, Form, Message, Grid, Input, Button, Divider } from 'semantic-ui-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import utils from 'utils/utils.js';
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
@ -11,44 +11,48 @@ import api from 'api';
|
||||
import UserChip from 'components/includes/UserChip';
|
||||
import HelpLink from 'components/includes/HelpLink';
|
||||
|
||||
class NewProject extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
name: 'My new project', description: '', visibility: 'public', openSource: true, groupVisibility: [], error: '', loading: false,
|
||||
};
|
||||
}
|
||||
function NewProject() {
|
||||
const [name, setName] = useState('My new project');
|
||||
const [description, setDescription] = useState('');
|
||||
const [visibility, setVisibility] = useState('public');
|
||||
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) => {
|
||||
this.setState({ name: event.target.value });
|
||||
}
|
||||
|
||||
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);
|
||||
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 };
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
name, description, visibility, openSource, groupVisibility
|
||||
} = this.state;
|
||||
const { user, groups } = this.props;
|
||||
const updateName = (event) => {
|
||||
setName(event.target.value);
|
||||
};
|
||||
|
||||
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 (
|
||||
<Grid stackable centered>
|
||||
<Helmet title={'Create Project'} />
|
||||
@ -66,17 +70,17 @@ Create a new project
|
||||
|
||||
<h3>About your project</h3>
|
||||
<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 />
|
||||
<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 />
|
||||
|
||||
<h3>Project visibility</h3>
|
||||
<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>
|
||||
<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>
|
||||
<Divider hidden />
|
||||
|
||||
@ -89,7 +93,7 @@ Create a new project
|
||||
label='Make this project always visible to members of these groups'
|
||||
value={groupVisibility}
|
||||
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 />
|
||||
|
||||
{this.state.error && <Message color="orange" content={this.state.error} />}
|
||||
{error && <Message color="orange" content={error} />}
|
||||
<div style={{textAlign:'right'}}>
|
||||
<Button basic onClick={this.props.history.goBack}>Cancel</Button>
|
||||
<Button color="teal" icon="check" content="Create project" onClick={this.createProject} loading={this.state.loading} />
|
||||
<Button basic onClick={() => navigate(-1)}>Cancel</Button>
|
||||
<Button color="teal" icon="check" content="Create project" onClick={createProject} loading={loading} />
|
||||
</div>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
export default NewProject;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Segment, Label, Input, Icon, Card, Loader } from 'semantic-ui-react';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import utils from 'utils/utils.js';
|
||||
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 [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(() => {
|
||||
setLoading(true);
|
||||
api.projects.getObjects(fullProjectPath, projects => {
|
||||
onReceiveObjects(projects);
|
||||
api.projects.getObjects(fullProjectPath, o => {
|
||||
dispatch(actions.objects.receiveMultiple(o));
|
||||
setLoading(false);
|
||||
}, err => setLoading(false));
|
||||
}, [fullProjectPath, onReceiveObjects]);
|
||||
}, [fullProjectPath, dispatch]);
|
||||
|
||||
let filteredObjects = objects;
|
||||
if (objectFilter) {
|
||||
@ -157,26 +171,4 @@ function ObjectList({ user, objects, currentObject, project, fullProjectPath, on
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
export default ObjectList;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
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 { Link, withRouter } from 'react-router-dom';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import moment from 'moment';
|
||||
import utils from 'utils/utils.js';
|
||||
import actions from 'actions';
|
||||
@ -15,23 +15,42 @@ import DraftPreview from './objects/DraftPreview';
|
||||
import NewFeedMessage from 'components/includes/NewFeedMessage';
|
||||
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 [editingDescription, setEditingDescription] = 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(() => {
|
||||
if (!objectId) return;
|
||||
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) => {
|
||||
utils.confirm('Delete object', 'Really delete this object? This cannot be undone.').then(() => {
|
||||
history.push(`/${fullProjectPath}`);
|
||||
api.objects.delete(object._id, () => onDeleteObject(object._id), err => toast.error(err.message));
|
||||
navigate(`/${fullProjectPath}`);
|
||||
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 ?
|
||||
<div style={{marginBottom: 5}}>
|
||||
<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)} />
|
||||
/>
|
||||
</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)'}}>
|
||||
{object.type === 'pattern' &&
|
||||
<div style={{maxHeight: 400, overflowY: 'scroll'}}>
|
||||
<DraftPreview object={object} onImageLoaded={i => onEditObject(object._id, 'patternImage', i)}/>
|
||||
<DraftPreview object={object} />
|
||||
</div>
|
||||
}
|
||||
{object.isImage &&
|
||||
@ -182,7 +201,7 @@ function ObjectViewer({ user, myProjects, project, fullProjectPath, onEditObject
|
||||
|
||||
{editingDescription ?
|
||||
<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)} />
|
||||
</div>
|
||||
:
|
||||
@ -202,42 +221,14 @@ function ObjectViewer({ user, myProjects, project, fullProjectPath, onEditObject
|
||||
}
|
||||
</h3>
|
||||
{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>
|
||||
}
|
||||
{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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
export default ObjectViewer;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Message, Form, TextArea, Container, Button, Icon, Grid, Card } from 'semantic-ui-react';
|
||||
import { Switch, Route, Link, withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { Outlet, Link, useParams, useLocation } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import utils from 'utils/utils.js';
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
@ -11,24 +11,28 @@ import UserChip from 'components/includes/UserChip';
|
||||
import HelpLink from 'components/includes/HelpLink';
|
||||
import ObjectCreator from 'components/includes/ObjectCreator';
|
||||
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 { 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(() => {
|
||||
onRequest();
|
||||
api.projects.get(fullName, onReceiveProject, onRequestFailed);
|
||||
}, [user, onRequest, fullName, onReceiveProject, onRequestFailed]);
|
||||
dispatch(actions.projects.request());
|
||||
api.projects.get(fullName, p => dispatch(actions.projects.receiveProject(p)), err => dispatch(actions.projects.requestFailed(err)));
|
||||
}, [user, dispatch, fullName]);
|
||||
|
||||
const wideBody = () => !match.isExact
|
||||
const wideBody = () => !location.pathname.toLowerCase().endsWith(fullName.toLowerCase());
|
||||
|
||||
const saveDescription = () => {
|
||||
onEditDescription(false);
|
||||
api.projects.update(fullName, { description: project.description }, onReceiveProject);
|
||||
dispatch(actions.projects.editDescription(false));
|
||||
api.projects.update(fullName, { description: project.description }, p => dispatch(actions.projects.receiveProject(p)));
|
||||
}
|
||||
|
||||
const getDescription = () => {
|
||||
@ -50,11 +54,11 @@ function Project({ user, project, fullName, errorMessage, editingDescription, on
|
||||
{project
|
||||
&& (
|
||||
<div>
|
||||
{history.location?.state?.prevPath &&
|
||||
{/*history.location?.state?.prevPath &&
|
||||
<div style={{marginBottom:15}}>
|
||||
<Button basic secondary onClick={e => history.goBack()} icon='arrow left' content='Go back' />
|
||||
</div>
|
||||
}
|
||||
*/}
|
||||
|
||||
{wideBody() && project.owner &&
|
||||
<>
|
||||
@ -85,7 +89,7 @@ function Project({ user, project, fullName, errorMessage, editingDescription, on
|
||||
{editingDescription
|
||||
? (
|
||||
<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>
|
||||
</Form>
|
||||
)
|
||||
@ -100,7 +104,7 @@ function Project({ user, project, fullName, errorMessage, editingDescription, on
|
||||
</div>
|
||||
}
|
||||
{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
|
||||
</Button>
|
||||
)
|
||||
@ -121,14 +125,7 @@ function Project({ user, project, fullName, errorMessage, editingDescription, on
|
||||
)}
|
||||
|
||||
<Grid.Column computer={wideBody() ? 16 : 12} tablet={wideBody() ? 16 : 10}>
|
||||
{project && (
|
||||
<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>
|
||||
) }
|
||||
{project && <Outlet />}
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
|
||||
@ -140,22 +137,4 @@ function Project({ user, project, fullName, errorMessage, editingDescription, on
|
||||
);
|
||||
}
|
||||
|
||||
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];
|
||||
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;
|
||||
export default Project;
|
||||
|
@ -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 { withRouter } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import { connect } from 'react-redux';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import utils from 'utils/utils.js';
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
|
||||
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) => {
|
||||
this.setState({ visibility: r.checked ? 'private' : 'public' });
|
||||
}
|
||||
function ProjectSettings() {
|
||||
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) => {
|
||||
this.setState({ openSource: c.checked });
|
||||
}
|
||||
const changeVisibility = (event, r) => {
|
||||
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(() => {
|
||||
api.projects.update(this.props.fullProjectPath, { name: this.state.name }, (project) => {
|
||||
this.props.onReceiveProject(project);
|
||||
this.props.history.push(`/${project.owner.username}/${project.path}`);
|
||||
api.projects.update(fullProjectPath, { name: name }, (project) => {
|
||||
dispatch(actions.projects.receiveProject(project));
|
||||
navigate(`/${project.owner.username}/${project.path}`);
|
||||
}, err => toast.error(err.message));
|
||||
}, () => {});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
saveVisibility = () => {
|
||||
api.projects.update(this.props.project.fullName, { visibility: this.state.visibility, openSource: this.state.openSource, groupVisibility: this.state.groupVisibility }, (project) => {
|
||||
this.props.onReceiveProject(project);
|
||||
const saveVisibility = () => {
|
||||
api.projects.update(project.fullName, { visibility, openSource, groupVisibility }, (p) => {
|
||||
dispatch(actions.projects.receiveProject(p));
|
||||
toast.info('Visibility saved');
|
||||
}, 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(() => {
|
||||
api.projects.delete(this.props.fullProjectPath, () => {
|
||||
this.props.onDeleteProject(this.props.project._id);
|
||||
api.projects.delete(fullProjectPath, () => {
|
||||
dispatch(actions.projects.deleteProject(project._id));
|
||||
toast.info('🗑️ Project deleted');
|
||||
this.props.history.push('/');
|
||||
this.setState({ nameError: '' });
|
||||
navigate('/');
|
||||
}, err => toast.error(err.message));
|
||||
}, () => {});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { name, visibility, openSource } = this.state;
|
||||
const { groups } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<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>
|
||||
<Input
|
||||
type="text" value={name}
|
||||
action=<Button color="teal" content="Update" onClick={this.saveName} />
|
||||
onChange={e => this.setState({ name: e.target.value })}
|
||||
action=<Button color="teal" content="Update" onClick={saveName} />
|
||||
onChange={e => setName(e.target.value)}
|
||||
/>
|
||||
</Segment>
|
||||
|
||||
@ -77,9 +83,9 @@ class ProjectSettings extends Component {
|
||||
<Segment color="yellow">
|
||||
<h3>Project visibility</h3>
|
||||
<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>
|
||||
<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>
|
||||
<Divider hidden />
|
||||
|
||||
@ -90,16 +96,16 @@ class ProjectSettings extends Component {
|
||||
|
||||
<Form.Select multiple
|
||||
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 }))}
|
||||
onChange={(e, s) => this.setState({ groupVisibility: s.value })}
|
||||
onChange={(e, s) => setGroupVisibility(s.value)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</Form>
|
||||
<Divider hidden />
|
||||
|
||||
<Button color="teal" content="Save visibility" onClick={this.saveVisibility} />
|
||||
<Button color="teal" content="Save visibility" onClick={saveVisibility} />
|
||||
</Segment>
|
||||
|
||||
<Divider hidden section />
|
||||
@ -107,28 +113,10 @@ class ProjectSettings extends Component {
|
||||
<Segment color="red">
|
||||
<h3>Delete project</h3>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
export default ProjectSettings;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { withRouter, Prompt } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import styled from 'styled-components';
|
||||
import ElementPan from 'components/includes/ElementPan';
|
||||
@ -25,67 +25,63 @@ export const StyledPattern = styled.div`
|
||||
margin:20px;
|
||||
`;
|
||||
|
||||
class Draft extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { unsaved: false, saving: false };
|
||||
}
|
||||
function Draft() {
|
||||
const [unsaved, setUnsaved] = useState(false);
|
||||
const [saving, setSaving] = useState(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() {
|
||||
api.objects.get(this.props.match.params.objectId, (object) => {
|
||||
if (!object.pattern.baseSize) object.pattern.baseSize = 10;
|
||||
this.setState(object);
|
||||
useEffect(() => {
|
||||
api.objects.get(objectId, (o) => {
|
||||
if (!o.pattern.baseSize) o.pattern.baseSize = 10;
|
||||
setObject(o);
|
||||
setPattern(o.pattern);
|
||||
});
|
||||
}
|
||||
}, [objectId]);
|
||||
|
||||
updateObject = (update) => {
|
||||
this.setState(Object.assign({}, this.state, update));
|
||||
this.setState({ unsaved: true });
|
||||
}
|
||||
const updateObject = (update) => {
|
||||
setObject(Object.assign({}, object, update));
|
||||
setUnsaved(true);
|
||||
};
|
||||
|
||||
updatePattern = (update) => {
|
||||
const newPattern = Object.assign({}, this.state.pattern, update);
|
||||
this.setState(Object.assign({}, this.state, { pattern: newPattern }));
|
||||
this.setState({ unsaved: true });
|
||||
}
|
||||
const updatePattern = (update) => {
|
||||
const newPattern = Object.assign({}, pattern, update);
|
||||
setPattern(Object.assign({}, pattern, newPattern));
|
||||
setUnsaved(true);
|
||||
};
|
||||
|
||||
saveObject = () => {
|
||||
this.setState({ saving: true });
|
||||
const saveObject = () => {
|
||||
setSaving(true);
|
||||
const canvas = document.getElementsByClassName('drawdown')[0];
|
||||
const object = Object.assign({}, this.state);
|
||||
object.preview = canvas.toDataURL();
|
||||
api.objects.update(this.props.match.params.objectId, object, (o) => {
|
||||
const newObject = Object.assign({}, object);
|
||||
newObject.preview = canvas.toDataURL();
|
||||
api.objects.update(objectId, newObject, (o) => {
|
||||
toast.success('Pattern saved');
|
||||
this.props.onReceiveObject(o);
|
||||
this.setState({ unsaved: false, saving: false });
|
||||
dispatch(actions.objects.receive(o));
|
||||
setUnsaved(false);
|
||||
setSaving(false);
|
||||
}, (err) => {
|
||||
toast.error(err.message);
|
||||
this.setState({ saving: false });
|
||||
setSaving(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
rerunTour = () => {
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.pattern) return null;
|
||||
const { unsaved, saving } = this.state;
|
||||
const { warp, weft, tieups, baseSize } = this.state.pattern;
|
||||
if (!pattern) return null;
|
||||
const { warp, weft, tieups, baseSize } = pattern;
|
||||
const cellStyle = { width: `${baseSize || 10}px`, height: `${baseSize || 10}px` };
|
||||
return (
|
||||
<div>
|
||||
<Helmet title={`${this.state?.name || 'Weaving Draft'}`} />
|
||||
<Prompt
|
||||
when={unsaved ? true : false}
|
||||
message='You have unsaved changes. Are you sure you want to leave tnis page?'
|
||||
/>
|
||||
<Helmet title={`${name || 'Weaving Draft'}`} />
|
||||
<Tour id='pattern' run={true} />
|
||||
<div style={{display: 'flex'}}>
|
||||
|
||||
<div style={{flex: 1, overflow: 'hidden'}}>
|
||||
<ElementPan
|
||||
disabled={!(this.props.editor && this.props.editor.tool === 'pan')}
|
||||
disabled={!(editor?.tool === 'pan')}
|
||||
startX={5000}
|
||||
startY={0}
|
||||
>
|
||||
@ -96,9 +92,9 @@ class Draft extends Component {
|
||||
}}
|
||||
>
|
||||
|
||||
<Warp baseSize={baseSize} cellStyle={cellStyle} warp={warp} weft={weft} updatePattern={this.updatePattern} />
|
||||
<Weft cellStyle={cellStyle} warp={warp} weft={weft} baseSize={baseSize} updatePattern={this.updatePattern} />
|
||||
<Tieups cellStyle={cellStyle} warp={warp} weft={weft} tieups={tieups} updatePattern={this.updatePattern} baseSize={baseSize}/>
|
||||
<Warp baseSize={baseSize} cellStyle={cellStyle} warp={warp} weft={weft} updatePattern={updatePattern} />
|
||||
<Weft cellStyle={cellStyle} warp={warp} weft={weft} baseSize={baseSize} updatePattern={updatePattern} />
|
||||
<Tieups cellStyle={cellStyle} warp={warp} weft={weft} tieups={tieups} updatePattern={updatePattern} baseSize={baseSize}/>
|
||||
<Drawdown warp={warp} weft={weft} tieups={tieups} baseSize={baseSize} />
|
||||
|
||||
</StyledPattern>
|
||||
@ -108,21 +104,12 @@ class Draft extends Component {
|
||||
<div style={{width: 300, marginLeft: 20}}>
|
||||
<HelpLink className='joyride-help' link={`${process.env.REACT_APP_SUPPORT_ROOT}Editing-patterns#using-the-pattern-editor`} marginBottom/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({ editor: state.objects.editor });
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onReceiveObject: object => dispatch(actions.objects.receive(object)),
|
||||
});
|
||||
const DraftContainer = withRouter(connect(
|
||||
mapStateToProps, mapDispatchToProps,
|
||||
)(Draft));
|
||||
|
||||
export default DraftContainer;
|
||||
export default Draft;
|
||||
|
@ -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;
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Loader } from 'semantic-ui-react';
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
@ -11,40 +11,40 @@ import Weft from './Weft.js';
|
||||
import Tieups from './Tieups.js';
|
||||
import Drawdown from './Drawdown.js';
|
||||
|
||||
class DraftPreview extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { loading: false };
|
||||
}
|
||||
function DraftPreview({ object }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [pattern, setPattern] = useState();
|
||||
const dispatch = useDispatch();
|
||||
const objectId = object?._id;
|
||||
|
||||
componentDidMount() {
|
||||
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) {
|
||||
const generatePreview = useCallback(() => {
|
||||
setTimeout(() => {
|
||||
const c = document.getElementsByClassName('drawdown')[0];
|
||||
const preview = c && c.toDataURL();
|
||||
const preview = c?.toDataURL();
|
||||
if (preview) {
|
||||
api.objects.update(object._id, { preview }, () => {
|
||||
this.props.onEditObject(object._id, 'preview', preview);
|
||||
api.objects.update(objectId, { preview }, () => {
|
||||
dispatch(actions.objects.update(objectId, 'preview', preview));
|
||||
});
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}, err => this.setState({ loading: false }));
|
||||
}
|
||||
}, [dispatch, objectId]);
|
||||
|
||||
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(() => {
|
||||
const id = this.props.object._id;
|
||||
const { warp, weft } = this.state.pattern;
|
||||
const baseSize = 6;
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
@ -53,12 +53,12 @@ class DraftPreview extends Component {
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = 'black';
|
||||
const warpCanvas = document.querySelector(`.preview-${id} .warp-threads`);
|
||||
const warpColourwayCanvas = document.querySelector(`.preview-${id} .warp-colourway`);
|
||||
const weftCanvas = document.querySelector(`.preview-${id} .weft-threads`);
|
||||
const weftColourwayCanvas = document.querySelector(`.preview-${id} .weft-colourway`);
|
||||
const drawdownCanvas = document.querySelector(`.preview-${id} .drawdown`);
|
||||
const tieupsCanvas = document.querySelector(`.preview-${id} .tieups`);
|
||||
const warpCanvas = document.querySelector(`.preview-${objectId} .warp-threads`);
|
||||
const warpColourwayCanvas = document.querySelector(`.preview-${objectId} .warp-colourway`);
|
||||
const weftCanvas = document.querySelector(`.preview-${objectId} .weft-threads`);
|
||||
const weftColourwayCanvas = document.querySelector(`.preview-${objectId} .weft-colourway`);
|
||||
const drawdownCanvas = document.querySelector(`.preview-${objectId} .drawdown`);
|
||||
const tieupsCanvas = document.querySelector(`.preview-${objectId} .tieups`);
|
||||
if (warpCanvas) {
|
||||
ctx.drawImage(warpColourwayCanvas, canvas.width - warpCanvas.width - weft.treadles * baseSize - 20, 0);
|
||||
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(tieupsCanvas, canvas.width - weft.treadles * baseSize - 10, 10);
|
||||
ctx.drawImage(drawdownCanvas, canvas.width - drawdownCanvas.width - weft.treadles * baseSize - 20, warp.shafts * baseSize + 20);
|
||||
|
||||
this.props.onImageLoaded(canvas.toDataURL());
|
||||
dispatch(actions.objects.update(objectId, 'patternImage', canvas.toDataURL()));
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}, [dispatch, objectId, pattern]);
|
||||
|
||||
render() {
|
||||
if (this.state.loading) return <Loader active />;
|
||||
if (!this.state.pattern) return null;
|
||||
const { warp, weft, tieups } = this.state.pattern;
|
||||
useEffect(() => {
|
||||
unifyCanvas();
|
||||
}, [unifyCanvas])
|
||||
|
||||
if (loading) return <Loader active />;
|
||||
if (!pattern) return null;
|
||||
const { warp, weft, tieups } = pattern;
|
||||
if (!warp || !weft || !tieups) return null;
|
||||
const baseSize = 6;
|
||||
const cellStyle = { width: `${baseSize || 10}px`, height: `${baseSize || 10}px` };
|
||||
@ -85,7 +87,7 @@ class DraftPreview extends Component {
|
||||
startY={0}
|
||||
>
|
||||
<StyledPattern
|
||||
className={`pattern preview-${this.props.object._id}`}
|
||||
className={`pattern preview-${objectId}`}
|
||||
style={{
|
||||
width: '2000px',
|
||||
height: '1000px',
|
||||
@ -98,15 +100,6 @@ class DraftPreview extends Component {
|
||||
</StyledPattern>
|
||||
</ElementPan>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
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;
|
||||
export default DraftPreview;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import utils from 'utils/utils';
|
||||
|
||||
@ -12,23 +12,17 @@ const StyledDrawdown = styled.canvas`
|
||||
width: ${props => props.warp.threads * props.baseSize}px;
|
||||
`;
|
||||
|
||||
class Drawdown extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.squares = {};
|
||||
}
|
||||
// Cache
|
||||
const squares = {};
|
||||
|
||||
componentDidMount() {
|
||||
this.paintDrawdown();
|
||||
}
|
||||
function Drawdown({ baseSize, warp, weft, tieups }) {
|
||||
const drawdownRef = useRef();
|
||||
useEffect(() => paintDrawdown());
|
||||
const { editor } = useSelector(state => ({ editor: state.objects.editor }));
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
this.paintDrawdown(prevProps);
|
||||
}
|
||||
|
||||
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 getSquare = (thread, size, colour) => {
|
||||
const { view } = editor;
|
||||
if (squares[view] && squares[view][thread] && squares[view][thread][size] && squares[view][thread][size][colour]) return squares[view][thread][size][colour];
|
||||
const m_canvas = document.createElement('canvas');
|
||||
m_canvas.width = size;
|
||||
m_canvas.height = size;
|
||||
@ -43,7 +37,7 @@ class Drawdown extends Component {
|
||||
if (view === 'colour' || view === 'interlacement') {
|
||||
mc.fillStyle = colour;
|
||||
mc.fillRect(0, 0, size, size);
|
||||
if (this.props.editor.view === 'interlacement') {
|
||||
if (editor.view === 'interlacement') {
|
||||
if (thread === 'warp') {
|
||||
const grd = mc.createLinearGradient(0, 0, size, 0);
|
||||
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 (!this.squares[view][thread]) this.squares[view][thread] = {};
|
||||
if (!this.squares[view][thread][size]) this.squares[view][thread][size] = {};
|
||||
this.squares[view][thread][size][colour] = m_canvas;
|
||||
if (!squares[view]) squares[view] = {};
|
||||
if (!squares[view][thread]) squares[view][thread] = {};
|
||||
if (!squares[view][thread][size]) squares[view][thread][size] = {};
|
||||
squares[view][thread][size][colour] = m_canvas;
|
||||
return m_canvas;
|
||||
}
|
||||
};
|
||||
|
||||
paintDrawdown(prevProps) {
|
||||
const canvas = this.refs.drawdown;
|
||||
const paintDrawdown = () => {
|
||||
const canvas = drawdownRef.current;
|
||||
const ctx = canvas.getContext('2d', { alpha: false });
|
||||
const {
|
||||
baseSize, warp, weft, tieups,
|
||||
} = this.props;
|
||||
|
||||
for (let tread = 0; tread < weft.threads; tread++) {
|
||||
for (let thread = 0; thread < warp.threads; thread++) {
|
||||
@ -83,40 +74,25 @@ class Drawdown extends Component {
|
||||
const shaft = warp.threading[thread].shaft;
|
||||
const tieup = tieups[treadle - 1];
|
||||
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) {
|
||||
const weftColour = utils.rgb(weft.treadling[tread].colour || weft.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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { warp, weft, baseSize } = this.props;
|
||||
return (
|
||||
<StyledDrawdown ref="drawdown" className="drawdown joyride-drawdown"
|
||||
<StyledDrawdown ref={drawdownRef} className="drawdown joyride-drawdown"
|
||||
width={warp.threads * baseSize}
|
||||
height={weft.threads * baseSize}
|
||||
weft={weft} warp={warp} baseSize={baseSize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({ editor: state.objects.editor });
|
||||
const DrawdownContainer = connect(
|
||||
mapStateToProps,
|
||||
)(Drawdown);
|
||||
|
||||
export default DrawdownContainer;
|
||||
export default Drawdown;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledTieups = styled.canvas`
|
||||
@ -7,46 +7,40 @@ const StyledTieups = styled.canvas`
|
||||
right:10px;
|
||||
`;
|
||||
|
||||
class Tieups extends Component {
|
||||
function Tieups({ cellStyle, warp, weft, tieups, updatePattern, baseSize }) {
|
||||
useEffect(() => paintTieups());
|
||||
const tieupRef = useRef(null);
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
this.paintTieups();
|
||||
}
|
||||
componentDidMount() {
|
||||
this.paintTieups();
|
||||
}
|
||||
|
||||
fillUpTo = (tieups, limit) => {
|
||||
let i = tieups.length;
|
||||
const fillUpTo = (t, limit) => {
|
||||
let i = t.length;
|
||||
while (i <= limit) {
|
||||
tieups.push([]);
|
||||
t.push([]);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
getTieupShaft = (event) => {
|
||||
};
|
||||
const getTieupShaft = (event) => {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const y = event.clientY - rect.top;
|
||||
const x = 0 - (event.clientX - rect.right);
|
||||
const shaft = this.props.warp.shafts - parseInt(y / this.props.baseSize);
|
||||
const tieup = this.props.weft.treadles - parseInt(x / this.props.baseSize) - 1;
|
||||
const shaft = warp.shafts - parseInt(y / baseSize);
|
||||
const tieup = weft.treadles - parseInt(x / baseSize) - 1;
|
||||
return { tieup, shaft };
|
||||
}
|
||||
click = (event) => {
|
||||
const { tieup, shaft } = this.getTieupShaft(event);
|
||||
const tieups = Object.assign([], this.props.tieups);
|
||||
};
|
||||
const click = (event) => {
|
||||
const { tieup, shaft } = getTieupShaft(event);
|
||||
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].indexOf(shaft) === -1) tieups[tieup].push(shaft);
|
||||
else tieups[tieup].splice(tieups[tieup].indexOf(shaft));
|
||||
}
|
||||
this.props.updatePattern({ tieups });
|
||||
if (tieups[tieup].indexOf(shaft) === -1) newTieups[tieup].push(shaft);
|
||||
else newTieups[tieup].splice(tieups[tieup].indexOf(shaft));
|
||||
}
|
||||
updatePattern({ tieups: newTieups });
|
||||
};
|
||||
|
||||
paintTieups() {
|
||||
const canvas = this.refs.tieups;
|
||||
const paintTieups = () => {
|
||||
const canvas = tieupRef.current;
|
||||
const ctx = canvas.getContext('2d');// , { alpha: false });
|
||||
const { baseSize, tieups } = this.props;
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
@ -68,12 +62,9 @@ class Tieups extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { warp, weft, baseSize } = this.props;
|
||||
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;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Confirm, Select, Segment, Accordion, Grid, Icon, Input, Button,
|
||||
} from 'semantic-ui-react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import Slider from 'rc-slider';
|
||||
import styled from 'styled-components';
|
||||
@ -35,69 +35,78 @@ const ColourSquare = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
class Tools extends Component {
|
||||
state = { colours: [], activeDrawers: ['properties', 'drawing', 'palette'], view: 'interlacement' }
|
||||
function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updatePattern, updateObject, saveObject }) {
|
||||
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) => {
|
||||
this.props.onEditorUpdated({ tool, colour: this.props.editor.colour });
|
||||
}
|
||||
const { project, editor } = useSelector(state => {
|
||||
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) => {
|
||||
this.props.onEditorUpdated({ tool: 'colour', colour });
|
||||
}
|
||||
const enableTool = (tool) => {
|
||||
dispatch(actions.objects.updateEditor({ tool, colour: editor.colour }));
|
||||
};
|
||||
|
||||
setView = (view) => {
|
||||
this.props.onEditorUpdated({ view });
|
||||
}
|
||||
const setColour = (colour) => {
|
||||
dispatch(actions.objects.updateEditor({ tool: 'colour', colour }));
|
||||
};
|
||||
|
||||
setName = (event) => {
|
||||
this.props.updateObject({ name: event.target.value });
|
||||
}
|
||||
const setEditorView = (view) => {
|
||||
dispatch(actions.objects.updateEditor({ view }));
|
||||
};
|
||||
|
||||
setShafts = (event) => {
|
||||
const warp = { ...this.props.warp, shafts: parseInt(event.target.value, 10) || 1 };
|
||||
this.props.updatePattern({ warp });
|
||||
}
|
||||
const setName = (event) => {
|
||||
updateObject({ name: event.target.value });
|
||||
};
|
||||
|
||||
setTreadles = (event) => {
|
||||
const weft = { ...this.props.weft, treadles: parseInt(event.target.value, 10) || 1 };
|
||||
this.props.updatePattern({ weft });
|
||||
}
|
||||
const setShafts = (event) => {
|
||||
updatePattern({ warp: { ...warp, shafts: parseInt(event.target.value, 10) || 1 } });
|
||||
};
|
||||
|
||||
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 index = this.state.activeDrawers.indexOf(drawer);
|
||||
const drawers = this.state.activeDrawers;
|
||||
const drawerIsActive = drawer => activeDrawers.indexOf(drawer) > -1;
|
||||
|
||||
const activateDrawer = (drawer) => {
|
||||
const index = activeDrawers.indexOf(drawer);
|
||||
const drawers = activeDrawers;
|
||||
if (index === -1) {
|
||||
drawers.push(drawer);
|
||||
} else {
|
||||
drawers.splice(index, 1);
|
||||
}
|
||||
this.setState({ activeDrawers: drawers });
|
||||
}
|
||||
setActiveDrawers(drawers);
|
||||
};
|
||||
|
||||
deleteObject = () => {
|
||||
api.objects.delete(this.props.match.params.objectId, () => {
|
||||
const deleteObject = () => {
|
||||
api.objects.delete(objectId, () => {
|
||||
toast('🗑️ Pattern deleted');
|
||||
this.props.onObjectDeleted(this.props.match.params.objectId);
|
||||
this.props.history.push(`/${this.props.project.fullName}`);
|
||||
dispatch(actions.objects.delete(objectId));
|
||||
navigate(`/${project.fullName}`);
|
||||
}, 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.')
|
||||
if (sure) {
|
||||
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.'));
|
||||
if (!newWidth) return;
|
||||
const { warp } = this.props;
|
||||
if (newWidth > warp.threading.length) {
|
||||
let i = warp.threading.length;
|
||||
while (i < newWidth) {
|
||||
@ -110,14 +119,13 @@ class Tools extends Component {
|
||||
warp.threading.splice(newWidth);
|
||||
warp.threads = warp.threading.length;
|
||||
}
|
||||
this.props.updatePattern({ warp });
|
||||
this.props.onEditorUpdated();
|
||||
this.props.onEditorUpdated({ tool: 'pan' });
|
||||
}
|
||||
changeHeight = () => {
|
||||
updatePattern({ warp });
|
||||
dispatch(actions.objects.updateEditor());
|
||||
dispatch(actions.objects.updateEditor( { tool: 'pan' }));
|
||||
};
|
||||
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.'));
|
||||
if (!newHeight) return;
|
||||
const { weft } = this.props;
|
||||
if (newHeight > weft.treadling.length) {
|
||||
let i = weft.treadling.length;
|
||||
while (i < newHeight) {
|
||||
@ -130,32 +138,30 @@ class Tools extends Component {
|
||||
weft.treadling.splice(newHeight);
|
||||
weft.threads = weft.treadling.length;
|
||||
}
|
||||
this.props.updatePattern({ weft });
|
||||
this.props.onEditorUpdated();
|
||||
}
|
||||
updatePattern({ weft });
|
||||
dispatch(actions.objects.updateEditor());
|
||||
};
|
||||
|
||||
render() {
|
||||
const { warp, weft, editor, unsaved, saving } = this.props;
|
||||
return (
|
||||
<div className="pattern-toolbox joyride-tools">
|
||||
{unsaved &&
|
||||
<Segment attached="top">
|
||||
<Button fluid color="teal" icon="save" content="Save pattern" onClick={() => this.props.saveObject(this.refs.canvas)} loading={saving}/>
|
||||
<Button style={{marginTop: 5}} fluid icon='refresh' content='Undo changes' onClick={this.revertChanges} />
|
||||
<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={revertChanges} />
|
||||
</Segment>
|
||||
}
|
||||
<Segment attached>
|
||||
<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
|
||||
</Accordion.Title>
|
||||
<Accordion.Content active={this.drawerIsActive('view')}>
|
||||
<Accordion.Content active={drawerIsActive('view')}>
|
||||
<small>Drawdown view</small>
|
||||
<Select
|
||||
size="tiny"
|
||||
fluid
|
||||
value={editor.view}
|
||||
onChange={(e, s) => this.setView(s.value)}
|
||||
onChange={(e, s) => setEditorView(s.value)}
|
||||
style={{ fontSize: '11px' }}
|
||||
options={[
|
||||
{ key: 1, value: 'interlacement', text: 'Interlacement' },
|
||||
@ -166,98 +172,81 @@ class Tools extends Component {
|
||||
/>
|
||||
<div style={{ marginTop: '5px' }} />
|
||||
<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.Title active={this.drawerIsActive('properties')} onClick={e => this.activateDrawer('properties')}>
|
||||
<Accordion.Title active={drawerIsActive('properties')} onClick={e => activateDrawer('properties')}>
|
||||
<Icon name="dropdown" /> Properties
|
||||
</Accordion.Title>
|
||||
<Accordion.Content active={this.drawerIsActive('properties')}>
|
||||
<Accordion.Content active={drawerIsActive('properties')}>
|
||||
<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.Row className='joyride-threads'>
|
||||
<Grid.Column>
|
||||
<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>
|
||||
<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.Row>
|
||||
<Grid.Row style={{paddingTop: 0}}>
|
||||
<Grid.Column>
|
||||
<small>Width</small>
|
||||
<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>
|
||||
<small>Height</small>
|
||||
<Input fluid readOnly value={weft.treadling?.length || 0} size="mini"
|
||||
action={{icon: 'edit', onClick: this.changeHeight}}
|
||||
action={{icon: 'edit', onClick: changeHeight}}
|
||||
/>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</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
|
||||
</Accordion.Title>
|
||||
<Accordion.Content active={this.drawerIsActive('drawing')}>
|
||||
<Accordion.Content active={drawerIsActive('drawing')}>
|
||||
<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-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-straight' data-tooltip="Straight draw" color={this.props.editor.tool === 'straight' && 'blue'} size="tiny" icon onClick={() => this.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-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={editor.tool === 'colour' && 'blue'} size="tiny" icon onClick={() => enableTool('colour')}><Icon name="paint brush" /></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={editor.tool === 'point' && 'blue'} size="tiny" icon onClick={() => enableTool('point')}><Icon name="chevron up" /></Button>
|
||||
</Button.Group>
|
||||
</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
|
||||
<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.Content active={this.drawerIsActive('palette')}>
|
||||
{this.props.pattern.colours && this.props.pattern.colours.map(colour =>
|
||||
<ColourSquare key={colour} colour={utils.rgb(colour)} onClick={() => this.setColour(colour)} />
|
||||
<Accordion.Content active={drawerIsActive('palette')}>
|
||||
{pattern.colours && pattern.colours.map(colour =>
|
||||
<ColourSquare key={colour} colour={utils.rgb(colour)} onClick={() => setColour(colour)} />
|
||||
)}
|
||||
</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
|
||||
</Accordion.Title>
|
||||
<Accordion.Content active={this.drawerIsActive('advanced')}>
|
||||
<Button size="small" basic color="red" fluid onClick={e => this.setState({ deleteModalOpen: true })}>Delete pattern</Button>
|
||||
<Accordion.Content active={drawerIsActive('advanced')}>
|
||||
<Button size="small" basic color="red" fluid onClick={e => setDeleteModalOpen(true)}>Delete pattern</Button>
|
||||
<Confirm
|
||||
open={this.state.deleteModalOpen}
|
||||
open={deleteModalOpen}
|
||||
content="Really delete this pattern?"
|
||||
onCancel={e => this.setState({ deleteModalOpen: false })}
|
||||
onConfirm={this.deleteObject}
|
||||
onCancel={e => setDeleteModalOpen(false)}
|
||||
onConfirm={deleteObject}
|
||||
/>
|
||||
</Accordion.Content>
|
||||
</Accordion>
|
||||
</Segment>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
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;
|
||||
export default Tools;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { connect } from 'react-redux';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import utils from 'utils/utils.js';
|
||||
|
||||
@ -17,157 +17,154 @@ const StyledWarp = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
class Warp extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.squares = {};
|
||||
this.markers = {};
|
||||
}
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
this.paintDrawdown();
|
||||
}
|
||||
componentDidMount() {
|
||||
this.paintDrawdown();
|
||||
}
|
||||
const squares = {};
|
||||
const markers = {};
|
||||
|
||||
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 y = event.clientY - rect.top;
|
||||
const x = 0 - (event.clientX - rect.right);
|
||||
const shaft = this.props.warp.shafts - parseInt(y / this.props.baseSize);
|
||||
const thread = parseInt(x / this.props.baseSize);
|
||||
const shaft = warp.shafts - parseInt(y / baseSize);
|
||||
const thread = parseInt(x / baseSize);
|
||||
return { shaft, thread };
|
||||
}
|
||||
};
|
||||
|
||||
mouseClickColourway = event => {
|
||||
const warp = Object.assign({}, this.props.warp);
|
||||
const { thread } = this.getThreadShaft(event);
|
||||
if (thread >= warp.threading.length) this.fillUpTo(warp, thread);
|
||||
warp.threading[thread].colour = this.props.colour;
|
||||
this.props.updatePattern({ warp });
|
||||
}
|
||||
mouseDownColourway = event => {
|
||||
const mouseClickColourway = event => {
|
||||
const newWarp = Object.assign({}, warp);
|
||||
const { thread } = getThreadShaft(event);
|
||||
if (thread >= warp.threading.length) fillUpTo(newWarp, thread);
|
||||
newWarp.threading[thread].colour = editor.colour;
|
||||
updatePattern({ warp: newWarp });
|
||||
};
|
||||
const mouseDownColourway = event => {
|
||||
event.preventDefault();
|
||||
this.draggingColourway = true;
|
||||
}
|
||||
mouseUpColourway = event => this.draggingColourway = false;
|
||||
mouseMoveColourway = (event) => {
|
||||
if (this.draggingColourway) {
|
||||
const warp = Object.assign({}, this.props.warp);
|
||||
const { thread } = this.getThreadShaft(event);
|
||||
if (thread >= warp.threading.length) this.fillUpTo(warp, thread);
|
||||
warp.threading[thread].colour = this.props.colour;
|
||||
this.props.updatePattern({ warp });
|
||||
}
|
||||
setDraggingColourway(true);
|
||||
};
|
||||
const mouseUpColourway = event => setDraggingColourway(false);
|
||||
const mouseMoveColourway = (event) => {
|
||||
if (draggingColourway) {
|
||||
const newWarp = Object.assign({}, warp);
|
||||
const { thread } = getThreadShaft(event);
|
||||
if (thread >= warp.threading.length) fillUpTo(newWarp, thread);
|
||||
newWarp.threading[thread].colour = editor.colour;
|
||||
updatePattern({ warp: newWarp });
|
||||
}
|
||||
};
|
||||
|
||||
mouseUp = event => this.dragging = false;
|
||||
mouseDown = (event) => {
|
||||
const mouseUp = event => setDragging(false);
|
||||
const mouseDown = (event) => {
|
||||
event.preventDefault();
|
||||
const { shaft, thread } = this.getThreadShaft(event);
|
||||
this.startShaft = shaft;
|
||||
this.startThread = thread;
|
||||
this.dragging = true;
|
||||
}
|
||||
mouseMove = (event) => {
|
||||
if (this.dragging && this.props.tool) {
|
||||
const warp = Object.assign({}, this.props.warp);
|
||||
const { shaft, thread } = this.getThreadShaft(event);
|
||||
const { shaft, thread } = getThreadShaft(event);
|
||||
setStartShaft(shaft);
|
||||
setStartThread(thread);
|
||||
setDragging(true);
|
||||
};
|
||||
const mouseMove = (event) => {
|
||||
if (dragging && editor.tool) {
|
||||
const newWarp = Object.assign({}, warp);
|
||||
const { shaft, thread } = getThreadShaft(event);
|
||||
|
||||
let lX = this.startThread; let hX = thread; let lY = this.startShaft; let
|
||||
hY = shaft;
|
||||
let xDirection = 1; let
|
||||
yDirection = 1;
|
||||
if (thread < this.startThread) {
|
||||
let lX = startThread; let hX = thread; let lY = startShaft; let hY = shaft;
|
||||
let xDirection = 1; let yDirection = 1;
|
||||
if (thread < startThread) {
|
||||
lX = thread;
|
||||
hX = this.startThread;
|
||||
hX = startThread;
|
||||
xDirection = -1;
|
||||
}
|
||||
if (shaft < this.startShaft) {
|
||||
if (shaft < startShaft) {
|
||||
lY = shaft;
|
||||
hY = this.startShaft;
|
||||
hY = startShaft;
|
||||
yDirection = -1;
|
||||
}
|
||||
|
||||
let x = xDirection > 0 ? lX : hX;
|
||||
let y = yDirection > 0 ? lY : hY;
|
||||
if (this.props.tool === 'colour') {
|
||||
if (thread >= warp.threading.length) this.fillUpTo(warp, thread);
|
||||
warp.threading[thread].colour = this.props.colour;
|
||||
if (editor.tool === 'colour') {
|
||||
if (thread >= warp.threading.length) fillUpTo(newWarp, thread);
|
||||
newWarp.threading[thread].colour = editor.colour;
|
||||
}
|
||||
if (this.props.tool === 'straight') {
|
||||
if (editor.tool === 'straight') {
|
||||
while (x <= hX && x >= lX) {
|
||||
if (x >= warp.threading.length || warp.threading.length - x < 5) this.fillUpTo(warp, x + 5);
|
||||
warp.threading[x].shaft = y;
|
||||
if (x >= warp.threading.length || warp.threading.length - x < 5) fillUpTo(newWarp, x + 5);
|
||||
newWarp.threading[x].shaft = y;
|
||||
x += xDirection;
|
||||
y += yDirection;
|
||||
if (y > hY || y < lY) y = yDirection > 0 ? lY : hY;
|
||||
}
|
||||
}
|
||||
if (this.props.tool === 'point') {
|
||||
if (editor.tool === 'point') {
|
||||
while (x <= hX && x >= lX) {
|
||||
if (x >= warp.threading.length || warp.threading.length - x < 5) this.fillUpTo(warp, x + 5);
|
||||
warp.threading[x].shaft = y;
|
||||
if (x >= warp.threading.length || warp.threading.length - x < 5) fillUpTo(newWarp, x + 5);
|
||||
newWarp.threading[x].shaft = y;
|
||||
x += xDirection;
|
||||
y += yDirection;
|
||||
if (y > hY || y <= lY) yDirection = 0 - yDirection;
|
||||
}
|
||||
}
|
||||
this.props.updatePattern({ warp });
|
||||
updatePattern({ warp: newWarp });
|
||||
}
|
||||
}
|
||||
click = (event) => {
|
||||
if (this.props.tool === 'point' || this.props.tool === 'straight') {
|
||||
const { thread, shaft } = this.getThreadShaft(event);
|
||||
const warp = Object.assign({}, this.props.warp);
|
||||
if (thread > warp.threading.length || warp.threading.length - thread < 5) this.fillUpTo(warp, thread + 5);
|
||||
const warpThread = warp.threading[thread];
|
||||
};
|
||||
const click = (event) => {
|
||||
if (editor.tool === 'point' || editor.tool === 'straight') {
|
||||
const { thread, shaft } = getThreadShaft(event);
|
||||
const newWarp = Object.assign({}, warp);
|
||||
if (thread > warp.threading.length || warp.threading.length - thread < 5) fillUpTo(newWarp, thread + 5);
|
||||
const warpThread = newWarp.threading[thread];
|
||||
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;
|
||||
while (i <= limit) {
|
||||
warp.threading.push({ shaft: 0 });
|
||||
warp.threads++;
|
||||
w.threading.push({ shaft: 0 });
|
||||
w.threads++;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
getMarker(size) {
|
||||
if (this.markers[size]) return this.markers[size];
|
||||
const getMarker = (size) => {
|
||||
if (markers[size]) return markers[size];
|
||||
const m_canvas = document.createElement('canvas');
|
||||
m_canvas.width = this.props.baseSize;
|
||||
m_canvas.height = this.props.baseSize;
|
||||
m_canvas.width = baseSize;
|
||||
m_canvas.height = baseSize;
|
||||
const mc = m_canvas.getContext('2d');
|
||||
mc.fillStyle = 'black';
|
||||
mc.fillRect(0, 0, this.props.baseSize, this.props.baseSize);
|
||||
this.markers[size] = m_canvas;
|
||||
mc.fillRect(0, 0, baseSize, baseSize);
|
||||
markers[size] = m_canvas;
|
||||
return m_canvas;
|
||||
}
|
||||
};
|
||||
|
||||
getSquare(size, colour) {
|
||||
if (this.squares[size] && this.squares[size][colour]) return this.squares[size][colour];
|
||||
const getSquare = (size, colour) => {
|
||||
if (squares[size] && squares[size][colour]) return squares[size][colour];
|
||||
const m_canvas = document.createElement('canvas');
|
||||
m_canvas.width = this.props.baseSize;
|
||||
m_canvas.width = baseSize;
|
||||
m_canvas.height = 10;
|
||||
const mc = m_canvas.getContext('2d');
|
||||
mc.fillStyle = utils.rgb(colour);
|
||||
mc.fillRect(0, 0, this.props.baseSize, 10);
|
||||
if (!this.squares[size]) this.squares[size] = {};
|
||||
this.squares[size][colour] = m_canvas;
|
||||
mc.fillRect(0, 0, baseSize, 10);
|
||||
if (!squares[size]) squares[size] = {};
|
||||
squares[size][colour] = m_canvas;
|
||||
return m_canvas;
|
||||
}
|
||||
};
|
||||
|
||||
paintDrawdown() {
|
||||
const canvas = this.refs.warp;
|
||||
const colourway = this.refs.colourway;
|
||||
const paintDrawdown = () => {
|
||||
const canvas = warpRef.current;
|
||||
const colourway = colourwayRef.current;
|
||||
const ctx = canvas.getContext('2d');// , { alpha: false });
|
||||
const ctx2 = colourway.getContext('2d');// , { alpha: false });
|
||||
const { baseSize, warp } = this.props;
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
@ -184,52 +181,40 @@ class Warp extends Component {
|
||||
|
||||
for (let thread = 0; thread < warp.threads; thread++) {
|
||||
const shaft = warp.threading[thread].shaft;
|
||||
const marker = this.getMarker(baseSize);
|
||||
ctx.drawImage(marker, canvas.width - ((thread + 1) * this.props.baseSize), canvas.height - (shaft * this.props.baseSize));
|
||||
const colourSquare = this.getSquare(baseSize, warp.threading[thread].colour || warp.defaultColour);
|
||||
ctx2.drawImage(colourSquare, canvas.width - ((thread + 1) * this.props.baseSize), 0);
|
||||
}
|
||||
const marker = getMarker(baseSize);
|
||||
ctx.drawImage(marker, canvas.width - ((thread + 1) * baseSize), canvas.height - (shaft * baseSize));
|
||||
const colourSquare = getSquare(baseSize, warp.threading[thread].colour || warp.defaultColour);
|
||||
ctx2.drawImage(colourSquare, canvas.width - ((thread + 1) * baseSize), 0);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
render() {
|
||||
const { warp, weft, baseSize } = this.props;
|
||||
return (
|
||||
<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={{
|
||||
position: 'absolute', top: 0, right: 0, height: 10, width: warp.threading.length * baseSize,
|
||||
}}
|
||||
onClick={this.mouseClickColourway}
|
||||
onMouseDown={this.mouseDownColourway}
|
||||
onMouseMove={this.mouseMoveColourway}
|
||||
onMouseUp={this.mouseUpColourway}
|
||||
onMouseLeave={this.mouseUpColourway}
|
||||
onClick={mouseClickColourway}
|
||||
onMouseDown={mouseDownColourway}
|
||||
onMouseMove={mouseMoveColourway}
|
||||
onMouseUp={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={{
|
||||
position: 'absolute', top: 10, right: 0,
|
||||
height: warp.shafts * baseSize,
|
||||
width: warp.threading.length * baseSize, borderRadius: 4,
|
||||
boxShadow: '0px 0px 10px rgba(0,0,0,0.15)',
|
||||
}}
|
||||
onClick={this.click}
|
||||
onMouseDown={this.mouseDown}
|
||||
onMouseMove={this.mouseMove}
|
||||
onMouseUp={this.mouseUp}
|
||||
onMouseLeave={this.mouseUp}
|
||||
onClick={click}
|
||||
onMouseDown={mouseDown}
|
||||
onMouseMove={mouseMove}
|
||||
onMouseUp={mouseUp}
|
||||
onMouseLeave={mouseUp}
|
||||
/>
|
||||
</StyledWarp>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => state.objects.editor;
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
});
|
||||
const WarpContainer = connect(
|
||||
mapStateToProps, mapDispatchToProps,
|
||||
)(Warp);
|
||||
|
||||
export default WarpContainer;
|
||||
export default Warp;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { connect } from 'react-redux';
|
||||
import { useSelector } from 'react-redux';
|
||||
import utils from 'utils/utils.js';
|
||||
|
||||
const StyledWeft = styled.div`
|
||||
@ -18,172 +18,156 @@ const StyledWeft = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
class Weft extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.squares = {};
|
||||
this.markers = {};
|
||||
}
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
this.paintDrawdown();
|
||||
}
|
||||
componentDidMount() {
|
||||
this.paintDrawdown();
|
||||
}
|
||||
// Cache
|
||||
const squares = {};
|
||||
const markers = {};
|
||||
|
||||
toggleWeft = (treadle, threadCount) => {
|
||||
const weft = Object.assign({}, this.props.weft);
|
||||
const thread = weft.treadling[threadCount];
|
||||
thread.treadle = thread.treadle === treadle ? 0 : treadle;
|
||||
this.props.updatePattern({ weft });
|
||||
}
|
||||
function Weft({ cellStyle, warp, weft, baseSize, updatePattern }) {
|
||||
const [draggingColourway, setDraggingColourway] = useState(false);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [startTreadle, setStartTreadle] = useState();
|
||||
const [startThread, setStartThread] = useState();
|
||||
|
||||
changeWeftColour = (threadIndex) => {
|
||||
const weft = Object.assign({}, this.props.weft);
|
||||
const colour = this.props.colour;
|
||||
if (colour) {
|
||||
weft.treadling[threadIndex].colour = colour;
|
||||
this.props.updatePattern({ weft });
|
||||
}
|
||||
}
|
||||
const { editor } = useSelector(state => ({ editor: state.objects.editor }));
|
||||
useEffect(() => paintDrawdown());
|
||||
const weftRef = useRef(null);
|
||||
const colourwayRef = useRef(null);
|
||||
|
||||
getThreadTreadle = (event) => {
|
||||
const getThreadTreadle = (event) => {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const y = event.clientY - rect.top;
|
||||
const x = (event.clientX - rect.left);
|
||||
const thread = parseInt(y / this.props.baseSize) + 1;
|
||||
const treadle = parseInt(x / this.props.baseSize);
|
||||
const thread = parseInt(y / baseSize) + 1;
|
||||
const treadle = parseInt(x / baseSize);
|
||||
return { treadle, thread };
|
||||
}
|
||||
};
|
||||
|
||||
mouseClickColourway = event => {
|
||||
const weft = Object.assign({}, this.props.weft);
|
||||
const { thread } = this.getThreadTreadle(event);
|
||||
if (thread >= weft.treadling.length) this.fillUpTo(weft, thread);
|
||||
weft.treadling[thread - 1].colour = this.props.colour;
|
||||
this.props.updatePattern({ weft });
|
||||
}
|
||||
mouseDownColourway = event => {
|
||||
const mouseClickColourway = event => {
|
||||
const newWeft = Object.assign({}, weft);
|
||||
const { thread } = getThreadTreadle(event);
|
||||
if (thread >= weft.treadling.length) fillUpTo(newWeft, thread);
|
||||
newWeft.treadling[thread - 1].colour = editor.colour;
|
||||
updatePattern({ weft: newWeft });
|
||||
};
|
||||
const mouseDownColourway = event => {
|
||||
event.preventDefault();
|
||||
this.draggingColourway = true;
|
||||
}
|
||||
mouseUpColourway = event => this.draggingColourway = false;
|
||||
mouseMoveColourway = (event) => {
|
||||
if (this.draggingColourway) {
|
||||
const weft = Object.assign({}, this.props.weft);
|
||||
const { thread } = this.getThreadTreadle(event);
|
||||
if (thread >= weft.treadling.length) this.fillUpTo(weft, thread);
|
||||
weft.treadling[thread - 1].colour = this.props.colour;
|
||||
this.props.updatePattern({ weft });
|
||||
}
|
||||
setDraggingColourway(true);
|
||||
};
|
||||
const mouseUpColourway = event => setDraggingColourway(false);
|
||||
const mouseMoveColourway = (event) => {
|
||||
if (draggingColourway) {
|
||||
const newWeft = Object.assign({}, weft);
|
||||
const { thread } = getThreadTreadle(event);
|
||||
if (thread >= weft.treadling.length) fillUpTo(newWeft, thread);
|
||||
newWeft.treadling[thread - 1].colour = editor.colour;
|
||||
updatePattern({ weft: newWeft });
|
||||
}
|
||||
};
|
||||
|
||||
mouseUp = event => this.dragging = false;
|
||||
mouseDown = (event) => {
|
||||
const mouseUp = event => setDragging(false);
|
||||
const mouseDown = (event) => {
|
||||
event.preventDefault();
|
||||
const { treadle, thread } = this.getThreadTreadle(event);
|
||||
this.startTreadle = treadle;
|
||||
this.startThread = thread;
|
||||
this.dragging = true;
|
||||
}
|
||||
mouseMove = (event) => {
|
||||
if (this.dragging && this.props.tool) {
|
||||
const weft = Object.assign({}, this.props.weft);
|
||||
const { treadle, thread } = this.getThreadTreadle(event);
|
||||
const { treadle, thread } = getThreadTreadle(event);
|
||||
setStartTreadle(treadle);
|
||||
setStartThread(thread);
|
||||
setDragging(true);
|
||||
};
|
||||
const mouseMove = (event) => {
|
||||
if (dragging && editor.tool) {
|
||||
const newWeft = Object.assign({}, weft);
|
||||
const { treadle, thread } = getThreadTreadle(event);
|
||||
|
||||
let lX = this.startTreadle; let hX = treadle; let lY = this.startThread; let
|
||||
hY = thread;
|
||||
let lX = startTreadle; let hX = treadle; let lY = startThread; let hY = thread;
|
||||
let xDirection = 1; let
|
||||
yDirection = 1;
|
||||
if (treadle < this.startTreadle) {
|
||||
if (treadle < startTreadle) {
|
||||
lX = treadle;
|
||||
hX = this.startTreadle;
|
||||
hX = startTreadle;
|
||||
xDirection = -1;
|
||||
}
|
||||
if (thread < this.startThread) {
|
||||
if (thread < startThread) {
|
||||
lY = thread;
|
||||
hY = this.startThread;
|
||||
hY = startThread;
|
||||
yDirection = -1;
|
||||
}
|
||||
let x = xDirection > 0 ? lX : hX;
|
||||
let y = yDirection > 0 ? lY : hY;
|
||||
if (this.props.tool === 'colour') {
|
||||
if ((thread - 1) >= weft.treadling.length) this.fillUpTo(weft, (thread - 1));
|
||||
weft.treadling[thread - 1].colour = this.props.colour;
|
||||
if (editor.tool === 'colour') {
|
||||
if ((thread - 1) >= weft.treadling.length) fillUpTo(newWeft, (thread - 1));
|
||||
newWeft.treadling[thread - 1].colour = editor.colour;
|
||||
}
|
||||
if (this.props.tool === 'straight') {
|
||||
if (editor.tool === 'straight') {
|
||||
while (y <= hY && y >= lY) {
|
||||
if ((y - 1) >= weft.treadling.length || weft.treadling.length - y - 1 < 5) this.fillUpTo(weft, (y + 5));
|
||||
weft.treadling[y - 1].treadle = x + 1;
|
||||
if ((y - 1) >= weft.treadling.length || weft.treadling.length - y - 1 < 5) fillUpTo(newWeft, (y + 5));
|
||||
newWeft.treadling[y - 1].treadle = x + 1;
|
||||
x += xDirection;
|
||||
y += yDirection;
|
||||
if (x > hX || x < lX) x = xDirection > 0 ? lX : hX;
|
||||
}
|
||||
}
|
||||
if (this.props.tool === 'point') {
|
||||
if (editor.tool === 'point') {
|
||||
while (y <= hY && y >= lY) {
|
||||
if ((y - 1) >= weft.treadling.length || weft.treadling.length - y -1 < 5) this.fillUpTo(weft, y + 5);
|
||||
weft.treadling[y - 1].treadle = x + 1;
|
||||
if ((y - 1) >= weft.treadling.length || weft.treadling.length - y -1 < 5) fillUpTo(newWeft, y + 5);
|
||||
newWeft.treadling[y - 1].treadle = x + 1;
|
||||
x += xDirection;
|
||||
y += yDirection;
|
||||
if (x > hX || x <= lX) xDirection = 0 - xDirection;
|
||||
}
|
||||
}
|
||||
this.props.updatePattern({ weft });
|
||||
updatePattern({ weft: newWeft });
|
||||
}
|
||||
}
|
||||
click = (event) => {
|
||||
if (this.props.tool === 'point' || this.props.tool === 'straight') {
|
||||
let { thread, treadle } = this.getThreadTreadle(event);
|
||||
};
|
||||
const click = (event) => {
|
||||
if (editor.tool === 'point' || editor.tool === 'straight') {
|
||||
let { thread, treadle } = getThreadTreadle(event);
|
||||
treadle += 1;
|
||||
const weft = Object.assign({}, this.props.weft);
|
||||
if (thread >= weft.treadling.length || weft.treadling.length - thread < 5) this.fillUpTo(weft, thread + 5);
|
||||
const weftThread = weft.treadling[thread - 1];
|
||||
const newWeft = Object.assign({}, weft);
|
||||
if (thread >= newWeft.treadling.length || newWeft.treadling.length - thread < 5) fillUpTo(newWeft, thread + 5);
|
||||
const weftThread = newWeft.treadling[thread - 1];
|
||||
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;
|
||||
while (i <= limit) {
|
||||
weft.treadling.push({ treadle: 0 });
|
||||
weft.threads++;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getMarker(size) {
|
||||
if (this.markers[size]) return this.markers[size];
|
||||
const getMarker = (size) => {
|
||||
if (markers[size]) return markers[size];
|
||||
const m_canvas = document.createElement('canvas');
|
||||
m_canvas.width = this.props.baseSize;
|
||||
m_canvas.height = this.props.baseSize;
|
||||
m_canvas.width = baseSize;
|
||||
m_canvas.height = baseSize;
|
||||
const mc = m_canvas.getContext('2d');
|
||||
mc.fillStyle = 'black';
|
||||
mc.fillRect(0, 0, this.props.baseSize, this.props.baseSize);
|
||||
this.markers[size] = m_canvas;
|
||||
mc.fillRect(0, 0, baseSize, baseSize);
|
||||
markers[size] = m_canvas;
|
||||
return m_canvas;
|
||||
}
|
||||
};
|
||||
|
||||
getSquare(size, colour) {
|
||||
if (this.squares[size] && this.squares[size][colour]) return this.squares[size][colour];
|
||||
const getSquare = (size, colour) => {
|
||||
if (squares[size] && squares[size][colour]) return squares[size][colour];
|
||||
const m_canvas = document.createElement('canvas');
|
||||
m_canvas.width = 10;
|
||||
m_canvas.height = this.props.baseSize;
|
||||
m_canvas.height = baseSize;
|
||||
const mc = m_canvas.getContext('2d');
|
||||
mc.fillStyle = utils.rgb(colour);
|
||||
mc.fillRect(0, 0, 10, this.props.baseSize);
|
||||
if (!this.squares[size]) this.squares[size] = {};
|
||||
this.squares[size][colour] = m_canvas;
|
||||
mc.fillRect(0, 0, 10, baseSize);
|
||||
if (!squares[size]) squares[size] = {};
|
||||
squares[size][colour] = m_canvas;
|
||||
return m_canvas;
|
||||
}
|
||||
};
|
||||
|
||||
paintDrawdown() {
|
||||
const canvas = this.refs.weft;
|
||||
const colourway = this.refs.colourway;
|
||||
const paintDrawdown = () => {
|
||||
const canvas = weftRef.current;
|
||||
const colourway = colourwayRef.current;
|
||||
const ctx = canvas.getContext('2d');// , { alpha: false });
|
||||
const ctx2 = colourway.getContext('2d');// , { alpha: false });
|
||||
const { baseSize, weft } = this.props;
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
@ -200,49 +184,37 @@ class Weft extends Component {
|
||||
|
||||
for (let thread = 0; thread < weft.threads; thread++) {
|
||||
const treadle = weft.treadling[thread].treadle;
|
||||
const marker = this.getMarker(baseSize);
|
||||
ctx.drawImage(marker, ((treadle - 1) * this.props.baseSize), ((thread) * this.props.baseSize));
|
||||
const colourSquare = this.getSquare(baseSize, weft.treadling[thread].colour || weft.defaultColour);
|
||||
ctx2.drawImage(colourSquare, 0, (thread * this.props.baseSize));
|
||||
const marker = getMarker(baseSize);
|
||||
ctx.drawImage(marker, ((treadle - 1) * baseSize), ((thread) * baseSize));
|
||||
const colourSquare = getSquare(baseSize, weft.treadling[thread].colour || weft.defaultColour);
|
||||
ctx2.drawImage(colourSquare, 0, (thread * baseSize));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
render() {
|
||||
const { warp, weft, baseSize } = this.props;
|
||||
return (
|
||||
<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}}
|
||||
onClick={this.mouseClickColourway}
|
||||
onMouseDown={this.mouseDownColourway}
|
||||
onMouseMove={this.mouseMoveColourway}
|
||||
onMouseUp={this.mouseUpColourway}
|
||||
onMouseLeave={this.mouseUpColourway}
|
||||
onClick={mouseClickColourway}
|
||||
onMouseDown={mouseDownColourway}
|
||||
onMouseMove={mouseMoveColourway}
|
||||
onMouseUp={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={{
|
||||
position: 'absolute',
|
||||
top: 0, right: 10, height: weft.threads * baseSize, width: weft.treadles * baseSize,
|
||||
borderRadius: 4, boxShadow: '0px 0px 10px rgba(0,0,0,0.15)',
|
||||
}}
|
||||
onClick={this.click}
|
||||
onMouseDown={this.mouseDown}
|
||||
onMouseMove={this.mouseMove}
|
||||
onMouseUp={this.mouseUp}
|
||||
onMouseLeave={this.mouseUp}
|
||||
onClick={click}
|
||||
onMouseDown={mouseDown}
|
||||
onMouseMove={mouseMove}
|
||||
onMouseUp={mouseUp}
|
||||
onMouseLeave={mouseUp}
|
||||
/>
|
||||
</StyledWeft>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => state.objects.editor;
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
});
|
||||
const WeftContainer = connect(
|
||||
mapStateToProps, mapDispatchToProps,
|
||||
)(Weft);
|
||||
|
||||
export default WeftContainer;
|
||||
export default Weft;
|
||||
|
@ -1,13 +1,18 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Table, Container } from 'semantic-ui-react';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useSelector } from 'react-redux';
|
||||
import moment from 'moment';
|
||||
import api from 'api';
|
||||
|
||||
function Root({ user }) {
|
||||
function Root() {
|
||||
const [users, setUsers] = useState([]);
|
||||
|
||||
const { user } = useSelector(state => {
|
||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
||||
return { user };
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!(user?.roles?.indexOf('root') > -1)) return;
|
||||
api.root.getUsers(({ users}) => {
|
||||
@ -60,15 +65,4 @@ function Root({ user }) {
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
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;
|
||||
export default Root;
|
||||
|
@ -1,22 +1,29 @@
|
||||
import React, { useState } from '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 { connect } from 'react-redux';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
|
||||
function AccountSettings({ user, onLogout, history, onReceiveUser }) {
|
||||
function AccountSettings() {
|
||||
const [newEmail, setNewEmail] = useState('');
|
||||
const [existingPassword, setExistingPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = 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 = () => {
|
||||
api.accounts.updateEmail(newEmail, data => {
|
||||
setNewEmail('');
|
||||
onReceiveUser(Object.assign({}, user, { email: data.email }));
|
||||
dispatch(actions.users.receive(Object.assign({}, user, { email: data.email })));
|
||||
}, err => toast.error(err.message));
|
||||
}
|
||||
|
||||
@ -32,8 +39,8 @@ function AccountSettings({ user, onLogout, history, onReceiveUser }) {
|
||||
const confirm = window.confirm('Really delete your account?');
|
||||
if (!confirm) return;
|
||||
api.accounts.delete(deletePassword, () => {
|
||||
api.auth.logout(onLogout);
|
||||
history.push('/');
|
||||
api.auth.logout(() => dispatch(actions.auth.logout()));
|
||||
navigate('/');
|
||||
toast.info('Sorry to see you go');
|
||||
}, err => toast.error(err.message));
|
||||
}
|
||||
@ -81,16 +88,4 @@ function AccountSettings({ user, onLogout, history, onReceiveUser }) {
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
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;
|
||||
export default AccountSettings;
|
||||
|
@ -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;
|
@ -1,27 +1,27 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Message, Input, Segment, Button } from 'semantic-ui-react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import { connect } from 'react-redux';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
|
||||
class IdentitySettings extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { newUsername: '' };
|
||||
}
|
||||
function IdentitySettings() {
|
||||
const [newUsername, setNewUsername] = useState('');
|
||||
const dispatch = useDispatch();
|
||||
const { user } = useSelector(state => {
|
||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
||||
return { user };
|
||||
});
|
||||
|
||||
updateUsername = () => {
|
||||
api.users.update(this.props.user.username, { username: this.state.newUsername }, (user) => {
|
||||
this.props.onUpdateUsername(this.props.user._id, this.state.newUsername);
|
||||
const updateUsername = () => {
|
||||
api.users.update(user.username, { username: newUsername }, (user) => {
|
||||
dispatch(actions.users.updateUsername(user._id, newUsername));
|
||||
toast.info('Username updated');
|
||||
this.setState({ newUsername: '' });
|
||||
setNewUsername('');
|
||||
}, err => toast.error(err.message));
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Segment raised color="blue">
|
||||
<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>
|
||||
</Message>
|
||||
<Input
|
||||
type="text" value={this.state.newUsername}
|
||||
onChange={e => this.setState({ newUsername: e.target.value })}
|
||||
action=<Button color="yellow" content="Set new username" onClick={this.updateUsername} />
|
||||
type="text" value={newUsername}
|
||||
onChange={e => setNewUsername(e.target.value)}
|
||||
action=<Button color="yellow" content="Set new username" onClick={updateUsername} />
|
||||
/>
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
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;
|
||||
export default IdentitySettings;
|
||||
|
@ -1,9 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { Label, Table, Checkbox, Divider, Segment } from 'semantic-ui-react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
import utils from 'utils/utils.js';
|
||||
@ -16,19 +14,22 @@ const subs = [
|
||||
{ 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);
|
||||
toggleEmailSub = (key, enable) => {
|
||||
const { user, onSubsUpdated } = this.props;
|
||||
const hasEmailSub = (key) => utils.hasSubscription(user, key);
|
||||
const toggleEmailSub = (key, 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
|
||||
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 (
|
||||
<Segment raised color="blue">
|
||||
<h3>Email preferences</h3>
|
||||
@ -52,32 +53,19 @@ class NotificationSettings extends Component {
|
||||
{subs.map(s =>
|
||||
<Table.Row key={s.key}>
|
||||
<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>
|
||||
)}
|
||||
{groups.map(g =>
|
||||
<Table.Row key={g._id}>
|
||||
<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.Body>
|
||||
</Table>
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
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;
|
||||
export default NotificationSettings;
|
||||
|
@ -1,16 +1,8 @@
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Divider, Container, Grid, Menu,
|
||||
} from 'semantic-ui-react';
|
||||
import { Link, Switch, Route } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import { Divider, Container, Grid, Menu } from 'semantic-ui-react';
|
||||
import { Outlet, NavLink } from 'react-router-dom';
|
||||
|
||||
import Identity from './Identity';
|
||||
import Notification from './Notification';
|
||||
import Billing from './Billing';
|
||||
import Account from './Account';
|
||||
|
||||
class Settings extends Component {
|
||||
render() {
|
||||
function Settings() {
|
||||
return (
|
||||
<Container style={{ marginTop: '40px' }}>
|
||||
<h2>Manage your account</h2>
|
||||
@ -18,26 +10,18 @@ class Settings extends Component {
|
||||
<Grid stackable>
|
||||
<Grid.Column computer={4}>
|
||||
<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={Link} to='/settings/notifications' content='Notifications' icon='envelope' active={this.props.location.pathname === '/settings/notifications'} />
|
||||
{/*<Menu.Item as={Link} to="/settings/billing" content="Plan & billing" icon="credit card" active={this.props.location.pathname === '/settings/billing'} />*/}
|
||||
<Menu.Item as={Link} to="/settings/account" name="Account" active={this.props.location.pathname === '/settings/account'} icon="key" />
|
||||
<Menu.Item as={NavLink} to="/settings/identity" name="identity" icon="user secret" />
|
||||
<Menu.Item as={NavLink} to='/settings/notifications' content='Notifications' icon='envelope' />
|
||||
<Menu.Item as={NavLink} to="/settings/account" name="Account" icon="key" />
|
||||
</Menu>
|
||||
</Grid.Column>
|
||||
|
||||
<Grid.Column stretched width={12}>
|
||||
<Switch>
|
||||
<Route path="/settings/notifications" component={Notification} />
|
||||
<Route path="/settings/account" component={Account} />
|
||||
<Route path="/settings/billing" component={Billing} />
|
||||
<Route path="/settings" component={Identity} />
|
||||
</Switch>
|
||||
|
||||
<Outlet />
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
|
@ -1,39 +1,45 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
Icon, Segment, Form, Button, Divider,
|
||||
} from 'semantic-ui-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import utils from 'utils/utils';
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
|
||||
import FileChooser from 'components/includes/FileChooser';
|
||||
|
||||
class EditProfile extends Component {
|
||||
updatePicture = (avatar) => {
|
||||
api.users.update(this.props.profileUser.username, { avatar }, this.props.onReceiveUser);
|
||||
}
|
||||
function EditProfile() {
|
||||
const { username } = useParams();
|
||||
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 { bio, location, website } = this.props.profileUser;
|
||||
api.users.update(this.props.profileUser.username, { bio, location, website }, (user) => {
|
||||
this.props.onReceiveUser(user);
|
||||
const updatePicture = (avatar) => {
|
||||
api.users.update(profileUser.username, { avatar }, u => dispatch(actions.users.receive(u)));
|
||||
};
|
||||
|
||||
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');
|
||||
}, err => toast.error(err.message));
|
||||
}
|
||||
};
|
||||
|
||||
updateSocial = () => {
|
||||
const { twitter, facebook, linkedIn, instagram } = this.props.profileUser;
|
||||
api.users.update(this.props.profileUser.username, { twitter, facebook, linkedIn, instagram }, (user) => {
|
||||
this.props.onReceiveUser(user);
|
||||
const updateSocial = () => {
|
||||
const { twitter, facebook, linkedIn, instagram } = profileUser;
|
||||
api.users.update(profileUser.username, { twitter, facebook, linkedIn, instagram }, (u) => {
|
||||
dispatch(actions.users.receive(u));
|
||||
toast.info('Profile saved');
|
||||
}, err => toast.error(err.message));
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { profileUser, onUserEdited } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<Link to={`/${profileUser.username}`} className="ui basic button">
|
||||
@ -47,9 +53,9 @@ class EditProfile extends Component {
|
||||
|
||||
<h5>Profile picture</h5>
|
||||
<FileChooser
|
||||
forType="user" for={profileUser}
|
||||
forType="user" forObject={profileUser}
|
||||
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>
|
||||
{utils.defaultAvatars().map(a => (
|
||||
@ -58,7 +64,7 @@ class EditProfile extends Component {
|
||||
style={{
|
||||
width: 40, height: 40, margin: 4, cursor: 'pointer',
|
||||
}}
|
||||
onClick={e => this.updatePicture(a.key)}
|
||||
onClick={e => updatePicture(a.key)}
|
||||
/>
|
||||
))}
|
||||
<Divider hidden />
|
||||
@ -67,19 +73,19 @@ class EditProfile extends Component {
|
||||
<Form.TextArea
|
||||
label="Bio" placeholder="Write a bit about yourself..."
|
||||
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
|
||||
label="Location" placeholder="Where in the world are you?" icon="map pin" iconPosition="left"
|
||||
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
|
||||
label="Website or URL" placeholder="https://yourwebsite.com" icon="globe" iconPosition="left"
|
||||
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>
|
||||
</Segment>
|
||||
|
||||
@ -90,44 +96,28 @@ class EditProfile extends Component {
|
||||
<Form.Input
|
||||
label="Twitter" placeholder="@username" icon="twitter" iconPosition="left"
|
||||
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
|
||||
label="Facebook" placeholder="username" icon="facebook" iconPosition="left"
|
||||
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
|
||||
label="Instagram" placeholder="username" icon="instagram" iconPosition="left"
|
||||
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
|
||||
label="LinkedIn" placeholder="username" icon="linkedin" iconPosition="left"
|
||||
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>
|
||||
</Segment>
|
||||
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
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;
|
||||
export default EditProfile;
|
||||
|
@ -1,48 +1,44 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Loader, Icon, List, Container, Card, Grid, Message } from 'semantic-ui-react';
|
||||
import { Link, Switch, Route } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link, Outlet, useParams } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import moment from 'moment';
|
||||
import utils from 'utils/utils';
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
|
||||
import EditProfile from './EditProfile';
|
||||
import ProfileProjects from './ProfileProjects';
|
||||
import BlurrableImage from 'components/includes/BlurrableImage';
|
||||
|
||||
class Profile extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { loading: false };
|
||||
}
|
||||
componentDidMount() {
|
||||
this.setState({ loading: true });
|
||||
api.users.get(this.props.match.params.username, user => {
|
||||
this.props.onReceiveUser(user);
|
||||
this.setState({ loading: false });
|
||||
}, err => {
|
||||
this.props.onRequestFailed(err);
|
||||
this.setState({ loading: false });
|
||||
function Profile() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const { username } = useParams();
|
||||
|
||||
const { user, profileUser, errorMessage } = useSelector(state => {
|
||||
const users = state.users.users;
|
||||
const profileUser = users.filter(u => username === u.username)[0];
|
||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
||||
return { user, profileUser, errorMessage: state.users.errorMessage };
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.match.params.username !== prevProps.match.params.username) {
|
||||
api.users.get(this.props.match.params.username, this.props.onReceiveUser);
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
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 (
|
||||
<Container style={{ marginTop: '40px' }}>
|
||||
<Helmet title={profileUser?.username ? `${profileUser.username}'s Profile` : 'Profile'} />
|
||||
{loading && !profileUser &&
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<h4>Loading {this.props.match.params.username}'s profile...</h4>
|
||||
<h4>Loading {username}'s profile...</h4>
|
||||
<Loader active inline="centered" />
|
||||
</div>
|
||||
}
|
||||
@ -166,33 +162,13 @@ class Profile extends Component {
|
||||
</Grid.Column>
|
||||
|
||||
<Grid.Column computer={11}>
|
||||
<Switch>
|
||||
<Route path="/:username/edit" component={EditProfile} />
|
||||
<Route component={ProfileProjects} />
|
||||
</Switch>
|
||||
<Outlet />
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
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;
|
||||
export default Profile;
|
||||
|
@ -1,12 +1,18 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from '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';
|
||||
|
||||
class ProfileProjects extends Component {
|
||||
render() {
|
||||
const { profileUser } = this.props;
|
||||
function ProfileProjects() {
|
||||
const { username } = useParams();
|
||||
const { profileUser } = useSelector(state => {
|
||||
const users = state.users.users;
|
||||
const profileUser = users.filter(u => u.username === username)[0];
|
||||
return { profileUser };
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3><Icon name='book' /> {profileUser.username}'s projects</h3>
|
||||
@ -26,17 +32,6 @@ class ProfileProjects extends Component {
|
||||
)
|
||||
}
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
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;
|
||||
export default ProfileProjects;
|
||||
|
@ -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;
|
@ -1,9 +1,8 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Container } from 'semantic-ui-react';
|
||||
|
||||
class PrivacyPolicy extends Component {
|
||||
render() {
|
||||
function PrivacyPolicy() {
|
||||
return (
|
||||
<Container style={{ marginTop: 50, marginBottom: 50 }}>
|
||||
<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>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PrivacyPolicy;
|
||||
|
@ -1,9 +1,8 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Container } from 'semantic-ui-react';
|
||||
|
||||
class TermsOfUse extends Component {
|
||||
render() {
|
||||
function TermsOfUse() {
|
||||
return (
|
||||
<Container style={{ marginTop: 50, marginBottom: 50 }}>
|
||||
<Helmet title='Terms of Use' />
|
||||
@ -38,7 +37,6 @@ class TermsOfUse extends Component {
|
||||
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TermsOfUse;
|
||||
|
@ -1,19 +1,17 @@
|
||||
import 'react-app-polyfill/ie9';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Switch, Route, Router } from 'react-router-dom';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { createStore } from 'redux';
|
||||
import { Provider } from 'react-redux';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { BrowserTracing } from '@sentry/tracing';
|
||||
import createBrowserHistory from 'history/createBrowserHistory';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import 'pell/dist/pell.min.css';
|
||||
|
||||
import './index.css';
|
||||
import reducers from './reducers';
|
||||
import App from './components/App.js';
|
||||
import DraftExport from 'components/main/projects/objects/DraftExport';
|
||||
import * as serviceWorker from './registerServiceWorker';
|
||||
|
||||
export const store = createStore(reducers);
|
||||
@ -28,12 +26,9 @@ if (process.env.REACT_APP_SENTRY_DSN) {
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<Router history={createBrowserHistory()}>
|
||||
<Switch>
|
||||
<Route path="/objects/:id/export" component={DraftExport} />
|
||||
<Route component={App} />
|
||||
</Switch>
|
||||
</Router>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</Provider>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
|
689
web/yarn.lock
689
web/yarn.lock
@ -1110,13 +1110,20 @@
|
||||
dependencies:
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
|
||||
integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
|
||||
dependencies:
|
||||
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":
|
||||
version "7.10.4"
|
||||
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"
|
||||
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":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
|
||||
@ -1642,6 +1656,21 @@
|
||||
"@types/minimatch" "*"
|
||||
"@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":
|
||||
version "2.0.3"
|
||||
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"
|
||||
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@*":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
|
||||
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@*":
|
||||
version "14.6.0"
|
||||
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"
|
||||
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":
|
||||
version "1.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
|
||||
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":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
|
||||
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@*":
|
||||
version "15.0.0"
|
||||
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"
|
||||
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:
|
||||
version "1.0.0"
|
||||
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"
|
||||
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:
|
||||
version "0.7.0"
|
||||
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:
|
||||
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:
|
||||
version "2.20.3"
|
||||
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:
|
||||
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:
|
||||
version "1.0.1"
|
||||
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:
|
||||
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:
|
||||
version "4.1.1"
|
||||
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"
|
||||
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:
|
||||
version "0.2.0"
|
||||
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"
|
||||
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:
|
||||
version "1.0.1"
|
||||
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"
|
||||
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:
|
||||
version "5.0.3"
|
||||
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"
|
||||
is-extendable "^1.0.1"
|
||||
|
||||
extend@~3.0.2:
|
||||
extend@^3.0.0, extend@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
|
||||
@ -5229,6 +5348,11 @@ hash.js@^1.0.0, hash.js@^1.0.3:
|
||||
inherits "^2.0.3"
|
||||
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:
|
||||
version "1.2.0"
|
||||
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"
|
||||
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
|
||||
|
||||
history@^4.7.2, history@^4.9.0:
|
||||
history@^4.7.2:
|
||||
version "4.10.1"
|
||||
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
|
||||
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
|
||||
@ -5251,6 +5375,13 @@ history@^4.7.2, history@^4.9.0:
|
||||
tiny-warning "^1.0.0"
|
||||
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:
|
||||
version "1.0.1"
|
||||
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-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"
|
||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||
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"
|
||||
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:
|
||||
version "7.0.4"
|
||||
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"
|
||||
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:
|
||||
version "1.2.0"
|
||||
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"
|
||||
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:
|
||||
version "2.0.4"
|
||||
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:
|
||||
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:
|
||||
version "1.0.0"
|
||||
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"
|
||||
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:
|
||||
version "3.0.0"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
@ -6896,6 +7042,54 @@ md5.js@^1.3.4:
|
||||
inherits "^2.0.1"
|
||||
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:
|
||||
version "2.0.4"
|
||||
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"
|
||||
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:
|
||||
version "0.3.0"
|
||||
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"
|
||||
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:
|
||||
version "3.1.10"
|
||||
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"
|
||||
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:
|
||||
version "0.9.0"
|
||||
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"
|
||||
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:
|
||||
version "2.0.0"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
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"
|
||||
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:
|
||||
version "2.0.0"
|
||||
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"
|
||||
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:
|
||||
version "2.0.6"
|
||||
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-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"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
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:
|
||||
version "2.4.0"
|
||||
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"
|
||||
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:
|
||||
version "1.3.7"
|
||||
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"
|
||||
integrity sha1-acLVdB5t9eCPIw82u8KUTuEiJVU=
|
||||
|
||||
react-redux@^7.2.0:
|
||||
version "7.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.1.tgz#8dedf784901014db2feca1ab633864dee68ad985"
|
||||
integrity sha512-T+VfD/bvgGTUA74iW9d2i5THrDQWbweXP0AVNI8tNd1Rk5ch1rnMiJkDD67ejw7YBKM4+REvcvqRuWJb7BLuEg==
|
||||
react-redux@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.1.tgz#2bc029f5ada9b443107914c373a2750f6bc0f40c"
|
||||
integrity sha512-LMZMsPY4DYdZfLJgd7i79n5Kps5N9XVLCJJeWAaPYTV+Eah2zTuBjTxKtNEbjiyitbq80/eIkm55CYSLqAub3w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
hoist-non-react-statics "^3.3.0"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^16.9.0"
|
||||
"@babel/runtime" "^7.12.1"
|
||||
"@types/hoist-non-react-statics" "^3.3.1"
|
||||
"@types/use-sync-external-store" "^0.0.3"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
react-is "^18.0.0"
|
||||
use-sync-external-store "^1.0.0"
|
||||
|
||||
react-router-dom@^5.1.2:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662"
|
||||
integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==
|
||||
react-router-dom@^6.3.0:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.3.0.tgz#a0216da813454e521905b5fa55e0e5176123f43d"
|
||||
integrity sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.1.2"
|
||||
history "^4.9.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"
|
||||
history "^5.2.0"
|
||||
react-router "6.3.0"
|
||||
|
||||
react-router@5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293"
|
||||
integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==
|
||||
react-router@6.3.0:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557"
|
||||
integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.1.2"
|
||||
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"
|
||||
history "^5.2.0"
|
||||
|
||||
react-scripts@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"
|
||||
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:
|
||||
version "1.1.0"
|
||||
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:
|
||||
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:
|
||||
version "5.1.2"
|
||||
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"
|
||||
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:
|
||||
version "3.1.1"
|
||||
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"
|
||||
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:
|
||||
version "5.1.1"
|
||||
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"
|
||||
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
|
||||
|
||||
tiny-warning@^1.0.0, tiny-warning@^1.0.3:
|
||||
tiny-warning@^1.0.0:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
||||
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
||||
@ -10691,6 +11140,11 @@ tree-changes@^0.9.0:
|
||||
"@gilbarbara/deep-equal" "^0.1.0"
|
||||
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:
|
||||
version "1.1.6"
|
||||
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"
|
||||
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:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
|
||||
@ -10837,6 +11304,71 @@ unique-slug@^2.0.0:
|
||||
dependencies:
|
||||
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:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||
@ -10902,6 +11434,11 @@ url@^0.11.0:
|
||||
punycode "1.3.2"
|
||||
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:
|
||||
version "3.1.1"
|
||||
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"
|
||||
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:
|
||||
version "2.1.1"
|
||||
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"
|
||||
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:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
|
||||
|
Loading…
Reference in New Issue
Block a user