Compare commits
7 Commits
9e9491e064
...
958edd7556
Author | SHA1 | Date | |
---|---|---|---|
958edd7556 | |||
2d680d7142 | |||
ecbfb2d9ca | |||
ec38525a43 | |||
d90b40d6f2 | |||
edd9c1e3ee | |||
f701bc46d8 |
BIN
web/.yarn/cache/react-content-loader-npm-6.2.1-1d54ed51d4-f777d40825.zip
vendored
Normal file
BIN
web/.yarn/cache/react-content-loader-npm-6.2.1-1d54ed51d4-f777d40825.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
@ -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",
|
||||
|
@ -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) {
|
||||
|
@ -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'/>
|
||||
|
@ -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;
|
||||
|
25
web/src/components/includes/PatternCard.jsx
Normal file
25
web/src/components/includes/PatternCard.jsx
Normal 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>
|
||||
);
|
||||
}
|
12
web/src/components/includes/PatternLoader.jsx
Normal file
12
web/src/components/includes/PatternLoader.jsx
Normal 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>
|
||||
)}
|
||||
</>);
|
||||
}
|
97
web/src/components/includes/SearchBar.jsx
Normal file
97
web/src/components/includes/SearchBar.jsx
Normal 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>}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user