diff --git a/app/components/audio-recorder/component.js b/app/components/audio-recorder/component.js index 1546fe4..c15c771 100644 --- a/app/components/audio-recorder/component.js +++ b/app/components/audio-recorder/component.js @@ -1,23 +1,47 @@ import Ember from 'ember'; +import firebase from 'firebase'; + +const { + computed: { notEmpty } +} = Ember; export default Ember.Component.extend({ + classNames: ['row'], audioRecorder: Ember.inject.service(), + hifi: Ember.inject.service(), + + hasAudio: notEmpty('model.audioUrl'), + saving: false, init() { this._super(...arguments); this.get("audioRecorder").requestAccess(); }, - mouseDown() { - const recorder = this.get("audioRecorder").createRecorder(); - this.set("recorder", recorder); - Ember.run.later(recorder.start, 100); - }, + actions: { + startRecording() { + const recorder = this.get("audioRecorder").createRecorder(); + this.set("recorder", recorder); + Ember.run.later(recorder.start, 100); + }, + + stopRecording() { + this.set("saving", true); + const recorder = this.get("recorder"); + recorder + .stop() + .then(this.get("created")) + .then(() => this.set("saving", false)); + }, + + play() { + const app = firebase.app(); + var storageRef = app.storage().ref(); + var audioRef = storageRef.child(this.get('model.audioUrl')); - mouseUp() { - const recorder = this.get("recorder"); - recorder - .stop() - .then(this.get("created")); + audioRef + .getDownloadURL() + .then(url => this.get("hifi").play(url)); + } } }); diff --git a/app/components/audio-recorder/styles.styl b/app/components/audio-recorder/styles.styl index 687a7da..701aefd 100644 --- a/app/components/audio-recorder/styles.styl +++ b/app/components/audio-recorder/styles.styl @@ -1,2 +1,20 @@ & border: none + +.btn + margin: 0.25em + display: block + border-radius: 50% + padding: 0.5em + background: rgba(off-white, 0.8) + height: 30px + width: 30px + transition: all 0.2s ease-in-out + +.btn:hover + background: rgba(off-white, 1) + +.btn:active + background: hot-pink + opacity: 1 + color: off-white diff --git a/app/components/audio-recorder/template.hbs b/app/components/audio-recorder/template.hbs index 6764e3b..f833fc2 100644 --- a/app/components/audio-recorder/template.hbs +++ b/app/components/audio-recorder/template.hbs @@ -1,3 +1,12 @@ {{ui/icon-button - label="Push & hold to record" + loading=saving + disabled=saving + mouseDown=(action 'startRecording') + mouseUp=(action 'stopRecording') leftIcon="microphone"}} + +{{#if hasAudio}} + {{ui/icon-button + click=(action "play") + leftIcon="play"}} +{{/if}} diff --git a/app/components/communication/video-chat/component.js b/app/components/communication/video-chat/component.js new file mode 100644 index 0000000..085a101 --- /dev/null +++ b/app/components/communication/video-chat/component.js @@ -0,0 +1,56 @@ +import Ember from 'ember'; + +const Video = Twilio.Video; + +export default Ember.Component.extend({ + startVideo() { + const { token, identity } = this.get("twilioData"); + + Video.connect(token, {name: "my-room"}) + .then(room => { + + this.attachTracks(room.localParticipant.tracks, this.$(".me:first")[0]); + + room.participants.forEach(::this.participantConnected); + room.on('participantConnected', ::this.participantConnected); + + room.on('participantDisconnected', ::this.participantDisconnected); + room.once('disconnected', error => room.participants.forEach(::this.participantDisconnected)); + }, e => console.log(e)); + }, + + attachTracks(tracks, container) { + tracks.forEach(track => this.attachTrack(track, container)); + }, + + attachTrack(track, container) { + container.appendChild(track.attach()); + }, + + removeTracks(tracks) { + tracks.forEach(track => this.removeTrack(track)); + }, + + removeTrack(track) { + track.detach().forEach(element => element.remove()); + }, + + participantConnected(participant) { + const container = this.$(".others:first")[0]; + + participant.on('trackAdded', track => ::this.attachTrack(track, container)); + participant.on('trackRemoved', track => ::this.removeTrack(track)); + + this.attachTracks(participant.tracks, container); + }, + + participantDisconnected(participant) { + this.removeTracks(participant.tracks); + }, + + actions: { + startVideo() { + this.startVideo(); + } + } +}); diff --git a/app/components/communication/video-chat/styles.styl b/app/components/communication/video-chat/styles.styl new file mode 100644 index 0000000..a212b12 --- /dev/null +++ b/app/components/communication/video-chat/styles.styl @@ -0,0 +1,25 @@ +& + position: fixed + bottom: 0 + right: 0 + +.previews + height: 75px + +video + border: 0 + margin: 0 + padding: 0 + width: 100px + +.vsc-controller + display: none + +.start-video + background: dope-blue + // padding: 0.5em + width: 100px + height: 100px + .icon-container + // font-size: 1.5em + color: white diff --git a/app/components/communication/video-chat/template.hbs b/app/components/communication/video-chat/template.hbs new file mode 100644 index 0000000..71f24c8 --- /dev/null +++ b/app/components/communication/video-chat/template.hbs @@ -0,0 +1,12 @@ +
+
+ +
+
+ +
+ {{ui/icon-button + class="start-video" + rightIcon="video-camera" + click=(action startVideo)}} +
diff --git a/app/components/conversation-group/component.js b/app/components/conversation-group/component.js new file mode 100644 index 0000000..473ed4e --- /dev/null +++ b/app/components/conversation-group/component.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; +import computed from 'ember-computed-decorators'; + +const { isPresent, computed: { alias, sort, gt } } = Ember; + +export default Ember.Component.extend({ + sortAsc: ["position:asc"], + sortedConversations: sort('model', 'sortAsc'), + conversationCount: alias("sortedConversations.length"), + hasConversations: gt("conversationCount", 0) +}); diff --git a/app/components/conversation-group/styles.styl b/app/components/conversation-group/styles.styl new file mode 100644 index 0000000..eca0332 --- /dev/null +++ b/app/components/conversation-group/styles.styl @@ -0,0 +1,13 @@ +& + margin-top: 3em + +.create-conversation + width: 100% + padding: 1em + color: dark-grey + font-size: 1em + opacity: 0.15 + transition: all 0.2s ease-in-out + +.create-conversation:hover + opacity: 1 diff --git a/app/components/conversation-group/template.hbs b/app/components/conversation-group/template.hbs new file mode 100644 index 0000000..7a85864 --- /dev/null +++ b/app/components/conversation-group/template.hbs @@ -0,0 +1,19 @@ +
+ {{ui/icon-button + class='create-conversation' + click=(action createConversation 0) + leftIcon="plus"}} + {{#each sortedConversations as |conversation index|}} + {{flash-card-group + model=conversation + index=(inc index) + onAudioCreated=onAudioCreated + createFlashCard=createFlashCard + destroyFlashCard=destroyFlashCard + saveModel=saveModel}} + {{ui/icon-button + class='create-conversation' + click=(action createConversation (inc index)) + leftIcon="plus"}} + {{/each}} +
diff --git a/app/components/flash-card-group/component.js b/app/components/flash-card-group/component.js new file mode 100644 index 0000000..d5aebf0 --- /dev/null +++ b/app/components/flash-card-group/component.js @@ -0,0 +1,20 @@ +import Ember from 'ember'; +import computed from 'ember-computed-decorators'; + +const { isPresent, computed: { alias, sort, gt } } = Ember; + +export default Ember.Component.extend({ + classNames: ['card-1'], + + sortAsc: ["position:asc"], + sortedFlashCards: sort('model.activeFlashCards', 'sortAsc'), + flashCardCount: alias("sortedFlashCards.length"), + hasFlashCards: gt("flashCardCount", 0), + + actions: { + handleUpdate(key, str) { + this.get("model").set(key, str); + this.get("saveModel")(this.get("model")); + } + } +}); diff --git a/app/components/flash-card-group/styles.styl b/app/components/flash-card-group/styles.styl new file mode 100644 index 0000000..13191e9 --- /dev/null +++ b/app/components/flash-card-group/styles.styl @@ -0,0 +1,39 @@ +& + width: 100% + padding: 1em + background: dope-blue + height: 24em + +.conversation-header + margin-left: calc(2em - 1em) + color: off-white + font-size: 1.5em + align-items: baseline + .index + margin-right: 0.25em + +.conversation-title + background: 0 + border: 0 + border-bottom: 1px dashed + color: off-white + font-size: 1.3em + width: 16em + +.conversation-title::placeholder + color: rgba(off-white, 0.5) + +.flash-card-container + overflow-x: auto + overflow-y: hidden + margin: 1em 0 + +.create-flash-card + color: white + font-size: 1em + padding: 1em + opacity: 0.15 + transition: all 0.2s ease-in-out + +.create-flash-card:hover + opacity: 1 diff --git a/app/components/flash-card-group/template.hbs b/app/components/flash-card-group/template.hbs new file mode 100644 index 0000000..6ebbb40 --- /dev/null +++ b/app/components/flash-card-group/template.hbs @@ -0,0 +1,31 @@ +
+ + {{index}}. + + {{one-way-input model.title + class='conversation-title' + placeholder='What\'s this conversation about...' + update=(action "handleUpdate" "title") + autocomplete="off" + autocorrect="off" + autocapitalize="off" + spellcheck=false}} +
+
+ {{ui/icon-button + class="create-flash-card" + leftIcon="plus" + click=(action createFlashCard model 0)}} + {{#each sortedFlashCards as |flashCard index|}} + {{flash-card + model=flashCard + index=(inc index) + onAudioCreated=onAudioCreated + destroyFlashCard=(action destroyFlashCard model) + saveModel=saveModel}} + {{ui/icon-button + class="create-flash-card" + leftIcon="plus" + click=(action createFlashCard model (inc index))}} + {{/each}} +
diff --git a/app/components/flash-card/component.js b/app/components/flash-card/component.js index 5b366bb..abcd94f 100644 --- a/app/components/flash-card/component.js +++ b/app/components/flash-card/component.js @@ -1,7 +1,6 @@ import Ember from 'ember'; import _ from 'lodash'; import computed from 'ember-computed-decorators'; -import firebase from 'firebase'; const TONE_REGEX = /([aeiouAEIOU][1234])/g; @@ -62,16 +61,7 @@ const { } = Ember; export default Ember.Component.extend({ - classNames: ['stretch'], - - hifi: Ember.inject.service(), - - @computed('total', 'index') - position(total, index) { - return total - index; - }, - - hasAudio: notEmpty('model.audioUrl'), + classNames: ['card-2', 'stretch'], setSelection(elm, start, end) { elm.selectionStart = start; @@ -100,16 +90,6 @@ export default Ember.Component.extend({ handleUpdate(key, str) { this.get("model").set(key, str); this.get("saveModel")(this.get("model")); - }, - - play() { - const app = firebase.app(); - var storageRef = app.storage().ref(); - var audioRef = storageRef.child(this.get('model.audioUrl')); - - audioRef - .getDownloadURL() - .then(url => this.get("hifi").play(url)); } } }); diff --git a/app/components/flash-card/styles.styl b/app/components/flash-card/styles.styl index 6b7deb1..91e868b 100644 --- a/app/components/flash-card/styles.styl +++ b/app/components/flash-card/styles.styl @@ -1,66 +1,46 @@ & - background: #FFB928 + background: dark-orange color: #2E282A - margin-bottom: 1em - border: 5px solid rgba(black, 0.25) - max-width: 80em - -&:nth-child(odd) - background: rgba(#59B5C1, 0.5) + min-width: 18em + width: 18em + margin: 0.5em .inputContainer - width: 100% - margin-right: 1em - .title - font-weight: 600 - height: 1.5em - padding-left: 0.2em - align-items: flex-end - -.inputContainer:last-child - margin-right: 0 + margin-left: 1em + margin-right: calc(1em + 0.25em) textarea + width: 100% resize: none - width: calc(100% - 10px - 0.2em) - padding: 0.2em - font-size: 1.5em - border: 5px solid rgba(black, 0.25) - - +below(950px) - font-size: 1.4em - +below(900px) - font-size: 1.3em - +below(850px) - font-size: 1.2em - +below(800px) - font-size: 1.1em - +below(750px) - font-size: 1em - +below(700px) - margin: 0.25em 0 - font-size: 1.4em - width: calc(100% - 10px - 0.2em) + font-weight: 600 + font-size: 1em + margin-bottom: 0.5em + border: 0 + padding: 0.25em + background: rgba(white, 0.75) .fieldsContainer - margin: 1em + // margin: 0.5em +below(700px) flex-direction: column header background: rgba(white, 0.5) - padding: 0.5em + align-items: baseline + margin-bottom: 1em + .delete + background: none + margin: 0 + padding: 0.5em + .index - margin-left: 1em - font-size: 1.5em + margin-left: 0.5em + font-size: 1.25em .audioUiContainer justify-content: flex-end margin: 1em - .btn - margin-left: 1em - border-radius: 3px hr border: 1px solid rgba(white, 0.5) diff --git a/app/components/flash-card/template.hbs b/app/components/flash-card/template.hbs index e37c5c2..322488f 100644 --- a/app/components/flash-card/template.hbs +++ b/app/components/flash-card/template.hbs @@ -1,6 +1,6 @@
- {{position}}. + {{index}}.
@@ -8,14 +8,15 @@
{{ui/icon-button label="X" - click=destroyModel}} + class='delete' + click=(action destroyFlashCard model)}}
-
+
-
汉语
{{one-way-textarea model.chinese + placeholder='汉语...' update=(action "handleUpdate" "chinese") autocomplete="off" autocorrect="off" @@ -24,8 +25,8 @@
-
Pinyin
{{one-way-textarea model.pinyin + placeholder='Pinyin...' class="pinyinEditor" update=(action 'processChange') autocomplete="off" @@ -35,8 +36,8 @@
-
English
{{one-way-textarea model.english + placeholder='English...' update=(action "handleUpdate" "english") autocomplete="off" autocorrect="off" @@ -45,16 +46,8 @@
-
-
- {{audio-recorder - created=onAudioCreated}} - - {{#if hasAudio}} - {{ui/icon-button - label="Play" - click=(action "play") - leftIcon="play"}} - {{/if}} + {{audio-recorder + model=model + created=(action onAudioCreated model)}}
diff --git a/app/components/navs/lesson-nav/component.js b/app/components/navs/lesson-nav/component.js index 926b613..0449065 100644 --- a/app/components/navs/lesson-nav/component.js +++ b/app/components/navs/lesson-nav/component.js @@ -1,4 +1,9 @@ import Ember from 'ember'; +import computed from 'ember-computed-decorators'; export default Ember.Component.extend({ + @computed('lessonId') + shareLink(lessonId) { + return `https://pin.else.run/lesson/${lessonId}`; + } }); diff --git a/app/components/navs/lesson-nav/styles.styl b/app/components/navs/lesson-nav/styles.styl index c93a10a..aafda8b 100644 --- a/app/components/navs/lesson-nav/styles.styl +++ b/app/components/navs/lesson-nav/styles.styl @@ -1,5 +1,6 @@ & position: relative + z-index: 1000 .uiContainer position: fixed @@ -13,6 +14,9 @@ +below(540px) flex-direction: column +.home-button + color: off-white + .leftContainer margin-left: 2em +below(540px) diff --git a/app/components/navs/lesson-nav/template.hbs b/app/components/navs/lesson-nav/template.hbs index 22ed534..a550e3a 100644 --- a/app/components/navs/lesson-nav/template.hbs +++ b/app/components/navs/lesson-nav/template.hbs @@ -1,20 +1,15 @@
{{ui/icon-button + class="home-button" label="Home" - click=navigateHome - leftIcon="arrow-left"}} + click=navigateHome}} - {{#copy-button +
- - {{ui/icon-button - label="Create Flashcard" - rightIcon="plus" - click=createFlashCard}}
diff --git a/app/components/ui/icon-button/component.js b/app/components/ui/icon-button/component.js index c566d49..55b8a36 100644 --- a/app/components/ui/icon-button/component.js +++ b/app/components/ui/icon-button/component.js @@ -3,7 +3,7 @@ import Ember from "ember"; const { notEmpty } = Ember.computed; export default Ember.Component.extend({ - classNames: ["row", "btn"], + classNames: ["btn"], hasLabel: notEmpty("label"), hasLeftIcon: notEmpty("leftIcon"), diff --git a/app/components/ui/icon-button/styles.styl b/app/components/ui/icon-button/styles.styl index 18ae556..d6e5b9f 100644 --- a/app/components/ui/icon-button/styles.styl +++ b/app/components/ui/icon-button/styles.styl @@ -2,8 +2,6 @@ cursor: pointer align-items: center justify-content: center - padding: 0.5rem - background: #F8F9F2 &:active opacity: 0.5 @@ -11,13 +9,33 @@ .label font-size: 1rem -.spacer - margin-right: 0.5rem - -.iconContainer - font-size: 0.5rem +.icon-container align-items: center justify-content: center -.content - padding: 0 1em +.spin-clockwise { + -webkit-animation: load8 1.1s infinite linear; + animation: load8 1.1s infinite linear; +} + +@-webkit-keyframes load8 { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes load8 { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} diff --git a/app/components/ui/icon-button/template.hbs b/app/components/ui/icon-button/template.hbs index 853475f..438ac10 100644 --- a/app/components/ui/icon-button/template.hbs +++ b/app/components/ui/icon-button/template.hbs @@ -1,25 +1,33 @@ -
- {{#if hasLeftIcon}} -
- {{fa-icon leftIcon}} -
- {{/if}} +
+ {{#if loading}} +
+ {{fa-icon 'spinner'}} +
+ {{else}} + {{#if hasLeftIcon}} +
+ {{fa-icon leftIcon}} +
+ {{/if}} - {{#if (and hasLabel hasLeftIcon)}} -
- {{/if}} + {{#if (and hasLabel hasLeftIcon)}} +
+ {{/if}} - {{#if hasLabel}} - {{label}} - {{/if}} + {{#if hasLabel}} + {{label}} + {{/if}} + + {{#if (and hasLabel hasRightIcon)}} +
+ {{/if}} + + {{#if hasRightIcon}} +
+ {{fa-icon rightIcon}} +
+ {{/if}} - {{#if (and hasLabel hasRightIcon)}} -
- {{/if}} - {{#if hasRightIcon}} -
- {{fa-icon rightIcon}} -
{{/if}}
diff --git a/app/controllers/.gitkeep b/app/controllers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/controllers/session.js b/app/controllers/session.js deleted file mode 100644 index 68eeacc..0000000 --- a/app/controllers/session.js +++ /dev/null @@ -1,20 +0,0 @@ -import Ember from 'ember'; -import computed from 'ember-computed-decorators'; - -const { isPresent, computed: { alias, sort } } = Ember; - -export default Ember.Controller.extend({ - timestampSorting: ['ts:desc'], - sortedFlashCards: sort('session.flashCards', 'timestampSorting'), - cardCount: alias("sortedFlashCards.length"), - - @computed("sortedFlashCards") - hasFlashCards(collection) { - return isPresent(collection); - }, - - @computed('sessionId') - shareLink(sessionId) { - return `https://pin.else.run/session/${sessionId}`; - } -}); diff --git a/app/index.html b/app/index.html index e4d8a79..e2b628a 100644 --- a/app/index.html +++ b/app/index.html @@ -14,6 +14,8 @@ + + {{content-for "head-footer"}} diff --git a/app/models/conversation.js b/app/models/conversation.js new file mode 100644 index 0000000..0a3f425 --- /dev/null +++ b/app/models/conversation.js @@ -0,0 +1,17 @@ +import DS from 'ember-data'; +import computed from 'ember-computed-decorators'; + +const { empty, filterBy } = Ember.computed; + +export default DS.Model.extend({ + title: DS.attr('string'), + lesson: DS.belongsTo('lesson'), + ts: DS.attr('number'), + position: DS.attr('number', {defaultValue: 1}), + + flashCards: DS.hasMany('flash-card'), + + activeFlashCards: filterBy('flashCards', 'isDeleted', false), + + isEmpty: empty("activeFlashCards") +}); diff --git a/app/models/flash-card.js b/app/models/flash-card.js index 074817b..7b0463d 100644 --- a/app/models/flash-card.js +++ b/app/models/flash-card.js @@ -6,6 +6,7 @@ export default DS.Model.extend({ english: DS.attr('string'), audioUrl: DS.attr('string'), ts: DS.attr('number'), + position: DS.attr('number', {defaultValue: 1}), - lesson: DS.belongsTo('lesson') + conversation: DS.belongsTo('conversation') }); diff --git a/app/models/lesson.js b/app/models/lesson.js index 0c798b7..60df22c 100644 --- a/app/models/lesson.js +++ b/app/models/lesson.js @@ -1,6 +1,6 @@ import DS from 'ember-data'; export default DS.Model.extend({ - flashCards: DS.hasMany('flash-card'), + conversations: DS.hasMany('conversation'), date: DS.attr('date') }); diff --git a/app/router.js b/app/router.js index e51855c..46d8eb6 100644 --- a/app/router.js +++ b/app/router.js @@ -7,7 +7,7 @@ const Router = Ember.Router.extend({ }); Router.map(function() { - this.route('session', {path:'/session/:id'}); + this.route('lesson', {path:'/lesson/:id'}); }); export default Router; diff --git a/app/routes/index.js b/app/routes/index.js index 885764a..c59b264 100644 --- a/app/routes/index.js +++ b/app/routes/index.js @@ -3,8 +3,8 @@ import { v4 as uuid} from 'uuid'; export default Ember.Route.extend({ actions: { - createSession() { - this.transitionTo(`/session/${uuid()}`) + createLesson() { + this.transitionTo(`/lesson/${uuid()}`) } } }); diff --git a/app/routes/lesson.js b/app/routes/lesson.js new file mode 100644 index 0000000..6aff8b2 --- /dev/null +++ b/app/routes/lesson.js @@ -0,0 +1,154 @@ +import Ember from 'ember'; +import firebase from 'firebase'; +import { v4 as uuid } from 'uuid'; +import moment from 'moment'; +import _ from 'lodash'; + +export default Ember.Route.extend({ + setupController(controller, model) { + const { record, lessonId, twilioData } = model; + + controller.set('lesson', record); + controller.set('twilioData', twilioData); + controller.set('lessonId', lessonId); + + this._super(...arguments); + }, + + async model(params) { + const twilioDataRes = await fetch("https://wt-brancusi-gmail_com-0.run.webtask.io/create-twilio-token"); + const twilioData = await twilioDataRes.json(); + + const record = await this.store.findRecord('lesson', params.id) + .catch(() => { + const lesson = this.store.createRecord('lesson', {id:params.id, date:new Date()}), + conversation = this.store.createRecord('conversation', {lesson}), + flashCard = this.store.createRecord('flash-card', {conversation}); + + return lesson.save() + .then(lesson => conversation.save()) + .then(conversation => flashCard.save()) + .then(flashCard => lesson); + }); + + return { + record, + twilioData, + lessonId: params.id + } + }, + + async createConversation(lesson, position = 0) { + const conversations = await lesson.get("conversations"); + + const sortedConversations = conversations.sortBy("position"); + + const prevCard = sortedConversations[position - 1]; + const nextCard = sortedConversations[position]; + + const prev = Ember.isPresent(prevCard) ? prevCard.get("position") : undefined; + const next = Ember.isPresent(nextCard) ? nextCard.get("position") : undefined; + + let newPosition; + + if(prev === undefined && next !== undefined) { + newPosition = next/2; + } + + if(prev !== undefined && next === undefined) { + newPosition = prev + 1; + } + + if(prev !== undefined && next !== undefined) { + newPosition = (next - prev)/2 + prev; + } + + if(prev === undefined && next === undefined) { + newPosition = 1; + } + + const conversation = this.store + .createRecord('conversation', { lesson, position: newPosition }); + + const flashCard = this.store + .createRecord('flash-card', { conversation }); + + await conversation.save(); + await flashCard.save(); + await lesson.save(); + }, + + actions: { + onAudioCreated(model, blob) { + const app = firebase.app(); + var storageRef = app.storage().ref(); + + const path = `audio/${uuid()}.wav`; + var audioRef = storageRef.child(path); + + return audioRef.put(blob).then(() => { + model.set("audioUrl", path); + return model.save(); + }); + }, + + navigateHome() { + this.transitionTo("index"); + }, + + saveModel(model) { + model.save(); + }, + + async destroyFlashCard(conversation, flashCard) { + flashCard.deleteRecord(); + + if(conversation.get("isEmpty")) { + conversation.deleteRecord(); + } + + await flashCard.save(); + await conversation.save(); + }, + + createConversation(lesson, position) { + this.createConversation(lesson, position); + }, + + async createFlashCard(conversation, position) { + const flashCards = await conversation.get("flashCards"); + + const sortedFlashCards = flashCards.sortBy("position"); + + const leftCard = sortedFlashCards[position - 1]; + const rightCard = sortedFlashCards[position]; + + const left = Ember.isPresent(leftCard) ? leftCard.get("position") : undefined; + const right = Ember.isPresent(rightCard) ? rightCard.get("position") : undefined; + + let newPosition; + + if(left === undefined && right !== undefined) { + newPosition = right/2; + } + + if(left !== undefined && right === undefined) { + newPosition = left + 1; + } + + if(left !== undefined && right !== undefined) { + newPosition = (right - left)/2 + left; + } + + if(left === undefined && right === undefined) { + newPosition = 1; + } + + const flashCard = this.store + .createRecord('flash-card', { conversation, position: newPosition }); + + await flashCard.save(); + await conversation.save(); + } + } +}); diff --git a/app/routes/session.js b/app/routes/session.js deleted file mode 100644 index 0e6f2bb..0000000 --- a/app/routes/session.js +++ /dev/null @@ -1,70 +0,0 @@ -import Ember from 'ember'; -import firebase from 'firebase'; -import { v4 as uuid } from 'uuid'; -import moment from 'moment'; - -export default Ember.Route.extend({ - setupController(controller, model) { - const { record, sessionId } = model; - - controller.set('session', record); - controller.set('sessionId', sessionId); - - this._super(...arguments); - }, - - async model(params) { - const record = await this.store.findRecord('lesson', params.id) - .catch(() => { - return this.store - .createRecord('lesson', {id:params.id, date:new Date()}) - .save(); - }); - - return { - record, - sessionId: params.id - } - }, - - actions: { - onAudioCreated(model, blob) { - const app = firebase.app(); - var storageRef = app.storage().ref(); - const id = uuid(); - - const path = `audio/${id}.wav`; - var audioRef = storageRef.child(path); - - audioRef.put(blob).then(() => { - model.set("audioUrl", path); - model.save(); - }); - }, - - navigateHome() { - this.transitionTo("index"); - }, - - saveModel(model) { - model.save(); - }, - - // @TODO: Not sure why this is failing. - async destroyModel(flashCard, lesson) { - await flashCard - .destroyRecord() - .catch(() => {}); - - await lesson.save(); - }, - - async createFlashCard(lesson) { - await this.store - .createRecord('flash-card', { lesson, ts:moment().valueOf() }) - .save(); - - await lesson.save(); - } - } -}); diff --git a/app/styles/app.styl b/app/styles/app.styl index 093c11a..22d13f3 100644 --- a/app/styles/app.styl +++ b/app/styles/app.styl @@ -1,8 +1,10 @@ @lost flexbox flex @import "variables" +@import "color" @import "reset" @import "layout" @import "typography" +@import "material" @import "utils" @import "pod-styles" diff --git a/app/styles/color.styl b/app/styles/color.styl new file mode 100644 index 0000000..6e74a2a --- /dev/null +++ b/app/styles/color.styl @@ -0,0 +1,19 @@ +body-color = #F4F4F4 +dope-grey = #1E1E1E +dope-pink = #F94848 +dope-green = #00ae9c + +slate = #232323 +off-white = #F9F9F9 +light-grey = #C9C9C9 +medium-grey = #EBEDEC +dark-grey = #4C4C4C +sky-blue = #00B3DD +baby-blue = #BDEFFF +light-blue = #F3FDFF +light-green = #DEFFEE +dark-green = #4ACE86 +dark-orange = #FFB928 +hot-pink = #FF646C +dope-blue = #4DA6FF +light-orange = #F5E385 diff --git a/app/styles/layout.styl b/app/styles/layout.styl index c034fe1..cd07419 100644 --- a/app/styles/layout.styl +++ b/app/styles/layout.styl @@ -22,6 +22,7 @@ .stretch width: 100% + height: 100% .space-between justify-content: space-between @@ -39,10 +40,6 @@ for num in (1..9) flex: num // Refactor out into components -.flashCardContainer - margin-top: 4em - padding: 1em - .landingContainer padding: 5em max-width: 60em diff --git a/app/styles/material.styl b/app/styles/material.styl new file mode 100644 index 0000000..80c4744 --- /dev/null +++ b/app/styles/material.styl @@ -0,0 +1,26 @@ +.card + border-radius: 2px + transition: all 0.2s ease-in-out + +.card-1 + @extend .card + box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24) + +.card-2 + @extend .card + box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23) + + +.card-3 + @extend .card + box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23) + + +.card-4 + @extend .card + box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22) + + +.card-5 + @extend .card + box-shadow: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22) diff --git a/app/styles/reset.styl b/app/styles/reset.styl index 32b8e0e..e19c4a2 100644 --- a/app/styles/reset.styl +++ b/app/styles/reset.styl @@ -16,3 +16,30 @@ button margin: 0 padding: 0 border: 0 + +::-webkit-scrollbar + width: 11px + height: 11px + +::-webkit-scrollbar-button + width: 0px + height: 0px + +::-webkit-scrollbar-thumb + background: #e1e1e1 + border: 0px none white + border-radius: 50px + +::-webkit-scrollbar-thumb:hover + background: white + +::-webkit-scrollbar-thumb:active + background: rgba(white, 0.9) + +::-webkit-scrollbar-track + background: rgba(white, 0.5) + border: 0px none white + border-radius: 50px + +::-webkit-scrollbar-corner + background: transparent diff --git a/app/styles/variables.styl b/app/styles/variables.styl index a65d37f..2812e90 100644 --- a/app/styles/variables.styl +++ b/app/styles/variables.styl @@ -1,6 +1 @@ rupture.enable-em-breakpoints = true - -body-color = #F4F4F4 -dope-grey = #1E1E1E -dope-pink = #F94848 -dope-green = #00ae9c diff --git a/app/templates/index.hbs b/app/templates/index.hbs index 5783c3e..6b9535d 100644 --- a/app/templates/index.hbs +++ b/app/templates/index.hbs @@ -8,7 +8,7 @@
{{ui/icon-button label="Start Lesson" - click=(route-action "createSession") + click=(route-action "createLesson") leftIcon="plus"}}
diff --git a/app/templates/lesson.hbs b/app/templates/lesson.hbs new file mode 100644 index 0000000..1a58a81 --- /dev/null +++ b/app/templates/lesson.hbs @@ -0,0 +1,13 @@ +{{navs/lesson-nav + lessonId=lessonId + navigateHome=(route-action "navigateHome")}} + +{{conversation-group + createConversation=(route-action "createConversation" lesson) + onAudioCreated=(route-action 'onAudioCreated') + createFlashCard=(route-action 'createFlashCard') + saveModel=(route-action 'saveModel') + destroyFlashCard=(route-action 'destroyFlashCard') + model=lesson.conversations}} + + diff --git a/app/templates/session.hbs b/app/templates/session.hbs deleted file mode 100644 index c17aa02..0000000 --- a/app/templates/session.hbs +++ /dev/null @@ -1,20 +0,0 @@ -{{navs/lesson-nav - shareLink=shareLink - navigateHome=(route-action "navigateHome") - createFlashCard=(route-action "createFlashCard" session)}} - -
- {{#if hasFlashCards}} - {{#each sortedFlashCards as |flashCard index|}} - {{flash-card - model=flashCard - total=cardCount - index=index - onAudioCreated=(route-action 'onAudioCreated' flashCard) - destroyModel=(route-action 'destroyModel' flashCard session) - saveModel=(route-action 'saveModel')}} - {{/each}} - {{else}} -

No flash cards yet. Why not create one?

- {{/if}} -
diff --git a/tests/integration/components/communication/video-chat/component-test.js b/tests/integration/components/communication/video-chat/component-test.js new file mode 100644 index 0000000..cd0028e --- /dev/null +++ b/tests/integration/components/communication/video-chat/component-test.js @@ -0,0 +1,25 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('communication/video-chat', 'Integration | Component | communication/video chat', { + integration: true +}); + +test('it renders', function(assert) { + + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + + this.render(hbs`{{communication/video-chat}}`); + + assert.equal(this.$().text().trim(), ''); + + // Template block usage: + this.render(hbs` + {{#communication/video-chat}} + template block text + {{/communication/video-chat}} + `); + + assert.equal(this.$().text().trim(), 'template block text'); +}); diff --git a/tests/integration/components/conversation-group/component-test.js b/tests/integration/components/conversation-group/component-test.js new file mode 100644 index 0000000..54fd555 --- /dev/null +++ b/tests/integration/components/conversation-group/component-test.js @@ -0,0 +1,25 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('conversation-group', 'Integration | Component | conversation group', { + integration: true +}); + +test('it renders', function(assert) { + + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + + this.render(hbs`{{conversation-group}}`); + + assert.equal(this.$().text().trim(), ''); + + // Template block usage: + this.render(hbs` + {{#conversation-group}} + template block text + {{/conversation-group}} + `); + + assert.equal(this.$().text().trim(), 'template block text'); +}); diff --git a/tests/integration/components/flash-card-group/component-test.js b/tests/integration/components/flash-card-group/component-test.js new file mode 100644 index 0000000..ce1651e --- /dev/null +++ b/tests/integration/components/flash-card-group/component-test.js @@ -0,0 +1,25 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('flash-card-group', 'Integration | Component | flash card group', { + integration: true +}); + +test('it renders', function(assert) { + + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + + this.render(hbs`{{flash-card-group}}`); + + assert.equal(this.$().text().trim(), ''); + + // Template block usage: + this.render(hbs` + {{#flash-card-group}} + template block text + {{/flash-card-group}} + `); + + assert.equal(this.$().text().trim(), 'template block text'); +}); diff --git a/tests/unit/controllers/session-test.js b/tests/unit/controllers/lesson-test.js similarity index 81% rename from tests/unit/controllers/session-test.js rename to tests/unit/controllers/lesson-test.js index f26028e..00f3651 100644 --- a/tests/unit/controllers/session-test.js +++ b/tests/unit/controllers/lesson-test.js @@ -1,6 +1,6 @@ import { moduleFor, test } from 'ember-qunit'; -moduleFor('controller:session', 'Unit | Controller | session', { +moduleFor('controller:lesson', 'Unit | Controller | lesson', { // Specify the other units that are required for this test. // needs: ['controller:foo'] }); diff --git a/tests/unit/models/conversation-test.js b/tests/unit/models/conversation-test.js new file mode 100644 index 0000000..f2d0aeb --- /dev/null +++ b/tests/unit/models/conversation-test.js @@ -0,0 +1,12 @@ +import { moduleForModel, test } from 'ember-qunit'; + +moduleForModel('conversation', 'Unit | Model | conversation', { + // Specify the other units that are required for this test. + needs: ['model:lesson', 'model:flash-card'] +}); + +test('it exists', function(assert) { + let model = this.subject(); + // let store = this.store(); + assert.ok(!!model); +}); diff --git a/tests/unit/routes/session-test.js b/tests/unit/routes/lesson-test.js similarity index 81% rename from tests/unit/routes/session-test.js rename to tests/unit/routes/lesson-test.js index bd32881..9f0aa77 100644 --- a/tests/unit/routes/session-test.js +++ b/tests/unit/routes/lesson-test.js @@ -1,6 +1,6 @@ import { moduleFor, test } from 'ember-qunit'; -moduleFor('route:session', 'Unit | Route | session', { +moduleFor('route:lesson', 'Unit | Route | lesson', { // Specify the other units that are required for this test. // needs: ['controller:foo'] });