Commit 17f28a6d authored by BsmhDev's avatar BsmhDev

Merge branch 'develop' into '129-react-router'

# Conflicts:
#   imports/ui/pages/game/home.js
parents 69b81e21 f90eb6b5
......@@ -2,3 +2,4 @@ node_modules/
.idea/
settings.json
docker-compose.yml
package-lock.json
\ No newline at end of file
......@@ -5,16 +5,16 @@
# but you can also edit it by hand.
meteor-base@1.1.0 # Packages every Meteor app needs to have
mobile-experience@1.0.4 # Packages for a great mobile UX
mongo@1.1.19 # The database Meteor supports right now
mobile-experience@1.0.5 # Packages for a great mobile UX
mongo@1.2.2 # The database Meteor supports right now
reactive-var@1.0.11 # Reactive variable for tracker
jquery@1.11.10 # Helpful client-side library
tracker@1.1.3 # Meteor's client-side reactive programming library
standard-minifier-css@1.3.4 # CSS minifier run for production mode
standard-minifier-js@2.1.1 # JS minifier run for production mode
standard-minifier-css@1.3.5 # CSS minifier run for production mode
standard-minifier-js@2.1.2 # JS minifier run for production mode
es5-shim@4.6.15 # ECMAScript 5 compatibility for older browsers.
ecmascript@0.8.1 # Enable ECMAScript2015+ syntax in app code
ecmascript@0.8.3 # Enable ECMAScript2015+ syntax in app code
less@2.7.9 # Leaner CSS language
......@@ -24,7 +24,7 @@ johanbrook:publication-collector # Test a Meteor publication by collecting its
arillo:flow-router-helpers
react-meteor-data
static-html
dynamic-import@0.1.1
dynamic-import@0.1.3
accounts-ui@1.1.9
accounts-github@1.3.0
service-configuration@1.0.11
......@@ -39,3 +39,4 @@ jagi:astronomy
dburles:factory
mizzao:jquery-ui
meteorhacks:aggregate
ros:publish-counts
accounts-base@1.3.1
accounts-github@1.3.0
accounts-base@1.4.0
accounts-github@1.4.0
accounts-oauth@1.1.15
accounts-ui@1.1.9
accounts-ui-unstyled@1.2.1
allow-deny@1.0.6
accounts-ui@1.2.0
accounts-ui-unstyled@1.3.0
allow-deny@1.1.0
arillo:flow-router-helpers@0.5.2
autoupdate@1.3.12
babel-compiler@6.19.4
babel-runtime@1.0.1
babel-compiler@6.24.7
babel-runtime@1.1.1
base64@1.0.10
binary-heap@1.0.10
blaze@2.3.2
blaze-tools@1.0.10
boilerplate-generator@1.1.1
boilerplate-generator@1.3.0
browser-policy@1.1.0
browser-policy-common@1.0.11
browser-policy-content@1.1.0
......@@ -21,98 +21,99 @@ caching-compiler@1.1.9
caching-html-compiler@1.1.2
callback-hook@1.0.10
check@1.2.5
coffeescript@1.12.6_1
coffeescript@1.12.7_3
coffeescript-compiler@1.12.7_3
dburles:factory@1.1.0
ddp@1.3.0
ddp-client@2.0.0
ddp-common@1.2.9
ddp@1.4.0
ddp-client@2.2.0
ddp-common@1.3.0
ddp-rate-limiter@1.0.7
ddp-server@2.0.0
ddp-server@2.1.0
deps@1.0.12
diff-sequence@1.0.7
dynamic-import@0.1.1
ecmascript@0.8.2
ecmascript-runtime@0.4.1
ecmascript-runtime-client@0.4.3
ecmascript-runtime-server@0.4.1
ejson@1.0.13
dynamic-import@0.2.0
ecmascript@0.9.0
ecmascript-runtime@0.5.0
ecmascript-runtime-client@0.5.0
ecmascript-runtime-server@0.5.0
ejson@1.1.0
es5-shim@4.6.15
fastclick@1.0.13
geojson-utils@1.0.10
github-oauth@1.2.0
hot-code-push@1.0.4
html-tools@1.0.11
htmljs@1.0.11
http@1.2.12
http@1.3.0
id-map@1.0.9
jagi:astronomy@2.4.8
jagi:astronomy@2.5.2
johanbrook:publication-collector@1.0.10
jquery@1.11.10
launch-screen@1.1.1
less@2.7.9
less@2.7.11
lidorcg:rocketchat-custom-oauth@1.0.0
lidorcg:rocketchat-gitlab@0.0.1
livedata@1.0.18
localstorage@1.1.1
logging@1.1.17
localstorage@1.2.0
logging@1.1.19
mdg:validation-error@0.5.1
meteor@1.7.1
meteor-base@1.1.0
meteor@1.8.0
meteor-base@1.2.0
meteorhacks:aggregate@1.3.0
meteorhacks:collection-utils@1.2.0
minifier-css@1.2.16
minifier-js@2.1.1
minimongo@1.2.1
mizzao:build-fetcher@0.2.0
minifier-js@2.2.1
minimongo@1.4.0
mizzao:build-fetcher@0.3.2
mizzao:jquery-ui@1.11.4
mobile-experience@1.0.4
mobile-experience@1.0.5
mobile-status-bar@1.0.14
modules@0.9.2
modules-runtime@0.8.0
mongo@1.1.22
modules@0.11.0
modules-runtime@0.9.0
mongo@1.3.0
mongo-dev-server@1.1.0
mongo-id@1.0.6
mongo-livedata@1.0.12
npm-mongo@2.2.30
oauth@1.1.13
oauth2@1.1.11
npm-mongo@2.2.33
oauth@1.2.0
oauth2@1.2.0
observe-sequence@1.0.16
ordered-dict@1.0.9
ostrio:cookies@2.2.2
ostrio:files@1.8.2
ostrio:cookies@2.2.3
ostrio:files@1.9.0
practicalmeteor:chai@2.1.0_1
practicalmeteor:loglevel@1.2.0_2
practicalmeteor:mocha@2.4.5_6
practicalmeteor:mocha-core@1.0.1
practicalmeteor:sinon@1.14.1_2
promise@0.8.9
promise@0.10.0
random@1.0.10
rate-limit@1.0.8
react-meteor-data@0.2.12
reactive-dict@1.1.9
react-meteor-data@0.2.15
reactive-dict@1.2.0
reactive-var@1.0.11
reload@1.1.11
retry@1.0.9
reywood:publish-composite@1.5.2
ros:publish-counts@0.5.1
routepolicy@1.0.12
service-configuration@1.0.11
session@1.1.7
spacebars@1.0.15
spacebars-compiler@1.1.2
standard-minifier-css@1.3.4
standard-minifier-js@2.1.1
spacebars-compiler@1.1.3
standard-minifier-css@1.3.5
standard-minifier-js@2.2.1
static-html@1.2.2
templating@1.3.2
templating-compiler@1.3.2
templating-compiler@1.3.3
templating-runtime@1.3.2
templating-tools@1.1.2
tmeasday:check-npm-versions@0.3.1
tmeasday:test-reporter-helpers@0.2.1
tracker@1.1.3
ui@1.0.13
underscore@1.0.10
underscorestring:underscore.string@3.3.4
url@1.1.0
webapp@1.3.17
webapp@1.4.0
webapp-hashing@1.0.9
xolvio:cleaner@0.3.1
zimme:active-route@2.3.2
......@@ -20,6 +20,7 @@
@import "{}/imports/ui/stylesheets/history.less";
@import "{}/imports/ui/stylesheets/snackbar.less";
@import "{}/imports/ui/stylesheets/game-close.less";
@import "{}/imports/ui/stylesheets/search.less";
.classic-padding-top {
......
import { Meteor } from 'meteor/meteor';
import { max } from 'underscore';
import { check } from 'meteor/check';
import { eventTypes } from '/imports/startup/both/constants';
import { eventTypes, joinGameResults } from '/imports/startup/both/constants';
import Game, {
PlayerReg,
GameStart,
......@@ -296,40 +296,57 @@ Meteor.methods({
// Methods without instance:
joinGame({ code }) {
check(code, String);
Game.update(
{
$and: [
{ code },
{
gameLog: {
$not: {
$elemMatch: {
nameType: eventTypes.GameStart,
const currGame = Game.findOne({code});
if (currGame === undefined){
return joinGameResults.noGame;
}
else if (currGame.isManager()){
return joinGameResults.isManager;
}
else if (currGame.isUserRegistered()){
return joinGameResults.alreadyRegistered;
}
else if (currGame.gameLog.find(event => event.nameType === eventTypes.GameStart)){
return joinGameResults.gameStarted;
}
else {
Game.update(
{
$and: [
{ code },
{
gameLog: {
$not: {
$elemMatch: {
nameType: eventTypes.GameStart,
},
},
},
},
},
{
gameLog: {
$not: {
$elemMatch: {
nameType: eventTypes.PlayerReg,
playerId: Meteor.userId(),
{
gameLog: {
$not: {
$elemMatch: {
nameType: eventTypes.PlayerReg,
playerId: Meteor.userId(),
},
},
},
},
{
'quiz.owner': { $ne: Meteor.userId() },
},
],
},
{
$push: {
gameLog: new PlayerReg({ playerId: Meteor.userId() }),
},
{
'quiz.owner': { $ne: Meteor.userId() },
},
],
},
{
$push: {
gameLog: new PlayerReg({ playerId: Meteor.userId() }),
},
},
(err, res) => res > 0,
);
);
return joinGameResults.regSucc;
}
},
});
import { FilesCollection } from 'meteor/ostrio:files';
const Image = new FilesCollection({
storagePath: '/shablool-images',
storagePath: '/resources',
collectionName: 'images',
allowClientCode: false, // Disallow remove files from Client
onBeforeUpload(file) {
......
import { Meteor } from 'meteor/meteor';
import { publishComposite } from 'meteor/reywood:publish-composite';
import { Counts } from 'meteor/ros:publish-counts';
import { check } from 'meteor/check';
import Image from '/imports/api/images/images.js';
import Quiz from '../quizes.js';
......@@ -22,6 +23,17 @@ publishComposite('quizes.my-quizes', function() {
};
});
Meteor.publish('quizes.count', function(query) {
check(query, String);
Counts.publish(this, 'quizzes-counter', Quiz.find({
$and: [
{ $or: [{ title: { $regex: query, $options: 'i' } },
{ tags: { $elemMatch: { $regex: query, $options: 'i' } } }] },
{ $or: [{ owner: this.userId }, { private: false }] },
],
}), { fastCount: true });
});
// Public/Owner publications :
publishComposite('quizes.get', function(id) {
return {
......@@ -46,17 +58,19 @@ publishComposite('quizes.get', function(id) {
};
});
publishComposite('quizes.search', function(query) {
publishComposite('quizes.search', function(query, numOfQuizzes) {
const numberOfQuizzes = parseInt(numOfQuizzes, 10);
return {
collectionName: 'quizes',
find() {
check(query, String);
return Quiz.find({
$and: [
{ title: { $regex: query, $options: 'i' } },
{ $or: [{ title: { $regex: query, $options: 'i' } },
{ tags: { $elemMatch: { $regex: query, $options: 'i' } } }] },
{ $or: [{ owner: this.userId }, { private: false }] },
],
});
}, { limit: numberOfQuizzes });
},
children: [
{
......
......@@ -105,5 +105,21 @@ describe('quizes publication', function() {
done();
});
});
it('search quiz by tags', function(done) {
const quiz = Factory.create('quiz', { title: 'title', tags: ['ONE', 'SECOND'] });
const collector = new PublicationCollector({ userId: 'not-owner' });
collector.collect('quizes.search', 'SECOND', (collections) => {
expect(collections.quizes.length).to.equal(1);
done();
});
});
it('search private quiz by tags', function(done) {
const quiz = Factory.create('quiz', { title: 'title', tags: ['ONE', 'SECOND'], private: true });
const collector = new PublicationCollector({ userId: 'not-owner' });
collector.collect('quizes.search', 'SECOND', (collections) => {
expect(collections.quizes).to.equal(undefined);
done();
});
});
});
});
......@@ -17,6 +17,7 @@ export const joinGameResults = {
isManager: 'IS_MANAGER',
noGame: 'NO_GAME',
regSucc: 'REG_SUCC',
gameStarted: 'GAME_STARTED',
};
export const startGameResults = {
......
import React from 'react';
import { Meteor } from 'meteor/meteor';
import PropTypes from 'prop-types';
import { eventTypes } from '/imports/startup/both/constants';
......@@ -27,6 +28,11 @@ const Answer = ({ answer, index, game }) => {
const selectAnswer = () => {
game.applyMethod('playerAnswer', [game.lastQuestionToStartId(), answer._id]);
};
const isSelected = game.gameLog.find(
event => event.nameType === eventTypes.PlayerAnswer &&
answer._id === event.answerId && event.playerId === Meteor.userId()) ? 'selected-answer' : '';
const isRightAnswer = answer.points > 0;
const calculateOpacity = () =>
!isRightAnswer && game.getLastEvent().nameType === eventTypes.QuestionEnd ? 'wrong-answer' : '';
......@@ -36,7 +42,7 @@ const Answer = ({ answer, index, game }) => {
<div className="answer-btn-area">
<button
type="button"
className={`btn btn-answer-${index} answer-button ${calculateOpacity()}`}
className={`btn btn-answer-${index} answer-button ${calculateOpacity()} ${isSelected}`}
onClick={selectAnswer}
>
<div className="col-md-1 col-xs-1 col-sm-1 col-lg-1 col-xg-1">
......
......@@ -65,6 +65,14 @@ class QuestionForm extends React.Component {
<div className="panel panel-default">
<div className="panel-heading">
<div className="form-group">
<div className="col-lg-1">
<button
className="btn btn-danger btn-lg"
onClick={actions.removeQuestion(question._id)}
>
<span className="glyphicon glyphicon-remove" aria-hidden="true" />
</button>
</div>
<div className="col-lg-8">
<div className={`form-group ${textValidation ? 'has-error' : ''}`}>
<input
......@@ -98,14 +106,6 @@ class QuestionForm extends React.Component {
: ''}
</div>
</div>
<div className="col-lg-1">
<button
className="btn btn-danger btn-lg"
onClick={actions.removeQuestion(question._id)}
>
<span className="glyphicon glyphicon-minus" aria-hidden="true" />
</button>
</div>
</div>
</div>
<div className="panel-body">
......
......@@ -5,6 +5,7 @@ import GameNavbar from '/imports/ui/components/game-navbar';
import CountdownTimer from '/imports/ui/components/count-down-timer';
import { noImage } from '/imports/startup/both/constants';
import Image from '/imports/api/images/images';
import { Line } from 'rc-progress';
const Question = ({ game }) => {
const question = game.lastQuestionToStart();
......@@ -49,6 +50,13 @@ const Question = ({ game }) => {
<p className="answer-count">תשובות</p>
</div>
</div>
<div>
<Line
className="progress-bar-location"
percent={(question.order / game.quiz.questions.length) * 100}
strokeColor="#85b8fe"
/>
</div>
<Answers answers={question.answers} game={game} />
{game.isManager() ? <audio src={`/game-audio-${Math.floor(Math.random() * 3) + 1}.mp3`} autoPlay loop /> : ''}
</div>
......
import React from 'react';
import { Meteor } from 'meteor/meteor';
import { Link, withRouter } from 'react-router-dom';
import { joinGameResults } from '/imports/startup/both/constants';
class Home extends React.Component {
constructor(props) {
......@@ -27,7 +28,7 @@ class Home extends React.Component {
{
code: gameCode,
},
(err, res) => (res ? notifyUser() : this.props.history.push(`/game/${gameCode}`)),
(err, res) => (res === joinGameResults.noGame) ? notifyUser() : this.props.history.push(`/game/${gameCode}`),
);
};
......
import React from 'react';
import { Meteor } from 'meteor/meteor';
import { createContainer } from 'meteor/react-meteor-data';
import { Counts } from 'meteor/ros:publish-counts';
import PropTypes from 'prop-types';
import SweetAlert from 'sweetalert-react';
import 'sweetalert/dist/sweetalert.css';
import Quiz from '/imports/api/quizes/quizes.js';
import QuizCard from '/imports/ui/components/quiz-card.js';
import Loading from '/imports/ui/components/loading';
import Loader from 'react-loading-components';
import InfiniteScroll from 'react-infinite-scroller';
import uuidV4 from 'uuid/v4';
class Search extends React.Component {
const LoaderAndUI = ({ results, loading, query, state,
actions, actionsForUI, numberOfQuizzes }) => {
if (results.length === 0 && loading) return <Loading />;
return results.length === 0
? <div className="row">
<img
className="col-md-6"
src="/img/no-search-results.png"
alt="No Search Results"
/>
<img
className="col-md-6"
src="/img/no-search-results-text.png"
alt="No Search Results"
/>
</div>
: <div id="search">
<h1>תוצאות חיפוש עבור <strong>{query}</strong></h1>
<InfiniteScroll
loadMore={actionsForUI.MoreQuizzesToDisplay}
hasMore={!(results.length < state.quizzesToDisplay)}
loader={<div key={uuidV4()} className="loader">
<Loader type="three_dots" width={100} height={100} fill="#000000" /> </div>}
threshold={45}
>
{results.map(quiz => (
<div key={quiz._id}>
<div className="row">
<QuizCard quiz={quiz} actions={actions} />
</div>
</div>
))}
</InfiniteScroll>
{results.length === numberOfQuizzes ?
<div className="show infinite-scroll-text">
אין עוד שאלונים להצגה
</div> : ''}
<div
id="snackbar"
className={
state.quizDeleted || state.quizForked ? 'show' : ''
}
>
{state.quizDeleted
? 'השאלון נמחק בהצלחה'
: 'השאלון הועתק בהצלחה'}
</div>
<SweetAlert
show={state.showDeleteQuizAlert}
title="מחיקת שאלון"
type="warning"
text={
state.showDeleteQuizAlert
? `האם אתה בטוח שברצונך למחוק את השאלון: ${state.quizToDelete.title}?`
: ''
}
showCancelButton
confirmButtonText="מחק!"
cancelButtonText="בטל"
onConfirm={() => {
actionsForUI.deleteQuiz();
actionsForUI.ConfirmOrCancel();
}}
onCancel={actionsForUI.ConfirmOrCancel}
onEscapeKey={actionsForUI.RemoveQuizAlert}
onOutsideClick={actionsForUI.RemoveQuizAlert}
/>
</div>;
};
LoaderAndUI.propTypes = {
results: PropTypes.arrayOf(PropTypes.object).isRequired,
loading: PropTypes.bool.isRequired,
query: PropTypes.string.isRequired,
state: PropTypes.instanceOf(Object).isRequired,
actionsForUI: PropTypes.instanceOf(Object).isRequired,
actions: PropTypes.instanceOf(Object).isRequired,
numberOfQuizzes: PropTypes.number.isRequired,
};
const DBProvider = createContainer(({ query, state, actions, actionsForUI }) => {
Meteor.subscribe('quizes.count', query);
const numberOfQuizzes = Counts.get('quizzes-counter');
const searchHandle = Meteor.subscribe('quizes.search', query, state.quizzesToDisplay);
const loading = !searchHandle.ready();
const results = Quiz.find({
$and: [
{ $or: [{ title: { $regex: query, $options: 'i' } },
{ tags: { $elemMatch: { $regex: query, $options: 'i' } } }] },
{ $or: [{ owner: this.userId }, { private: false }] },
],
}, { limit: state.quizzesToDisplay }).fetch();
return {
results,
loading,
query,
state,
actions,
actionsForUI,
numberOfQuizzes,
};
}, LoaderAndUI);
export default class Search extends React.Component {
constructor(props) {
super(props);
this.state = {
......@@ -16,10 +124,20 @@ class Search extends React.Component {
quizForked: false,
showDeleteQuizAlert: false,
quizToDelete: null,
quizzesToDisplay: 10,
formerQuery: null,
};
}
componentDidUpdate() {
const { query } = this.props;
const newQuery = () => {
this.setState({ quizzesToDisplay: 10 });
this.setState({ formerQuery: query });
};
if (this.state.formerQuery !== query) newQuery();
}
render() {
const { results, query } = this.props;
const showDeleteAlert = (quiz) => {
this.setState({ quizToDelete: quiz, showDeleteQuizAlert: true });
};
......@@ -45,91 +163,41 @@ class Search extends React.Component {
};
notifyUser();
};
const MoreQuizzesToDisplay = () => {
this.setState({ quizzesToDisplay: this.state.quizzesToDisplay + 10 });
};
const RemoveQuizAlert = () => {
this.setState({ showDeleteQuizAlert: false });
};
const ConfirmOrCancel = () => {
this.setState({ quizToDelete: null, showDeleteQuizAlert: false });
};
const { query } = this.props;
const actions = {
showDeleteAlert,
forkQuiz,
};
return results.length === 0
? <div className="row">
<img
className="col-md-6"
src="/img/no-search-results.png"
alt="No Search Results"
/>
<img
className="col-md-6"
src="/img/no-search-results-text.png"
alt="No Search Results"
/>
</div>
: <div id="search">
<h1>תוצאות חיפוש עבור <strong>{query}</strong></h1>
{results.map(quiz => (
<div key={quiz._id}>
<div className="row">
<QuizCard quiz={quiz} actions={actions} />
</div>
</div>
))}
<div
id="snackbar"
className={
this.state.quizDeleted || this.state.quizForked ? 'show' : ''
}
>
{this.state.quizDeleted
? 'השאלון נמחק בהצלחה'
: 'השאלון הועתק בהצלחה'}
</div>
<SweetAlert
show={this.state.showDeleteQuizAlert}
title="מחיקת שאלון"
type="warning"
text={
this.state.showDeleteQuizAlert
? `האם אתה בטוח שברצונך למחוק את השאלון: ${this.state.quizToDelete.title}?`
: ''
}
showCancelButton
confirmButtonText="מחק!"
cancelButtonText="בטל"
onConfirm={() => {
deleteQuiz();
this.setState({ quizToDelete: null, showDeleteQuizAlert: false });
}}
onCancel={() => {
this.setState({ quizToDelete: null, showDeleteQuizAlert: false });
}}
onEscapeKey={() => this.setState({ showDeleteQuizAlert: false })}
onOutsideClick={() => this.setState({ showDeleteQuizAlert: false })}
/>
</div>;
const actionsForUI = {
deleteQuiz,
forkQuiz,
MoreQuizzesToDisplay,