Compare commits
3 Commits
447f76e807
...
9e9491e064
Author | SHA1 | Date | |
---|---|---|---|
9e9491e064 | |||
a6de05a0ca | |||
d4f56345c6 |
@ -42,15 +42,15 @@ def users(user, params):
|
||||
u['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(u['_id'], u['avatar']))
|
||||
return {'users': users}
|
||||
|
||||
def discover(user):
|
||||
if not user: raise util.errors.Forbidden('You need to be logged in')
|
||||
|
||||
def discover(user, count = 3):
|
||||
db = database.get_db()
|
||||
projects = []
|
||||
users = []
|
||||
count = 3
|
||||
|
||||
all_projects = list(db.projects.find({'name': {'$not': re.compile('my new project', re.IGNORECASE)}, 'visibility': 'public', 'user': {'$ne': user['_id']}}, {'name': 1, 'path': 1, 'user': 1}))
|
||||
all_projects_query = {'name': {'$not': re.compile('my new project', re.IGNORECASE)}, 'visibility': 'public'}
|
||||
if user and user.get('_id'):
|
||||
all_projects_query['user'] = {'$ne': user['_id']}
|
||||
all_projects = list(db.projects.find(all_projects_query, {'name': 1, 'path': 1, 'user': 1}))
|
||||
random.shuffle(all_projects)
|
||||
for p in all_projects:
|
||||
if db.objects.find_one({'project': p['_id'], 'name': {'$ne': 'Untitled pattern'}}):
|
||||
@ -60,7 +60,10 @@ def discover(user):
|
||||
if len(projects) >= count: break
|
||||
|
||||
interest_fields = ['bio', 'avatar', 'website', 'facebook', 'twitter', 'instagram', 'location']
|
||||
all_users = list(db.users.find({'_id': {'$ne': user['_id']}, '$or': list(map(lambda f: {f: {'$exists': True}}, interest_fields))}, {'username': 1, 'avatar': 1, 'isSilverSupporter': 1, 'isGoldSupporter': 1}))
|
||||
all_users_query = {'$or': list(map(lambda f: {f: {'$exists': True}}, interest_fields))}
|
||||
if user and user.get('_id'):
|
||||
all_users_query['_id'] = {'$ne': user['_id']}
|
||||
all_users = list(db.users.find(all_users_query, {'username': 1, 'avatar': 1, 'isSilverSupporter': 1, 'isGoldSupporter': 1}))
|
||||
random.shuffle(all_users)
|
||||
for u in all_users:
|
||||
if 'avatar' in u:
|
||||
@ -73,16 +76,17 @@ def discover(user):
|
||||
'highlightUsers': users,
|
||||
}
|
||||
|
||||
def explore():
|
||||
def explore(page = 1):
|
||||
db = database.get_db()
|
||||
per_page = 10
|
||||
|
||||
project_map = {}
|
||||
user_map = {}
|
||||
|
||||
all_public_projects = list(db.projects.find({'name': {'$not': re.compile('my new project', re.IGNORECASE)}, 'visibility': 'public'}, {'name': 1, 'path': 1, 'user': 1}))
|
||||
all_public_project_ids = list(map(lambda p: p['_id'], all_public_projects))
|
||||
for project in all_public_projects:
|
||||
project_map[project['_id']] = project
|
||||
objects = list(db.objects.find({'project': {'$in': all_public_project_ids}, 'name': {'$not': re.compile('untitled pattern', re.IGNORECASE)}, 'preview': {'$exists': True}}, {'project': 1, 'name': 1, 'createdAt': 1, 'preview': 1}).sort('createdAt', pymongo.DESCENDING).limit(20))
|
||||
objects = list(db.objects.find({'project': {'$in': all_public_project_ids}, 'name': {'$not': re.compile('untitled pattern', re.IGNORECASE)}, 'preview': {'$exists': True}}, {'project': 1, 'name': 1, 'createdAt': 1, 'preview': 1}).sort('createdAt', pymongo.DESCENDING).skip((page - 1) * per_page).limit(per_page))
|
||||
for object in objects:
|
||||
object['projectObject'] = project_map.get(object['project'])
|
||||
authors = list(db.users.find({'_id': {'$in': list(map(lambda o: o.get('projectObject', {}).get('user'), objects))}}, {'username': 1, 'avatar': 1}))
|
||||
@ -92,5 +96,6 @@ def explore():
|
||||
user_map[a['_id']] = a
|
||||
for object in objects:
|
||||
object['userObject'] = user_map.get(object.get('projectObject', {}).get('user'))
|
||||
|
||||
return {'objects': objects}
|
||||
|
@ -265,11 +265,15 @@ def search_users():
|
||||
|
||||
@app.route('/search/discover', methods=['GET'])
|
||||
def search_discover():
|
||||
return util.jsonify(search.discover(util.get_user(required=True)))
|
||||
count = request.args.get('count', 3)
|
||||
if count: count = int(count)
|
||||
return util.jsonify(search.discover(util.get_user(required=False), count=count))
|
||||
|
||||
@app.route('/search/explore', methods=['GET'])
|
||||
def search_explore():
|
||||
return util.jsonify(search.explore())
|
||||
page = request.args.get('page', 1)
|
||||
if page: page = int(page)
|
||||
return util.jsonify(search.explore(page=page))
|
||||
|
||||
# INVITATIONS
|
||||
|
||||
|
@ -2,6 +2,7 @@ export default {
|
||||
|
||||
RECEIVE_OBJECTS: 'RECEIVE_OBJECTS',
|
||||
RECEIVE_OBJECT: 'RECEIVE_OBJECT',
|
||||
RECEIVE_EXPLORE_OBJECTS: 'RECEIVE_EXPLORE_OBJECTS',
|
||||
CREATE_OBJECT: 'CREATE_OBJECT',
|
||||
UPDATE_OBJECT: 'UPDATE_OBJECT',
|
||||
DELETE_OBJECT: 'DELETE_OBJECT',
|
||||
@ -17,6 +18,10 @@ export default {
|
||||
receive(object) {
|
||||
return { type: this.RECEIVE_OBJECT, object };
|
||||
},
|
||||
|
||||
receiveExplore(objects) {
|
||||
return { type: this.RECEIVE_EXPLORE_OBJECTS, objects };
|
||||
},
|
||||
|
||||
create(object) {
|
||||
return { type: this.CREATE_OBJECT, object };
|
||||
|
@ -8,6 +8,7 @@ export default {
|
||||
REQUEST_USERS: 'REQUEST_USERS',
|
||||
REQUEST_FAILED: 'REQUEST_FAILED',
|
||||
RECEIVE_USER: 'RECEIVE_USERS',
|
||||
RECEIVE_EXPLORE: 'RECEIVE_EXPLORE',
|
||||
UPDATE_USER: 'UPDATE_USER',
|
||||
UPDATE_USERNAME: 'UPDATE_USERNAME',
|
||||
JOIN_GROUP: 'JOIN_GROUP',
|
||||
@ -33,6 +34,10 @@ export default {
|
||||
receive(user) {
|
||||
return { type: this.RECEIVE_USER, user };
|
||||
},
|
||||
|
||||
receiveExplore(users) {
|
||||
return { type: this.RECEIVE_EXPLORE, users };
|
||||
},
|
||||
|
||||
update(id, data) {
|
||||
return { type: this.UPDATE_USER, id, data };
|
||||
|
@ -7,10 +7,10 @@ export const search = {
|
||||
users(username, success, fail) {
|
||||
api.authenticatedRequest('GET', `/search/users?username=${username}`, null, data => success && success(data.users), fail);
|
||||
},
|
||||
discover(success, fail) {
|
||||
api.authenticatedRequest('GET', `/search/discover`, null, data => success && success(data), fail);
|
||||
discover(count, success, fail) {
|
||||
api.authenticatedRequest('GET', `/search/discover?count=${count || 3}`, null, data => success && success(data), fail);
|
||||
},
|
||||
explore(success, fail) {
|
||||
api.unauthenticatedRequest('GET', `/search/explore`, null, data => success && success(data), fail);
|
||||
explore(page, success, fail) {
|
||||
api.unauthenticatedRequest('GET', `/search/explore?page=${page || 1}`, null, data => success && success(data), fail);
|
||||
},
|
||||
};
|
||||
|
@ -78,6 +78,12 @@ function App() {
|
||||
useEffect(() => {
|
||||
api.auth.autoLogin(token => dispatch(actions.auth.receiveLogin(token)));
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
api.search.explore(1, data => { // Page is always 1 on app-load
|
||||
dispatch(actions.objects.receiveExplore(data.objects));
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loggedInUserId) return;
|
||||
|
53
web/src/components/includes/DiscoverCard.jsx
Normal file
53
web/src/components/includes/DiscoverCard.jsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, List } from 'semantic-ui-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import UserChip from './UserChip';
|
||||
import api from '../../api';
|
||||
import utils from '../../utils/utils.js';
|
||||
|
||||
export default function ExploreCard({ count }) {
|
||||
const [highlightProjects, setHighlightProjects] = useState([]);
|
||||
const [highlightUsers, setHighlightUsers] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
api.search.discover(count || 3, ({ highlightProjects, highlightUsers }) => {
|
||||
setHighlightProjects(highlightProjects);
|
||||
setHighlightUsers(highlightUsers);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if ((highlightProjects?.length === 0 || highlightUsers?.length === 0)) return null;
|
||||
|
||||
return (
|
||||
<Card fluid>
|
||||
<Card.Content>
|
||||
{highlightProjects?.length > 0 && <>
|
||||
<h4>Discover a project</h4>
|
||||
<List relaxed>
|
||||
{highlightProjects.map(p =>
|
||||
<List.Item key={p._id}>
|
||||
<List.Icon name='book' size='large' verticalAlign='middle' />
|
||||
<List.Content>
|
||||
<List.Header className='umami--click--discover-project' as={Link} to={`/${p.fullName}`}>{p.name}</List.Header>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
)}
|
||||
</List>
|
||||
</>}
|
||||
|
||||
{highlightUsers?.length > 0 && <>
|
||||
<h4>Find others on {utils.appName()}</h4>
|
||||
<List relaxed>
|
||||
{highlightUsers.map(u =>
|
||||
<List.Item key={u._id}>
|
||||
<List.Content>
|
||||
<UserChip user={u} className='umami--click--discover-user'/>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
)}
|
||||
</List>
|
||||
</>}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
@ -12,11 +12,10 @@ import UserChip from '../includes/UserChip';
|
||||
import HelpLink from '../includes/HelpLink';
|
||||
import ProjectCard from '../includes/ProjectCard';
|
||||
import Tour from '../includes/Tour';
|
||||
import DiscoverCard from '../includes/DiscoverCard';
|
||||
|
||||
function Home() {
|
||||
const [runJoyride, setRunJoyride] = useState(false);
|
||||
const [highlightProjects, setHighlightProjects] = useState([]);
|
||||
const [highlightUsers, setHighlightUsers] = useState([]);
|
||||
const dispatch = useDispatch();
|
||||
const { user, projects, groups, invitations, loadingProjects } = useSelector(state => {
|
||||
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
|
||||
@ -30,10 +29,6 @@ function Home() {
|
||||
api.invitations.get(({ invitations, sentInvitations}) => {
|
||||
dispatch(actions.invitations.receiveInvitations(invitations.concat(sentInvitations)));
|
||||
});
|
||||
api.search.discover(({ highlightProjects, highlightUsers }) => {
|
||||
setHighlightProjects(highlightProjects);
|
||||
setHighlightUsers(highlightUsers);
|
||||
});
|
||||
}, [dispatch]);
|
||||
useEffect(() => {
|
||||
api.users.getMyProjects(p => dispatch(actions.projects.receiveProjects(p)));
|
||||
@ -92,38 +87,7 @@ function Home() {
|
||||
|
||||
<h2><span role="img" aria-label="wave">👋</span> {greeting}{user && <span>, {user.username}</span>}</h2>
|
||||
|
||||
{(highlightProjects?.length > 0 || highlightUsers?.length > 0) &&
|
||||
<Card fluid>
|
||||
<Card.Content>
|
||||
{highlightProjects?.length > 0 && <>
|
||||
<h4>Discover public projects</h4>
|
||||
<List relaxed>
|
||||
{highlightProjects.map(p =>
|
||||
<List.Item key={p._id}>
|
||||
<List.Icon name='book' size='large' verticalAlign='middle' />
|
||||
<List.Content>
|
||||
<List.Header className='umami--click--discover-project' as={Link} to={p.fullName}>{p.name}</List.Header>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
)}
|
||||
</List>
|
||||
</>}
|
||||
|
||||
{highlightUsers?.length > 0 && <>
|
||||
<h4>Find others on {utils.appName()}</h4>
|
||||
<List relaxed>
|
||||
{highlightUsers.map(u =>
|
||||
<List.Item key={u._id}>
|
||||
<List.Content>
|
||||
<UserChip user={u} className='umami--click--discover-user'/>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
)}
|
||||
</List>
|
||||
</>}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
}
|
||||
<DiscoverCard count={3} />
|
||||
|
||||
{(groups && groups.length) ?
|
||||
<Card fluid className='joyride-groups' style={{opacity: 0.8}}>
|
||||
|
@ -1,47 +1,68 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Container, Card } 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 actions from '../../../actions';
|
||||
import api from '../../../api';
|
||||
import utils from '../../../utils/utils.js';
|
||||
|
||||
import UserChip from '../../includes/UserChip';
|
||||
import DiscoverCard from '../../includes/DiscoverCard';
|
||||
import DraftPreview from '../projects/objects/DraftPreview';
|
||||
|
||||
export default function Explore() {
|
||||
const [objects, setObjects] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { objects, page } = useSelector(state => {
|
||||
return { objects: state.objects.exploreObjects, page: state.objects.explorePage };
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
api.search.explore(data => {
|
||||
setObjects(data.objects);
|
||||
if (page < 2) loadMoreExplore();
|
||||
}, []);
|
||||
|
||||
function loadMoreExplore() {
|
||||
setLoading(true);
|
||||
api.search.explore(page + 1, data => {
|
||||
dispatch(actions.objects.receiveExplore(data.objects));
|
||||
setLoading(false);
|
||||
});
|
||||
}, [])
|
||||
}
|
||||
|
||||
return (
|
||||
<Container style={{ marginTop: '40px' }}>
|
||||
<h1>Explore {utils.appName()}</h1>
|
||||
|
||||
<Card.Group stackable doubling itemsPerRow={4}>
|
||||
|
||||
{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>
|
||||
}
|
||||
<Grid stackable>
|
||||
<Grid.Column computer={5} tablet={8}>
|
||||
<DiscoverCard count={7} />
|
||||
</Grid.Column>
|
||||
<Grid.Column computer={11} tablet={8}>
|
||||
<h1>Recent patterns on {utils.appName()}</h1>
|
||||
|
||||
<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>
|
||||
)}
|
||||
</Card.Group>
|
||||
<div style={{display: 'flex', justifyContent: 'center', marginTop: 30}}>
|
||||
<Button loading={loading} onClick={loadMoreExplore}>Load more</Button>
|
||||
</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>
|
||||
)}
|
||||
</Card.Group>
|
||||
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -119,7 +119,7 @@ function ObjectViewer() {
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'end' }}>
|
||||
{object.type === 'pattern' && (project.user === (user && user._id) || project.openSource || object.preview) && <>
|
||||
<Dropdown icon={null} trigger={<Button size='small' secondary icon='download' content='Download pattern' loading={downloading} disabled={downloading}/>}>
|
||||
<Dropdown direction='left' icon={null} trigger={<Button size='small' secondary icon='download' content='Download pattern' loading={downloading} disabled={downloading}/>}>
|
||||
<Dropdown.Menu>
|
||||
{object.preview &&
|
||||
<Dropdown.Item onClick={e => downloadDrawdownImage(object)} content='Download drawdown as an image' icon='file outline' />
|
||||
@ -136,12 +136,14 @@ function ObjectViewer() {
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
|
||||
<Dropdown icon={null} trigger={<Button size="small" icon="copy" secondary content="Copy to.." />}>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Header>Select a project to copy this pattern to</Dropdown.Header>
|
||||
{myProjects?.map(myProject => <Dropdown.Item content={myProject.name} onClick={e => copyPattern(myProject)} />)}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
{user &&
|
||||
<Dropdown icon={null} trigger={<Button size="small" icon="copy" secondary content="Copy to.." />}>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Header>Select a project to copy this pattern to</Dropdown.Header>
|
||||
{myProjects?.map(myProject => <Dropdown.Item content={myProject.name} onClick={e => copyPattern(myProject)} />)}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
}
|
||||
</>}
|
||||
|
||||
{utils.canEditProject(user, project) &&
|
||||
|
@ -3,6 +3,8 @@ import actions from '../actions';
|
||||
const initialState = {
|
||||
loading: false,
|
||||
objects: [],
|
||||
exploreObjects: [],
|
||||
explorePage: 1,
|
||||
comments: [],
|
||||
selected: null,
|
||||
editor: { tool: 'straight', colour: 'orange', view: 'interlacement' },
|
||||
@ -36,6 +38,10 @@ function objects(state = initialState, action) {
|
||||
});
|
||||
if (!found) objects.push(action.object);
|
||||
return Object.assign({}, state, { loading: false, objects });
|
||||
case actions.objects.RECEIVE_EXPLORE_OBJECTS:
|
||||
const newObjects = Object.assign([], state.exploreObjects);
|
||||
action.objects?.forEach(o => newObjects.push(o));
|
||||
return Object.assign({}, state, { exploreObjects: newObjects, explorePage: state.explorePage + 1 });
|
||||
case actions.objects.CREATE_OBJECT:
|
||||
const objectList = state.objects;
|
||||
objectList.push(action.object);
|
||||
|
Loading…
Reference in New Issue
Block a user