Compare commits

...

7 Commits

Author SHA1 Message Date
958edd7556 Small fix tweaks
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-05-22 20:17:03 +00:00
2d680d7142 Navbar compatible search box 2023-05-22 19:11:42 +00:00
ecbfb2d9ca Component-ised search bar 2023-05-22 17:55:03 +00:00
ec38525a43 Improved loading throughout 2023-05-22 17:26:29 +00:00
d90b40d6f2 Add a PatternLoader component 2023-05-22 16:48:06 +00:00
edd9c1e3ee Use loading indicators on discover card 2023-05-22 16:38:23 +00:00
f701bc46d8 Explore area from marketing homepage 2023-05-21 22:26:05 +00:00
14 changed files with 353 additions and 279 deletions

Binary file not shown.

View File

@ -17,6 +17,7 @@
"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,4 +1,6 @@
import api from '.';
import actions from '../actions';
import { store } from '..';
export const groups = {
create(data, success, fail) {
@ -8,6 +10,7 @@ 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,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import { Card, List } from 'semantic-ui-react';
import { Card, List, Dimmer } 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';
@ -8,23 +9,25 @@ 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>
{highlightProjects?.length > 0 && <>
<h4>Discover a project</h4>
{loading && <BulletList />}
{highlightProjects?.length > 0 && <>
<List relaxed>
{highlightProjects.map(p =>
{highlightProjects?.map(p =>
<List.Item key={p._id}>
<List.Icon name='book' size='large' verticalAlign='middle' />
<List.Content>
@ -35,10 +38,11 @@ export default function ExploreCard({ count }) {
</List>
</>}
{highlightUsers?.length > 0 && <>
<h4>Find others on {utils.appName()}</h4>
{loading && <BulletList />}
{highlightUsers?.length > 0 && <>
<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, { useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import React from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { Loader, List, Popup, Modal, Grid, Icon, Button, Container, Dropdown } from 'semantic-ui-react';
import { Modal, Menu, Button, Container, Dropdown } from 'semantic-ui-react';
import api from '../../api';
import actions from '../../actions';
import utils from '../../utils/utils.js';
@ -10,6 +10,7 @@ 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;
@ -18,12 +19,10 @@ const StyledNavBar = styled.div`
.logo{
height:40px;
margin-top:5px;
}
.nav-links{
display: flex;
align-items: center;
.ui.button{
margin-right: 8px;
margin-right: 50px;
transition: opacity 0.3s;
&:hover{
opacity: 0.5;
}
}
.only-mobile{
@ -33,56 +32,23 @@ const StyledNavBar = styled.div`
}
.above-mobile{
@media only screen and (max-width: 767px) {
display:none;
display:none !important;
}
}
`;
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() {
export default function NavBar() {
const navigate = useNavigate();
const location = useLocation();
const dispatch = useDispatch();
const { isAuthenticated, user, groups, helpModalOpen, searchPopupOpen, searchTerm, searchResults, searching } = useSelector(state => {
const { isAuthenticated, user, groups, helpModalOpen } = useSelector(state => {
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
const groups = state.groups.groups.filter(g => utils.isInGroup(user, g._id));
const { isAuthenticated } = state.auth;
const { helpModalOpen, searchPopupOpen, searchTerm, searchResults, searching } = state.app;
return { isAuthenticated, user, groups, helpModalOpen, searchPopupOpen, searchTerm, searchResults, searching };
const { helpModalOpen } = state.app;
return { isAuthenticated, user, groups, helpModalOpen };
});
const navigate = useNavigate();
useEffect(() => {
dispatch(actions.app.openSearchPopup(false));
}, [dispatch]);
const logout = () => api.auth.logout(() => {
dispatch(actions.auth.logout());
dispatch(actions.users.syncDrift(false))
@ -90,77 +56,17 @@ 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>
{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'/>}
<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>}
>
<Dropdown.Menu>
<Dropdown.Header icon='users' content='Your groups' />
@ -171,14 +77,12 @@ function NavBar() {
<Dropdown.Item as={Link} to='/groups/new' icon='plus' content='Create a new group' />
</Dropdown.Menu>
</Dropdown>
</span>
}
</Menu.Item>
<span className='above-mobile'>
<Button size='small' icon='help' basic onClick={e => dispatch(actions.app.openHelpModal(true))}/>
</span>
<Dropdown direction="left" pointing="top right" icon={null} style={{marginLeft: 10, marginTop: 5}}
<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 }}>
@ -198,33 +102,32 @@ function NavBar() {
<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>
</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())}>
</>}
{!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>
);
}
<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.Content>
<h3>Introduction</h3>
@ -245,12 +148,8 @@ 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>
</Modal.Content>
<Modal.Actions>
<Button onClick={e => dispatch(actions.app.openHelpModal(false))} color='teal' icon='check' content='OK' />
<Button onClick={e => onClose()} color='teal' icon='check' content='OK' />
</Modal.Actions>
</Modal>
</Container>
</StyledNavBar>
);
}
export default NavBar;
}

View File

@ -0,0 +1,25 @@
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

@ -0,0 +1,12 @@
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

@ -0,0 +1,97 @@
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,6 +3,7 @@ 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';
@ -11,18 +12,19 @@ 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 } = useSelector(state => {
const { user, projects, groups, invitations, loadingProjects, loadingGroups } = 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 };
return { user, projects, groups, invitations, loadingProjects: state.projects.loading, loadingGroups: state.groups.loading };
});
useEffect(() => {
@ -89,11 +91,17 @@ function Home() {
<DiscoverCard count={3} />
{(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}>
@ -105,25 +113,26 @@ function Home() {
</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>
:
<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 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>
<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>
}
{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>
@ -135,42 +144,39 @@ function Home() {
</Grid.Column>
<Grid.Column computer={11} className='joyride-projects'>
{loadingProjects && !projects.length &&
<div style={{textAlign: 'center'}}>
<h4>Loading your projects...</h4>
<Loader active inline="centered" />
</div>
}
{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'/>
<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 clearing hidden />
<Divider hidden />
{loadingProjects && !projects?.length &&
<Card.Group itemsPerRow={2} stackable>
{projects && projects.map(proj => (
<PatternLoader isCompact count={3} />
</Card.Group>
}
{user && !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>
</div>
}
{projects?.length > 0 &&
<div>
<Card.Group itemsPerRow={2} stackable>
{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,25 +41,14 @@ export default function Explore() {
<Card.Group stackable doubling itemsPerRow={3} style={{marginTop: 30}}>
{objects?.filter(o => o.projectObject && o.userObject).map(object =>
<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>
<PatternCard key={object._id} object={object} project={object.projectObject} user={object.userObject} />
)}
{objects?.length === 0 && <>
<PatternLoader count={6} />
</>}
</Card.Group>
<div style={{display: 'flex', justifyContent: 'center', marginTop: 30}}>
<Button loading={loading} onClick={loadMoreExplore}>Load more</Button>
<Button loading={loading} onClick={loadMoreExplore}>View more</Button>
</div>
</Grid.Column>
</Grid>

View File

@ -137,10 +137,11 @@ function ObjectViewer() {
</Dropdown>
{user &&
<Dropdown icon={null} trigger={<Button size="small" icon="copy" secondary content="Copy to.." />}>
<Dropdown direction='left' 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,9 +1,13 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { Divider, Grid, Button, Container } from 'semantic-ui-react';
import { Divider, Grid, Button, Container, Card } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { useSelector } from 'react-redux';
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';
@ -11,10 +15,14 @@ import filesImage from '../../images/files.png';
const StyledHero = styled.div`
background: linen;
min-height: 200px;
margin-top: 10px;
padding-top: 50px;
`;
function MarketingHome({ onRegisterClicked }) {
const { objects } = useSelector(state => {
return { objects: state.objects.exploreObjects };
});
return (
<div>
<Helmet title='Home' />
@ -55,6 +63,25 @@ 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,6 +2881,15 @@ __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"
@ -3564,6 +3573,7 @@ __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