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-app-polyfill": "^0.2.0",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-confirm": "^0.1.18", "react-confirm": "^0.1.18",
"react-content-loader": "^6.2.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-helmet": "^6.0.0", "react-helmet": "^6.0.0",
"react-joyride": "^2.4.0", "react-joyride": "^2.4.0",

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import React from 'react'; import React, { useEffect } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components'; 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 api from '../../api';
import actions from '../../actions'; import actions from '../../actions';
import utils from '../../utils/utils.js'; import utils from '../../utils/utils.js';
@ -10,7 +10,6 @@ import utils from '../../utils/utils.js';
import logo from '../../images/logo/main.png'; import logo from '../../images/logo/main.png';
import UserChip from './UserChip'; import UserChip from './UserChip';
import SupporterBadge from './SupporterBadge'; import SupporterBadge from './SupporterBadge';
import SearchBar from './SearchBar';
const StyledNavBar = styled.div` const StyledNavBar = styled.div`
height:60px; height:60px;
@ -19,10 +18,12 @@ const StyledNavBar = styled.div`
.logo{ .logo{
height:40px; height:40px;
margin-top:5px; margin-top:5px;
margin-right: 50px; }
transition: opacity 0.3s; .nav-links{
&:hover{ display: flex;
opacity: 0.5; align-items: center;
.ui.button{
margin-right: 8px;
} }
} }
.only-mobile{ .only-mobile{
@ -32,23 +33,56 @@ const StyledNavBar = styled.div`
} }
.above-mobile{ .above-mobile{
@media only screen and (max-width: 767px) { @media only screen and (max-width: 767px) {
display:none !important; display:none;
} }
} }
`; `;
export default function NavBar() { const SearchBar = styled.div`
const navigate = useNavigate(); background-color:rgba(0,0,0,0.1);
const location = useLocation(); 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 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 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 groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
const { isAuthenticated } = state.auth; const { isAuthenticated } = state.auth;
const { helpModalOpen } = state.app; const { helpModalOpen, searchPopupOpen, searchTerm, searchResults, searching } = state.app;
return { isAuthenticated, user, groups, helpModalOpen }; return { isAuthenticated, user, groups, helpModalOpen, searchPopupOpen, searchTerm, searchResults, searching };
}); });
const navigate = useNavigate();
useEffect(() => {
dispatch(actions.app.openSearchPopup(false));
}, [dispatch]);
const logout = () => api.auth.logout(() => { const logout = () => api.auth.logout(() => {
dispatch(actions.auth.logout()); dispatch(actions.auth.logout());
dispatch(actions.users.syncDrift(false)) dispatch(actions.users.syncDrift(false))
@ -56,17 +90,77 @@ export default function NavBar() {
navigate('/'); navigate('/');
}); });
const search = () => {
dispatch(actions.app.updateSearching(true));
api.search.all(searchTerm, r => dispatch(actions.app.updateSearchResults(r)));
};
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="/"><img alt={`${utils.appName()} logo`} src={logo} className="logo" /></Link>
<div style={{flex: 1}}> {isAuthenticated
<Menu secondary> ? (
<Menu.Item className='above-mobile' as={Link} to='/' name='home' active={location.pathname === '/'} /> <div className='nav-links'>
<Menu.Item className='above-mobile' as={Link} to='/explore' name='explore' active={location.pathname === '/explore'} /> <Popup basic on='focus' open={searchPopupOpen}
<Menu.Item className='above-mobile' active={location.pathname.startsWith('/groups')} name='Groups'> onOpen={e => dispatch(actions.app.openSearchPopup(true))} onClose={e => dispatch(actions.app.openSearchPopup(false))}
<Dropdown pointing='top left' 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>}
trigger={<span>Groups</span>} 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.Menu>
<Dropdown.Header icon='users' content='Your groups' /> <Dropdown.Header icon='users' content='Your groups' />
@ -77,12 +171,14 @@ export default function NavBar() {
<Dropdown.Item as={Link} to='/groups/new' icon='plus' content='Create a new group' /> <Dropdown.Item as={Link} to='/groups/new' icon='plus' content='Create a new group' />
</Dropdown.Menu> </Dropdown.Menu>
</Dropdown> </Dropdown>
</Menu.Item> </span>
}
<Menu.Menu position='right'> <span className='above-mobile'>
{isAuthenticated && <> <Button size='small' icon='help' basic onClick={e => dispatch(actions.app.openHelpModal(true))}/>
<Menu.Item className='above-mobile'><SearchBar /></Menu.Item> </span>
<Dropdown direction="left" pointing="top right" icon={null} style={{ marginTop: 10}}
<Dropdown direction="left" pointing="top right" icon={null} style={{marginLeft: 10, marginTop: 5}}
trigger={<UserChip user={user} withoutLink avatarOnly />} trigger={<UserChip user={user} withoutLink avatarOnly />}
> >
<Dropdown.Menu style={{ minWidth: '200px', paddingTop: 10 }}> <Dropdown.Menu style={{ minWidth: '200px', paddingTop: 10 }}>
@ -102,32 +198,33 @@ export default function NavBar() {
<Dropdown.Item as={Link} to='/root'>Root</Dropdown.Item> <Dropdown.Item as={Link} to='/root'>Root</Dropdown.Item>
} }
<Dropdown.Item as={Link} to='/docs'>Help</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.Item onClick={logout}>Logout</Dropdown.Item>
</Dropdown.Menu> </Dropdown.Menu>
</Dropdown> </Dropdown>
</>} </div>
)
{!isAuthenticated && <> : (
<Menu.Item name='Login' onClick={() => dispatch(actions.auth.openLogin())} /> <div className='nav-links'>
<Menu.Item> <span className="only-mobile">
<Button size='small' color="teal" onClick={() => dispatch(actions.auth.openRegister())}> <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 <span role="img" aria-label="wave">👋</span> Sign-up
</Button> </Button>
</Menu.Item>
</>}
</Menu.Menu>
</Menu>
</div> </div>
</Container> )
<AboutModal open={helpModalOpen} onClose={e => dispatch(actions.app.openHelpModal(false))} />
</StyledNavBar>
);
} }
<Modal open={helpModalOpen} onClose={e => dispatch(actions.app.openHelpModal(false))}>
function AboutModal({ open, onClose }) {
return (
<Modal open={open} onClose={e => onClose()}>
<Modal.Header>Welcome to {utils.appName()}!</Modal.Header> <Modal.Header>Welcome to {utils.appName()}!</Modal.Header>
<Modal.Content> <Modal.Content>
<h3>Introduction</h3> <h3>Introduction</h3>
@ -148,8 +245,12 @@ export default function NavBar() {
<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> <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.Content>
<Modal.Actions> <Modal.Actions>
<Button onClick={e => onClose()} color='teal' icon='check' content='OK' /> <Button onClick={e => dispatch(actions.app.openHelpModal(false))} color='teal' icon='check' content='OK' />
</Modal.Actions> </Modal.Actions>
</Modal> </Modal>
</Container>
</StyledNavBar>
); );
} }
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 { Loader, Divider, Button, Message, Container, Segment, Grid, Card, Icon, List } from 'semantic-ui-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { BulletList } from 'react-content-loader'
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import actions from '../../actions'; import actions from '../../actions';
import api from '../../api'; import api from '../../api';
@ -12,19 +11,18 @@ import utils from '../../utils/utils.js';
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';
import PatternLoader from '../includes/PatternLoader';
import Tour from '../includes/Tour'; import Tour from '../includes/Tour';
import DiscoverCard from '../includes/DiscoverCard'; import DiscoverCard from '../includes/DiscoverCard';
function Home() { function Home() {
const [runJoyride, setRunJoyride] = useState(false); const [runJoyride, setRunJoyride] = useState(false);
const dispatch = useDispatch(); 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 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 groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
const invitations = state.invitations.invitations.filter(i => i.recipient === user?._id); const invitations = state.invitations.invitations.filter(i => i.recipient === user?._id);
const projects = state.projects.projects.filter(p => p.user === 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(() => { useEffect(() => {
@ -91,17 +89,11 @@ function Home() {
<DiscoverCard count={3} /> <DiscoverCard count={3} />
{(groups && groups.length) ?
<Card fluid className='joyride-groups' style={{opacity: 0.8}}> <Card fluid className='joyride-groups' style={{opacity: 0.8}}>
<Card.Content> <Card.Content>
<Card.Header>Your groups</Card.Header> <Card.Header>Your groups</Card.Header>
{(loadingGroups && !groups?.length) ?
<div>
<BulletList />
<BulletList />
</div>
:
(groups?.length > 0 ?
<List relaxed> <List relaxed>
{groups.map(g => {groups.map(g =>
<List.Item key={g._id}> <List.Item key={g._id}>
@ -113,26 +105,25 @@ function Home() {
</List.Item> </List.Item>
)} )}
</List> </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' /> <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/> <HelpLink link={`/docs/groups`} text='Learn more about groups' marginTop/>
</Card.Content> </Card.Content>
</Card> </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) && {(import.meta.env.VITE_PATREON_URL || import.meta.env.VITE_KOFI_URL) &&
<Card fluid style={{opacity: 0.8}}> <Card fluid style={{opacity: 0.8}}>
<Card.Content> <Card.Content>
<Card.Header><span role="img" aria-label="Dancer">🕺</span> Support {utils.appName()}</Card.Header> <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> <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 && {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 && {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> <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>
<Grid.Column computer={11} className='joyride-projects'> <Grid.Column computer={11} className='joyride-projects'>
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}> {loadingProjects && !projects.length &&
<h2><Icon name='book' /> Your projects</h2> <div style={{textAlign: 'center'}}>
<div><Button className='joyride-createProject' as={Link} to="/projects/new" color='teal' content='Create a project' icon='plus' /></div> <h4>Loading your projects...</h4>
<Loader active inline="centered" />
</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 && {user && !loadingProjects && (!projects || !projects.length) &&
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<h1>
<span role="img" aria-label="chequered flag">🚀</span> Let's get started
</h1>
<Divider hidden/>
<Segment placeholder 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> <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>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 /> <p><HelpLink className='joyride-help' link={`/docs/projects`} text='Learn more about projects' marginTop/></p>
<h4>Start by creating your first project. You can keep it private if you prefer.</h4> <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" /> <Button className='joyride-createProject' as={Link} to="/projects/new" color="teal" icon="plus" content="Create a project" />
</Segment> </Segment>
</div> </div>
} }
{projects?.length > 0 && {projects && projects.length > 0 &&
<div> <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> <Card.Group itemsPerRow={2} stackable>
{projects.map(proj => ( {projects && projects.map(proj => (
<ProjectCard key={proj._id} project={proj} /> <ProjectCard key={proj._id} project={proj} />
))} ))}
</Card.Group> </Card.Group>

View File

@ -1,13 +1,13 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Container, Card, Grid, Button } from 'semantic-ui-react'; import { Container, Card, Grid, Button } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import actions from '../../../actions'; import actions from '../../../actions';
import api from '../../../api'; import api from '../../../api';
import utils from '../../../utils/utils.js'; import utils from '../../../utils/utils.js';
import UserChip from '../../includes/UserChip';
import DiscoverCard from '../../includes/DiscoverCard'; import DiscoverCard from '../../includes/DiscoverCard';
import PatternCard from '../../includes/PatternCard';
import PatternLoader from '../../includes/PatternLoader';
import DraftPreview from '../projects/objects/DraftPreview'; import DraftPreview from '../projects/objects/DraftPreview';
export default function Explore() { export default function Explore() {
@ -18,9 +18,9 @@ export default function Explore() {
return { objects: state.objects.exploreObjects, page: state.objects.explorePage }; return { objects: state.objects.exploreObjects, page: state.objects.explorePage };
}); });
/*useEffect(() => { useEffect(() => {
if (page < 2) loadMoreExplore(); if (page < 2) loadMoreExplore();
}, []);*/ }, []);
function loadMoreExplore() { function loadMoreExplore() {
setLoading(true); setLoading(true);
@ -41,14 +41,25 @@ export default function Explore() {
<Card.Group stackable doubling itemsPerRow={3} style={{marginTop: 30}}> <Card.Group stackable doubling itemsPerRow={3} style={{marginTop: 30}}>
{objects?.filter(o => o.projectObject && o.userObject).map(object => {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> </Card.Group>
<div style={{display: 'flex', justifyContent: 'center', marginTop: 30}}> <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> </div>
</Grid.Column> </Grid.Column>
</Grid> </Grid>

View File

@ -137,11 +137,10 @@ function ObjectViewer() {
</Dropdown> </Dropdown>
{user && {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.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)} />)}
{myProjects?.length === 0 && <Dropdown.Item>You don't have any projects.</Dropdown.Item>}
</Dropdown.Menu> </Dropdown.Menu>
</Dropdown> </Dropdown>
} }

View File

@ -1,13 +1,9 @@
import React from 'react'; import React from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Divider, Grid, Button, Container, Card } from 'semantic-ui-react'; import { Divider, Grid, Button, Container } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { useSelector } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import utils from '../../utils/utils.js'; import utils from '../../utils/utils.js';
import PatternCard from '../includes/PatternCard';
import PatternLoader from '../includes/PatternLoader';
import projectImage from '../../images/project.png'; import projectImage from '../../images/project.png';
import toolsImage from '../../images/tools.png'; import toolsImage from '../../images/tools.png';
import filesImage from '../../images/files.png'; import filesImage from '../../images/files.png';
@ -15,14 +11,10 @@ import filesImage from '../../images/files.png';
const StyledHero = styled.div` const StyledHero = styled.div`
background: linen; background: linen;
min-height: 200px; min-height: 200px;
padding-top: 50px; margin-top: 10px;
`; `;
function MarketingHome({ onRegisterClicked }) { function MarketingHome({ onRegisterClicked }) {
const { objects } = useSelector(state => {
return { objects: state.objects.exploreObjects };
});
return ( return (
<div> <div>
<Helmet title='Home' /> <Helmet title='Home' />
@ -63,25 +55,6 @@ function MarketingHome({ onRegisterClicked }) {
</Container> </Container>
</StyledHero> </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 }}> <Container style={{ marginTop: 50 }}>
<Grid stackable centered> <Grid stackable centered>
<Grid.Column computer={6} textAlign="right"> <Grid.Column computer={6} textAlign="right">

View File

@ -2881,15 +2881,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "react-dom@npm:^18.2.0":
version: 18.2.0 version: 18.2.0
resolution: "react-dom@npm:18.2.0" resolution: "react-dom@npm:18.2.0"
@ -3573,7 +3564,6 @@ __metadata:
react-app-polyfill: ^0.2.0 react-app-polyfill: ^0.2.0
react-color: ^2.19.3 react-color: ^2.19.3
react-confirm: ^0.1.18 react-confirm: ^0.1.18
react-content-loader: ^6.2.1
react-dom: ^18.2.0 react-dom: ^18.2.0
react-helmet: ^6.0.0 react-helmet: ^6.0.0
react-joyride: ^2.4.0 react-joyride: ^2.4.0