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_API_URL="http://localhost:2001"
|
||||||
REACT_APP_IMAGINARY_URL="http://localhost:9000"
|
REACT_APP_IMAGINARY_URL="http://localhost:9000"
|
||||||
REACT_APP_SENTRY_DSN=""
|
|
||||||
REACT_APP_SOURCE_REPO_URL="https://git.wilw.dev/wilw/treadl"
|
REACT_APP_SOURCE_REPO_URL="https://git.wilw.dev/wilw/treadl"
|
||||||
REACT_APP_SUPPORT_ROOT="https://git.wilw.dev/wilw/treadl/wiki/"
|
REACT_APP_SUPPORT_ROOT="https://git.wilw.dev/wilw/treadl/wiki/"
|
@ -18,9 +18,8 @@
|
|||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"react-helmet": "^6.0.0",
|
"react-helmet": "^6.0.0",
|
||||||
"react-joyride": "^2.4.0",
|
"react-joyride": "^2.4.0",
|
||||||
"react-markdown": "^8.0.3",
|
"react-redux": "^7.2.0",
|
||||||
"react-redux": "^8.0.1",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-router-dom": "^6.3.0",
|
|
||||||
"react-scripts": "3.4.1",
|
"react-scripts": "3.4.1",
|
||||||
"react-toastify": "^4.4.0",
|
"react-toastify": "^4.4.0",
|
||||||
"redux": "^4.0.0",
|
"redux": "^4.0.0",
|
||||||
|
@ -1,79 +1,55 @@
|
|||||||
import React, { useEffect } from 'react';
|
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 { Helmet } from 'react-helmet';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { ToastContainer, toast } from 'react-toastify';
|
import { ToastContainer, toast } from 'react-toastify';
|
||||||
import { Grid, Divider, Icon, Container } from 'semantic-ui-react';
|
import { Grid, Divider, Icon, Container } from 'semantic-ui-react';
|
||||||
|
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
import actions from 'actions';
|
import actions from 'actions';
|
||||||
|
import utils from 'utils/utils.js';
|
||||||
import NavBar from 'components/includes/NavBar';
|
import NavBar from 'components/includes/NavBar';
|
||||||
import logo from 'images/logo/main.png';
|
|
||||||
|
|
||||||
|
import logo from 'images/logo/main.png';
|
||||||
import MarketingHome from './marketing/Home.js';
|
import MarketingHome from './marketing/Home.js';
|
||||||
|
import MarketingPricing from './marketing/Pricing.js';
|
||||||
import PrivacyPolicy from './marketing/PrivacyPolicy';
|
import PrivacyPolicy from './marketing/PrivacyPolicy';
|
||||||
import TermsOfUse from './marketing/TermsOfUse';
|
import TermsOfUse from './marketing/TermsOfUse';
|
||||||
|
|
||||||
import Login from './Login.js';
|
import Login from './Login.js';
|
||||||
import ForgottenPassword from './ForgottenPassword';
|
import ForgottenPassword from './ForgottenPassword';
|
||||||
import ResetPassword from './ResetPassword';
|
import ResetPassword from './ResetPassword';
|
||||||
|
|
||||||
import Home from './main/Home.js';
|
import Home from './main/Home.js';
|
||||||
|
|
||||||
import Profile from './main/users/Profile.js';
|
import Profile from './main/users/Profile.js';
|
||||||
import ProfileEdit from './main/users/EditProfile';
|
|
||||||
import ProfileProjects from './main/users/ProfileProjects';
|
|
||||||
|
|
||||||
import NewProject from './main/projects/New.js';
|
import NewProject from './main/projects/New.js';
|
||||||
import Project from './main/projects/Project.js';
|
import Project from './main/projects/Project.js';
|
||||||
import ProjectObjects from './main/projects/ProjectObjects.js';
|
|
||||||
import ProjectSettings from './main/projects/Settings.js';
|
|
||||||
import ObjectDraft from './main/projects/objects/Draft.js';
|
|
||||||
import ObjectList from './main/projects/ObjectList.js';
|
|
||||||
|
|
||||||
import Settings from './main/settings/Settings.js';
|
import Settings from './main/settings/Settings.js';
|
||||||
import SettingsIdentity from './main/settings/Identity';
|
|
||||||
import SettingsNotification from './main/settings/Notification';
|
|
||||||
import SettingsAccount from './main/settings/Account';
|
|
||||||
|
|
||||||
import NewGroup from './main/groups/New.js';
|
import NewGroup from './main/groups/New.js';
|
||||||
import Group from './main/groups/Group.js';
|
import Group from './main/groups/Group.js';
|
||||||
import GroupFeed from './main/groups/Feed.js';
|
|
||||||
import GroupMembers from './main/groups/Members.js';
|
|
||||||
import GroupProjects from './main/groups/Projects.js';
|
|
||||||
import GroupSettings from './main/groups/Settings.js';
|
|
||||||
|
|
||||||
import Root from './main/root';
|
import Root from './main/root';
|
||||||
//import Docs from './docs';
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { isAuthenticated, isAuthenticating, isAuthenticatingType, user, driftReady, syncedToDrift } = useSelector(state => {
|
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 user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
|
||||||
const { isAuthenticated, isAuthenticating, isAuthenticatingType } = state.auth;
|
|
||||||
const { driftReady, syncedToDrift } = state.users;
|
|
||||||
return { isAuthenticated, isAuthenticating, isAuthenticatingType, user, driftReady, syncedToDrift };
|
|
||||||
});
|
|
||||||
const loggedInUserId = user?._id;
|
const loggedInUserId = user?._id;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.auth.autoLogin(token => dispatch(actions.auth.receiveLogin(token)));
|
api.auth.autoLogin(onLoginSuccess);
|
||||||
}, [dispatch]);
|
}, [onLoginSuccess]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loggedInUserId) return;
|
if (!loggedInUserId) return;
|
||||||
api.users.getMyProjects(p => dispatch(actions.projects.receiveProjects(p)));
|
api.users.getMyProjects(onReceiveProjects);
|
||||||
api.groups.getMine(g => dispatch(actions.groups.receiveGroups(g)));
|
api.groups.getMine(onReceiveGroups);
|
||||||
api.invitations.get(({ invitations, sentInvitations}) => {
|
api.invitations.get(({ invitations, sentInvitations}) => {
|
||||||
dispatch(actions.invitations.receiveInvitations(invitations.concat(sentInvitations)));
|
onReceiveInvitations(invitations.concat(sentInvitations));
|
||||||
});
|
});
|
||||||
}, [dispatch, loggedInUserId]);
|
}, [loggedInUserId, onReceiveProjects, onReceiveGroups, onReceiveInvitations]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.drift && window.drift.on('ready', () => {
|
window.drift && window.drift.on('ready', () => {
|
||||||
dispatch(actions.users.initDrift());
|
onDriftReady();
|
||||||
});
|
});
|
||||||
}, [dispatch]);
|
}, [onDriftReady]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user && driftReady && !syncedToDrift && window.drift) {
|
if (user && driftReady && !syncedToDrift && window.drift) {
|
||||||
@ -82,52 +58,35 @@ function App() {
|
|||||||
username: user.username,
|
username: user.username,
|
||||||
createdAt: user.createdAt,
|
createdAt: user.createdAt,
|
||||||
});
|
});
|
||||||
dispatch(actions.users.syncDrift(null));
|
onDriftSynced();
|
||||||
}
|
}
|
||||||
}, [dispatch, user, driftReady, syncedToDrift]);
|
}, [user, driftReady, syncedToDrift, onDriftSynced]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{display: 'flex', flexDirection: 'column', minHeight: '100vh'}}>
|
<div style={{display: 'flex', flexDirection: 'column', minHeight: '100vh'}}>
|
||||||
<Helmet defaultTitle={'Treadl'} titleTemplate={`%s | Treadl`} />
|
<Helmet defaultTitle={'Treadl'} titleTemplate={`%s | Treadl`} />
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<div style={{ flex: '1 0 0' }}>
|
<div style={{ flex: '1 0 0' }}>
|
||||||
<Routes>
|
<Switch>
|
||||||
<Route end path="/" element={isAuthenticated
|
<Route exact path="/" render={props => (isAuthenticated
|
||||||
? <Home />
|
? <Home {...props} />
|
||||||
: <MarketingHome onRegisterClicked={() => dispatch(actions.auth.openRegister())} />
|
: <MarketingHome {...props} onRegisterClicked={onOpenRegister} />)
|
||||||
} />
|
} />
|
||||||
<Route path="/privacy" element={<PrivacyPolicy />} />
|
<Route path="/pricing" render={props => <MarketingPricing {...props} onRegisterClicked={onOpenRegister} />} />
|
||||||
<Route path="/terms-of-use" element={<TermsOfUse />} />
|
<Route path="/privacy" component={PrivacyPolicy} />
|
||||||
<Route path="/password/forgotten" element={<ForgottenPassword />} />
|
<Route path="/terms-of-use" component={TermsOfUse} />
|
||||||
<Route path="/password/reset" element={<ResetPassword />} />
|
<Route path="/password/forgotten" component={ForgottenPassword} />
|
||||||
<Route path="/settings" element={<Settings />}>
|
<Route path="/password/reset" component={ResetPassword} />
|
||||||
<Route path='identity' element={<SettingsIdentity />} />
|
<Route path="/settings" component={Settings} />
|
||||||
<Route path='notifications' element={<SettingsNotification />} />
|
<Route path="/projects/new" component={NewProject} />
|
||||||
<Route path='account' element={<SettingsAccount />} />
|
<Route path="/groups/new" component={NewGroup} />
|
||||||
<Route path='' element={<SettingsIdentity />} />
|
<Route path="/groups/:id" component={Group} />
|
||||||
</Route>
|
<Route path='/root' component={Root} />
|
||||||
<Route path="/projects/new" element={<NewProject />} />
|
<Route path="/:username/edit" component={Profile} />
|
||||||
<Route path="/groups/new" element={<NewGroup />} />
|
<Route path="/:username/:projectPath" component={Project} />
|
||||||
<Route path="/groups/:id" element={<Group />}>
|
<Route path="/:username" component={Profile} />
|
||||||
<Route path='feed' element={<GroupFeed />} />
|
</Switch>
|
||||||
<Route path='members' element={<GroupMembers />} />
|
<Login open={isAuthenticating} authType={isAuthenticatingType} onClose={onCloseAuthentication} />
|
||||||
<Route path='projects' element={<GroupProjects />} />
|
|
||||||
<Route path='settings' element={<GroupSettings />} />
|
|
||||||
<Route path='' end element={<GroupFeed />} />
|
|
||||||
</Route>
|
|
||||||
<Route path='/root' element={<Root />} />
|
|
||||||
<Route path='/:username/:projectPath' element={<Project />}>
|
|
||||||
<Route path="settings" element={<ProjectSettings />} />
|
|
||||||
<Route path=":objectId/edit" element={<ObjectDraft />} />
|
|
||||||
<Route path=":objectId" element={<ProjectObjects />} />
|
|
||||||
<Route path='' element={<ObjectList />} />
|
|
||||||
</Route>
|
|
||||||
<Route path="/:username" element={<Profile />}>
|
|
||||||
<Route path="edit" element={<ProfileEdit />} />
|
|
||||||
<Route path='' element={<ProfileProjects />} />
|
|
||||||
</Route>
|
|
||||||
</Routes>
|
|
||||||
<Login open={isAuthenticating} authType={isAuthenticatingType} onClose={() => dispatch(actions.auth.closeAuthentication())} />
|
|
||||||
<ToastContainer position={toast.POSITION.BOTTOM_CENTER} hideProgressBar/>
|
<ToastContainer position={toast.POSITION.BOTTOM_CENTER} hideProgressBar/>
|
||||||
<Divider hidden section />
|
<Divider hidden section />
|
||||||
</div>
|
</div>
|
||||||
@ -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 {
|
import {
|
||||||
Card, Input, Divider, Button,
|
Card, Input, Divider, Button,
|
||||||
} from 'semantic-ui-react';
|
} from 'semantic-ui-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
|
|
||||||
function ForgottenPassword() {
|
class ForgottenPassword extends Component {
|
||||||
const [email, setEmail] = useState('');
|
constructor(props) {
|
||||||
const [loading, setLoading] = useState(false);
|
super(props);
|
||||||
const navigate = useNavigate();
|
this.state = { email: '', loading: false };
|
||||||
|
}
|
||||||
|
|
||||||
const sendEmail = () => {
|
sendEmail = () => {
|
||||||
setLoading(true);
|
this.setState({ loading: true });
|
||||||
api.auth.sendPasswordResetEmail(email, () => {
|
api.auth.sendPasswordResetEmail(this.state.email, () => {
|
||||||
setLoading(false);
|
this.setState({ loading: false });
|
||||||
toast.info('If your account exists, a password email has been sent');
|
toast.info('If your account exists, a password email has been sent');
|
||||||
navigate('/');
|
this.props.history.push('/');
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
setLoading(false);
|
this.setState({ loading: false });
|
||||||
toast.error(err.message);
|
toast.error(err.message);
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { email, loading } = this.state;
|
||||||
return (
|
return (
|
||||||
<Card.Group centered style={{ marginTop: 50 }}>
|
<Card.Group centered style={{ marginTop: 50 }}>
|
||||||
<Card raised color="yellow">
|
<Card raised color="yellow">
|
||||||
@ -30,15 +33,24 @@ function ForgottenPassword() {
|
|||||||
<Card.Header>Forgotten your password?</Card.Header>
|
<Card.Header>Forgotten your password?</Card.Header>
|
||||||
<Card.Meta>Type your email address below, and we'll send you a password-reset email.</Card.Meta>
|
<Card.Meta>Type your email address below, and we'll send you a password-reset email.</Card.Meta>
|
||||||
<Divider hidden />
|
<Divider hidden />
|
||||||
<Input fluid type="email" value={email} onChange={e => 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>
|
||||||
<Card.Content extra textAlign="right">
|
<Card.Content extra textAlign="right">
|
||||||
<Button basic onClick={() => navigate('/')} content="Cancel" />
|
<Button basic onClick={this.props.history.goBack} content="Cancel" />
|
||||||
<Button color="teal" content="Send email" onClick={sendEmail} loading={loading} />
|
<Button color="teal" content="Send email" onClick={this.sendEmail} loading={loading} />
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card>
|
</Card>
|
||||||
</Card.Group>
|
</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 {
|
import {
|
||||||
Message, Modal, Grid, Form, Input, Button,
|
Message, Modal, Grid, Form, Input, Button,
|
||||||
} from 'semantic-ui-react';
|
} from 'semantic-ui-react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import actions from 'actions';
|
import actions from 'actions';
|
||||||
import { api } from 'api';
|
import { api } from 'api';
|
||||||
|
|
||||||
import ReadingImage from 'images/reading.png';
|
import ReadingImage from 'images/reading.png';
|
||||||
|
|
||||||
function Login({ open, authType, onClose }) {
|
class Login extends Component {
|
||||||
const [username, setUsername] = useState('');
|
constructor(props) {
|
||||||
const [email, setEmail] = useState('');
|
super(props);
|
||||||
const [password, setPassword] = useState('');
|
this.state = {
|
||||||
const [loading, setLoading] = useState(false);
|
registering: false, username: '', email: '', password: '', loading: 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);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const register = () => {
|
login = () => {
|
||||||
setLoading(true);
|
const { email, password } = this.state;
|
||||||
api.auth.register(username, email, password, () => dispatch(actions.auth.requestLogin()), (data) => {
|
this.setState({ loading: true });
|
||||||
setLoading(false);
|
api.auth.login(email, password, this.props.onLoginStart, (data) => {
|
||||||
setPassword('');
|
this.setState({ loading: false, password: '', email: '', username: '' });
|
||||||
setEmail('');
|
this.props.onLoginSuccess(data);
|
||||||
setUsername('');
|
this.props.onClose();
|
||||||
dispatch(actions.auth.receiveLogin(data));
|
|
||||||
onClose();
|
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
dispatch(actions.auth.loginError(err));
|
this.props.onLoginFailure(err);
|
||||||
setLoading(false);
|
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 (
|
return (
|
||||||
<div>
|
<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>
|
<Modal.Header>
|
||||||
<span role="img" aria-label="wave">👋</span> Welcome!
|
<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.Header>
|
||||||
<Modal.Content>
|
<Modal.Content>
|
||||||
<Grid stackable>
|
<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.
|
</strong> Sign-up to see what it's about, and we'd love to hear your feedback and work with you as we grow.
|
||||||
</Message>
|
</Message>
|
||||||
|
|
||||||
{error && (<div className="ui warning message">{error.message}</div>)}
|
{this.props.error && (<div className="ui warning message">{this.props.error.message}</div>)}
|
||||||
<Form onSubmit={register}>
|
<Form onSubmit={this.register}>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<label>Pick a username
|
<label>Pick a username
|
||||||
<small> (you can use letters, numbers and underscores)</small>
|
<small> (you can use letters, numbers and underscores)</small>
|
||||||
</label>
|
</label>
|
||||||
<Input autoFocus size="large" fluid name="username" type="text" value={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>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<label>Email address
|
<label>Email address
|
||||||
<small> (for password resets & other important things)</small>
|
<small> (for password resets & other important things)</small>
|
||||||
</label>
|
</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>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<label>Choose a strong password
|
<label>Choose a strong password
|
||||||
<small> (at least 6 characters)</small>
|
<small> (at least 6 characters)</small>
|
||||||
</label>
|
</label>
|
||||||
<Input size="large" fluid name="password" type="password" value={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>
|
</Form.Field>
|
||||||
<div className="ui hidden divider" />
|
<div className="ui hidden divider" />
|
||||||
<p style={{ fontSize: 11, color: 'rgb(180,180,180)' }}>By signing-up, you agree to the Treadl <a href="/privacy" target="_blank">Privacy Policy</a> and <a href="terms-of-use" target="_blank">Terms of Use</a>.</p>
|
<p style={{ fontSize: 11, color: 'rgb(180,180,180)' }}>By signing-up, you agree to the Treadl <a href="/privacy" target="_blank">Privacy Policy</a> and <a href="terms-of-use" target="_blank">Terms of Use</a>.</p>
|
||||||
@ -132,12 +133,12 @@ function Login({ open, authType, onClose }) {
|
|||||||
</Modal>
|
</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>
|
<Modal.Header>
|
||||||
<span role="img" aria-label="Peace">✌️ </span>
|
<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.Header>
|
||||||
<Modal.Content>
|
<Modal.Content>
|
||||||
<Grid stackable>
|
<Grid stackable>
|
||||||
@ -147,17 +148,17 @@ Welcome back <Button floated="right" onClick={onClose} basic content="Close" />
|
|||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
|
|
||||||
<Grid.Column computer={8}>
|
<Grid.Column computer={8}>
|
||||||
{error && (<div className="ui warning message">{error.message}</div>)}
|
{this.props.error && (<div className="ui warning message">{this.props.error.message}</div>)}
|
||||||
<Form onSubmit={login}>
|
<Form onSubmit={this.login}>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<label>Your email address or username</label>
|
<label>Your email address or username</label>
|
||||||
<Input autoFocus size="large" fluid name="email" type="text" value={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>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<label>Password
|
<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>
|
</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>
|
</Form.Field>
|
||||||
<div className="ui hidden divider" />
|
<div className="ui hidden divider" />
|
||||||
<Form.Button type='submit' size="large" color="teal" fluid loading={loading}>Login</Form.Button>
|
<Form.Button type='submit' size="large" color="teal" fluid loading={loading}>Login</Form.Button>
|
||||||
@ -171,5 +172,22 @@ Welcome back <Button floated="right" onClick={onClose} basic content="Close" />
|
|||||||
</div>
|
</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 {
|
import {
|
||||||
Card, Input, Divider, Button,
|
Card, Input, Divider, Button,
|
||||||
} from 'semantic-ui-react';
|
} from 'semantic-ui-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
|
|
||||||
function ResetPassword() {
|
class ResetPassword extends Component {
|
||||||
const [loading, setLoading] = useState(false);
|
constructor(props) {
|
||||||
const [password, setPassword] = useState('');
|
super(props);
|
||||||
const navigate = useNavigate();
|
this.state = { password: '', loading: false };
|
||||||
|
}
|
||||||
|
|
||||||
const resetPassword = () => {
|
resetPassword = () => {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const token = params.get('token');
|
const token = params.get('token');
|
||||||
setLoading(true);
|
this.setState({ loading: true });
|
||||||
api.auth.updatePasswordWithToken(token, password, () => {
|
api.auth.updatePasswordWithToken(token, this.state.password, () => {
|
||||||
setLoading(false);
|
this.setState({ loading: false });
|
||||||
toast.info('Password changed successfully.');
|
toast.info('Password changed successfully.');
|
||||||
navigate('/');
|
this.props.history.push('/');
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
setLoading(false);
|
this.setState({ loading: false });
|
||||||
toast.error(err.message);
|
toast.error(err.message);
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { password, loading } = this.state;
|
||||||
return (
|
return (
|
||||||
<Card.Group centered style={{ marginTop: 50 }}>
|
<Card.Group centered style={{ marginTop: 50 }}>
|
||||||
<Card raised color="yellow">
|
<Card raised color="yellow">
|
||||||
@ -32,14 +35,23 @@ function ResetPassword() {
|
|||||||
<Card.Header>Enter a new password</Card.Header>
|
<Card.Header>Enter a new password</Card.Header>
|
||||||
<Card.Meta>Enter a new password below.</Card.Meta>
|
<Card.Meta>Enter a new password below.</Card.Meta>
|
||||||
<Divider hidden />
|
<Divider hidden />
|
||||||
<Input fluid type="password" value={password} onChange={e => setPassword(e.target.value)} autoFocus />
|
<Input fluid type="password" value={password} onChange={e => this.setState({ password: e.target.value })} autoFocus />
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
<Card.Content extra textAlign="right">
|
<Card.Content extra textAlign="right">
|
||||||
<Button color="teal" content="Change password" onClick={resetPassword} loading={loading} />
|
<Button color="teal" content="Change password" onClick={this.resetPassword} loading={loading} />
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card>
|
</Card>
|
||||||
</Card.Group>
|
</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 { Button } from 'semantic-ui-react';
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
|
class FileChooser extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { isUploading: false };
|
||||||
|
}
|
||||||
|
|
||||||
function FileChooser({ onUploadStart, onUploadFinish, onError, onComplete, content, trigger, accept, forType, forObject }) {
|
startUpload = () => {
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
this.setState({ isUploading: true });
|
||||||
const inputRef = useRef();
|
this.props.onUploadStart && this.props.onUploadStart();
|
||||||
|
}
|
||||||
|
|
||||||
const startUpload = () => {
|
finishUpload = () => {
|
||||||
setIsUploading(true);
|
this.setState({ isUploading: false });
|
||||||
onUploadStart && onUploadStart();
|
this.props.onUploadFinish && this.props.onUploadFinish();
|
||||||
};
|
}
|
||||||
|
|
||||||
const finishUpload = () => {
|
chooseFile = () => this.refs.fileInput.click()
|
||||||
setIsUploading(false);
|
|
||||||
onUploadFinish && onUploadFinish();
|
|
||||||
};
|
|
||||||
|
|
||||||
const chooseFile = () => inputRef.current.click();
|
handleFileChosen = (e) => {
|
||||||
|
|
||||||
const handleFileChosen = (e) => {
|
|
||||||
const file = e.target.files && e.target.files[0];
|
const file = e.target.files && e.target.files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
startUpload();
|
this.startUpload();
|
||||||
const fileName = file.name.replace(/[^a-zA-Z0-9_.]/g, '_');
|
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();
|
const reader = new FileReader();
|
||||||
reader.onload = (e2) => {
|
reader.onload = (e2) => {
|
||||||
finishUpload();
|
this.finishUpload();
|
||||||
onComplete({ wif: e2.target.result, type: 'pattern' });
|
this.props.onComplete({ wif: e2.target.result, type: 'pattern' });
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
} else {
|
} else {
|
||||||
api.uploads.generateFileUploadRequest({
|
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) => {
|
}, async (response) => {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open('PUT', response.signedRequest);
|
xhr.open('PUT', response.signedRequest);
|
||||||
xhr.setRequestHeader('Content-Type', file.type);
|
xhr.setRequestHeader('Content-Type', file.type);
|
||||||
xhr.onreadystatechange = () => {
|
xhr.onreadystatechange = () => {
|
||||||
if (xhr.readyState === 4) {
|
if (xhr.readyState === 4) {
|
||||||
finishUpload();
|
this.finishUpload();
|
||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
// We pass back the original file name so it can be displayed nicely
|
// We pass back the original file name so it can be displayed nicely
|
||||||
onComplete({ storedName: response.fileName, name: file.name, type: 'file' });
|
this.props.onComplete({ storedName: response.fileName, name: file.name, type: 'file' });
|
||||||
} else if (onError) {
|
} else if (this.props.onError) {
|
||||||
finishUpload();
|
this.finishUpload();
|
||||||
onError('Unable to upload file');
|
this.props.onError('Unable to upload file');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
xhr.send(file);
|
xhr.send(file);
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
finishUpload();
|
this.finishUpload();
|
||||||
if (onError) onError(err.message || 'Unable to upload file');
|
if (this.props.onError) this.props.onError(err.message || 'Unable to upload file');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { content, trigger, accept } = this.props;
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<input type="file" style={{ display: 'none' }} ref={inputRef} onChange={handleFileChosen} accept={accept || '*'} />
|
<input type="file" style={{ display: 'none' }} ref="fileInput" onChange={this.handleFileChosen} accept={accept || '*'} />
|
||||||
{trigger
|
{trigger
|
||||||
? React.cloneElement(trigger, { 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={isUploading} onClick={chooseFile} />
|
: <Button size="small" color="blue" icon="file" fluid content={content || 'Choose a file'} loading={this.state.isUploading} onClick={this.chooseFile} />
|
||||||
}
|
}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default FileChooser;
|
export default FileChooser;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, withRouter } from 'react-router-dom';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Loader, List, Popup, Modal, Grid, Icon, Button, Container, Dropdown } from 'semantic-ui-react';
|
import { Loader, List, Popup, Modal, Grid, Icon, Button, Container, Dropdown } from 'semantic-ui-react';
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
@ -74,31 +74,22 @@ const SearchBar = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function NavBar() {
|
function NavBar({ user, groups, onOpenLogin, onOpenRegister, isAuthenticated, onLogout, onDriftSynced, helpModalOpen, openHelpModal, searchTerm, updateSearchTerm, searchPopupOpen, openSearchPopup, searchResults, updateSearchResults, searching, updateSearching, history }) {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { isAuthenticated, user, groups, helpModalOpen, searchPopupOpen, searchTerm, searchResults, searching } = useSelector(state => {
|
|
||||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
|
||||||
const groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
|
|
||||||
const { isAuthenticated } = state.auth;
|
|
||||||
const { helpModalOpen, searchPopupOpen, searchTerm, searchResults, searching } = state.app;
|
|
||||||
return { isAuthenticated, user, groups, helpModalOpen, searchPopupOpen, searchTerm, searchResults, searching };
|
|
||||||
});
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(actions.app.openSearchPopup(false));
|
openSearchPopup(false);
|
||||||
}, [dispatch]);
|
}, [history.location.pathname, openSearchPopup]);
|
||||||
|
|
||||||
const logout = () => api.auth.logout(() => {
|
const logout = () => api.auth.logout(() => {
|
||||||
dispatch(actions.auth.logout());
|
onLogout();
|
||||||
dispatch(actions.users.syncDrift(false))
|
onDriftSynced(false);
|
||||||
if (window.drift) window.drift.reset();
|
if (window.drift) window.drift.reset();
|
||||||
navigate('/');
|
history.push('/');
|
||||||
});
|
});
|
||||||
|
|
||||||
const search = () => {
|
const search = () => {
|
||||||
dispatch(actions.app.updateSearching(true));
|
updateSearching(true);
|
||||||
api.search.all(searchTerm, r => dispatch(actions.app.updateSearchResults(r)));
|
api.search.all(searchTerm, updateSearchResults);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -108,9 +99,8 @@ function NavBar() {
|
|||||||
{isAuthenticated
|
{isAuthenticated
|
||||||
? (
|
? (
|
||||||
<div className='nav-links'>
|
<div className='nav-links'>
|
||||||
<Popup basic on='focus' open={searchPopupOpen}
|
<Popup basic on='focus' open={searchPopupOpen} onOpen={e => openSearchPopup(true)} onClose={e => openSearchPopup(false)}
|
||||||
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 => updateSearchTerm(e.target.value)} onKeyDown={e => e.keyCode === 13 && search()} /></SearchBar>}
|
||||||
trigger={<SearchBar><input placeholder='Click to search...' value={searchTerm} onChange={e => dispatch(actions.app.updateSearchTerm(e.target.value))} onKeyDown={e => e.keyCode === 13 && search()} /></SearchBar>}
|
|
||||||
content={<div style={{width: 300}} className='joyride-search'>
|
content={<div style={{width: 300}} className='joyride-search'>
|
||||||
{!searchResults?.users && !searchResults?.groups ?
|
{!searchResults?.users && !searchResults?.groups ?
|
||||||
<small>
|
<small>
|
||||||
@ -181,7 +171,7 @@ function NavBar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
<span className='above-mobile'>
|
<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>
|
</span>
|
||||||
|
|
||||||
<Dropdown direction="left" pointing="top right" icon={null} style={{marginLeft: 10}}
|
<Dropdown direction="left" pointing="top right" icon={null} style={{marginLeft: 10}}
|
||||||
@ -223,20 +213,20 @@ function NavBar() {
|
|||||||
trigger=<Button basic inverted icon="bars" />
|
trigger=<Button basic inverted icon="bars" />
|
||||||
>
|
>
|
||||||
<Dropdown.Menu direction="left">
|
<Dropdown.Menu direction="left">
|
||||||
<Dropdown.Item onClick={() => dispatch(actions.auth.openLogin())}>Login</Dropdown.Item>
|
<Dropdown.Item onClick={onOpenLogin}>Login</Dropdown.Item>
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</span>
|
</span>
|
||||||
<span className="above-mobile">
|
<span className="above-mobile">
|
||||||
<Button inverted basic onClick={() => dispatch(actions.auth.openLogin())}>Login</Button>
|
<Button inverted basic onClick={onOpenLogin}>Login</Button>
|
||||||
</span>
|
</span>
|
||||||
<Button color="teal" onClick={() => dispatch(actions.auth.openRegister())}>
|
<Button color="teal" onClick={onOpenRegister}>
|
||||||
<span role="img" aria-label="wave">👋</span> Sign-up
|
<span role="img" aria-label="wave">👋</span> Sign-up
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<Modal open={helpModalOpen} onClose={e => dispatch(actions.app.openHelpModal(false))}>
|
<Modal open={helpModalOpen} onClose={e => openHelpModal(false)}>
|
||||||
<Modal.Header>Welcome to Treadl!</Modal.Header>
|
<Modal.Header>Welcome to Treadl!</Modal.Header>
|
||||||
<Modal.Content>
|
<Modal.Content>
|
||||||
<h3>Introduction</h3>
|
<h3>Introduction</h3>
|
||||||
@ -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>
|
<p>If you have any comments or feedback please tell us by emailing <a href="mailto:hello@treadl.com">hello@treadl.com</a>!</p>
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
<Modal.Actions>
|
<Modal.Actions>
|
||||||
<Button onClick={e => 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.Actions>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Container>
|
</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>
|
<Dropdown.Menu>
|
||||||
<FileChooser
|
<FileChooser
|
||||||
forType={forType} forObject={forObj}
|
forType={forType} for={forObj}
|
||||||
trigger=<Dropdown.Item icon="upload" content="Upload a file from your computer" />
|
trigger=<Dropdown.Item icon="upload" content="Upload a file from your computer" />
|
||||||
onUploadStart={e => updateAttachmentUploading(true) }
|
onUploadStart={e => updateAttachmentUploading(true) }
|
||||||
onUploadFinish={e => updateAttachmentUploading(false) }
|
onUploadFinish={e => updateAttachmentUploading(false) }
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { Button, Dropdown } from 'semantic-ui-react';
|
import { Button, Dropdown } from 'semantic-ui-react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import actions from 'actions';
|
import actions from 'actions';
|
||||||
@ -8,33 +8,37 @@ import api from 'api';
|
|||||||
|
|
||||||
import FileChooser from 'components/includes/FileChooser';
|
import FileChooser from 'components/includes/FileChooser';
|
||||||
|
|
||||||
function ObjectCreator({ project, onCreateObject, onError, fluid }) {
|
class ObjectCreator extends Component {
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
constructor(props) {
|
||||||
const navigate = useNavigate();
|
super(props);
|
||||||
const dispatch = useDispatch();
|
this.state = { isUploading: false };
|
||||||
|
}
|
||||||
|
|
||||||
const createNewPattern = () => {
|
createNewPattern = () => {
|
||||||
api.projects.createObject(project.fullName, { name: 'Untitled pattern', type: 'pattern' }, (object) => {
|
api.projects.createObject(this.props.project.fullName, { name: 'Untitled pattern', type: 'pattern' }, (object) => {
|
||||||
dispatch(actions.objects.create(object));
|
this.props.onCreateObject(object);
|
||||||
navigate(`/${project.fullName}/${object._id}/edit`);
|
this.props.history.push(`/${this.props.project.fullName}/${object._id}/edit`);
|
||||||
});
|
}, err => this.setState({ loading: false }));
|
||||||
};
|
}
|
||||||
|
|
||||||
const fileUploaded = (file) => {
|
fileUploaded = (file) => {
|
||||||
setIsUploading(true);
|
this.setState({ isUploading: true });
|
||||||
api.projects.createObject(project.fullName, {
|
api.projects.createObject(this.props.project.fullName, {
|
||||||
name: file.name, storedName: file.storedName, type: file.type, wif: file.wif,
|
name: file.name, storedName: file.storedName, type: file.type, wif: file.wif,
|
||||||
}, (object) => {
|
}, (object) => {
|
||||||
setIsUploading(false);
|
this.setState({ isUploading: false });
|
||||||
dispatch(actions.objects.create(object));
|
this.props.onCreateObject(object);
|
||||||
navigate(`/${project.fullName}/${object._id}`);
|
this.props.history.push(`/${this.props.project.fullName}/${object._id}`);
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
toast.error(err.message);
|
toast.error(err.message);
|
||||||
setIsUploading(false);
|
this.setState({ isUploading: false });
|
||||||
onError && onError(err);
|
this.props.onError && this.props.onError(err);
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { project, fluid } = this.props;
|
||||||
|
const { isUploading } = this.state;
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
fluid={!!fluid}
|
fluid={!!fluid}
|
||||||
@ -42,27 +46,35 @@ function ObjectCreator({ project, onCreateObject, onError, fluid }) {
|
|||||||
trigger=<Button color="teal" fluid content="Add something" icon="plus" loading={isUploading} />
|
trigger=<Button color="teal" fluid content="Add something" icon="plus" loading={isUploading} />
|
||||||
>
|
>
|
||||||
<Dropdown.Menu>
|
<Dropdown.Menu>
|
||||||
<Dropdown.Item onClick={createNewPattern} icon="pencil" content="Create a new weaving pattern" />
|
<Dropdown.Item onClick={this.createNewPattern} icon="pencil" content="Create a new weaving pattern" />
|
||||||
<FileChooser
|
<FileChooser
|
||||||
forType="project"
|
forType="project"
|
||||||
forObject={project}
|
for={project}
|
||||||
trigger=<Dropdown.Item icon="upload" content="Import a WIF file" />
|
trigger=<Dropdown.Item icon="upload" content="Import a WIF file" />
|
||||||
accept=".wif"
|
accept=".wif"
|
||||||
onUploadStart={e => setIsUploading(true)}
|
onUploadStart={e => this.setState({ isUploading: true })}
|
||||||
onUploadFinish={e => setIsUploading(false)}
|
onUploadFinish={e => this.setState({ isUploading: false })}
|
||||||
onComplete={fileUploaded}
|
onComplete={this.fileUploaded}
|
||||||
/>
|
/>
|
||||||
<FileChooser
|
<FileChooser
|
||||||
forType="project"
|
forType="project"
|
||||||
forObject={project}
|
for={project}
|
||||||
trigger=<Dropdown.Item icon="cloud upload" content="Upload an image or a file" />
|
trigger=<Dropdown.Item icon="cloud upload" content="Upload an image or a file" />
|
||||||
onUploadStart={e => setIsUploading(true)}
|
onUploadStart={e => this.setState({ isUploading: true })}
|
||||||
onUploadFinish={e => setIsUploading(false)}
|
onUploadFinish={e => this.setState({ isUploading: false })}
|
||||||
onComplete={fileUploaded}
|
onComplete={this.fileUploaded}
|
||||||
/>
|
/>
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
onCreateObject: name => dispatch(actions.objects.create(name)),
|
||||||
|
onSelectObject: id => dispatch(actions.objects.select(id)),
|
||||||
|
});
|
||||||
|
const ObjectCreatorContainer = withRouter(connect(
|
||||||
|
null, mapDispatchToProps,
|
||||||
|
)(ObjectCreator));
|
||||||
|
|
||||||
export default ObjectCreator;
|
export default ObjectCreatorContainer;
|
||||||
|
@ -1,20 +1,16 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { Component } from 'react';
|
||||||
import pell from 'pell';
|
import pell from 'pell';
|
||||||
|
|
||||||
function RichText({ value, onChange }) {
|
class RichText extends Component {
|
||||||
const [completedInit, setCompletedInit] = useState(false);
|
ensureHTTP(url) {
|
||||||
const textboxRef = useRef();
|
|
||||||
|
|
||||||
const ensureHTTP = (url) => {
|
|
||||||
if (url.trim().toLowerCase().indexOf('http') !== 0) return `http://${url}`;
|
if (url.trim().toLowerCase().indexOf('http') !== 0) return `http://${url}`;
|
||||||
return url;
|
return url;
|
||||||
};
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
componentDidMount() {
|
||||||
if (completedInit) return;
|
|
||||||
pell.init({
|
pell.init({
|
||||||
element: textboxRef.current,
|
element: this.refs.textbox,
|
||||||
onChange: onChange,
|
onChange: this.props.onChange,
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
icon: '<i class="italic icon"></i>',
|
icon: '<i class="italic icon"></i>',
|
||||||
@ -58,7 +54,7 @@ function RichText({ value, onChange }) {
|
|||||||
title: 'Insert an image using a direct URL link',
|
title: 'Insert an image using a direct URL link',
|
||||||
result: () => {
|
result: () => {
|
||||||
const url = window.prompt('Enter the image URL'); // eslint-disable-line no-alert
|
const url = window.prompt('Enter the image URL'); // eslint-disable-line no-alert
|
||||||
if (url) pell.exec('insertImage', ensureHTTP(url));
|
if (url) pell.exec('insertImage', this.ensureHTTP(url));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -67,16 +63,17 @@ function RichText({ value, onChange }) {
|
|||||||
title: 'Add hyperlink to selected text',
|
title: 'Add hyperlink to selected text',
|
||||||
result: () => {
|
result: () => {
|
||||||
const url = window.prompt('Enter the link URL'); // eslint-disable-line no-alert
|
const url = window.prompt('Enter the link URL'); // eslint-disable-line no-alert
|
||||||
if (url) pell.exec('createLink', ensureHTTP(url));
|
if (url) pell.exec('createLink', this.ensureHTTP(url));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
defaultParagraphSeparator: 'p',
|
defaultParagraphSeparator: 'p',
|
||||||
}).content.innerHTML = value || '';
|
}).content.innerHTML = this.props.value || '';
|
||||||
setCompletedInit(true);
|
}
|
||||||
}, [completedInit, value, onChange]);
|
|
||||||
|
|
||||||
return <div ref={textboxRef} />;
|
render() {
|
||||||
|
return <div ref="textbox" />;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RichText;
|
export default RichText;
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import React from 'react';
|
import React, { Component } from 'react';
|
||||||
import sanitizeHtml from 'sanitize-html';
|
import sanitizeHtml from 'sanitize-html';
|
||||||
|
|
||||||
function RichTextViewer({ content, style, className }) {
|
class RichTextViewer extends Component {
|
||||||
if (!content) return null;
|
render() {
|
||||||
|
if (!this.props.content) return null;
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
style={Object.assign({}, { breakLines: 'pre-line'}, style)}
|
style={Object.assign({}, { breakLines: 'pre-line'}, this.props.style)}
|
||||||
className={className}
|
className={this.props.className}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html:
|
__html:
|
||||||
sanitizeHtml(content, {
|
sanitizeHtml(this.props.content, {
|
||||||
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'u']),
|
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'u']),
|
||||||
allowedAttributes: {
|
allowedAttributes: {
|
||||||
a: ['href', 'name', 'target'],
|
a: ['href', 'name', 'target'],
|
||||||
@ -20,5 +21,6 @@ function RichTextViewer({ content, style, className }) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default RichTextViewer;
|
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 { Menu, Popup, Input } from 'semantic-ui-react';
|
||||||
|
import { connect } from 'react-redux'
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
|
|
||||||
function UserSearch({ onSelected, fluid }) {
|
class UserSearch extends Component {
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
|
||||||
const [oldSearchTerm, setOldSearchTerm] = useState('');
|
|
||||||
const [searchResults, setSearchResults] = useState();
|
|
||||||
const [searching, setSearching] = useState(false);
|
|
||||||
|
|
||||||
const updateSearchTerm = e => {
|
constructor(props) {
|
||||||
setSearching(true);
|
super(props);
|
||||||
setSearchTerm(e.target.value);
|
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(() => {
|
updateSearchTerm = e => {
|
||||||
if (searchTerm === oldSearchTerm) return;
|
this.setState({ searching: true, searchTerm: e.target.value })
|
||||||
setOpen(false);
|
}
|
||||||
setSearching(true);
|
search = () => {
|
||||||
setOldSearchTerm(searchTerm);
|
this.setState({ open: false, searching: true, oldSearchTerm: this.state.searchTerm });
|
||||||
if (!searchTerm) return setSearching(false);
|
if (!this.state.searchTerm) return this.setState({ searching: false });
|
||||||
api.search.users(searchTerm, searchResults => {
|
api.search.users(this.state.searchTerm, searchResults => this.setState({ open: true, searching: false, searchResults }), err => this.setState({searching: false}));
|
||||||
setOpen(true);
|
}
|
||||||
setSearching(false);
|
|
||||||
setSearchResults(searchResults);
|
|
||||||
}, () => setSearching(false));
|
|
||||||
}, [oldSearchTerm, searchTerm]);
|
|
||||||
useEffect(() => {
|
|
||||||
const searcher = setInterval(() => search(), 500);
|
|
||||||
return () => clearInterval(searcher);
|
|
||||||
}, [search]);
|
|
||||||
|
|
||||||
const inputClicked = () => {
|
inputClicked = () => {
|
||||||
if (searchResults) setOpen(true);
|
if (this.state.searchResults) this.setState({ open: true });
|
||||||
};
|
}
|
||||||
const choose = user => {
|
onSelected = user => {
|
||||||
onSelected && onSelected(user);
|
this.props.onSelected && this.props.onSelected(user);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { searchResults, searching, open } = this.state;
|
||||||
|
const { fluid } = this.props;
|
||||||
return (
|
return (
|
||||||
<Popup hoverable position='bottom left' open={open} onClose={e => setOpen(false)}
|
<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={updateSearchTerm} loading={searching} onClick={inputClicked}/>}
|
trigger={<Input fluid={fluid} icon='search' iconPosition='left' placeholder='Search for a username...' onChange={this.updateSearchTerm} loading={searching} onClick={this.inputClicked}/>}
|
||||||
content={(
|
content={(
|
||||||
<Menu borderless vertical>
|
<Menu borderless vertical>
|
||||||
{searchResults && searchResults.map(r =>
|
{searchResults && searchResults.map(r =>
|
||||||
<Menu.Item key={r._id} as='a' icon='user' content={r.username} onClick={e => 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>
|
</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 { Helmet } from 'react-helmet';
|
||||||
import { Loader, Divider, Button, Message, Container, Segment, Grid, Card, Icon, List } from 'semantic-ui-react';
|
import { Loader, Divider, Button, Message, Container, Segment, Grid, Card, Icon, List } from 'semantic-ui-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import actions from 'actions';
|
import actions from 'actions';
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
@ -13,37 +13,29 @@ import HelpLink from 'components/includes/HelpLink';
|
|||||||
import ProjectCard from 'components/includes/ProjectCard';
|
import ProjectCard from 'components/includes/ProjectCard';
|
||||||
import Tour from 'components/includes/Tour';
|
import Tour from 'components/includes/Tour';
|
||||||
|
|
||||||
function Home() {
|
function Home({ user, groups, projects, invitations, loadingProjects, onReceiveProjects, onReceiveInvitations, onDismissInvitation, onReceiveGroup, onJoinGroup }) {
|
||||||
const [runJoyride, setRunJoyride] = useState(false);
|
const [runJoyride, setRunJoyride] = useState(false);
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { user, projects, groups, invitations, loadingProjects } = useSelector(state => {
|
|
||||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
|
||||||
const groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
|
|
||||||
const invitations = state.invitations.invitations.filter(i => i.recipient === user?._id);
|
|
||||||
const projects = state.projects.projects.filter(p => p.user === user?._id);
|
|
||||||
return { user, projects, groups, invitations, loadingProjects: state.projects.loading };
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.invitations.get(({ invitations, sentInvitations}) => {
|
api.invitations.get(({ invitations, sentInvitations}) => {
|
||||||
dispatch(actions.invitations.receiveInvitations(invitations.concat(sentInvitations)));
|
onReceiveInvitations(invitations.concat(sentInvitations));
|
||||||
});
|
});
|
||||||
}, [dispatch]);
|
}, [onReceiveInvitations]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.users.getMyProjects(p => dispatch(actions.projects.receiveProjects(p)));
|
api.users.getMyProjects(onReceiveProjects);
|
||||||
setTimeout(() =>
|
setTimeout(() =>
|
||||||
setRunJoyride(true), 2000);
|
setRunJoyride(true), 2000);
|
||||||
}, [dispatch]);
|
}, [onReceiveProjects]);
|
||||||
|
|
||||||
const declineInvite = (invite) => {
|
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) => {
|
const acceptInvite = (invite) => {
|
||||||
api.invitations.accept(invite._id, (result) => {
|
api.invitations.accept(invite._id, (result) => {
|
||||||
dispatch(actions.invitations.dismiss(invite._id));
|
onDismissInvitation(invite._id);
|
||||||
if (result.group) {
|
if (result.group) {
|
||||||
dispatch(actions.groups.receiveGroup(result.group));
|
onReceiveGroup(result.group);
|
||||||
dispatch(actions.users.joinGroup(user._id, result.group._id));
|
onJoinGroup(user._id, result.group._id);
|
||||||
}
|
}
|
||||||
}, err => toast.error(err.message));
|
}, 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 React, { useEffect } from 'react';
|
||||||
import { Loader, Button, Segment } from 'semantic-ui-react';
|
import { Loader, Button, Segment } from 'semantic-ui-react';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { useParams } from 'react-router-dom';
|
import { connect } from 'react-redux';
|
||||||
import utils from 'utils/utils.js';
|
import utils from 'utils/utils.js';
|
||||||
import actions from 'actions';
|
import actions from 'actions';
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
@ -10,29 +10,16 @@ import FeedMessage from 'components/includes/FeedMessage';
|
|||||||
import NewFeedMessage from 'components/includes/NewFeedMessage';
|
import NewFeedMessage from 'components/includes/NewFeedMessage';
|
||||||
import MessagesImage from 'images/messages.png';
|
import MessagesImage from 'images/messages.png';
|
||||||
|
|
||||||
function Feed() {
|
function Feed({ user, group, entries, onReceiveEntry, onDeleteEntry, newEntry, onJoinGroup, replyingTo, updateReplyingTo, loadingEntries, updateLoadingEntries, match }) {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { id } = useParams();
|
|
||||||
const { user, group, entries, replyingTo, loadingEntries } = useSelector(state => {
|
|
||||||
const group = state.groups.groups.filter(g => g._id === id)[0];
|
|
||||||
const entries = state.groups.entries.filter(e => e.group === id).sort((a, b) => {
|
|
||||||
const aDate = new Date(a.createdAt);
|
|
||||||
const bDate = new Date(b.createdAt);
|
|
||||||
return aDate < bDate;
|
|
||||||
});
|
|
||||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
|
||||||
const { replyingTo } = state.posts;
|
|
||||||
return { user, group, entries, replyingTo, loadingEntries: state.groups.loadingEntries };
|
|
||||||
});
|
|
||||||
const myGroups = user?.groups || [];
|
const myGroups = user?.groups || [];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(actions.groups.updateLoadingEntries(true));
|
updateLoadingEntries(true);
|
||||||
api.groups.getEntries(id, entries => {
|
api.groups.getEntries(match.params.id, entries => {
|
||||||
dispatch(actions.groups.updateLoadingEntries(false));
|
updateLoadingEntries(false);
|
||||||
entries.forEach(e => dispatch(actions.groups.receiveEntry(e)));
|
entries.forEach(e => onReceiveEntry(e));
|
||||||
});
|
});
|
||||||
}, [dispatch, id, myGroups.length]);
|
}, [match.params.id, myGroups.length, onReceiveEntry, updateLoadingEntries]);
|
||||||
|
|
||||||
const mainEntries = entries && entries.filter(e => !e.inReplyTo);
|
const mainEntries = entries && entries.filter(e => !e.inReplyTo);
|
||||||
|
|
||||||
@ -40,9 +27,9 @@ function Feed() {
|
|||||||
<div>
|
<div>
|
||||||
{utils.isInGroup(user, group._id) && <>
|
{utils.isInGroup(user, group._id) && <>
|
||||||
{replyingTo ?
|
{replyingTo ?
|
||||||
<Button style={{marginBottom: 20}} color='teal' content='Write a new post' onClick={() => 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 &&
|
{loadingEntries && !mainEntries?.length &&
|
||||||
<div style={{textAlign:'center'}}>
|
<div style={{textAlign:'center'}}>
|
||||||
@ -58,11 +45,35 @@ function Feed() {
|
|||||||
</Segment>
|
</Segment>
|
||||||
}
|
}
|
||||||
{mainEntries?.map(e =>
|
{mainEntries?.map(e =>
|
||||||
<FeedMessage key={e._id} user={user} forType='group' group={group} post={e} replies={entries.filter(r => r.inReplyTo === e._id)} onDeleted={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>
|
</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 React, { useEffect } from 'react';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { Segment, Loader, Menu, Message, Container, Button, Icon, Grid, Card } from 'semantic-ui-react';
|
import { Segment, Loader, Menu, Message, Container, Button, Icon, Grid, Card } from 'semantic-ui-react';
|
||||||
import { Outlet, Link, useParams } from 'react-router-dom';
|
import { Switch, Route, Link, withRouter } from 'react-router-dom';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import utils from 'utils/utils.js';
|
import utils from 'utils/utils.js';
|
||||||
import actions from 'actions';
|
import actions from 'actions';
|
||||||
@ -10,62 +10,53 @@ import api from 'api';
|
|||||||
|
|
||||||
import UserChip from 'components/includes/UserChip';
|
import UserChip from 'components/includes/UserChip';
|
||||||
import HelpLink from 'components/includes/HelpLink';
|
import HelpLink from 'components/includes/HelpLink';
|
||||||
|
import Feed from './Feed.js';
|
||||||
|
import Members from './Members.js';
|
||||||
|
import Projects from './Projects.js';
|
||||||
|
import Settings from './Settings.js';
|
||||||
|
|
||||||
function Group() {
|
function Group({ user, group, requests, myRequests, loading, errorMessage, onReceiveGroup, onRequest, onRequestFailed, onJoinGroup, onLeaveGroup, onSubsUpdated, onReceiveInvitations, invitations, onDismissInvitation, match }) {
|
||||||
const { id } = useParams();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { user, group, loading, errorMessage, requests, myRequests, invitations } = useSelector(state => {
|
|
||||||
let group;
|
|
||||||
state.groups.groups.forEach((g) => {
|
|
||||||
if (g._id === id) group = g;
|
|
||||||
});
|
|
||||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
|
||||||
const requests = state.invitations.invitations.filter(i => i.recipientGroup === group?._id);
|
|
||||||
const myRequests = state.invitations.invitations.filter(i => i.recipientGroup === group?._id && i.user === user?._id);
|
|
||||||
const invitations = state.invitations.invitations.filter(i => i.recipient === user?._id && i.typeId === group?._id);
|
|
||||||
return { user, group, loading: state.groups.loading, errorMessage: state.groups.errorMessage, requests, myRequests, invitations };
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(actions.groups.request());
|
onRequest();
|
||||||
api.groups.get(id, g => dispatch(actions.groups.receiveGroup(g)), err => dispatch(actions.groups.requestFailed(err)));
|
api.groups.get(match.params.id, onReceiveGroup, onRequestFailed);
|
||||||
}, [dispatch, id]);
|
}, [match.params.id, onRequest, onReceiveGroup, onRequestFailed]);
|
||||||
|
|
||||||
const join = () => {
|
const join = () => {
|
||||||
if (!user) return toast.warning('Please login or sign-up first');
|
if (!user) return toast.warning('Please login or sign-up first');
|
||||||
api.groups.createMember(id, user._id, () => {
|
api.groups.createMember(match.params.id, user._id, () => {
|
||||||
dispatch(actions.users.joingGroup(user._id, id));
|
onJoinGroup(user._id, match.params.id);
|
||||||
}, err => toast.error(err.message));
|
}, err => toast.error(err.message));
|
||||||
}
|
}
|
||||||
const leave = () => {
|
const leave = () => {
|
||||||
utils.confirm('Really leave this group?', 'You may not be able to re-join the group yourself.').then(() => {
|
utils.confirm('Really leave this group?', 'You may not be able to re-join the group yourself.').then(() => {
|
||||||
api.groups.deleteMember(id, user._id, () => {
|
api.groups.deleteMember(match.params.id, user._id, () => {
|
||||||
dispatch(actions.users.leaveGroup(user._id, id));
|
onLeaveGroup(user._id, match.params.id);
|
||||||
}, err => toast.error(err.message));
|
}, err => toast.error(err.message));
|
||||||
}, () => {});
|
}, () => {});
|
||||||
}
|
}
|
||||||
const toggleEmailSub = (key, enable) => {
|
const toggleEmailSub = (key, enable) => {
|
||||||
if (enable)
|
if (enable)
|
||||||
api.users.createEmailSubscription(user.username, key, ({ subscriptions }) => 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
|
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 = () => {
|
const requestToJoin = () => {
|
||||||
api.groups.createJoinRequest(group._id, invitation => {
|
api.groups.createJoinRequest(group._id, invitation => {
|
||||||
toast.success('Request to join sent');
|
toast.success('Request to join sent');
|
||||||
dispatch(actions.invitations.receiveInvitations([invitation]));
|
onReceiveInvitations([invitation]);
|
||||||
}, err => toast.error(err.message));
|
}, err => toast.error(err.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
const declineInvite = (invite) => {
|
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) => {
|
const acceptInvite = (invite) => {
|
||||||
api.invitations.accept(invite._id, (result) => {
|
api.invitations.accept(invite._id, (result) => {
|
||||||
dispatch(actions.invitations.dismiss(invite._id));
|
onDismissInvitation(invite._id);
|
||||||
if (result.group) {
|
if (result.group) {
|
||||||
dispatch(actions.users.joinGroup(user._id, result.group._id));
|
onJoinGroup(user._id, result.group._id);
|
||||||
}
|
}
|
||||||
}, err => toast.error(err.message));
|
}, err => toast.error(err.message));
|
||||||
}
|
}
|
||||||
@ -177,7 +168,13 @@ function Group() {
|
|||||||
}
|
}
|
||||||
</Segment>
|
</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>
|
<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 React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Grid, Table, Button, Input, Label, Header, Loader, Segment, Dropdown, Card } from 'semantic-ui-react';
|
import { Grid, Table, Button, Input, Label, Header, Loader, Segment, Dropdown, Card } from 'semantic-ui-react';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { useParams } from 'react-router-dom';
|
import { connect } from 'react-redux';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import utils from 'utils/utils.js';
|
import utils from 'utils/utils.js';
|
||||||
@ -12,19 +12,9 @@ import UserChip from 'components/includes/UserChip';
|
|||||||
import HelpLink from 'components/includes/HelpLink';
|
import HelpLink from 'components/includes/HelpLink';
|
||||||
import UserSearch from 'components/includes/UserSearch';
|
import UserSearch from 'components/includes/UserSearch';
|
||||||
|
|
||||||
function Members() {
|
function Members({ user, group, members, requests, loading, onReceiveUser, onJoinGroup, onLeaveGroup, onUpdateGroupLoading, onDismissInvitation }) {
|
||||||
const [invitations, setInvitations] = useState([]);
|
const [invitations, setInvitations] = useState([]);
|
||||||
const joinLinkRef = useRef(null);
|
const joinLinkRef = useRef(null);
|
||||||
const { id } = useParams();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const { user, group, members, loading, requests } = useSelector(state => {
|
|
||||||
const group = state.groups.groups.filter(g => g._id === id)[0];
|
|
||||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
|
||||||
const members = state.users.users.filter(u => utils.isInGroup(u, id));
|
|
||||||
const requests = state.invitations.invitations.filter(i => i.recipientGroup === group?._id);
|
|
||||||
return { user, group, members, loading: state.groups.loading, requests };
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (utils.isGroupAdmin(user, group)) {
|
if (utils.isGroupAdmin(user, group)) {
|
||||||
@ -32,15 +22,15 @@ function Members() {
|
|||||||
}
|
}
|
||||||
}, [user, group]);
|
}, [user, group]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(actions.users.request(true));
|
onUpdateGroupLoading(true);
|
||||||
api.groups.getMembers(group._id, members => {
|
api.groups.getMembers(group._id, members => {
|
||||||
members.forEach(u => dispatch(actions.users.receive(u)));
|
members.forEach(onReceiveUser);
|
||||||
dispatch(actions.users.request(false));
|
onUpdateGroupLoading(false);
|
||||||
}, err => {
|
}, err => {
|
||||||
toast.error(err.message);
|
toast.error(err.message);
|
||||||
dispatch(actions.users.request(false));
|
onUpdateGroupLoading(false);
|
||||||
});
|
});
|
||||||
}, [dispatch, group]);
|
}, [group, onReceiveUser, onUpdateGroupLoading]);
|
||||||
|
|
||||||
const copyLink = () => {
|
const copyLink = () => {
|
||||||
joinLinkRef.current.select();
|
joinLinkRef.current.select();
|
||||||
@ -50,7 +40,7 @@ function Members() {
|
|||||||
const kickUser = (id) => {
|
const kickUser = (id) => {
|
||||||
utils.confirm('Really kick this user?').then(() => {
|
utils.confirm('Really kick this user?').then(() => {
|
||||||
api.groups.deleteMember(group._id, id, () => {
|
api.groups.deleteMember(group._id, id, () => {
|
||||||
dispatch(actions.users.leaveGroup(id, group._id));
|
onLeaveGroup(id, group._id);
|
||||||
}, err => toast.error(err.message));
|
}, err => toast.error(err.message));
|
||||||
}, () => {});
|
}, () => {});
|
||||||
}
|
}
|
||||||
@ -70,14 +60,14 @@ function Members() {
|
|||||||
}
|
}
|
||||||
const approveRequest = (invite) => {
|
const approveRequest = (invite) => {
|
||||||
api.invitations.accept(invite._id, (result) => {
|
api.invitations.accept(invite._id, (result) => {
|
||||||
dispatch(actions.invitations.dismiss(invite._id))
|
onDismissInvitation(invite._id);
|
||||||
dispatch(actions.users.receive(invite.invitedBy));
|
onReceiveUser(invite.invitedBy);
|
||||||
dispatch(actions.users.joinGroup(invite.user, group._id));
|
onJoinGroup(invite.user, group._id);
|
||||||
toast.success(`${invite.invitedBy.username} is now a member`);
|
toast.success(`${invite.invitedBy.username} is now a member`);
|
||||||
}, err => toast.error(err.message));
|
}, err => toast.error(err.message));
|
||||||
}
|
}
|
||||||
const declineRequest = (request) => {
|
const declineRequest = (request) => {
|
||||||
api.invitations.decline(request._id, () => dispatch(actions.invitations.dismiss(request._id)),
|
api.invitations.decline(request._id, () => onDismissInvitation(request._id),
|
||||||
err => toast.error(err.message));
|
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 { Helmet } from 'react-helmet';
|
||||||
import { Icon, Form, Grid, Input, Checkbox, Button, Divider } from 'semantic-ui-react';
|
import { Icon, Form, Grid, Input, Checkbox, Button, Divider } from 'semantic-ui-react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import actions from 'actions';
|
import actions from 'actions';
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
|
|
||||||
import HelpLink from 'components/includes/HelpLink';
|
import HelpLink from 'components/includes/HelpLink';
|
||||||
|
|
||||||
function NewGroup() {
|
function NewGroup({ user, newGroupName, newGroupDescription, newGroupClosed, onUpdateGroupName, onUpdateGroupDescription, onUpdateGroupClosed, onReceiveGroup, loading, onUpdateGroupLoading, history }) {
|
||||||
const navigate = useNavigate();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const { newGroupName, newGroupDescription, newGroupClosed, loading } = useSelector(state => {
|
|
||||||
const { loading, newGroupName, newGroupDescription, newGroupClosed } = state.groups;
|
|
||||||
return { newGroupName, newGroupDescription, newGroupClosed, loading };
|
|
||||||
});
|
|
||||||
|
|
||||||
const createGroup = () => {
|
const createGroup = () => {
|
||||||
dispatch(actions.groups.request(true));
|
onUpdateGroupLoading(true);
|
||||||
api.groups.create({ name: newGroupName, description: newGroupDescription, closed: newGroupClosed }, (group) => {
|
api.groups.create({ name: newGroupName, description: newGroupDescription, closed: newGroupClosed }, (group) => {
|
||||||
dispatch(actions.groups.receiveGroup(group));
|
onReceiveGroup(group);
|
||||||
dispatch(actions.groups.request(false));
|
onUpdateGroupLoading(false);
|
||||||
navigate(`/groups/${group._id}/members`);
|
history.push(`/groups/${group._id}/members`);
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
dispatch(actions.groups.request(false));
|
onUpdateGroupLoading(false);
|
||||||
toast.error(err.message);
|
toast.error(err.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -45,12 +38,12 @@ function NewGroup() {
|
|||||||
|
|
||||||
<h3>About your group</h3>
|
<h3>About your group</h3>
|
||||||
<p>Give your group a short name. You can change this whenever you like.</p>
|
<p>Give your group a short name. You can change this whenever you like.</p>
|
||||||
<Input autoFocus type="text" fluid onChange={e => dispatch(actions.groups.updateNewGroupName(e.target.value))} value={newGroupName} />
|
<Input autoFocus type="text" fluid onChange={e => onUpdateGroupName(e.target.value)} value={newGroupName} />
|
||||||
<Divider hidden />
|
<Divider hidden />
|
||||||
<p><strong>Optional:</strong> Write a short description to describe your group to others.</p>
|
<p><strong>Optional:</strong> Write a short description to describe your group to others.</p>
|
||||||
<Form><Form.TextArea placeholder="Group description (optional)..." value={newGroupDescription} onChange={e => 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)' }}>
|
<div style={{ marginLeft: 63, color: 'rgb(150,150,150)' }}>
|
||||||
<p>Closed groups are more restrictive and new members must be invited or approved to join. Members can join non-closed groups without being invited.</p>
|
<p>Closed groups are more restrictive and new members must be invited or approved to join. Members can join non-closed groups without being invited.</p>
|
||||||
</div>
|
</div>
|
||||||
@ -59,7 +52,7 @@ function NewGroup() {
|
|||||||
<p>You can add and invite others to join your group after you've created it.</p>
|
<p>You can add and invite others to join your group after you've created it.</p>
|
||||||
|
|
||||||
<div style={{textAlign: 'right'}}>
|
<div style={{textAlign: 'right'}}>
|
||||||
<Button basic onClick={() => navigate(-1)}>Cancel</Button>
|
<Button basic onClick={history.goBack}>Cancel</Button>
|
||||||
<Button color="teal" icon="check" content="Create group" onClick={createGroup} loading={loading} />
|
<Button color="teal" icon="check" content="Create group" onClick={createGroup} loading={loading} />
|
||||||
</div>
|
</div>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
@ -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 React, { useState, useEffect } from 'react';
|
||||||
import { Input, Divider, Loader, Segment, Card, Dropdown, Button } from 'semantic-ui-react';
|
import { Input, Divider, Loader, Segment, Card, Dropdown, Button } from 'semantic-ui-react';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { useParams } from 'react-router-dom';
|
import { connect } from 'react-redux';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import actions from 'actions';
|
import actions from 'actions';
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
|
|
||||||
import ProjectCard from 'components/includes/ProjectCard';
|
import ProjectCard from 'components/includes/ProjectCard';
|
||||||
|
|
||||||
function Projects() {
|
function Projects({ group, myProjects, onReceiveProject, projectFilter, updateProjectFilter }) {
|
||||||
const [loadingProjects, setLoadingProjects] = useState(false);
|
const [loadingProjects, setLoadingProjects] = useState(false);
|
||||||
const [projects, setProjects] = useState([]);
|
const [projects, setProjects] = useState([]);
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { id } = useParams();
|
|
||||||
|
|
||||||
const { group, myProjects, projectFilter } = useSelector(state => {
|
|
||||||
let group;
|
|
||||||
state.groups.groups.forEach((g) => {
|
|
||||||
if (g._id === id) group = g;
|
|
||||||
});
|
|
||||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
|
||||||
const myProjects = state.projects.projects.filter(p => p.user === user?._id);
|
|
||||||
const projectFilter = state.groups.projectFilter;
|
|
||||||
return { group, myProjects, projectFilter };
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoadingProjects(true);
|
setLoadingProjects(true);
|
||||||
@ -42,7 +29,7 @@ function Projects() {
|
|||||||
if (index > -1) groupVisibility.splice(index, 1);
|
if (index > -1) groupVisibility.splice(index, 1);
|
||||||
else groupVisibility.push(group._id);
|
else groupVisibility.push(group._id);
|
||||||
api.projects.update(project.fullName, { groupVisibility }, updatedProject => {
|
api.projects.update(project.fullName, { groupVisibility }, updatedProject => {
|
||||||
dispatch(actions.projects.receiveProject(updatedProject));
|
onReceiveProject(updatedProject);
|
||||||
const existingIndex = projects.map(p => p._id).indexOf(updatedProject._id);
|
const existingIndex = projects.map(p => p._id).indexOf(updatedProject._id);
|
||||||
const newProjects = Object.assign([], projects);
|
const newProjects = Object.assign([], projects);
|
||||||
if (index > -1 && existingIndex > -1) newProjects.splice(existingIndex, 1);
|
if (index > -1 && existingIndex > -1) newProjects.splice(existingIndex, 1);
|
||||||
@ -79,7 +66,7 @@ function Projects() {
|
|||||||
<AddProject style={{float:'right'}} />
|
<AddProject style={{float:'right'}} />
|
||||||
</>}
|
</>}
|
||||||
{projects?.length > 0 &&
|
{projects?.length > 0 &&
|
||||||
<Input autoFocus style={{float:'right', marginRight: 5}} size='small' icon='search' value={projectFilter} onChange={e => 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 />
|
<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 React from 'react';
|
||||||
import { Header, Button, Divider, Segment, Form, } from 'semantic-ui-react';
|
import { Header, Button, Divider, Segment, Form, } from 'semantic-ui-react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import utils from 'utils/utils.js';
|
import utils from 'utils/utils.js';
|
||||||
import actions from 'actions';
|
import actions from 'actions';
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
|
|
||||||
function Settings() {
|
function Settings({ user, group, loading, onUpdateGroupLoading, onUpdateGroup, onDeleteGroup, history }) {
|
||||||
const navigate = useNavigate();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { id } = useParams();
|
|
||||||
|
|
||||||
const { loading, group } = useSelector(state => {
|
|
||||||
const { loading } = state.groups;
|
|
||||||
const group = state.groups.groups.filter(g => g._id === id)[0];
|
|
||||||
return { loading, group };
|
|
||||||
});
|
|
||||||
|
|
||||||
const saveGroup = () => {
|
const saveGroup = () => {
|
||||||
dispatch(actions.groups.request(true));
|
onUpdateGroupLoading(true);
|
||||||
const { _id, name, description, closed } = group;
|
const { _id, name, description, closed } = group;
|
||||||
api.groups.update(_id, { name, description, closed }, () => {
|
api.groups.update(_id, { name, description, closed }, () => {
|
||||||
dispatch(actions.groups.request(false));
|
onUpdateGroupLoading(false);
|
||||||
toast.info('Group updated');
|
toast.info('Group updated');
|
||||||
}, err => {
|
}, err => {
|
||||||
toast.error(err.message);
|
toast.error(err.message);
|
||||||
dispatch(actions.groups.request(false));
|
onUpdateGroupLoading(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const deleteGroup = () => {
|
const deleteGroup = () => {
|
||||||
utils.confirm('Really delete this group?', 'You\'ll lose all entries in the group feed and anything else you\'ve added to it.').then(() => {
|
utils.confirm('Really delete this group?', 'You\'ll lose all entries in the group feed and anything else you\'ve added to it.').then(() => {
|
||||||
api.groups.delete(group._id, () => {
|
api.groups.delete(group._id, () => {
|
||||||
toast.info('Group deleted');
|
toast.info('Group deleted');
|
||||||
dispatch(actions.groups.deleteGroup(group._id));
|
onDeleteGroup(group._id);
|
||||||
navigate(`/`);
|
history.push(`/`);
|
||||||
}, err => toast.error(err.message));
|
}, err => toast.error(err.message));
|
||||||
}, () => {});
|
}, () => {});
|
||||||
}
|
}
|
||||||
@ -44,9 +35,9 @@ function Settings() {
|
|||||||
<Segment color='blue'>
|
<Segment color='blue'>
|
||||||
<Header>About this group</Header>
|
<Header>About this group</Header>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Input label='Group name' value={group.name} onChange={e => dispatch(actions.groups.updateGroup(group._id, { name: e.target.value }))} />
|
<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 => dispatch(actions.groups.updateGroup(group._id, { description: 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) => dispatch(actions.groups.updateGroup(group._id, { closed: c.checked }))} />
|
<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)' }}>
|
<div style={{ marginLeft: 63, color: 'rgb(150,150,150)' }}>
|
||||||
<p>Closed groups are more restrictive and new members must be invited or approved to join. Members can join non-closed groups without being invited.</p>
|
<p>Closed groups are more restrictive and new members must be invited or approved to join. Members can join non-closed groups without being invited.</p>
|
||||||
</div>
|
</div>
|
||||||
@ -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 { Helmet } from 'react-helmet';
|
||||||
import { Icon, Form, Message, Grid, Input, Button, Divider } from 'semantic-ui-react';
|
import { Icon, Form, Message, Grid, Input, Button, Divider } from 'semantic-ui-react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import utils from 'utils/utils.js';
|
import utils from 'utils/utils.js';
|
||||||
import actions from 'actions';
|
import actions from 'actions';
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
@ -11,48 +11,44 @@ import api from 'api';
|
|||||||
import UserChip from 'components/includes/UserChip';
|
import UserChip from 'components/includes/UserChip';
|
||||||
import HelpLink from 'components/includes/HelpLink';
|
import HelpLink from 'components/includes/HelpLink';
|
||||||
|
|
||||||
function NewProject() {
|
class NewProject extends Component {
|
||||||
const [name, setName] = useState('My new project');
|
constructor(props) {
|
||||||
const [description, setDescription] = useState('');
|
super(props);
|
||||||
const [visibility, setVisibility] = useState('public');
|
this.state = {
|
||||||
const [openSource, setOpenSource] = useState(true);
|
name: 'My new project', description: '', visibility: 'public', openSource: true, groupVisibility: [], error: '', loading: false,
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const changeVisibility = (event, r) => {
|
updateName = (event) => {
|
||||||
setVisibility(r.checked ? 'private' : 'public');
|
this.setState({ name: event.target.value });
|
||||||
};
|
}
|
||||||
|
|
||||||
const changeOpenSource = (event, c) => {
|
changeVisibility = (event, r) => {
|
||||||
setOpenSource(c.checked);
|
this.setState({ visibility: r.checked ? 'private' : 'public' });
|
||||||
};
|
}
|
||||||
|
|
||||||
const createProject = () => {
|
changeOpenSource = (event, c) => {
|
||||||
setLoading(true);
|
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) => {
|
api.projects.create({ name, description, visibility, openSource, groupVisibility }, (project) => {
|
||||||
dispatch(actions.projects.receiveProject(project));
|
this.props.onReceiveProject(project);
|
||||||
setLoading(false);
|
this.setState({ loading: false });
|
||||||
navigate(`/${user.username}/${project.path}`);
|
this.props.history.push(`/${this.props.user.username}/${project.path}`);
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
setLoading(false);
|
this.setState({ loading: false });
|
||||||
toast.error(err.message);
|
toast.error(err.message);
|
||||||
setError(err.message);
|
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
name, description, visibility, openSource, groupVisibility
|
||||||
|
} = this.state;
|
||||||
|
const { user, groups } = this.props;
|
||||||
return (
|
return (
|
||||||
<Grid stackable centered>
|
<Grid stackable centered>
|
||||||
<Helmet title={'Create Project'} />
|
<Helmet title={'Create Project'} />
|
||||||
@ -70,17 +66,17 @@ Create a new project
|
|||||||
|
|
||||||
<h3>About your project</h3>
|
<h3>About your project</h3>
|
||||||
<p>Give your project a short name. You can always rename it later on.</p>
|
<p>Give your project a short name. You can always rename it later on.</p>
|
||||||
<Input autoFocus type="text" fluid onChange={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 />
|
<Divider hidden />
|
||||||
<p>Write a project description (optional).</p>
|
<p>Write a project description (optional).</p>
|
||||||
<Form><Form.TextArea placeholder="Project description (optional)..." value={description} onChange={e => 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 />
|
<Divider section />
|
||||||
|
|
||||||
<h3>Project visibility</h3>
|
<h3>Project visibility</h3>
|
||||||
<Form>
|
<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>
|
<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>
|
<p style={{color:'rgb(150,150,150)'}}><Icon name='info' /> Open-source projects allow other people to download any pattern source files they contain in WIF format.</p>
|
||||||
<Divider hidden />
|
<Divider hidden />
|
||||||
|
|
||||||
@ -93,7 +89,7 @@ Create a new project
|
|||||||
label='Make this project always visible to members of these groups'
|
label='Make this project always visible to members of these groups'
|
||||||
value={groupVisibility}
|
value={groupVisibility}
|
||||||
options={groups.map(g => ({ key: g._id, value: g._id, text: g.name }))}
|
options={groups.map(g => ({ key: g._id, value: g._id, text: g.name }))}
|
||||||
onChange={(e, s) => setGroupVisibility(s.value)}
|
onChange={(e, s) => this.setState({ groupVisibility: s.value })}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@ -101,13 +97,27 @@ Create a new project
|
|||||||
|
|
||||||
<Divider section />
|
<Divider section />
|
||||||
|
|
||||||
{error && <Message color="orange" content={error} />}
|
{this.state.error && <Message color="orange" content={this.state.error} />}
|
||||||
<div style={{textAlign:'right'}}>
|
<div style={{textAlign:'right'}}>
|
||||||
<Button basic onClick={() => navigate(-1)}>Cancel</Button>
|
<Button basic onClick={this.props.history.goBack}>Cancel</Button>
|
||||||
<Button color="teal" icon="check" content="Create project" onClick={createProject} loading={loading} />
|
<Button color="teal" icon="check" content="Create project" onClick={this.createProject} loading={this.state.loading} />
|
||||||
</div>
|
</div>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default NewProject;
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
||||||
|
const groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
|
||||||
|
return { user, groups };
|
||||||
|
};
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
onReceiveProject: project => dispatch(actions.projects.receiveProject(project)),
|
||||||
|
});
|
||||||
|
const NewProjectContainer = withRouter(connect(
|
||||||
|
mapStateToProps, mapDispatchToProps,
|
||||||
|
)(NewProject));
|
||||||
|
|
||||||
|
export default NewProjectContainer;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Segment, Label, Input, Icon, Card, Loader } from 'semantic-ui-react';
|
import { Segment, Label, Input, Icon, Card, Loader } from 'semantic-ui-react';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link, withRouter } from 'react-router-dom';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import utils from 'utils/utils.js';
|
import utils from 'utils/utils.js';
|
||||||
import actions from 'actions';
|
import actions from 'actions';
|
||||||
@ -22,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 [loading, setLoading] = useState(false);
|
||||||
const [objectFilter, setObjectFilter] = useState('');
|
const [objectFilter, setObjectFilter] = useState('');
|
||||||
const { username, projectPath, objectId } = useParams();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const { user, project, objects, currentObject, fullProjectPath } = useSelector(state => {
|
|
||||||
const project = state.projects.projects.filter(p => p.path === projectPath && p.owner && p.owner.username === username)[0];
|
|
||||||
const objects = [];
|
|
||||||
let currentObject;
|
|
||||||
state.objects.objects.forEach((d) => {
|
|
||||||
if (d.project === project._id) objects.push(d);
|
|
||||||
if (d._id === objectId) currentObject = d;
|
|
||||||
});
|
|
||||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
|
||||||
return { user, project, objects, currentObject, fullProjectPath: `${username}/${projectPath}` };
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
api.projects.getObjects(fullProjectPath, o => {
|
api.projects.getObjects(fullProjectPath, projects => {
|
||||||
dispatch(actions.objects.receiveMultiple(o));
|
onReceiveObjects(projects);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, err => setLoading(false));
|
}, err => setLoading(false));
|
||||||
}, [fullProjectPath, dispatch]);
|
}, [fullProjectPath, onReceiveObjects]);
|
||||||
|
|
||||||
let filteredObjects = objects;
|
let filteredObjects = objects;
|
||||||
if (objectFilter) {
|
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 React, { useState, useEffect } from 'react';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { Input, Message, Icon, Button, Dropdown } from 'semantic-ui-react';
|
import { Input, Message, Icon, Button, Dropdown } from 'semantic-ui-react';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { toast } from 'react-toastify';
|
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 moment from 'moment';
|
||||||
import utils from 'utils/utils.js';
|
import utils from 'utils/utils.js';
|
||||||
import actions from 'actions';
|
import actions from 'actions';
|
||||||
@ -15,42 +15,23 @@ import DraftPreview from './objects/DraftPreview';
|
|||||||
import NewFeedMessage from 'components/includes/NewFeedMessage';
|
import NewFeedMessage from 'components/includes/NewFeedMessage';
|
||||||
import FeedMessage from 'components/includes/FeedMessage';
|
import FeedMessage from 'components/includes/FeedMessage';
|
||||||
|
|
||||||
function ObjectViewer() {
|
function ObjectViewer({ user, myProjects, project, fullProjectPath, onEditObject, onDeleteObject, object, comments, history, onReceiveComment, onDeleteComment }) {
|
||||||
const [editingName, setEditingName] = useState(false);
|
const [editingName, setEditingName] = useState(false);
|
||||||
const [editingDescription, setEditingDescription] = useState(false);
|
const [editingDescription, setEditingDescription] = useState(false);
|
||||||
const [downloading, setDownloading] = useState(false);
|
const [downloading, setDownloading] = useState(false);
|
||||||
const navigate = useNavigate();
|
const objectId = object?._id;
|
||||||
const { username, projectPath, objectId } = useParams();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const { user, myProjects, project, fullProjectPath, object, comments } = useSelector(state => {
|
|
||||||
const project = state.projects.projects.filter(p => p.path === projectPath && p.owner && p.owner.username === username)[0];
|
|
||||||
const objects = [];
|
|
||||||
state.objects.objects.forEach((d) => {
|
|
||||||
if (d.project === project._id) objects.push(d);
|
|
||||||
});
|
|
||||||
const object = objects.filter(o => o._id === objectId)[0];
|
|
||||||
const comments = state.objects.comments?.filter(c => c.object === object?._id).sort((a, b) => {
|
|
||||||
const aDate = new Date(a.createdAt);
|
|
||||||
const bDate = new Date(b.createdAt);
|
|
||||||
return aDate < bDate;
|
|
||||||
});
|
|
||||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
|
||||||
const myProjects = state.projects.projects.filter(p => p.owner?.username === user?.username);
|
|
||||||
return { user, myProjects, project, fullProjectPath: `${username}/${projectPath}`, object, comments };
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!objectId) return;
|
if (!objectId) return;
|
||||||
api.objects.getComments(objectId, data => {
|
api.objects.getComments(objectId, data => {
|
||||||
data.comments.forEach(c => dispatch(actions.objects.receiveComment(c)));
|
data.comments.forEach(onReceiveComment);
|
||||||
});
|
});
|
||||||
}, [dispatch, objectId]);
|
}, [objectId, onReceiveComment]);
|
||||||
|
|
||||||
const deleteObject = (object) => {
|
const deleteObject = (object) => {
|
||||||
utils.confirm('Delete object', 'Really delete this object? This cannot be undone.').then(() => {
|
utils.confirm('Delete object', 'Really delete this object? This cannot be undone.').then(() => {
|
||||||
navigate(`/${fullProjectPath}`);
|
history.push(`/${fullProjectPath}`);
|
||||||
api.objects.delete(object._id, () => dispatch(actions.objects.delete(object._id)), err => toast.error(err.message));
|
api.objects.delete(object._id, () => onDeleteObject(object._id), err => toast.error(err.message));
|
||||||
}, () => {});
|
}, () => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,7 +146,7 @@ function ObjectViewer() {
|
|||||||
{editingName ?
|
{editingName ?
|
||||||
<div style={{marginBottom: 5}}>
|
<div style={{marginBottom: 5}}>
|
||||||
<Input autoFocus size="small" value={object.name} style={{marginBottom: 10}}
|
<Input autoFocus size="small" value={object.name} style={{marginBottom: 10}}
|
||||||
onChange={e => 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)} />
|
action=<Button icon="check" primary onClick={e => saveObjectName(object)} />
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -185,7 +166,7 @@ function ObjectViewer() {
|
|||||||
<div style={{marginTop: 20, marginBottom: 20, padding: 10, border: '1px solid rgb(240,240,240)'}}>
|
<div style={{marginTop: 20, marginBottom: 20, padding: 10, border: '1px solid rgb(240,240,240)'}}>
|
||||||
{object.type === 'pattern' &&
|
{object.type === 'pattern' &&
|
||||||
<div style={{maxHeight: 400, overflowY: 'scroll'}}>
|
<div style={{maxHeight: 400, overflowY: 'scroll'}}>
|
||||||
<DraftPreview object={object} />
|
<DraftPreview object={object} onImageLoaded={i => onEditObject(object._id, 'patternImage', i)}/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{object.isImage &&
|
{object.isImage &&
|
||||||
@ -201,7 +182,7 @@ function ObjectViewer() {
|
|||||||
|
|
||||||
{editingDescription ?
|
{editingDescription ?
|
||||||
<div style={{padding: 10, border: '1px solid rgb(230,230,230)'}}>
|
<div style={{padding: 10, border: '1px solid rgb(230,230,230)'}}>
|
||||||
<RichText value={object.description} onChange={t => 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)} />
|
<Button primary size="small" icon="check" content="Save description" onClick={e => saveObjectDescription(object)} />
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
@ -221,14 +202,42 @@ function ObjectViewer() {
|
|||||||
}
|
}
|
||||||
</h3>
|
</h3>
|
||||||
{user ?
|
{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>
|
: <Message size='small'>Please login to write your own comments.</Message>
|
||||||
}
|
}
|
||||||
{comments?.map(c =>
|
{comments?.map(c =>
|
||||||
<FeedMessage key={c._id} user={user} forType='object' object={object} post={c} replies={[]} onDeleted={id => dispatch(actions.objects.deleteComment(id))} />
|
<FeedMessage key={c._id} user={user} forType='object' object={object} post={c} replies={[]} onDeleted={onDeleteComment} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default ObjectViewer;
|
|
||||||
|
const mapStateToProps = (state, ownProps) => {
|
||||||
|
const { username, projectPath, objectId } = ownProps.match.params;
|
||||||
|
const project = state.projects.projects.filter(p => p.path === projectPath && p.owner && p.owner.username === username)[0];
|
||||||
|
const objects = [];
|
||||||
|
state.objects.objects.forEach((d) => {
|
||||||
|
if (d.project === project._id) objects.push(d);
|
||||||
|
});
|
||||||
|
const object = objects.filter(o => o._id === objectId)[0];
|
||||||
|
const comments = state.objects.comments?.filter(c => c.object === object?._id).sort((a, b) => {
|
||||||
|
const aDate = new Date(a.createdAt);
|
||||||
|
const bDate = new Date(b.createdAt);
|
||||||
|
return aDate < bDate;
|
||||||
|
});
|
||||||
|
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
||||||
|
const myProjects = state.projects.projects.filter(p => p.owner?.username === user?.username);
|
||||||
|
return { user, myProjects, project, objects, fullProjectPath: `${username}/${projectPath}`, object, comments };
|
||||||
|
};
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
onEditObject: (id, field, value) => dispatch(actions.objects.update(id, field, value)),
|
||||||
|
onDeleteObject: id => dispatch(actions.objects.delete(id)),
|
||||||
|
onReceiveComment: c => dispatch(actions.objects.receiveComment(c)),
|
||||||
|
onDeleteComment: id => dispatch(actions.objects.deleteComment(id)),
|
||||||
|
});
|
||||||
|
const ObjectViewerContainer = withRouter(connect(
|
||||||
|
mapStateToProps, mapDispatchToProps,
|
||||||
|
)(ObjectViewer));
|
||||||
|
|
||||||
|
export default ObjectViewerContainer;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { Message, Form, TextArea, Container, Button, Icon, Grid, Card } from 'semantic-ui-react';
|
import { Message, Form, TextArea, Container, Button, Icon, Grid, Card } from 'semantic-ui-react';
|
||||||
import { Outlet, Link, useParams, useLocation } from 'react-router-dom';
|
import { Switch, Route, Link, withRouter } from 'react-router-dom';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import utils from 'utils/utils.js';
|
import utils from 'utils/utils.js';
|
||||||
import actions from 'actions';
|
import actions from 'actions';
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
@ -11,28 +11,24 @@ import UserChip from 'components/includes/UserChip';
|
|||||||
import HelpLink from 'components/includes/HelpLink';
|
import HelpLink from 'components/includes/HelpLink';
|
||||||
import ObjectCreator from 'components/includes/ObjectCreator';
|
import ObjectCreator from 'components/includes/ObjectCreator';
|
||||||
import FormattedMessage from 'components/includes/FormattedMessage';
|
import FormattedMessage from 'components/includes/FormattedMessage';
|
||||||
|
import Draft from './objects/Draft.js';
|
||||||
|
import ObjectList from './ObjectList.js';
|
||||||
|
import ProjectObjects from './ProjectObjects.js';
|
||||||
|
import ProjectSettings from './Settings.js';
|
||||||
|
|
||||||
function Project() {
|
function Project({ user, project, fullName, errorMessage, editingDescription, onUpdateProject, onReceiveProject, onEditDescription, onRequest, onRequestFailed, match, history }) {
|
||||||
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
|
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
|
||||||
const { username, projectPath } = useParams();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const location = useLocation();
|
|
||||||
const { user, project, fullName, errorMessage, editingDescription } = useSelector(state => {
|
|
||||||
const project = state.projects.projects.filter(p => p.path === projectPath && p.owner && p.owner.username === username)[0];
|
|
||||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
|
||||||
return { user, project, fullName: `${username}/${projectPath}`, errorMessage: state.projects.errorMessage, editingDescription: state.projects.editingDescription };
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(actions.projects.request());
|
onRequest();
|
||||||
api.projects.get(fullName, p => dispatch(actions.projects.receiveProject(p)), err => dispatch(actions.projects.requestFailed(err)));
|
api.projects.get(fullName, onReceiveProject, onRequestFailed);
|
||||||
}, [user, dispatch, fullName]);
|
}, [user, onRequest, fullName, onReceiveProject, onRequestFailed]);
|
||||||
|
|
||||||
const wideBody = () => !location.pathname.toLowerCase().endsWith(fullName.toLowerCase());
|
const wideBody = () => !match.isExact
|
||||||
|
|
||||||
const saveDescription = () => {
|
const saveDescription = () => {
|
||||||
dispatch(actions.projects.editDescription(false));
|
onEditDescription(false);
|
||||||
api.projects.update(fullName, { description: project.description }, p => dispatch(actions.projects.receiveProject(p)));
|
api.projects.update(fullName, { description: project.description }, onReceiveProject);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDescription = () => {
|
const getDescription = () => {
|
||||||
@ -54,11 +50,11 @@ function Project() {
|
|||||||
{project
|
{project
|
||||||
&& (
|
&& (
|
||||||
<div>
|
<div>
|
||||||
{/*history.location?.state?.prevPath &&
|
{history.location?.state?.prevPath &&
|
||||||
<div style={{marginBottom:15}}>
|
<div style={{marginBottom:15}}>
|
||||||
<Button basic secondary onClick={e => history.goBack()} icon='arrow left' content='Go back' />
|
<Button basic secondary onClick={e => history.goBack()} icon='arrow left' content='Go back' />
|
||||||
</div>
|
</div>
|
||||||
*/}
|
}
|
||||||
|
|
||||||
{wideBody() && project.owner &&
|
{wideBody() && project.owner &&
|
||||||
<>
|
<>
|
||||||
@ -89,7 +85,7 @@ function Project() {
|
|||||||
{editingDescription
|
{editingDescription
|
||||||
? (
|
? (
|
||||||
<Form>
|
<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>
|
<Button size="tiny" color="green" fluid onClick={saveDescription}>Save description</Button>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
@ -104,7 +100,7 @@ function Project() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{utils.canEditProject(user, project) && (
|
{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
|
<Icon name="pencil" /> {project.description ? 'Edit' : 'Add a project'} description
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
@ -125,7 +121,14 @@ function Project() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Grid.Column computer={wideBody() ? 16 : 12} tablet={wideBody() ? 16 : 10}>
|
<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.Column>
|
||||||
</Grid>
|
</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 { 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 { toast } from 'react-toastify';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import utils from 'utils/utils.js';
|
import utils from 'utils/utils.js';
|
||||||
import actions from 'actions';
|
import actions from 'actions';
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
|
|
||||||
import HelpLink from 'components/includes/HelpLink';
|
import HelpLink from 'components/includes/HelpLink';
|
||||||
|
class ProjectSettings extends Component {
|
||||||
function ProjectSettings() {
|
constructor(props) {
|
||||||
const { username, projectPath } = useParams();
|
super(props);
|
||||||
const { groups, project, fullProjectPath } = useSelector(state => {
|
this.state = { name: props.project.name, visibility: props.project.visibility || 'public', openSource: props.project.openSource, groupVisibility: props.project.groupVisibility || [] };
|
||||||
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));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>Project settings</h2>
|
<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>
|
<p>Changing the name of your project will also change its URL. If you have previously shared a link to your project, then visitors may not be able to find it if the name is changed.</p>
|
||||||
<Input
|
<Input
|
||||||
type="text" value={name}
|
type="text" value={name}
|
||||||
action=<Button color="teal" content="Update" onClick={saveName} />
|
action=<Button color="teal" content="Update" onClick={this.saveName} />
|
||||||
onChange={e => setName(e.target.value)}
|
onChange={e => this.setState({ name: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</Segment>
|
</Segment>
|
||||||
|
|
||||||
@ -83,9 +77,9 @@ function ProjectSettings() {
|
|||||||
<Segment color="yellow">
|
<Segment color="yellow">
|
||||||
<h3>Project visibility</h3>
|
<h3>Project visibility</h3>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Checkbox label='This is a private project' checked={visibility === 'private'} onChange={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>
|
<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>
|
<p style={{color:'rgb(150,150,150)'}}><Icon name='info' /> Open-source projects allow other people to download any pattern source files they contain in WIF format.</p>
|
||||||
<Divider hidden />
|
<Divider hidden />
|
||||||
|
|
||||||
@ -96,16 +90,16 @@ function ProjectSettings() {
|
|||||||
|
|
||||||
<Form.Select multiple
|
<Form.Select multiple
|
||||||
label='Make this project always visible to members of these groups'
|
label='Make this project always visible to members of these groups'
|
||||||
value={groupVisibility}
|
value={this.state.groupVisibility}
|
||||||
options={groups.map(g => ({ key: g._id, value: g._id, text: g.name }))}
|
options={groups.map(g => ({ key: g._id, value: g._id, text: g.name }))}
|
||||||
onChange={(e, s) => setGroupVisibility(s.value)}
|
onChange={(e, s) => this.setState({ groupVisibility: s.value })}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</Form>
|
</Form>
|
||||||
<Divider hidden />
|
<Divider hidden />
|
||||||
|
|
||||||
<Button color="teal" content="Save visibility" onClick={saveVisibility} />
|
<Button color="teal" content="Save visibility" onClick={this.saveVisibility} />
|
||||||
</Segment>
|
</Segment>
|
||||||
|
|
||||||
<Divider hidden section />
|
<Divider hidden section />
|
||||||
@ -113,10 +107,28 @@ function ProjectSettings() {
|
|||||||
<Segment color="red">
|
<Segment color="red">
|
||||||
<h3>Delete project</h3>
|
<h3>Delete project</h3>
|
||||||
<p>Immediately and irreversibly delete this project, along with all of its contents.</p>
|
<p>Immediately and irreversibly delete this project, along with all of its contents.</p>
|
||||||
<Button icon="trash" content="Delete project" color="red" onClick={deleteProject} />
|
<Button icon="trash" content="Delete project" color="red" onClick={this.deleteProject} />
|
||||||
</Segment>
|
</Segment>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default ProjectSettings;
|
|
||||||
|
const mapStateToProps = (state, ownProps) => {
|
||||||
|
const { username, projectPath } = ownProps.match.params;
|
||||||
|
const project = state.projects.projects.filter(p => p.path === projectPath && p.owner && p.owner.username === username)[0];
|
||||||
|
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
||||||
|
const groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
|
||||||
|
return { user, groups, project, fullProjectPath: `${username}/${projectPath}` };
|
||||||
|
};
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
onDeleteProject: id => dispatch(actions.projects.deleteProject(id)),
|
||||||
|
onReceiveProject: project => dispatch(actions.projects.receiveProject(project)),
|
||||||
|
onUpdateProject: (id, update) => dispatch(actions.projects.updateProject(id, update)),
|
||||||
|
});
|
||||||
|
const ProjectSettingsContainer = withRouter(connect(
|
||||||
|
mapStateToProps, mapDispatchToProps,
|
||||||
|
)(ProjectSettings));
|
||||||
|
|
||||||
|
export default ProjectSettingsContainer;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { withRouter, Prompt } from 'react-router-dom';
|
||||||
import { useParams } from 'react-router-dom';
|
import { connect } from 'react-redux';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import ElementPan from 'components/includes/ElementPan';
|
import ElementPan from 'components/includes/ElementPan';
|
||||||
@ -25,63 +25,67 @@ export const StyledPattern = styled.div`
|
|||||||
margin:20px;
|
margin:20px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function Draft() {
|
class Draft extends Component {
|
||||||
const [unsaved, setUnsaved] = useState(false);
|
constructor(props) {
|
||||||
const [saving, setSaving] = useState(false);
|
super(props);
|
||||||
const [object, setObject] = useState();
|
this.state = { unsaved: false, saving: false };
|
||||||
const [pattern, setPattern] = useState();
|
}
|
||||||
const [name] = useState();
|
|
||||||
const { objectId } = useParams();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { editor } = useSelector(state => ({ editor: state.objects.editor }));
|
|
||||||
|
|
||||||
useEffect(() => {
|
componentDidMount() {
|
||||||
api.objects.get(objectId, (o) => {
|
api.objects.get(this.props.match.params.objectId, (object) => {
|
||||||
if (!o.pattern.baseSize) o.pattern.baseSize = 10;
|
if (!object.pattern.baseSize) object.pattern.baseSize = 10;
|
||||||
setObject(o);
|
this.setState(object);
|
||||||
setPattern(o.pattern);
|
|
||||||
});
|
});
|
||||||
}, [objectId]);
|
}
|
||||||
|
|
||||||
const updateObject = (update) => {
|
updateObject = (update) => {
|
||||||
setObject(Object.assign({}, object, update));
|
this.setState(Object.assign({}, this.state, update));
|
||||||
setUnsaved(true);
|
this.setState({ unsaved: true });
|
||||||
};
|
}
|
||||||
|
|
||||||
const updatePattern = (update) => {
|
updatePattern = (update) => {
|
||||||
const newPattern = Object.assign({}, pattern, update);
|
const newPattern = Object.assign({}, this.state.pattern, update);
|
||||||
setPattern(Object.assign({}, pattern, newPattern));
|
this.setState(Object.assign({}, this.state, { pattern: newPattern }));
|
||||||
setUnsaved(true);
|
this.setState({ unsaved: true });
|
||||||
};
|
}
|
||||||
|
|
||||||
const saveObject = () => {
|
saveObject = () => {
|
||||||
setSaving(true);
|
this.setState({ saving: true });
|
||||||
const canvas = document.getElementsByClassName('drawdown')[0];
|
const canvas = document.getElementsByClassName('drawdown')[0];
|
||||||
const newObject = Object.assign({}, object);
|
const object = Object.assign({}, this.state);
|
||||||
newObject.preview = canvas.toDataURL();
|
object.preview = canvas.toDataURL();
|
||||||
api.objects.update(objectId, newObject, (o) => {
|
api.objects.update(this.props.match.params.objectId, object, (o) => {
|
||||||
toast.success('Pattern saved');
|
toast.success('Pattern saved');
|
||||||
dispatch(actions.objects.receive(o));
|
this.props.onReceiveObject(o);
|
||||||
setUnsaved(false);
|
this.setState({ unsaved: false, saving: false });
|
||||||
setSaving(false);
|
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
toast.error(err.message);
|
toast.error(err.message);
|
||||||
setSaving(false);
|
this.setState({ saving: false });
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
if (!pattern) return null;
|
rerunTour = () => {
|
||||||
const { warp, weft, tieups, baseSize } = pattern;
|
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.state.pattern) return null;
|
||||||
|
const { unsaved, saving } = this.state;
|
||||||
|
const { warp, weft, tieups, baseSize } = this.state.pattern;
|
||||||
const cellStyle = { width: `${baseSize || 10}px`, height: `${baseSize || 10}px` };
|
const cellStyle = { width: `${baseSize || 10}px`, height: `${baseSize || 10}px` };
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Helmet title={`${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} />
|
<Tour id='pattern' run={true} />
|
||||||
<div style={{display: 'flex'}}>
|
<div style={{display: 'flex'}}>
|
||||||
|
|
||||||
<div style={{flex: 1, overflow: 'hidden'}}>
|
<div style={{flex: 1, overflow: 'hidden'}}>
|
||||||
<ElementPan
|
<ElementPan
|
||||||
disabled={!(editor?.tool === 'pan')}
|
disabled={!(this.props.editor && this.props.editor.tool === 'pan')}
|
||||||
startX={5000}
|
startX={5000}
|
||||||
startY={0}
|
startY={0}
|
||||||
>
|
>
|
||||||
@ -92,9 +96,9 @@ function Draft() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
||||||
<Warp baseSize={baseSize} cellStyle={cellStyle} warp={warp} weft={weft} updatePattern={updatePattern} />
|
<Warp baseSize={baseSize} cellStyle={cellStyle} warp={warp} weft={weft} updatePattern={this.updatePattern} />
|
||||||
<Weft cellStyle={cellStyle} warp={warp} weft={weft} baseSize={baseSize} updatePattern={updatePattern} />
|
<Weft cellStyle={cellStyle} warp={warp} weft={weft} baseSize={baseSize} updatePattern={this.updatePattern} />
|
||||||
<Tieups cellStyle={cellStyle} warp={warp} weft={weft} tieups={tieups} updatePattern={updatePattern} baseSize={baseSize}/>
|
<Tieups cellStyle={cellStyle} warp={warp} weft={weft} tieups={tieups} updatePattern={this.updatePattern} baseSize={baseSize}/>
|
||||||
<Drawdown warp={warp} weft={weft} tieups={tieups} baseSize={baseSize} />
|
<Drawdown warp={warp} weft={weft} tieups={tieups} baseSize={baseSize} />
|
||||||
|
|
||||||
</StyledPattern>
|
</StyledPattern>
|
||||||
@ -104,12 +108,21 @@ function Draft() {
|
|||||||
<div style={{width: 300, marginLeft: 20}}>
|
<div style={{width: 300, marginLeft: 20}}>
|
||||||
<HelpLink className='joyride-help' link={`${process.env.REACT_APP_SUPPORT_ROOT}Editing-patterns#using-the-pattern-editor`} marginBottom/>
|
<HelpLink className='joyride-help' link={`${process.env.REACT_APP_SUPPORT_ROOT}Editing-patterns#using-the-pattern-editor`} marginBottom/>
|
||||||
<ReRunTour id='pattern' />
|
<ReRunTour id='pattern' />
|
||||||
<Tools warp={warp} weft={weft} object={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>
|
</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 React, { Component } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Loader } from 'semantic-ui-react';
|
import { Loader } from 'semantic-ui-react';
|
||||||
import actions from 'actions';
|
import actions from 'actions';
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
@ -11,40 +11,40 @@ import Weft from './Weft.js';
|
|||||||
import Tieups from './Tieups.js';
|
import Tieups from './Tieups.js';
|
||||||
import Drawdown from './Drawdown.js';
|
import Drawdown from './Drawdown.js';
|
||||||
|
|
||||||
function DraftPreview({ object }) {
|
class DraftPreview extends Component {
|
||||||
const [loading, setLoading] = useState(false);
|
constructor(props) {
|
||||||
const [pattern, setPattern] = useState();
|
super(props);
|
||||||
const dispatch = useDispatch();
|
this.state = { loading: false };
|
||||||
const objectId = object?._id;
|
}
|
||||||
|
|
||||||
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(() => {
|
setTimeout(() => {
|
||||||
const c = document.getElementsByClassName('drawdown')[0];
|
const c = document.getElementsByClassName('drawdown')[0];
|
||||||
const preview = c?.toDataURL();
|
const preview = c && c.toDataURL();
|
||||||
if (preview) {
|
if (preview) {
|
||||||
api.objects.update(objectId, { preview }, () => {
|
api.objects.update(object._id, { preview }, () => {
|
||||||
dispatch(actions.objects.update(objectId, 'preview', preview));
|
this.props.onEditObject(object._id, 'preview', preview);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 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));
|
}, err => this.setState({ loading: false }));
|
||||||
}, [dispatch, objectId, generatePreview]);
|
}
|
||||||
|
|
||||||
const unifyCanvas = useCallback(() => {
|
unifyCanvas() {
|
||||||
if (!pattern) return;
|
|
||||||
const { warp, weft } = pattern;
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
const id = this.props.object._id;
|
||||||
|
const { warp, weft } = this.state.pattern;
|
||||||
const baseSize = 6;
|
const baseSize = 6;
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
@ -53,12 +53,12 @@ function DraftPreview({ object }) {
|
|||||||
ctx.fillStyle = 'white';
|
ctx.fillStyle = 'white';
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
ctx.fillStyle = 'black';
|
ctx.fillStyle = 'black';
|
||||||
const warpCanvas = document.querySelector(`.preview-${objectId} .warp-threads`);
|
const warpCanvas = document.querySelector(`.preview-${id} .warp-threads`);
|
||||||
const warpColourwayCanvas = document.querySelector(`.preview-${objectId} .warp-colourway`);
|
const warpColourwayCanvas = document.querySelector(`.preview-${id} .warp-colourway`);
|
||||||
const weftCanvas = document.querySelector(`.preview-${objectId} .weft-threads`);
|
const weftCanvas = document.querySelector(`.preview-${id} .weft-threads`);
|
||||||
const weftColourwayCanvas = document.querySelector(`.preview-${objectId} .weft-colourway`);
|
const weftColourwayCanvas = document.querySelector(`.preview-${id} .weft-colourway`);
|
||||||
const drawdownCanvas = document.querySelector(`.preview-${objectId} .drawdown`);
|
const drawdownCanvas = document.querySelector(`.preview-${id} .drawdown`);
|
||||||
const tieupsCanvas = document.querySelector(`.preview-${objectId} .tieups`);
|
const tieupsCanvas = document.querySelector(`.preview-${id} .tieups`);
|
||||||
if (warpCanvas) {
|
if (warpCanvas) {
|
||||||
ctx.drawImage(warpColourwayCanvas, canvas.width - warpCanvas.width - weft.treadles * baseSize - 20, 0);
|
ctx.drawImage(warpColourwayCanvas, canvas.width - warpCanvas.width - weft.treadles * baseSize - 20, 0);
|
||||||
ctx.drawImage(warpCanvas, canvas.width - warpCanvas.width - weft.treadles * baseSize - 20, 10);
|
ctx.drawImage(warpCanvas, canvas.width - warpCanvas.width - weft.treadles * baseSize - 20, 10);
|
||||||
@ -66,18 +66,16 @@ function DraftPreview({ object }) {
|
|||||||
ctx.drawImage(weftColourwayCanvas, canvas.width - 10, warp.shafts * baseSize + 20);
|
ctx.drawImage(weftColourwayCanvas, canvas.width - 10, warp.shafts * baseSize + 20);
|
||||||
ctx.drawImage(tieupsCanvas, canvas.width - weft.treadles * baseSize - 10, 10);
|
ctx.drawImage(tieupsCanvas, canvas.width - weft.treadles * baseSize - 10, 10);
|
||||||
ctx.drawImage(drawdownCanvas, canvas.width - drawdownCanvas.width - weft.treadles * baseSize - 20, warp.shafts * baseSize + 20);
|
ctx.drawImage(drawdownCanvas, canvas.width - drawdownCanvas.width - weft.treadles * baseSize - 20, warp.shafts * baseSize + 20);
|
||||||
dispatch(actions.objects.update(objectId, 'patternImage', canvas.toDataURL()));
|
|
||||||
|
this.props.onImageLoaded(canvas.toDataURL());
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
}, [dispatch, objectId, pattern]);
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
render() {
|
||||||
unifyCanvas();
|
if (this.state.loading) return <Loader active />;
|
||||||
}, [unifyCanvas])
|
if (!this.state.pattern) return null;
|
||||||
|
const { warp, weft, tieups } = this.state.pattern;
|
||||||
if (loading) return <Loader active />;
|
|
||||||
if (!pattern) return null;
|
|
||||||
const { warp, weft, tieups } = pattern;
|
|
||||||
if (!warp || !weft || !tieups) return null;
|
if (!warp || !weft || !tieups) return null;
|
||||||
const baseSize = 6;
|
const baseSize = 6;
|
||||||
const cellStyle = { width: `${baseSize || 10}px`, height: `${baseSize || 10}px` };
|
const cellStyle = { width: `${baseSize || 10}px`, height: `${baseSize || 10}px` };
|
||||||
@ -87,7 +85,7 @@ function DraftPreview({ object }) {
|
|||||||
startY={0}
|
startY={0}
|
||||||
>
|
>
|
||||||
<StyledPattern
|
<StyledPattern
|
||||||
className={`pattern preview-${objectId}`}
|
className={`pattern preview-${this.props.object._id}`}
|
||||||
style={{
|
style={{
|
||||||
width: '2000px',
|
width: '2000px',
|
||||||
height: '1000px',
|
height: '1000px',
|
||||||
@ -101,5 +99,14 @@ function DraftPreview({ object }) {
|
|||||||
</ElementPan>
|
</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 React, { Component } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import utils from 'utils/utils';
|
import utils from 'utils/utils';
|
||||||
|
|
||||||
@ -12,17 +12,23 @@ const StyledDrawdown = styled.canvas`
|
|||||||
width: ${props => props.warp.threads * props.baseSize}px;
|
width: ${props => props.warp.threads * props.baseSize}px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Cache
|
class Drawdown extends Component {
|
||||||
const squares = {};
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.squares = {};
|
||||||
|
}
|
||||||
|
|
||||||
function Drawdown({ baseSize, warp, weft, tieups }) {
|
componentDidMount() {
|
||||||
const drawdownRef = useRef();
|
this.paintDrawdown();
|
||||||
useEffect(() => paintDrawdown());
|
}
|
||||||
const { editor } = useSelector(state => ({ editor: state.objects.editor }));
|
|
||||||
|
|
||||||
const getSquare = (thread, size, colour) => {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
const { view } = editor;
|
this.paintDrawdown(prevProps);
|
||||||
if (squares[view] && squares[view][thread] && squares[view][thread][size] && squares[view][thread][size][colour]) return squares[view][thread][size][colour];
|
}
|
||||||
|
|
||||||
|
getSquare(thread, size, colour) {
|
||||||
|
const { view } = this.props.editor;
|
||||||
|
if (this.squares[view] && this.squares[view][thread] && this.squares[view][thread][size] && this.squares[view][thread][size][colour]) return this.squares[view][thread][size][colour];
|
||||||
const m_canvas = document.createElement('canvas');
|
const m_canvas = document.createElement('canvas');
|
||||||
m_canvas.width = size;
|
m_canvas.width = size;
|
||||||
m_canvas.height = size;
|
m_canvas.height = size;
|
||||||
@ -37,7 +43,7 @@ function Drawdown({ baseSize, warp, weft, tieups }) {
|
|||||||
if (view === 'colour' || view === 'interlacement') {
|
if (view === 'colour' || view === 'interlacement') {
|
||||||
mc.fillStyle = colour;
|
mc.fillStyle = colour;
|
||||||
mc.fillRect(0, 0, size, size);
|
mc.fillRect(0, 0, size, size);
|
||||||
if (editor.view === 'interlacement') {
|
if (this.props.editor.view === 'interlacement') {
|
||||||
if (thread === 'warp') {
|
if (thread === 'warp') {
|
||||||
const grd = mc.createLinearGradient(0, 0, size, 0);
|
const grd = mc.createLinearGradient(0, 0, size, 0);
|
||||||
grd.addColorStop(0.1, 'rgba(0,0,0,0.3)');
|
grd.addColorStop(0.1, 'rgba(0,0,0,0.3)');
|
||||||
@ -57,16 +63,19 @@ function Drawdown({ baseSize, warp, weft, tieups }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!squares[view]) squares[view] = {};
|
if (!this.squares[view]) this.squares[view] = {};
|
||||||
if (!squares[view][thread]) squares[view][thread] = {};
|
if (!this.squares[view][thread]) this.squares[view][thread] = {};
|
||||||
if (!squares[view][thread][size]) squares[view][thread][size] = {};
|
if (!this.squares[view][thread][size]) this.squares[view][thread][size] = {};
|
||||||
squares[view][thread][size][colour] = m_canvas;
|
this.squares[view][thread][size][colour] = m_canvas;
|
||||||
return m_canvas;
|
return m_canvas;
|
||||||
};
|
}
|
||||||
|
|
||||||
const paintDrawdown = () => {
|
paintDrawdown(prevProps) {
|
||||||
const canvas = drawdownRef.current;
|
const canvas = this.refs.drawdown;
|
||||||
const ctx = canvas.getContext('2d', { alpha: false });
|
const ctx = canvas.getContext('2d', { alpha: false });
|
||||||
|
const {
|
||||||
|
baseSize, warp, weft, tieups,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
for (let tread = 0; tread < weft.threads; tread++) {
|
for (let tread = 0; tread < weft.threads; tread++) {
|
||||||
for (let thread = 0; thread < warp.threads; thread++) {
|
for (let thread = 0; thread < warp.threads; thread++) {
|
||||||
@ -74,25 +83,40 @@ function Drawdown({ baseSize, warp, weft, tieups }) {
|
|||||||
const shaft = warp.threading[thread].shaft;
|
const shaft = warp.threading[thread].shaft;
|
||||||
const tieup = tieups[treadle - 1];
|
const tieup = tieups[treadle - 1];
|
||||||
const proceed = true;
|
const proceed = true;
|
||||||
|
/* if (prevProps) {
|
||||||
|
const prevTreadle = prevProps.weft.treadling[tread].treadle;
|
||||||
|
const prevShaft = prevProps.warp.threading[thread].shaft;
|
||||||
|
const prevTieup = prevProps.tieups[prevTreadle - 1];
|
||||||
|
const prevBaseSize = prevProps.baseSize;
|
||||||
|
proceed = prevTreadle !== treadle || prevShaft !== shaft || baseSize !== prevBaseSize || prevTieup !== tieup;
|
||||||
|
} */
|
||||||
if (proceed) {
|
if (proceed) {
|
||||||
const weftColour = utils.rgb(weft.treadling[tread].colour || weft.defaultColour);
|
const weftColour = utils.rgb(weft.treadling[tread].colour || weft.defaultColour);
|
||||||
const warpColour = utils.rgb(warp.threading[thread].colour || warp.defaultColour);
|
const warpColour = utils.rgb(warp.threading[thread].colour || warp.defaultColour);
|
||||||
const threadType = tieup && tieup.filter(t => t <= warp.shafts).indexOf(shaft) > -1 ? 'warp' : 'weft';
|
const threadType = tieup && tieup.filter(t => t <= warp.shafts).indexOf(shaft) > -1 ? 'warp' : 'weft';
|
||||||
const square = 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);
|
ctx.drawImage(square, canvas.width - (baseSize * (thread + 1)), tread * baseSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { warp, weft, baseSize } = this.props;
|
||||||
return (
|
return (
|
||||||
<StyledDrawdown ref={drawdownRef} className="drawdown joyride-drawdown"
|
<StyledDrawdown ref="drawdown" className="drawdown joyride-drawdown"
|
||||||
width={warp.threads * baseSize}
|
width={warp.threads * baseSize}
|
||||||
height={weft.threads * baseSize}
|
height={weft.threads * baseSize}
|
||||||
weft={weft} warp={warp} baseSize={baseSize}
|
weft={weft} warp={warp} baseSize={baseSize}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const StyledTieups = styled.canvas`
|
const StyledTieups = styled.canvas`
|
||||||
@ -7,40 +7,46 @@ const StyledTieups = styled.canvas`
|
|||||||
right:10px;
|
right:10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function Tieups({ cellStyle, warp, weft, tieups, updatePattern, baseSize }) {
|
class Tieups extends Component {
|
||||||
useEffect(() => paintTieups());
|
|
||||||
const tieupRef = useRef(null);
|
|
||||||
|
|
||||||
const fillUpTo = (t, limit) => {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
let i = t.length;
|
this.paintTieups();
|
||||||
|
}
|
||||||
|
componentDidMount() {
|
||||||
|
this.paintTieups();
|
||||||
|
}
|
||||||
|
|
||||||
|
fillUpTo = (tieups, limit) => {
|
||||||
|
let i = tieups.length;
|
||||||
while (i <= limit) {
|
while (i <= limit) {
|
||||||
t.push([]);
|
tieups.push([]);
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
const getTieupShaft = (event) => {
|
getTieupShaft = (event) => {
|
||||||
const rect = event.currentTarget.getBoundingClientRect();
|
const rect = event.currentTarget.getBoundingClientRect();
|
||||||
const y = event.clientY - rect.top;
|
const y = event.clientY - rect.top;
|
||||||
const x = 0 - (event.clientX - rect.right);
|
const x = 0 - (event.clientX - rect.right);
|
||||||
const shaft = warp.shafts - parseInt(y / baseSize);
|
const shaft = this.props.warp.shafts - parseInt(y / this.props.baseSize);
|
||||||
const tieup = weft.treadles - parseInt(x / baseSize) - 1;
|
const tieup = this.props.weft.treadles - parseInt(x / this.props.baseSize) - 1;
|
||||||
return { tieup, shaft };
|
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 = () => {
|
if (tieup >= tieups.length) this.fillUpTo(tieups, tieup);
|
||||||
const canvas = tieupRef.current;
|
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 ctx = canvas.getContext('2d');// , { alpha: false });
|
||||||
|
const { baseSize, tieups } = this.props;
|
||||||
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
@ -62,9 +68,12 @@ function Tieups({ cellStyle, warp, weft, tieups, updatePattern, baseSize }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { warp, weft, baseSize } = this.props;
|
||||||
return (
|
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;
|
export default Tieups;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { Component } from 'react';
|
||||||
import {
|
import {
|
||||||
Confirm, Select, Segment, Accordion, Grid, Icon, Input, Button,
|
Confirm, Select, Segment, Accordion, Grid, Icon, Input, Button,
|
||||||
} from 'semantic-ui-react';
|
} from 'semantic-ui-react';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import Slider from 'rc-slider';
|
import Slider from 'rc-slider';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@ -35,78 +35,69 @@ const ColourSquare = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updatePattern, updateObject, saveObject }) {
|
class Tools extends Component {
|
||||||
const [activeDrawers, setActiveDrawers] = useState(['properties', 'drawing', 'palette']);
|
state = { colours: [], activeDrawers: ['properties', 'drawing', 'palette'], view: 'interlacement' }
|
||||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { objectId, username, projectPath } = useParams();
|
|
||||||
|
|
||||||
const { project, editor } = useSelector(state => {
|
enableTool = (tool) => {
|
||||||
let project = {};
|
this.props.onEditorUpdated({ tool, colour: this.props.editor.colour });
|
||||||
state.projects.projects.forEach((p) => {
|
}
|
||||||
if (p.path === projectPath && p.owner && p.owner.username === username) project = p;
|
|
||||||
});
|
|
||||||
return { project, editor: state.objects.editor };
|
|
||||||
});
|
|
||||||
|
|
||||||
const enableTool = (tool) => {
|
setColour = (colour) => {
|
||||||
dispatch(actions.objects.updateEditor({ tool, colour: editor.colour }));
|
this.props.onEditorUpdated({ tool: 'colour', colour });
|
||||||
};
|
}
|
||||||
|
|
||||||
const setColour = (colour) => {
|
setView = (view) => {
|
||||||
dispatch(actions.objects.updateEditor({ tool: 'colour', colour }));
|
this.props.onEditorUpdated({ view });
|
||||||
};
|
}
|
||||||
|
|
||||||
const setEditorView = (view) => {
|
setName = (event) => {
|
||||||
dispatch(actions.objects.updateEditor({ view }));
|
this.props.updateObject({ name: event.target.value });
|
||||||
};
|
}
|
||||||
|
|
||||||
const setName = (event) => {
|
setShafts = (event) => {
|
||||||
updateObject({ name: event.target.value });
|
const warp = { ...this.props.warp, shafts: parseInt(event.target.value, 10) || 1 };
|
||||||
};
|
this.props.updatePattern({ warp });
|
||||||
|
}
|
||||||
|
|
||||||
const setShafts = (event) => {
|
setTreadles = (event) => {
|
||||||
updatePattern({ warp: { ...warp, shafts: parseInt(event.target.value, 10) || 1 } });
|
const weft = { ...this.props.weft, treadles: parseInt(event.target.value, 10) || 1 };
|
||||||
};
|
this.props.updatePattern({ weft });
|
||||||
|
}
|
||||||
|
|
||||||
const setTreadles = (event) => {
|
onZoomChange = zoom => this.props.updatePattern({ baseSize: zoom || 10 })
|
||||||
updatePattern({ weft: { ...weft, treadles: parseInt(event.target.value, 10) || 1 } });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onZoomChange = zoom => updatePattern({ baseSize: zoom || 10 });
|
drawerIsActive = drawer => this.state.activeDrawers.indexOf(drawer) > -1
|
||||||
|
|
||||||
const drawerIsActive = drawer => activeDrawers.indexOf(drawer) > -1;
|
activateDrawer = (drawer) => {
|
||||||
|
const index = this.state.activeDrawers.indexOf(drawer);
|
||||||
const activateDrawer = (drawer) => {
|
const drawers = this.state.activeDrawers;
|
||||||
const index = activeDrawers.indexOf(drawer);
|
|
||||||
const drawers = activeDrawers;
|
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
drawers.push(drawer);
|
drawers.push(drawer);
|
||||||
} else {
|
} else {
|
||||||
drawers.splice(index, 1);
|
drawers.splice(index, 1);
|
||||||
}
|
}
|
||||||
setActiveDrawers(drawers);
|
this.setState({ activeDrawers: drawers });
|
||||||
};
|
}
|
||||||
|
|
||||||
const deleteObject = () => {
|
deleteObject = () => {
|
||||||
api.objects.delete(objectId, () => {
|
api.objects.delete(this.props.match.params.objectId, () => {
|
||||||
toast('🗑️ Pattern deleted');
|
toast('🗑️ Pattern deleted');
|
||||||
dispatch(actions.objects.delete(objectId));
|
this.props.onObjectDeleted(this.props.match.params.objectId);
|
||||||
navigate(`/${project.fullName}`);
|
this.props.history.push(`/${this.props.project.fullName}`);
|
||||||
}, err => console.log(err));
|
}, 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.')
|
const sure = window.confirm('Really revert your changes to your last save point?\n\nAny updates to your pattern since you last saved will be lost.')
|
||||||
if (sure) {
|
if (sure) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
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.'));
|
const newWidth = parseInt(window.prompt('Enter a new width for your pattern.\n\nIMPORTANT: If your new width is less than the current width, then any shafts selected beyond the new width will be lost.'));
|
||||||
if (!newWidth) return;
|
if (!newWidth) return;
|
||||||
|
const { warp } = this.props;
|
||||||
if (newWidth > warp.threading.length) {
|
if (newWidth > warp.threading.length) {
|
||||||
let i = warp.threading.length;
|
let i = warp.threading.length;
|
||||||
while (i < newWidth) {
|
while (i < newWidth) {
|
||||||
@ -119,13 +110,14 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
|
|||||||
warp.threading.splice(newWidth);
|
warp.threading.splice(newWidth);
|
||||||
warp.threads = warp.threading.length;
|
warp.threads = warp.threading.length;
|
||||||
}
|
}
|
||||||
updatePattern({ warp });
|
this.props.updatePattern({ warp });
|
||||||
dispatch(actions.objects.updateEditor());
|
this.props.onEditorUpdated();
|
||||||
dispatch(actions.objects.updateEditor( { tool: 'pan' }));
|
this.props.onEditorUpdated({ tool: 'pan' });
|
||||||
};
|
}
|
||||||
const changeHeight = () => {
|
changeHeight = () => {
|
||||||
const newHeight = parseInt(window.prompt('Enter a new height for your pattern.\n\nIMPORTANT: If your new height is less than the current height, then any treadles selected beyond the new height will be lost.'));
|
const newHeight = parseInt(window.prompt('Enter a new height for your pattern.\n\nIMPORTANT: If your new height is less than the current height, then any treadles selected beyond the new height will be lost.'));
|
||||||
if (!newHeight) return;
|
if (!newHeight) return;
|
||||||
|
const { weft } = this.props;
|
||||||
if (newHeight > weft.treadling.length) {
|
if (newHeight > weft.treadling.length) {
|
||||||
let i = weft.treadling.length;
|
let i = weft.treadling.length;
|
||||||
while (i < newHeight) {
|
while (i < newHeight) {
|
||||||
@ -138,30 +130,32 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
|
|||||||
weft.treadling.splice(newHeight);
|
weft.treadling.splice(newHeight);
|
||||||
weft.threads = weft.treadling.length;
|
weft.threads = weft.treadling.length;
|
||||||
}
|
}
|
||||||
updatePattern({ weft });
|
this.props.updatePattern({ weft });
|
||||||
dispatch(actions.objects.updateEditor());
|
this.props.onEditorUpdated();
|
||||||
};
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { warp, weft, editor, unsaved, saving } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className="pattern-toolbox joyride-tools">
|
<div className="pattern-toolbox joyride-tools">
|
||||||
{unsaved &&
|
{unsaved &&
|
||||||
<Segment attached="top">
|
<Segment attached="top">
|
||||||
<Button fluid color="teal" icon="save" content="Save pattern" onClick={() => saveObject(/*this.refs.canvas*/)} loading={saving}/>
|
<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={revertChanges} />
|
<Button style={{marginTop: 5}} fluid icon='refresh' content='Undo changes' onClick={this.revertChanges} />
|
||||||
</Segment>
|
</Segment>
|
||||||
}
|
}
|
||||||
<Segment attached>
|
<Segment attached>
|
||||||
<Accordion fluid>
|
<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
|
<Icon name="dropdown" /> View
|
||||||
</Accordion.Title>
|
</Accordion.Title>
|
||||||
<Accordion.Content active={drawerIsActive('view')}>
|
<Accordion.Content active={this.drawerIsActive('view')}>
|
||||||
<small>Drawdown view</small>
|
<small>Drawdown view</small>
|
||||||
<Select
|
<Select
|
||||||
size="tiny"
|
size="tiny"
|
||||||
fluid
|
fluid
|
||||||
value={editor.view}
|
value={editor.view}
|
||||||
onChange={(e, s) => setEditorView(s.value)}
|
onChange={(e, s) => this.setView(s.value)}
|
||||||
style={{ fontSize: '11px' }}
|
style={{ fontSize: '11px' }}
|
||||||
options={[
|
options={[
|
||||||
{ key: 1, value: 'interlacement', text: 'Interlacement' },
|
{ key: 1, value: 'interlacement', text: 'Interlacement' },
|
||||||
@ -172,75 +166,75 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
|
|||||||
/>
|
/>
|
||||||
<div style={{ marginTop: '5px' }} />
|
<div style={{ marginTop: '5px' }} />
|
||||||
<small>Zoom</small>
|
<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.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
|
<Icon name="dropdown" /> Properties
|
||||||
</Accordion.Title>
|
</Accordion.Title>
|
||||||
<Accordion.Content active={drawerIsActive('properties')}>
|
<Accordion.Content active={this.drawerIsActive('properties')}>
|
||||||
<small>Name</small>
|
<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 columns={2}>
|
||||||
<Grid.Row className='joyride-threads'>
|
<Grid.Row className='joyride-threads'>
|
||||||
<Grid.Column>
|
<Grid.Column>
|
||||||
<small>Shafts</small>
|
<small>Shafts</small>
|
||||||
<Input fluid type="number" value={warp.shafts} onKeyDown={e => false} onChange={setShafts} size="mini" />
|
<Input fluid type="number" value={warp.shafts} onKeyDown={e => false} onChange={this.setShafts} size="mini" />
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
<Grid.Column>
|
<Grid.Column>
|
||||||
<small>Treadles</small>
|
<small>Treadles</small>
|
||||||
<Input fluid type="number" value={weft.treadles} onKeyDown={e => false} onChange={setTreadles} size="mini" />
|
<Input fluid type="number" value={weft.treadles} onKeyDown={e => false} onChange={this.setTreadles} size="mini" />
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
</Grid.Row>
|
</Grid.Row>
|
||||||
<Grid.Row style={{paddingTop: 0}}>
|
<Grid.Row style={{paddingTop: 0}}>
|
||||||
<Grid.Column>
|
<Grid.Column>
|
||||||
<small>Width</small>
|
<small>Width</small>
|
||||||
<Input fluid readOnly value={warp.threading?.length || 0} size="mini"
|
<Input fluid readOnly value={warp.threading?.length || 0} size="mini"
|
||||||
action={{icon: 'edit', onClick: changeWidth}}
|
action={{icon: 'edit', onClick: this.changeWidth}}
|
||||||
/>
|
/>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
<Grid.Column>
|
<Grid.Column>
|
||||||
<small>Height</small>
|
<small>Height</small>
|
||||||
<Input fluid readOnly value={weft.treadling?.length || 0} size="mini"
|
<Input fluid readOnly value={weft.treadling?.length || 0} size="mini"
|
||||||
action={{icon: 'edit', onClick: changeHeight}}
|
action={{icon: 'edit', onClick: this.changeHeight}}
|
||||||
/>
|
/>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
</Grid.Row>
|
</Grid.Row>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Accordion.Content>
|
</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
|
<Icon name="dropdown" /> Tools
|
||||||
</Accordion.Title>
|
</Accordion.Title>
|
||||||
<Accordion.Content active={drawerIsActive('drawing')}>
|
<Accordion.Content active={this.drawerIsActive('drawing')}>
|
||||||
<Button.Group fluid>
|
<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-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={editor.tool === 'colour' && 'blue'} size="tiny" icon onClick={() => enableTool('colour')}><Icon name="paint brush" /></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={editor.tool === 'straight' && 'blue'} size="tiny" icon onClick={() => enableTool('straight')}>/ /</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={editor.tool === 'point' && 'blue'} size="tiny" icon onClick={() => enableTool('point')}><Icon name="chevron up" /></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>
|
</Button.Group>
|
||||||
</Accordion.Content>
|
</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
|
<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.Title>
|
||||||
<Accordion.Content active={drawerIsActive('palette')}>
|
<Accordion.Content active={this.drawerIsActive('palette')}>
|
||||||
{pattern.colours && pattern.colours.map(colour =>
|
{this.props.pattern.colours && this.props.pattern.colours.map(colour =>
|
||||||
<ColourSquare key={colour} colour={utils.rgb(colour)} onClick={() => setColour(colour)} />
|
<ColourSquare key={colour} colour={utils.rgb(colour)} onClick={() => this.setColour(colour)} />
|
||||||
)}
|
)}
|
||||||
</Accordion.Content>
|
</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
|
<Icon name="dropdown" /> Advanced
|
||||||
</Accordion.Title>
|
</Accordion.Title>
|
||||||
<Accordion.Content active={drawerIsActive('advanced')}>
|
<Accordion.Content active={this.drawerIsActive('advanced')}>
|
||||||
<Button size="small" basic color="red" fluid onClick={e => setDeleteModalOpen(true)}>Delete pattern</Button>
|
<Button size="small" basic color="red" fluid onClick={e => this.setState({ deleteModalOpen: true })}>Delete pattern</Button>
|
||||||
<Confirm
|
<Confirm
|
||||||
open={deleteModalOpen}
|
open={this.state.deleteModalOpen}
|
||||||
content="Really delete this pattern?"
|
content="Really delete this pattern?"
|
||||||
onCancel={e => setDeleteModalOpen(false)}
|
onCancel={e => this.setState({ deleteModalOpen: false })}
|
||||||
onConfirm={deleteObject}
|
onConfirm={this.deleteObject}
|
||||||
/>
|
/>
|
||||||
</Accordion.Content>
|
</Accordion.Content>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
@ -248,5 +242,22 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
|
|||||||
</div>
|
</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 styled from 'styled-components';
|
||||||
import { useSelector } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import utils from 'utils/utils.js';
|
import utils from 'utils/utils.js';
|
||||||
|
|
||||||
@ -17,154 +17,157 @@ const StyledWarp = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const squares = {};
|
class Warp extends Component {
|
||||||
const markers = {};
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.squares = {};
|
||||||
|
this.markers = {};
|
||||||
|
}
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
this.paintDrawdown();
|
||||||
|
}
|
||||||
|
componentDidMount() {
|
||||||
|
this.paintDrawdown();
|
||||||
|
}
|
||||||
|
|
||||||
function Warp({ baseSize, cellStyle, warp, weft, updatePattern }) {
|
getThreadShaft = (event) => {
|
||||||
const [draggingColourway, setDraggingColourway] = useState(false);
|
|
||||||
const [dragging, setDragging] = useState(false);
|
|
||||||
const [startShaft, setStartShaft] = useState();
|
|
||||||
const [startThread, setStartThread] = useState();
|
|
||||||
|
|
||||||
const { editor } = useSelector(state => ({ editor: state.objects.editor }));
|
|
||||||
useEffect(() => paintDrawdown());
|
|
||||||
const warpRef = useRef(null);
|
|
||||||
const colourwayRef = useRef(null);
|
|
||||||
|
|
||||||
const getThreadShaft = (event) => {
|
|
||||||
const rect = event.currentTarget.getBoundingClientRect();
|
const rect = event.currentTarget.getBoundingClientRect();
|
||||||
const y = event.clientY - rect.top;
|
const y = event.clientY - rect.top;
|
||||||
const x = 0 - (event.clientX - rect.right);
|
const x = 0 - (event.clientX - rect.right);
|
||||||
const shaft = warp.shafts - parseInt(y / baseSize);
|
const shaft = this.props.warp.shafts - parseInt(y / this.props.baseSize);
|
||||||
const thread = parseInt(x / baseSize);
|
const thread = parseInt(x / this.props.baseSize);
|
||||||
return { shaft, thread };
|
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);
|
mouseClickColourway = event => {
|
||||||
const mouseDown = (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();
|
event.preventDefault();
|
||||||
const { shaft, thread } = getThreadShaft(event);
|
this.draggingColourway = true;
|
||||||
setStartShaft(shaft);
|
}
|
||||||
setStartThread(thread);
|
mouseUpColourway = event => this.draggingColourway = false;
|
||||||
setDragging(true);
|
mouseMoveColourway = (event) => {
|
||||||
};
|
if (this.draggingColourway) {
|
||||||
const mouseMove = (event) => {
|
const warp = Object.assign({}, this.props.warp);
|
||||||
if (dragging && editor.tool) {
|
const { thread } = this.getThreadShaft(event);
|
||||||
const newWarp = Object.assign({}, warp);
|
if (thread >= warp.threading.length) this.fillUpTo(warp, thread);
|
||||||
const { shaft, thread } = getThreadShaft(event);
|
warp.threading[thread].colour = this.props.colour;
|
||||||
|
this.props.updatePattern({ warp });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let lX = startThread; let hX = thread; let lY = startShaft; let hY = shaft;
|
mouseUp = event => this.dragging = false;
|
||||||
let xDirection = 1; let yDirection = 1;
|
mouseDown = (event) => {
|
||||||
if (thread < startThread) {
|
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;
|
lX = thread;
|
||||||
hX = startThread;
|
hX = this.startThread;
|
||||||
xDirection = -1;
|
xDirection = -1;
|
||||||
}
|
}
|
||||||
if (shaft < startShaft) {
|
if (shaft < this.startShaft) {
|
||||||
lY = shaft;
|
lY = shaft;
|
||||||
hY = startShaft;
|
hY = this.startShaft;
|
||||||
yDirection = -1;
|
yDirection = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let x = xDirection > 0 ? lX : hX;
|
let x = xDirection > 0 ? lX : hX;
|
||||||
let y = yDirection > 0 ? lY : hY;
|
let y = yDirection > 0 ? lY : hY;
|
||||||
if (editor.tool === 'colour') {
|
if (this.props.tool === 'colour') {
|
||||||
if (thread >= warp.threading.length) fillUpTo(newWarp, thread);
|
if (thread >= warp.threading.length) this.fillUpTo(warp, thread);
|
||||||
newWarp.threading[thread].colour = editor.colour;
|
warp.threading[thread].colour = this.props.colour;
|
||||||
}
|
}
|
||||||
if (editor.tool === 'straight') {
|
if (this.props.tool === 'straight') {
|
||||||
while (x <= hX && x >= lX) {
|
while (x <= hX && x >= lX) {
|
||||||
if (x >= warp.threading.length || warp.threading.length - x < 5) fillUpTo(newWarp, x + 5);
|
if (x >= warp.threading.length || warp.threading.length - x < 5) this.fillUpTo(warp, x + 5);
|
||||||
newWarp.threading[x].shaft = y;
|
warp.threading[x].shaft = y;
|
||||||
x += xDirection;
|
x += xDirection;
|
||||||
y += yDirection;
|
y += yDirection;
|
||||||
if (y > hY || y < lY) y = yDirection > 0 ? lY : hY;
|
if (y > hY || y < lY) y = yDirection > 0 ? lY : hY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (editor.tool === 'point') {
|
if (this.props.tool === 'point') {
|
||||||
while (x <= hX && x >= lX) {
|
while (x <= hX && x >= lX) {
|
||||||
if (x >= warp.threading.length || warp.threading.length - x < 5) fillUpTo(newWarp, x + 5);
|
if (x >= warp.threading.length || warp.threading.length - x < 5) this.fillUpTo(warp, x + 5);
|
||||||
newWarp.threading[x].shaft = y;
|
warp.threading[x].shaft = y;
|
||||||
x += xDirection;
|
x += xDirection;
|
||||||
y += yDirection;
|
y += yDirection;
|
||||||
if (y > hY || y <= lY) yDirection = 0 - yDirection;
|
if (y > hY || y <= lY) yDirection = 0 - yDirection;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updatePattern({ warp: newWarp });
|
this.props.updatePattern({ warp });
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
const click = (event) => {
|
click = (event) => {
|
||||||
if (editor.tool === 'point' || editor.tool === 'straight') {
|
if (this.props.tool === 'point' || this.props.tool === 'straight') {
|
||||||
const { thread, shaft } = getThreadShaft(event);
|
const { thread, shaft } = this.getThreadShaft(event);
|
||||||
const newWarp = Object.assign({}, warp);
|
const warp = Object.assign({}, this.props.warp);
|
||||||
if (thread > warp.threading.length || warp.threading.length - thread < 5) fillUpTo(newWarp, thread + 5);
|
if (thread > warp.threading.length || warp.threading.length - thread < 5) this.fillUpTo(warp, thread + 5);
|
||||||
const warpThread = newWarp.threading[thread];
|
const warpThread = warp.threading[thread];
|
||||||
warpThread.shaft = warpThread.shaft === shaft ? 0 : shaft;
|
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;
|
let i = warp.threading.length;
|
||||||
while (i <= limit) {
|
while (i <= limit) {
|
||||||
w.threading.push({ shaft: 0 });
|
warp.threading.push({ shaft: 0 });
|
||||||
w.threads++;
|
warp.threads++;
|
||||||
i++;
|
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');
|
const m_canvas = document.createElement('canvas');
|
||||||
m_canvas.width = baseSize;
|
m_canvas.width = this.props.baseSize;
|
||||||
m_canvas.height = baseSize;
|
m_canvas.height = this.props.baseSize;
|
||||||
const mc = m_canvas.getContext('2d');
|
const mc = m_canvas.getContext('2d');
|
||||||
mc.fillStyle = 'black';
|
mc.fillStyle = 'black';
|
||||||
mc.fillRect(0, 0, baseSize, baseSize);
|
mc.fillRect(0, 0, this.props.baseSize, this.props.baseSize);
|
||||||
markers[size] = m_canvas;
|
this.markers[size] = m_canvas;
|
||||||
return m_canvas;
|
return m_canvas;
|
||||||
};
|
}
|
||||||
|
|
||||||
const getSquare = (size, colour) => {
|
getSquare(size, colour) {
|
||||||
if (squares[size] && squares[size][colour]) return squares[size][colour];
|
if (this.squares[size] && this.squares[size][colour]) return this.squares[size][colour];
|
||||||
const m_canvas = document.createElement('canvas');
|
const m_canvas = document.createElement('canvas');
|
||||||
m_canvas.width = baseSize;
|
m_canvas.width = this.props.baseSize;
|
||||||
m_canvas.height = 10;
|
m_canvas.height = 10;
|
||||||
const mc = m_canvas.getContext('2d');
|
const mc = m_canvas.getContext('2d');
|
||||||
mc.fillStyle = utils.rgb(colour);
|
mc.fillStyle = utils.rgb(colour);
|
||||||
mc.fillRect(0, 0, baseSize, 10);
|
mc.fillRect(0, 0, this.props.baseSize, 10);
|
||||||
if (!squares[size]) squares[size] = {};
|
if (!this.squares[size]) this.squares[size] = {};
|
||||||
squares[size][colour] = m_canvas;
|
this.squares[size][colour] = m_canvas;
|
||||||
return m_canvas;
|
return m_canvas;
|
||||||
};
|
}
|
||||||
|
|
||||||
const paintDrawdown = () => {
|
paintDrawdown() {
|
||||||
const canvas = warpRef.current;
|
const canvas = this.refs.warp;
|
||||||
const colourway = colourwayRef.current;
|
const colourway = this.refs.colourway;
|
||||||
const ctx = canvas.getContext('2d');// , { alpha: false });
|
const ctx = canvas.getContext('2d');// , { alpha: false });
|
||||||
const ctx2 = colourway.getContext('2d');// , { alpha: false });
|
const ctx2 = colourway.getContext('2d');// , { alpha: false });
|
||||||
|
const { baseSize, warp } = this.props;
|
||||||
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
@ -181,40 +184,52 @@ function Warp({ baseSize, cellStyle, warp, weft, updatePattern }) {
|
|||||||
|
|
||||||
for (let thread = 0; thread < warp.threads; thread++) {
|
for (let thread = 0; thread < warp.threads; thread++) {
|
||||||
const shaft = warp.threading[thread].shaft;
|
const shaft = warp.threading[thread].shaft;
|
||||||
const marker = getMarker(baseSize);
|
const marker = this.getMarker(baseSize);
|
||||||
ctx.drawImage(marker, canvas.width - ((thread + 1) * baseSize), canvas.height - (shaft * baseSize));
|
ctx.drawImage(marker, canvas.width - ((thread + 1) * this.props.baseSize), canvas.height - (shaft * this.props.baseSize));
|
||||||
const colourSquare = getSquare(baseSize, warp.threading[thread].colour || warp.defaultColour);
|
const colourSquare = this.getSquare(baseSize, warp.threading[thread].colour || warp.defaultColour);
|
||||||
ctx2.drawImage(colourSquare, canvas.width - ((thread + 1) * baseSize), 0);
|
ctx2.drawImage(colourSquare, canvas.width - ((thread + 1) * this.props.baseSize), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { warp, weft, baseSize } = this.props;
|
||||||
return (
|
return (
|
||||||
<StyledWarp treadles={weft.treadles} shafts={warp.shafts} baseSize={baseSize}>
|
<StyledWarp treadles={weft.treadles} shafts={warp.shafts} baseSize={baseSize}>
|
||||||
<canvas className='warp-colourway joyride-warpColourway' ref={colourwayRef} width={warp.threading.length * baseSize} height={10}
|
<canvas className='warp-colourway joyride-warpColourway' ref="colourway" width={warp.threading.length * baseSize} height={10}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute', top: 0, right: 0, height: 10, width: warp.threading.length * baseSize,
|
position: 'absolute', top: 0, right: 0, height: 10, width: warp.threading.length * baseSize,
|
||||||
}}
|
}}
|
||||||
onClick={mouseClickColourway}
|
onClick={this.mouseClickColourway}
|
||||||
onMouseDown={mouseDownColourway}
|
onMouseDown={this.mouseDownColourway}
|
||||||
onMouseMove={mouseMoveColourway}
|
onMouseMove={this.mouseMoveColourway}
|
||||||
onMouseUp={mouseUpColourway}
|
onMouseUp={this.mouseUpColourway}
|
||||||
onMouseLeave={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={{
|
style={{
|
||||||
position: 'absolute', top: 10, right: 0,
|
position: 'absolute', top: 10, right: 0,
|
||||||
height: warp.shafts * baseSize,
|
height: warp.shafts * baseSize,
|
||||||
width: warp.threading.length * baseSize, borderRadius: 4,
|
width: warp.threading.length * baseSize, borderRadius: 4,
|
||||||
boxShadow: '0px 0px 10px rgba(0,0,0,0.15)',
|
boxShadow: '0px 0px 10px rgba(0,0,0,0.15)',
|
||||||
}}
|
}}
|
||||||
onClick={click}
|
onClick={this.click}
|
||||||
onMouseDown={mouseDown}
|
onMouseDown={this.mouseDown}
|
||||||
onMouseMove={mouseMove}
|
onMouseMove={this.mouseMove}
|
||||||
onMouseUp={mouseUp}
|
onMouseUp={this.mouseUp}
|
||||||
onMouseLeave={mouseUp}
|
onMouseLeave={this.mouseUp}
|
||||||
/>
|
/>
|
||||||
</StyledWarp>
|
</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 styled from 'styled-components';
|
||||||
import { useSelector } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import utils from 'utils/utils.js';
|
import utils from 'utils/utils.js';
|
||||||
|
|
||||||
const StyledWeft = styled.div`
|
const StyledWeft = styled.div`
|
||||||
@ -18,156 +18,172 @@ const StyledWeft = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Cache
|
class Weft extends Component {
|
||||||
const squares = {};
|
constructor(props) {
|
||||||
const markers = {};
|
super(props);
|
||||||
|
this.squares = {};
|
||||||
|
this.markers = {};
|
||||||
|
}
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
this.paintDrawdown();
|
||||||
|
}
|
||||||
|
componentDidMount() {
|
||||||
|
this.paintDrawdown();
|
||||||
|
}
|
||||||
|
|
||||||
function Weft({ cellStyle, warp, weft, baseSize, updatePattern }) {
|
toggleWeft = (treadle, threadCount) => {
|
||||||
const [draggingColourway, setDraggingColourway] = useState(false);
|
const weft = Object.assign({}, this.props.weft);
|
||||||
const [dragging, setDragging] = useState(false);
|
const thread = weft.treadling[threadCount];
|
||||||
const [startTreadle, setStartTreadle] = useState();
|
thread.treadle = thread.treadle === treadle ? 0 : treadle;
|
||||||
const [startThread, setStartThread] = useState();
|
this.props.updatePattern({ weft });
|
||||||
|
}
|
||||||
|
|
||||||
const { editor } = useSelector(state => ({ editor: state.objects.editor }));
|
changeWeftColour = (threadIndex) => {
|
||||||
useEffect(() => paintDrawdown());
|
const weft = Object.assign({}, this.props.weft);
|
||||||
const weftRef = useRef(null);
|
const colour = this.props.colour;
|
||||||
const colourwayRef = useRef(null);
|
if (colour) {
|
||||||
|
weft.treadling[threadIndex].colour = colour;
|
||||||
|
this.props.updatePattern({ weft });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getThreadTreadle = (event) => {
|
getThreadTreadle = (event) => {
|
||||||
const rect = event.currentTarget.getBoundingClientRect();
|
const rect = event.currentTarget.getBoundingClientRect();
|
||||||
const y = event.clientY - rect.top;
|
const y = event.clientY - rect.top;
|
||||||
const x = (event.clientX - rect.left);
|
const x = (event.clientX - rect.left);
|
||||||
const thread = parseInt(y / baseSize) + 1;
|
const thread = parseInt(y / this.props.baseSize) + 1;
|
||||||
const treadle = parseInt(x / baseSize);
|
const treadle = parseInt(x / this.props.baseSize);
|
||||||
return { treadle, thread };
|
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);
|
mouseClickColourway = event => {
|
||||||
const mouseDown = (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();
|
event.preventDefault();
|
||||||
const { treadle, thread } = getThreadTreadle(event);
|
this.draggingColourway = true;
|
||||||
setStartTreadle(treadle);
|
}
|
||||||
setStartThread(thread);
|
mouseUpColourway = event => this.draggingColourway = false;
|
||||||
setDragging(true);
|
mouseMoveColourway = (event) => {
|
||||||
};
|
if (this.draggingColourway) {
|
||||||
const mouseMove = (event) => {
|
const weft = Object.assign({}, this.props.weft);
|
||||||
if (dragging && editor.tool) {
|
const { thread } = this.getThreadTreadle(event);
|
||||||
const newWeft = Object.assign({}, weft);
|
if (thread >= weft.treadling.length) this.fillUpTo(weft, thread);
|
||||||
const { treadle, thread } = getThreadTreadle(event);
|
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
|
let xDirection = 1; let
|
||||||
yDirection = 1;
|
yDirection = 1;
|
||||||
if (treadle < startTreadle) {
|
if (treadle < this.startTreadle) {
|
||||||
lX = treadle;
|
lX = treadle;
|
||||||
hX = startTreadle;
|
hX = this.startTreadle;
|
||||||
xDirection = -1;
|
xDirection = -1;
|
||||||
}
|
}
|
||||||
if (thread < startThread) {
|
if (thread < this.startThread) {
|
||||||
lY = thread;
|
lY = thread;
|
||||||
hY = startThread;
|
hY = this.startThread;
|
||||||
yDirection = -1;
|
yDirection = -1;
|
||||||
}
|
}
|
||||||
let x = xDirection > 0 ? lX : hX;
|
let x = xDirection > 0 ? lX : hX;
|
||||||
let y = yDirection > 0 ? lY : hY;
|
let y = yDirection > 0 ? lY : hY;
|
||||||
if (editor.tool === 'colour') {
|
if (this.props.tool === 'colour') {
|
||||||
if ((thread - 1) >= weft.treadling.length) fillUpTo(newWeft, (thread - 1));
|
if ((thread - 1) >= weft.treadling.length) this.fillUpTo(weft, (thread - 1));
|
||||||
newWeft.treadling[thread - 1].colour = editor.colour;
|
weft.treadling[thread - 1].colour = this.props.colour;
|
||||||
}
|
}
|
||||||
if (editor.tool === 'straight') {
|
if (this.props.tool === 'straight') {
|
||||||
while (y <= hY && y >= lY) {
|
while (y <= hY && y >= lY) {
|
||||||
if ((y - 1) >= weft.treadling.length || weft.treadling.length - y - 1 < 5) fillUpTo(newWeft, (y + 5));
|
if ((y - 1) >= weft.treadling.length || weft.treadling.length - y - 1 < 5) this.fillUpTo(weft, (y + 5));
|
||||||
newWeft.treadling[y - 1].treadle = x + 1;
|
weft.treadling[y - 1].treadle = x + 1;
|
||||||
x += xDirection;
|
x += xDirection;
|
||||||
y += yDirection;
|
y += yDirection;
|
||||||
if (x > hX || x < lX) x = xDirection > 0 ? lX : hX;
|
if (x > hX || x < lX) x = xDirection > 0 ? lX : hX;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (editor.tool === 'point') {
|
if (this.props.tool === 'point') {
|
||||||
while (y <= hY && y >= lY) {
|
while (y <= hY && y >= lY) {
|
||||||
if ((y - 1) >= weft.treadling.length || weft.treadling.length - y -1 < 5) fillUpTo(newWeft, y + 5);
|
if ((y - 1) >= weft.treadling.length || weft.treadling.length - y -1 < 5) this.fillUpTo(weft, y + 5);
|
||||||
newWeft.treadling[y - 1].treadle = x + 1;
|
weft.treadling[y - 1].treadle = x + 1;
|
||||||
x += xDirection;
|
x += xDirection;
|
||||||
y += yDirection;
|
y += yDirection;
|
||||||
if (x > hX || x <= lX) xDirection = 0 - xDirection;
|
if (x > hX || x <= lX) xDirection = 0 - xDirection;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updatePattern({ weft: newWeft });
|
this.props.updatePattern({ weft });
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
const click = (event) => {
|
click = (event) => {
|
||||||
if (editor.tool === 'point' || editor.tool === 'straight') {
|
if (this.props.tool === 'point' || this.props.tool === 'straight') {
|
||||||
let { thread, treadle } = getThreadTreadle(event);
|
let { thread, treadle } = this.getThreadTreadle(event);
|
||||||
treadle += 1;
|
treadle += 1;
|
||||||
const newWeft = Object.assign({}, weft);
|
const weft = Object.assign({}, this.props.weft);
|
||||||
if (thread >= newWeft.treadling.length || newWeft.treadling.length - thread < 5) fillUpTo(newWeft, thread + 5);
|
if (thread >= weft.treadling.length || weft.treadling.length - thread < 5) this.fillUpTo(weft, thread + 5);
|
||||||
const weftThread = newWeft.treadling[thread - 1];
|
const weftThread = weft.treadling[thread - 1];
|
||||||
weftThread.treadle = weftThread.treadle === treadle ? 0 : treadle;
|
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;
|
let i = weft.treadling.length;
|
||||||
while (i <= limit) {
|
while (i <= limit) {
|
||||||
weft.treadling.push({ treadle: 0 });
|
weft.treadling.push({ treadle: 0 });
|
||||||
weft.threads++;
|
weft.threads++;
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const getMarker = (size) => {
|
getMarker(size) {
|
||||||
if (markers[size]) return markers[size];
|
if (this.markers[size]) return this.markers[size];
|
||||||
const m_canvas = document.createElement('canvas');
|
const m_canvas = document.createElement('canvas');
|
||||||
m_canvas.width = baseSize;
|
m_canvas.width = this.props.baseSize;
|
||||||
m_canvas.height = baseSize;
|
m_canvas.height = this.props.baseSize;
|
||||||
const mc = m_canvas.getContext('2d');
|
const mc = m_canvas.getContext('2d');
|
||||||
mc.fillStyle = 'black';
|
mc.fillStyle = 'black';
|
||||||
mc.fillRect(0, 0, baseSize, baseSize);
|
mc.fillRect(0, 0, this.props.baseSize, this.props.baseSize);
|
||||||
markers[size] = m_canvas;
|
this.markers[size] = m_canvas;
|
||||||
return m_canvas;
|
return m_canvas;
|
||||||
};
|
}
|
||||||
|
|
||||||
const getSquare = (size, colour) => {
|
getSquare(size, colour) {
|
||||||
if (squares[size] && squares[size][colour]) return squares[size][colour];
|
if (this.squares[size] && this.squares[size][colour]) return this.squares[size][colour];
|
||||||
const m_canvas = document.createElement('canvas');
|
const m_canvas = document.createElement('canvas');
|
||||||
m_canvas.width = 10;
|
m_canvas.width = 10;
|
||||||
m_canvas.height = baseSize;
|
m_canvas.height = this.props.baseSize;
|
||||||
const mc = m_canvas.getContext('2d');
|
const mc = m_canvas.getContext('2d');
|
||||||
mc.fillStyle = utils.rgb(colour);
|
mc.fillStyle = utils.rgb(colour);
|
||||||
mc.fillRect(0, 0, 10, baseSize);
|
mc.fillRect(0, 0, 10, this.props.baseSize);
|
||||||
if (!squares[size]) squares[size] = {};
|
if (!this.squares[size]) this.squares[size] = {};
|
||||||
squares[size][colour] = m_canvas;
|
this.squares[size][colour] = m_canvas;
|
||||||
return m_canvas;
|
return m_canvas;
|
||||||
};
|
}
|
||||||
|
|
||||||
const paintDrawdown = () => {
|
paintDrawdown() {
|
||||||
const canvas = weftRef.current;
|
const canvas = this.refs.weft;
|
||||||
const colourway = colourwayRef.current;
|
const colourway = this.refs.colourway;
|
||||||
const ctx = canvas.getContext('2d');// , { alpha: false });
|
const ctx = canvas.getContext('2d');// , { alpha: false });
|
||||||
const ctx2 = colourway.getContext('2d');// , { alpha: false });
|
const ctx2 = colourway.getContext('2d');// , { alpha: false });
|
||||||
|
const { baseSize, weft } = this.props;
|
||||||
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
@ -184,37 +200,49 @@ function Weft({ cellStyle, warp, weft, baseSize, updatePattern }) {
|
|||||||
|
|
||||||
for (let thread = 0; thread < weft.threads; thread++) {
|
for (let thread = 0; thread < weft.threads; thread++) {
|
||||||
const treadle = weft.treadling[thread].treadle;
|
const treadle = weft.treadling[thread].treadle;
|
||||||
const marker = getMarker(baseSize);
|
const marker = this.getMarker(baseSize);
|
||||||
ctx.drawImage(marker, ((treadle - 1) * baseSize), ((thread) * baseSize));
|
ctx.drawImage(marker, ((treadle - 1) * this.props.baseSize), ((thread) * this.props.baseSize));
|
||||||
const colourSquare = getSquare(baseSize, weft.treadling[thread].colour || weft.defaultColour);
|
const colourSquare = this.getSquare(baseSize, weft.treadling[thread].colour || weft.defaultColour);
|
||||||
ctx2.drawImage(colourSquare, 0, (thread * baseSize));
|
ctx2.drawImage(colourSquare, 0, (thread * this.props.baseSize));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { warp, weft, baseSize } = this.props;
|
||||||
return (
|
return (
|
||||||
<StyledWeft baseSize={baseSize} treadles={weft.treadles} shafts={warp.shafts} threads={weft.threads}>
|
<StyledWeft baseSize={baseSize} treadles={weft.treadles} shafts={warp.shafts} threads={weft.threads}>
|
||||||
<canvas className='weft-colourway' ref={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}}
|
style={{ position: 'absolute', top: 0, right: 0, width: 10, height: weft.threads * baseSize}}
|
||||||
onClick={mouseClickColourway}
|
onClick={this.mouseClickColourway}
|
||||||
onMouseDown={mouseDownColourway}
|
onMouseDown={this.mouseDownColourway}
|
||||||
onMouseMove={mouseMoveColourway}
|
onMouseMove={this.mouseMoveColourway}
|
||||||
onMouseUp={mouseUpColourway}
|
onMouseUp={this.mouseUpColourway}
|
||||||
onMouseLeave={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={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0, right: 10, height: weft.threads * baseSize, width: weft.treadles * baseSize,
|
top: 0, right: 10, height: weft.threads * baseSize, width: weft.treadles * baseSize,
|
||||||
borderRadius: 4, boxShadow: '0px 0px 10px rgba(0,0,0,0.15)',
|
borderRadius: 4, boxShadow: '0px 0px 10px rgba(0,0,0,0.15)',
|
||||||
}}
|
}}
|
||||||
onClick={click}
|
onClick={this.click}
|
||||||
onMouseDown={mouseDown}
|
onMouseDown={this.mouseDown}
|
||||||
onMouseMove={mouseMove}
|
onMouseMove={this.mouseMove}
|
||||||
onMouseUp={mouseUp}
|
onMouseUp={this.mouseUp}
|
||||||
onMouseLeave={mouseUp}
|
onMouseLeave={this.mouseUp}
|
||||||
/>
|
/>
|
||||||
</StyledWeft>
|
</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 React, { useState, useEffect } from 'react';
|
||||||
import { Table, Container } from 'semantic-ui-react';
|
import { Table, Container } from 'semantic-ui-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, withRouter } from 'react-router-dom';
|
||||||
import { useSelector } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
|
|
||||||
function Root() {
|
function Root({ user }) {
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
|
|
||||||
const { user } = useSelector(state => {
|
|
||||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
|
||||||
return { user };
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!(user?.roles?.indexOf('root') > -1)) return;
|
if (!(user?.roles?.indexOf('root') > -1)) return;
|
||||||
api.root.getUsers(({ users}) => {
|
api.root.getUsers(({ users}) => {
|
||||||
@ -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 React, { useState } from 'react';
|
||||||
import { Message, Form, Divider, Segment, Icon } from 'semantic-ui-react';
|
import { Message, Form, Divider, Segment, Icon } from 'semantic-ui-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import actions from 'actions';
|
import actions from 'actions';
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
|
|
||||||
function AccountSettings() {
|
function AccountSettings({ user, onLogout, history, onReceiveUser }) {
|
||||||
const [newEmail, setNewEmail] = useState('');
|
const [newEmail, setNewEmail] = useState('');
|
||||||
const [existingPassword, setExistingPassword] = useState('');
|
const [existingPassword, setExistingPassword] = useState('');
|
||||||
const [newPassword, setNewPassword] = useState('');
|
const [newPassword, setNewPassword] = useState('');
|
||||||
const [deletePassword, setDeletePassword] = useState('');
|
const [deletePassword, setDeletePassword] = useState('');
|
||||||
const navigate = useNavigate();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const { user } = useSelector(state => {
|
|
||||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
|
||||||
return { user };
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateEmail = () => {
|
const updateEmail = () => {
|
||||||
api.accounts.updateEmail(newEmail, data => {
|
api.accounts.updateEmail(newEmail, data => {
|
||||||
setNewEmail('');
|
setNewEmail('');
|
||||||
dispatch(actions.users.receive(Object.assign({}, user, { email: data.email })));
|
onReceiveUser(Object.assign({}, user, { email: data.email }));
|
||||||
}, err => toast.error(err.message));
|
}, err => toast.error(err.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,8 +32,8 @@ function AccountSettings() {
|
|||||||
const confirm = window.confirm('Really delete your account?');
|
const confirm = window.confirm('Really delete your account?');
|
||||||
if (!confirm) return;
|
if (!confirm) return;
|
||||||
api.accounts.delete(deletePassword, () => {
|
api.accounts.delete(deletePassword, () => {
|
||||||
api.auth.logout(() => dispatch(actions.auth.logout()));
|
api.auth.logout(onLogout);
|
||||||
navigate('/');
|
history.push('/');
|
||||||
toast.info('Sorry to see you go');
|
toast.info('Sorry to see you go');
|
||||||
}, err => toast.error(err.message));
|
}, 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 { Message, Input, Segment, Button } from 'semantic-ui-react';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import actions from 'actions';
|
import actions from 'actions';
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
|
|
||||||
function IdentitySettings() {
|
class IdentitySettings extends Component {
|
||||||
const [newUsername, setNewUsername] = useState('');
|
constructor(props) {
|
||||||
const dispatch = useDispatch();
|
super(props);
|
||||||
const { user } = useSelector(state => {
|
this.state = { newUsername: '' };
|
||||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
}
|
||||||
return { user };
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateUsername = () => {
|
updateUsername = () => {
|
||||||
api.users.update(user.username, { username: newUsername }, (user) => {
|
api.users.update(this.props.user.username, { username: this.state.newUsername }, (user) => {
|
||||||
dispatch(actions.users.updateUsername(user._id, newUsername));
|
this.props.onUpdateUsername(this.props.user._id, this.state.newUsername);
|
||||||
toast.info('Username updated');
|
toast.info('Username updated');
|
||||||
setNewUsername('');
|
this.setState({ newUsername: '' });
|
||||||
}, err => toast.error(err.message));
|
}, err => toast.error(err.message));
|
||||||
};
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
return (
|
return (
|
||||||
<Segment raised color="blue">
|
<Segment raised color="blue">
|
||||||
<h3>Username</h3>
|
<h3>Username</h3>
|
||||||
@ -33,12 +33,24 @@ function IdentitySettings() {
|
|||||||
<p>If you change your username, your old one will become available to be taken by somebody else.</p>
|
<p>If you change your username, your old one will become available to be taken by somebody else.</p>
|
||||||
</Message>
|
</Message>
|
||||||
<Input
|
<Input
|
||||||
type="text" value={newUsername}
|
type="text" value={this.state.newUsername}
|
||||||
onChange={e => setNewUsername(e.target.value)}
|
onChange={e => this.setState({ newUsername: e.target.value })}
|
||||||
action=<Button color="yellow" content="Set new username" onClick={updateUsername} />
|
action=<Button color="yellow" content="Set new username" onClick={this.updateUsername} />
|
||||||
/>
|
/>
|
||||||
</Segment>
|
</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 { Label, Table, Checkbox, Divider, Segment } from 'semantic-ui-react';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import actions from 'actions';
|
import actions from 'actions';
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
import utils from 'utils/utils.js';
|
import utils from 'utils/utils.js';
|
||||||
@ -14,22 +16,19 @@ const subs = [
|
|||||||
{ key: 'projects.commented', text: 'Someone comments on one of your projects'},
|
{ key: 'projects.commented', text: 'Someone comments on one of your projects'},
|
||||||
];
|
];
|
||||||
|
|
||||||
function NotificationSettings() {
|
class NotificationSettings extends Component {
|
||||||
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 hasEmailSub = (key) => utils.hasSubscription(user, key);
|
hasEmailSub = (key) => utils.hasSubscription(this.props.user, key);
|
||||||
const toggleEmailSub = (key, enable) => {
|
toggleEmailSub = (key, enable) => {
|
||||||
|
const { user, onSubsUpdated } = this.props;
|
||||||
if (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
|
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 (
|
return (
|
||||||
<Segment raised color="blue">
|
<Segment raised color="blue">
|
||||||
<h3>Email preferences</h3>
|
<h3>Email preferences</h3>
|
||||||
@ -53,13 +52,13 @@ function NotificationSettings() {
|
|||||||
{subs.map(s =>
|
{subs.map(s =>
|
||||||
<Table.Row key={s.key}>
|
<Table.Row key={s.key}>
|
||||||
<Table.Cell>{s.text}</Table.Cell>
|
<Table.Cell>{s.text}</Table.Cell>
|
||||||
<Table.Cell><Checkbox toggle onChange={(e, c) => 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>
|
</Table.Row>
|
||||||
)}
|
)}
|
||||||
{groups.map(g =>
|
{groups.map(g =>
|
||||||
<Table.Row key={g._id}>
|
<Table.Row key={g._id}>
|
||||||
<Table.Cell>Someone writes in the Notice Board of {g.name}</Table.Cell>
|
<Table.Cell>Someone writes in the Notice Board of {g.name}</Table.Cell>
|
||||||
<Table.Cell><Checkbox toggle onChange={(e, c) => 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.Row>
|
||||||
)}
|
)}
|
||||||
</Table.Body>
|
</Table.Body>
|
||||||
@ -67,5 +66,18 @@ function NotificationSettings() {
|
|||||||
</Segment>
|
</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 React, { Component } from 'react';
|
||||||
import { Divider, Container, Grid, Menu } from 'semantic-ui-react';
|
import {
|
||||||
import { Outlet, NavLink } from 'react-router-dom';
|
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 (
|
return (
|
||||||
<Container style={{ marginTop: '40px' }}>
|
<Container style={{ marginTop: '40px' }}>
|
||||||
<h2>Manage your account</h2>
|
<h2>Manage your account</h2>
|
||||||
@ -10,18 +18,26 @@ function Settings() {
|
|||||||
<Grid stackable>
|
<Grid stackable>
|
||||||
<Grid.Column computer={4}>
|
<Grid.Column computer={4}>
|
||||||
<Menu fluid vertical tabular>
|
<Menu fluid vertical tabular>
|
||||||
<Menu.Item as={NavLink} to="/settings/identity" name="identity" icon="user secret" />
|
<Menu.Item as={Link} to="/settings/identity" name="identity" active={this.props.location.pathname === '/settings/identity' || this.props.location.pathname === '/settings'} icon="user secret" />
|
||||||
<Menu.Item as={NavLink} to='/settings/notifications' content='Notifications' icon='envelope' />
|
<Menu.Item as={Link} to='/settings/notifications' content='Notifications' icon='envelope' active={this.props.location.pathname === '/settings/notifications'} />
|
||||||
<Menu.Item as={NavLink} to="/settings/account" name="Account" icon="key" />
|
{/*<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>
|
</Menu>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
|
|
||||||
<Grid.Column stretched width={12}>
|
<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.Column>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default Settings;
|
export default Settings;
|
||||||
|
@ -1,45 +1,39 @@
|
|||||||
import React from 'react';
|
import React, { Component } from 'react';
|
||||||
import {
|
import {
|
||||||
Icon, Segment, Form, Button, Divider,
|
Icon, Segment, Form, Button, Divider,
|
||||||
} from 'semantic-ui-react';
|
} from 'semantic-ui-react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import utils from 'utils/utils';
|
import utils from 'utils/utils';
|
||||||
import actions from 'actions';
|
import actions from 'actions';
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
|
|
||||||
import FileChooser from 'components/includes/FileChooser';
|
import FileChooser from 'components/includes/FileChooser';
|
||||||
|
|
||||||
function EditProfile() {
|
class EditProfile extends Component {
|
||||||
const { username } = useParams();
|
updatePicture = (avatar) => {
|
||||||
const dispatch = useDispatch();
|
api.users.update(this.props.profileUser.username, { avatar }, this.props.onReceiveUser);
|
||||||
const { profileUser } = useSelector(state => {
|
}
|
||||||
const users = state.users.users;
|
|
||||||
const profileUser = users.filter(u => username === u.username)[0];
|
|
||||||
return { profileUser };
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatePicture = (avatar) => {
|
updateProfile = () => {
|
||||||
api.users.update(profileUser.username, { avatar }, u => dispatch(actions.users.receive(u)));
|
const { bio, location, website } = this.props.profileUser;
|
||||||
};
|
api.users.update(this.props.profileUser.username, { bio, location, website }, (user) => {
|
||||||
|
this.props.onReceiveUser(user);
|
||||||
const updateProfile = () => {
|
|
||||||
const { bio, location, website } = profileUser;
|
|
||||||
api.users.update(profileUser.username, { bio, location, website }, (u) => {
|
|
||||||
dispatch(actions.users.receive(u));
|
|
||||||
toast.info('Profile saved');
|
toast.info('Profile saved');
|
||||||
}, err => toast.error(err.message));
|
}, err => toast.error(err.message));
|
||||||
};
|
}
|
||||||
|
|
||||||
const updateSocial = () => {
|
updateSocial = () => {
|
||||||
const { twitter, facebook, linkedIn, instagram } = profileUser;
|
const { twitter, facebook, linkedIn, instagram } = this.props.profileUser;
|
||||||
api.users.update(profileUser.username, { twitter, facebook, linkedIn, instagram }, (u) => {
|
api.users.update(this.props.profileUser.username, { twitter, facebook, linkedIn, instagram }, (user) => {
|
||||||
dispatch(actions.users.receive(u));
|
this.props.onReceiveUser(user);
|
||||||
toast.info('Profile saved');
|
toast.info('Profile saved');
|
||||||
}, err => toast.error(err.message));
|
}, err => toast.error(err.message));
|
||||||
};
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { profileUser, onUserEdited } = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Link to={`/${profileUser.username}`} className="ui basic button">
|
<Link to={`/${profileUser.username}`} className="ui basic button">
|
||||||
@ -53,9 +47,9 @@ function EditProfile() {
|
|||||||
|
|
||||||
<h5>Profile picture</h5>
|
<h5>Profile picture</h5>
|
||||||
<FileChooser
|
<FileChooser
|
||||||
forType="user" forObject={profileUser}
|
forType="user" for={profileUser}
|
||||||
trigger=<Button basic color="yellow" icon="image" content="Choose an image" />
|
trigger=<Button basic color="yellow" icon="image" content="Choose an image" />
|
||||||
accept="image/*" onComplete={f => updatePicture(f.storedName)}
|
accept="image/*" onComplete={f => this.updatePicture(f.storedName)}
|
||||||
/>
|
/>
|
||||||
<h4>Or choose one of ours:</h4>
|
<h4>Or choose one of ours:</h4>
|
||||||
{utils.defaultAvatars().map(a => (
|
{utils.defaultAvatars().map(a => (
|
||||||
@ -64,7 +58,7 @@ function EditProfile() {
|
|||||||
style={{
|
style={{
|
||||||
width: 40, height: 40, margin: 4, cursor: 'pointer',
|
width: 40, height: 40, margin: 4, cursor: 'pointer',
|
||||||
}}
|
}}
|
||||||
onClick={e => updatePicture(a.key)}
|
onClick={e => this.updatePicture(a.key)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<Divider hidden />
|
<Divider hidden />
|
||||||
@ -73,19 +67,19 @@ function EditProfile() {
|
|||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
label="Bio" placeholder="Write a bit about yourself..."
|
label="Bio" placeholder="Write a bit about yourself..."
|
||||||
value={profileUser.bio}
|
value={profileUser.bio}
|
||||||
onChange={e => dispatch(actions.users.update(profileUser._id, { bio: e.target.value }))}
|
onChange={e => onUserEdited(profileUser._id, { bio: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label="Location" placeholder="Where in the world are you?" icon="map pin" iconPosition="left"
|
label="Location" placeholder="Where in the world are you?" icon="map pin" iconPosition="left"
|
||||||
value={profileUser.location}
|
value={profileUser.location}
|
||||||
onChange={e => dispatch(actions.users.update(profileUser._id, { location: e.target.value }))}
|
onChange={e => onUserEdited(profileUser._id, { location: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label="Website or URL" placeholder="https://yourwebsite.com" icon="globe" iconPosition="left"
|
label="Website or URL" placeholder="https://yourwebsite.com" icon="globe" iconPosition="left"
|
||||||
value={profileUser.website}
|
value={profileUser.website}
|
||||||
onChange={e => 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>
|
</Form>
|
||||||
</Segment>
|
</Segment>
|
||||||
|
|
||||||
@ -96,28 +90,44 @@ function EditProfile() {
|
|||||||
<Form.Input
|
<Form.Input
|
||||||
label="Twitter" placeholder="@username" icon="twitter" iconPosition="left"
|
label="Twitter" placeholder="@username" icon="twitter" iconPosition="left"
|
||||||
value={profileUser.twitter}
|
value={profileUser.twitter}
|
||||||
onChange={e => dispatch(actions.users.update(profileUser._id, { twitter: e.target.value }))}
|
onChange={e => onUserEdited(profileUser._id, { twitter: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label="Facebook" placeholder="username" icon="facebook" iconPosition="left"
|
label="Facebook" placeholder="username" icon="facebook" iconPosition="left"
|
||||||
value={profileUser.facebook}
|
value={profileUser.facebook}
|
||||||
onChange={e => dispatch(actions.users.update(profileUser._id, { facebook: e.target.value }))}
|
onChange={e => onUserEdited(profileUser._id, { facebook: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label="Instagram" placeholder="username" icon="instagram" iconPosition="left"
|
label="Instagram" placeholder="username" icon="instagram" iconPosition="left"
|
||||||
value={profileUser.instagram}
|
value={profileUser.instagram}
|
||||||
onChange={e => dispatch(actions.users.update(profileUser._id, { instagram: e.target.value }))}
|
onChange={e => onUserEdited(profileUser._id, { instagram: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label="LinkedIn" placeholder="username" icon="linkedin" iconPosition="left"
|
label="LinkedIn" placeholder="username" icon="linkedin" iconPosition="left"
|
||||||
value={profileUser.linkedIn}
|
value={profileUser.linkedIn}
|
||||||
onChange={e => 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>
|
</Form>
|
||||||
</Segment>
|
</Segment>
|
||||||
|
|
||||||
</div>);
|
</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 { Helmet } from 'react-helmet';
|
||||||
import { Loader, Icon, List, Container, Card, Grid, Message } from 'semantic-ui-react';
|
import { Loader, Icon, List, Container, Card, Grid, Message } from 'semantic-ui-react';
|
||||||
import { Link, Outlet, useParams } from 'react-router-dom';
|
import { Link, Switch, Route } from 'react-router-dom';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import utils from 'utils/utils';
|
import utils from 'utils/utils';
|
||||||
import actions from 'actions';
|
import actions from 'actions';
|
||||||
import api from 'api';
|
import api from 'api';
|
||||||
|
|
||||||
|
import EditProfile from './EditProfile';
|
||||||
|
import ProfileProjects from './ProfileProjects';
|
||||||
import BlurrableImage from 'components/includes/BlurrableImage';
|
import BlurrableImage from 'components/includes/BlurrableImage';
|
||||||
|
|
||||||
function Profile() {
|
class Profile extends Component {
|
||||||
const [loading, setLoading] = useState(false);
|
constructor(props) {
|
||||||
const dispatch = useDispatch();
|
super(props);
|
||||||
const { username } = useParams();
|
this.state = { loading: false };
|
||||||
|
}
|
||||||
const { user, profileUser, errorMessage } = useSelector(state => {
|
componentDidMount() {
|
||||||
const users = state.users.users;
|
this.setState({ loading: true });
|
||||||
const profileUser = users.filter(u => username === u.username)[0];
|
api.users.get(this.props.match.params.username, user => {
|
||||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
this.props.onReceiveUser(user);
|
||||||
return { user, profileUser, errorMessage: state.users.errorMessage };
|
this.setState({ loading: false });
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(true);
|
|
||||||
api.users.get(username, user => {
|
|
||||||
dispatch(actions.users.receive(user));
|
|
||||||
setLoading(false);
|
|
||||||
}, err => {
|
}, err => {
|
||||||
dispatch(actions.users.requestFailed(err));
|
this.props.onRequestFailed(err);
|
||||||
setLoading(false);
|
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 (
|
return (
|
||||||
<Container style={{ marginTop: '40px' }}>
|
<Container style={{ marginTop: '40px' }}>
|
||||||
<Helmet title={profileUser?.username ? `${profileUser.username}'s Profile` : 'Profile'} />
|
<Helmet title={profileUser?.username ? `${profileUser.username}'s Profile` : 'Profile'} />
|
||||||
{loading && !profileUser &&
|
{loading && !profileUser &&
|
||||||
<div style={{textAlign: 'center'}}>
|
<div style={{textAlign: 'center'}}>
|
||||||
<h4>Loading {username}'s profile...</h4>
|
<h4>Loading {this.props.match.params.username}'s profile...</h4>
|
||||||
<Loader active inline="centered" />
|
<Loader active inline="centered" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -162,7 +166,10 @@ function Profile() {
|
|||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
|
|
||||||
<Grid.Column computer={11}>
|
<Grid.Column computer={11}>
|
||||||
<Outlet />
|
<Switch>
|
||||||
|
<Route path="/:username/edit" component={EditProfile} />
|
||||||
|
<Route component={ProfileProjects} />
|
||||||
|
</Switch>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
)
|
||||||
@ -170,5 +177,22 @@ function Profile() {
|
|||||||
</Container>
|
</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 { Icon, Divider, Card, Message } from 'semantic-ui-react';
|
||||||
import { useSelector } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import ProjectCard from 'components/includes/ProjectCard';
|
import ProjectCard from 'components/includes/ProjectCard';
|
||||||
|
|
||||||
function ProfileProjects() {
|
class ProfileProjects extends Component {
|
||||||
const { username } = useParams();
|
render() {
|
||||||
const { profileUser } = useSelector(state => {
|
const { profileUser } = this.props;
|
||||||
const users = state.users.users;
|
|
||||||
const profileUser = users.filter(u => u.username === username)[0];
|
|
||||||
return { profileUser };
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3><Icon name='book' /> {profileUser.username}'s projects</h3>
|
<h3><Icon name='book' /> {profileUser.username}'s projects</h3>
|
||||||
@ -33,5 +27,16 @@ function ProfileProjects() {
|
|||||||
}
|
}
|
||||||
</div>);
|
</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 { Helmet } from 'react-helmet';
|
||||||
import { Container } from 'semantic-ui-react';
|
import { Container } from 'semantic-ui-react';
|
||||||
|
|
||||||
function PrivacyPolicy() {
|
class PrivacyPolicy extends Component {
|
||||||
|
render() {
|
||||||
return (
|
return (
|
||||||
<Container style={{ marginTop: 50, marginBottom: 50 }}>
|
<Container style={{ marginTop: 50, marginBottom: 50 }}>
|
||||||
<Helmet title='Privacy Policy' />
|
<Helmet title='Privacy Policy' />
|
||||||
@ -72,5 +73,6 @@ function PrivacyPolicy() {
|
|||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default PrivacyPolicy;
|
export default PrivacyPolicy;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { Container } from 'semantic-ui-react';
|
import { Container } from 'semantic-ui-react';
|
||||||
|
|
||||||
function TermsOfUse() {
|
class TermsOfUse extends Component {
|
||||||
|
render() {
|
||||||
return (
|
return (
|
||||||
<Container style={{ marginTop: 50, marginBottom: 50 }}>
|
<Container style={{ marginTop: 50, marginBottom: 50 }}>
|
||||||
<Helmet title='Terms of Use' />
|
<Helmet title='Terms of Use' />
|
||||||
@ -38,5 +39,6 @@ function TermsOfUse() {
|
|||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default TermsOfUse;
|
export default TermsOfUse;
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
import 'react-app-polyfill/ie9';
|
import 'react-app-polyfill/ie9';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { Switch, Route, Router } from 'react-router-dom';
|
||||||
import { createStore } from 'redux';
|
import { createStore } from 'redux';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import { BrowserTracing } from '@sentry/tracing';
|
import { BrowserTracing } from '@sentry/tracing';
|
||||||
|
import createBrowserHistory from 'history/createBrowserHistory';
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
import 'pell/dist/pell.min.css';
|
import 'pell/dist/pell.min.css';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import reducers from './reducers';
|
import reducers from './reducers';
|
||||||
import App from './components/App.js';
|
import App from './components/App.js';
|
||||||
|
import DraftExport from 'components/main/projects/objects/DraftExport';
|
||||||
import * as serviceWorker from './registerServiceWorker';
|
import * as serviceWorker from './registerServiceWorker';
|
||||||
|
|
||||||
export const store = createStore(reducers);
|
export const store = createStore(reducers);
|
||||||
@ -26,9 +28,12 @@ if (process.env.REACT_APP_SENTRY_DSN) {
|
|||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<BrowserRouter>
|
<Router history={createBrowserHistory()}>
|
||||||
<App />
|
<Switch>
|
||||||
</BrowserRouter>
|
<Route path="/objects/:id/export" component={DraftExport} />
|
||||||
|
<Route component={App} />
|
||||||
|
</Switch>
|
||||||
|
</Router>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
document.getElementById('root'),
|
document.getElementById('root'),
|
||||||
);
|
);
|
||||||
|
689
web/yarn.lock
689
web/yarn.lock
@ -1110,20 +1110,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.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"
|
version "7.11.2"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
|
||||||
integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
|
integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/runtime@^7.12.1", "@babel/runtime@^7.7.6":
|
|
||||||
version "7.17.9"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72"
|
|
||||||
integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==
|
|
||||||
dependencies:
|
|
||||||
regenerator-runtime "^0.13.4"
|
|
||||||
|
|
||||||
"@babel/template@^7.10.4", "@babel/template@^7.4.0", "@babel/template@^7.8.6":
|
"@babel/template@^7.10.4", "@babel/template@^7.4.0", "@babel/template@^7.8.6":
|
||||||
version "7.10.4"
|
version "7.10.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
|
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
|
||||||
@ -1636,13 +1629,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
|
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
|
||||||
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
|
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
|
||||||
|
|
||||||
"@types/debug@^4.0.0":
|
|
||||||
version "4.1.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
|
|
||||||
integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==
|
|
||||||
dependencies:
|
|
||||||
"@types/ms" "*"
|
|
||||||
|
|
||||||
"@types/eslint-visitor-keys@^1.0.0":
|
"@types/eslint-visitor-keys@^1.0.0":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
|
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
|
||||||
@ -1656,21 +1642,6 @@
|
|||||||
"@types/minimatch" "*"
|
"@types/minimatch" "*"
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/hast@^2.0.0":
|
|
||||||
version "2.3.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc"
|
|
||||||
integrity sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==
|
|
||||||
dependencies:
|
|
||||||
"@types/unist" "*"
|
|
||||||
|
|
||||||
"@types/hoist-non-react-statics@^3.3.1":
|
|
||||||
version "3.3.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
|
|
||||||
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
|
|
||||||
dependencies:
|
|
||||||
"@types/react" "*"
|
|
||||||
hoist-non-react-statics "^3.3.0"
|
|
||||||
|
|
||||||
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
|
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
|
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
|
||||||
@ -1696,28 +1667,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd"
|
||||||
integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==
|
integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==
|
||||||
|
|
||||||
"@types/mdast@^3.0.0":
|
|
||||||
version "3.0.10"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af"
|
|
||||||
integrity sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==
|
|
||||||
dependencies:
|
|
||||||
"@types/unist" "*"
|
|
||||||
|
|
||||||
"@types/mdurl@^1.0.0":
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9"
|
|
||||||
integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==
|
|
||||||
|
|
||||||
"@types/minimatch@*":
|
"@types/minimatch@*":
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
|
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
|
||||||
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
|
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
|
||||||
|
|
||||||
"@types/ms@*":
|
|
||||||
version "0.7.31"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
|
|
||||||
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
|
|
||||||
|
|
||||||
"@types/node@*":
|
"@types/node@*":
|
||||||
version "14.6.0"
|
version "14.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.0.tgz#7d4411bf5157339337d7cff864d9ff45f177b499"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.0.tgz#7d4411bf5157339337d7cff864d9ff45f177b499"
|
||||||
@ -1728,45 +1682,16 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||||
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
||||||
|
|
||||||
"@types/prop-types@*", "@types/prop-types@^15.0.0":
|
|
||||||
version "15.7.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
|
||||||
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
|
|
||||||
|
|
||||||
"@types/q@^1.5.1":
|
"@types/q@^1.5.1":
|
||||||
version "1.5.4"
|
version "1.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
|
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
|
||||||
integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
|
integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
|
||||||
|
|
||||||
"@types/react@*":
|
|
||||||
version "18.0.8"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.8.tgz#a051eb380a9fbcaa404550543c58e1cf5ce4ab87"
|
|
||||||
integrity sha512-+j2hk9BzCOrrOSJASi5XiOyBbERk9jG5O73Ya4M0env5Ixi6vUNli4qy994AINcEF+1IEHISYFfIT4zwr++LKw==
|
|
||||||
dependencies:
|
|
||||||
"@types/prop-types" "*"
|
|
||||||
"@types/scheduler" "*"
|
|
||||||
csstype "^3.0.2"
|
|
||||||
|
|
||||||
"@types/scheduler@*":
|
|
||||||
version "0.16.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
|
|
||||||
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
|
|
||||||
|
|
||||||
"@types/stack-utils@^1.0.1":
|
"@types/stack-utils@^1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
|
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
|
||||||
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
|
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
|
||||||
|
|
||||||
"@types/unist@*", "@types/unist@^2.0.0":
|
|
||||||
version "2.0.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
|
|
||||||
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
|
|
||||||
|
|
||||||
"@types/use-sync-external-store@^0.0.3":
|
|
||||||
version "0.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43"
|
|
||||||
integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==
|
|
||||||
|
|
||||||
"@types/yargs-parser@*":
|
"@types/yargs-parser@*":
|
||||||
version "15.0.0"
|
version "15.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d"
|
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d"
|
||||||
@ -2529,11 +2454,6 @@ babylon@^6.18.0:
|
|||||||
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
|
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
|
||||||
integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==
|
integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==
|
||||||
|
|
||||||
bail@^2.0.0:
|
|
||||||
version "2.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d"
|
|
||||||
integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==
|
|
||||||
|
|
||||||
balanced-match@^1.0.0:
|
balanced-match@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||||
@ -2991,11 +2911,6 @@ chalk@^4.1.0:
|
|||||||
ansi-styles "^4.1.0"
|
ansi-styles "^4.1.0"
|
||||||
supports-color "^7.1.0"
|
supports-color "^7.1.0"
|
||||||
|
|
||||||
character-entities@^2.0.0:
|
|
||||||
version "2.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.1.tgz#98724833e1e27990dee0bd0f2b8a859c3476aac7"
|
|
||||||
integrity sha512-OzmutCf2Kmc+6DrFrrPS8/tDh2+DpnrfzdICHWhcVC9eOd0N1PXmQEE1a8iM4IziIAG+8tmTq3K+oo0ubH6RRQ==
|
|
||||||
|
|
||||||
chardet@^0.7.0:
|
chardet@^0.7.0:
|
||||||
version "0.7.0"
|
version "0.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
|
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
|
||||||
@ -3221,11 +3136,6 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
|
|||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream "~1.0.0"
|
delayed-stream "~1.0.0"
|
||||||
|
|
||||||
comma-separated-tokens@^2.0.0:
|
|
||||||
version "2.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz#d4c25abb679b7751c880be623c1179780fe1dd98"
|
|
||||||
integrity sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg==
|
|
||||||
|
|
||||||
commander@^2.11.0, commander@^2.20.0:
|
commander@^2.11.0, commander@^2.20.0:
|
||||||
version "2.20.3"
|
version "2.20.3"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||||
@ -3749,11 +3659,6 @@ cssstyle@^1.0.0, cssstyle@^1.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
cssom "0.3.x"
|
cssom "0.3.x"
|
||||||
|
|
||||||
csstype@^3.0.2:
|
|
||||||
version "3.0.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33"
|
|
||||||
integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==
|
|
||||||
|
|
||||||
cyclist@^1.0.1:
|
cyclist@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
|
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
|
||||||
@ -3802,13 +3707,6 @@ debug@^3.1.1, debug@^3.2.5:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "^2.1.1"
|
ms "^2.1.1"
|
||||||
|
|
||||||
debug@^4.0.0:
|
|
||||||
version "4.3.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
|
||||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
|
||||||
dependencies:
|
|
||||||
ms "2.1.2"
|
|
||||||
|
|
||||||
debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
|
debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
|
||||||
version "4.1.1"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
|
||||||
@ -3821,13 +3719,6 @@ decamelize@^1.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||||
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
|
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
|
||||||
|
|
||||||
decode-named-character-reference@^1.0.0:
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.1.tgz#57b2bd9112659cacbc449d3577d7dadb8e1f3d1b"
|
|
||||||
integrity sha512-YV/0HQHreRwKb7uBopyIkLG17jG6Sv2qUchk9qSoVJ2f+flwRsPNBO0hAnjt6mTNYUT+vw9Gy2ihXg4sUWPi2w==
|
|
||||||
dependencies:
|
|
||||||
character-entities "^2.0.0"
|
|
||||||
|
|
||||||
decode-uri-component@^0.2.0:
|
decode-uri-component@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
|
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
|
||||||
@ -3920,11 +3811,6 @@ depd@~1.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||||
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
|
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
|
||||||
|
|
||||||
dequal@^2.0.0:
|
|
||||||
version "2.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d"
|
|
||||||
integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==
|
|
||||||
|
|
||||||
des.js@^1.0.0:
|
des.js@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
|
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
|
||||||
@ -3961,11 +3847,6 @@ diff-sequences@^24.9.0:
|
|||||||
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5"
|
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5"
|
||||||
integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==
|
integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==
|
||||||
|
|
||||||
diff@^5.0.0:
|
|
||||||
version "5.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
|
|
||||||
integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
|
|
||||||
|
|
||||||
diffie-hellman@^5.0.0:
|
diffie-hellman@^5.0.0:
|
||||||
version "5.0.3"
|
version "5.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
|
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
|
||||||
@ -4713,7 +4594,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
|
|||||||
assign-symbols "^1.0.0"
|
assign-symbols "^1.0.0"
|
||||||
is-extendable "^1.0.1"
|
is-extendable "^1.0.1"
|
||||||
|
|
||||||
extend@^3.0.0, extend@~3.0.2:
|
extend@~3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||||
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
|
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
|
||||||
@ -5348,11 +5229,6 @@ hash.js@^1.0.0, hash.js@^1.0.3:
|
|||||||
inherits "^2.0.3"
|
inherits "^2.0.3"
|
||||||
minimalistic-assert "^1.0.1"
|
minimalistic-assert "^1.0.1"
|
||||||
|
|
||||||
hast-util-whitespace@^2.0.0:
|
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz#4fc1086467cc1ef5ba20673cb6b03cec3a970f1c"
|
|
||||||
integrity sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg==
|
|
||||||
|
|
||||||
he@^1.2.0:
|
he@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||||
@ -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"
|
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
|
||||||
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
|
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
|
||||||
|
|
||||||
history@^4.7.2:
|
history@^4.7.2, history@^4.9.0:
|
||||||
version "4.10.1"
|
version "4.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
|
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
|
||||||
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
|
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
|
||||||
@ -5375,13 +5251,6 @@ history@^4.7.2:
|
|||||||
tiny-warning "^1.0.0"
|
tiny-warning "^1.0.0"
|
||||||
value-equal "^1.0.1"
|
value-equal "^1.0.1"
|
||||||
|
|
||||||
history@^5.2.0:
|
|
||||||
version "5.3.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b"
|
|
||||||
integrity sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.7.6"
|
|
||||||
|
|
||||||
hmac-drbg@^1.0.0:
|
hmac-drbg@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
||||||
@ -5391,7 +5260,7 @@ hmac-drbg@^1.0.0:
|
|||||||
minimalistic-assert "^1.0.0"
|
minimalistic-assert "^1.0.0"
|
||||||
minimalistic-crypto-utils "^1.0.1"
|
minimalistic-crypto-utils "^1.0.1"
|
||||||
|
|
||||||
hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.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"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||||
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
||||||
@ -5699,11 +5568,6 @@ ini@^1.3.5:
|
|||||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
||||||
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
|
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
|
||||||
|
|
||||||
inline-style-parser@0.1.1:
|
|
||||||
version "0.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1"
|
|
||||||
integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==
|
|
||||||
|
|
||||||
inquirer@7.0.4:
|
inquirer@7.0.4:
|
||||||
version "7.0.4"
|
version "7.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.4.tgz#99af5bde47153abca23f5c7fc30db247f39da703"
|
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.4.tgz#99af5bde47153abca23f5c7fc30db247f39da703"
|
||||||
@ -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"
|
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
||||||
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
|
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
|
||||||
|
|
||||||
is-buffer@^2.0.0:
|
|
||||||
version "2.0.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191"
|
|
||||||
integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==
|
|
||||||
|
|
||||||
is-callable@^1.1.4, is-callable@^1.2.0:
|
is-callable@^1.1.4, is-callable@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb"
|
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb"
|
||||||
@ -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"
|
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
|
||||||
integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
|
integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
|
||||||
|
|
||||||
is-plain-obj@^4.0.0:
|
|
||||||
version "4.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.0.0.tgz#06c0999fd7574edf5a906ba5644ad0feb3a84d22"
|
|
||||||
integrity sha512-NXRbBtUdBioI73y/HmOhogw/U5msYPC9DAtGkJXeFcFWSFZw0mCUsPxk/snTuJHzNKA8kLBK4rH97RMB1BfCXw==
|
|
||||||
|
|
||||||
is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
|
is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
|
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
|
||||||
@ -6104,6 +5958,11 @@ is-wsl@^2.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-docker "^2.0.0"
|
is-docker "^2.0.0"
|
||||||
|
|
||||||
|
isarray@0.0.1:
|
||||||
|
version "0.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
|
||||||
|
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
|
||||||
|
|
||||||
isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
|
isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||||
@ -6787,11 +6646,6 @@ kleur@^3.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
|
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
|
||||||
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
|
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
|
||||||
|
|
||||||
kleur@^4.0.3:
|
|
||||||
version "4.1.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.4.tgz#8c202987d7e577766d039a8cd461934c01cda04d"
|
|
||||||
integrity sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==
|
|
||||||
|
|
||||||
last-call-webpack-plugin@^3.0.0:
|
last-call-webpack-plugin@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555"
|
resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555"
|
||||||
@ -6966,7 +6820,7 @@ loglevel@^1.6.6:
|
|||||||
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171"
|
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171"
|
||||||
integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA==
|
integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA==
|
||||||
|
|
||||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.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"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||||
@ -7042,54 +6896,6 @@ md5.js@^1.3.4:
|
|||||||
inherits "^2.0.1"
|
inherits "^2.0.1"
|
||||||
safe-buffer "^5.1.2"
|
safe-buffer "^5.1.2"
|
||||||
|
|
||||||
mdast-util-definitions@^5.0.0:
|
|
||||||
version "5.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.0.tgz#b6d10ef00a3c4cf191e8d9a5fa58d7f4a366f817"
|
|
||||||
integrity sha512-5hcR7FL2EuZ4q6lLMUK5w4lHT2H3vqL9quPvYZ/Ku5iifrirfMHiGdhxdXMUbUkDmz5I+TYMd7nbaxUhbQkfpQ==
|
|
||||||
dependencies:
|
|
||||||
"@types/mdast" "^3.0.0"
|
|
||||||
"@types/unist" "^2.0.0"
|
|
||||||
unist-util-visit "^3.0.0"
|
|
||||||
|
|
||||||
mdast-util-from-markdown@^1.0.0:
|
|
||||||
version "1.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.2.0.tgz#84df2924ccc6c995dec1e2368b2b208ad0a76268"
|
|
||||||
integrity sha512-iZJyyvKD1+K7QX1b5jXdE7Sc5dtoTry1vzV28UZZe8Z1xVnB/czKntJ7ZAkG0tANqRnBF6p3p7GpU1y19DTf2Q==
|
|
||||||
dependencies:
|
|
||||||
"@types/mdast" "^3.0.0"
|
|
||||||
"@types/unist" "^2.0.0"
|
|
||||||
decode-named-character-reference "^1.0.0"
|
|
||||||
mdast-util-to-string "^3.1.0"
|
|
||||||
micromark "^3.0.0"
|
|
||||||
micromark-util-decode-numeric-character-reference "^1.0.0"
|
|
||||||
micromark-util-decode-string "^1.0.0"
|
|
||||||
micromark-util-normalize-identifier "^1.0.0"
|
|
||||||
micromark-util-symbol "^1.0.0"
|
|
||||||
micromark-util-types "^1.0.0"
|
|
||||||
unist-util-stringify-position "^3.0.0"
|
|
||||||
uvu "^0.5.0"
|
|
||||||
|
|
||||||
mdast-util-to-hast@^12.1.0:
|
|
||||||
version "12.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.1.1.tgz#89a2bb405eaf3b05eb8bf45157678f35eef5dbca"
|
|
||||||
integrity sha512-qE09zD6ylVP14jV4mjLIhDBOrpFdShHZcEsYvvKGABlr9mGbV7mTlRWdoFxL/EYSTNDiC9GZXy7y8Shgb9Dtzw==
|
|
||||||
dependencies:
|
|
||||||
"@types/hast" "^2.0.0"
|
|
||||||
"@types/mdast" "^3.0.0"
|
|
||||||
"@types/mdurl" "^1.0.0"
|
|
||||||
mdast-util-definitions "^5.0.0"
|
|
||||||
mdurl "^1.0.0"
|
|
||||||
micromark-util-sanitize-uri "^1.0.0"
|
|
||||||
unist-builder "^3.0.0"
|
|
||||||
unist-util-generated "^2.0.0"
|
|
||||||
unist-util-position "^4.0.0"
|
|
||||||
unist-util-visit "^4.0.0"
|
|
||||||
|
|
||||||
mdast-util-to-string@^3.1.0:
|
|
||||||
version "3.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz#56c506d065fbf769515235e577b5a261552d56e9"
|
|
||||||
integrity sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==
|
|
||||||
|
|
||||||
mdn-data@2.0.4:
|
mdn-data@2.0.4:
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
|
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
|
||||||
@ -7100,11 +6906,6 @@ mdn-data@2.0.6:
|
|||||||
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978"
|
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978"
|
||||||
integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==
|
integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==
|
||||||
|
|
||||||
mdurl@^1.0.0:
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
|
|
||||||
integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
|
|
||||||
|
|
||||||
media-typer@0.3.0:
|
media-typer@0.3.0:
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||||
@ -7169,201 +6970,6 @@ microevent.ts@~0.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0"
|
resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0"
|
||||||
integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==
|
integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==
|
||||||
|
|
||||||
micromark-core-commonmark@^1.0.1:
|
|
||||||
version "1.0.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz#edff4c72e5993d93724a3c206970f5a15b0585ad"
|
|
||||||
integrity sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==
|
|
||||||
dependencies:
|
|
||||||
decode-named-character-reference "^1.0.0"
|
|
||||||
micromark-factory-destination "^1.0.0"
|
|
||||||
micromark-factory-label "^1.0.0"
|
|
||||||
micromark-factory-space "^1.0.0"
|
|
||||||
micromark-factory-title "^1.0.0"
|
|
||||||
micromark-factory-whitespace "^1.0.0"
|
|
||||||
micromark-util-character "^1.0.0"
|
|
||||||
micromark-util-chunked "^1.0.0"
|
|
||||||
micromark-util-classify-character "^1.0.0"
|
|
||||||
micromark-util-html-tag-name "^1.0.0"
|
|
||||||
micromark-util-normalize-identifier "^1.0.0"
|
|
||||||
micromark-util-resolve-all "^1.0.0"
|
|
||||||
micromark-util-subtokenize "^1.0.0"
|
|
||||||
micromark-util-symbol "^1.0.0"
|
|
||||||
micromark-util-types "^1.0.1"
|
|
||||||
uvu "^0.5.0"
|
|
||||||
|
|
||||||
micromark-factory-destination@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz#fef1cb59ad4997c496f887b6977aa3034a5a277e"
|
|
||||||
integrity sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==
|
|
||||||
dependencies:
|
|
||||||
micromark-util-character "^1.0.0"
|
|
||||||
micromark-util-symbol "^1.0.0"
|
|
||||||
micromark-util-types "^1.0.0"
|
|
||||||
|
|
||||||
micromark-factory-label@^1.0.0:
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz#6be2551fa8d13542fcbbac478258fb7a20047137"
|
|
||||||
integrity sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==
|
|
||||||
dependencies:
|
|
||||||
micromark-util-character "^1.0.0"
|
|
||||||
micromark-util-symbol "^1.0.0"
|
|
||||||
micromark-util-types "^1.0.0"
|
|
||||||
uvu "^0.5.0"
|
|
||||||
|
|
||||||
micromark-factory-space@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz#cebff49968f2b9616c0fcb239e96685cb9497633"
|
|
||||||
integrity sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==
|
|
||||||
dependencies:
|
|
||||||
micromark-util-character "^1.0.0"
|
|
||||||
micromark-util-types "^1.0.0"
|
|
||||||
|
|
||||||
micromark-factory-title@^1.0.0:
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz#7e09287c3748ff1693930f176e1c4a328382494f"
|
|
||||||
integrity sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==
|
|
||||||
dependencies:
|
|
||||||
micromark-factory-space "^1.0.0"
|
|
||||||
micromark-util-character "^1.0.0"
|
|
||||||
micromark-util-symbol "^1.0.0"
|
|
||||||
micromark-util-types "^1.0.0"
|
|
||||||
uvu "^0.5.0"
|
|
||||||
|
|
||||||
micromark-factory-whitespace@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz#e991e043ad376c1ba52f4e49858ce0794678621c"
|
|
||||||
integrity sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==
|
|
||||||
dependencies:
|
|
||||||
micromark-factory-space "^1.0.0"
|
|
||||||
micromark-util-character "^1.0.0"
|
|
||||||
micromark-util-symbol "^1.0.0"
|
|
||||||
micromark-util-types "^1.0.0"
|
|
||||||
|
|
||||||
micromark-util-character@^1.0.0:
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.1.0.tgz#d97c54d5742a0d9611a68ca0cd4124331f264d86"
|
|
||||||
integrity sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==
|
|
||||||
dependencies:
|
|
||||||
micromark-util-symbol "^1.0.0"
|
|
||||||
micromark-util-types "^1.0.0"
|
|
||||||
|
|
||||||
micromark-util-chunked@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz#5b40d83f3d53b84c4c6bce30ed4257e9a4c79d06"
|
|
||||||
integrity sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==
|
|
||||||
dependencies:
|
|
||||||
micromark-util-symbol "^1.0.0"
|
|
||||||
|
|
||||||
micromark-util-classify-character@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz#cbd7b447cb79ee6997dd274a46fc4eb806460a20"
|
|
||||||
integrity sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==
|
|
||||||
dependencies:
|
|
||||||
micromark-util-character "^1.0.0"
|
|
||||||
micromark-util-symbol "^1.0.0"
|
|
||||||
micromark-util-types "^1.0.0"
|
|
||||||
|
|
||||||
micromark-util-combine-extensions@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz#91418e1e74fb893e3628b8d496085639124ff3d5"
|
|
||||||
integrity sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==
|
|
||||||
dependencies:
|
|
||||||
micromark-util-chunked "^1.0.0"
|
|
||||||
micromark-util-types "^1.0.0"
|
|
||||||
|
|
||||||
micromark-util-decode-numeric-character-reference@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz#dcc85f13b5bd93ff8d2868c3dba28039d490b946"
|
|
||||||
integrity sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==
|
|
||||||
dependencies:
|
|
||||||
micromark-util-symbol "^1.0.0"
|
|
||||||
|
|
||||||
micromark-util-decode-string@^1.0.0:
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz#942252ab7a76dec2dbf089cc32505ee2bc3acf02"
|
|
||||||
integrity sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==
|
|
||||||
dependencies:
|
|
||||||
decode-named-character-reference "^1.0.0"
|
|
||||||
micromark-util-character "^1.0.0"
|
|
||||||
micromark-util-decode-numeric-character-reference "^1.0.0"
|
|
||||||
micromark-util-symbol "^1.0.0"
|
|
||||||
|
|
||||||
micromark-util-encode@^1.0.0:
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz#2c1c22d3800870ad770ece5686ebca5920353383"
|
|
||||||
integrity sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==
|
|
||||||
|
|
||||||
micromark-util-html-tag-name@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.0.0.tgz#75737e92fef50af0c6212bd309bc5cb8dbd489ed"
|
|
||||||
integrity sha512-NenEKIshW2ZI/ERv9HtFNsrn3llSPZtY337LID/24WeLqMzeZhBEE6BQ0vS2ZBjshm5n40chKtJ3qjAbVV8S0g==
|
|
||||||
|
|
||||||
micromark-util-normalize-identifier@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz#4a3539cb8db954bbec5203952bfe8cedadae7828"
|
|
||||||
integrity sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==
|
|
||||||
dependencies:
|
|
||||||
micromark-util-symbol "^1.0.0"
|
|
||||||
|
|
||||||
micromark-util-resolve-all@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz#a7c363f49a0162e931960c44f3127ab58f031d88"
|
|
||||||
integrity sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==
|
|
||||||
dependencies:
|
|
||||||
micromark-util-types "^1.0.0"
|
|
||||||
|
|
||||||
micromark-util-sanitize-uri@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.0.0.tgz#27dc875397cd15102274c6c6da5585d34d4f12b2"
|
|
||||||
integrity sha512-cCxvBKlmac4rxCGx6ejlIviRaMKZc0fWm5HdCHEeDWRSkn44l6NdYVRyU+0nT1XC72EQJMZV8IPHF+jTr56lAg==
|
|
||||||
dependencies:
|
|
||||||
micromark-util-character "^1.0.0"
|
|
||||||
micromark-util-encode "^1.0.0"
|
|
||||||
micromark-util-symbol "^1.0.0"
|
|
||||||
|
|
||||||
micromark-util-subtokenize@^1.0.0:
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz#ff6f1af6ac836f8bfdbf9b02f40431760ad89105"
|
|
||||||
integrity sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==
|
|
||||||
dependencies:
|
|
||||||
micromark-util-chunked "^1.0.0"
|
|
||||||
micromark-util-symbol "^1.0.0"
|
|
||||||
micromark-util-types "^1.0.0"
|
|
||||||
uvu "^0.5.0"
|
|
||||||
|
|
||||||
micromark-util-symbol@^1.0.0:
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz#b90344db62042ce454f351cf0bebcc0a6da4920e"
|
|
||||||
integrity sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==
|
|
||||||
|
|
||||||
micromark-util-types@^1.0.0, micromark-util-types@^1.0.1:
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.0.2.tgz#f4220fdb319205812f99c40f8c87a9be83eded20"
|
|
||||||
integrity sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==
|
|
||||||
|
|
||||||
micromark@^3.0.0:
|
|
||||||
version "3.0.10"
|
|
||||||
resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.0.10.tgz#1eac156f0399d42736458a14b0ca2d86190b457c"
|
|
||||||
integrity sha512-ryTDy6UUunOXy2HPjelppgJ2sNfcPz1pLlMdA6Rz9jPzhLikWXv/irpWV/I2jd68Uhmny7hHxAlAhk4+vWggpg==
|
|
||||||
dependencies:
|
|
||||||
"@types/debug" "^4.0.0"
|
|
||||||
debug "^4.0.0"
|
|
||||||
decode-named-character-reference "^1.0.0"
|
|
||||||
micromark-core-commonmark "^1.0.1"
|
|
||||||
micromark-factory-space "^1.0.0"
|
|
||||||
micromark-util-character "^1.0.0"
|
|
||||||
micromark-util-chunked "^1.0.0"
|
|
||||||
micromark-util-combine-extensions "^1.0.0"
|
|
||||||
micromark-util-decode-numeric-character-reference "^1.0.0"
|
|
||||||
micromark-util-encode "^1.0.0"
|
|
||||||
micromark-util-normalize-identifier "^1.0.0"
|
|
||||||
micromark-util-resolve-all "^1.0.0"
|
|
||||||
micromark-util-sanitize-uri "^1.0.0"
|
|
||||||
micromark-util-subtokenize "^1.0.0"
|
|
||||||
micromark-util-symbol "^1.0.0"
|
|
||||||
micromark-util-types "^1.0.1"
|
|
||||||
uvu "^0.5.0"
|
|
||||||
|
|
||||||
micromatch@^3.1.10, micromatch@^3.1.4:
|
micromatch@^3.1.10, micromatch@^3.1.4:
|
||||||
version "3.1.10"
|
version "3.1.10"
|
||||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
|
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
|
||||||
@ -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"
|
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
|
||||||
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
|
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
|
||||||
|
|
||||||
|
mini-create-react-context@^0.4.0:
|
||||||
|
version "0.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz#df60501c83151db69e28eac0ef08b4002efab040"
|
||||||
|
integrity sha512-b0TytUgFSbgFJGzJqXPKCFCBWigAjpjo+Fl7Vf7ZbKRDptszpppKxXH6DRXEABZ/gcEQczeb0iZ7JvL8e8jjCA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.5.5"
|
||||||
|
tiny-warning "^1.0.3"
|
||||||
|
|
||||||
mini-css-extract-plugin@0.9.0:
|
mini-css-extract-plugin@0.9.0:
|
||||||
version "0.9.0"
|
version "0.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz#47f2cf07aa165ab35733b1fc97d4c46c0564339e"
|
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz#47f2cf07aa165ab35733b1fc97d4c46c0564339e"
|
||||||
@ -7534,11 +7148,6 @@ move-concurrently@^1.0.1:
|
|||||||
rimraf "^2.5.4"
|
rimraf "^2.5.4"
|
||||||
run-queue "^1.0.3"
|
run-queue "^1.0.3"
|
||||||
|
|
||||||
mri@^1.1.0:
|
|
||||||
version "1.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
|
|
||||||
integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==
|
|
||||||
|
|
||||||
ms@2.0.0:
|
ms@2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||||
@ -7549,7 +7158,7 @@ ms@2.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
|
||||||
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
|
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
|
||||||
|
|
||||||
ms@2.1.2, ms@^2.1.1:
|
ms@^2.1.1:
|
||||||
version "2.1.2"
|
version "2.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||||
@ -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"
|
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
|
||||||
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
|
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
|
||||||
|
|
||||||
|
path-to-regexp@^1.7.0:
|
||||||
|
version "1.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
|
||||||
|
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
|
||||||
|
dependencies:
|
||||||
|
isarray "0.0.1"
|
||||||
|
|
||||||
path-type@^2.0.0:
|
path-type@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
|
resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
|
||||||
@ -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"
|
object-assign "^4.1.1"
|
||||||
react-is "^16.8.1"
|
react-is "^16.8.1"
|
||||||
|
|
||||||
prop-types@^15.0.0:
|
|
||||||
version "15.8.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
|
||||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
|
||||||
dependencies:
|
|
||||||
loose-envify "^1.4.0"
|
|
||||||
object-assign "^4.1.1"
|
|
||||||
react-is "^16.13.1"
|
|
||||||
|
|
||||||
property-information@^6.0.0:
|
|
||||||
version "6.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.1.1.tgz#5ca85510a3019726cb9afed4197b7b8ac5926a22"
|
|
||||||
integrity sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w==
|
|
||||||
|
|
||||||
proxy-addr@~2.0.5:
|
proxy-addr@~2.0.5:
|
||||||
version "2.0.6"
|
version "2.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
|
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
|
||||||
@ -9433,16 +9035,11 @@ react-helmet@^6.0.0:
|
|||||||
react-fast-compare "^3.1.1"
|
react-fast-compare "^3.1.1"
|
||||||
react-side-effect "^2.1.0"
|
react-side-effect "^2.1.0"
|
||||||
|
|
||||||
react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.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"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
|
|
||||||
react-is@^18.0.0:
|
|
||||||
version "18.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67"
|
|
||||||
integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==
|
|
||||||
|
|
||||||
react-joyride@^2.4.0:
|
react-joyride@^2.4.0:
|
||||||
version "2.4.0"
|
version "2.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-joyride/-/react-joyride-2.4.0.tgz#273a99fea4804a48155e7cc7bae308dcbc8cb725"
|
resolved "https://registry.yarnpkg.com/react-joyride/-/react-joyride-2.4.0.tgz#273a99fea4804a48155e7cc7bae308dcbc8cb725"
|
||||||
@ -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"
|
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
||||||
|
|
||||||
react-markdown@^8.0.3:
|
|
||||||
version "8.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.3.tgz#e8aba0d2f5a1b2124d476ee1fff9448a2f57e4b3"
|
|
||||||
integrity sha512-We36SfqaKoVNpN1QqsZwWSv/OZt5J15LNgTLWynwAN5b265hrQrsjMtlRNwUvS+YyR3yDM8HpTNc4pK9H/Gc0A==
|
|
||||||
dependencies:
|
|
||||||
"@types/hast" "^2.0.0"
|
|
||||||
"@types/prop-types" "^15.0.0"
|
|
||||||
"@types/unist" "^2.0.0"
|
|
||||||
comma-separated-tokens "^2.0.0"
|
|
||||||
hast-util-whitespace "^2.0.0"
|
|
||||||
prop-types "^15.0.0"
|
|
||||||
property-information "^6.0.0"
|
|
||||||
react-is "^18.0.0"
|
|
||||||
remark-parse "^10.0.0"
|
|
||||||
remark-rehype "^10.0.0"
|
|
||||||
space-separated-tokens "^2.0.0"
|
|
||||||
style-to-object "^0.3.0"
|
|
||||||
unified "^10.0.0"
|
|
||||||
unist-util-visit "^4.0.0"
|
|
||||||
vfile "^5.0.0"
|
|
||||||
|
|
||||||
react-popper@^1.3.4:
|
react-popper@^1.3.4:
|
||||||
version "1.3.7"
|
version "1.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.7.tgz#f6a3471362ef1f0d10a4963673789de1baca2324"
|
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.7.tgz#f6a3471362ef1f0d10a4963673789de1baca2324"
|
||||||
@ -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"
|
resolved "https://registry.yarnpkg.com/react-proptype-conditional-require/-/react-proptype-conditional-require-1.0.4.tgz#69c2d5741e6df5e08f230f36bbc2944ee1222555"
|
||||||
integrity sha1-acLVdB5t9eCPIw82u8KUTuEiJVU=
|
integrity sha1-acLVdB5t9eCPIw82u8KUTuEiJVU=
|
||||||
|
|
||||||
react-redux@^8.0.1:
|
react-redux@^7.2.0:
|
||||||
version "8.0.1"
|
version "7.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.1.tgz#2bc029f5ada9b443107914c373a2750f6bc0f40c"
|
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.1.tgz#8dedf784901014db2feca1ab633864dee68ad985"
|
||||||
integrity sha512-LMZMsPY4DYdZfLJgd7i79n5Kps5N9XVLCJJeWAaPYTV+Eah2zTuBjTxKtNEbjiyitbq80/eIkm55CYSLqAub3w==
|
integrity sha512-T+VfD/bvgGTUA74iW9d2i5THrDQWbweXP0AVNI8tNd1Rk5ch1rnMiJkDD67ejw7YBKM4+REvcvqRuWJb7BLuEg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.1"
|
"@babel/runtime" "^7.5.5"
|
||||||
"@types/hoist-non-react-statics" "^3.3.1"
|
hoist-non-react-statics "^3.3.0"
|
||||||
"@types/use-sync-external-store" "^0.0.3"
|
loose-envify "^1.4.0"
|
||||||
hoist-non-react-statics "^3.3.2"
|
prop-types "^15.7.2"
|
||||||
react-is "^18.0.0"
|
react-is "^16.9.0"
|
||||||
use-sync-external-store "^1.0.0"
|
|
||||||
|
|
||||||
react-router-dom@^6.3.0:
|
react-router-dom@^5.1.2:
|
||||||
version "6.3.0"
|
version "5.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.3.0.tgz#a0216da813454e521905b5fa55e0e5176123f43d"
|
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662"
|
||||||
integrity sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==
|
integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==
|
||||||
dependencies:
|
dependencies:
|
||||||
history "^5.2.0"
|
"@babel/runtime" "^7.1.2"
|
||||||
react-router "6.3.0"
|
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:
|
react-router@5.2.0:
|
||||||
version "6.3.0"
|
version "5.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557"
|
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293"
|
||||||
integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==
|
integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==
|
||||||
dependencies:
|
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:
|
react-scripts@3.4.1:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
|
||||||
integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
|
integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
|
||||||
|
|
||||||
remark-parse@^10.0.0:
|
|
||||||
version "10.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.1.tgz#6f60ae53edbf0cf38ea223fe643db64d112e0775"
|
|
||||||
integrity sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==
|
|
||||||
dependencies:
|
|
||||||
"@types/mdast" "^3.0.0"
|
|
||||||
mdast-util-from-markdown "^1.0.0"
|
|
||||||
unified "^10.0.0"
|
|
||||||
|
|
||||||
remark-rehype@^10.0.0:
|
|
||||||
version "10.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-10.1.0.tgz#32dc99d2034c27ecaf2e0150d22a6dcccd9a6279"
|
|
||||||
integrity sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==
|
|
||||||
dependencies:
|
|
||||||
"@types/hast" "^2.0.0"
|
|
||||||
"@types/mdast" "^3.0.0"
|
|
||||||
mdast-util-to-hast "^12.1.0"
|
|
||||||
unified "^10.0.0"
|
|
||||||
|
|
||||||
remove-trailing-separator@^1.0.1:
|
remove-trailing-separator@^1.0.1:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
|
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
|
||||||
@ -10062,13 +9632,6 @@ rxjs@^6.5.3, rxjs@^6.6.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^1.9.0"
|
tslib "^1.9.0"
|
||||||
|
|
||||||
sade@^1.7.3:
|
|
||||||
version "1.8.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701"
|
|
||||||
integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==
|
|
||||||
dependencies:
|
|
||||||
mri "^1.1.0"
|
|
||||||
|
|
||||||
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||||
version "5.1.2"
|
version "5.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||||
@ -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"
|
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
|
||||||
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
|
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
|
||||||
|
|
||||||
space-separated-tokens@^2.0.0:
|
|
||||||
version "2.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz#43193cec4fb858a2ce934b7f98b7f2c18107098b"
|
|
||||||
integrity sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw==
|
|
||||||
|
|
||||||
spdx-correct@^3.0.0:
|
spdx-correct@^3.0.0:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
|
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
|
||||||
@ -10844,13 +10402,6 @@ style-loader@0.23.1:
|
|||||||
loader-utils "^1.1.0"
|
loader-utils "^1.1.0"
|
||||||
schema-utils "^1.0.0"
|
schema-utils "^1.0.0"
|
||||||
|
|
||||||
style-to-object@^0.3.0:
|
|
||||||
version "0.3.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46"
|
|
||||||
integrity sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==
|
|
||||||
dependencies:
|
|
||||||
inline-style-parser "0.1.1"
|
|
||||||
|
|
||||||
styled-components@^5.1.0:
|
styled-components@^5.1.0:
|
||||||
version "5.1.1"
|
version "5.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.1.1.tgz#96dfb02a8025794960863b9e8e365e3b6be5518d"
|
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.1.1.tgz#96dfb02a8025794960863b9e8e365e3b6be5518d"
|
||||||
@ -11045,7 +10596,7 @@ tiny-invariant@^1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
|
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
|
||||||
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
|
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
|
||||||
|
|
||||||
tiny-warning@^1.0.0:
|
tiny-warning@^1.0.0, tiny-warning@^1.0.3:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
||||||
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
||||||
@ -11140,11 +10691,6 @@ tree-changes@^0.9.0:
|
|||||||
"@gilbarbara/deep-equal" "^0.1.0"
|
"@gilbarbara/deep-equal" "^0.1.0"
|
||||||
is-lite "^0.8.1"
|
is-lite "^0.8.1"
|
||||||
|
|
||||||
trough@^2.0.0:
|
|
||||||
version "2.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876"
|
|
||||||
integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==
|
|
||||||
|
|
||||||
ts-pnp@1.1.6:
|
ts-pnp@1.1.6:
|
||||||
version "1.1.6"
|
version "1.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.1.6.tgz#389a24396d425a0d3162e96d2b4638900fdc289a"
|
resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.1.6.tgz#389a24396d425a0d3162e96d2b4638900fdc289a"
|
||||||
@ -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"
|
resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4"
|
||||||
integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==
|
integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==
|
||||||
|
|
||||||
unified@^10.0.0:
|
|
||||||
version "10.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df"
|
|
||||||
integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==
|
|
||||||
dependencies:
|
|
||||||
"@types/unist" "^2.0.0"
|
|
||||||
bail "^2.0.0"
|
|
||||||
extend "^3.0.0"
|
|
||||||
is-buffer "^2.0.0"
|
|
||||||
is-plain-obj "^4.0.0"
|
|
||||||
trough "^2.0.0"
|
|
||||||
vfile "^5.0.0"
|
|
||||||
|
|
||||||
union-value@^1.0.0:
|
union-value@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
|
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
|
||||||
@ -11304,71 +10837,6 @@ unique-slug@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
imurmurhash "^0.1.4"
|
imurmurhash "^0.1.4"
|
||||||
|
|
||||||
unist-builder@^3.0.0:
|
|
||||||
version "3.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-3.0.0.tgz#728baca4767c0e784e1e64bb44b5a5a753021a04"
|
|
||||||
integrity sha512-GFxmfEAa0vi9i5sd0R2kcrI9ks0r82NasRq5QHh2ysGngrc6GiqD5CDf1FjPenY4vApmFASBIIlk/jj5J5YbmQ==
|
|
||||||
dependencies:
|
|
||||||
"@types/unist" "^2.0.0"
|
|
||||||
|
|
||||||
unist-util-generated@^2.0.0:
|
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.0.tgz#86fafb77eb6ce9bfa6b663c3f5ad4f8e56a60113"
|
|
||||||
integrity sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw==
|
|
||||||
|
|
||||||
unist-util-is@^5.0.0:
|
|
||||||
version "5.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.1.1.tgz#e8aece0b102fa9bc097b0fef8f870c496d4a6236"
|
|
||||||
integrity sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ==
|
|
||||||
|
|
||||||
unist-util-position@^4.0.0:
|
|
||||||
version "4.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-4.0.3.tgz#5290547b014f6222dff95c48d5c3c13a88fadd07"
|
|
||||||
integrity sha512-p/5EMGIa1qwbXjA+QgcBXaPWjSnZfQ2Sc3yBEEfgPwsEmJd8Qh+DSk3LGnmOM4S1bY2C0AjmMnB8RuEYxpPwXQ==
|
|
||||||
dependencies:
|
|
||||||
"@types/unist" "^2.0.0"
|
|
||||||
|
|
||||||
unist-util-stringify-position@^3.0.0:
|
|
||||||
version "3.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.2.tgz#5c6aa07c90b1deffd9153be170dce628a869a447"
|
|
||||||
integrity sha512-7A6eiDCs9UtjcwZOcCpM4aPII3bAAGv13E96IkawkOAW0OhH+yRxtY0lzo8KiHpzEMfH7Q+FizUmwp8Iqy5EWg==
|
|
||||||
dependencies:
|
|
||||||
"@types/unist" "^2.0.0"
|
|
||||||
|
|
||||||
unist-util-visit-parents@^4.0.0:
|
|
||||||
version "4.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz#e83559a4ad7e6048a46b1bdb22614f2f3f4724f2"
|
|
||||||
integrity sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==
|
|
||||||
dependencies:
|
|
||||||
"@types/unist" "^2.0.0"
|
|
||||||
unist-util-is "^5.0.0"
|
|
||||||
|
|
||||||
unist-util-visit-parents@^5.0.0:
|
|
||||||
version "5.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.0.tgz#44bbc5d25f2411e7dfc5cecff12de43296aa8521"
|
|
||||||
integrity sha512-y+QVLcY5eR/YVpqDsLf/xh9R3Q2Y4HxkZTp7ViLDU6WtJCEcPmRzW1gpdWDCDIqIlhuPDXOgttqPlykrHYDekg==
|
|
||||||
dependencies:
|
|
||||||
"@types/unist" "^2.0.0"
|
|
||||||
unist-util-is "^5.0.0"
|
|
||||||
|
|
||||||
unist-util-visit@^3.0.0:
|
|
||||||
version "3.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-3.1.0.tgz#9420d285e1aee938c7d9acbafc8e160186dbaf7b"
|
|
||||||
integrity sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==
|
|
||||||
dependencies:
|
|
||||||
"@types/unist" "^2.0.0"
|
|
||||||
unist-util-is "^5.0.0"
|
|
||||||
unist-util-visit-parents "^4.0.0"
|
|
||||||
|
|
||||||
unist-util-visit@^4.0.0:
|
|
||||||
version "4.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.0.tgz#f41e407a9e94da31594e6b1c9811c51ab0b3d8f5"
|
|
||||||
integrity sha512-n7lyhFKJfVZ9MnKtqbsqkQEk5P1KShj0+//V7mAcoI6bpbUjh3C/OG8HVD+pBihfh6Ovl01m8dkcv9HNqYajmQ==
|
|
||||||
dependencies:
|
|
||||||
"@types/unist" "^2.0.0"
|
|
||||||
unist-util-is "^5.0.0"
|
|
||||||
unist-util-visit-parents "^5.0.0"
|
|
||||||
|
|
||||||
universalify@^0.1.0:
|
universalify@^0.1.0:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||||
@ -11434,11 +10902,6 @@ url@^0.11.0:
|
|||||||
punycode "1.3.2"
|
punycode "1.3.2"
|
||||||
querystring "0.2.0"
|
querystring "0.2.0"
|
||||||
|
|
||||||
use-sync-external-store@^1.0.0:
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz#3343c3fe7f7e404db70f8c687adf5c1652d34e82"
|
|
||||||
integrity sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ==
|
|
||||||
|
|
||||||
use@^3.1.0:
|
use@^3.1.0:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
||||||
@ -11496,16 +10959,6 @@ uuid@^3.0.1, uuid@^3.3.2:
|
|||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||||
|
|
||||||
uvu@^0.5.0:
|
|
||||||
version "0.5.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.3.tgz#3d83c5bc1230f153451877bfc7f4aea2392219ae"
|
|
||||||
integrity sha512-brFwqA3FXzilmtnIyJ+CxdkInkY/i4ErvP7uV0DnUVxQcQ55reuHphorpF+tZoVHK2MniZ/VJzI7zJQoc9T9Yw==
|
|
||||||
dependencies:
|
|
||||||
dequal "^2.0.0"
|
|
||||||
diff "^5.0.0"
|
|
||||||
kleur "^4.0.3"
|
|
||||||
sade "^1.7.3"
|
|
||||||
|
|
||||||
v8-compile-cache@^2.0.3:
|
v8-compile-cache@^2.0.3:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745"
|
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745"
|
||||||
@ -11543,24 +10996,6 @@ verror@1.10.0:
|
|||||||
core-util-is "1.0.2"
|
core-util-is "1.0.2"
|
||||||
extsprintf "^1.2.0"
|
extsprintf "^1.2.0"
|
||||||
|
|
||||||
vfile-message@^3.0.0:
|
|
||||||
version "3.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.2.tgz#a2908f64d9e557315ec9d7ea3a910f658ac05f7d"
|
|
||||||
integrity sha512-QjSNP6Yxzyycd4SVOtmKKyTsSvClqBPJcd00Z0zuPj3hOIjg0rUPG6DbFGPvUKRgYyaIWLPKpuEclcuvb3H8qA==
|
|
||||||
dependencies:
|
|
||||||
"@types/unist" "^2.0.0"
|
|
||||||
unist-util-stringify-position "^3.0.0"
|
|
||||||
|
|
||||||
vfile@^5.0.0:
|
|
||||||
version "5.3.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.2.tgz#b499fbc50197ea50ad3749e9b60beb16ca5b7c54"
|
|
||||||
integrity sha512-w0PLIugRY3Crkgw89TeMvHCzqCs/zpreR31hl4D92y6SOE07+bfJe+dK5Q2akwS+i/c801kzjoOr9gMcTe6IAA==
|
|
||||||
dependencies:
|
|
||||||
"@types/unist" "^2.0.0"
|
|
||||||
is-buffer "^2.0.0"
|
|
||||||
unist-util-stringify-position "^3.0.0"
|
|
||||||
vfile-message "^3.0.0"
|
|
||||||
|
|
||||||
vm-browserify@^1.0.1:
|
vm-browserify@^1.0.1:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
|
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
|
||||||
|
Loading…
Reference in New Issue
Block a user