Compare commits
22 Commits
3b691fed81
...
2f92b3b883
Author | SHA1 | Date | |
---|---|---|---|
2f92b3b883 | |||
e51f3af984 | |||
1e85112f6d | |||
c3598dfa5c | |||
3aa70c48e7 | |||
c368e675b4 | |||
68351f84e3 | |||
e46d1c63a7 | |||
29aba03ba8 | |||
e387c0e05a | |||
164ca73913 | |||
d70858ffb7 | |||
073335a322 | |||
450efe8b36 | |||
ba1ba5ed94 | |||
80cf4d3b4c | |||
79299ab978 | |||
46e2f76778 | |||
a00de971ae | |||
8eb4eb4bd9 | |||
78c3908bf9 | |||
c394de8286 |
@ -53,6 +53,7 @@ def copy_to_project(user, id, project_id):
|
|||||||
obj['project'] = target_project['_id']
|
obj['project'] = target_project['_id']
|
||||||
obj['createdAt'] = datetime.datetime.now()
|
obj['createdAt'] = datetime.datetime.now()
|
||||||
obj['commentCount'] = 0
|
obj['commentCount'] = 0
|
||||||
|
if 'preview' in obj: del obj['preview']
|
||||||
db.objects.insert_one(obj)
|
db.objects.insert_one(obj)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
BIN
web/.yarn/cache/@remix-run-router-npm-1.14.1-a13db4ccdf-a3a0e7bd19.zip
vendored
Normal file
BIN
web/.yarn/cache/@remix-run-router-npm-1.14.1-a13db4ccdf-a3a0e7bd19.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/react-router-dom-npm-6.21.1-e60eb3f846-d8ea3370ba.zip
vendored
Normal file
BIN
web/.yarn/cache/react-router-dom-npm-6.21.1-e60eb3f846-d8ea3370ba.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/react-router-npm-6.21.1-4cd474a63c-c6774cf444.zip
vendored
Normal file
BIN
web/.yarn/cache/react-router-npm-6.21.1-4cd474a63c-c6774cf444.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -23,7 +23,7 @@
|
|||||||
"react-joyride": "^2.4.0",
|
"react-joyride": "^2.4.0",
|
||||||
"react-redux": "^8.0.1",
|
"react-redux": "^8.0.1",
|
||||||
"react-rewards": "^2.0.3",
|
"react-rewards": "^2.0.3",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.21.1",
|
||||||
"react-toastify": "^4.4.0",
|
"react-toastify": "^4.4.0",
|
||||||
"redux": "^4.0.0",
|
"redux": "^4.0.0",
|
||||||
"sanitize-html": "^1.19.1",
|
"sanitize-html": "^1.19.1",
|
||||||
|
@ -3,8 +3,6 @@ import { store } from '../index';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
INIT_DRIFT: 'INIT_DRIFT',
|
|
||||||
SYNC_DRIFT: 'SYNC_DRIFT',
|
|
||||||
REQUEST_USERS: 'REQUEST_USERS',
|
REQUEST_USERS: 'REQUEST_USERS',
|
||||||
REQUEST_FAILED: 'REQUEST_FAILED',
|
REQUEST_FAILED: 'REQUEST_FAILED',
|
||||||
RECEIVE_USER: 'RECEIVE_USERS',
|
RECEIVE_USER: 'RECEIVE_USERS',
|
||||||
@ -15,14 +13,6 @@ export default {
|
|||||||
LEAVE_GROUP: 'LEAVE_GROUP',
|
LEAVE_GROUP: 'LEAVE_GROUP',
|
||||||
UPDATE_SUBSCRIPTIONS: 'UPDATE_SUBSCRIPTIONS',
|
UPDATE_SUBSCRIPTIONS: 'UPDATE_SUBSCRIPTIONS',
|
||||||
|
|
||||||
initDrift() {
|
|
||||||
return { type: this.INIT_DRIFT };
|
|
||||||
},
|
|
||||||
|
|
||||||
syncDrift(synced) {
|
|
||||||
return { type: this.SYNC_DRIFT, synced };
|
|
||||||
},
|
|
||||||
|
|
||||||
request() {
|
request() {
|
||||||
return { type: this.REQUEST_USERS };
|
return { type: this.REQUEST_USERS };
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { ToastContainer, toast } from 'react-toastify';
|
import { ToastContainer, toast } from 'react-toastify';
|
||||||
@ -11,47 +11,7 @@ import actions from '../actions';
|
|||||||
import utils from '../utils/utils.js';
|
import utils from '../utils/utils.js';
|
||||||
import NavBar from './includes/NavBar';
|
import NavBar from './includes/NavBar';
|
||||||
import Footer from './includes/Footer';
|
import Footer from './includes/Footer';
|
||||||
|
|
||||||
import MarketingHome from './marketing/Home';
|
|
||||||
import PrivacyPolicy from './marketing/PrivacyPolicy';
|
|
||||||
import TermsOfUse from './marketing/TermsOfUse';
|
|
||||||
|
|
||||||
import Login from './Login';
|
import Login from './Login';
|
||||||
import ForgottenPassword from './ForgottenPassword';
|
|
||||||
import ResetPassword from './ResetPassword';
|
|
||||||
|
|
||||||
import Home from './main/Home';
|
|
||||||
|
|
||||||
import Profile from './main/users/Profile';
|
|
||||||
import ProfileEdit from './main/users/EditProfile';
|
|
||||||
import ProfileProjects from './main/users/ProfileProjects';
|
|
||||||
|
|
||||||
import NewProject from './main/projects/New';
|
|
||||||
import Project from './main/projects/Project';
|
|
||||||
import ProjectObjects from './main/projects/ProjectObjects';
|
|
||||||
import ProjectSettings from './main/projects/Settings';
|
|
||||||
import ObjectDraft from './main/projects/objects/Draft';
|
|
||||||
import ObjectList from './main/projects/ObjectList';
|
|
||||||
|
|
||||||
import Settings from './main/settings/Settings';
|
|
||||||
import SettingsIdentity from './main/settings/Identity';
|
|
||||||
import SettingsNotification from './main/settings/Notification';
|
|
||||||
import SettingsAccount from './main/settings/Account';
|
|
||||||
|
|
||||||
import NewGroup from './main/groups/New';
|
|
||||||
import Group from './main/groups/Group';
|
|
||||||
import GroupFeed from './main/groups/Feed';
|
|
||||||
import GroupMembers from './main/groups/Members';
|
|
||||||
import GroupProjects from './main/groups/Projects';
|
|
||||||
import GroupSettings from './main/groups/Settings';
|
|
||||||
|
|
||||||
import Explore from './main/explore/Explore'
|
|
||||||
|
|
||||||
import Docs from './docs';
|
|
||||||
import DocsHome from './docs/Home';
|
|
||||||
import DocsDoc from './docs/Doc';
|
|
||||||
|
|
||||||
import Root from './main/root';
|
|
||||||
|
|
||||||
const StyledContent = styled.div`
|
const StyledContent = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -67,11 +27,10 @@ const GlobalStyle = createGlobalStyle`
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { isAuthenticated, isAuthenticating, isAuthenticatingType, user, driftReady, syncedToDrift } = useSelector(state => {
|
const { isAuthenticated, isAuthenticating, isAuthenticatingType, user } = useSelector(state => {
|
||||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
||||||
const { isAuthenticated, isAuthenticating, isAuthenticatingType } = state.auth;
|
const { isAuthenticated, isAuthenticating, isAuthenticatingType } = state.auth;
|
||||||
const { driftReady, syncedToDrift } = state.users;
|
return { isAuthenticated, isAuthenticating, isAuthenticatingType, user };
|
||||||
return { isAuthenticated, isAuthenticating, isAuthenticatingType, user, driftReady, syncedToDrift };
|
|
||||||
});
|
});
|
||||||
const loggedInUserId = user?._id;
|
const loggedInUserId = user?._id;
|
||||||
|
|
||||||
@ -94,74 +53,17 @@ function App() {
|
|||||||
});
|
});
|
||||||
}, [dispatch, loggedInUserId]);
|
}, [dispatch, loggedInUserId]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.drift && window.drift.on('ready', () => {
|
|
||||||
dispatch(actions.users.initDrift());
|
|
||||||
});
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user && driftReady && !syncedToDrift && window.drift) {
|
|
||||||
window.drift.identify(user._id, {
|
|
||||||
email: user.email,
|
|
||||||
username: user.username,
|
|
||||||
createdAt: user.createdAt,
|
|
||||||
});
|
|
||||||
dispatch(actions.users.syncDrift(null));
|
|
||||||
}
|
|
||||||
}, [dispatch, user, driftReady, syncedToDrift]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContent>
|
<StyledContent>
|
||||||
<GlobalStyle whiteColor />
|
<GlobalStyle whiteColor />
|
||||||
<Helmet defaultTitle={utils.appName()} titleTemplate={`%s | ${utils.appName()}`} />
|
<Helmet defaultTitle={utils.appName()} titleTemplate={`%s | ${utils.appName()}`} />
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<div style={{ flex: '1' }}>
|
<div style={{ flex: '1' }}>
|
||||||
<Routes>
|
<Outlet />
|
||||||
<Route end path="/" element={isAuthenticated
|
</div>
|
||||||
? <Home />
|
|
||||||
: <MarketingHome onRegisterClicked={() => dispatch(actions.auth.openRegister())} />
|
|
||||||
} />
|
|
||||||
<Route path="/privacy" element={<PrivacyPolicy />} />
|
|
||||||
<Route path="/terms-of-use" element={<TermsOfUse />} />
|
|
||||||
<Route path="/password/forgotten" element={<ForgottenPassword />} />
|
|
||||||
<Route path="/password/reset" element={<ResetPassword />} />
|
|
||||||
<Route path="/settings" element={<Settings />}>
|
|
||||||
<Route path='identity' element={<SettingsIdentity />} />
|
|
||||||
<Route path='notifications' element={<SettingsNotification />} />
|
|
||||||
<Route path='account' element={<SettingsAccount />} />
|
|
||||||
<Route path='' element={<SettingsIdentity />} />
|
|
||||||
</Route>
|
|
||||||
<Route path="/projects/new" element={<NewProject />} />
|
|
||||||
<Route path="/groups/new" element={<NewGroup />} />
|
|
||||||
<Route path="/groups/:id" element={<Group />}>
|
|
||||||
<Route path='feed' element={<GroupFeed />} />
|
|
||||||
<Route path='members' element={<GroupMembers />} />
|
|
||||||
<Route path='projects' element={<GroupProjects />} />
|
|
||||||
<Route path='settings' element={<GroupSettings />} />
|
|
||||||
<Route path='' end element={<GroupFeed />} />
|
|
||||||
</Route>
|
|
||||||
<Route path='/root' element={<Root />} />
|
|
||||||
<Route path='/docs' element={<Docs />}>
|
|
||||||
<Route path=":doc" element={<DocsDoc />} />
|
|
||||||
<Route path='' element={<DocsHome />} />
|
|
||||||
</Route>
|
|
||||||
<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='/explore' element={<Explore />} />
|
|
||||||
<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())} />
|
<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>
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</StyledContent>
|
</StyledContent>
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,7 @@ 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 { useDispatch, useSelector } from 'react-redux';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import actions from '../actions';
|
import actions from '../actions';
|
||||||
import { api } from '../api';
|
import { api } from '../api';
|
||||||
import utils from '../utils/utils.js';
|
import utils from '../utils/utils.js';
|
||||||
@ -16,6 +16,8 @@ function Login({ open, authType, onClose }) {
|
|||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const { error } = useSelector(state => {
|
const { error } = useSelector(state => {
|
||||||
const { loading, error } = state.auth;
|
const { loading, error } = state.auth;
|
||||||
@ -31,6 +33,7 @@ function Login({ open, authType, onClose }) {
|
|||||||
setUsername('');
|
setUsername('');
|
||||||
dispatch(actions.auth.receiveLogin(data));
|
dispatch(actions.auth.receiveLogin(data));
|
||||||
onClose();
|
onClose();
|
||||||
|
if (location.pathname === '/') navigate('/projects');
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
dispatch(actions.auth.loginError(err));
|
dispatch(actions.auth.loginError(err));
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -46,6 +49,7 @@ function Login({ open, authType, onClose }) {
|
|||||||
setUsername('');
|
setUsername('');
|
||||||
dispatch(actions.auth.receiveLogin(data));
|
dispatch(actions.auth.receiveLogin(data));
|
||||||
onClose();
|
onClose();
|
||||||
|
if (location.pathname === '/') navigate('/projects');
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
dispatch(actions.auth.loginError(err));
|
dispatch(actions.auth.loginError(err));
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
24
web/src/components/includes/LoginNeeded.jsx
Normal file
24
web/src/components/includes/LoginNeeded.jsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Container, Segment, Button, Message } from 'semantic-ui-react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import utils from '../../utils/utils.js';
|
||||||
|
import actions from '../../actions';
|
||||||
|
|
||||||
|
export default function LoginNeeded() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container style={{marginTop: 30}}>
|
||||||
|
<Message info>
|
||||||
|
<h3>Welcome to {utils.appName()}</h3>
|
||||||
|
<p>You need to have a {utils.appName()} account in order to access this. Please login or register first.</p>
|
||||||
|
|
||||||
|
<Button.Group style={{marginTop: 30}}>
|
||||||
|
<Button color="white" onClick={() => dispatch(actions.auth.openLogin())}>Login</Button>
|
||||||
|
<Button color="teal" onClick={() => dispatch(actions.auth.openRegister())}>Register</Button>
|
||||||
|
</Button.Group>
|
||||||
|
</Message>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
@ -51,8 +51,6 @@ export default function NavBar() {
|
|||||||
|
|
||||||
const logout = () => api.auth.logout(() => {
|
const logout = () => api.auth.logout(() => {
|
||||||
dispatch(actions.auth.logout());
|
dispatch(actions.auth.logout());
|
||||||
dispatch(actions.users.syncDrift(false))
|
|
||||||
if (window.drift) window.drift.reset();
|
|
||||||
navigate('/');
|
navigate('/');
|
||||||
});
|
});
|
||||||
const isSupporter = user?.isSilverSupporter || user?.isGoldSupporter;
|
const isSupporter = user?.isSilverSupporter || user?.isGoldSupporter;
|
||||||
@ -60,10 +58,10 @@ export default function NavBar() {
|
|||||||
return (
|
return (
|
||||||
<StyledNavBar>
|
<StyledNavBar>
|
||||||
<Container style={{display:'flex', justifyContent: 'space-between', alignItems: 'center'}}>
|
<Container style={{display:'flex', justifyContent: 'space-between', alignItems: 'center'}}>
|
||||||
<Link to="/"><img alt={`${utils.appName()} logo`} src={logo} className="logo" /></Link>
|
<Link to={user ? '/projects' : '/'}><img alt={`${utils.appName()} logo`} src={logo} className="logo" /></Link>
|
||||||
<div style={{flex: 1}}>
|
<div style={{flex: 1}}>
|
||||||
<Menu secondary>
|
<Menu secondary>
|
||||||
<Menu.Item className='above-mobile' as={Link} to='/' name='home' active={location.pathname === '/'} />
|
<Menu.Item className='above-mobile' as={Link} to='/projects' name='projects' active={location.pathname === '/projects'} />
|
||||||
<Menu.Item className='above-mobile' as={Link} to='/explore' name='explore' active={location.pathname === '/explore'} />
|
<Menu.Item className='above-mobile' as={Link} to='/explore' name='explore' active={location.pathname === '/explore'} />
|
||||||
<Menu.Item className='above-mobile' active={location.pathname.startsWith('/groups')} name='Groups'>
|
<Menu.Item className='above-mobile' active={location.pathname.startsWith('/groups')} name='Groups'>
|
||||||
<Dropdown pointing='top left' icon={null}
|
<Dropdown pointing='top left' icon={null}
|
||||||
@ -115,7 +113,7 @@ export default function NavBar() {
|
|||||||
|
|
||||||
<Menu.Menu position='right'>
|
<Menu.Menu position='right'>
|
||||||
{isAuthenticated && <>
|
{isAuthenticated && <>
|
||||||
<Menu.Item className='above-mobile'><SearchBar /></Menu.Item>
|
<Menu.Item className='abovee-mobile'><SearchBar /></Menu.Item>
|
||||||
<Dropdown direction="left" pointing="top right" icon={null} style={{ marginTop: 10}}
|
<Dropdown direction="left" pointing="top right" icon={null} style={{ marginTop: 10}}
|
||||||
trigger={<UserChip user={user} withoutLink avatarOnly />}
|
trigger={<UserChip user={user} withoutLink avatarOnly />}
|
||||||
>
|
>
|
||||||
@ -128,7 +126,8 @@ export default function NavBar() {
|
|||||||
{user?.isGoldSupporter && <Dropdown.Header><SupporterBadge type='gold' /></Dropdown.Header>}
|
{user?.isGoldSupporter && <Dropdown.Header><SupporterBadge type='gold' /></Dropdown.Header>}
|
||||||
{user?.isSilverSupporter && !user?.isGoldSupporter && <Dropdown.Header><SupporterBadge type='silver' /></Dropdown.Header>}
|
{user?.isSilverSupporter && !user?.isGoldSupporter && <Dropdown.Header><SupporterBadge type='silver' /></Dropdown.Header>}
|
||||||
<Dropdown.Divider />
|
<Dropdown.Divider />
|
||||||
<Link to="/" className="item">Projects</Link>
|
<Link to="/projects" className="item">My projects</Link>
|
||||||
|
<Link to="/explore" className="item">Explore</Link>
|
||||||
{user &&<Link to={`/${user.username}`} className="item">Profile</Link>}
|
{user &&<Link to={`/${user.username}`} className="item">Profile</Link>}
|
||||||
<Link to="/settings" className="item">Settings</Link>
|
<Link to="/settings" className="item">Settings</Link>
|
||||||
<Dropdown.Divider />
|
<Dropdown.Divider />
|
||||||
|
@ -9,6 +9,7 @@ import actions from '../../actions';
|
|||||||
import api from '../../api';
|
import api from '../../api';
|
||||||
import utils from '../../utils/utils.js';
|
import utils from '../../utils/utils.js';
|
||||||
|
|
||||||
|
import LoginNeeded from '../includes/LoginNeeded';
|
||||||
import UserChip from '../includes/UserChip';
|
import UserChip from '../includes/UserChip';
|
||||||
import HelpLink from '../includes/HelpLink';
|
import HelpLink from '../includes/HelpLink';
|
||||||
import ProjectCard from '../includes/ProjectCard';
|
import ProjectCard from '../includes/ProjectCard';
|
||||||
@ -131,7 +132,9 @@ function Home() {
|
|||||||
<Grid.Column computer={11} className='joyride-projects'>
|
<Grid.Column computer={11} className='joyride-projects'>
|
||||||
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
|
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
|
||||||
<h2><Icon name='book' /> Your projects</h2>
|
<h2><Icon name='book' /> Your projects</h2>
|
||||||
|
{user &&
|
||||||
<div><Button className='joyride-createProject' as={Link} to="/projects/new" color='teal' content='Create a project' icon='plus' /></div>
|
<div><Button className='joyride-createProject' as={Link} to="/projects/new" color='teal' content='Create a project' icon='plus' /></div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<p>Projects contain the patterns and files that make up your creations.
|
<p>Projects contain the patterns and files that make up your creations.
|
||||||
<HelpLink className='joyride-help' link={`/docs/projects`} text='Learn more about projects' marginLeft/>
|
<HelpLink className='joyride-help' link={`/docs/projects`} text='Learn more about projects' marginLeft/>
|
||||||
@ -139,7 +142,11 @@ function Home() {
|
|||||||
|
|
||||||
<Divider hidden />
|
<Divider hidden />
|
||||||
|
|
||||||
{loadingProjects && !projects?.length &&
|
{!user &&
|
||||||
|
<LoginNeeded />
|
||||||
|
}
|
||||||
|
|
||||||
|
{user && loadingProjects && !projects?.length &&
|
||||||
<Card.Group itemsPerRow={2} stackable>
|
<Card.Group itemsPerRow={2} stackable>
|
||||||
<PatternLoader isCompact count={3} />
|
<PatternLoader isCompact count={3} />
|
||||||
</Card.Group>
|
</Card.Group>
|
||||||
|
@ -9,6 +9,7 @@ import api from '../../../api';
|
|||||||
import utils from '../../../utils/utils.js';
|
import utils from '../../../utils/utils.js';
|
||||||
|
|
||||||
import HelpLink from '../../includes/HelpLink';
|
import HelpLink from '../../includes/HelpLink';
|
||||||
|
import LoginNeeded from '../../includes/LoginNeeded';
|
||||||
|
|
||||||
function NewGroup() {
|
function NewGroup() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -33,12 +34,7 @@ function NewGroup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return (
|
return (<LoginNeeded />);
|
||||||
<Container style={{marginTop: 40}}>
|
|
||||||
<h1>Login required</h1>
|
|
||||||
<p>You need to have a {utils.appName()} account in order to create a group. Please register or login first.</p>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -114,7 +114,7 @@ function ObjectList({ compact }) {
|
|||||||
<div style={{flex: 1, marginLeft: 5}}>
|
<div style={{flex: 1, marginLeft: 5}}>
|
||||||
<h3 style={{fontSize: 13, marginBottom: 0, wordBreak: 'break-all'}}>{object.name}</h3>
|
<h3 style={{fontSize: 13, marginBottom: 0, wordBreak: 'break-all'}}>{object.name}</h3>
|
||||||
<Label size='mini' rounded>
|
<Label size='mini' rounded>
|
||||||
{object.type === 'pattern' && <><Icon name='pencil' /> WIF pattern</>}
|
{object.type === 'pattern' && <><Icon name='pencil' /> Weaving pattern</>}
|
||||||
{object.type === 'file' &&
|
{object.type === 'file' &&
|
||||||
(object.isImage ? <><Icon name='image' /> Image</> : <><Icon name='file outline' /> File</>)
|
(object.isImage ? <><Icon name='image' /> Image</> : <><Icon name='file outline' /> File</>)
|
||||||
}
|
}
|
||||||
|
@ -83,27 +83,6 @@ function ObjectViewer() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloadDrawdownImage = (object) => {
|
|
||||||
const element = document.createElement('a');
|
|
||||||
element.setAttribute('href', object.previewUrl);
|
|
||||||
element.setAttribute('download', `${object.name.replace(/ /g, '_')}-drawdown.png`);
|
|
||||||
element.style.display = 'none';
|
|
||||||
document.body.appendChild(element);
|
|
||||||
element.click();
|
|
||||||
document.body.removeChild(element);
|
|
||||||
toast.info('The file has been downloaded');
|
|
||||||
}
|
|
||||||
const downloadPatternImage = (object) => {
|
|
||||||
const element = document.createElement('a');
|
|
||||||
element.setAttribute('href', object.patternImage);
|
|
||||||
element.setAttribute('download', `${object.name.replace(/ /g, '_')}-pattern.png`);
|
|
||||||
element.style.display = 'none';
|
|
||||||
document.body.appendChild(element);
|
|
||||||
element.click();
|
|
||||||
document.body.removeChild(element);
|
|
||||||
toast.info('The file has been downloaded');
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyPattern = (project) => {
|
const copyPattern = (project) => {
|
||||||
api.objects.copyTo(object._id, project._id, newObject => {
|
api.objects.copyTo(object._id, project._id, newObject => {
|
||||||
window.location = `/${project.fullName}/${newObject._id}`;
|
window.location = `/${project.fullName}/${newObject._id}`;
|
||||||
@ -125,15 +104,15 @@ function ObjectViewer() {
|
|||||||
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'end' }}>
|
<div style={{ display: 'flex', justifyContent: 'end' }}>
|
||||||
{object.type === 'pattern' && (utils.canEditProject(user, project) || project.openSource || object.previewUrl) && <>
|
{object.type === 'pattern' && (utils.canEditProject(user, project) || project.openSource || object.previewUrl) && <>
|
||||||
<Dropdown direction='left' icon={null} trigger={<Button size='small' secondary icon='download' content='Download pattern' loading={downloading} disabled={downloading}/>}>
|
<Dropdown direction='left' icon={null} trigger={<Button size='tiny' secondary icon='download' content='Download pattern' loading={downloading} disabled={downloading}/>}>
|
||||||
<Dropdown.Menu>
|
<Dropdown.Menu>
|
||||||
{object.previewUrl &&
|
{object.previewUrl &&
|
||||||
<Dropdown.Item onClick={e => downloadDrawdownImage(object)} content='Download drawdown as an image' icon='file outline' />
|
<Dropdown.Item onClick={e => utils.downloadDrawdownImage(object)} content='Download drawdown as an image' icon='file outline' />
|
||||||
}
|
}
|
||||||
{(utils.canEditProject(user, project) || project.openSource) &&
|
{(utils.canEditProject(user, project) || project.openSource) &&
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{object.patternImage &&
|
{object.patternImage &&
|
||||||
<Dropdown.Item icon='file outline' content='Download complete pattern as an image' onClick={e => downloadPatternImage(object)}/>
|
<Dropdown.Item icon='file outline' content='Download complete pattern as an image' onClick={e => utils.downloadPatternImage(object)}/>
|
||||||
}
|
}
|
||||||
<Dropdown.Divider />
|
<Dropdown.Divider />
|
||||||
<Dropdown.Item onClick={e => downloadWif(object)} content="Download pattern in WIF format" icon="text file" />
|
<Dropdown.Item onClick={e => downloadWif(object)} content="Download pattern in WIF format" icon="text file" />
|
||||||
@ -143,7 +122,7 @@ function ObjectViewer() {
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
{user &&
|
{user &&
|
||||||
<Dropdown direction='left' icon={null} trigger={<Button size="small" icon="copy" secondary content="Copy to.." />}>
|
<Dropdown direction='left' icon={null} trigger={<Button size="tiny" icon="copy" secondary content="Copy to.." />}>
|
||||||
<Dropdown.Menu>
|
<Dropdown.Menu>
|
||||||
<Dropdown.Header>Select a project to copy this pattern to</Dropdown.Header>
|
<Dropdown.Header>Select a project to copy this pattern to</Dropdown.Header>
|
||||||
{myProjects?.map(myProject => <Dropdown.Item content={myProject.name} onClick={e => copyPattern(myProject)} />)}
|
{myProjects?.map(myProject => <Dropdown.Item content={myProject.name} onClick={e => copyPattern(myProject)} />)}
|
||||||
@ -156,15 +135,17 @@ function ObjectViewer() {
|
|||||||
{utils.canEditProject(user, project) &&
|
{utils.canEditProject(user, project) &&
|
||||||
<>
|
<>
|
||||||
{object.type === 'pattern'
|
{object.type === 'pattern'
|
||||||
&& <Button size="small" icon="pencil" primary content="Edit pattern" as={Link} to={`/${fullProjectPath}/${object._id}/edit`} />
|
&& <Button size="tiny" icon="pencil" primary content="Edit pattern" as={Link} to={`/${fullProjectPath}/${object._id}/edit`} />
|
||||||
}
|
}
|
||||||
<Dropdown
|
<Dropdown
|
||||||
icon={null}
|
icon={null}
|
||||||
direction='left'
|
direction='left'
|
||||||
trigger={<Button size="small" icon="cogs" content="Options" />}
|
trigger={<Button size="tiny" icon="cogs" content="Options" />}
|
||||||
>
|
>
|
||||||
<Dropdown.Menu>
|
<Dropdown.Menu>
|
||||||
<Dropdown.Item onClick={e => regeneratePreview(object)} content="Regenerate preview" icon="refresh" />
|
{object.type === 'pattern'
|
||||||
|
&& <Dropdown.Item onClick={e => regeneratePreview(object)} content="Regenerate preview" icon="refresh" />
|
||||||
|
}
|
||||||
<Dropdown.Item onClick={e => deleteObject(object)} content="Delete" icon="trash" />
|
<Dropdown.Item onClick={e => deleteObject(object)} content="Delete" icon="trash" />
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
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, Breadcrumb } from 'semantic-ui-react';
|
||||||
import { Outlet, Link, useParams, useLocation } from 'react-router-dom';
|
import { Outlet, Link, useParams, useLocation } from 'react-router-dom';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import utils from '../../../utils/utils.js';
|
import utils from '../../../utils/utils.js';
|
||||||
@ -14,13 +14,14 @@ import FormattedMessage from '../../includes/FormattedMessage';
|
|||||||
|
|
||||||
function Project() {
|
function Project() {
|
||||||
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
|
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
|
||||||
const { username, projectPath } = useParams();
|
const { username, projectPath, objectId } = useParams();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { user, project, fullName, errorMessage, editingDescription } = useSelector(state => {
|
const { user, project, fullName, errorMessage, editingDescription, object } = useSelector(state => {
|
||||||
const project = state.projects.projects.filter(p => p.path === projectPath && p.owner && p.owner.username === username)[0];
|
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 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 };
|
const object = state.objects.objects.filter(o => o._id === objectId)[0];
|
||||||
|
return { user, project, object, fullName: `${username}/${projectPath}`, errorMessage: state.projects.errorMessage, editingDescription: state.projects.editingDescription };
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -55,16 +56,27 @@ function Project() {
|
|||||||
&& (
|
&& (
|
||||||
<div>
|
<div>
|
||||||
{wideBody() && project.owner &&
|
{wideBody() && project.owner &&
|
||||||
<>
|
<div style={{display: 'flex', alignItems: 'center'}}>
|
||||||
<h3 style={{ marginBottom: 0 }}>
|
|
||||||
{project.name}
|
|
||||||
<Button as={Link} size='tiny' style={{marginLeft: 10}} to={`/${project.fullName}`} basic secondary icon='arrow left' content='Back to project' />
|
|
||||||
</h3>
|
|
||||||
<UserChip user={project.owner} />
|
<UserChip user={project.owner} />
|
||||||
</>
|
<Breadcrumb size='large' style={{marginLeft: 20}}>
|
||||||
|
<Breadcrumb.Section as={Link} to={`/${project.fullName}`}>{project.name}</Breadcrumb.Section>
|
||||||
|
{location.pathname === `/${project.fullName}/settings` && <>
|
||||||
|
<Breadcrumb.Divider icon='right chevron' />
|
||||||
|
<Breadcrumb.Section>Settings</Breadcrumb.Section>
|
||||||
|
</>}
|
||||||
|
{object && <>
|
||||||
|
<Breadcrumb.Divider icon='right chevron' />
|
||||||
|
<Breadcrumb.Section as={Link} to={`/${project.fullName}/${object._id}`}>{object.name}</Breadcrumb.Section>
|
||||||
|
{location.pathname === `/${project.fullName}/${object._id}/edit` && <>
|
||||||
|
<Breadcrumb.Divider icon='right chevron' />
|
||||||
|
<Breadcrumb.Section>Edit</Breadcrumb.Section>
|
||||||
|
</>}
|
||||||
|
</>}
|
||||||
|
</Breadcrumb>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<Grid stackable style={{marginTop: 30}}>
|
<Grid stackable style={{marginTop: 10}}>
|
||||||
{ !wideBody() && (
|
{ !wideBody() && (
|
||||||
<Grid.Column computer={4} tablet={6}>
|
<Grid.Column computer={4} tablet={6}>
|
||||||
<Card fluid raised>
|
<Card fluid raised>
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Modal, Button } from 'semantic-ui-react';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { useBlocker } from 'react-router';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import Tour from '../../../includes/Tour';
|
||||||
import ElementPan from '../../../includes/ElementPan';
|
import ElementPan from '../../../includes/ElementPan';
|
||||||
import HelpLink from '../../../includes/HelpLink';
|
|
||||||
import Tour, { ReRunTour } from '../../../includes/Tour';
|
|
||||||
import util from '../../../../utils/utils.js';
|
import util from '../../../../utils/utils.js';
|
||||||
|
|
||||||
import Warp from './Warp';
|
import Warp from './Warp';
|
||||||
@ -39,11 +40,18 @@ function Draft() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.objects.get(objectId, (o) => {
|
api.objects.get(objectId, (o) => {
|
||||||
if (!o.pattern.baseSize) o.pattern.baseSize = 10;
|
if (!o.pattern.baseSize) o.pattern.baseSize = 10;
|
||||||
|
dispatch(actions.objects.receive(o));
|
||||||
setObject(o);
|
setObject(o);
|
||||||
setPattern(o.pattern);
|
setPattern(o.pattern);
|
||||||
|
setTimeout(() => generatePreviews(o), 1000);
|
||||||
});
|
});
|
||||||
}, [objectId]);
|
}, [objectId]);
|
||||||
|
|
||||||
|
const blocker = useBlocker( ({ currentLocation, nextLocation }) =>
|
||||||
|
unsaved &&
|
||||||
|
currentLocation.pathname !== nextLocation.pathname
|
||||||
|
);
|
||||||
|
|
||||||
const updateObject = (update) => {
|
const updateObject = (update) => {
|
||||||
setObject(Object.assign({}, object, update));
|
setObject(Object.assign({}, object, update));
|
||||||
setUnsaved(true);
|
setUnsaved(true);
|
||||||
@ -55,13 +63,20 @@ function Draft() {
|
|||||||
setUnsaved(true);
|
setUnsaved(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generatePreviews = (o) => {
|
||||||
|
util.generatePatternPreview(o, previewUrl => {
|
||||||
|
util.generateCompletePattern(o?.pattern, ``, patternImage => {
|
||||||
|
setObject(Object.assign({}, o, { previewUrl, patternImage }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const saveObject = () => {
|
const saveObject = () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
util.generatePatternPreview(object, previewUrl => {
|
generatePreviews();
|
||||||
dispatch(actions.objects.update(objectId, 'previewUrl', previewUrl));
|
|
||||||
});
|
|
||||||
const newObject = Object.assign({}, object);
|
const newObject = Object.assign({}, object);
|
||||||
newObject.pattern = pattern;
|
newObject.pattern = pattern;
|
||||||
|
generatePreviews(newObject);
|
||||||
api.objects.update(objectId, newObject, (o) => {
|
api.objects.update(objectId, newObject, (o) => {
|
||||||
toast.success('Pattern saved');
|
toast.success('Pattern saved');
|
||||||
dispatch(actions.objects.receive(o));
|
dispatch(actions.objects.receive(o));
|
||||||
@ -77,12 +92,12 @@ function Draft() {
|
|||||||
const { warp, weft, tieups, baseSize } = pattern;
|
const { warp, weft, tieups, baseSize } = 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 style={{position: 'relative'}}>
|
||||||
<Helmet title={`${name || 'Weaving Draft'}`} />
|
<Helmet title={`${name || 'Weaving Draft'}`} />
|
||||||
<Tour id='pattern' run={true} />
|
|
||||||
<div style={{display: 'flex'}}>
|
|
||||||
|
|
||||||
<div style={{flex: 1, overflow: 'hidden'}}>
|
<Tools warp={warp} weft={weft} object={object} pattern={pattern} updateObject={updateObject} updatePattern={updatePattern} saveObject={saveObject} baseSize={baseSize} unsaved={unsaved} saving={saving}/>
|
||||||
|
|
||||||
|
<div style={{overflow: 'hidden', zIndex: 10}}>
|
||||||
<ElementPan
|
<ElementPan
|
||||||
disabled={!(editor?.tool === 'pan')}
|
disabled={!(editor?.tool === 'pan')}
|
||||||
startX={5000}
|
startX={5000}
|
||||||
@ -91,10 +106,9 @@ function Draft() {
|
|||||||
<StyledPattern
|
<StyledPattern
|
||||||
style={{
|
style={{
|
||||||
width: `${warp.threading?.length * baseSize + weft.treadles * baseSize + 20}px`,
|
width: `${warp.threading?.length * baseSize + weft.treadles * baseSize + 20}px`,
|
||||||
height: '1000px', // `${warp.shafts * baseSize + weft.threads * baseSize + 20}px`
|
height: `${warp.shafts * baseSize + weft.treadling?.length * baseSize + 20}px`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
||||||
<Warp baseSize={baseSize} cellStyle={cellStyle} warp={warp} weft={weft} updatePattern={updatePattern} />
|
<Warp baseSize={baseSize} cellStyle={cellStyle} warp={warp} weft={weft} updatePattern={updatePattern} />
|
||||||
<Weft cellStyle={cellStyle} warp={warp} weft={weft} baseSize={baseSize} updatePattern={updatePattern} />
|
<Weft cellStyle={cellStyle} warp={warp} weft={weft} baseSize={baseSize} updatePattern={updatePattern} />
|
||||||
<Tieups cellStyle={cellStyle} warp={warp} weft={weft} tieups={tieups} updatePattern={updatePattern} baseSize={baseSize}/>
|
<Tieups cellStyle={cellStyle} warp={warp} weft={weft} tieups={tieups} updatePattern={updatePattern} baseSize={baseSize}/>
|
||||||
@ -104,13 +118,20 @@ function Draft() {
|
|||||||
</ElementPan>
|
</ElementPan>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{width: 300, marginLeft: 20}}>
|
<Modal open={blocker.state === "blocked"} basic size="small">
|
||||||
<HelpLink className='joyride-help' link={`/docs/patterns#using-the-pattern-editor`} marginBottom/>
|
<Modal.Header content='Your pattern has not been saved' />
|
||||||
<ReRunTour id='pattern' />
|
<Modal.Content>
|
||||||
<Tools warp={warp} weft={weft} object={object} pattern={pattern} updateObject={updateObject} updatePattern={updatePattern} saveObject={saveObject} baseSize={baseSize} unsaved={unsaved} saving={saving}/>
|
Would you like to save your draft before leaving?
|
||||||
</div>
|
</Modal.Content>
|
||||||
|
<Modal.Actions>
|
||||||
</div>
|
<Button basic color="red" inverted onClick={() => blocker.proceed()}>
|
||||||
|
Leave without saving
|
||||||
|
</Button>
|
||||||
|
<Button color="green" inverted onClick={() => blocker.reset()}>
|
||||||
|
Return to editor
|
||||||
|
</Button>
|
||||||
|
</Modal.Actions>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -25,54 +25,24 @@ function DraftPreview({ object }) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
if (o.pattern && o.pattern.warp) {
|
if (o.pattern && o.pattern.warp) {
|
||||||
setPattern(o.pattern);
|
setPattern(o.pattern);
|
||||||
|
|
||||||
|
// Generate images
|
||||||
|
setTimeout(() => {
|
||||||
// Generate the preview if not yet set (e.g. if from uploaded WIF)
|
// Generate the preview if not yet set (e.g. if from uploaded WIF)
|
||||||
if (!o.previewUrl) {
|
if (!o.previewUrl) {
|
||||||
setTimeout(() => {
|
|
||||||
util.generatePatternPreview(object, previewUrl => {
|
util.generatePatternPreview(object, previewUrl => {
|
||||||
dispatch(actions.objects.update(objectId, 'previewUrl', previewUrl));
|
dispatch(actions.objects.update(objectId, 'previewUrl', previewUrl));
|
||||||
});
|
});
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
|
// Generate the entire pattern and store in memory
|
||||||
|
util.generateCompletePattern(o.pattern, `.preview-${objectId}`, image => {
|
||||||
|
dispatch(actions.objects.update(objectId, 'patternImage', image));
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
}
|
}
|
||||||
}, err => setLoading(false));
|
}, err => setLoading(false));
|
||||||
}, [dispatch, objectId]);
|
}, [dispatch, objectId]);
|
||||||
|
|
||||||
const unifyCanvas = useCallback(() => {
|
|
||||||
if (!pattern) return;
|
|
||||||
const { warp, weft } = pattern;
|
|
||||||
setTimeout(() => {
|
|
||||||
const baseSize = 6;
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
canvas.width = warp.threading?.length * baseSize + weft.treadles * baseSize + 20;
|
|
||||||
canvas.height = warp.shafts * baseSize + weft.treadling?.length * baseSize + 20;
|
|
||||||
ctx.fillStyle = 'white';
|
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
||||||
ctx.fillStyle = 'black';
|
|
||||||
const warpCanvas = document.querySelector(`.preview-${objectId} .warp-threads`);
|
|
||||||
const warpColourwayCanvas = document.querySelector(`.preview-${objectId} .warp-colourway`);
|
|
||||||
const weftCanvas = document.querySelector(`.preview-${objectId} .weft-threads`);
|
|
||||||
const weftColourwayCanvas = document.querySelector(`.preview-${objectId} .weft-colourway`);
|
|
||||||
const drawdownCanvas = document.querySelector(`.preview-${objectId} .drawdown`);
|
|
||||||
const tieupsCanvas = document.querySelector(`.preview-${objectId} .tieups`);
|
|
||||||
if (warpCanvas) {
|
|
||||||
ctx.drawImage(warpColourwayCanvas, canvas.width - warpCanvas.width - weft.treadles * baseSize - 20, 0);
|
|
||||||
ctx.drawImage(warpCanvas, canvas.width - warpCanvas.width - weft.treadles * baseSize - 20, 10);
|
|
||||||
ctx.drawImage(weftCanvas, canvas.width - 10 - weft.treadles * baseSize, 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(drawdownCanvas, canvas.width - drawdownCanvas.width - weft.treadles * baseSize - 20, warp.shafts * baseSize + 20);
|
|
||||||
setTimeout(() => {
|
|
||||||
const im = canvas.toDataURL('image/png')
|
|
||||||
if (im?.length > 20)
|
|
||||||
dispatch(actions.objects.update(objectId, 'patternImage', im));
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}, [dispatch, objectId, pattern]);
|
|
||||||
|
|
||||||
useEffect(() => unifyCanvas(), [unifyCanvas]);
|
|
||||||
|
|
||||||
if (loading) return <Loader active />;
|
if (loading) return <Loader active />;
|
||||||
if (!pattern) return null;
|
if (!pattern) return null;
|
||||||
const { warp, weft, tieups } = pattern;
|
const { warp, weft, tieups } = pattern;
|
||||||
|
@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import utils from '../../../../utils/utils';
|
import utils from '../../../../utils/utils';
|
||||||
|
|
||||||
const StyledDrawdown = styled.canvas`
|
const DrawdownCanvas = styled.canvas`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border: 1px dashed rgb(70,70,70);
|
border: 1px dashed rgb(70,70,70);
|
||||||
top: ${props => (props.warp.shafts * props.baseSize) + 20}px;
|
top: ${props => (props.warp.shafts * props.baseSize) + 20}px;
|
||||||
@ -17,6 +17,7 @@ function Drawdown({ baseSize, warp, weft, tieups }) {
|
|||||||
const drawdownRef = useRef();
|
const drawdownRef = useRef();
|
||||||
useEffect(() => paintDrawdown());
|
useEffect(() => paintDrawdown());
|
||||||
const { editor } = useSelector(state => ({ editor: state.objects.editor }));
|
const { editor } = useSelector(state => ({ editor: state.objects.editor }));
|
||||||
|
const { viewingBack } = editor;
|
||||||
|
|
||||||
const getSquare = (thread, size, colour) => {
|
const getSquare = (thread, size, colour) => {
|
||||||
const { view } = editor;
|
const { view } = editor;
|
||||||
@ -75,7 +76,11 @@ function Drawdown({ baseSize, warp, weft, tieups }) {
|
|||||||
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';
|
let threadType = tieup?.filter(t => t <= warp.shafts).indexOf(shaft) > -1 ? 'warp' : 'weft';
|
||||||
|
if (viewingBack) {
|
||||||
|
if (threadType === 'warp') threadType = 'weft';
|
||||||
|
else threadType = 'warp';
|
||||||
|
}
|
||||||
const square = getSquare(threadType, baseSize, threadType === 'warp' ? warpColour : weftColour);
|
const square = getSquare(threadType, baseSize, threadType === 'warp' ? warpColour : weftColour);
|
||||||
|
|
||||||
ctx.drawImage(square, canvas.width - (baseSize * (thread + 1)), tread * baseSize);
|
ctx.drawImage(square, canvas.width - (baseSize * (thread + 1)), tread * baseSize);
|
||||||
@ -87,7 +92,7 @@ function Drawdown({ baseSize, warp, weft, tieups }) {
|
|||||||
const warpThreads = warp.threading?.length || 0;
|
const warpThreads = warp.threading?.length || 0;
|
||||||
const weftThreads = weft.treadling?.length || 0;
|
const weftThreads = weft.treadling?.length || 0;
|
||||||
return (
|
return (
|
||||||
<StyledDrawdown ref={drawdownRef} className="drawdown joyride-drawdown"
|
<DrawdownCanvas ref={drawdownRef} className="drawdown joyride-drawdown"
|
||||||
width={warpThreads * baseSize}
|
width={warpThreads * baseSize}
|
||||||
height={weftThreads * baseSize}
|
height={weftThreads * baseSize}
|
||||||
weft={weft} warp={warp} baseSize={baseSize}
|
weft={weft} warp={warp} baseSize={baseSize}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
const StyledTieups = styled.canvas`
|
const StyledTieups = styled.canvas`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -10,6 +11,7 @@ const StyledTieups = styled.canvas`
|
|||||||
function Tieups({ cellStyle, warp, weft, tieups, updatePattern, baseSize }) {
|
function Tieups({ cellStyle, warp, weft, tieups, updatePattern, baseSize }) {
|
||||||
useEffect(() => paintTieups());
|
useEffect(() => paintTieups());
|
||||||
const tieupRef = useRef(null);
|
const tieupRef = useRef(null);
|
||||||
|
const { editor } = useSelector(state => ({ editor: state.objects.editor }));
|
||||||
|
|
||||||
const fillUpTo = (t, limit) => {
|
const fillUpTo = (t, limit) => {
|
||||||
let i = t.length;
|
let i = t.length;
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Confirm, Header, Select, Segment, Accordion, Grid, Icon, Input, Button, Popup, Checkbox
|
Confirm, Header, Segment, Accordion, Grid, Icon, Input, Button, Popup, Checkbox, Dropdown
|
||||||
} from 'semantic-ui-react';
|
} from 'semantic-ui-react';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams, Link } from 'react-router-dom';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { SketchPicker } from 'react-color';
|
import { SketchPicker } from 'react-color';
|
||||||
import Slider from 'rc-slider';
|
import Slider from 'rc-slider';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import HelpLink from '../../../includes/HelpLink';
|
||||||
|
import Tour, { ReRunTour } from '../../../includes/Tour';
|
||||||
|
|
||||||
import 'rc-slider/assets/index.css';
|
import 'rc-slider/assets/index.css';
|
||||||
|
|
||||||
@ -15,15 +17,22 @@ import utils from '../../../../utils/utils.js';
|
|||||||
import actions from '../../../../actions';
|
import actions from '../../../../actions';
|
||||||
import api from '../../../../api';
|
import api from '../../../../api';
|
||||||
|
|
||||||
|
const VIEWS = [
|
||||||
|
{ key: 1, value: 'interlacement', text: 'Interlacement' },
|
||||||
|
{ key: 2, value: 'colour', text: 'Colour only' },
|
||||||
|
{ key: 3, value: 'warp', text: 'Warp view' },
|
||||||
|
{ key: 4, value: 'weft', text: 'Weft view' },
|
||||||
|
];
|
||||||
|
|
||||||
const ColourSquare = styled.div`
|
const ColourSquare = styled.div`
|
||||||
background-color: ${props => props.colour};
|
background-color: ${props => props.colour};
|
||||||
display:inline-block;
|
display: ${props => props.small ? 'block' : 'inline-block'};
|
||||||
position:relative;
|
position:relative;
|
||||||
box-shadow:0px 0px 3px rgba(0,0,0,0.1);
|
box-shadow:0px 0px 3px rgba(0,0,0,0.1);
|
||||||
border-radius:2px;
|
border-radius:2px;
|
||||||
width:15px;
|
width: ${props => props.small ? '8' : '15'}px;
|
||||||
height:15px;
|
height: ${props => props.small ? '8' : '15'}px;
|
||||||
margin:2px;
|
margin: ${props => props.small ? '8' : '2'}px;
|
||||||
cursor:pointer;
|
cursor:pointer;
|
||||||
top:0px;
|
top:0px;
|
||||||
transition:top 0.1s;
|
transition:top 0.1s;
|
||||||
@ -36,6 +45,11 @@ const ColourSquare = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const ToolLabel = styled.div`
|
||||||
|
font-size: small;
|
||||||
|
font-weight: bold;
|
||||||
|
`;
|
||||||
|
|
||||||
function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updatePattern, updateObject, saveObject }) {
|
function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updatePattern, updateObject, saveObject }) {
|
||||||
const [activeDrawers, setActiveDrawers] = useState(['properties', 'drawing', 'palette']);
|
const [activeDrawers, setActiveDrawers] = useState(['properties', 'drawing', 'palette']);
|
||||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||||
@ -44,13 +58,15 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { objectId, username, projectPath } = useParams();
|
const { objectId, username, projectPath } = useParams();
|
||||||
|
const { colours } = pattern;
|
||||||
|
|
||||||
const { project, editor } = useSelector(state => {
|
const { user, project, editor } = useSelector(state => {
|
||||||
|
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
||||||
let project = {};
|
let project = {};
|
||||||
state.projects.projects.forEach((p) => {
|
state.projects.projects.forEach((p) => {
|
||||||
if (p.path === projectPath && p.owner && p.owner.username === username) project = p;
|
if (p.path === projectPath && p.owner && p.owner.username === username) project = p;
|
||||||
});
|
});
|
||||||
return { project, editor: state.objects.editor };
|
return { user, project, editor: state.objects.editor };
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -62,6 +78,10 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
|
|||||||
setSelectedThreadCount(selected);
|
setSelectedThreadCount(selected);
|
||||||
}, [pattern]);
|
}, [pattern]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (colours?.length && !editor.colour) setColour(colours[colours.length - 1]);
|
||||||
|
}, [colours]);
|
||||||
|
|
||||||
const enableTool = (tool) => {
|
const enableTool = (tool) => {
|
||||||
dispatch(actions.objects.updateEditor({ tool, colour: editor.colour }));
|
dispatch(actions.objects.updateEditor({ tool, colour: editor.colour }));
|
||||||
};
|
};
|
||||||
@ -74,6 +94,10 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
|
|||||||
dispatch(actions.objects.updateEditor({ view }));
|
dispatch(actions.objects.updateEditor({ view }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setEditorViewingBack = (viewingBack) => {
|
||||||
|
dispatch(actions.objects.updateEditor({ viewingBack }));
|
||||||
|
};
|
||||||
|
|
||||||
const setName = (event) => {
|
const setName = (event) => {
|
||||||
updateObject({ name: event.target.value });
|
updateObject({ name: event.target.value });
|
||||||
};
|
};
|
||||||
@ -177,50 +201,91 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pattern-toolbox joyride-tools">
|
<div className="joyride-tools" style={{position: 'sticky', top: 10, zIndex: 20}}>
|
||||||
{selectedThreadCount > 0 &&
|
<Segment>
|
||||||
<Segment attached="top">
|
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
|
||||||
|
{selectedThreadCount > 0 ?
|
||||||
|
<div>
|
||||||
<Header>{selectedThreadCount} threads selected</Header>
|
<Header>{selectedThreadCount} threads selected</Header>
|
||||||
<Button basic onClick={deselectThreads}>De-select all</Button>
|
<Button size='small' basic onClick={deselectThreads}>De-select all</Button>
|
||||||
<Button color='orange' onClick={deleteSelectedThreads}>Delete threads</Button>
|
<Button size='small' color='orange' onClick={deleteSelectedThreads}>Delete threads</Button>
|
||||||
</Segment>
|
</div>
|
||||||
}
|
:
|
||||||
|
<div style={{display: 'flex', alignItems: 'end'}}>
|
||||||
{unsaved &&
|
<div>
|
||||||
<Segment attached>
|
<ToolLabel>View</ToolLabel>
|
||||||
<Button fluid color="teal" icon="save" content="Save pattern" onClick={() => saveObject(/*this.refs.canvas*/)} loading={saving}/>
|
<Popup hoverable on='click'
|
||||||
<Button style={{marginTop: 5}} fluid icon='refresh' content='Undo changes' onClick={revertChanges} />
|
trigger={<Button color='blue' size="tiny" icon="zoom" />}
|
||||||
</Segment>
|
content={<div style={{width: 150}}>
|
||||||
}
|
<h4>Zoom</h4>
|
||||||
<Segment attached>
|
|
||||||
<Accordion fluid>
|
|
||||||
<Accordion.Title active={drawerIsActive('view')} onClick={e => activateDrawer('view')}>
|
|
||||||
<Icon name="dropdown" /> View
|
|
||||||
</Accordion.Title>
|
|
||||||
<Accordion.Content active={drawerIsActive('view')}>
|
|
||||||
<small>Drawdown view</small>
|
|
||||||
<Select
|
|
||||||
size="tiny"
|
|
||||||
fluid
|
|
||||||
value={editor.view}
|
|
||||||
onChange={(e, s) => setEditorView(s.value)}
|
|
||||||
style={{ fontSize: '11px' }}
|
|
||||||
options={[
|
|
||||||
{ key: 1, value: 'interlacement', text: 'Interlacement' },
|
|
||||||
{ key: 2, value: 'colour', text: 'Colour only' },
|
|
||||||
{ key: 3, value: 'warp', text: 'Warp view' },
|
|
||||||
{ key: 4, value: 'weft', text: 'Weft view' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<div style={{ marginTop: '5px' }} />
|
|
||||||
<small>Zoom</small>
|
|
||||||
<Slider defaultValue={baseSize} min={5} max={13} step={1} onAfterChange={onZoomChange} />
|
<Slider defaultValue={baseSize} min={5} max={13} step={1} onAfterChange={onZoomChange} />
|
||||||
</Accordion.Content>
|
</div>}
|
||||||
|
/>
|
||||||
|
|
||||||
<Accordion.Title active={drawerIsActive('properties')} onClick={e => activateDrawer('properties')}>
|
<small><Dropdown text={`${VIEWS.find(v => v.value === editor.view)?.text} (${editor.viewingBack ? 'Back' : 'Front'})`} size='tiny'>
|
||||||
<Icon name="dropdown" /> Properties
|
<Dropdown.Menu>
|
||||||
</Accordion.Title>
|
{VIEWS.map(view =>
|
||||||
<Accordion.Content active={drawerIsActive('properties')}>
|
<Dropdown.Item key={view.value} text={view.text} onClick={e => setEditorView(view.value)} />
|
||||||
|
)}
|
||||||
|
<Dropdown.Divider />
|
||||||
|
<Dropdown.Item onClick={() => setEditorViewingBack(false)}>Front{!editor.viewingBack && <Icon style={{marginLeft: 5}} name='check' />}</Dropdown.Item>
|
||||||
|
<Dropdown.Item onClick={() => setEditorViewingBack(true)}>Back {editor.viewingBack && <Icon style={{marginLeft: 5}} name='check' />}</Dropdown.Item>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown></small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{marginLeft: 10}}>
|
||||||
|
<ToolLabel>Tools & drawing</ToolLabel>
|
||||||
|
<Button.Group size="tiny">
|
||||||
|
<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 data-tooltip="Select threads" color={editor.tool === 'select' && 'blue'} size="tiny" icon onClick={() => enableTool('select')}><Icon name="i cursor" /></Button>
|
||||||
|
<Button data-tooltip="Insert threads" color={editor.tool === 'insert' && 'blue'} size="tiny" icon onClick={() => enableTool('insert')}><Icon name="plus" /></Button>
|
||||||
|
<Button className='joyride-colour' data-tooltip="Apply thread colour" color={editor.tool === 'colour' && 'blue'} size="tiny" icon onClick={() => enableTool('colour')}><Icon name="paint brush" /></Button>
|
||||||
|
<Button data-tooltip="Erase threads" color={editor.tool === 'eraser' && 'blue'} size="tiny" icon onClick={() => enableTool('eraser')}><Icon name="eraser" /></Button>
|
||||||
|
<Button className='joyride-straight' data-tooltip="Straight draw" color={editor.tool === 'straight' && 'blue'} size="tiny" icon onClick={() => enableTool('straight')}>/ /</Button>
|
||||||
|
<Button className='joyride-point' data-tooltip="Point draw" color={editor.tool === 'point' && 'blue'} size="tiny" icon onClick={() => enableTool('point')}><Icon name="chevron up" /></Button>
|
||||||
|
</Button.Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{marginLeft: 10}}>
|
||||||
|
<div style={{display: 'flex', 'alignItems': 'end'}}>
|
||||||
|
<ToolLabel>Colour</ToolLabel>
|
||||||
|
<div><ColourSquare small colour={utils.rgb(editor.colour)} style={{top: 4, marginLeft: 10}} /></div>
|
||||||
|
</div>
|
||||||
|
<Button.Group size="tiny">
|
||||||
|
<Popup hoverable on="click"
|
||||||
|
trigger={<Button style={{marginLeft: 5}} size="tiny" icon="tint" style={{color: utils.rgb(editor.colour)}} />}
|
||||||
|
content={<div style={{width: 150}}>
|
||||||
|
{pattern.colours && pattern.colours.map(colour =>
|
||||||
|
<ColourSquare key={colour} colour={utils.rgb(colour)} onClick={() => setColour(colour)} />
|
||||||
|
)}
|
||||||
|
</div>}
|
||||||
|
/>
|
||||||
|
<Popup hoverable on='click'
|
||||||
|
trigger={<Button color='blue' size='mini' icon='add' />}
|
||||||
|
content={
|
||||||
|
<div style={{padding: 3}}>
|
||||||
|
<SketchPicker color={newColour} onChangeComplete={c => setNewColour(c.rgb)} />
|
||||||
|
<Button size='sm' style={{marginTop: 10}} onClick={e => {
|
||||||
|
const { r, g, b } = newColour;
|
||||||
|
const newColours = Object.assign([], pattern.colours);
|
||||||
|
newColours.push(`${r},${g},${b}`);
|
||||||
|
updatePattern({ colours: newColours })
|
||||||
|
}}>Add colour to palette</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Button.Group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<Popup hoverable on='click' position='top right'
|
||||||
|
trigger={<Button color='blue' style={{marginLeft: 5}} size='tiny' content='Properties' />}
|
||||||
|
content={
|
||||||
|
<div style={{padding: 3, width: 300}}>
|
||||||
<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={object.name} onChange={setName} />
|
||||||
<Grid columns={2}>
|
<Grid columns={2}>
|
||||||
@ -253,62 +318,56 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
|
|||||||
content='Add new threads to the warp and weft as you edit near the end'
|
content='Add new threads to the warp and weft as you edit near the end'
|
||||||
trigger={<Checkbox checked={editor.autoExtend ?? true} onChange={setAutomaticallyExtend} label='Auto-extend warp and weft' style={{marginTop: 10}} />}
|
trigger={<Checkbox checked={editor.autoExtend ?? true} onChange={setAutomaticallyExtend} label='Auto-extend warp and weft' style={{marginTop: 10}} />}
|
||||||
/>
|
/>
|
||||||
</Accordion.Content>
|
|
||||||
|
|
||||||
<Accordion.Title active={drawerIsActive('drawing')} onClick={e => activateDrawer('drawing')}>
|
<div style={{marginTop: 20}}>
|
||||||
<Icon name="dropdown" /> Tools
|
<Button basic color='red' fluid onClick={e => setDeleteModalOpen(true)}>Delete pattern</Button>
|
||||||
</Accordion.Title>
|
</div>
|
||||||
<Accordion.Content active={drawerIsActive('drawing')}>
|
|
||||||
<Button.Group fluid>
|
|
||||||
<Button className='joyride-pan' data-tooltip="Pan (drag to move) pattern" color={editor.tool === 'pan' && 'blue'} size="tiny" icon onClick={() => enableTool('pan')}><Icon name="move" /></Button>
|
|
||||||
<Button data-tooltip="Select threads" color={editor.tool === 'select' && 'blue'} size="tiny" icon onClick={() => enableTool('select')}><Icon name="i cursor" /></Button>
|
|
||||||
<Button data-tooltip="Insert threads" color={editor.tool === 'insert' && 'blue'} size="tiny" icon onClick={() => enableTool('insert')}><Icon name="plus" /></Button>
|
|
||||||
<Button className='joyride-colour' data-tooltip="Apply thread colour" color={editor.tool === 'colour' && 'blue'} size="tiny" icon onClick={() => enableTool('colour')}><Icon name="paint brush" /></Button>
|
|
||||||
<Button className='joyride-straight' data-tooltip="Straight draw" color={editor.tool === 'straight' && 'blue'} size="tiny" icon onClick={() => enableTool('straight')}>/ /</Button>
|
|
||||||
<Button className='joyride-point' data-tooltip="Point draw" color={editor.tool === 'point' && 'blue'} size="tiny" icon onClick={() => enableTool('point')}><Icon name="chevron up" /></Button>
|
|
||||||
</Button.Group>
|
|
||||||
</Accordion.Content>
|
|
||||||
|
|
||||||
<Accordion.Title active={drawerIsActive('palette')} onClick={e => activateDrawer('palette')}>
|
|
||||||
<Icon name="dropdown" /> Palette
|
|
||||||
<ColourSquare colour={utils.rgb(editor.colour)} style={{top: 4, marginLeft: 10}} />
|
|
||||||
</Accordion.Title>
|
|
||||||
<Accordion.Content active={drawerIsActive('palette')}>
|
|
||||||
{pattern.colours && pattern.colours.map(colour =>
|
|
||||||
<ColourSquare key={colour} colour={utils.rgb(colour)} onClick={() => setColour(colour)} />
|
|
||||||
)}
|
|
||||||
<div style={{marginTop: 10}}>
|
|
||||||
<Popup
|
|
||||||
trigger={<Button size='mini'><Icon name='add' /> Colour</Button>}
|
|
||||||
on='click' hoverable
|
|
||||||
content={
|
|
||||||
<div style={{padding: 5}}>
|
|
||||||
<SketchPicker color={newColour} onChangeComplete={c => setNewColour(c.rgb)} />
|
|
||||||
<Button size='sm' style={{marginTop: 10}} onClick={e => {
|
|
||||||
const { r, g, b } = newColour;
|
|
||||||
const newColours = Object.assign([], pattern.colours);
|
|
||||||
newColours.push(`${r},${g},${b}`);
|
|
||||||
updatePattern({ colours: newColours })
|
|
||||||
}}>Add colour to palette</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</Accordion.Content>
|
|
||||||
|
|
||||||
<Accordion.Title active={drawerIsActive('advanced')} onClick={e => activateDrawer('advanced')}>
|
|
||||||
<Icon name="dropdown" /> Advanced
|
|
||||||
</Accordion.Title>
|
|
||||||
<Accordion.Content active={drawerIsActive('advanced')}>
|
|
||||||
<Button size="small" basic color="red" fluid onClick={e => setDeleteModalOpen(true)}>Delete pattern</Button>
|
|
||||||
<Confirm
|
<Confirm
|
||||||
open={deleteModalOpen}
|
open={deleteModalOpen}
|
||||||
content="Really delete this pattern?"
|
content="Really delete this pattern?"
|
||||||
onCancel={e => setDeleteModalOpen(false)}
|
onCancel={e => setDeleteModalOpen(false)}
|
||||||
onConfirm={deleteObject}
|
onConfirm={deleteObject}
|
||||||
/>
|
/>
|
||||||
</Accordion.Content>
|
|
||||||
</Accordion>
|
<Dropdown icon={null} direction='left' disabled={unsaved}
|
||||||
|
trigger={<Button color='blue' size='tiny' icon='download' style={{marginLeft: 5}} />}
|
||||||
|
>
|
||||||
|
<Dropdown.Menu>
|
||||||
|
{object.previewUrl &&
|
||||||
|
<Dropdown.Item onClick={e => utils.downloadDrawdownImage(object)} content='Download drawdown as an image' icon='file outline' />
|
||||||
|
}
|
||||||
|
{(utils.canEditProject(user, project) || project.openSource) &&
|
||||||
|
<>
|
||||||
|
{object.patternImage &&
|
||||||
|
<Dropdown.Item icon='file outline' content='Download complete pattern as an image' onClick={e => utils.downloadPatternImage(object)}/>
|
||||||
|
}
|
||||||
|
<Dropdown.Divider />
|
||||||
|
<Dropdown.Item onClick={e => utils.downloadWif(object)} content="Download pattern in WIF format" icon="text file" />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
|
||||||
|
<Dropdown style={{marginLeft: 5}} icon={null} direction='left'
|
||||||
|
trigger={<Button basic size='tiny' icon='help' />}
|
||||||
|
>
|
||||||
|
<Dropdown.Menu>
|
||||||
|
<Dropdown.Item as={Link} to={`/docs/patterns#using-the-pattern-editor`}target="_blank">Documentation</Dropdown.Item>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{marginTop: 5, textAlign: 'right'}}>
|
||||||
|
<Button.Group size="tiny">
|
||||||
|
<Button size="tiny" color="teal" icon="save" content="Save" onClick={() => saveObject()} loading={saving} disabled={!unsaved} />
|
||||||
|
<Button size="tiny" color="orange" icon='refresh' content='Revert' onClick={revertChanges} disabled={!unsaved} />
|
||||||
|
</Button.Group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Segment>
|
</Segment>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -3,9 +3,8 @@ import styled from 'styled-components';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import utils from '../../../../utils/utils.js';
|
import utils from '../../../../utils/utils.js';
|
||||||
|
|
||||||
const StyledWarp = styled.div`
|
const WarpContainer = styled.div`
|
||||||
top:0px;
|
top:0px;
|
||||||
right:0px;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: ${props => (props.treadles * props.baseSize) + 20}px;
|
right: ${props => (props.treadles * props.baseSize) + 20}px;
|
||||||
height: ${props => (props.shafts * props.baseSize) + 40}px;
|
height: ${props => (props.shafts * props.baseSize) + 40}px;
|
||||||
@ -17,6 +16,24 @@ const StyledWarp = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const WarpCanvas = styled.canvas`
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 0px;
|
||||||
|
height: ${props => props.warp.shafts * props.baseSize}px;
|
||||||
|
width: ${props => props.warp.threading.length * props.baseSize}px;
|
||||||
|
border-radius: 4;
|
||||||
|
box-shadow: 0px 0px 10px rgba(0,0,0,0.15);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Colourway = styled.canvas`
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
right: 0px;
|
||||||
|
height: 10px;
|
||||||
|
width: ${props => props.warp.threading.length * props.baseSize}px;
|
||||||
|
`;
|
||||||
|
|
||||||
const squares = {};
|
const squares = {};
|
||||||
const markers = {};
|
const markers = {};
|
||||||
const selectedMarkers = {};
|
const selectedMarkers = {};
|
||||||
@ -147,6 +164,10 @@ function Warp({ baseSize, cellStyle, warp, weft, updatePattern }) {
|
|||||||
if (thread >= warp.threading.length) fillUpTo(newWarp, thread);
|
if (thread >= warp.threading.length) fillUpTo(newWarp, thread);
|
||||||
newWarp.threading[thread].colour = editor.colour;
|
newWarp.threading[thread].colour = editor.colour;
|
||||||
}
|
}
|
||||||
|
if (editor.tool === 'eraser') {
|
||||||
|
if (thread >= warp.threading.length) fillUpTo(newWarp, thread);
|
||||||
|
newWarp.threading[thread].shaft = 0;
|
||||||
|
}
|
||||||
if (editor.tool === 'straight') {
|
if (editor.tool === 'straight') {
|
||||||
while (x <= hX && x >= lX) {
|
while (x <= hX && x >= lX) {
|
||||||
if (x >= warp.threading.length || warp.threading.length - x < 5) fillUpTo(newWarp, x + 5);
|
if (x >= warp.threading.length || warp.threading.length - x < 5) fillUpTo(newWarp, x + 5);
|
||||||
@ -183,6 +204,13 @@ function Warp({ baseSize, cellStyle, warp, weft, updatePattern }) {
|
|||||||
warpThread.shaft = warpThread.shaft === shaft ? 0 : shaft;
|
warpThread.shaft = warpThread.shaft === shaft ? 0 : shaft;
|
||||||
updatePattern({ warp: newWarp });
|
updatePattern({ warp: newWarp });
|
||||||
}
|
}
|
||||||
|
if (editor.tool === 'eraser') {
|
||||||
|
const newWarp = Object.assign({}, warp);
|
||||||
|
if (thread > warp.threading.length || warp.threading.length - thread < 5) fillUpTo(newWarp, thread + 5);
|
||||||
|
const warpThread = newWarp.threading[thread];
|
||||||
|
warpThread.shaft = 0;
|
||||||
|
updatePattern({ warp: newWarp });
|
||||||
|
}
|
||||||
if (editor.tool === 'select') {
|
if (editor.tool === 'select') {
|
||||||
const newWarp = Object.assign({}, warp);
|
const newWarp = Object.assign({}, warp);
|
||||||
const warpThread = newWarp.threading[thread];
|
const warpThread = newWarp.threading[thread];
|
||||||
@ -306,31 +334,22 @@ function Warp({ baseSize, cellStyle, warp, weft, updatePattern }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWarp treadles={weft.treadles} shafts={warp.shafts} baseSize={baseSize} tool={tool}>
|
<WarpContainer treadles={weft.treadles} shafts={warp.shafts} baseSize={baseSize} tool={tool}>
|
||||||
<canvas className='warp-colourway joyride-warpColourway' ref={colourwayRef} width={warp.threading.length * baseSize} height={10}
|
<Colourway className='warp-colourway joyride-warpColourway' ref={colourwayRef} width={warp.threading.length * baseSize} height={10} warp={warp} baseSize={baseSize}
|
||||||
style={{
|
|
||||||
position: 'absolute', top: 0, right: 0, height: 10, width: warp.threading.length * baseSize,
|
|
||||||
}}
|
|
||||||
onClick={mouseClickColourway}
|
onClick={mouseClickColourway}
|
||||||
onMouseDown={mouseDownColourway}
|
onMouseDown={mouseDownColourway}
|
||||||
onMouseMove={mouseMoveColourway}
|
onMouseMove={mouseMoveColourway}
|
||||||
onMouseUp={mouseUpColourway}
|
onMouseUp={mouseUpColourway}
|
||||||
onMouseLeave={mouseUpColourway}
|
onMouseLeave={mouseUpColourway}
|
||||||
/>
|
/>
|
||||||
<canvas className='warp-threads joyride-warp' ref={warpRef} width={warp.threading.length * baseSize} height={warp.shafts * baseSize}
|
<WarpCanvas className='warp-threads joyride-warp' ref={warpRef} width={warp.threading.length * baseSize} height={warp.shafts * baseSize} warp={warp} baseSize={baseSize}
|
||||||
style={{
|
|
||||||
position: 'absolute', top: 10, right: 0,
|
|
||||||
height: warp.shafts * baseSize,
|
|
||||||
width: warp.threading.length * baseSize, borderRadius: 4,
|
|
||||||
boxShadow: '0px 0px 10px rgba(0,0,0,0.15)',
|
|
||||||
}}
|
|
||||||
onClick={click}
|
onClick={click}
|
||||||
onMouseDown={mouseDown}
|
onMouseDown={mouseDown}
|
||||||
onMouseMove={mouseMove}
|
onMouseMove={mouseMove}
|
||||||
onMouseUp={mouseUp}
|
onMouseUp={mouseUp}
|
||||||
onMouseLeave={mouseUp}
|
onMouseLeave={mouseUp}
|
||||||
/>
|
/>
|
||||||
</StyledWarp>
|
</WarpContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,9 +3,7 @@ import styled from 'styled-components';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import utils from '../../../../utils/utils.js';
|
import utils from '../../../../utils/utils.js';
|
||||||
|
|
||||||
const StyledWeft = styled.div`
|
const WeftContainer = styled.div`
|
||||||
top:0px;
|
|
||||||
right:0px;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: ${props => (props.shafts * props.baseSize) + 20}px;
|
top: ${props => (props.shafts * props.baseSize) + 20}px;
|
||||||
right: 0;
|
right: 0;
|
||||||
@ -19,6 +17,24 @@ const StyledWeft = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const WeftCanvas = styled.canvas`
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
right: 10px;
|
||||||
|
height: ${props => props.weft.treadling?.length * props.baseSize}px;
|
||||||
|
width: ${props => props.weft.treadles * props.baseSize}px;
|
||||||
|
border-radius: 4;
|
||||||
|
box-shadow: 0px 0px 10px rgba(0,0,0,0.15);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Colourway = styled.canvas`
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
right: 0px;
|
||||||
|
width: 10px;
|
||||||
|
height: ${props => props.weft.treadling?.length * props.baseSize}px;
|
||||||
|
`;
|
||||||
|
|
||||||
const squares = {};
|
const squares = {};
|
||||||
const markers = {};
|
const markers = {};
|
||||||
let dragging = false;
|
let dragging = false;
|
||||||
@ -148,6 +164,10 @@ function Weft({ cellStyle, warp, weft, baseSize, updatePattern }) {
|
|||||||
if ((thread - 1) >= weft.treadling.length) fillUpTo(newWeft, (thread - 1));
|
if ((thread - 1) >= weft.treadling.length) fillUpTo(newWeft, (thread - 1));
|
||||||
newWeft.treadling[thread - 1].colour = editor.colour;
|
newWeft.treadling[thread - 1].colour = editor.colour;
|
||||||
}
|
}
|
||||||
|
if (editor.tool === 'eraser') {
|
||||||
|
if ((thread - 1) >= weft.treadling.length) fillUpTo(newWeft, (thread - 1));
|
||||||
|
newWeft.treadling[thread - 1].treadle = 0;
|
||||||
|
}
|
||||||
if (editor.tool === 'straight') {
|
if (editor.tool === 'straight') {
|
||||||
while (y <= hY && y >= lY) {
|
while (y <= hY && y >= lY) {
|
||||||
if ((y - 1) >= weft.treadling.length || weft.treadling.length - y - 1 < 5) fillUpTo(newWeft, (y + 5));
|
if ((y - 1) >= weft.treadling.length || weft.treadling.length - y - 1 < 5) fillUpTo(newWeft, (y + 5));
|
||||||
@ -185,6 +205,14 @@ function Weft({ cellStyle, warp, weft, baseSize, updatePattern }) {
|
|||||||
weftThread.treadle = weftThread.treadle === treadle ? 0 : treadle;
|
weftThread.treadle = weftThread.treadle === treadle ? 0 : treadle;
|
||||||
updatePattern({ weft: newWeft });
|
updatePattern({ weft: newWeft });
|
||||||
}
|
}
|
||||||
|
if (editor.tool === 'eraser') {
|
||||||
|
treadle += 1;
|
||||||
|
const newWeft = Object.assign({}, weft);
|
||||||
|
if (thread >= newWeft.treadling.length || newWeft.treadling.length - thread < 5) fillUpTo(newWeft, thread + 5);
|
||||||
|
const weftThread = newWeft.treadling[thread - 1];
|
||||||
|
weftThread.treadle = 0;
|
||||||
|
updatePattern({ weft: newWeft });
|
||||||
|
}
|
||||||
if (editor.tool === 'select') {
|
if (editor.tool === 'select') {
|
||||||
const newWeft = Object.assign({}, weft);
|
const newWeft = Object.assign({}, weft);
|
||||||
const weftThread = newWeft.treadling[thread - 1];
|
const weftThread = newWeft.treadling[thread - 1];
|
||||||
@ -309,28 +337,22 @@ function Weft({ cellStyle, warp, weft, baseSize, updatePattern }) {
|
|||||||
|
|
||||||
const threadCount = weft.treadling?.length || 0;
|
const threadCount = weft.treadling?.length || 0;
|
||||||
return (
|
return (
|
||||||
<StyledWeft baseSize={baseSize} treadles={weft.treadles} shafts={warp.shafts} threads={threadCount} tool={tool}>
|
<WeftContainer baseSize={baseSize} treadles={weft.treadles} shafts={warp.shafts} threads={threadCount} tool={tool}>
|
||||||
<canvas className='weft-colourway' ref={colourwayRef} width={10} height={threadCount * baseSize}
|
<Colourway className='weft-colourway' ref={colourwayRef} width={10} height={threadCount * baseSize} weft={weft} baseSize={baseSize}
|
||||||
style={{ position: 'absolute', top: 0, right: 0, width: 10, height: threadCount * baseSize}}
|
|
||||||
onClick={mouseClickColourway}
|
onClick={mouseClickColourway}
|
||||||
onMouseDown={mouseDownColourway}
|
onMouseDown={mouseDownColourway}
|
||||||
onMouseMove={mouseMoveColourway}
|
onMouseMove={mouseMoveColourway}
|
||||||
onMouseUp={mouseUpColourway}
|
onMouseUp={mouseUpColourway}
|
||||||
onMouseLeave={mouseUpColourway}
|
onMouseLeave={mouseUpColourway}
|
||||||
/>
|
/>
|
||||||
<canvas className='weft-threads joyride-weft' ref={weftRef} width={weft.treadles * baseSize} height={threadCount * baseSize}
|
<WeftCanvas className='weft-threads joyride-weft' ref={weftRef} width={weft.treadles * baseSize} height={threadCount * baseSize} weft={weft} baseSize={baseSize}
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0, right: 10, height: threadCount * baseSize, width: weft.treadles * baseSize,
|
|
||||||
borderRadius: 4, boxShadow: '0px 0px 10px rgba(0,0,0,0.15)',
|
|
||||||
}}
|
|
||||||
onClick={click}
|
onClick={click}
|
||||||
onMouseDown={mouseDown}
|
onMouseDown={mouseDown}
|
||||||
onMouseMove={mouseMove}
|
onMouseMove={mouseMove}
|
||||||
onMouseUp={mouseUp}
|
onMouseUp={mouseUp}
|
||||||
onMouseLeave={mouseUp}
|
onMouseLeave={mouseUp}
|
||||||
/>
|
/>
|
||||||
</StyledWeft>
|
</WeftContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'react-app-polyfill/ie9';
|
import 'react-app-polyfill/ie9';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { RouterProvider, createBrowserRouter } 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';
|
||||||
@ -10,7 +10,48 @@ import 'react-toastify/dist/ReactToastify.css';
|
|||||||
import 'pell/dist/pell.min.css';
|
import 'pell/dist/pell.min.css';
|
||||||
|
|
||||||
import reducers from './reducers';
|
import reducers from './reducers';
|
||||||
|
|
||||||
import App from './components/App';
|
import App from './components/App';
|
||||||
|
import Home from './components/main/Home';
|
||||||
|
|
||||||
|
import MarketingHome from './components/marketing/Home';
|
||||||
|
import PrivacyPolicy from './components/marketing/PrivacyPolicy';
|
||||||
|
import TermsOfUse from './components//marketing/TermsOfUse';
|
||||||
|
|
||||||
|
import Login from './components/Login';
|
||||||
|
import ForgottenPassword from './components/ForgottenPassword';
|
||||||
|
import ResetPassword from './components/ResetPassword';
|
||||||
|
|
||||||
|
import Settings from './components/main/settings/Settings';
|
||||||
|
import SettingsIdentity from './components/main/settings/Identity';
|
||||||
|
import SettingsNotification from './components/main/settings/Notification';
|
||||||
|
import SettingsAccount from './components/main/settings/Account';
|
||||||
|
|
||||||
|
import Profile from './components/main/users/Profile';
|
||||||
|
import ProfileEdit from './components/main/users/EditProfile';
|
||||||
|
import ProfileProjects from './components/main/users/ProfileProjects';
|
||||||
|
|
||||||
|
import NewProject from './components/main/projects/New';
|
||||||
|
import Project from './components/main/projects/Project';
|
||||||
|
import ProjectObjects from './components/main/projects/ProjectObjects';
|
||||||
|
import ProjectSettings from './components/main/projects/Settings';
|
||||||
|
import ObjectDraft from './components/main/projects/objects/Draft';
|
||||||
|
import ObjectList from './components/main/projects/ObjectList';
|
||||||
|
|
||||||
|
import NewGroup from './components/main/groups/New';
|
||||||
|
import Group from './components/main/groups/Group';
|
||||||
|
import GroupFeed from './components/main/groups/Feed';
|
||||||
|
import GroupMembers from './components/main/groups/Members';
|
||||||
|
import GroupProjects from './components/main/groups/Projects';
|
||||||
|
import GroupSettings from './components/main/groups/Settings';
|
||||||
|
|
||||||
|
import Explore from './components/main/explore/Explore'
|
||||||
|
|
||||||
|
import Docs from './components/docs';
|
||||||
|
import DocsHome from './components/docs/Home';
|
||||||
|
import DocsDoc from './components/docs/Doc';
|
||||||
|
|
||||||
|
import Root from './components/main/root';
|
||||||
|
|
||||||
export const store = createStore(reducers);
|
export const store = createStore(reducers);
|
||||||
|
|
||||||
@ -22,12 +63,56 @@ if (import.meta.env.VITE_SENTRY_DSN) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <App />,
|
||||||
|
children: [
|
||||||
|
{ path: "", element: <MarketingHome /> },
|
||||||
|
{ path: "root", element: <Root /> },
|
||||||
|
{ path: "explore", element: <Explore /> },
|
||||||
|
{ path: "privacy", element: <PrivacyPolicy /> },
|
||||||
|
{ path: "terms-of-use", element: <TermsOfUse /> },
|
||||||
|
{ path: "password/forgotten", element: <ForgottenPassword /> },
|
||||||
|
{ path: "password/reset", element: <ResetPassword /> },
|
||||||
|
{ path: "settings", element: <Settings />, children: [
|
||||||
|
{ path: "identity", element: <SettingsIdentity /> },
|
||||||
|
{ path: "notifications", element: <SettingsNotification /> },
|
||||||
|
{ path: "account", element: <SettingsAccount /> },
|
||||||
|
{ path: "", element: <SettingsIdentity /> },
|
||||||
|
] },
|
||||||
|
{ path: "projects/new", element: <NewProject /> },
|
||||||
|
{ path: "projects", element: <Home /> },
|
||||||
|
{ path: "groups/new", element: <NewGroup /> },
|
||||||
|
{ path: "groups/:id", element: <Group />, children: [
|
||||||
|
{ path: "feed", element: <GroupFeed /> },
|
||||||
|
{ path: "members", element: <GroupMembers /> },
|
||||||
|
{ path: "projects", element: <GroupProjects /> },
|
||||||
|
{ path: "settings", element: <GroupSettings /> },
|
||||||
|
{ path: "", element: <GroupFeed /> },
|
||||||
|
] },
|
||||||
|
{ path: "docs", element: <Docs />, children: [
|
||||||
|
{ path: ":doc", element: <DocsDoc /> },
|
||||||
|
{ path: "", element: <DocsHome /> },
|
||||||
|
] },
|
||||||
|
{ path: ":username/:projectPath", element: <Project />, children: [
|
||||||
|
{ path: "settings", element: <ProjectSettings /> },
|
||||||
|
{ path: ":objectId/edit", element: <ObjectDraft /> },
|
||||||
|
{ path: ":objectId", element: <ProjectObjects /> },
|
||||||
|
{ path: "", element: <ObjectList /> },
|
||||||
|
] },
|
||||||
|
{ path: ":username", element: <Profile />, children: [
|
||||||
|
{ path: "edit", element: <ProfileEdit /> },
|
||||||
|
{ path: "", element: <ProfileProjects /> },
|
||||||
|
] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
const container = document.getElementById('root');
|
const container = document.getElementById('root');
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
root.render(
|
root.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<BrowserRouter>
|
<RouterProvider router={router} />
|
||||||
<App />
|
|
||||||
</BrowserRouter>
|
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
@ -7,7 +7,7 @@ const initialState = {
|
|||||||
explorePage: 1,
|
explorePage: 1,
|
||||||
comments: [],
|
comments: [],
|
||||||
selected: null,
|
selected: null,
|
||||||
editor: { tool: 'straight', colour: 'orange', view: 'interlacement', autoExtend: true },
|
editor: { tool: 'straight', colour: null, view: 'interlacement', autoExtend: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
function objects(state = initialState, action) {
|
function objects(state = initialState, action) {
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import actions from '../actions';
|
import actions from '../actions';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
driftReady: false,
|
|
||||||
syncedToDrift: false,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
users: [],
|
users: [],
|
||||||
@ -10,10 +8,6 @@ const initialState = {
|
|||||||
|
|
||||||
function users(state = initialState, action) {
|
function users(state = initialState, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case actions.users.INIT_DRIFT:
|
|
||||||
return Object.assign({}, state, { driftReady: true });
|
|
||||||
case actions.users.SYNC_DRIFT:
|
|
||||||
return Object.assign({}, state, { syncedToDrift: action.synced ?? true });
|
|
||||||
case actions.users.REQUEST_USERS:
|
case actions.users.REQUEST_USERS:
|
||||||
return Object.assign({}, state, { loading: true, errorMessage: '' });
|
return Object.assign({}, state, { loading: true, errorMessage: '' });
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { createConfirmation } from 'react-confirm';
|
import { createConfirmation } from 'react-confirm';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
import ConfirmModal from '../components/includes/ConfirmModal';
|
import ConfirmModal from '../components/includes/ConfirmModal';
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
|
||||||
@ -75,6 +76,7 @@ const utils = {
|
|||||||
return confirm({ title, confirmation: content });
|
return confirm({ title, confirmation: content });
|
||||||
},
|
},
|
||||||
rgb(s) {
|
rgb(s) {
|
||||||
|
if (!s) return s;
|
||||||
if (s.match(/^[0-9]+,[0-9]+,[0-9]+$/)) return `rgb(${s})`;
|
if (s.match(/^[0-9]+,[0-9]+,[0-9]+$/)) return `rgb(${s})`;
|
||||||
if (s.match(/^rgb\([0-9]+,[0-9]+[0-9]+\)$/)) return s;
|
if (s.match(/^rgb\([0-9]+,[0-9]+[0-9]+\)$/)) return s;
|
||||||
if (s.match(/^#[a-zA-Z0-9]+$/)) {
|
if (s.match(/^#[a-zA-Z0-9]+$/)) {
|
||||||
@ -118,7 +120,61 @@ const utils = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
downloadDrawdownImage(object) {
|
||||||
|
const element = document.createElement('a');
|
||||||
|
element.setAttribute('target', '_blank');
|
||||||
|
element.setAttribute('href', object.previewUrl);
|
||||||
|
element.setAttribute('download', `${object.name.replace(/ /g, '_')}-drawdown.png`);
|
||||||
|
element.style.display = 'none';
|
||||||
|
document.body.appendChild(element);
|
||||||
|
element.click();
|
||||||
|
document.body.removeChild(element);
|
||||||
|
toast.info('The file has been downloaded');
|
||||||
|
},
|
||||||
|
downloadPatternImage(object) {
|
||||||
|
const element = document.createElement('a');
|
||||||
|
element.setAttribute('target', '_blank');
|
||||||
|
element.setAttribute('href', object.patternImage);
|
||||||
|
element.setAttribute('download', `${object.name.replace(/ /g, '_')}-pattern.png`);
|
||||||
|
element.style.display = 'none';
|
||||||
|
document.body.appendChild(element);
|
||||||
|
element.click();
|
||||||
|
document.body.removeChild(element);
|
||||||
|
toast.info('The file has been downloaded');
|
||||||
|
},
|
||||||
|
generateCompletePattern(pattern, parentSelector, cb) {
|
||||||
|
if (!pattern) return;
|
||||||
|
const { warp, weft } = pattern;
|
||||||
|
setTimeout(() => {
|
||||||
|
const baseSize = 6;
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
canvas.width = warp.threading?.length * baseSize + weft.treadles * baseSize + 20;
|
||||||
|
canvas.height = warp.shafts * baseSize + weft.treadling?.length * baseSize + 20;
|
||||||
|
ctx.fillStyle = 'white';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.fillStyle = 'black';
|
||||||
|
const warpCanvas = document.querySelector(`${parentSelector} .warp-threads`);
|
||||||
|
const warpColourwayCanvas = document.querySelector(`${parentSelector} .warp-colourway`);
|
||||||
|
const weftCanvas = document.querySelector(`${parentSelector} .weft-threads`);
|
||||||
|
const weftColourwayCanvas = document.querySelector(`${parentSelector} .weft-colourway`);
|
||||||
|
const drawdownCanvas = document.querySelector(`${parentSelector} .drawdown`);
|
||||||
|
const tieupsCanvas = document.querySelector(`${parentSelector} .tieups`);
|
||||||
|
if (warpCanvas) {
|
||||||
|
ctx.drawImage(warpColourwayCanvas, canvas.width - warpCanvas.width - weft.treadles * baseSize - 20, 0);
|
||||||
|
ctx.drawImage(warpCanvas, canvas.width - warpCanvas.width - weft.treadles * baseSize - 20, 10);
|
||||||
|
ctx.drawImage(weftCanvas, canvas.width - 10 - weft.treadles * baseSize, 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(drawdownCanvas, canvas.width - drawdownCanvas.width - weft.treadles * baseSize - 20, warp.shafts * baseSize + 20);
|
||||||
|
setTimeout(() => {
|
||||||
|
const im = canvas.toDataURL('image/png')
|
||||||
|
if (im?.length > 20) cb(im);
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
|
}, 500);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default utils;
|
export default utils;
|
||||||
|
@ -424,7 +424,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.7.6":
|
"@babel/runtime@npm:^7.12.1":
|
||||||
version: 7.17.9
|
version: 7.17.9
|
||||||
resolution: "@babel/runtime@npm:7.17.9"
|
resolution: "@babel/runtime@npm:7.17.9"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -691,6 +691,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@remix-run/router@npm:1.14.1":
|
||||||
|
version: 1.14.1
|
||||||
|
resolution: "@remix-run/router@npm:1.14.1"
|
||||||
|
checksum: a3a0e7bd1917a4fd10322269b78ce1c5917a74889dfa747305b5a56e5d441b9187e1d8aaa6d58c4bf77713e7baca825f87057a64b2d21718cdb361f1cda75687
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@rollup/pluginutils@npm:^4.2.1":
|
"@rollup/pluginutils@npm:^4.2.1":
|
||||||
version: 4.2.1
|
version: 4.2.1
|
||||||
resolution: "@rollup/pluginutils@npm:4.2.1"
|
resolution: "@rollup/pluginutils@npm:4.2.1"
|
||||||
@ -1984,15 +1991,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"history@npm:^5.2.0":
|
|
||||||
version: 5.3.0
|
|
||||||
resolution: "history@npm:5.3.0"
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime": ^7.7.6
|
|
||||||
checksum: d73c35df49d19ac172f9547d30a21a26793e83f16a78386d99583b5bf1429cc980799fcf1827eb215d31816a6600684fba9686ce78104e23bd89ec239e7c726f
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2":
|
"hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2":
|
||||||
version: 3.3.2
|
version: 3.3.2
|
||||||
resolution: "hoist-non-react-statics@npm:3.3.2"
|
resolution: "hoist-non-react-statics@npm:3.3.2"
|
||||||
@ -3057,27 +3055,27 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-router-dom@npm:^6.3.0":
|
"react-router-dom@npm:^6.21.1":
|
||||||
version: 6.3.0
|
version: 6.21.1
|
||||||
resolution: "react-router-dom@npm:6.3.0"
|
resolution: "react-router-dom@npm:6.21.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
history: ^5.2.0
|
"@remix-run/router": 1.14.1
|
||||||
react-router: 6.3.0
|
react-router: 6.21.1
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ">=16.8"
|
react: ">=16.8"
|
||||||
react-dom: ">=16.8"
|
react-dom: ">=16.8"
|
||||||
checksum: 77603a654f8a8dc7f65535a2e5917a65f8d9ffcb06546d28dd297e52adcc4b8a84377e0115f48dca330b080af2da3e78f29d590c89307094d36927d2b1751ec3
|
checksum: d8ea3370babab626827008ced8a776ac991023745dc69d44332b6a5229dd2a6899f3e46fc50365cef0c7487cf726bcdccb365ec106915f92c735da6e4c2e6b97
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-router@npm:6.3.0":
|
"react-router@npm:6.21.1":
|
||||||
version: 6.3.0
|
version: 6.21.1
|
||||||
resolution: "react-router@npm:6.3.0"
|
resolution: "react-router@npm:6.21.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
history: ^5.2.0
|
"@remix-run/router": 1.14.1
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ">=16.8"
|
react: ">=16.8"
|
||||||
checksum: 7be673f5e72104be01e6ab274516bdb932efd93305243170690f6560e3bd1035dd1df3d3c9ce1e0f452638a2529f43a1e77dcf0934fc8031c4783da657be13ca
|
checksum: c6774cf4440b524401cecb40f4fbf4dbc89d61405f9f6d3cb3d2338e262a5254b9e27bc6039dc32e2c5f8b3009e83e9d6b30a61159dd6c423297cb0e3bd46fb3
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -3579,7 +3577,7 @@ __metadata:
|
|||||||
react-joyride: ^2.4.0
|
react-joyride: ^2.4.0
|
||||||
react-redux: ^8.0.1
|
react-redux: ^8.0.1
|
||||||
react-rewards: ^2.0.3
|
react-rewards: ^2.0.3
|
||||||
react-router-dom: ^6.3.0
|
react-router-dom: ^6.21.1
|
||||||
react-toastify: ^4.4.0
|
react-toastify: ^4.4.0
|
||||||
redux: ^4.0.0
|
redux: ^4.0.0
|
||||||
sanitize-html: ^1.19.1
|
sanitize-html: ^1.19.1
|
||||||
|
Loading…
Reference in New Issue
Block a user