Compare commits

..

No commits in common. "958edd755626e94ce5a0e18c73a05b35163860fa" and "9e9491e064f20916fb075b9f67707126b986d176" have entirely different histories.

14 changed files with 287 additions and 361 deletions

Binary file not shown.

View File

@ -17,7 +17,6 @@
"react-app-polyfill": "^0.2.0",
"react-color": "^2.19.3",
"react-confirm": "^0.1.18",
"react-content-loader": "^6.2.1",
"react-dom": "^18.2.0",
"react-helmet": "^6.0.0",
"react-joyride": "^2.4.0",

View File

@ -1,6 +1,4 @@
import api from '.';
import actions from '../actions';
import { store } from '..';
export const groups = {
create(data, success, fail) {
@ -10,7 +8,6 @@ export const groups = {
api.authenticatedRequest('DELETE', `/groups/${id}`, null, success, fail);
},
getMine(success, fail) {
store.dispatch(actions.groups.request());
api.authenticatedRequest('GET', '/groups', null, data => success && success(data.groups), fail);
},
get(id, success, fail) {

View File

@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Card, List, Dimmer } from 'semantic-ui-react';
import { Card, List } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { BulletList } from 'react-content-loader'
import UserChip from './UserChip';
import api from '../../api';
import utils from '../../utils/utils.js';
@ -9,25 +8,23 @@ import utils from '../../utils/utils.js';
export default function ExploreCard({ count }) {
const [highlightProjects, setHighlightProjects] = useState([]);
const [highlightUsers, setHighlightUsers] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
api.search.discover(count || 3, ({ highlightProjects, highlightUsers }) => {
setHighlightProjects(highlightProjects);
setHighlightUsers(highlightUsers);
setLoading(false);
});
}, []);
if ((highlightProjects?.length === 0 || highlightUsers?.length === 0)) return null;
return (
<Card fluid>
<Card.Content>
<h4>Discover a project</h4>
{loading && <BulletList />}
{highlightProjects?.length > 0 && <>
<h4>Discover a project</h4>
<List relaxed>
{highlightProjects?.map(p =>
{highlightProjects.map(p =>
<List.Item key={p._id}>
<List.Icon name='book' size='large' verticalAlign='middle' />
<List.Content>
@ -38,11 +35,10 @@ export default function ExploreCard({ count }) {
</List>
</>}
<h4>Find others on {utils.appName()}</h4>
{loading && <BulletList />}
{highlightUsers?.length > 0 && <>
<h4>Find others on {utils.appName()}</h4>
<List relaxed>
{highlightUsers?.map(u =>
{highlightUsers.map(u =>
<List.Item key={u._id}>
<List.Content>
<UserChip user={u} className='umami--click--discover-user'/>

View File

@ -1,8 +1,8 @@
import React from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import React, { useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { Modal, Menu, Button, Container, Dropdown } from 'semantic-ui-react';
import { Loader, List, Popup, Modal, Grid, Icon, Button, Container, Dropdown } from 'semantic-ui-react';
import api from '../../api';
import actions from '../../actions';
import utils from '../../utils/utils.js';
@ -10,7 +10,6 @@ import utils from '../../utils/utils.js';
import logo from '../../images/logo/main.png';
import UserChip from './UserChip';
import SupporterBadge from './SupporterBadge';
import SearchBar from './SearchBar';
const StyledNavBar = styled.div`
height:60px;
@ -19,10 +18,12 @@ const StyledNavBar = styled.div`
.logo{
height:40px;
margin-top:5px;
margin-right: 50px;
transition: opacity 0.3s;
&:hover{
opacity: 0.5;
}
.nav-links{
display: flex;
align-items: center;
.ui.button{
margin-right: 8px;
}
}
.only-mobile{
@ -32,23 +33,56 @@ const StyledNavBar = styled.div`
}
.above-mobile{
@media only screen and (max-width: 767px) {
display:none !important;
display:none;
}
}
`;
export default function NavBar() {
const navigate = useNavigate();
const location = useLocation();
const SearchBar = styled.div`
background-color:rgba(0,0,0,0.1);
padding-left:5px;
padding-top: 3px;
border: none;
border-radius:5px;
transition: background-color 0.5s;
margin-right:8px;
display:inline-block;
&:before{
display:inline-block;
content: '🔍';
}
&:focus-within{
background-color:rgba(250,250,250,0.5);
color:rgb(50,50,50);
input {
outline: none;
}
input::placeholder {
color:black;
}
}
input{
border:none;
background:none;
padding:8px;
}
`;
function NavBar() {
const dispatch = useDispatch();
const { isAuthenticated, user, groups, helpModalOpen } = useSelector(state => {
const { isAuthenticated, user, groups, helpModalOpen, searchPopupOpen, searchTerm, searchResults, searching } = useSelector(state => {
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
const { isAuthenticated } = state.auth;
const { helpModalOpen } = state.app;
return { isAuthenticated, user, groups, helpModalOpen };
const { helpModalOpen, searchPopupOpen, searchTerm, searchResults, searching } = state.app;
return { isAuthenticated, user, groups, helpModalOpen, searchPopupOpen, searchTerm, searchResults, searching };
});
const navigate = useNavigate();
useEffect(() => {
dispatch(actions.app.openSearchPopup(false));
}, [dispatch]);
const logout = () => api.auth.logout(() => {
dispatch(actions.auth.logout());
dispatch(actions.users.syncDrift(false))
@ -56,17 +90,77 @@ export default function NavBar() {
navigate('/');
});
const search = () => {
dispatch(actions.app.updateSearching(true));
api.search.all(searchTerm, r => dispatch(actions.app.updateSearchResults(r)));
};
return (
<StyledNavBar>
<Container style={{display:'flex', justifyContent: 'space-between', alignItems: 'center'}}>
<Link to="/"><img alt={`${utils.appName()} logo`} src={logo} className="logo" /></Link>
<div style={{flex: 1}}>
<Menu secondary>
<Menu.Item className='above-mobile' as={Link} to='/' name='home' active={location.pathname === '/'} />
<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'>
<Dropdown pointing='top left'
trigger={<span>Groups</span>}
<Link to="/"><img alt={`${utils.appName()} logo`} src={logo} className="logo" /></Link>
{isAuthenticated
? (
<div className='nav-links'>
<Popup basic on='focus' open={searchPopupOpen}
onOpen={e => dispatch(actions.app.openSearchPopup(true))} onClose={e => dispatch(actions.app.openSearchPopup(false))}
trigger={<SearchBar><input placeholder='Click to search...' value={searchTerm} onChange={e => dispatch(actions.app.updateSearchTerm(e.target.value))} onKeyDown={e => e.keyCode === 13 && search()} /></SearchBar>}
content={<div style={{width: 300}} className='joyride-search'>
{!searchResults?.users && !searchResults?.groups ?
<small>
{searching
? <span><Loader size='tiny' inline active style={{marginRight: 10}}/> Searching...</span>
: <span>Type something and press enter to search</span>
}
</small>
: <>
{(!searchResults.users?.length && !searchResults?.groups?.length && !searchResults?.projects?.length) ?
<span><small>No results found</small></span>
:
<Grid stackable>
{searchResults?.users?.length > 0 &&
<Grid.Column width={6}>
{searchResults?.users?.map(u =>
<div style={{marginBottom: 5}}><UserChip user={u} key={u._id} /></div>
)}
</Grid.Column>
}
{(searchResults?.projects.length > 0 || searchResults.groups.length > 0) &&
<Grid.Column width={10}>
<List>
{searchResults?.projects?.map(p =>
<List.Item key={p._id}>
<List.Icon name='book' size='large' verticalAlign='middle' />
<List.Content>
<List.Header as={Link} to={'/' + p.fullName}>{p.name}</List.Header>
<List.Description><UserChip compact user={p.owner} /></List.Description>
</List.Content>
</List.Item>
)}
{searchResults?.groups?.map(g =>
<List.Item key={g._id}>
<List.Icon name='users' size='large' verticalAlign='middle' />
<List.Content>
<List.Header as={Link} to={`/groups/${g._id}`}>{g.name}</List.Header>
<List.Description><small>{g.closed ? <span><Icon name='lock' /> Closed group</span> : <span>Open group</span>}</small></List.Description>
</List.Content>
</List.Item>
)}
</List>
</Grid.Column>
}
</Grid>
}</>}
</div>} />
<span className='above-mobile'>
<Button as={Link} to="/" size="small" icon='home' basic content='Home' />
</span>
{groups.length > 0 &&
<span className='above-mobile'>
<Dropdown icon={null} direction='left' pointing='top right'
trigger={<Button size='small' icon='users' basic content='Groups'/>}
>
<Dropdown.Menu>
<Dropdown.Header icon='users' content='Your groups' />
@ -77,79 +171,86 @@ export default function NavBar() {
<Dropdown.Item as={Link} to='/groups/new' icon='plus' content='Create a new group' />
</Dropdown.Menu>
</Dropdown>
</Menu.Item>
</span>
}
<Menu.Menu position='right'>
{isAuthenticated && <>
<Menu.Item className='above-mobile'><SearchBar /></Menu.Item>
<Dropdown direction="left" pointing="top right" icon={null} style={{ marginTop: 10}}
trigger={<UserChip user={user} withoutLink avatarOnly />}
>
<Dropdown.Menu style={{ minWidth: '200px', paddingTop: 10 }}>
{user &&
<Dropdown.Header as={Link} to={`/${user.username}`}>
<UserChip user={user} />
</Dropdown.Header>
}
{user?.isGoldSupporter && <Dropdown.Header><SupporterBadge type='gold' /></Dropdown.Header>}
{user?.isSilverSupporter && !user?.isGoldSupporter && <Dropdown.Header><SupporterBadge type='silver' /></Dropdown.Header>}
<Dropdown.Divider />
<Link to="/" className="item">Projects</Link>
{user &&<Link to={`/${user.username}`} className="item">Profile</Link>}
<Link to="/settings" className="item">Settings</Link>
<Dropdown.Divider />
{user?.roles?.indexOf('root') > -1 &&
<Dropdown.Item as={Link} to='/root'>Root</Dropdown.Item>
}
<Dropdown.Item as={Link} to='/docs'>Help</Dropdown.Item>
<Dropdown.Item onClick={e => dispatch(actions.app.openHelpModal(true))}>About {utils.appName()}</Dropdown.Item>
<Dropdown.Item onClick={logout}>Logout</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</>}
<span className='above-mobile'>
<Button size='small' icon='help' basic onClick={e => dispatch(actions.app.openHelpModal(true))}/>
</span>
{!isAuthenticated && <>
<Menu.Item name='Login' onClick={() => dispatch(actions.auth.openLogin())} />
<Menu.Item>
<Button size='small' color="teal" onClick={() => dispatch(actions.auth.openRegister())}>
<span role="img" aria-label="wave">👋</span> Sign-up
</Button>
</Menu.Item>
</>}
</Menu.Menu>
</Menu>
</div>
</Container>
<AboutModal open={helpModalOpen} onClose={e => dispatch(actions.app.openHelpModal(false))} />
</StyledNavBar>
);
}
<Dropdown direction="left" pointing="top right" icon={null} style={{marginLeft: 10, marginTop: 5}}
trigger={<UserChip user={user} withoutLink avatarOnly />}
>
<Dropdown.Menu style={{ minWidth: '200px', paddingTop: 10 }}>
{user &&
<Dropdown.Header as={Link} to={`/${user.username}`}>
<UserChip user={user} />
</Dropdown.Header>
}
{user?.isGoldSupporter && <Dropdown.Header><SupporterBadge type='gold' /></Dropdown.Header>}
{user?.isSilverSupporter && !user?.isGoldSupporter && <Dropdown.Header><SupporterBadge type='silver' /></Dropdown.Header>}
<Dropdown.Divider />
<Link to="/" className="item">Projects</Link>
{user &&<Link to={`/${user.username}`} className="item">Profile</Link>}
<Link to="/settings" className="item">Settings</Link>
<Dropdown.Divider />
{user?.roles?.indexOf('root') > -1 &&
<Dropdown.Item as={Link} to='/root'>Root</Dropdown.Item>
}
<Dropdown.Item as={Link} to='/docs'>Help</Dropdown.Item>
<Dropdown.Item onClick={logout}>Logout</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</div>
)
: (
<div className='nav-links'>
<span className="only-mobile">
<Dropdown
icon={null}
trigger={<Button basic icon="bars" />}
>
<Dropdown.Menu direction="left">
<Dropdown.Item onClick={() => dispatch(actions.auth.openLogin())}>Login</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</span>
<span className="above-mobile">
<Button basic color='teal' onClick={() => dispatch(actions.auth.openLogin())}>Login</Button>
</span>
<Button color="teal" onClick={() => dispatch(actions.auth.openRegister())}>
<span role="img" aria-label="wave">👋</span> Sign-up
</Button>
</div>
)
}
<Modal open={helpModalOpen} onClose={e => dispatch(actions.app.openHelpModal(false))}>
<Modal.Header>Welcome to {utils.appName()}!</Modal.Header>
<Modal.Content>
<h3>Introduction</h3>
<p>{utils.appName()} has been designed as a resource for weavers not only for those working alone as individuals, but also for groups who wish to share ideas, design inspirations and weaving patterns. It is ideal for those looking for a depository to store their individual work, and also for groups such as guilds, teaching groups, or any other collaborative working partnerships.</p>
<p>Projects can be created within {utils.appName()} using the integral WIF-compatible draft editor, or alternatively files can be imported from other design software along with supporting images and other information you may wish to be saved within the project file. Once complete, projects may be stored privately, shared within a closed group, or made public for other {utils.appName()} users to see. The choice is yours!</p>
function AboutModal({ open, onClose }) {
return (
<Modal open={open} onClose={e => onClose()}>
<Modal.Header>Welcome to {utils.appName()}!</Modal.Header>
<Modal.Content>
<h3>Introduction</h3>
<p>{utils.appName()} has been designed as a resource for weavers not only for those working alone as individuals, but also for groups who wish to share ideas, design inspirations and weaving patterns. It is ideal for those looking for a depository to store their individual work, and also for groups such as guilds, teaching groups, or any other collaborative working partnerships.</p>
<p>Projects can be created within {utils.appName()} using the integral WIF-compatible draft editor, or alternatively files can be imported from other design software along with supporting images and other information you may wish to be saved within the project file. Once complete, projects may be stored privately, shared within a closed group, or made public for other {utils.appName()} users to see. The choice is yours!</p>
<h3>Getting started</h3>
<p><strong>Creating a profile:</strong> You can add a picture, links to a personal website, and other social media accounts to tell others more about yourself.</p>
<p><strong>Creating a group:</strong> You have the option to do things alone, or create a group. By clicking on the Create a group button, you can name your group, and then invite members via email or directly through {utils.appName()} if they are existing {utils.appName()} users.</p>
<p><strong>Creating a new project:</strong> When you are ready to create/store a project on the system, you are invited to give the project a name, and a brief description. You will then be taken to a Welcome to your project screen, where if you click on add something, you have the option of creating a new weaving pattern directly inside {utils.appName()} or you can simply import a WIF file from your preferred weaving software. Once imported, you can perform further editing within {utils.appName()}, or you can add supporting picture files and any other additional information you wish to keep (eg weaving notes, yarn details etc).</p>
<p>Once complete you then have the option of saving the file privately, shared within a group, or made public for other {utils.appName()} users to see.</p>
<h3>Getting started</h3>
<p><strong>Creating a profile:</strong> You can add a picture, links to a personal website, and other social media accounts to tell others more about yourself.</p>
<p><strong>Creating a group:</strong> You have the option to do things alone, or create a group. By clicking on the Create a group button, you can name your group, and then invite members via email or directly through {utils.appName()} if they are existing {utils.appName()} users.</p>
<p><strong>Creating a new project:</strong> When you are ready to create/store a project on the system, you are invited to give the project a name, and a brief description. You will then be taken to a Welcome to your project screen, where if you click on add something, you have the option of creating a new weaving pattern directly inside {utils.appName()} or you can simply import a WIF file from your preferred weaving software. Once imported, you can perform further editing within {utils.appName()}, or you can add supporting picture files and any other additional information you wish to keep (eg weaving notes, yarn details etc).</p>
<p>Once complete you then have the option of saving the file privately, shared within a group, or made public for other {utils.appName()} users to see.</p>
<h3>Help and support</h3>
<p>The documentation provides useful information that might help you if you get stuck.</p>
<Button as='a' href='/docs' target='_blank' rel='noopener noreferrer'>View the docs</Button>
<h3>Help and support</h3>
<p>The documentation provides useful information that might help you if you get stuck.</p>
<Button as='a' href='/docs' target='_blank' rel='noopener noreferrer'>View the docs</Button>
<h3>We hope you enjoy using {utils.appName()}</h3>
<p>If you have any comments or feedback please tell us by emailing <a href={`mailTo:${import.meta.env.VITE_CONTACT_EMAIL}`}>{import.meta.env.VITE_CONTACT_EMAIL}</a>!</p>
</Modal.Content>
<Modal.Actions>
<Button onClick={e => dispatch(actions.app.openHelpModal(false))} color='teal' icon='check' content='OK' />
</Modal.Actions>
</Modal>
</Container>
</StyledNavBar>
);
}
<h3>We hope you enjoy using {utils.appName()}</h3>
<p>If you have any comments or feedback please tell us by emailing <a href={`mailTo:${import.meta.env.VITE_CONTACT_EMAIL}`}>{import.meta.env.VITE_CONTACT_EMAIL}</a>!</p>
</Modal.Content>
<Modal.Actions>
<Button onClick={e => onClose()} color='teal' icon='check' content='OK' />
</Modal.Actions>
</Modal>
);
}
export default NavBar;

View File

@ -1,25 +0,0 @@
import React from 'react';
import { Card } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import UserChip from './UserChip';
export default function PatternCard({ object, project, user }) {
if (!object) return null;
return (
<Card raised key={object._id} style={{ cursor: 'pointer' }} as={Link} to={`/${user?.username}/${project?.path}/${object._id}`}>
<div style={{ height: 200, backgroundImage: `url(${object.preview})`, backgroundSize: 'cover', backgroundPosition: 'top right', position: 'relative' }}>
{user &&
<div style={{position: 'absolute', top: 5, left: 5, padding: '3px 6px', background: 'rgba(250,250,250,0.8)', borderRadius: 5}}>
<UserChip user={user} />
</div>
}
</div>
<Card.Content>
<p style={{ wordBreak: 'break-all' }}>{object.name}</p>
{project?.path &&
<p style={{fontSize: 11, color: 'black'}}>{project.path}</p>
}
</Card.Content>
</Card>
);
}

View File

@ -1,12 +0,0 @@
import React from 'react';
import { Instagram, List } from 'react-content-loader';
import { Card } from 'semantic-ui-react';
export default function PatternLoader({ count, isCompact }) {
if (!count) count = 1;
return (<>
{[...new Array(count)].map((item, index) =>
<Card key={index}>{isCompact ? <Card.Content><List /></Card.Content> : <Instagram />}</Card>
)}
</>);
}

View File

@ -1,97 +0,0 @@
import React, { useEffect } from 'react';
import styled from 'styled-components';
import { Popup, Loader, Grid, List, Input } from 'semantic-ui-react';
import { useDispatch, useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import actions from '../../actions';
import api from '../../api';
import UserChip from './UserChip';
const StyledSearchBar = styled.div`
background-color:rgba(0,0,0,0.1);
padding:5px;
border: none;
border-radius:5px;
transition: background-color 0.5s;
&:focus-within{
background-color:rgba(250,250,250,0.5);
}
input::placeholder {
color: black !important;
}
`;
export default function SearchBar() {
const dispatch = useDispatch();
const { searchPopupOpen, searchTerm, searchResults, searching } = useSelector(state => {
const { searchPopupOpen, searchTerm, searchResults, searching } = state.app;
return { searchPopupOpen, searchTerm, searchResults, searching };
});
useEffect(() => {
dispatch(actions.app.openSearchPopup(false));
}, [dispatch]);
const search = () => {
dispatch(actions.app.updateSearching(true));
api.search.all(searchTerm, r => dispatch(actions.app.updateSearchResults(r)));
};
return (
<Popup basic on='focus' open={searchPopupOpen}
onOpen={e => dispatch(actions.app.openSearchPopup(true))} onClose={e => dispatch(actions.app.openSearchPopup(false))}
trigger={
<StyledSearchBar><Input transparent size='small' placeholder='Search...' icon='search' iconPosition='left' value={searchTerm} onChange={e => dispatch(actions.app.updateSearchTerm(e.target.value))} onKeyDown={e => e.keyCode === 13 && search()} /></StyledSearchBar>
}
content={<div style={{width: 300}} className='joyride-search'>
{!searchResults?.users && !searchResults?.groups ?
<small>
{searching
? <span><Loader size='tiny' inline active style={{marginRight: 10}}/> Searching...</span>
: <span>Type something and press enter to search</span>
}
</small>
: <>
{(!searchResults.users?.length && !searchResults?.groups?.length && !searchResults?.projects?.length) ?
<span><small>No results found</small></span>
:
<Grid stackable>
{searchResults?.users?.length > 0 &&
<Grid.Column width={6}>
{searchResults?.users?.map(u =>
<div style={{marginBottom: 5}}><UserChip user={u} key={u._id} /></div>
)}
</Grid.Column>
}
{(searchResults?.projects.length > 0 || searchResults.groups.length > 0) &&
<Grid.Column width={10}>
<List>
{searchResults?.projects?.map(p =>
<List.Item key={p._id}>
<List.Icon name='book' size='large' verticalAlign='middle' />
<List.Content>
<List.Header as={Link} to={'/' + p.fullName}>{p.name}</List.Header>
<List.Description><UserChip compact user={p.owner} /></List.Description>
</List.Content>
</List.Item>
)}
{searchResults?.groups?.map(g =>
<List.Item key={g._id}>
<List.Icon name='users' size='large' verticalAlign='middle' />
<List.Content>
<List.Header as={Link} to={`/groups/${g._id}`}>{g.name}</List.Header>
<List.Description><small>{g.closed ? <span><Icon name='lock' /> Closed group</span> : <span>Open group</span>}</small></List.Description>
</List.Content>
</List.Item>
)}
</List>
</Grid.Column>
}
</Grid>
}</>}
</div>}
/>
);
}

View File

@ -3,7 +3,6 @@ import { Helmet } from 'react-helmet';
import { Loader, Divider, Button, Message, Container, Segment, Grid, Card, Icon, List } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { BulletList } from 'react-content-loader'
import { toast } from 'react-toastify';
import actions from '../../actions';
import api from '../../api';
@ -12,19 +11,18 @@ import utils from '../../utils/utils.js';
import UserChip from '../includes/UserChip';
import HelpLink from '../includes/HelpLink';
import ProjectCard from '../includes/ProjectCard';
import PatternLoader from '../includes/PatternLoader';
import Tour from '../includes/Tour';
import DiscoverCard from '../includes/DiscoverCard';
function Home() {
const [runJoyride, setRunJoyride] = useState(false);
const dispatch = useDispatch();
const { user, projects, groups, invitations, loadingProjects, loadingGroups } = useSelector(state => {
const { user, projects, groups, invitations, loadingProjects } = useSelector(state => {
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
const invitations = state.invitations.invitations.filter(i => i.recipient === user?._id);
const projects = state.projects.projects.filter(p => p.user === user?._id);
return { user, projects, groups, invitations, loadingProjects: state.projects.loading, loadingGroups: state.groups.loading };
return { user, projects, groups, invitations, loadingProjects: state.projects.loading };
});
useEffect(() => {
@ -91,48 +89,41 @@ function Home() {
<DiscoverCard count={3} />
<Card fluid className='joyride-groups' style={{opacity: 0.8}}>
<Card.Content>
<Card.Header>Your groups</Card.Header>
{(groups && groups.length) ?
<Card fluid className='joyride-groups' style={{opacity: 0.8}}>
<Card.Content>
<Card.Header>Your groups</Card.Header>
{(loadingGroups && !groups?.length) ?
<div>
<BulletList />
<BulletList />
</div>
:
(groups?.length > 0 ?
<List relaxed>
{groups.map(g =>
<List.Item key={g._id}>
<List.Icon name='users' size='large' verticalAlign='middle' />
<List.Content>
<List.Header as={Link} to={`/groups/${g._id}`}>{g.name}</List.Header>
<List.Description>{utils.isGroupAdmin(user, g) ? 'Administrator' : 'Member'}</List.Description>
</List.Content>
</List.Item>
)}
</List>
:
<Card.Description>
Groups enable you to join or build communities of weavers and makers with similar interests.
</Card.Description>
)
}
<Divider hidden />
<Button className='joyride-createGroup' fluid size='small' icon='plus' content='Create a new group' as={Link} to='/groups/new' />
<HelpLink link={`/docs/groups`} text='Learn more about groups' marginTop/>
</Card.Content>
</Card>
<List relaxed>
{groups.map(g =>
<List.Item key={g._id}>
<List.Icon name='users' size='large' verticalAlign='middle' />
<List.Content>
<List.Header as={Link} to={`/groups/${g._id}`}>{g.name}</List.Header>
<List.Description>{utils.isGroupAdmin(user, g) ? 'Administrator' : 'Member'}</List.Description>
</List.Content>
</List.Item>
)}
</List>
<Button className='joyride-createGroup' fluid size='small' icon='plus' content='Create a new group' as={Link} to='/groups/new' />
<HelpLink link={`/docs/groups`} text='Learn more about groups' marginTop/>
</Card.Content>
</Card>
:
<Message>
<Message.Header>Groups</Message.Header>
<p>Groups enable you to build communities of weavers and makers with similar interests. Create one for your weaving group or class today.</p>
<Button className='joyride-createGroup' as={Link} to='/groups/new' size='small' color='purple' icon='plus' content='Create a group' />
</Message>
}
{(import.meta.env.VITE_PATREON_URL || import.meta.env.VITE_KOFI_URL) &&
<Card fluid style={{opacity: 0.8}}>
<Card.Content>
<Card.Header><span role="img" aria-label="Dancer">🕺</span> Support {utils.appName()}</Card.Header>
<Card.Description>{utils.appName()} is offered free of charge, but costs money to run and build. If you get value out of {utils.appName()} you may like to consider supporting it.</Card.Description>
<Divider hidden />
{import.meta.env.VITE_KOFI_URL &&
<Button size='small' fluid as='a' href={import.meta.env.VITE_KOFI_URL} target='_blank' rel='noopener noreferrer' className='umami--click--kofi-button'><span role='img' aria-label='Coffee' style={{marginRight: 5}}></span> Buy me a coffee</Button>
<Button style={{marginTop: 10}} size='small' fluid as='a' href={import.meta.env.VITE_KOFI_URL} target='_blank' rel='noopener noreferrer' className='umami--click--kofi-button'><span role='img' aria-label='Coffee' style={{marginRight: 5}}></span> Buy me a coffee</Button>
}
{import.meta.env.VITE_PATREON_URL &&
<Button style={{marginTop: 10}} size='small' fluid as='a' href={import.meta.env.VITE_PATREON_URL} target='_blank' rel='noopener noreferrer' className='umami--click--patreon-button'><span role='img' aria-label='Party' style={{marginRight: 5}}>🥳</span> Become a patron</Button>
@ -144,39 +135,42 @@ function Home() {
</Grid.Column>
<Grid.Column computer={11} className='joyride-projects'>
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
<h2><Icon name='book' /> Your projects</h2>
<div><Button className='joyride-createProject' as={Link} to="/projects/new" color='teal' content='Create a project' icon='plus' /></div>
</div>
<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/>
</p>
<Divider hidden />
{loadingProjects && !projects?.length &&
<Card.Group itemsPerRow={2} stackable>
<PatternLoader isCompact count={3} />
</Card.Group>
}
{user && !loadingProjects && !projects?.length &&
{loadingProjects && !projects.length &&
<div style={{textAlign: 'center'}}>
<Segment placeholder textAlign='center'>
<h3>On {utils.appName()}, your patterns and files are stored in <strong><span role="img" aria-label="box">📦</span> projects</strong></h3>
<p>Projects can contain anything: from rough ideas or design experiments through to commissions and exhibitions. Treat them as if they were just <span role="img" aria-label="folder">📁</span> folders on your computer.</p>
<Divider section hidden />
<h4>Start by creating your first project. You can keep it private if you prefer.</h4>
<Button className='joyride-createProject' as={Link} to="/projects/new" color="teal" icon="plus" content="Create a project" />
</Segment>
<h4>Loading your projects...</h4>
<Loader active inline="centered" />
</div>
}
{projects?.length > 0 &&
{user && !loadingProjects && (!projects || !projects.length) &&
<div style={{textAlign: 'center'}}>
<h1>
<span role="img" aria-label="chequered flag">🚀</span> Let's get started
</h1>
<Divider hidden/>
<Segment placeholder textAlign='center'>
<h3>On {utils.appName()}, your patterns and files are stored in <strong><span role="img" aria-label="box">📦</span> projects</strong></h3>
<p>Projects can contain anything: from rough ideas or design experiments through to commissions and exhibitions. Treat them as if they were just <span role="img" aria-label="folder">📁</span> folders on your computer.</p>
<p><HelpLink className='joyride-help' link={`/docs/projects`} text='Learn more about projects' marginTop/></p>
<Divider />
<h4>Start by creating a new project. Don't worry, you can keep it private.</h4>
<Button className='joyride-createProject' as={Link} to="/projects/new" color="teal" icon="plus" content="Create a project" />
</Segment>
</div>
}
{projects && projects.length > 0 &&
<div>
<Button className='joyride-createProject' as={Link} to="/projects/new" color='teal' content='Create a project' icon='plus' floated='right'/>
<h2><Icon name='book' /> Your projects</h2>
<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/>
</p>
<Divider clearing hidden />
<Card.Group itemsPerRow={2} stackable>
{projects.map(proj => (
{projects && projects.map(proj => (
<ProjectCard key={proj._id} project={proj} />
))}
</Card.Group>

View File

@ -1,13 +1,13 @@
import React, { useState, useEffect } from 'react';
import { Container, Card, Grid, Button } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import actions from '../../../actions';
import api from '../../../api';
import utils from '../../../utils/utils.js';
import UserChip from '../../includes/UserChip';
import DiscoverCard from '../../includes/DiscoverCard';
import PatternCard from '../../includes/PatternCard';
import PatternLoader from '../../includes/PatternLoader';
import DraftPreview from '../projects/objects/DraftPreview';
export default function Explore() {
@ -18,9 +18,9 @@ export default function Explore() {
return { objects: state.objects.exploreObjects, page: state.objects.explorePage };
});
/*useEffect(() => {
useEffect(() => {
if (page < 2) loadMoreExplore();
}, []);*/
}, []);
function loadMoreExplore() {
setLoading(true);
@ -41,14 +41,25 @@ export default function Explore() {
<Card.Group stackable doubling itemsPerRow={3} style={{marginTop: 30}}>
{objects?.filter(o => o.projectObject && o.userObject).map(object =>
<PatternCard key={object._id} object={object} project={object.projectObject} user={object.userObject} />
<Card raised key={object._id} style={{ cursor: 'pointer' }} as={Link} to={`/${object.userObject?.username}/${object.projectObject?.path}/${object._id}`}>
<div style={{ height: 200, backgroundImage: `url(${object.preview})`, backgroundSize: 'cover', backgroundPosition: 'top right', position: 'relative' }}>
{object.userObject &&
<div style={{position: 'absolute', top: 5, left: 5, padding: 3, background: 'rgba(250,250,250,0.5)', borderRadius: 5}}>
<UserChip user={object.userObject} />
</div>
}
</div>
<Card.Content>
<p style={{ wordBreak: 'break-all' }}>{object.name}</p>
{object.projectObject?.path &&
<p style={{fontSize: 11, color: 'black'}}>{object.projectObject.path}</p>
}
</Card.Content>
</Card>
)}
{objects?.length === 0 && <>
<PatternLoader count={6} />
</>}
</Card.Group>
<div style={{display: 'flex', justifyContent: 'center', marginTop: 30}}>
<Button loading={loading} onClick={loadMoreExplore}>View more</Button>
<Button loading={loading} onClick={loadMoreExplore}>Load more</Button>
</div>
</Grid.Column>
</Grid>

View File

@ -137,11 +137,10 @@ function ObjectViewer() {
</Dropdown>
{user &&
<Dropdown direction='left' icon={null} trigger={<Button size="small" icon="copy" secondary content="Copy to.." />}>
<Dropdown icon={null} trigger={<Button size="small" icon="copy" secondary content="Copy to.." />}>
<Dropdown.Menu>
<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?.length === 0 && <Dropdown.Item>You don't have any projects.</Dropdown.Item>}
</Dropdown.Menu>
</Dropdown>
}

View File

@ -1,13 +1,9 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { Divider, Grid, Button, Container, Card } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { Divider, Grid, Button, Container } from 'semantic-ui-react';
import styled from 'styled-components';
import utils from '../../utils/utils.js';
import PatternCard from '../includes/PatternCard';
import PatternLoader from '../includes/PatternLoader';
import projectImage from '../../images/project.png';
import toolsImage from '../../images/tools.png';
import filesImage from '../../images/files.png';
@ -15,14 +11,10 @@ import filesImage from '../../images/files.png';
const StyledHero = styled.div`
background: linen;
min-height: 200px;
padding-top: 50px;
margin-top: 10px;
`;
function MarketingHome({ onRegisterClicked }) {
const { objects } = useSelector(state => {
return { objects: state.objects.exploreObjects };
});
return (
<div>
<Helmet title='Home' />
@ -63,25 +55,6 @@ function MarketingHome({ onRegisterClicked }) {
</Container>
</StyledHero>
<div style={{paddingTop: 75, marginBottom: 75, background: 'linear-gradient(linen, rgb(255,251,248)'}}>
<Container>
<div style={{display: 'flex', justifyContent:'space-between', alignItems: 'center'}}>
<h2>See what people have been creating</h2>
<Button size='large' as={Link} to='/explore' primary>Explore {utils.appName()}</Button>
</div>
<Card.Group stackable doubling itemsPerRow={3} style={{marginTop: 30}}>
{objects?.length > 0 &&
(objects?.filter(o => o.projectObject && o.userObject).slice(0, 2).map(object =>
<PatternCard object={object} project={object.projectObject} user={object.userObject} />
))
}
{objects?.length === 0 && <>
<PatternLoader count={3} />
</>}
</Card.Group>
</Container>
</div>
<Container style={{ marginTop: 50 }}>
<Grid stackable centered>
<Grid.Column computer={6} textAlign="right">

View File

@ -2881,15 +2881,6 @@ __metadata:
languageName: node
linkType: hard
"react-content-loader@npm:^6.2.1":
version: 6.2.1
resolution: "react-content-loader@npm:6.2.1"
peerDependencies:
react: ">=16.0.0"
checksum: f777d408256a4218677e47f4cf3988d9fd8e556450e9b85ee1eb3952a5d5802573cea0df5eaf4dbc936c9522f355657de6f8ab0ecdf035d7dccdef15b45c9dae
languageName: node
linkType: hard
"react-dom@npm:^18.2.0":
version: 18.2.0
resolution: "react-dom@npm:18.2.0"
@ -3573,7 +3564,6 @@ __metadata:
react-app-polyfill: ^0.2.0
react-color: ^2.19.3
react-confirm: ^0.1.18
react-content-loader: ^6.2.1
react-dom: ^18.2.0
react-helmet: ^6.0.0
react-joyride: ^2.4.0