diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..40b878d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+node_modules/
\ No newline at end of file
diff --git a/client/main.css b/client/main.css
new file mode 100644
index 0000000..ed5e77e
--- /dev/null
+++ b/client/main.css
@@ -0,0 +1,126 @@
+/* CSS declarations go here */
+body {
+ font-family: sans-serif;
+ background-color: #315481;
+ background-image: linear-gradient(to bottom, #315481, #918e82 100%);
+ background-attachment: fixed;
+
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+
+ padding: 0;
+ margin: 0;
+
+ font-size: 14px;
+}
+
+.container {
+ max-width: 600px;
+ margin: 0 auto;
+ min-height: 100%;
+ background: white;
+}
+
+header {
+ background: #d2edf4;
+ background-image: linear-gradient(to bottom, #d0edf5, #e1e5f0 100%);
+ padding: 20px 15px 15px 15px;
+ position: relative;
+}
+
+#login-buttons {
+ display: block;
+}
+
+h1 {
+ font-size: 1.5em;
+ margin: 0;
+ margin-bottom: 10px;
+ display: inline-block;
+ margin-right: 1em;
+}
+
+form {
+ margin-top: 10px;
+ margin-bottom: -10px;
+ position: relative;
+}
+
+.new-task input {
+ box-sizing: border-box;
+ padding: 10px 0;
+ background: transparent;
+ border: none;
+ width: 100%;
+ padding-right: 80px;
+ font-size: 1em;
+}
+
+.new-task input:focus{
+ outline: 0;
+}
+
+ul {
+ margin: 0;
+ padding: 0;
+ background: white;
+}
+
+.delete {
+ float: right;
+ font-weight: bold;
+ background: none;
+ font-size: 1em;
+ border: none;
+ position: relative;
+}
+
+li {
+ position: relative;
+ list-style: none;
+ padding: 15px;
+ border-bottom: #eee solid 1px;
+}
+
+li .text {
+ margin-left: 10px;
+}
+
+li.checked {
+ color: #888;
+}
+
+li.checked .text {
+ text-decoration: line-through;
+}
+
+li.private {
+ background: #eee;
+ border-color: #ddd;
+}
+
+header .hide-completed {
+ float: right;
+}
+
+.toggle-private {
+ margin-left: 5px;
+}
+
+@media (max-width: 600px) {
+ li {
+ padding: 12px 15px;
+ }
+
+ .search {
+ width: 150px;
+ clear: both;
+ }
+
+ .new-task input {
+ padding-bottom: 5px;
+ }
+}
diff --git a/client/main.html b/client/main.html
new file mode 100644
index 0000000..7c48505
--- /dev/null
+++ b/client/main.html
@@ -0,0 +1,3 @@
+
+ simple
+
diff --git a/client/main.js b/client/main.js
new file mode 100644
index 0000000..739c50f
--- /dev/null
+++ b/client/main.js
@@ -0,0 +1,2 @@
+import '../imports/startup/accounts-config.js';
+import '../imports/ui/body.js';
diff --git a/imports/api/tasks.js b/imports/api/tasks.js
new file mode 100644
index 0000000..d7d72dd
--- /dev/null
+++ b/imports/api/tasks.js
@@ -0,0 +1,72 @@
+import { Meteor } from 'meteor/meteor';
+import { Mongo } from 'meteor/mongo';
+import { check } from 'meteor/check';
+
+export const Tasks = new Mongo.Collection('tasks');
+
+if (Meteor.isServer) {
+ // This code only runs on the server
+ // Only publish tasks that are public or belong to the current user
+ Meteor.publish('tasks', function tasksPublication() {
+ return Tasks.find({
+ $or: [
+ { private: { $ne: true } },
+ { owner: this.userId },
+ ],
+ });
+ });
+}
+
+Meteor.methods({
+ 'tasks.insert'(text) {
+ check(text, String);
+
+ // Make sure the user is logged in before inserting a task
+ if (! this.userId) {
+ throw new Meteor.Error('not-authorized');
+ }
+
+ Tasks.insert({
+ text,
+ createdAt: new Date(),
+ owner: this.userId,
+ username: Meteor.users.findOne(this.userId).username,
+ });
+ },
+ 'tasks.remove'(taskId) {
+ check(taskId, String);
+
+ const task = Tasks.findOne(taskId);
+ if (task.private && task.owner !== this.userId) {
+ // If the task is private, make sure only the owner can delete it
+ throw new Meteor.Error('not-authorized');
+ }
+
+ Tasks.remove(taskId);
+ },
+ 'tasks.setChecked'(taskId, setChecked) {
+ check(taskId, String);
+ check(setChecked, Boolean);
+
+ const task = Tasks.findOne(taskId);
+ if (task.private && task.owner !== this.userId) {
+ // If the task is private, make sure only the owner can check it off
+ throw new Meteor.Error('not-authorized');
+ }
+
+ Tasks.update(taskId, { $set: { checked: setChecked } });
+ },
+ 'tasks.setPrivate'(taskId, setToPrivate) {
+ check(taskId, String);
+ check(setToPrivate, Boolean);
+
+ const task = Tasks.findOne(taskId);
+
+ // Make sure only the task owner can make a task private
+ if (task.owner !== this.userId) {
+ throw new Meteor.Error('not-authorized');
+ }
+
+ Tasks.update(taskId, { $set: { private: setToPrivate } });
+ },
+});
diff --git a/imports/api/tasks.tests.js b/imports/api/tasks.tests.js
new file mode 100644
index 0000000..9b61c5a
--- /dev/null
+++ b/imports/api/tasks.tests.js
@@ -0,0 +1,41 @@
+/* eslint-env mocha */
+
+import { Meteor } from 'meteor/meteor';
+import { Random } from 'meteor/random';
+import { assert } from 'meteor/practicalmeteor:chai';
+
+import { Tasks } from './tasks.js';
+
+if (Meteor.isServer) {
+ describe('Tasks', () => {
+ describe('methods', () => {
+ const userId = Random.id();
+ let taskId;
+
+ beforeEach(() => {
+ Tasks.remove({});
+ taskId = Tasks.insert({
+ text: 'test task',
+ createdAt: new Date(),
+ owner: userId,
+ username: 'tmeasday',
+ });
+ });
+
+ it('can delete owned task', () => {
+ // Find the internal implementation of the task method so we can
+ // test it in isolation
+ const deleteTask = Meteor.server.method_handlers['tasks.remove'];
+
+ // Set up a fake method invocation that looks like what the method expects
+ const invocation = { userId };
+
+ // Run the method with `this` set to the fake invocation
+ deleteTask.apply(invocation, [taskId]);
+
+ // Verify that the method does what we expected
+ assert.equal(Tasks.find().count(), 0);
+ });
+ });
+ });
+}
diff --git a/imports/startup/accounts-config.js b/imports/startup/accounts-config.js
new file mode 100644
index 0000000..7e4f7e5
--- /dev/null
+++ b/imports/startup/accounts-config.js
@@ -0,0 +1,5 @@
+import { Accounts } from 'meteor/accounts-base';
+
+Accounts.ui.config({
+ passwordSignupFields: 'USERNAME_ONLY',
+});
diff --git a/imports/ui/body.html b/imports/ui/body.html
new file mode 100644
index 0000000..6a9e4e1
--- /dev/null
+++ b/imports/ui/body.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+ {{#each tasks}}
+ {{> task}}
+ {{/each}}
+
+
+
diff --git a/imports/ui/body.js b/imports/ui/body.js
new file mode 100644
index 0000000..8a4407f
--- /dev/null
+++ b/imports/ui/body.js
@@ -0,0 +1,48 @@
+import { Meteor } from 'meteor/meteor';
+import { Template } from 'meteor/templating';
+import { ReactiveDict } from 'meteor/reactive-dict';
+
+import { Tasks } from '../api/tasks.js';
+
+import './task.js';
+import './body.html';
+
+Template.body.onCreated(function bodyOnCreated() {
+ this.state = new ReactiveDict();
+ Meteor.subscribe('tasks');
+});
+
+Template.body.helpers({
+ tasks() {
+ const instance = Template.instance();
+ if (instance.state.get('hideCompleted')) {
+ // If hide completed is checked, filter tasks
+ return Tasks.find({ checked: { $ne: true } }, { sort: { createdAt: -1 } });
+ }
+ // Otherwise, return all of the tasks
+ return Tasks.find({}, { sort: { createdAt: -1 } });
+ },
+ incompleteCount() {
+ return Tasks.find({ checked: { $ne: true } }).count();
+ },
+});
+
+Template.body.events({
+ 'submit .new-task'(event) {
+ // Prevent default browser form submit
+ event.preventDefault();
+
+ // Get value from form element
+ const target = event.target;
+ const text = target.text.value;
+
+ // Insert a task into the collection
+ Meteor.call('tasks.insert', text);
+
+ // Clear form
+ target.text.value = '';
+ },
+ 'change .hide-completed input'(event, instance) {
+ instance.state.set('hideCompleted', event.target.checked);
+ },
+});
diff --git a/imports/ui/task.html b/imports/ui/task.html
new file mode 100644
index 0000000..218789f
--- /dev/null
+++ b/imports/ui/task.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ {{#if isOwner}}
+
+ {{/if}}
+
+ {{username}} - {{text}}
+
+
diff --git a/imports/ui/task.js b/imports/ui/task.js
new file mode 100644
index 0000000..6b51496
--- /dev/null
+++ b/imports/ui/task.js
@@ -0,0 +1,23 @@
+import { Meteor } from 'meteor/meteor';
+import { Template } from 'meteor/templating';
+
+import './task.html';
+
+Template.task.helpers({
+ isOwner() {
+ return this.owner === Meteor.userId();
+ },
+});
+
+Template.task.events({
+ 'click .toggle-checked'() {
+ // Set the checked property to the opposite of its current value
+ Meteor.call('tasks.setChecked', this._id, !this.checked);
+ },
+ 'click .delete'() {
+ Meteor.call('tasks.remove', this._id);
+ },
+ 'click .toggle-private'() {
+ Meteor.call('tasks.setPrivate', this._id, !this.private);
+ },
+});
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..2afe1d5
--- /dev/null
+++ b/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "mindcontrol2",
+ "private": true,
+ "scripts": {
+ "start": "meteor run"
+ },
+ "dependencies": {
+ "meteor-node-stubs": "~0.2.0"
+ }
+}
diff --git a/server/main.js b/server/main.js
new file mode 100644
index 0000000..ab941a4
--- /dev/null
+++ b/server/main.js
@@ -0,0 +1 @@
+import '../imports/api/tasks.js';