Compare commits
No commits in common. "adc4f163b7651141f015f24d11d5c523c2dfa9b5" and "919382f4e7d13bd2ce63f75b64aa5edfd819d8fb" have entirely different histories.
adc4f163b7
...
919382f4e7
@ -1,5 +1,4 @@
|
||||
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,9 +18,8 @@
|
||||
"react-dom": "^16.13.1",
|
||||
"react-helmet": "^6.0.0",
|
||||
"react-joyride": "^2.4.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-redux": "^8.0.1",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-redux": "^7.2.0",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "3.4.1",
|
||||
"react-toastify": "^4.4.0",
|
||||
"redux": "^4.0.0",
|
||||
|
@ -1,79 +1,55 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Routes, Route, Link } from 'react-router-dom';
|
||||
import { Switch, Route, Link, withRouter } from 'react-router-dom';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { connect } 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 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() {
|
||||
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 };
|
||||
});
|
||||
|
||||
|
||||
function App({ user, groups, syncedToDrift, driftReady, onOpenRegister, onCloseAuthentication, isAuthenticating, isAuthenticatingType, isAuthenticated, onLoginSuccess, onLogout, onReceiveProjects, onReceiveInvitations, onReceiveGroups, onDriftReady, onDriftSynced, helpModalOpen, openHelpModal, searchTerm, updateSearchTerm, searchPopupOpen, openSearchPopup, searchResults, updateSearchResults, searching, updateSearching, history }) {
|
||||
const loggedInUserId = user?._id;
|
||||
|
||||
useEffect(() => {
|
||||
api.auth.autoLogin(token => dispatch(actions.auth.receiveLogin(token)));
|
||||
}, [dispatch]);
|
||||
api.auth.autoLogin(onLoginSuccess);
|
||||
}, [onLoginSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loggedInUserId) return;
|
||||
api.users.getMyProjects(p => dispatch(actions.projects.receiveProjects(p)));
|
||||
api.groups.getMine(g => dispatch(actions.groups.receiveGroups(g)));
|
||||
api.users.getMyProjects(onReceiveProjects);
|
||||
api.groups.getMine(onReceiveGroups);
|
||||
api.invitations.get(({ invitations, sentInvitations}) => {
|
||||
dispatch(actions.invitations.receiveInvitations(invitations.concat(sentInvitations)));
|
||||
onReceiveInvitations(invitations.concat(sentInvitations));
|
||||
});
|
||||
}, [dispatch, loggedInUserId]);
|
||||
}, [loggedInUserId, onReceiveProjects, onReceiveGroups, onReceiveInvitations]);
|
||||
|
||||
useEffect(() => {
|
||||
window.drift && window.drift.on('ready', () => {
|
||||
dispatch(actions.users.initDrift());
|
||||
onDriftReady();
|
||||
});
|
||||
}, [dispatch]);
|
||||
}, [onDriftReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user && driftReady && !syncedToDrift && window.drift) {
|
||||
@ -82,52 +58,35 @@ function App() {
|
||||
username: user.username,
|
||||
createdAt: user.createdAt,
|
||||
});
|
||||
dispatch(actions.users.syncDrift(null));
|
||||
onDriftSynced();
|
||||
}
|
||||
}, [dispatch, user, driftReady, syncedToDrift]);
|
||||
}, [user, driftReady, syncedToDrift, onDriftSynced]);
|
||||
|
||||
return (
|
||||
<div style={{display: 'flex', flexDirection: 'column', minHeight: '100vh'}}>
|
||||
<Helmet defaultTitle={'Treadl'} titleTemplate={`%s | Treadl`} />
|
||||
<NavBar />
|
||||
<div style={{ flex: '1 0 0' }}>
|
||||
<Routes>
|
||||
<Route end path="/" element={isAuthenticated
|
||||
? <Home />
|
||||
: <MarketingHome onRegisterClicked={() => dispatch(actions.auth.openRegister())} />
|
||||
<Switch>
|
||||
<Route exact path="/" render={props => (isAuthenticated
|
||||
? <Home {...props} />
|
||||
: <MarketingHome {...props} onRegisterClicked={onOpenRegister} />)
|
||||
} />
|
||||
<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())} />
|
||||
<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} />
|
||||
<ToastContainer position={toast.POSITION.BOTTOM_CENTER} hideProgressBar/>
|
||||
<Divider hidden section />
|
||||
</div>
|
||||
@ -172,4 +131,34 @@ function App() {
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
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;
|
||||
|
@ -1,28 +1,31 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { Component } 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';
|
||||
|
||||
function ForgottenPassword() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
class ForgottenPassword extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { email: '', loading: false };
|
||||
}
|
||||
|
||||
const sendEmail = () => {
|
||||
setLoading(true);
|
||||
api.auth.sendPasswordResetEmail(email, () => {
|
||||
setLoading(false);
|
||||
sendEmail = () => {
|
||||
this.setState({ loading: true });
|
||||
api.auth.sendPasswordResetEmail(this.state.email, () => {
|
||||
this.setState({ loading: false });
|
||||
toast.info('If your account exists, a password email has been sent');
|
||||
navigate('/');
|
||||
this.props.history.push('/');
|
||||
}, (err) => {
|
||||
setLoading(false);
|
||||
this.setState({ loading: false });
|
||||
toast.error(err.message);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { email, loading } = this.state;
|
||||
return (
|
||||
<Card.Group centered style={{ marginTop: 50 }}>
|
||||
<Card raised color="yellow">
|
||||
@ -30,15 +33,24 @@ function ForgottenPassword() {
|
||||
<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 => setEmail(e.target.value)} placeholder="mary@example.com" autoFocus />
|
||||
<Input fluid type="email" value={email} onChange={e => this.setState({ email: e.target.value })} placeholder="mary@example.com" autoFocus />
|
||||
</Card.Content>
|
||||
<Card.Content extra textAlign="right">
|
||||
<Button basic onClick={() => navigate('/')} content="Cancel" />
|
||||
<Button color="teal" content="Send email" onClick={sendEmail} loading={loading} />
|
||||
<Button basic onClick={this.props.history.goBack} content="Cancel" />
|
||||
<Button color="teal" content="Send email" onClick={this.sendEmail} loading={loading} />
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</Card.Group>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ForgottenPassword;
|
||||
const mapStateToProps = state => ({ });
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
});
|
||||
const ForgottenPasswordContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(ForgottenPassword);
|
||||
|
||||
export default ForgottenPasswordContainer;
|
||||
|
@ -1,64 +1,65 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Message, Modal, Grid, Form, Input, Button,
|
||||
} from 'semantic-ui-react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
|
||||
import actions from 'actions';
|
||||
import { api } from 'api';
|
||||
|
||||
import ReadingImage from 'images/reading.png';
|
||||
|
||||
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);
|
||||
});
|
||||
class Login extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
registering: false, username: '', email: '', password: '', loading: false,
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
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();
|
||||
}, (err) => {
|
||||
dispatch(actions.auth.loginError(err));
|
||||
setLoading(false);
|
||||
this.props.onLoginFailure(err);
|
||||
this.setState({ loading: 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>
|
||||
{authType === 'register'
|
||||
{this.props.authType === 'register'
|
||||
&& (
|
||||
<Modal dimmer="inverted" open={open} onClose={onClose}>
|
||||
<Modal dimmer="inverted" open={this.props.open} onClose={this.props.onClose}>
|
||||
<Modal.Header>
|
||||
<span role="img" aria-label="wave">👋</span> Welcome!
|
||||
<Button floated="right" onClick={onClose} basic content="Close" />
|
||||
<Button floated="right" onClick={this.props.onClose} basic content="Close" />
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<Grid stackable>
|
||||
@ -100,25 +101,25 @@ function Login({ open, authType, onClose }) {
|
||||
</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>
|
||||
|
||||
{error && (<div className="ui warning message">{error.message}</div>)}
|
||||
<Form onSubmit={register}>
|
||||
{this.props.error && (<div className="ui warning message">{this.props.error.message}</div>)}
|
||||
<Form onSubmit={this.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={username} onChange={e => setUsername(e.target.value)} />
|
||||
<Input autoFocus size="large" fluid name="username" type="text" value={this.state.username} onChange={event => this.handleChange(event)} />
|
||||
</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={email} onChange={e => setEmail(e.target.value)} />
|
||||
<Input size="large" fluid name="email" type="email" value={this.state.email} onChange={event => this.handleChange(event)} />
|
||||
</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={password} onChange={e => setPassword(e.target.value)} />
|
||||
<Input size="large" fluid name="password" type="password" value={this.state.password} onChange={event => this.handleChange(event)} />
|
||||
</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>
|
||||
@ -132,12 +133,12 @@ function Login({ open, authType, onClose }) {
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
{authType === 'login'
|
||||
{this.props.authType === 'login'
|
||||
&& (
|
||||
<Modal dimmer="inverted" open={open} onClose={onClose}>
|
||||
<Modal dimmer="inverted" open={this.props.open} onClose={this.props.onClose}>
|
||||
<Modal.Header>
|
||||
<span role="img" aria-label="Peace">✌️ </span>
|
||||
Welcome back <Button floated="right" onClick={onClose} basic content="Close" />
|
||||
Welcome back <Button floated="right" onClick={this.props.onClose} basic content="Close" />
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<Grid stackable>
|
||||
@ -147,17 +148,17 @@ Welcome back <Button floated="right" onClick={onClose} basic content="Close" />
|
||||
</Grid.Column>
|
||||
|
||||
<Grid.Column computer={8}>
|
||||
{error && (<div className="ui warning message">{error.message}</div>)}
|
||||
<Form onSubmit={login}>
|
||||
{this.props.error && (<div className="ui warning message">{this.props.error.message}</div>)}
|
||||
<Form onSubmit={this.login}>
|
||||
<Form.Field>
|
||||
<label>Your email address or username</label>
|
||||
<Input autoFocus size="large" fluid name="email" type="text" value={email} onChange={e => setEmail(e.target.value)} placeholder='Email or username' />
|
||||
<Input autoFocus size="large" fluid name="email" type="text" value={this.state.email} onChange={event => this.handleChange(event)} placeholder='Email or username' />
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>Password
|
||||
<Link to="/password/forgotten" style={{ float: 'right' }} onClick={onClose}>Forgotten your password?</Link>
|
||||
<Link to="/password/forgotten" style={{ float: 'right' }} onClick={this.props.onClose}>Forgotten your password?</Link>
|
||||
</label>
|
||||
<Input size="large" fluid name="password" type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder='Password' />
|
||||
<Input size="large" fluid name="password" type="password" value={this.state.password} onChange={event => this.handleChange(event)} placeholder='Password' />
|
||||
</Form.Field>
|
||||
<div className="ui hidden divider" />
|
||||
<Form.Button type='submit' size="large" color="teal" fluid loading={loading}>Login</Form.Button>
|
||||
@ -170,6 +171,23 @@ Welcome back <Button floated="right" onClick={onClose} basic content="Close" />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Login;
|
||||
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;
|
||||
|
@ -1,30 +1,33 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { Component } 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';
|
||||
|
||||
function ResetPassword() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [password, setPassword] = useState('');
|
||||
const navigate = useNavigate();
|
||||
class ResetPassword extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { password: '', loading: false };
|
||||
}
|
||||
|
||||
const resetPassword = () => {
|
||||
resetPassword = () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const token = params.get('token');
|
||||
setLoading(true);
|
||||
api.auth.updatePasswordWithToken(token, password, () => {
|
||||
setLoading(false);
|
||||
this.setState({ loading: true });
|
||||
api.auth.updatePasswordWithToken(token, this.state.password, () => {
|
||||
this.setState({ loading: false });
|
||||
toast.info('Password changed successfully.');
|
||||
navigate('/');
|
||||
this.props.history.push('/');
|
||||
}, (err) => {
|
||||
setLoading(false);
|
||||
this.setState({ loading: false });
|
||||
toast.error(err.message);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { password, loading } = this.state;
|
||||
return (
|
||||
<Card.Group centered style={{ marginTop: 50 }}>
|
||||
<Card raised color="yellow">
|
||||
@ -32,14 +35,23 @@ function ResetPassword() {
|
||||
<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 => setPassword(e.target.value)} autoFocus />
|
||||
<Input fluid type="password" value={password} onChange={e => this.setState({ password: e.target.value })} autoFocus />
|
||||
</Card.Content>
|
||||
<Card.Content extra textAlign="right">
|
||||
<Button color="teal" content="Change password" onClick={resetPassword} loading={loading} />
|
||||
<Button color="teal" content="Change password" onClick={this.resetPassword} loading={loading} />
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</Card.Group>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ResetPassword;
|
||||
const mapStateToProps = state => ({ });
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
});
|
||||
const ResetPasswordContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(ResetPassword);
|
||||
|
||||
export default ResetPasswordContainer;
|
||||
|
@ -1,29 +0,0 @@
|
||||
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;
|
@ -1,6 +0,0 @@
|
||||
# Hello
|
||||
|
||||
This is some content
|
||||
|
||||
* and here
|
||||
* there
|
@ -1,72 +1,76 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { Button } from 'semantic-ui-react';
|
||||
import api from 'api';
|
||||
class FileChooser extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isUploading: false };
|
||||
}
|
||||
|
||||
function FileChooser({ onUploadStart, onUploadFinish, onError, onComplete, content, trigger, accept, forType, forObject }) {
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const inputRef = useRef();
|
||||
startUpload = () => {
|
||||
this.setState({ isUploading: true });
|
||||
this.props.onUploadStart && this.props.onUploadStart();
|
||||
}
|
||||
|
||||
const startUpload = () => {
|
||||
setIsUploading(true);
|
||||
onUploadStart && onUploadStart();
|
||||
};
|
||||
finishUpload = () => {
|
||||
this.setState({ isUploading: false });
|
||||
this.props.onUploadFinish && this.props.onUploadFinish();
|
||||
}
|
||||
|
||||
const finishUpload = () => {
|
||||
setIsUploading(false);
|
||||
onUploadFinish && onUploadFinish();
|
||||
};
|
||||
chooseFile = () => this.refs.fileInput.click()
|
||||
|
||||
const chooseFile = () => inputRef.current.click();
|
||||
|
||||
const handleFileChosen = (e) => {
|
||||
handleFileChosen = (e) => {
|
||||
const file = e.target.files && e.target.files[0];
|
||||
if (file) {
|
||||
startUpload();
|
||||
this.startUpload();
|
||||
const fileName = file.name.replace(/[^a-zA-Z0-9_.]/g, '_');
|
||||
if (forType === 'project' && fileName.toLowerCase().indexOf('.wif') > -1) {
|
||||
if (this.props.forType === 'project' && fileName.toLowerCase().indexOf('.wif') > -1) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e2) => {
|
||||
finishUpload();
|
||||
onComplete({ wif: e2.target.result, type: 'pattern' });
|
||||
this.finishUpload();
|
||||
this.props.onComplete({ wif: e2.target.result, type: 'pattern' });
|
||||
};
|
||||
reader.readAsText(file);
|
||||
} else {
|
||||
api.uploads.generateFileUploadRequest({
|
||||
forType: forType, forId: forObject._id, name: fileName, size: file.size, type: file.type,
|
||||
forType: this.props.forType, forId: this.props.for._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) {
|
||||
finishUpload();
|
||||
this.finishUpload();
|
||||
if (xhr.status === 200) {
|
||||
// We pass back the original file name so it can be displayed nicely
|
||||
onComplete({ storedName: response.fileName, name: file.name, type: 'file' });
|
||||
} else if (onError) {
|
||||
finishUpload();
|
||||
onError('Unable to upload file');
|
||||
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');
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send(file);
|
||||
}, (err) => {
|
||||
finishUpload();
|
||||
if (onError) onError(err.message || 'Unable to upload file');
|
||||
this.finishUpload();
|
||||
if (this.props.onError) this.props.onError(err.message || 'Unable to upload file');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { content, trigger, accept } = this.props;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<input type="file" style={{ display: 'none' }} ref={inputRef} onChange={handleFileChosen} accept={accept || '*'} />
|
||||
<input type="file" style={{ display: 'none' }} ref="fileInput" onChange={this.handleFileChosen} accept={accept || '*'} />
|
||||
{trigger
|
||||
? 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.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.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FileChooser;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import { connect } 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,31 +74,22 @@ const SearchBar = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
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 };
|
||||
});
|
||||
function NavBar({ user, groups, onOpenLogin, onOpenRegister, isAuthenticated, onLogout, onDriftSynced, helpModalOpen, openHelpModal, searchTerm, updateSearchTerm, searchPopupOpen, openSearchPopup, searchResults, updateSearchResults, searching, updateSearching, history }) {
|
||||
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
dispatch(actions.app.openSearchPopup(false));
|
||||
}, [dispatch]);
|
||||
openSearchPopup(false);
|
||||
}, [history.location.pathname, openSearchPopup]);
|
||||
|
||||
const logout = () => api.auth.logout(() => {
|
||||
dispatch(actions.auth.logout());
|
||||
dispatch(actions.users.syncDrift(false))
|
||||
onLogout();
|
||||
onDriftSynced(false);
|
||||
if (window.drift) window.drift.reset();
|
||||
navigate('/');
|
||||
history.push('/');
|
||||
});
|
||||
|
||||
const search = () => {
|
||||
dispatch(actions.app.updateSearching(true));
|
||||
api.search.all(searchTerm, r => dispatch(actions.app.updateSearchResults(r)));
|
||||
updateSearching(true);
|
||||
api.search.all(searchTerm, updateSearchResults);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -108,9 +99,8 @@ function NavBar() {
|
||||
{isAuthenticated
|
||||
? (
|
||||
<div className='nav-links'>
|
||||
<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>}
|
||||
<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>}
|
||||
content={<div style={{width: 300}} className='joyride-search'>
|
||||
{!searchResults?.users && !searchResults?.groups ?
|
||||
<small>
|
||||
@ -181,7 +171,7 @@ function NavBar() {
|
||||
}
|
||||
|
||||
<span className='above-mobile'>
|
||||
<Button size='small' icon='help' basic inverted onClick={e => dispatch(actions.app.openHelpModal(true))}/>
|
||||
<Button size='small' icon='help' basic inverted onClick={e => openHelpModal(true)}/>
|
||||
</span>
|
||||
|
||||
<Dropdown direction="left" pointing="top right" icon={null} style={{marginLeft: 10}}
|
||||
@ -223,20 +213,20 @@ function NavBar() {
|
||||
trigger=<Button basic inverted icon="bars" />
|
||||
>
|
||||
<Dropdown.Menu direction="left">
|
||||
<Dropdown.Item onClick={() => dispatch(actions.auth.openLogin())}>Login</Dropdown.Item>
|
||||
<Dropdown.Item onClick={onOpenLogin}>Login</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</span>
|
||||
<span className="above-mobile">
|
||||
<Button inverted basic onClick={() => dispatch(actions.auth.openLogin())}>Login</Button>
|
||||
<Button inverted basic onClick={onOpenLogin}>Login</Button>
|
||||
</span>
|
||||
<Button color="teal" onClick={() => dispatch(actions.auth.openRegister())}>
|
||||
<Button color="teal" onClick={onOpenRegister}>
|
||||
<span role="img" aria-label="wave">👋</span> Sign-up
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Modal open={helpModalOpen} onClose={e => dispatch(actions.app.openHelpModal(false))}>
|
||||
<Modal open={helpModalOpen} onClose={e => openHelpModal(false)}>
|
||||
<Modal.Header>Welcome to Treadl!</Modal.Header>
|
||||
<Modal.Content>
|
||||
<h3>Introduction</h3>
|
||||
@ -257,7 +247,7 @@ function NavBar() {
|
||||
<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 => dispatch(actions.app.openHelpModal(false))} color='teal' icon='check' content='OK' />
|
||||
<Button onClick={e => openHelpModal(false)} color='teal' icon='check' content='OK' />
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
</Container>
|
||||
@ -265,4 +255,29 @@ function NavBar() {
|
||||
);
|
||||
}
|
||||
|
||||
export default NavBar;
|
||||
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;
|
||||
|
@ -84,7 +84,7 @@ const NewFeedMessage = connect(
|
||||
>
|
||||
<Dropdown.Menu>
|
||||
<FileChooser
|
||||
forType={forType} forObject={forObj}
|
||||
forType={forType} for={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, { useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { Button, Dropdown } from 'semantic-ui-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import actions from 'actions';
|
||||
@ -8,33 +8,37 @@ import api from 'api';
|
||||
|
||||
import FileChooser from 'components/includes/FileChooser';
|
||||
|
||||
function ObjectCreator({ project, onCreateObject, onError, fluid }) {
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
class ObjectCreator extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isUploading: 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`);
|
||||
});
|
||||
};
|
||||
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 fileUploaded = (file) => {
|
||||
setIsUploading(true);
|
||||
api.projects.createObject(project.fullName, {
|
||||
fileUploaded = (file) => {
|
||||
this.setState({ isUploading: true });
|
||||
api.projects.createObject(this.props.project.fullName, {
|
||||
name: file.name, storedName: file.storedName, type: file.type, wif: file.wif,
|
||||
}, (object) => {
|
||||
setIsUploading(false);
|
||||
dispatch(actions.objects.create(object));
|
||||
navigate(`/${project.fullName}/${object._id}`);
|
||||
this.setState({ isUploading: false });
|
||||
this.props.onCreateObject(object);
|
||||
this.props.history.push(`/${this.props.project.fullName}/${object._id}`);
|
||||
}, (err) => {
|
||||
toast.error(err.message);
|
||||
setIsUploading(false);
|
||||
onError && onError(err);
|
||||
this.setState({ isUploading: false });
|
||||
this.props.onError && this.props.onError(err);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { project, fluid } = this.props;
|
||||
const { isUploading } = this.state;
|
||||
return (
|
||||
<Dropdown
|
||||
fluid={!!fluid}
|
||||
@ -42,27 +46,35 @@ function ObjectCreator({ project, onCreateObject, onError, fluid }) {
|
||||
trigger=<Button color="teal" fluid content="Add something" icon="plus" loading={isUploading} />
|
||||
>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item onClick={createNewPattern} icon="pencil" content="Create a new weaving pattern" />
|
||||
<Dropdown.Item onClick={this.createNewPattern} icon="pencil" content="Create a new weaving pattern" />
|
||||
<FileChooser
|
||||
forType="project"
|
||||
forObject={project}
|
||||
for={project}
|
||||
trigger=<Dropdown.Item icon="upload" content="Import a WIF file" />
|
||||
accept=".wif"
|
||||
onUploadStart={e => setIsUploading(true)}
|
||||
onUploadFinish={e => setIsUploading(false)}
|
||||
onComplete={fileUploaded}
|
||||
onUploadStart={e => this.setState({ isUploading: true })}
|
||||
onUploadFinish={e => this.setState({ isUploading: false })}
|
||||
onComplete={this.fileUploaded}
|
||||
/>
|
||||
<FileChooser
|
||||
forType="project"
|
||||
forObject={project}
|
||||
for={project}
|
||||
trigger=<Dropdown.Item icon="cloud upload" content="Upload an image or a file" />
|
||||
onUploadStart={e => setIsUploading(true)}
|
||||
onUploadFinish={e => setIsUploading(false)}
|
||||
onComplete={fileUploaded}
|
||||
onUploadStart={e => this.setState({ isUploading: true })}
|
||||
onUploadFinish={e => this.setState({ isUploading: false })}
|
||||
onComplete={this.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 ObjectCreator;
|
||||
export default ObjectCreatorContainer;
|
||||
|
@ -1,20 +1,16 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import pell from 'pell';
|
||||
|
||||
function RichText({ value, onChange }) {
|
||||
const [completedInit, setCompletedInit] = useState(false);
|
||||
const textboxRef = useRef();
|
||||
|
||||
const ensureHTTP = (url) => {
|
||||
class RichText extends Component {
|
||||
ensureHTTP(url) {
|
||||
if (url.trim().toLowerCase().indexOf('http') !== 0) return `http://${url}`;
|
||||
return url;
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (completedInit) return;
|
||||
componentDidMount() {
|
||||
pell.init({
|
||||
element: textboxRef.current,
|
||||
onChange: onChange,
|
||||
element: this.refs.textbox,
|
||||
onChange: this.props.onChange,
|
||||
actions: [
|
||||
{
|
||||
icon: '<i class="italic icon"></i>',
|
||||
@ -58,7 +54,7 @@ function RichText({ value, onChange }) {
|
||||
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', ensureHTTP(url));
|
||||
if (url) pell.exec('insertImage', this.ensureHTTP(url));
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -67,16 +63,17 @@ function RichText({ value, onChange }) {
|
||||
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', ensureHTTP(url));
|
||||
if (url) pell.exec('createLink', this.ensureHTTP(url));
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultParagraphSeparator: 'p',
|
||||
}).content.innerHTML = value || '';
|
||||
setCompletedInit(true);
|
||||
}, [completedInit, value, onChange]);
|
||||
}).content.innerHTML = this.props.value || '';
|
||||
}
|
||||
|
||||
return <div ref={textboxRef} />;
|
||||
render() {
|
||||
return <div ref="textbox" />;
|
||||
}
|
||||
}
|
||||
|
||||
export default RichText;
|
||||
|
@ -1,15 +1,16 @@
|
||||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
|
||||
function RichTextViewer({ content, style, className }) {
|
||||
if (!content) return null;
|
||||
class RichTextViewer extends Component {
|
||||
render() {
|
||||
if (!this.props.content) return null;
|
||||
return (
|
||||
<p
|
||||
style={Object.assign({}, { breakLines: 'pre-line'}, style)}
|
||||
className={className}
|
||||
style={Object.assign({}, { breakLines: 'pre-line'}, this.props.style)}
|
||||
className={this.props.className}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
sanitizeHtml(content, {
|
||||
sanitizeHtml(this.props.content, {
|
||||
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'u']),
|
||||
allowedAttributes: {
|
||||
a: ['href', 'name', 'target'],
|
||||
@ -19,6 +20,7 @@ function RichTextViewer({ content, style, className }) {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RichTextViewer;
|
||||
|
@ -1,55 +1,58 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { Menu, Popup, Input } from 'semantic-ui-react';
|
||||
import { connect } from 'react-redux'
|
||||
import api from 'api';
|
||||
|
||||
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);
|
||||
class UserSearch extends Component {
|
||||
|
||||
const updateSearchTerm = e => {
|
||||
setSearching(true);
|
||||
setSearchTerm(e.target.value);
|
||||
};
|
||||
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 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]);
|
||||
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 inputClicked = () => {
|
||||
if (searchResults) setOpen(true);
|
||||
};
|
||||
const choose = user => {
|
||||
onSelected && onSelected(user);
|
||||
};
|
||||
inputClicked = () => {
|
||||
if (this.state.searchResults) this.setState({ open: true });
|
||||
}
|
||||
onSelected = user => {
|
||||
this.props.onSelected && this.props.onSelected(user);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { searchResults, searching, open } = this.state;
|
||||
const { fluid } = this.props;
|
||||
return (
|
||||
<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}/>}
|
||||
<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}/>}
|
||||
content={(
|
||||
<Menu borderless vertical>
|
||||
{searchResults && searchResults.map(r =>
|
||||
<Menu.Item key={r._id} as='a' icon='user' content={r.username} onClick={e => choose(r)} image={r.avatarUrl}/>
|
||||
<Menu.Item key={r._id} as='a' icon='user' content={r.username} onClick={e => this.onSelected(r)} image={r.avatarUrl}/>
|
||||
)}
|
||||
</Menu>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UserSearch;
|
||||
|
||||
const UserSearchContainer = connect(null, null)(UserSearch);
|
||||
|
||||
export default UserSearchContainer;
|
||||
|
@ -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 { useSelector, useDispatch } from 'react-redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { toast } from 'react-toastify';
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
@ -13,37 +13,29 @@ import HelpLink from 'components/includes/HelpLink';
|
||||
import ProjectCard from 'components/includes/ProjectCard';
|
||||
import Tour from 'components/includes/Tour';
|
||||
|
||||
function Home() {
|
||||
function Home({ user, groups, projects, invitations, loadingProjects, onReceiveProjects, onReceiveInvitations, onDismissInvitation, onReceiveGroup, onJoinGroup }) {
|
||||
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}) => {
|
||||
dispatch(actions.invitations.receiveInvitations(invitations.concat(sentInvitations)));
|
||||
onReceiveInvitations(invitations.concat(sentInvitations));
|
||||
});
|
||||
}, [dispatch]);
|
||||
}, [onReceiveInvitations]);
|
||||
useEffect(() => {
|
||||
api.users.getMyProjects(p => dispatch(actions.projects.receiveProjects(p)));
|
||||
api.users.getMyProjects(onReceiveProjects);
|
||||
setTimeout(() =>
|
||||
setRunJoyride(true), 2000);
|
||||
}, [dispatch]);
|
||||
}, [onReceiveProjects]);
|
||||
|
||||
const declineInvite = (invite) => {
|
||||
api.invitations.decline(invite._id, () => dispatch(actions.invitations.dismiss(invite._id)), err => toast.error(err.message));
|
||||
api.invitations.decline(invite._id, () => onDismissInvitation(invite._id), err => toast.error(err.message));
|
||||
}
|
||||
const acceptInvite = (invite) => {
|
||||
api.invitations.accept(invite._id, (result) => {
|
||||
dispatch(actions.invitations.dismiss(invite._id));
|
||||
onDismissInvitation(invite._id);
|
||||
if (result.group) {
|
||||
dispatch(actions.groups.receiveGroup(result.group));
|
||||
dispatch(actions.users.joinGroup(user._id, result.group._id));
|
||||
onReceiveGroup(result.group);
|
||||
onJoinGroup(user._id, result.group._id);
|
||||
}
|
||||
}, err => toast.error(err.message));
|
||||
}
|
||||
@ -178,4 +170,23 @@ function Home() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
||||
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;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Loader, Button, Segment } from 'semantic-ui-react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import utils from 'utils/utils.js';
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
@ -10,29 +10,16 @@ import FeedMessage from 'components/includes/FeedMessage';
|
||||
import NewFeedMessage from 'components/includes/NewFeedMessage';
|
||||
import MessagesImage from 'images/messages.png';
|
||||
|
||||
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 };
|
||||
});
|
||||
function Feed({ user, group, entries, onReceiveEntry, onDeleteEntry, newEntry, onJoinGroup, replyingTo, updateReplyingTo, loadingEntries, updateLoadingEntries, match }) {
|
||||
const myGroups = user?.groups || [];
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(actions.groups.updateLoadingEntries(true));
|
||||
api.groups.getEntries(id, entries => {
|
||||
dispatch(actions.groups.updateLoadingEntries(false));
|
||||
entries.forEach(e => dispatch(actions.groups.receiveEntry(e)));
|
||||
updateLoadingEntries(true);
|
||||
api.groups.getEntries(match.params.id, entries => {
|
||||
updateLoadingEntries(false);
|
||||
entries.forEach(e => onReceiveEntry(e));
|
||||
});
|
||||
}, [dispatch, id, myGroups.length]);
|
||||
}, [match.params.id, myGroups.length, onReceiveEntry, updateLoadingEntries]);
|
||||
|
||||
const mainEntries = entries && entries.filter(e => !e.inReplyTo);
|
||||
|
||||
@ -40,9 +27,9 @@ function Feed() {
|
||||
<div>
|
||||
{utils.isInGroup(user, group._id) && <>
|
||||
{replyingTo ?
|
||||
<Button style={{marginBottom: 20}} color='teal' content='Write a new post' onClick={() => dispatch(actions.posts.updateReplyingTo(null))} />
|
||||
<Button style={{marginBottom: 20}} color='teal' content='Write a new post' onClick={() => updateReplyingTo(null)} />
|
||||
:
|
||||
<NewFeedMessage user={user} group={group} forType='group' onPosted={e => dispatch(actions.groups.receiveEntry(e))}/>
|
||||
<NewFeedMessage user={user} group={group} forType='group' onPosted={onReceiveEntry}/>
|
||||
}
|
||||
{loadingEntries && !mainEntries?.length &&
|
||||
<div style={{textAlign:'center'}}>
|
||||
@ -58,11 +45,35 @@ function Feed() {
|
||||
</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={id => dispatch(actions.groups.deleteEntry(id))} onReplyPosted={e => dispatch(actions.groups.receiveEntry(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} />
|
||||
)}
|
||||
</>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Feed;
|
||||
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;
|
||||
|
@ -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 { Outlet, Link, useParams } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Switch, Route, Link, withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { toast } from 'react-toastify';
|
||||
import utils from 'utils/utils.js';
|
||||
import actions from 'actions';
|
||||
@ -10,62 +10,53 @@ 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() {
|
||||
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 };
|
||||
});
|
||||
function Group({ user, group, requests, myRequests, loading, errorMessage, onReceiveGroup, onRequest, onRequestFailed, onJoinGroup, onLeaveGroup, onSubsUpdated, onReceiveInvitations, invitations, onDismissInvitation, match }) {
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(actions.groups.request());
|
||||
api.groups.get(id, g => dispatch(actions.groups.receiveGroup(g)), err => dispatch(actions.groups.requestFailed(err)));
|
||||
}, [dispatch, id]);
|
||||
onRequest();
|
||||
api.groups.get(match.params.id, onReceiveGroup, onRequestFailed);
|
||||
}, [match.params.id, onRequest, onReceiveGroup, onRequestFailed]);
|
||||
|
||||
const join = () => {
|
||||
if (!user) return toast.warning('Please login or sign-up first');
|
||||
api.groups.createMember(id, user._id, () => {
|
||||
dispatch(actions.users.joingGroup(user._id, id));
|
||||
api.groups.createMember(match.params.id, user._id, () => {
|
||||
onJoinGroup(user._id, match.params.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(id, user._id, () => {
|
||||
dispatch(actions.users.leaveGroup(user._id, id));
|
||||
api.groups.deleteMember(match.params.id, user._id, () => {
|
||||
onLeaveGroup(user._id, match.params.id);
|
||||
}, err => toast.error(err.message));
|
||||
}, () => {});
|
||||
}
|
||||
const toggleEmailSub = (key, enable) => {
|
||||
if (enable)
|
||||
api.users.createEmailSubscription(user.username, key, ({ subscriptions }) => dispatch(actions.users.updateSubscriptions(user._id, subscriptions)), err => toast.error(err.message));
|
||||
api.users.createEmailSubscription(user.username, key, ({ subscriptions }) => onSubsUpdated(user._id, subscriptions), err => toast.error(err.message));
|
||||
else
|
||||
api.users.deleteEmailSubscription(user.username, key, ({ subscriptions }) => dispatch(actions.users.updateSubscriptions(user._id, subscriptions)), err => toast.error(err.message));
|
||||
api.users.deleteEmailSubscription(user.username, key, ({ subscriptions }) => onSubsUpdated(user._id, subscriptions), err => toast.error(err.message));
|
||||
}
|
||||
|
||||
const requestToJoin = () => {
|
||||
api.groups.createJoinRequest(group._id, invitation => {
|
||||
toast.success('Request to join sent');
|
||||
dispatch(actions.invitations.receiveInvitations([invitation]));
|
||||
onReceiveInvitations([invitation]);
|
||||
}, err => toast.error(err.message));
|
||||
}
|
||||
|
||||
const declineInvite = (invite) => {
|
||||
api.invitations.decline(invite._id, () => dispatch(actions.invitations.dismiss(invite._id)), err => toast.error(err.message));
|
||||
api.invitations.decline(invite._id, () => onDismissInvitation(invite._id), err => toast.error(err.message));
|
||||
}
|
||||
const acceptInvite = (invite) => {
|
||||
api.invitations.accept(invite._id, (result) => {
|
||||
dispatch(actions.invitations.dismiss(invite._id));
|
||||
onDismissInvitation(invite._id);
|
||||
if (result.group) {
|
||||
dispatch(actions.users.joinGroup(user._id, result.group._id));
|
||||
onJoinGroup(user._id, result.group._id);
|
||||
}
|
||||
}, err => toast.error(err.message));
|
||||
}
|
||||
@ -177,7 +168,13 @@ function Group() {
|
||||
}
|
||||
</Segment>
|
||||
}
|
||||
<Outlet />
|
||||
<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>
|
||||
</>
|
||||
:
|
||||
<Message>Please login to view or join this group.</Message>
|
||||
@ -190,4 +187,31 @@ function Group() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Group;
|
||||
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;
|
||||
|
@ -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 { useSelector, useDispatch } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { toast } from 'react-toastify';
|
||||
import moment from 'moment';
|
||||
import utils from 'utils/utils.js';
|
||||
@ -12,19 +12,9 @@ import UserChip from 'components/includes/UserChip';
|
||||
import HelpLink from 'components/includes/HelpLink';
|
||||
import UserSearch from 'components/includes/UserSearch';
|
||||
|
||||
function Members() {
|
||||
function Members({ user, group, members, requests, loading, onReceiveUser, onJoinGroup, onLeaveGroup, onUpdateGroupLoading, onDismissInvitation }) {
|
||||
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)) {
|
||||
@ -32,15 +22,15 @@ function Members() {
|
||||
}
|
||||
}, [user, group]);
|
||||
useEffect(() => {
|
||||
dispatch(actions.users.request(true));
|
||||
onUpdateGroupLoading(true);
|
||||
api.groups.getMembers(group._id, members => {
|
||||
members.forEach(u => dispatch(actions.users.receive(u)));
|
||||
dispatch(actions.users.request(false));
|
||||
members.forEach(onReceiveUser);
|
||||
onUpdateGroupLoading(false);
|
||||
}, err => {
|
||||
toast.error(err.message);
|
||||
dispatch(actions.users.request(false));
|
||||
onUpdateGroupLoading(false);
|
||||
});
|
||||
}, [dispatch, group]);
|
||||
}, [group, onReceiveUser, onUpdateGroupLoading]);
|
||||
|
||||
const copyLink = () => {
|
||||
joinLinkRef.current.select();
|
||||
@ -50,7 +40,7 @@ function Members() {
|
||||
const kickUser = (id) => {
|
||||
utils.confirm('Really kick this user?').then(() => {
|
||||
api.groups.deleteMember(group._id, id, () => {
|
||||
dispatch(actions.users.leaveGroup(id, group._id));
|
||||
onLeaveGroup(id, group._id);
|
||||
}, err => toast.error(err.message));
|
||||
}, () => {});
|
||||
}
|
||||
@ -70,14 +60,14 @@ function Members() {
|
||||
}
|
||||
const approveRequest = (invite) => {
|
||||
api.invitations.accept(invite._id, (result) => {
|
||||
dispatch(actions.invitations.dismiss(invite._id))
|
||||
dispatch(actions.users.receive(invite.invitedBy));
|
||||
dispatch(actions.users.joinGroup(invite.user, group._id));
|
||||
onDismissInvitation(invite._id);
|
||||
onReceiveUser(invite.invitedBy);
|
||||
onJoinGroup(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, () => dispatch(actions.invitations.dismiss(request._id)),
|
||||
api.invitations.decline(request._id, () => onDismissInvitation(request._id),
|
||||
err => toast.error(err.message));
|
||||
}
|
||||
|
||||
@ -174,4 +164,23 @@ function Members() {
|
||||
)
|
||||
}
|
||||
|
||||
export default Members;
|
||||
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;
|
||||
|
@ -2,30 +2,23 @@ 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 { useNavigate } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
|
||||
import HelpLink from 'components/includes/HelpLink';
|
||||
|
||||
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 };
|
||||
});
|
||||
function NewGroup({ user, newGroupName, newGroupDescription, newGroupClosed, onUpdateGroupName, onUpdateGroupDescription, onUpdateGroupClosed, onReceiveGroup, loading, onUpdateGroupLoading, history }) {
|
||||
|
||||
const createGroup = () => {
|
||||
dispatch(actions.groups.request(true));
|
||||
onUpdateGroupLoading(true);
|
||||
api.groups.create({ name: newGroupName, description: newGroupDescription, closed: newGroupClosed }, (group) => {
|
||||
dispatch(actions.groups.receiveGroup(group));
|
||||
dispatch(actions.groups.request(false));
|
||||
navigate(`/groups/${group._id}/members`);
|
||||
onReceiveGroup(group);
|
||||
onUpdateGroupLoading(false);
|
||||
history.push(`/groups/${group._id}/members`);
|
||||
}, (err) => {
|
||||
dispatch(actions.groups.request(false));
|
||||
onUpdateGroupLoading(false);
|
||||
toast.error(err.message);
|
||||
});
|
||||
}
|
||||
@ -45,12 +38,12 @@ function NewGroup() {
|
||||
|
||||
<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 => dispatch(actions.groups.updateNewGroupName(e.target.value))} value={newGroupName} />
|
||||
<Input autoFocus type="text" fluid onChange={e => onUpdateGroupName(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 => dispatch(actions.groups.updateNewGroupDescription(e.target.value))} /></Form>
|
||||
<Form><Form.TextArea placeholder="Group description (optional)..." value={newGroupDescription} onChange={e => onUpdateGroupDescription(e.target.value)} /></Form>
|
||||
|
||||
<Checkbox style={{marginTop: 40}} toggle checked={newGroupClosed} label="Make this group a closed group" onChange={(e,c) => dispatch(actions.groups.updateNewGroupClosed(c.checked))} />
|
||||
<Checkbox style={{marginTop: 40}} toggle checked={newGroupClosed} label="Make this group a closed group" onChange={(e,c) => onUpdateGroupClosed(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>
|
||||
@ -59,7 +52,7 @@ function NewGroup() {
|
||||
<p>You can add and invite others to join your group after you've created it.</p>
|
||||
|
||||
<div style={{textAlign: 'right'}}>
|
||||
<Button basic onClick={() => navigate(-1)}>Cancel</Button>
|
||||
<Button basic onClick={history.goBack}>Cancel</Button>
|
||||
<Button color="teal" icon="check" content="Create group" onClick={createGroup} loading={loading} />
|
||||
</div>
|
||||
</Grid.Column>
|
||||
@ -67,4 +60,19 @@ function NewGroup() {
|
||||
);
|
||||
}
|
||||
|
||||
export default NewGroup;
|
||||
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;
|
||||
|
@ -1,29 +1,16 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Input, Divider, Loader, Segment, Card, Dropdown, Button } from 'semantic-ui-react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { toast } from 'react-toastify';
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
|
||||
import ProjectCard from 'components/includes/ProjectCard';
|
||||
|
||||
function Projects() {
|
||||
function Projects({ group, myProjects, onReceiveProject, projectFilter, updateProjectFilter }) {
|
||||
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);
|
||||
@ -42,7 +29,7 @@ function Projects() {
|
||||
if (index > -1) groupVisibility.splice(index, 1);
|
||||
else groupVisibility.push(group._id);
|
||||
api.projects.update(project.fullName, { groupVisibility }, updatedProject => {
|
||||
dispatch(actions.projects.receiveProject(updatedProject));
|
||||
onReceiveProject(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);
|
||||
@ -79,7 +66,7 @@ function Projects() {
|
||||
<AddProject style={{float:'right'}} />
|
||||
</>}
|
||||
{projects?.length > 0 &&
|
||||
<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...' />
|
||||
<Input autoFocus style={{float:'right', marginRight: 5}} size='small' icon='search' value={projectFilter} onChange={e => updateProjectFilter(e.target.value)} placeholder='Filter projects...' />
|
||||
}
|
||||
<Divider hidden clearing />
|
||||
|
||||
@ -101,4 +88,23 @@ function Projects() {
|
||||
)
|
||||
}
|
||||
|
||||
export default Projects;
|
||||
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;
|
||||
|
@ -1,40 +1,31 @@
|
||||
import React from 'react';
|
||||
import { Header, Button, Divider, Segment, Form, } from 'semantic-ui-react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { toast } from 'react-toastify';
|
||||
import utils from 'utils/utils.js';
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
|
||||
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 };
|
||||
});
|
||||
function Settings({ user, group, loading, onUpdateGroupLoading, onUpdateGroup, onDeleteGroup, history }) {
|
||||
|
||||
const saveGroup = () => {
|
||||
dispatch(actions.groups.request(true));
|
||||
onUpdateGroupLoading(true);
|
||||
const { _id, name, description, closed } = group;
|
||||
api.groups.update(_id, { name, description, closed }, () => {
|
||||
dispatch(actions.groups.request(false));
|
||||
onUpdateGroupLoading(false);
|
||||
toast.info('Group updated');
|
||||
}, err => {
|
||||
toast.error(err.message);
|
||||
dispatch(actions.groups.request(false));
|
||||
onUpdateGroupLoading(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');
|
||||
dispatch(actions.groups.deleteGroup(group._id));
|
||||
navigate(`/`);
|
||||
onDeleteGroup(group._id);
|
||||
history.push(`/`);
|
||||
}, err => toast.error(err.message));
|
||||
}, () => {});
|
||||
}
|
||||
@ -44,9 +35,9 @@ function Settings() {
|
||||
<Segment color='blue'>
|
||||
<Header>About this group</Header>
|
||||
<Form>
|
||||
<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 }))} />
|
||||
<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 })} />
|
||||
<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>
|
||||
@ -65,4 +56,17 @@ function Settings() {
|
||||
)
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
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;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { Component } 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 { useNavigate } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import utils from 'utils/utils.js';
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
@ -11,48 +11,44 @@ import api from 'api';
|
||||
import UserChip from 'components/includes/UserChip';
|
||||
import HelpLink from 'components/includes/HelpLink';
|
||||
|
||||
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();
|
||||
|
||||
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 };
|
||||
});
|
||||
|
||||
const updateName = (event) => {
|
||||
setName(event.target.value);
|
||||
class NewProject extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
name: 'My new project', description: '', visibility: 'public', openSource: true, groupVisibility: [], error: '', loading: false,
|
||||
};
|
||||
}
|
||||
|
||||
const changeVisibility = (event, r) => {
|
||||
setVisibility(r.checked ? 'private' : 'public');
|
||||
};
|
||||
updateName = (event) => {
|
||||
this.setState({ name: event.target.value });
|
||||
}
|
||||
|
||||
const changeOpenSource = (event, c) => {
|
||||
setOpenSource(c.checked);
|
||||
};
|
||||
changeVisibility = (event, r) => {
|
||||
this.setState({ visibility: r.checked ? 'private' : 'public' });
|
||||
}
|
||||
|
||||
const createProject = () => {
|
||||
setLoading(true);
|
||||
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) => {
|
||||
dispatch(actions.projects.receiveProject(project));
|
||||
setLoading(false);
|
||||
navigate(`/${user.username}/${project.path}`);
|
||||
this.props.onReceiveProject(project);
|
||||
this.setState({ loading: false });
|
||||
this.props.history.push(`/${this.props.user.username}/${project.path}`);
|
||||
}, (err) => {
|
||||
setLoading(false);
|
||||
this.setState({ loading: false });
|
||||
toast.error(err.message);
|
||||
setError(err.message);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
name, description, visibility, openSource, groupVisibility
|
||||
} = this.state;
|
||||
const { user, groups } = this.props;
|
||||
return (
|
||||
<Grid stackable centered>
|
||||
<Helmet title={'Create Project'} />
|
||||
@ -70,17 +66,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={updateName} value={name} label={<UserChip user={user} style={{ marginRight: 10, marginTop: 4 }} />} />
|
||||
<Input autoFocus type="text" fluid onChange={this.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 => setDescription(e.target.value)} /></Form>
|
||||
<Form><Form.TextArea placeholder="Project description (optional)..." value={description} onChange={e => this.setState({ description: e.target.value })} /></Form>
|
||||
<Divider section />
|
||||
|
||||
<h3>Project visibility</h3>
|
||||
<Form>
|
||||
<Form.Checkbox label='This is a private project' checked={visibility === 'private'} onChange={changeVisibility} />
|
||||
<Form.Checkbox label='This is a private project' checked={visibility === 'private'} onChange={this.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={changeOpenSource} />
|
||||
<Form.Checkbox label='This project is "open-source"' checked={openSource} onChange={this.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 />
|
||||
|
||||
@ -93,7 +89,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) => setGroupVisibility(s.value)}
|
||||
onChange={(e, s) => this.setState({ groupVisibility: s.value })}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
@ -101,13 +97,27 @@ Create a new project
|
||||
|
||||
<Divider section />
|
||||
|
||||
{error && <Message color="orange" content={error} />}
|
||||
{this.state.error && <Message color="orange" content={this.state.error} />}
|
||||
<div style={{textAlign:'right'}}>
|
||||
<Button basic onClick={() => navigate(-1)}>Cancel</Button>
|
||||
<Button color="teal" icon="check" content="Create project" onClick={createProject} loading={loading} />
|
||||
<Button basic onClick={this.props.history.goBack}>Cancel</Button>
|
||||
<Button color="teal" icon="check" content="Create project" onClick={this.createProject} loading={this.state.loading} />
|
||||
</div>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default NewProject;
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
||||
const groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
|
||||
return { user, groups };
|
||||
};
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onReceiveProject: project => dispatch(actions.projects.receiveProject(project)),
|
||||
});
|
||||
const NewProjectContainer = withRouter(connect(
|
||||
mapStateToProps, mapDispatchToProps,
|
||||
)(NewProject));
|
||||
|
||||
export default NewProjectContainer;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Segment, Label, Input, Icon, Card, Loader } from 'semantic-ui-react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import utils from 'utils/utils.js';
|
||||
import actions from 'actions';
|
||||
@ -22,31 +22,17 @@ const CompactObject = styled(Link)`
|
||||
}
|
||||
`;
|
||||
|
||||
function ObjectList({ compact }) {
|
||||
function ObjectList({ user, objects, currentObject, project, fullProjectPath, onReceiveObjects, 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, o => {
|
||||
dispatch(actions.objects.receiveMultiple(o));
|
||||
api.projects.getObjects(fullProjectPath, projects => {
|
||||
onReceiveObjects(projects);
|
||||
setLoading(false);
|
||||
}, err => setLoading(false));
|
||||
}, [fullProjectPath, dispatch]);
|
||||
}, [fullProjectPath, onReceiveObjects]);
|
||||
|
||||
let filteredObjects = objects;
|
||||
if (objectFilter) {
|
||||
@ -171,4 +157,26 @@ function ObjectList({ compact }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default ObjectList;
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { username, projectPath, objectId } = ownProps.match.params;
|
||||
const project = state.projects.projects.filter(p => p.path === projectPath && p.owner && p.owner.username === username)[0];
|
||||
const objects = [];
|
||||
let currentObject;
|
||||
state.objects.objects.forEach((d) => {
|
||||
if (d.project === project._id) objects.push(d);
|
||||
if (d._id === objectId) currentObject = d;
|
||||
});
|
||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
||||
return { user, project, objects, currentObject, fullProjectPath: `${username}/${projectPath}` };
|
||||
};
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onReceiveObjects: objects => dispatch(actions.objects.receiveMultiple(objects)),
|
||||
onEditObject: (id, field, value) => dispatch(actions.objects.update(id, field, value)),
|
||||
onDeleteObject: id => dispatch(actions.objects.delete(id)),
|
||||
});
|
||||
const ObjectListContainer = withRouter(connect(
|
||||
mapStateToProps, mapDispatchToProps,
|
||||
)(ObjectList));
|
||||
|
||||
export default ObjectListContainer;
|
||||
|
@ -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 { useSelector, useDispatch } from 'react-redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import moment from 'moment';
|
||||
import utils from 'utils/utils.js';
|
||||
import actions from 'actions';
|
||||
@ -15,42 +15,23 @@ import DraftPreview from './objects/DraftPreview';
|
||||
import NewFeedMessage from 'components/includes/NewFeedMessage';
|
||||
import FeedMessage from 'components/includes/FeedMessage';
|
||||
|
||||
function ObjectViewer() {
|
||||
function ObjectViewer({ user, myProjects, project, fullProjectPath, onEditObject, onDeleteObject, object, comments, history, onReceiveComment, onDeleteComment }) {
|
||||
const [editingName, setEditingName] = useState(false);
|
||||
const [editingDescription, setEditingDescription] = useState(false);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
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 };
|
||||
});
|
||||
const objectId = object?._id;
|
||||
|
||||
useEffect(() => {
|
||||
if (!objectId) return;
|
||||
api.objects.getComments(objectId, data => {
|
||||
data.comments.forEach(c => dispatch(actions.objects.receiveComment(c)));
|
||||
data.comments.forEach(onReceiveComment);
|
||||
});
|
||||
}, [dispatch, objectId]);
|
||||
}, [objectId, onReceiveComment]);
|
||||
|
||||
const deleteObject = (object) => {
|
||||
utils.confirm('Delete object', 'Really delete this object? This cannot be undone.').then(() => {
|
||||
navigate(`/${fullProjectPath}`);
|
||||
api.objects.delete(object._id, () => dispatch(actions.objects.delete(object._id)), err => toast.error(err.message));
|
||||
history.push(`/${fullProjectPath}`);
|
||||
api.objects.delete(object._id, () => onDeleteObject(object._id), err => toast.error(err.message));
|
||||
}, () => {});
|
||||
}
|
||||
|
||||
@ -165,7 +146,7 @@ function ObjectViewer() {
|
||||
{editingName ?
|
||||
<div style={{marginBottom: 5}}>
|
||||
<Input autoFocus size="small" value={object.name} style={{marginBottom: 10}}
|
||||
onChange={e => dispatch(actions.objects.update(object._id, 'name', e.target.value))}
|
||||
onChange={e => onEditObject(object._id, 'name', e.target.value)}
|
||||
action=<Button icon="check" primary onClick={e => saveObjectName(object)} />
|
||||
/>
|
||||
</div>
|
||||
@ -185,7 +166,7 @@ function ObjectViewer() {
|
||||
<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} />
|
||||
<DraftPreview object={object} onImageLoaded={i => onEditObject(object._id, 'patternImage', i)}/>
|
||||
</div>
|
||||
}
|
||||
{object.isImage &&
|
||||
@ -201,7 +182,7 @@ function ObjectViewer() {
|
||||
|
||||
{editingDescription ?
|
||||
<div style={{padding: 10, border: '1px solid rgb(230,230,230)'}}>
|
||||
<RichText value={object.description} onChange={t => dispatch(actions.objects.update(object._id, 'description', t))} />
|
||||
<RichText value={object.description} onChange={t => onEditObject(object._id, 'description', t)} />
|
||||
<Button primary size="small" icon="check" content="Save description" onClick={e => saveObjectDescription(object)} />
|
||||
</div>
|
||||
:
|
||||
@ -221,14 +202,42 @@ function ObjectViewer() {
|
||||
}
|
||||
</h3>
|
||||
{user ?
|
||||
<NewFeedMessage noAttachments user={user} forType='object' object={object} onPosted={c => dispatch(actions.objects.receiveComment(c))} placeholder='Write a new comment...'/>
|
||||
<NewFeedMessage noAttachments user={user} forType='object' object={object} onPosted={onReceiveComment} 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={id => dispatch(actions.objects.deleteComment(id))} />
|
||||
<FeedMessage key={c._id} user={user} forType='object' object={object} post={c} replies={[]} onDeleted={onDeleteComment} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default ObjectViewer;
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { username, projectPath, objectId } = ownProps.match.params;
|
||||
const project = state.projects.projects.filter(p => p.path === projectPath && p.owner && p.owner.username === username)[0];
|
||||
const objects = [];
|
||||
state.objects.objects.forEach((d) => {
|
||||
if (d.project === project._id) objects.push(d);
|
||||
});
|
||||
const object = objects.filter(o => o._id === objectId)[0];
|
||||
const comments = state.objects.comments?.filter(c => c.object === object?._id).sort((a, b) => {
|
||||
const aDate = new Date(a.createdAt);
|
||||
const bDate = new Date(b.createdAt);
|
||||
return aDate < bDate;
|
||||
});
|
||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
||||
const myProjects = state.projects.projects.filter(p => p.owner?.username === user?.username);
|
||||
return { user, myProjects, project, objects, fullProjectPath: `${username}/${projectPath}`, object, comments };
|
||||
};
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onEditObject: (id, field, value) => dispatch(actions.objects.update(id, field, value)),
|
||||
onDeleteObject: id => dispatch(actions.objects.delete(id)),
|
||||
onReceiveComment: c => dispatch(actions.objects.receiveComment(c)),
|
||||
onDeleteComment: id => dispatch(actions.objects.deleteComment(id)),
|
||||
});
|
||||
const ObjectViewerContainer = withRouter(connect(
|
||||
mapStateToProps, mapDispatchToProps,
|
||||
)(ObjectViewer));
|
||||
|
||||
export default ObjectViewerContainer;
|
||||
|
@ -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 { Outlet, Link, useParams, useLocation } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Switch, Route, Link, withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import utils from 'utils/utils.js';
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
@ -11,28 +11,24 @@ 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() {
|
||||
function Project({ user, project, fullName, errorMessage, editingDescription, onUpdateProject, onReceiveProject, onEditDescription, onRequest, onRequestFailed, match, history }) {
|
||||
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(() => {
|
||||
dispatch(actions.projects.request());
|
||||
api.projects.get(fullName, p => dispatch(actions.projects.receiveProject(p)), err => dispatch(actions.projects.requestFailed(err)));
|
||||
}, [user, dispatch, fullName]);
|
||||
onRequest();
|
||||
api.projects.get(fullName, onReceiveProject, onRequestFailed);
|
||||
}, [user, onRequest, fullName, onReceiveProject, onRequestFailed]);
|
||||
|
||||
const wideBody = () => !location.pathname.toLowerCase().endsWith(fullName.toLowerCase());
|
||||
const wideBody = () => !match.isExact
|
||||
|
||||
const saveDescription = () => {
|
||||
dispatch(actions.projects.editDescription(false));
|
||||
api.projects.update(fullName, { description: project.description }, p => dispatch(actions.projects.receiveProject(p)));
|
||||
onEditDescription(false);
|
||||
api.projects.update(fullName, { description: project.description }, onReceiveProject);
|
||||
}
|
||||
|
||||
const getDescription = () => {
|
||||
@ -54,11 +50,11 @@ function Project() {
|
||||
{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 &&
|
||||
<>
|
||||
@ -89,7 +85,7 @@ function Project() {
|
||||
{editingDescription
|
||||
? (
|
||||
<Form>
|
||||
<TextArea style={{ marginBottom: '5px' }} placeholder="Describe this project..." value={project.description} onChange={e => dispatch(actions.projects.updateProject(project._id, { description: e.target.value }))} />
|
||||
<TextArea style={{ marginBottom: '5px' }} placeholder="Describe this project..." value={project.description} onChange={e => onUpdateProject(project._id, { description: e.target.value })} />
|
||||
<Button size="tiny" color="green" fluid onClick={saveDescription}>Save description</Button>
|
||||
</Form>
|
||||
)
|
||||
@ -104,7 +100,7 @@ function Project() {
|
||||
</div>
|
||||
}
|
||||
{utils.canEditProject(user, project) && (
|
||||
<Button size='mini' fluid className="right floated" onClick={e => dispatch(actions.projects.editDescription(true))}>
|
||||
<Button size='mini' fluid className="right floated" onClick={e => onEditDescription(true)}>
|
||||
<Icon name="pencil" /> {project.description ? 'Edit' : 'Add a project'} description
|
||||
</Button>
|
||||
)
|
||||
@ -125,7 +121,14 @@ function Project() {
|
||||
)}
|
||||
|
||||
<Grid.Column computer={wideBody() ? 16 : 12} tablet={wideBody() ? 16 : 10}>
|
||||
{project && <Outlet />}
|
||||
{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>
|
||||
) }
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
|
||||
@ -137,4 +140,22 @@ function Project() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Project;
|
||||
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;
|
||||
|
@ -1,63 +1,57 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { Form, Input, Divider, Segment, Button, Icon } from 'semantic-ui-react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { connect } from 'react-redux';
|
||||
import utils from 'utils/utils.js';
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
|
||||
import HelpLink from 'components/includes/HelpLink';
|
||||
|
||||
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();
|
||||
|
||||
const changeVisibility = (event, r) => {
|
||||
setVisibility(r.checked ? 'private' : 'public');
|
||||
};
|
||||
|
||||
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(fullProjectPath, { name: name }, (project) => {
|
||||
dispatch(actions.projects.receiveProject(project));
|
||||
navigate(`/${project.owner.username}/${project.path}`);
|
||||
}, err => toast.error(err.message));
|
||||
});
|
||||
};
|
||||
|
||||
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));
|
||||
};
|
||||
|
||||
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(fullProjectPath, () => {
|
||||
dispatch(actions.projects.deleteProject(project._id));
|
||||
toast.info('🗑️ Project deleted');
|
||||
navigate('/');
|
||||
}, err => toast.error(err.message));
|
||||
});
|
||||
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' });
|
||||
}
|
||||
|
||||
changeOpenSource = (event, c) => {
|
||||
this.setState({ openSource: c.checked });
|
||||
}
|
||||
|
||||
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}`);
|
||||
}, 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);
|
||||
toast.info('Visibility saved');
|
||||
}, err => toast.error(err.message));
|
||||
}
|
||||
|
||||
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);
|
||||
toast.info('🗑️ Project deleted');
|
||||
this.props.history.push('/');
|
||||
this.setState({ nameError: '' });
|
||||
}, err => toast.error(err.message));
|
||||
}, () => {});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { name, visibility, openSource } = this.state;
|
||||
const { groups } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<h2>Project settings</h2>
|
||||
@ -73,8 +67,8 @@ function ProjectSettings() {
|
||||
<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={saveName} />
|
||||
onChange={e => setName(e.target.value)}
|
||||
action=<Button color="teal" content="Update" onClick={this.saveName} />
|
||||
onChange={e => this.setState({ name: e.target.value })}
|
||||
/>
|
||||
</Segment>
|
||||
|
||||
@ -83,9 +77,9 @@ function ProjectSettings() {
|
||||
<Segment color="yellow">
|
||||
<h3>Project visibility</h3>
|
||||
<Form>
|
||||
<Form.Checkbox label='This is a private project' checked={visibility === 'private'} onChange={changeVisibility} />
|
||||
<Form.Checkbox label='This is a private project' checked={visibility === 'private'} onChange={this.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={changeOpenSource} />
|
||||
<Form.Checkbox label='This project is "open-source"' checked={openSource} onChange={this.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 />
|
||||
|
||||
@ -96,16 +90,16 @@ function ProjectSettings() {
|
||||
|
||||
<Form.Select multiple
|
||||
label='Make this project always visible to members of these groups'
|
||||
value={groupVisibility}
|
||||
value={this.state.groupVisibility}
|
||||
options={groups.map(g => ({ key: g._id, value: g._id, text: g.name }))}
|
||||
onChange={(e, s) => setGroupVisibility(s.value)}
|
||||
onChange={(e, s) => this.setState({ groupVisibility: s.value })}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</Form>
|
||||
<Divider hidden />
|
||||
|
||||
<Button color="teal" content="Save visibility" onClick={saveVisibility} />
|
||||
<Button color="teal" content="Save visibility" onClick={this.saveVisibility} />
|
||||
</Segment>
|
||||
|
||||
<Divider hidden section />
|
||||
@ -113,10 +107,28 @@ function ProjectSettings() {
|
||||
<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={deleteProject} />
|
||||
<Button icon="trash" content="Delete project" color="red" onClick={this.deleteProject} />
|
||||
</Segment>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ProjectSettings;
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { username, projectPath } = ownProps.match.params;
|
||||
const project = state.projects.projects.filter(p => p.path === projectPath && p.owner && p.owner.username === username)[0];
|
||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
||||
const groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
|
||||
return { user, groups, project, fullProjectPath: `${username}/${projectPath}` };
|
||||
};
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onDeleteProject: id => dispatch(actions.projects.deleteProject(id)),
|
||||
onReceiveProject: project => dispatch(actions.projects.receiveProject(project)),
|
||||
onUpdateProject: (id, update) => dispatch(actions.projects.updateProject(id, update)),
|
||||
});
|
||||
const ProjectSettingsContainer = withRouter(connect(
|
||||
mapStateToProps, mapDispatchToProps,
|
||||
)(ProjectSettings));
|
||||
|
||||
export default ProjectSettingsContainer;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { withRouter, Prompt } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { toast } from 'react-toastify';
|
||||
import styled from 'styled-components';
|
||||
import ElementPan from 'components/includes/ElementPan';
|
||||
@ -25,63 +25,67 @@ export const StyledPattern = styled.div`
|
||||
margin:20px;
|
||||
`;
|
||||
|
||||
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 }));
|
||||
class Draft extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { unsaved: false, saving: false };
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
api.objects.get(objectId, (o) => {
|
||||
if (!o.pattern.baseSize) o.pattern.baseSize = 10;
|
||||
setObject(o);
|
||||
setPattern(o.pattern);
|
||||
componentDidMount() {
|
||||
api.objects.get(this.props.match.params.objectId, (object) => {
|
||||
if (!object.pattern.baseSize) object.pattern.baseSize = 10;
|
||||
this.setState(object);
|
||||
});
|
||||
}, [objectId]);
|
||||
}
|
||||
|
||||
const updateObject = (update) => {
|
||||
setObject(Object.assign({}, object, update));
|
||||
setUnsaved(true);
|
||||
};
|
||||
updateObject = (update) => {
|
||||
this.setState(Object.assign({}, this.state, update));
|
||||
this.setState({ unsaved: true });
|
||||
}
|
||||
|
||||
const updatePattern = (update) => {
|
||||
const newPattern = Object.assign({}, pattern, update);
|
||||
setPattern(Object.assign({}, pattern, newPattern));
|
||||
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 saveObject = () => {
|
||||
setSaving(true);
|
||||
saveObject = () => {
|
||||
this.setState({ saving: true });
|
||||
const canvas = document.getElementsByClassName('drawdown')[0];
|
||||
const newObject = Object.assign({}, object);
|
||||
newObject.preview = canvas.toDataURL();
|
||||
api.objects.update(objectId, newObject, (o) => {
|
||||
const object = Object.assign({}, this.state);
|
||||
object.preview = canvas.toDataURL();
|
||||
api.objects.update(this.props.match.params.objectId, object, (o) => {
|
||||
toast.success('Pattern saved');
|
||||
dispatch(actions.objects.receive(o));
|
||||
setUnsaved(false);
|
||||
setSaving(false);
|
||||
this.props.onReceiveObject(o);
|
||||
this.setState({ unsaved: false, saving: false });
|
||||
}, (err) => {
|
||||
toast.error(err.message);
|
||||
setSaving(false);
|
||||
this.setState({ saving: false });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
if (!pattern) return null;
|
||||
const { warp, weft, tieups, baseSize } = pattern;
|
||||
rerunTour = () => {
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.pattern) return null;
|
||||
const { unsaved, saving } = this.state;
|
||||
const { warp, weft, tieups, baseSize } = this.state.pattern;
|
||||
const cellStyle = { width: `${baseSize || 10}px`, height: `${baseSize || 10}px` };
|
||||
return (
|
||||
<div>
|
||||
<Helmet title={`${name || 'Weaving Draft'}`} />
|
||||
<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?'
|
||||
/>
|
||||
<Tour id='pattern' run={true} />
|
||||
<div style={{display: 'flex'}}>
|
||||
|
||||
<div style={{flex: 1, overflow: 'hidden'}}>
|
||||
<ElementPan
|
||||
disabled={!(editor?.tool === 'pan')}
|
||||
disabled={!(this.props.editor && this.props.editor.tool === 'pan')}
|
||||
startX={5000}
|
||||
startY={0}
|
||||
>
|
||||
@ -92,9 +96,9 @@ function Draft() {
|
||||
}}
|
||||
>
|
||||
|
||||
<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}/>
|
||||
<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}/>
|
||||
<Drawdown warp={warp} weft={weft} tieups={tieups} baseSize={baseSize} />
|
||||
|
||||
</StyledPattern>
|
||||
@ -104,12 +108,21 @@ function Draft() {
|
||||
<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={object} pattern={pattern} updateObject={updateObject} updatePattern={updatePattern} saveObject={saveObject} baseSize={baseSize} unsaved={unsaved} saving={saving}/>
|
||||
<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}/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Draft;
|
||||
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;
|
||||
|
52
web/src/components/main/projects/objects/DraftExport.js
Normal file
52
web/src/components/main/projects/objects/DraftExport.js
Normal file
@ -0,0 +1,52 @@
|
||||
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, { useEffect, useState, useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } 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';
|
||||
|
||||
function DraftPreview({ object }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [pattern, setPattern] = useState();
|
||||
const dispatch = useDispatch();
|
||||
const objectId = object?._id;
|
||||
class DraftPreview extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { loading: false };
|
||||
}
|
||||
|
||||
const generatePreview = useCallback(() => {
|
||||
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) {
|
||||
setTimeout(() => {
|
||||
const c = document.getElementsByClassName('drawdown')[0];
|
||||
const preview = c?.toDataURL();
|
||||
const preview = c && c.toDataURL();
|
||||
if (preview) {
|
||||
api.objects.update(objectId, { preview }, () => {
|
||||
dispatch(actions.objects.update(objectId, 'preview', preview));
|
||||
api.objects.update(object._id, { preview }, () => {
|
||||
this.props.onEditObject(object._id, 'preview', preview);
|
||||
});
|
||||
}
|
||||
}, 1000);
|
||||
}, [dispatch, objectId]);
|
||||
|
||||
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]);
|
||||
}, err => this.setState({ loading: false }));
|
||||
}
|
||||
|
||||
const unifyCanvas = useCallback(() => {
|
||||
if (!pattern) return;
|
||||
const { warp, weft } = pattern;
|
||||
unifyCanvas() {
|
||||
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 @@ function DraftPreview({ object }) {
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = 'black';
|
||||
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`);
|
||||
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`);
|
||||
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,18 +66,16 @@ function DraftPreview({ object }) {
|
||||
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);
|
||||
dispatch(actions.objects.update(objectId, 'patternImage', canvas.toDataURL()));
|
||||
|
||||
this.props.onImageLoaded(canvas.toDataURL());
|
||||
}
|
||||
}, 500);
|
||||
}, [dispatch, objectId, pattern]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
unifyCanvas();
|
||||
}, [unifyCanvas])
|
||||
|
||||
if (loading) return <Loader active />;
|
||||
if (!pattern) return null;
|
||||
const { warp, weft, tieups } = pattern;
|
||||
render() {
|
||||
if (this.state.loading) return <Loader active />;
|
||||
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` };
|
||||
@ -87,7 +85,7 @@ function DraftPreview({ object }) {
|
||||
startY={0}
|
||||
>
|
||||
<StyledPattern
|
||||
className={`pattern preview-${objectId}`}
|
||||
className={`pattern preview-${this.props.object._id}`}
|
||||
style={{
|
||||
width: '2000px',
|
||||
height: '1000px',
|
||||
@ -100,6 +98,15 @@ function DraftPreview({ object }) {
|
||||
</StyledPattern>
|
||||
</ElementPan>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DraftPreview;
|
||||
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;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import utils from 'utils/utils';
|
||||
|
||||
@ -12,17 +12,23 @@ const StyledDrawdown = styled.canvas`
|
||||
width: ${props => props.warp.threads * props.baseSize}px;
|
||||
`;
|
||||
|
||||
// Cache
|
||||
const squares = {};
|
||||
class Drawdown extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.squares = {};
|
||||
}
|
||||
|
||||
function Drawdown({ baseSize, warp, weft, tieups }) {
|
||||
const drawdownRef = useRef();
|
||||
useEffect(() => paintDrawdown());
|
||||
const { editor } = useSelector(state => ({ editor: state.objects.editor }));
|
||||
componentDidMount() {
|
||||
this.paintDrawdown();
|
||||
}
|
||||
|
||||
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];
|
||||
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 m_canvas = document.createElement('canvas');
|
||||
m_canvas.width = size;
|
||||
m_canvas.height = size;
|
||||
@ -37,7 +43,7 @@ function Drawdown({ baseSize, warp, weft, tieups }) {
|
||||
if (view === 'colour' || view === 'interlacement') {
|
||||
mc.fillStyle = colour;
|
||||
mc.fillRect(0, 0, size, size);
|
||||
if (editor.view === 'interlacement') {
|
||||
if (this.props.editor.view === 'interlacement') {
|
||||
if (thread === 'warp') {
|
||||
const grd = mc.createLinearGradient(0, 0, size, 0);
|
||||
grd.addColorStop(0.1, 'rgba(0,0,0,0.3)');
|
||||
@ -57,16 +63,19 @@ function Drawdown({ baseSize, warp, weft, tieups }) {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
return m_canvas;
|
||||
};
|
||||
}
|
||||
|
||||
const paintDrawdown = () => {
|
||||
const canvas = drawdownRef.current;
|
||||
paintDrawdown(prevProps) {
|
||||
const canvas = this.refs.drawdown;
|
||||
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++) {
|
||||
@ -74,25 +83,40 @@ function Drawdown({ baseSize, warp, weft, tieups }) {
|
||||
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 = getSquare(threadType, baseSize, threadType === 'warp' ? warpColour : weftColour);
|
||||
const square = this.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={drawdownRef} className="drawdown joyride-drawdown"
|
||||
<StyledDrawdown ref="drawdown" className="drawdown joyride-drawdown"
|
||||
width={warp.threads * baseSize}
|
||||
height={weft.threads * baseSize}
|
||||
weft={weft} warp={warp} baseSize={baseSize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Drawdown;
|
||||
const mapStateToProps = (state, ownProps) => ({ editor: state.objects.editor });
|
||||
const DrawdownContainer = connect(
|
||||
mapStateToProps,
|
||||
)(Drawdown);
|
||||
|
||||
export default DrawdownContainer;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledTieups = styled.canvas`
|
||||
@ -7,40 +7,46 @@ const StyledTieups = styled.canvas`
|
||||
right:10px;
|
||||
`;
|
||||
|
||||
function Tieups({ cellStyle, warp, weft, tieups, updatePattern, baseSize }) {
|
||||
useEffect(() => paintTieups());
|
||||
const tieupRef = useRef(null);
|
||||
class Tieups extends Component {
|
||||
|
||||
const fillUpTo = (t, limit) => {
|
||||
let i = t.length;
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
this.paintTieups();
|
||||
}
|
||||
componentDidMount() {
|
||||
this.paintTieups();
|
||||
}
|
||||
|
||||
fillUpTo = (tieups, limit) => {
|
||||
let i = tieups.length;
|
||||
while (i <= limit) {
|
||||
t.push([]);
|
||||
tieups.push([]);
|
||||
i++;
|
||||
}
|
||||
};
|
||||
const getTieupShaft = (event) => {
|
||||
}
|
||||
getTieupShaft = (event) => {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const y = event.clientY - rect.top;
|
||||
const x = 0 - (event.clientX - rect.right);
|
||||
const shaft = warp.shafts - parseInt(y / baseSize);
|
||||
const tieup = weft.treadles - parseInt(x / baseSize) - 1;
|
||||
const shaft = this.props.warp.shafts - parseInt(y / this.props.baseSize);
|
||||
const tieup = this.props.weft.treadles - parseInt(x / this.props.baseSize) - 1;
|
||||
return { tieup, shaft };
|
||||
};
|
||||
const click = (event) => {
|
||||
const { tieup, shaft } = getTieupShaft(event);
|
||||
const newTieups = Object.assign([], tieups);
|
||||
|
||||
if (tieup >= tieups.length) fillUpTo(newTieups, tieup);
|
||||
if (tieups[tieup] !== undefined) {
|
||||
if (tieups[tieup].indexOf(shaft) === -1) newTieups[tieup].push(shaft);
|
||||
else newTieups[tieup].splice(tieups[tieup].indexOf(shaft));
|
||||
}
|
||||
updatePattern({ tieups: newTieups });
|
||||
};
|
||||
click = (event) => {
|
||||
const { tieup, shaft } = this.getTieupShaft(event);
|
||||
const tieups = Object.assign([], this.props.tieups);
|
||||
|
||||
const paintTieups = () => {
|
||||
const canvas = tieupRef.current;
|
||||
if (tieup >= tieups.length) this.fillUpTo(tieups, 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 });
|
||||
}
|
||||
|
||||
paintTieups() {
|
||||
const canvas = this.refs.tieups;
|
||||
const ctx = canvas.getContext('2d');// , { alpha: false });
|
||||
const { baseSize, tieups } = this.props;
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
@ -62,9 +68,12 @@ function Tieups({ cellStyle, warp, weft, tieups, updatePattern, baseSize }) {
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { warp, weft, baseSize } = this.props;
|
||||
return (
|
||||
<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}/>
|
||||
<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}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Tieups;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Confirm, Select, Segment, Accordion, Grid, Icon, Input, Button,
|
||||
} from 'semantic-ui-react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import Slider from 'rc-slider';
|
||||
import styled from 'styled-components';
|
||||
@ -35,78 +35,69 @@ const ColourSquare = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
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();
|
||||
class Tools extends Component {
|
||||
state = { colours: [], activeDrawers: ['properties', 'drawing', 'palette'], view: 'interlacement' }
|
||||
|
||||
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 };
|
||||
});
|
||||
enableTool = (tool) => {
|
||||
this.props.onEditorUpdated({ tool, colour: this.props.editor.colour });
|
||||
}
|
||||
|
||||
const enableTool = (tool) => {
|
||||
dispatch(actions.objects.updateEditor({ tool, colour: editor.colour }));
|
||||
};
|
||||
setColour = (colour) => {
|
||||
this.props.onEditorUpdated({ tool: 'colour', colour });
|
||||
}
|
||||
|
||||
const setColour = (colour) => {
|
||||
dispatch(actions.objects.updateEditor({ tool: 'colour', colour }));
|
||||
};
|
||||
setView = (view) => {
|
||||
this.props.onEditorUpdated({ view });
|
||||
}
|
||||
|
||||
const setEditorView = (view) => {
|
||||
dispatch(actions.objects.updateEditor({ view }));
|
||||
};
|
||||
setName = (event) => {
|
||||
this.props.updateObject({ name: event.target.value });
|
||||
}
|
||||
|
||||
const setName = (event) => {
|
||||
updateObject({ name: event.target.value });
|
||||
};
|
||||
setShafts = (event) => {
|
||||
const warp = { ...this.props.warp, shafts: parseInt(event.target.value, 10) || 1 };
|
||||
this.props.updatePattern({ warp });
|
||||
}
|
||||
|
||||
const setShafts = (event) => {
|
||||
updatePattern({ warp: { ...warp, shafts: parseInt(event.target.value, 10) || 1 } });
|
||||
};
|
||||
setTreadles = (event) => {
|
||||
const weft = { ...this.props.weft, treadles: parseInt(event.target.value, 10) || 1 };
|
||||
this.props.updatePattern({ weft });
|
||||
}
|
||||
|
||||
const setTreadles = (event) => {
|
||||
updatePattern({ weft: { ...weft, treadles: parseInt(event.target.value, 10) || 1 } });
|
||||
};
|
||||
onZoomChange = zoom => this.props.updatePattern({ baseSize: zoom || 10 })
|
||||
|
||||
const onZoomChange = zoom => updatePattern({ baseSize: zoom || 10 });
|
||||
drawerIsActive = drawer => this.state.activeDrawers.indexOf(drawer) > -1
|
||||
|
||||
const drawerIsActive = drawer => activeDrawers.indexOf(drawer) > -1;
|
||||
|
||||
const activateDrawer = (drawer) => {
|
||||
const index = activeDrawers.indexOf(drawer);
|
||||
const drawers = activeDrawers;
|
||||
activateDrawer = (drawer) => {
|
||||
const index = this.state.activeDrawers.indexOf(drawer);
|
||||
const drawers = this.state.activeDrawers;
|
||||
if (index === -1) {
|
||||
drawers.push(drawer);
|
||||
} else {
|
||||
drawers.splice(index, 1);
|
||||
}
|
||||
setActiveDrawers(drawers);
|
||||
};
|
||||
this.setState({ activeDrawers: drawers });
|
||||
}
|
||||
|
||||
const deleteObject = () => {
|
||||
api.objects.delete(objectId, () => {
|
||||
deleteObject = () => {
|
||||
api.objects.delete(this.props.match.params.objectId, () => {
|
||||
toast('🗑️ Pattern deleted');
|
||||
dispatch(actions.objects.delete(objectId));
|
||||
navigate(`/${project.fullName}`);
|
||||
this.props.onObjectDeleted(this.props.match.params.objectId);
|
||||
this.props.history.push(`/${this.props.project.fullName}`);
|
||||
}, err => console.log(err));
|
||||
}
|
||||
|
||||
const revertChanges = () => {
|
||||
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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const changeWidth = () => {
|
||||
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) {
|
||||
@ -119,13 +110,14 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
|
||||
warp.threading.splice(newWidth);
|
||||
warp.threads = warp.threading.length;
|
||||
}
|
||||
updatePattern({ warp });
|
||||
dispatch(actions.objects.updateEditor());
|
||||
dispatch(actions.objects.updateEditor( { tool: 'pan' }));
|
||||
};
|
||||
const changeHeight = () => {
|
||||
this.props.updatePattern({ warp });
|
||||
this.props.onEditorUpdated();
|
||||
this.props.onEditorUpdated({ tool: 'pan' });
|
||||
}
|
||||
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) {
|
||||
@ -138,30 +130,32 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
|
||||
weft.treadling.splice(newHeight);
|
||||
weft.threads = weft.treadling.length;
|
||||
}
|
||||
updatePattern({ weft });
|
||||
dispatch(actions.objects.updateEditor());
|
||||
};
|
||||
this.props.updatePattern({ weft });
|
||||
this.props.onEditorUpdated();
|
||||
}
|
||||
|
||||
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={() => saveObject(/*this.refs.canvas*/)} loading={saving}/>
|
||||
<Button style={{marginTop: 5}} fluid icon='refresh' content='Undo changes' onClick={revertChanges} />
|
||||
<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} />
|
||||
</Segment>
|
||||
}
|
||||
<Segment attached>
|
||||
<Accordion fluid>
|
||||
<Accordion.Title active={drawerIsActive('view')} onClick={e => activateDrawer('view')}>
|
||||
<Accordion.Title active={this.drawerIsActive('view')} onClick={e => this.activateDrawer('view')}>
|
||||
<Icon name="dropdown" /> View
|
||||
</Accordion.Title>
|
||||
<Accordion.Content active={drawerIsActive('view')}>
|
||||
<Accordion.Content active={this.drawerIsActive('view')}>
|
||||
<small>Drawdown view</small>
|
||||
<Select
|
||||
size="tiny"
|
||||
fluid
|
||||
value={editor.view}
|
||||
onChange={(e, s) => setEditorView(s.value)}
|
||||
onChange={(e, s) => this.setView(s.value)}
|
||||
style={{ fontSize: '11px' }}
|
||||
options={[
|
||||
{ key: 1, value: 'interlacement', text: 'Interlacement' },
|
||||
@ -172,81 +166,98 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
|
||||
/>
|
||||
<div style={{ marginTop: '5px' }} />
|
||||
<small>Zoom</small>
|
||||
<Slider defaultValue={baseSize} min={5} max={13} step={1} onAfterChange={onZoomChange} />
|
||||
<Slider defaultValue={this.props.baseSize} min={5} max={13} step={1} onAfterChange={this.onZoomChange} />
|
||||
</Accordion.Content>
|
||||
|
||||
<Accordion.Title active={drawerIsActive('properties')} onClick={e => activateDrawer('properties')}>
|
||||
<Accordion.Title active={this.drawerIsActive('properties')} onClick={e => this.activateDrawer('properties')}>
|
||||
<Icon name="dropdown" /> Properties
|
||||
</Accordion.Title>
|
||||
<Accordion.Content active={drawerIsActive('properties')}>
|
||||
<Accordion.Content active={this.drawerIsActive('properties')}>
|
||||
<small>Name</small>
|
||||
<Input type="text" size="small" fluid style={{ marginBottom: '5px' }} value={object.name} onChange={setName} />
|
||||
<Input type="text" size="small" fluid style={{ marginBottom: '5px' }} value={this.props.object.name} onChange={this.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={setShafts} size="mini" />
|
||||
<Input fluid type="number" value={warp.shafts} onKeyDown={e => false} onChange={this.setShafts} size="mini" />
|
||||
</Grid.Column>
|
||||
<Grid.Column>
|
||||
<small>Treadles</small>
|
||||
<Input fluid type="number" value={weft.treadles} onKeyDown={e => false} onChange={setTreadles} size="mini" />
|
||||
<Input fluid type="number" value={weft.treadles} onKeyDown={e => false} onChange={this.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: changeWidth}}
|
||||
action={{icon: 'edit', onClick: this.changeWidth}}
|
||||
/>
|
||||
</Grid.Column>
|
||||
<Grid.Column>
|
||||
<small>Height</small>
|
||||
<Input fluid readOnly value={weft.treadling?.length || 0} size="mini"
|
||||
action={{icon: 'edit', onClick: changeHeight}}
|
||||
action={{icon: 'edit', onClick: this.changeHeight}}
|
||||
/>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Accordion.Content>
|
||||
|
||||
<Accordion.Title active={drawerIsActive('drawing')} onClick={e => activateDrawer('drawing')}>
|
||||
<Accordion.Title active={this.drawerIsActive('drawing')} onClick={e => this.activateDrawer('drawing')}>
|
||||
<Icon name="dropdown" /> Tools
|
||||
</Accordion.Title>
|
||||
<Accordion.Content active={drawerIsActive('drawing')}>
|
||||
<Accordion.Content active={this.drawerIsActive('drawing')}>
|
||||
<Button.Group fluid>
|
||||
<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 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.Group>
|
||||
</Accordion.Content>
|
||||
|
||||
<Accordion.Title active={drawerIsActive('palette')} onClick={e => activateDrawer('palette')}>
|
||||
<Accordion.Title active={this.drawerIsActive('palette')} onClick={e => this.activateDrawer('palette')}>
|
||||
<Icon name="dropdown" /> Palette
|
||||
<ColourSquare colour={utils.rgb(editor.colour)} style={{top: 4, marginLeft: 10}}/>
|
||||
<ColourSquare colour={utils.rgb(this.props.editor.colour)} style={{top: 4, marginLeft: 10}}/>
|
||||
</Accordion.Title>
|
||||
<Accordion.Content active={drawerIsActive('palette')}>
|
||||
{pattern.colours && pattern.colours.map(colour =>
|
||||
<ColourSquare key={colour} colour={utils.rgb(colour)} onClick={() => setColour(colour)} />
|
||||
<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>
|
||||
|
||||
<Accordion.Title active={drawerIsActive('advanced')} onClick={e => activateDrawer('advanced')}>
|
||||
<Accordion.Title active={this.drawerIsActive('advanced')} onClick={e => this.activateDrawer('advanced')}>
|
||||
<Icon name="dropdown" /> Advanced
|
||||
</Accordion.Title>
|
||||
<Accordion.Content active={drawerIsActive('advanced')}>
|
||||
<Button size="small" basic color="red" fluid onClick={e => setDeleteModalOpen(true)}>Delete pattern</Button>
|
||||
<Accordion.Content active={this.drawerIsActive('advanced')}>
|
||||
<Button size="small" basic color="red" fluid onClick={e => this.setState({ deleteModalOpen: true })}>Delete pattern</Button>
|
||||
<Confirm
|
||||
open={deleteModalOpen}
|
||||
open={this.state.deleteModalOpen}
|
||||
content="Really delete this pattern?"
|
||||
onCancel={e => setDeleteModalOpen(false)}
|
||||
onConfirm={deleteObject}
|
||||
onCancel={e => this.setState({ deleteModalOpen: false })}
|
||||
onConfirm={this.deleteObject}
|
||||
/>
|
||||
</Accordion.Content>
|
||||
</Accordion>
|
||||
</Segment>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Tools;
|
||||
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;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import utils from 'utils/utils.js';
|
||||
|
||||
@ -17,154 +17,157 @@ const StyledWarp = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const squares = {};
|
||||
const markers = {};
|
||||
class Warp extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.squares = {};
|
||||
this.markers = {};
|
||||
}
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
this.paintDrawdown();
|
||||
}
|
||||
componentDidMount() {
|
||||
this.paintDrawdown();
|
||||
}
|
||||
|
||||
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) => {
|
||||
getThreadShaft = (event) => {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const y = event.clientY - rect.top;
|
||||
const x = 0 - (event.clientX - rect.right);
|
||||
const shaft = warp.shafts - parseInt(y / baseSize);
|
||||
const thread = parseInt(x / baseSize);
|
||||
const shaft = this.props.warp.shafts - parseInt(y / this.props.baseSize);
|
||||
const thread = parseInt(x / this.props.baseSize);
|
||||
return { shaft, thread };
|
||||
};
|
||||
|
||||
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();
|
||||
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 });
|
||||
}
|
||||
};
|
||||
|
||||
const mouseUp = event => setDragging(false);
|
||||
const mouseDown = (event) => {
|
||||
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 => {
|
||||
event.preventDefault();
|
||||
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);
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
let lX = startThread; let hX = thread; let lY = startShaft; let hY = shaft;
|
||||
let xDirection = 1; let yDirection = 1;
|
||||
if (thread < startThread) {
|
||||
mouseUp = event => this.dragging = false;
|
||||
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);
|
||||
|
||||
let lX = this.startThread; let hX = thread; let lY = this.startShaft; let
|
||||
hY = shaft;
|
||||
let xDirection = 1; let
|
||||
yDirection = 1;
|
||||
if (thread < this.startThread) {
|
||||
lX = thread;
|
||||
hX = startThread;
|
||||
hX = this.startThread;
|
||||
xDirection = -1;
|
||||
}
|
||||
if (shaft < startShaft) {
|
||||
if (shaft < this.startShaft) {
|
||||
lY = shaft;
|
||||
hY = startShaft;
|
||||
hY = this.startShaft;
|
||||
yDirection = -1;
|
||||
}
|
||||
|
||||
let x = xDirection > 0 ? lX : hX;
|
||||
let y = yDirection > 0 ? lY : hY;
|
||||
if (editor.tool === 'colour') {
|
||||
if (thread >= warp.threading.length) fillUpTo(newWarp, thread);
|
||||
newWarp.threading[thread].colour = editor.colour;
|
||||
if (this.props.tool === 'colour') {
|
||||
if (thread >= warp.threading.length) this.fillUpTo(warp, thread);
|
||||
warp.threading[thread].colour = this.props.colour;
|
||||
}
|
||||
if (editor.tool === 'straight') {
|
||||
if (this.props.tool === 'straight') {
|
||||
while (x <= hX && x >= lX) {
|
||||
if (x >= warp.threading.length || warp.threading.length - x < 5) fillUpTo(newWarp, x + 5);
|
||||
newWarp.threading[x].shaft = y;
|
||||
if (x >= warp.threading.length || warp.threading.length - x < 5) this.fillUpTo(warp, x + 5);
|
||||
warp.threading[x].shaft = y;
|
||||
x += xDirection;
|
||||
y += yDirection;
|
||||
if (y > hY || y < lY) y = yDirection > 0 ? lY : hY;
|
||||
}
|
||||
}
|
||||
if (editor.tool === 'point') {
|
||||
if (this.props.tool === 'point') {
|
||||
while (x <= hX && x >= lX) {
|
||||
if (x >= warp.threading.length || warp.threading.length - x < 5) fillUpTo(newWarp, x + 5);
|
||||
newWarp.threading[x].shaft = y;
|
||||
if (x >= warp.threading.length || warp.threading.length - x < 5) this.fillUpTo(warp, x + 5);
|
||||
warp.threading[x].shaft = y;
|
||||
x += xDirection;
|
||||
y += yDirection;
|
||||
if (y > hY || y <= lY) yDirection = 0 - yDirection;
|
||||
}
|
||||
}
|
||||
updatePattern({ warp: newWarp });
|
||||
this.props.updatePattern({ warp });
|
||||
}
|
||||
};
|
||||
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];
|
||||
}
|
||||
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];
|
||||
warpThread.shaft = warpThread.shaft === shaft ? 0 : shaft;
|
||||
updatePattern({ warp: newWarp });
|
||||
this.props.updatePattern({ warp });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fillUpTo = (w, limit) => {
|
||||
fillUpTo = (warp, limit) => {
|
||||
let i = warp.threading.length;
|
||||
while (i <= limit) {
|
||||
w.threading.push({ shaft: 0 });
|
||||
w.threads++;
|
||||
warp.threading.push({ shaft: 0 });
|
||||
warp.threads++;
|
||||
i++;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const getMarker = (size) => {
|
||||
if (markers[size]) return markers[size];
|
||||
|
||||
getMarker(size) {
|
||||
if (this.markers[size]) return this.markers[size];
|
||||
const m_canvas = document.createElement('canvas');
|
||||
m_canvas.width = baseSize;
|
||||
m_canvas.height = baseSize;
|
||||
m_canvas.width = this.props.baseSize;
|
||||
m_canvas.height = this.props.baseSize;
|
||||
const mc = m_canvas.getContext('2d');
|
||||
mc.fillStyle = 'black';
|
||||
mc.fillRect(0, 0, baseSize, baseSize);
|
||||
markers[size] = m_canvas;
|
||||
mc.fillRect(0, 0, this.props.baseSize, this.props.baseSize);
|
||||
this.markers[size] = m_canvas;
|
||||
return m_canvas;
|
||||
};
|
||||
}
|
||||
|
||||
const getSquare = (size, colour) => {
|
||||
if (squares[size] && squares[size][colour]) return squares[size][colour];
|
||||
getSquare(size, colour) {
|
||||
if (this.squares[size] && this.squares[size][colour]) return this.squares[size][colour];
|
||||
const m_canvas = document.createElement('canvas');
|
||||
m_canvas.width = baseSize;
|
||||
m_canvas.width = this.props.baseSize;
|
||||
m_canvas.height = 10;
|
||||
const mc = m_canvas.getContext('2d');
|
||||
mc.fillStyle = utils.rgb(colour);
|
||||
mc.fillRect(0, 0, baseSize, 10);
|
||||
if (!squares[size]) squares[size] = {};
|
||||
squares[size][colour] = m_canvas;
|
||||
mc.fillRect(0, 0, this.props.baseSize, 10);
|
||||
if (!this.squares[size]) this.squares[size] = {};
|
||||
this.squares[size][colour] = m_canvas;
|
||||
return m_canvas;
|
||||
};
|
||||
}
|
||||
|
||||
const paintDrawdown = () => {
|
||||
const canvas = warpRef.current;
|
||||
const colourway = colourwayRef.current;
|
||||
paintDrawdown() {
|
||||
const canvas = this.refs.warp;
|
||||
const colourway = this.refs.colourway;
|
||||
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);
|
||||
|
||||
@ -181,40 +184,52 @@ function Warp({ baseSize, cellStyle, warp, weft, updatePattern }) {
|
||||
|
||||
for (let thread = 0; thread < warp.threads; thread++) {
|
||||
const shaft = warp.threading[thread].shaft;
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
render() {
|
||||
const { warp, weft, baseSize } = this.props;
|
||||
return (
|
||||
<StyledWarp treadles={weft.treadles} shafts={warp.shafts} baseSize={baseSize}>
|
||||
<canvas className='warp-colourway joyride-warpColourway' ref={colourwayRef} width={warp.threading.length * baseSize} height={10}
|
||||
<canvas className='warp-colourway joyride-warpColourway' ref="colourway" width={warp.threading.length * baseSize} height={10}
|
||||
style={{
|
||||
position: 'absolute', top: 0, right: 0, height: 10, width: warp.threading.length * baseSize,
|
||||
}}
|
||||
onClick={mouseClickColourway}
|
||||
onMouseDown={mouseDownColourway}
|
||||
onMouseMove={mouseMoveColourway}
|
||||
onMouseUp={mouseUpColourway}
|
||||
onMouseLeave={mouseUpColourway}
|
||||
onClick={this.mouseClickColourway}
|
||||
onMouseDown={this.mouseDownColourway}
|
||||
onMouseMove={this.mouseMoveColourway}
|
||||
onMouseUp={this.mouseUpColourway}
|
||||
onMouseLeave={this.mouseUpColourway}
|
||||
/>
|
||||
<canvas className='warp-threads joyride-warp' ref={warpRef} width={warp.threading.length * baseSize} height={warp.shafts * baseSize}
|
||||
<canvas className='warp-threads joyride-warp' ref="warp" 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={click}
|
||||
onMouseDown={mouseDown}
|
||||
onMouseMove={mouseMove}
|
||||
onMouseUp={mouseUp}
|
||||
onMouseLeave={mouseUp}
|
||||
onClick={this.click}
|
||||
onMouseDown={this.mouseDown}
|
||||
onMouseMove={this.mouseMove}
|
||||
onMouseUp={this.mouseUp}
|
||||
onMouseLeave={this.mouseUp}
|
||||
/>
|
||||
</StyledWarp>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Warp;
|
||||
const mapStateToProps = (state, ownProps) => state.objects.editor;
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
});
|
||||
const WarpContainer = connect(
|
||||
mapStateToProps, mapDispatchToProps,
|
||||
)(Warp);
|
||||
|
||||
export default WarpContainer;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { connect } from 'react-redux';
|
||||
import utils from 'utils/utils.js';
|
||||
|
||||
const StyledWeft = styled.div`
|
||||
@ -18,156 +18,172 @@ const StyledWeft = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
// Cache
|
||||
const squares = {};
|
||||
const markers = {};
|
||||
class Weft extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.squares = {};
|
||||
this.markers = {};
|
||||
}
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
this.paintDrawdown();
|
||||
}
|
||||
componentDidMount() {
|
||||
this.paintDrawdown();
|
||||
}
|
||||
|
||||
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();
|
||||
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 });
|
||||
}
|
||||
|
||||
const { editor } = useSelector(state => ({ editor: state.objects.editor }));
|
||||
useEffect(() => paintDrawdown());
|
||||
const weftRef = useRef(null);
|
||||
const colourwayRef = useRef(null);
|
||||
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 getThreadTreadle = (event) => {
|
||||
getThreadTreadle = (event) => {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const y = event.clientY - rect.top;
|
||||
const x = (event.clientX - rect.left);
|
||||
const thread = parseInt(y / baseSize) + 1;
|
||||
const treadle = parseInt(x / baseSize);
|
||||
const thread = parseInt(y / this.props.baseSize) + 1;
|
||||
const treadle = parseInt(x / this.props.baseSize);
|
||||
return { treadle, thread };
|
||||
};
|
||||
|
||||
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();
|
||||
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 });
|
||||
}
|
||||
};
|
||||
|
||||
const mouseUp = event => setDragging(false);
|
||||
const mouseDown = (event) => {
|
||||
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 => {
|
||||
event.preventDefault();
|
||||
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);
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
let lX = startTreadle; let hX = treadle; let lY = startThread; let hY = thread;
|
||||
mouseUp = event => this.dragging = false;
|
||||
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);
|
||||
|
||||
let lX = this.startTreadle; let hX = treadle; let lY = this.startThread; let
|
||||
hY = thread;
|
||||
let xDirection = 1; let
|
||||
yDirection = 1;
|
||||
if (treadle < startTreadle) {
|
||||
if (treadle < this.startTreadle) {
|
||||
lX = treadle;
|
||||
hX = startTreadle;
|
||||
hX = this.startTreadle;
|
||||
xDirection = -1;
|
||||
}
|
||||
if (thread < startThread) {
|
||||
if (thread < this.startThread) {
|
||||
lY = thread;
|
||||
hY = startThread;
|
||||
hY = this.startThread;
|
||||
yDirection = -1;
|
||||
}
|
||||
let x = xDirection > 0 ? lX : hX;
|
||||
let y = yDirection > 0 ? lY : hY;
|
||||
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 === 'colour') {
|
||||
if ((thread - 1) >= weft.treadling.length) this.fillUpTo(weft, (thread - 1));
|
||||
weft.treadling[thread - 1].colour = this.props.colour;
|
||||
}
|
||||
if (editor.tool === 'straight') {
|
||||
if (this.props.tool === 'straight') {
|
||||
while (y <= hY && y >= lY) {
|
||||
if ((y - 1) >= weft.treadling.length || weft.treadling.length - y - 1 < 5) fillUpTo(newWeft, (y + 5));
|
||||
newWeft.treadling[y - 1].treadle = x + 1;
|
||||
if ((y - 1) >= weft.treadling.length || weft.treadling.length - y - 1 < 5) this.fillUpTo(weft, (y + 5));
|
||||
weft.treadling[y - 1].treadle = x + 1;
|
||||
x += xDirection;
|
||||
y += yDirection;
|
||||
if (x > hX || x < lX) x = xDirection > 0 ? lX : hX;
|
||||
}
|
||||
}
|
||||
if (editor.tool === 'point') {
|
||||
if (this.props.tool === 'point') {
|
||||
while (y <= hY && y >= lY) {
|
||||
if ((y - 1) >= weft.treadling.length || weft.treadling.length - y -1 < 5) fillUpTo(newWeft, y + 5);
|
||||
newWeft.treadling[y - 1].treadle = x + 1;
|
||||
if ((y - 1) >= weft.treadling.length || weft.treadling.length - y -1 < 5) this.fillUpTo(weft, y + 5);
|
||||
weft.treadling[y - 1].treadle = x + 1;
|
||||
x += xDirection;
|
||||
y += yDirection;
|
||||
if (x > hX || x <= lX) xDirection = 0 - xDirection;
|
||||
}
|
||||
}
|
||||
updatePattern({ weft: newWeft });
|
||||
this.props.updatePattern({ weft });
|
||||
}
|
||||
};
|
||||
const click = (event) => {
|
||||
if (editor.tool === 'point' || editor.tool === 'straight') {
|
||||
let { thread, treadle } = getThreadTreadle(event);
|
||||
}
|
||||
click = (event) => {
|
||||
if (this.props.tool === 'point' || this.props.tool === 'straight') {
|
||||
let { thread, treadle } = this.getThreadTreadle(event);
|
||||
treadle += 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];
|
||||
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];
|
||||
weftThread.treadle = weftThread.treadle === treadle ? 0 : treadle;
|
||||
updatePattern({ weft: newWeft });
|
||||
this.props.updatePattern({ weft });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fillUpTo = (weft, limit) => {
|
||||
fillUpTo = (weft, limit) => {
|
||||
let i = weft.treadling.length;
|
||||
while (i <= limit) {
|
||||
weft.treadling.push({ treadle: 0 });
|
||||
weft.threads++;
|
||||
i++;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const getMarker = (size) => {
|
||||
if (markers[size]) return markers[size];
|
||||
getMarker(size) {
|
||||
if (this.markers[size]) return this.markers[size];
|
||||
const m_canvas = document.createElement('canvas');
|
||||
m_canvas.width = baseSize;
|
||||
m_canvas.height = baseSize;
|
||||
m_canvas.width = this.props.baseSize;
|
||||
m_canvas.height = this.props.baseSize;
|
||||
const mc = m_canvas.getContext('2d');
|
||||
mc.fillStyle = 'black';
|
||||
mc.fillRect(0, 0, baseSize, baseSize);
|
||||
markers[size] = m_canvas;
|
||||
mc.fillRect(0, 0, this.props.baseSize, this.props.baseSize);
|
||||
this.markers[size] = m_canvas;
|
||||
return m_canvas;
|
||||
};
|
||||
}
|
||||
|
||||
const getSquare = (size, colour) => {
|
||||
if (squares[size] && squares[size][colour]) return squares[size][colour];
|
||||
getSquare(size, colour) {
|
||||
if (this.squares[size] && this.squares[size][colour]) return this.squares[size][colour];
|
||||
const m_canvas = document.createElement('canvas');
|
||||
m_canvas.width = 10;
|
||||
m_canvas.height = baseSize;
|
||||
m_canvas.height = this.props.baseSize;
|
||||
const mc = m_canvas.getContext('2d');
|
||||
mc.fillStyle = utils.rgb(colour);
|
||||
mc.fillRect(0, 0, 10, baseSize);
|
||||
if (!squares[size]) squares[size] = {};
|
||||
squares[size][colour] = m_canvas;
|
||||
mc.fillRect(0, 0, 10, this.props.baseSize);
|
||||
if (!this.squares[size]) this.squares[size] = {};
|
||||
this.squares[size][colour] = m_canvas;
|
||||
return m_canvas;
|
||||
};
|
||||
}
|
||||
|
||||
const paintDrawdown = () => {
|
||||
const canvas = weftRef.current;
|
||||
const colourway = colourwayRef.current;
|
||||
paintDrawdown() {
|
||||
const canvas = this.refs.weft;
|
||||
const colourway = this.refs.colourway;
|
||||
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);
|
||||
|
||||
@ -184,37 +200,49 @@ function Weft({ cellStyle, warp, weft, baseSize, updatePattern }) {
|
||||
|
||||
for (let thread = 0; thread < weft.threads; thread++) {
|
||||
const treadle = weft.treadling[thread].treadle;
|
||||
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));
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
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={colourwayRef} width={10} height={weft.threads * baseSize}
|
||||
<canvas className='weft-colourway' ref="colourway" width={10} height={weft.threads * baseSize}
|
||||
style={{ position: 'absolute', top: 0, right: 0, width: 10, height: weft.threads * baseSize}}
|
||||
onClick={mouseClickColourway}
|
||||
onMouseDown={mouseDownColourway}
|
||||
onMouseMove={mouseMoveColourway}
|
||||
onMouseUp={mouseUpColourway}
|
||||
onMouseLeave={mouseUpColourway}
|
||||
onClick={this.mouseClickColourway}
|
||||
onMouseDown={this.mouseDownColourway}
|
||||
onMouseMove={this.mouseMoveColourway}
|
||||
onMouseUp={this.mouseUpColourway}
|
||||
onMouseLeave={this.mouseUpColourway}
|
||||
/>
|
||||
<canvas className='weft-threads joyride-weft' ref={weftRef} width={weft.treadles * baseSize} height={weft.threads * baseSize}
|
||||
<canvas className='weft-threads joyride-weft' ref="weft" 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={click}
|
||||
onMouseDown={mouseDown}
|
||||
onMouseMove={mouseMove}
|
||||
onMouseUp={mouseUp}
|
||||
onMouseLeave={mouseUp}
|
||||
onClick={this.click}
|
||||
onMouseDown={this.mouseDown}
|
||||
onMouseMove={this.mouseMove}
|
||||
onMouseUp={this.mouseUp}
|
||||
onMouseLeave={this.mouseUp}
|
||||
/>
|
||||
</StyledWeft>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Weft;
|
||||
const mapStateToProps = (state, ownProps) => state.objects.editor;
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
});
|
||||
const WeftContainer = connect(
|
||||
mapStateToProps, mapDispatchToProps,
|
||||
)(Weft);
|
||||
|
||||
export default WeftContainer;
|
||||
|
@ -1,18 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Table, Container } from 'semantic-ui-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import moment from 'moment';
|
||||
import api from 'api';
|
||||
|
||||
function Root() {
|
||||
function Root({ user }) {
|
||||
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}) => {
|
||||
@ -65,4 +60,15 @@ function Root() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Root;
|
||||
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;
|
||||
|
@ -1,29 +1,22 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Message, Form, Divider, Segment, Icon } from 'semantic-ui-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
|
||||
function AccountSettings() {
|
||||
function AccountSettings({ user, onLogout, history, onReceiveUser }) {
|
||||
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('');
|
||||
dispatch(actions.users.receive(Object.assign({}, user, { email: data.email })));
|
||||
onReceiveUser(Object.assign({}, user, { email: data.email }));
|
||||
}, err => toast.error(err.message));
|
||||
}
|
||||
|
||||
@ -39,8 +32,8 @@ function AccountSettings() {
|
||||
const confirm = window.confirm('Really delete your account?');
|
||||
if (!confirm) return;
|
||||
api.accounts.delete(deletePassword, () => {
|
||||
api.auth.logout(() => dispatch(actions.auth.logout()));
|
||||
navigate('/');
|
||||
api.auth.logout(onLogout);
|
||||
history.push('/');
|
||||
toast.info('Sorry to see you go');
|
||||
}, err => toast.error(err.message));
|
||||
}
|
||||
@ -88,4 +81,16 @@ function AccountSettings() {
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountSettings;
|
||||
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;
|
||||
|
214
web/src/components/main/settings/Billing.js
Normal file
214
web/src/components/main/settings/Billing.js
Normal file
@ -0,0 +1,214 @@
|
||||
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, { useState } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { Message, Input, Segment, Button } from 'semantic-ui-react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
|
||||
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 };
|
||||
});
|
||||
class IdentitySettings extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { newUsername: '' };
|
||||
}
|
||||
|
||||
const updateUsername = () => {
|
||||
api.users.update(user.username, { username: newUsername }, (user) => {
|
||||
dispatch(actions.users.updateUsername(user._id, newUsername));
|
||||
updateUsername = () => {
|
||||
api.users.update(this.props.user.username, { username: this.state.newUsername }, (user) => {
|
||||
this.props.onUpdateUsername(this.props.user._id, this.state.newUsername);
|
||||
toast.info('Username updated');
|
||||
setNewUsername('');
|
||||
this.setState({ newUsername: '' });
|
||||
}, err => toast.error(err.message));
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Segment raised color="blue">
|
||||
<h3>Username</h3>
|
||||
@ -33,12 +33,24 @@ function IdentitySettings() {
|
||||
<p>If you change your username, your old one will become available to be taken by somebody else.</p>
|
||||
</Message>
|
||||
<Input
|
||||
type="text" value={newUsername}
|
||||
onChange={e => setNewUsername(e.target.value)}
|
||||
action=<Button color="yellow" content="Set new username" onClick={updateUsername} />
|
||||
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} />
|
||||
/>
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default IdentitySettings;
|
||||
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;
|
||||
|
@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { Label, Table, Checkbox, Divider, Segment } from 'semantic-ui-react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
import utils from 'utils/utils.js';
|
||||
@ -14,22 +16,19 @@ const subs = [
|
||||
{ key: 'projects.commented', text: 'Someone comments on one of your projects'},
|
||||
];
|
||||
|
||||
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 };
|
||||
});
|
||||
class NotificationSettings extends Component {
|
||||
|
||||
const hasEmailSub = (key) => utils.hasSubscription(user, key);
|
||||
const toggleEmailSub = (key, enable) => {
|
||||
hasEmailSub = (key) => utils.hasSubscription(this.props.user, key);
|
||||
toggleEmailSub = (key, enable) => {
|
||||
const { user, onSubsUpdated } = this.props;
|
||||
if (enable)
|
||||
api.users.createEmailSubscription(user.username, key, ({ subscriptions }) => dispatch(actions.users.updateSubscriptions(user._id, subscriptions)), err => toast.error(err.message));
|
||||
api.users.createEmailSubscription(user.username, key, ({ subscriptions }) => onSubsUpdated(user._id, subscriptions), err => toast.error(err.message));
|
||||
else
|
||||
api.users.deleteEmailSubscription(user.username, key, ({ subscriptions }) => dispatch(actions.users.updateSubscriptions(user._id, subscriptions)), err => toast.error(err.message));
|
||||
};
|
||||
api.users.deleteEmailSubscription(user.username, key, ({ subscriptions }) => onSubsUpdated(user._id, subscriptions), err => toast.error(err.message));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { groups } = this.props;
|
||||
return (
|
||||
<Segment raised color="blue">
|
||||
<h3>Email preferences</h3>
|
||||
@ -53,19 +52,32 @@ function NotificationSettings() {
|
||||
{subs.map(s =>
|
||||
<Table.Row key={s.key}>
|
||||
<Table.Cell>{s.text}</Table.Cell>
|
||||
<Table.Cell><Checkbox toggle onChange={(e, c) => toggleEmailSub(s.key, c.checked)} checked={hasEmailSub(s.key)}/></Table.Cell>
|
||||
<Table.Cell><Checkbox toggle onChange={(e, c) => this.toggleEmailSub(s.key, c.checked)} checked={this.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) => toggleEmailSub(`groupFeed-${g._id}`, c.checked)} checked={hasEmailSub(`groupFeed-${g._id}`)}/></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.Row>
|
||||
)}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationSettings;
|
||||
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;
|
||||
|
@ -1,8 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Divider, Container, Grid, Menu } from 'semantic-ui-react';
|
||||
import { Outlet, NavLink } from 'react-router-dom';
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Divider, Container, Grid, Menu,
|
||||
} from 'semantic-ui-react';
|
||||
import { Link, Switch, Route } from 'react-router-dom';
|
||||
|
||||
function Settings() {
|
||||
import Identity from './Identity';
|
||||
import Notification from './Notification';
|
||||
import Billing from './Billing';
|
||||
import Account from './Account';
|
||||
|
||||
class Settings extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Container style={{ marginTop: '40px' }}>
|
||||
<h2>Manage your account</h2>
|
||||
@ -10,18 +18,26 @@ function Settings() {
|
||||
<Grid stackable>
|
||||
<Grid.Column computer={4}>
|
||||
<Menu fluid vertical tabular>
|
||||
<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.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>
|
||||
</Grid.Column>
|
||||
|
||||
<Grid.Column stretched width={12}>
|
||||
<Outlet />
|
||||
<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>
|
||||
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
|
@ -1,45 +1,39 @@
|
||||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Icon, Segment, Form, Button, Divider,
|
||||
} from 'semantic-ui-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import utils from 'utils/utils';
|
||||
import actions from 'actions';
|
||||
import api from 'api';
|
||||
|
||||
import FileChooser from 'components/includes/FileChooser';
|
||||
|
||||
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 };
|
||||
});
|
||||
class EditProfile extends Component {
|
||||
updatePicture = (avatar) => {
|
||||
api.users.update(this.props.profileUser.username, { avatar }, this.props.onReceiveUser);
|
||||
}
|
||||
|
||||
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));
|
||||
updateProfile = () => {
|
||||
const { bio, location, website } = this.props.profileUser;
|
||||
api.users.update(this.props.profileUser.username, { bio, location, website }, (user) => {
|
||||
this.props.onReceiveUser(user);
|
||||
toast.info('Profile saved');
|
||||
}, err => toast.error(err.message));
|
||||
};
|
||||
}
|
||||
|
||||
const updateSocial = () => {
|
||||
const { twitter, facebook, linkedIn, instagram } = profileUser;
|
||||
api.users.update(profileUser.username, { twitter, facebook, linkedIn, instagram }, (u) => {
|
||||
dispatch(actions.users.receive(u));
|
||||
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);
|
||||
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">
|
||||
@ -53,9 +47,9 @@ function EditProfile() {
|
||||
|
||||
<h5>Profile picture</h5>
|
||||
<FileChooser
|
||||
forType="user" forObject={profileUser}
|
||||
forType="user" for={profileUser}
|
||||
trigger=<Button basic color="yellow" icon="image" content="Choose an image" />
|
||||
accept="image/*" onComplete={f => updatePicture(f.storedName)}
|
||||
accept="image/*" onComplete={f => this.updatePicture(f.storedName)}
|
||||
/>
|
||||
<h4>Or choose one of ours:</h4>
|
||||
{utils.defaultAvatars().map(a => (
|
||||
@ -64,7 +58,7 @@ function EditProfile() {
|
||||
style={{
|
||||
width: 40, height: 40, margin: 4, cursor: 'pointer',
|
||||
}}
|
||||
onClick={e => updatePicture(a.key)}
|
||||
onClick={e => this.updatePicture(a.key)}
|
||||
/>
|
||||
))}
|
||||
<Divider hidden />
|
||||
@ -73,19 +67,19 @@ function EditProfile() {
|
||||
<Form.TextArea
|
||||
label="Bio" placeholder="Write a bit about yourself..."
|
||||
value={profileUser.bio}
|
||||
onChange={e => dispatch(actions.users.update(profileUser._id, { bio: e.target.value }))}
|
||||
onChange={e => onUserEdited(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 => dispatch(actions.users.update(profileUser._id, { location: e.target.value }))}
|
||||
onChange={e => onUserEdited(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 => dispatch(actions.users.update(profileUser._id, { website: e.target.value }))}
|
||||
onChange={e => onUserEdited(profileUser._id, { website: e.target.value })}
|
||||
/>
|
||||
<Form.Button color="yellow" content="Save profile" onClick={updateProfile} />
|
||||
<Form.Button color="yellow" content="Save profile" onClick={this.updateProfile} />
|
||||
</Form>
|
||||
</Segment>
|
||||
|
||||
@ -96,28 +90,44 @@ function EditProfile() {
|
||||
<Form.Input
|
||||
label="Twitter" placeholder="@username" icon="twitter" iconPosition="left"
|
||||
value={profileUser.twitter}
|
||||
onChange={e => dispatch(actions.users.update(profileUser._id, { twitter: e.target.value }))}
|
||||
onChange={e => onUserEdited(profileUser._id, { twitter: e.target.value })}
|
||||
/>
|
||||
<Form.Input
|
||||
label="Facebook" placeholder="username" icon="facebook" iconPosition="left"
|
||||
value={profileUser.facebook}
|
||||
onChange={e => dispatch(actions.users.update(profileUser._id, { facebook: e.target.value }))}
|
||||
onChange={e => onUserEdited(profileUser._id, { facebook: e.target.value })}
|
||||
/>
|
||||
<Form.Input
|
||||
label="Instagram" placeholder="username" icon="instagram" iconPosition="left"
|
||||
value={profileUser.instagram}
|
||||
onChange={e => dispatch(actions.users.update(profileUser._id, { instagram: e.target.value }))}
|
||||
onChange={e => onUserEdited(profileUser._id, { instagram: e.target.value })}
|
||||
/>
|
||||
<Form.Input
|
||||
label="LinkedIn" placeholder="username" icon="linkedin" iconPosition="left"
|
||||
value={profileUser.linkedIn}
|
||||
onChange={e => dispatch(actions.users.update(profileUser._id, { linkedIn: e.target.value }))}
|
||||
onChange={e => onUserEdited(profileUser._id, { linkedIn: e.target.value })}
|
||||
/>
|
||||
<Form.Button color="yellow" content="Save social profiles" onClick={updateSocial} />
|
||||
<Form.Button color="yellow" content="Save social profiles" onClick={this.updateSocial} />
|
||||
</Form>
|
||||
</Segment>
|
||||
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
export default EditProfile;
|
||||
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;
|
||||
|
@ -1,44 +1,48 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Loader, Icon, List, Container, Card, Grid, Message } from 'semantic-ui-react';
|
||||
import { Link, Outlet, useParams } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link, Switch, Route } from 'react-router-dom';
|
||||
import { connect } 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';
|
||||
|
||||
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 };
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
api.users.get(username, user => {
|
||||
dispatch(actions.users.receive(user));
|
||||
setLoading(false);
|
||||
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 => {
|
||||
dispatch(actions.users.requestFailed(err));
|
||||
setLoading(false);
|
||||
this.props.onRequestFailed(err);
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
}, [dispatch, username])
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.match.params.username !== prevProps.match.params.username) {
|
||||
api.users.get(this.props.match.params.username, this.props.onReceiveUser);
|
||||
}
|
||||
}
|
||||
|
||||
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 {username}'s profile...</h4>
|
||||
<h4>Loading {this.props.match.params.username}'s profile...</h4>
|
||||
<Loader active inline="centered" />
|
||||
</div>
|
||||
}
|
||||
@ -162,13 +166,33 @@ function Profile() {
|
||||
</Grid.Column>
|
||||
|
||||
<Grid.Column computer={11}>
|
||||
<Outlet />
|
||||
<Switch>
|
||||
<Route path="/:username/edit" component={EditProfile} />
|
||||
<Route component={ProfileProjects} />
|
||||
</Switch>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Profile;
|
||||
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;
|
||||
|
@ -1,18 +1,12 @@
|
||||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { Icon, Divider, Card, Message } from 'semantic-ui-react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import ProjectCard from 'components/includes/ProjectCard';
|
||||
|
||||
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 };
|
||||
});
|
||||
|
||||
class ProfileProjects extends Component {
|
||||
render() {
|
||||
const { profileUser } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<h3><Icon name='book' /> {profileUser.username}'s projects</h3>
|
||||
@ -32,6 +26,17 @@ function ProfileProjects() {
|
||||
)
|
||||
}
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
export default ProfileProjects;
|
||||
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;
|
||||
|
93
web/src/components/marketing/Pricing.js
Normal file
93
web/src/components/marketing/Pricing.js
Normal file
@ -0,0 +1,93 @@
|
||||
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,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Container } from 'semantic-ui-react';
|
||||
|
||||
function PrivacyPolicy() {
|
||||
class PrivacyPolicy extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Container style={{ marginTop: 50, marginBottom: 50 }}>
|
||||
<Helmet title='Privacy Policy' />
|
||||
@ -71,6 +72,7 @@ function PrivacyPolicy() {
|
||||
<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,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Container } from 'semantic-ui-react';
|
||||
|
||||
function TermsOfUse() {
|
||||
class TermsOfUse extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Container style={{ marginTop: 50, marginBottom: 50 }}>
|
||||
<Helmet title='Terms of Use' />
|
||||
@ -37,6 +38,7 @@ function TermsOfUse() {
|
||||
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TermsOfUse;
|
||||
|
@ -1,17 +1,19 @@
|
||||
import 'react-app-polyfill/ie9';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { Switch, Route, Router } 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);
|
||||
@ -26,9 +28,12 @@ if (process.env.REACT_APP_SENTRY_DSN) {
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
<Router history={createBrowserHistory()}>
|
||||
<Switch>
|
||||
<Route path="/objects/:id/export" component={DraftExport} />
|
||||
<Route component={App} />
|
||||
</Switch>
|
||||
</Router>
|
||||
</Provider>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
|
689
web/yarn.lock
689
web/yarn.lock
@ -1110,20 +1110,13 @@
|
||||
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.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.5.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"
|
||||
@ -1636,13 +1629,6 @@
|
||||
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"
|
||||
@ -1656,21 +1642,6 @@
|
||||
"@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"
|
||||
@ -1696,28 +1667,11 @@
|
||||
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"
|
||||
@ -1728,45 +1682,16 @@
|
||||
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"
|
||||
@ -2529,11 +2454,6 @@ 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"
|
||||
@ -2991,11 +2911,6 @@ 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"
|
||||
@ -3221,11 +3136,6 @@ 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"
|
||||
@ -3749,11 +3659,6 @@ 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"
|
||||
@ -3802,13 +3707,6 @@ 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"
|
||||
@ -3821,13 +3719,6 @@ 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"
|
||||
@ -3920,11 +3811,6 @@ 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"
|
||||
@ -3961,11 +3847,6 @@ 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"
|
||||
@ -4713,7 +4594,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
|
||||
assign-symbols "^1.0.0"
|
||||
is-extendable "^1.0.1"
|
||||
|
||||
extend@^3.0.0, extend@~3.0.2:
|
||||
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==
|
||||
@ -5348,11 +5229,6 @@ 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"
|
||||
@ -5363,7 +5239,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.7.2, history@^4.9.0:
|
||||
version "4.10.1"
|
||||
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
|
||||
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
|
||||
@ -5375,13 +5251,6 @@ history@^4.7.2:
|
||||
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"
|
||||
@ -5391,7 +5260,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.3.0, hoist-non-react-statics@^3.3.2:
|
||||
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:
|
||||
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==
|
||||
@ -5699,11 +5568,6 @@ 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"
|
||||
@ -5844,11 +5708,6 @@ 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"
|
||||
@ -6024,11 +5883,6 @@ 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"
|
||||
@ -6104,6 +5958,11 @@ 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"
|
||||
@ -6787,11 +6646,6 @@ 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"
|
||||
@ -6966,7 +6820,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.4.0:
|
||||
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:
|
||||
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==
|
||||
@ -7042,54 +6896,6 @@ 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"
|
||||
@ -7100,11 +6906,6 @@ 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"
|
||||
@ -7169,201 +6970,6 @@ 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"
|
||||
@ -7418,6 +7024,14 @@ 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"
|
||||
@ -7534,11 +7148,6 @@ 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"
|
||||
@ -7549,7 +7158,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.2, ms@^2.1.1:
|
||||
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==
|
||||
@ -8214,6 +7823,13 @@ 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"
|
||||
@ -9104,20 +8720,6 @@ 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"
|
||||
@ -9433,16 +9035,11 @@ 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.3, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6:
|
||||
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:
|
||||
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"
|
||||
@ -9464,27 +9061,6 @@ 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"
|
||||
@ -9503,32 +9079,45 @@ 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@^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==
|
||||
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==
|
||||
dependencies:
|
||||
"@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"
|
||||
"@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"
|
||||
|
||||
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==
|
||||
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==
|
||||
dependencies:
|
||||
history "^5.2.0"
|
||||
react-router "6.3.0"
|
||||
"@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"
|
||||
|
||||
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==
|
||||
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==
|
||||
dependencies:
|
||||
history "^5.2.0"
|
||||
"@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"
|
||||
|
||||
react-scripts@3.4.1:
|
||||
version "3.4.1"
|
||||
@ -9806,25 +9395,6 @@ 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"
|
||||
@ -10062,13 +9632,6 @@ 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"
|
||||
@ -10522,11 +10085,6 @@ 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"
|
||||
@ -10844,13 +10402,6 @@ 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"
|
||||
@ -11045,7 +10596,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.0, tiny-warning@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
||||
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
||||
@ -11140,11 +10691,6 @@ 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"
|
||||
@ -11257,19 +10803,6 @@ 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"
|
||||
@ -11304,71 +10837,6 @@ 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"
|
||||
@ -11434,11 +10902,6 @@ 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"
|
||||
@ -11496,16 +10959,6 @@ 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"
|
||||
@ -11543,24 +10996,6 @@ 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