diff --git a/.eslintrc.js b/.eslintrc.js index 2e92f79b..8b48a31a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -24,6 +24,7 @@ module.exports = { }, rules: { 'qunit/require-expect': [1, 'except-simple'], + 'no-self-assign': ['warn'], }, overrides: [ // node files diff --git a/app/components/answer-reply-modal.hbs b/app/components/answer-reply-modal.hbs new file mode 100644 index 00000000..668b3c81 --- /dev/null +++ b/app/components/answer-reply-modal.hbs @@ -0,0 +1,46 @@ + +
+

Host asked you a question😀

+
+ +

+ ¡ + Do not use abusive words, this event is moderated!

+
+ + +
+ +
+ +
\ No newline at end of file diff --git a/app/components/answer-view-card.hbs b/app/components/answer-view-card.hbs new file mode 100644 index 00000000..a5773969 --- /dev/null +++ b/app/components/answer-view-card.hbs @@ -0,0 +1,39 @@ +
+

+ {{this.answerText}} + + {{#if this.isTextMoreThanMaxCharacters}} + + {{/if}} +

+ +
+ + +
+
\ No newline at end of file diff --git a/app/components/answer-view-card.js b/app/components/answer-view-card.js new file mode 100644 index 00000000..b69493b4 --- /dev/null +++ b/app/components/answer-view-card.js @@ -0,0 +1,36 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { readMoreFormatter } from '../utils/common-utils'; +import { action } from '@ember/object'; +import { ANSWER_STATUS } from '../constants/live'; + +const maxCharactersToShow = 70; +export default class AnswerViewCardComponent extends Component { + @tracked answerText = readMoreFormatter( + this.args.answerObject.answer, + maxCharactersToShow, + ); + + @tracked isTextMoreThanMaxCharacters = + this.args.answerObject.answer?.length > maxCharactersToShow; + @tracked isReadMoreEnabled = false; + @tracked readMoreOrLessText = this.isReadMoreEnabled ? 'Less' : 'More'; + @tracked isApproved = + this.args.answerObject.status === ANSWER_STATUS.APPROVED; + @tracked isPending = this.args.answerObject.status === ANSWER_STATUS.PENDING; + @tracked isRejected = + this.args.answerObject.status === ANSWER_STATUS.REJECTED; + + @action toggleReadMore() { + this.isReadMoreEnabled = !this.isReadMoreEnabled; + this.readMoreOrLessText = this.isReadMoreEnabled ? 'Less' : 'More'; + if (this.isReadMoreEnabled) { + this.answerText = this.args.answerObject.answer; + } else { + this.answerText = readMoreFormatter( + this.args.answerObject.answer, + maxCharactersToShow, + ); + } + } +} diff --git a/app/components/ask-question-modal.hbs b/app/components/ask-question-modal.hbs new file mode 100644 index 00000000..9fe79e34 --- /dev/null +++ b/app/components/ask-question-modal.hbs @@ -0,0 +1,62 @@ + +
+

Ask Question

+ +
+ + +
+ +
+ + +
+
+
\ No newline at end of file diff --git a/app/components/events/survey-page.hbs b/app/components/events/survey-page.hbs new file mode 100644 index 00000000..9d09a710 --- /dev/null +++ b/app/components/events/survey-page.hbs @@ -0,0 +1,68 @@ +
+ +
+ +
+

Recent Question

+

{{(or + @questionAsked.question "No recent question" + )}}

+
+
+
+

+ Answers +

+
+ + +
+
+ {{#if this.isAnswersPresent}} + {{#each this.answers as |answer|}} + + {{/each}} + {{else}} +
No answers present currently!
+ {{/if}} + +
+
+
\ No newline at end of file diff --git a/app/components/events/survey-page.js b/app/components/events/survey-page.js new file mode 100644 index 00000000..80d94d56 --- /dev/null +++ b/app/components/events/survey-page.js @@ -0,0 +1,161 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { debounce } from '@ember/runloop'; +import { inject as service } from '@ember/service'; +import { + QUESTION_DEBOUNCE_TIME, + QUESTION_MIN_LENGTH, + POST_API_CONFIGS, +} from '../../constants/live'; +import { APPS } from '../../constants/urls'; +import { TOAST_OPTIONS } from '../../constants/toast-options'; + +export default class SurveyPageComponent extends Component { + @service live; + @service login; + @service toast; + @tracked isMaxCharactersChecked = false; + @tracked isAskQuestionModalOpen = false; + @tracked maxCharacters; + @tracked question = ''; + @tracked isQuestionValid = false; + @tracked isQuestionSubmitButtonDisabled = true; + @tracked isQuestionApiLoading = false; + @tracked ANSWER_STATUS_FILTERS = ['ALL', 'PENDING', 'APPROVED', 'REJECTED']; + @tracked activeAnswerFilterValue = 'ALL'; + @tracked userData = this.login?.userData; + get isAnswersPresent() { + return Boolean(this.answers?.length); + } + + get answers() { + const allAnswers = this.args.answers; + + if (this.activeAnswerFilterValue === 'ALL') return allAnswers; + + const answerToShow = allAnswers.filter( + (answer) => answer.status === this.activeAnswerFilterValue, + ); + + return answerToShow; + } + + get isAskQuestionButtonDisabled() { + return !this.userData?.roles?.super_user; + } + + @action onAnswerFilterChange(event) { + this.activeAnswerFilterValue = event.target.value; + } + + @action openAskQuestionModal() { + this.isAskQuestionModalOpen = true; + } + + @action closeAskQuestionModal() { + this.isAskQuestionModalOpen = false; + this.onQuestionModalUnmount(); + } + + @action async onQuestionSubmit() { + this.isQuestionApiLoading = true; + const questionBody = { + question: this.question.trim(), + createdBy: this.login.userData.id, + eventId: this.live.activeRoomId, + maxCharacters: this.maxCharacters || null, + }; + + try { + const questionResponse = await fetch(`${APPS.API_BACKEND}/questions`, { + ...POST_API_CONFIGS, + body: JSON.stringify(questionBody), + }); + const question = await questionResponse.json(); + + if (!questionResponse.ok) + return this.toast.error( + question.message, + question.error, + TOAST_OPTIONS, + ); + + this.toast.success(question.message, question.error, TOAST_OPTIONS); + } catch (error) { + console.error(error); + } finally { + this.isQuestionApiLoading = false; + this.onQuestionModalUnmount(); + this.isAskQuestionModalOpen = false; + } + } + + @action toggleMaxCharacterChecked() { + this.isMaxCharactersChecked = !this.isMaxCharactersChecked; + + if (!this.isMaxCharactersChecked) { + this.maxCharacters = null; + } + + if (!this.isMaxCharactersChecked && this.isQuestionValid) { + this.isQuestionSubmitButtonDisabled = false; + } else { + this.isQuestionSubmitButtonDisabled = true; + } + } + + @action onCharacterLimitInput(event) { + this.maxCharacters = event.target.value && Number(event.target.value); + + if (!this.isMaxCharactersChecked && this.isQuestionValid) { + this.isQuestionSubmitButtonDisabled = false; + return; + } + + if (this.maxCharacters && this.isQuestionValid) { + this.isQuestionSubmitButtonDisabled = false; + return; + } + + this.isQuestionSubmitButtonDisabled = true; + } + + @action onQuestionInput(event) { + const setQuestion = () => { + this.question = event.target.value; + + if (this.question.length > QUESTION_MIN_LENGTH) { + this.isQuestionValid = true; + } else { + this.isQuestionValid = false; + } + + if (!this.isMaxCharactersChecked && this.isQuestionValid) { + this.isQuestionSubmitButtonDisabled = false; + return; + } + + if ( + this.isMaxCharactersChecked && + this.maxCharacters && + this.isQuestionValid + ) { + this.isQuestionSubmitButtonDisabled = false; + return; + } + + this.isQuestionSubmitButtonDisabled = true; + }; + + debounce(setQuestion, QUESTION_DEBOUNCE_TIME, event.target.value); + } + + onQuestionModalUnmount() { + this.isMaxCharactersChecked = false; + this.isQuestionValid = false; + this.isQuestionSubmitButtonDisabled = true; + this.question = ''; + this.maxCharacters = null; + } +} diff --git a/app/components/live-header.hbs b/app/components/live-header.hbs index 40d8e018..d6ae77ea 100644 --- a/app/components/live-header.hbs +++ b/app/components/live-header.hbs @@ -1,27 +1,37 @@ - -
+ +
{{@activeTab}}
-
+
{{#each @tabs as |tab|}} - {{#if (eq tab.label 'Logs')}} + {{#if (eq tab.label "Logs")}} {{#if this.live.userData.roles.super_user}} + {{/if}} + {{else if (eq tab.label "Survey")}} + {{#if this.isWordCloudFeatureOn}} + {{/if}} @@ -30,7 +40,7 @@ @tabId={{tab.id}} @test={{tab.label}} @label={{tab.label}} - @variant={{if tab.active 'active' ''}} + @variant={{if tab.active "active" ""}} @onClick={{@tabHandler}} /> {{/if}} diff --git a/app/components/live-header.js b/app/components/live-header.js index e359cd19..b8b446be 100644 --- a/app/components/live-header.js +++ b/app/components/live-header.js @@ -5,7 +5,13 @@ import { inject as service } from '@ember/service'; export default class LiveHeaderComponent extends Component { @service live; + @service featureFlag; @tracked isTabOpen = false; + + get isWordCloudFeatureOn() { + return this.featureFlag.isWordCloud; + } + @action toggleTabs() { this.isTabOpen = !this.isTabOpen; } diff --git a/app/components/reusables/button.hbs b/app/components/reusables/button.hbs index 21d07845..60c9caf1 100644 --- a/app/components/reusables/button.hbs +++ b/app/components/reusables/button.hbs @@ -1,14 +1,15 @@ \ No newline at end of file diff --git a/app/components/reusables/input-box.hbs b/app/components/reusables/input-box.hbs index 031269ef..4d842bb4 100644 --- a/app/components/reusables/input-box.hbs +++ b/app/components/reusables/input-box.hbs @@ -1,17 +1,25 @@ -
- +
+ {{#if @required}} - * + * {{/if}} + {{#if @shouldShowHelperText}} +

{{@helperText}}

+ {{/if}}
\ No newline at end of file diff --git a/app/components/word-cloud.hbs b/app/components/word-cloud.hbs new file mode 100644 index 00000000..9752b6f7 --- /dev/null +++ b/app/components/word-cloud.hbs @@ -0,0 +1,6 @@ +{{! word cloud forms in this component with the help of d3.js }} +{{#if @isWordCloud}} +
+ +
+{{/if}} \ No newline at end of file diff --git a/app/components/word-cloud.js b/app/components/word-cloud.js new file mode 100644 index 00000000..e0824ed5 --- /dev/null +++ b/app/components/word-cloud.js @@ -0,0 +1,24 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { registerDestructor } from '@ember/destroyable'; + +const SHOW_WORD_CLOUD_AFTER_TIME = 1000; //time in milliseconds +export default class WordCloudComponent extends Component { + @service survey; + @service fastboot; + + constructor() { + super(...arguments); + let timeout; + + if (!this.fastboot.isFastBoot) { + timeout = setTimeout(() => { + this.survey.showWordCloud(); + }, SHOW_WORD_CLOUD_AFTER_TIME); + } + + registerDestructor(this, () => { + clearTimeout(timeout); + }); + } +} diff --git a/app/constants/live.js b/app/constants/live.js index 501cfcc5..02dc92ea 100644 --- a/app/constants/live.js +++ b/app/constants/live.js @@ -39,3 +39,12 @@ export const EVENTS_LOGS_POLL_TIME = 40000; export const EVENTS_LOGS_TYPE = { EVENTS_REMOVE_PEER: 'EVENTS_REMOVE_PEER', }; + +export const QUESTION_DEBOUNCE_TIME = 200; +export const QUESTION_MIN_LENGTH = 2; +export const ANSWER_MIN_LENGTH = 2; +export const ANSWER_STATUS = { + PENDING: 'PENDING', + APPROVED: 'APPROVED', + REJECTED: 'REJECTED', +}; diff --git a/app/controllers/live.js b/app/controllers/live.js index e432b0a8..718d0e55 100644 --- a/app/controllers/live.js +++ b/app/controllers/live.js @@ -4,18 +4,30 @@ import { tracked } from '@glimmer/tracking'; import { inject as service } from '@ember/service'; import { getOwner } from '@ember/application'; import { globalRef } from 'ember-ref-bucket'; -import { ROLES, BUTTONS_TYPE } from '../constants/live'; +import { registerDestructor } from '@ember/destroyable'; +import { + ROLES, + BUTTONS_TYPE, + ANSWER_STATUS, + ANSWER_MIN_LENGTH, +} from '../constants/live'; import { TOAST_OPTIONS } from '../constants/toast-options'; +import { APPS } from '../constants/urls'; export default class LiveController extends Controller { queryParams = ['dev']; ROLES = ROLES; + @service featureFlag; @service login; @service toast; + @service fastboot; + @service survey; + answerEventSource; + questionEventSource; @tracked TABS = [ { id: 1, label: 'Screenshare', active: true }, - { id: 2, label: 'Previous Events', active: false }, - { id: 3, label: 'Real Dev Squad', active: false }, + { id: 2, label: 'Survey', active: false }, { id: 4, label: 'Logs', active: false }, + { id: 3, label: 'More', active: false }, ]; @tracked activeTab = 'Screenshare'; @tracked isLoading = false; @@ -30,6 +42,17 @@ export default class LiveController extends Controller { @tracked newRoomCode = ''; @tracked isActiveEventFound; @tracked buttonText = ''; + @tracked isAnswerReplyModalOpen = false; + @tracked answerValue = ''; + @tracked answerValidationDetails = { + isError: false, + isHelperTextVisible: true, + helperText: `Minimum character limit is ${ANSWER_MIN_LENGTH} characters`, + }; + @tracked answerSubmitButtonState = { + isDisabled: true, + isLoading: false, + }; @globalRef('videoEl') videoEl; get liveService() { return getOwner(this).lookup('service:live'); @@ -37,9 +60,24 @@ export default class LiveController extends Controller { constructor() { super(...arguments); + + if (!this.fastboot.isFastBoot) { + const queryParams = new URLSearchParams(window.location.search); + const isWordCloudFeatureOn = queryParams.get('wordCloud') === 'true'; + + if (isWordCloudFeatureOn) { + this.questionSSEListener(); + this.answerSSEListener(); + } + } setTimeout(() => { this.isLoading = false; }, 4000); + + registerDestructor(this, () => { + this.questionEventSource?.close(); + this.answerEventSource?.close(); + }); } @action inputHandler(type, event) { @@ -136,6 +174,82 @@ export default class LiveController extends Controller { this.isWarningModalOpen = !this.isWarningModalOpen; } + @action openAnswerReplyModal() { + this.isAnswerReplyModalOpen = true; + } + + @action closeAnswerReplyModal() { + this.isAnswerReplyModalOpen = false; + } + + @action onAnswerInput(event) { + const maxCharacters = this.survey.recentQuestion.max_characters; + + this.answerValue = event.target.value; + const answerLength = this.answerValue.trim().length; + const isAnswerEqualToMinLength = answerLength >= ANSWER_MIN_LENGTH; + + if (!isAnswerEqualToMinLength) { + this.answerValidationDetails.helperText = `Minimum character limit is ${ANSWER_MIN_LENGTH} characters`; + this.answerValidationDetails.isHelperTextVisible = true; + + this.answerValidationDetails = this.answerValidationDetails; + + this.answerSubmitButtonState.isDisabled = true; + this.answerSubmitButtonState = this.answerSubmitButtonState; + + return; + } + + if (maxCharacters === null) { + this.resetAnswerValidators(); + return; + } + + if (this.answerValue.trim().length > maxCharacters) { + this.answerValidationDetails.isError = true; + this.answerValidationDetails.helperText = `Maximum character limit is ${maxCharacters} characters`; + this.answerValidationDetails.isHelperTextVisible = true; + this.answerValidationDetails = this.answerValidationDetails; + + this.answerSubmitButtonState.isDisabled = true; + this.answerSubmitButtonState = this.answerSubmitButtonState; + } else { + this.resetAnswerValidators(); + } + } + + @action async submitAnswer() { + this.answerSubmitButtonState.isLoading = true; + this.answerSubmitButtonState.isDisabled = true; + + this.answerSubmitButtonState = this.answerSubmitButtonState; + + const answerBody = { + answer: this.answerValue.trim(), + answeredBy: this.liveService.localPeer?.id, + eventId: this.liveService?.activeRoomId, + questionId: this.survey.recentQuestion?.id, + }; + + const { error } = await this.survey.answerSubmitHandler(answerBody); + + if (!error) { + this.isAnswerReplyModalOpen = false; + this.answerSubmitButtonState.isLoading = false; + this.answerSubmitButtonState.isDisabled = false; + this.answerSubmitButtonState = this.answerSubmitButtonState; + } + } + + @action async onAnswerReject(id) { + this.survey.answerRejectHandler(id); + } + + @action async onAnswerApprove(id) { + this.survey.answerApproveHandler(id); + } + @action buttonClickHandler(buttonId) { switch (buttonId) { case BUTTONS_TYPE.SCREEN_SHARE: @@ -172,4 +286,85 @@ export default class LiveController extends Controller { this.newRoomCode = ''; } } + + resetAnswerValidators() { + this.answerValidationDetails.isError = false; + this.answerValidationDetails.helperText = ''; + this.answerValidationDetails.isHelperTextVisible = false; + this.answerValidationDetails = this.answerValidationDetails; + + this.answerSubmitButtonState.isDisabled = false; + this.answerSubmitButtonState = this.answerSubmitButtonState; + } + questionSSEListener() { + const event = new EventSource(`${APPS.API_BACKEND}/questions`); + this.questionEventSource = event; + + event.onmessage = async (event) => { + const parsedQuestion = JSON.parse(event.data); + const question = parsedQuestion || {}; + + const isQuestionChanged = question?.id !== this.survey.recentQuestion?.id; + + this.survey.setRecentQuestion(question); + + this.answerValue = ''; + this.answerValidationDetails.isError = false; + this.answerValidationDetails.helperText = `Minimum character limit is ${ANSWER_MIN_LENGTH} characters`; + this.answerValidationDetails.isHelperTextVisible = true; + this.answerValidationDetails = this.answerValidationDetails; + + if (isQuestionChanged) { + this.answerEventSource?.close(); + this.answerSSEListener(); + } + + if ( + question && + this.liveService.isJoined && + this.liveService.localPeer.roleName !== this.ROLES.host + ) { + this.isAnswerReplyModalOpen = true; + } + }; + + event.onerror = (event) => { + console.error(event); + }; + } + + answerSSEListener() { + const localPeerRole = this.liveService.localPeer?.roleName; + const isHost = localPeerRole === this.ROLES.host; + const isModerator = localPeerRole === this.ROLES.moderator; + const activeEventId = this.liveService?.activeRoomId; + let answersEventStreamURL = ''; + + if (isHost || isModerator) { + answersEventStreamURL = `${APPS.API_BACKEND}/answers?eventId=${activeEventId}&questionId=${this.survey.recentQuestion?.id}`; + } else { + answersEventStreamURL = `${APPS.API_BACKEND}/answers?eventId=${activeEventId}&questionId=${this.survey.recentQuestion?.id}&status=${ANSWER_STATUS.APPROVED}`; + } + + const event = new EventSource(answersEventStreamURL); + this.answerEventSource = event; + + event.onmessage = async (event) => { + const parsedAnswers = JSON.parse(event.data); + const answers = parsedAnswers || []; + + if (isHost || isModerator) { + this.survey.setAnswers(answers); + } else { + this.survey.setApprovedAnswers(answers); + } + this.survey.showWordCloud(); + + console.log('answerSSEListener answers ', answers); + }; + + event.onerror = (event) => { + console.error(event); + }; + } } diff --git a/app/d3/word-cloud.js b/app/d3/word-cloud.js new file mode 100644 index 00000000..e1dcdd8b --- /dev/null +++ b/app/d3/word-cloud.js @@ -0,0 +1,122 @@ +import cloud from 'd3-cloud'; +import { select } from 'd3-selection'; + +const colors = [ + '#FF6633', + '#FFB399', + '#FF33FF', + '#FFFF99', + '#00B3E6', + '#E6B333', + '#3366E6', + '#999966', + '#99FF99', + '#B34D4D', + '#80B300', + '#809900', + '#E6B3B3', + '#6680B3', + '#66991A', + '#FF99E6', + '#CCFF1A', + '#FF1A66', + '#E6331A', + '#33FFCC', + '#66994D', + '#B366CC', + '#4D8000', + '#B33300', + '#CC80CC', + '#66664D', + '#991AFF', + '#E666FF', + '#4DB3FF', + '#1AB399', + '#E666B3', + '#33991A', + '#CC9999', + '#B3B31A', + '#00E680', + '#4D8066', + '#809980', + '#E6FF80', + '#1AFF33', + '#999933', + '#FF3380', + '#CCCC00', + '#66E64D', + '#4D80CC', + '#9900B3', + '#E64D66', + '#4DB380', + '#FF4D4D', + '#99E6E6', + '#6666FF', +]; + +const defaultSize = { + x: 500, + y: 400, +}; +function generateWordCloud(words, elementSelector, size = defaultSize) { + const container = select(elementSelector); + container.selectAll('*').remove(); + + var layout = cloud() + .size([size.x, size.y]) + .words( + words?.map(function (d) { + return { + text: d, + size: 10 + Math.random() * 37, + color: colors[Math.floor(Math.random() * colors.length)], + }; + }), + ) + .padding(5) + .rotate(function () { + return ~~(Math.random() * 2) * 90; + }) + .font('raleway') + .fontSize(function (d) { + return d.size; + }) + .timeInterval(100) + .on('end', draw); + + function draw(words) { + const container = select(elementSelector); + + container + .append('svg') + .attr('width', layout.size()[0]) + .attr('height', layout.size()[1]) + .append('g') + .attr( + 'transform', + 'translate(' + layout.size()[0] / 2 + ',' + layout.size()[1] / 2 + ')', + ) + .selectAll('text') + .data(words) + .enter() + .append('text') + .style('font-size', function (d) { + return d.size + 'px'; + }) + .style('fill', function (d) { + return d.color; + }) + .style('font-family', 'Impact') + .attr('text-anchor', 'middle') + .attr('transform', function (d) { + return 'translate(' + [d.x, d.y] + ')rotate(' + d.rotate + ')'; + }) + .text(function (d) { + return d.text; + }); + } + + //layout started + layout.start(); +} +export { generateWordCloud }; diff --git a/app/services/feature-flag.js b/app/services/feature-flag.js index 87fa2ec7..6ee6188e 100644 --- a/app/services/feature-flag.js +++ b/app/services/feature-flag.js @@ -7,4 +7,9 @@ export default class FeatureFlagService extends Service { const queryParams = this.router?.currentRoute?.queryParams; return queryParams?.dev === 'true'; } + + get isWordCloud() { + const queryParams = this.router?.currentRoute?.queryParams; + return queryParams?.wordCloud === 'true'; + } } diff --git a/app/services/live.js b/app/services/live.js index 92c9fd8b..d232e848 100644 --- a/app/services/live.js +++ b/app/services/live.js @@ -327,6 +327,7 @@ export default class LiveService extends Service { }); const peer = this.hmsStore.getState(selectLocalPeer); this.localPeer = peer; + this.activeRoomId = roomId; const addedPeerData = await this.addPeer(roomId, peer); if (addedPeerData) { this.toast.success( @@ -346,6 +347,7 @@ export default class LiveService extends Service { }); const peer = this.hmsStore.getState(selectLocalPeer); this.localPeer = peer; + this.activeRoomId = roomId; const addedPeerData = await this.addPeer(roomId, peer); if (addedPeerData) { this.toast.success( diff --git a/app/services/survey.js b/app/services/survey.js new file mode 100644 index 00000000..1c0b921c --- /dev/null +++ b/app/services/survey.js @@ -0,0 +1,164 @@ +import Service, { inject as service } from '@ember/service'; +import { registerDestructor } from '@ember/destroyable'; +import { tracked } from '@glimmer/tracking'; +import { TOAST_OPTIONS } from '../constants/toast-options'; +import { APPS } from '../constants/urls'; +import { + ANSWER_STATUS, + API_METHOD, + PATCH_API_CONFIGS, + ROLES, +} from '../constants/live'; +import { generateWordCloud } from '../d3/word-cloud'; + +export default class SurveyService extends Service { + @service router; + @service toast; + @service live; + @service fastboot; + @tracked answers = []; + @tracked approvedAnswers = []; + @tracked recentQuestion; + @tracked screenWidth; + + constructor() { + super(...arguments); + + const onResize = () => { + this.screenWidth = window.innerWidth; + this.showWordCloud(); + }; + + if (!this.fastboot.isFastBoot) { + this.screenWidth = window.innerWidth; + window.addEventListener('resize', onResize); + registerDestructor(this, () => { + window.removeEventListener('resize', onResize); + }); + } + } + setApprovedAnswers(approvedAnswers) { + this.approvedAnswers = approvedAnswers; + } + + setAnswers(answers) { + this.answers = answers; + } + + setRecentQuestion(question) { + this.recentQuestion = question; + } + + async answerSubmitHandler(payload) { + let error = null, + answer = null; + + try { + const answerResponse = await fetch(`${APPS.API_BACKEND}/answers`, { + method: API_METHOD.POST, + headers: { + 'Content-Type': 'application/json', + }, + + body: JSON.stringify(payload), + }); + answer = await answerResponse.json(); + + if (!answerResponse.ok) + return this.toast.error(answer.message, answer.error, TOAST_OPTIONS); + + this.toast.success(answer.message, answer.error, TOAST_OPTIONS); + + return { error: error, result: answer }; + } catch (error) { + console.error('Error while submitting answer: ', error); + return { error: error, result: null }; + } + } + async answerApproveHandler(id) { + const approvalPayload = { + status: ANSWER_STATUS.APPROVED, + }; + + try { + const approveResponse = await fetch(`${APPS.API_BACKEND}/answers/${id}`, { + ...PATCH_API_CONFIGS, + body: JSON.stringify(approvalPayload), + }); + + if (!approveResponse.ok) throw new Error(); + + this.toast.success( + 'Answer approved successfully', + 'Success', + TOAST_OPTIONS, + ); + } catch (error) { + console.error('Error while approving answer: ', error); + this.toast.error('Error while approving answer', 'Error', TOAST_OPTIONS); + } + } + + async answerRejectHandler(id) { + const rejectionPayload = { + status: ANSWER_STATUS.REJECTED, + }; + try { + const rejectResponse = await fetch(`${APPS.API_BACKEND}/answers/${id}`, { + ...PATCH_API_CONFIGS, + body: JSON.stringify(rejectionPayload), + }); + + if (!rejectResponse.ok) throw new Error(); + + this.toast.success( + 'Answer rejected successfully', + 'Success', + TOAST_OPTIONS, + ); + } catch (error) { + console.error('Error while rejecting answer: ', error); + this.toast.error('Error while rejecting answer', 'Error', TOAST_OPTIONS); + } + } + + getFilteredApprovedAnswersArray() { + const isHost = this.live.localPeer?.roleName === ROLES.host; + const isModerator = this.live.localPeer?.roleName === ROLES.moderator; + const filteredApprovedAnswersArray = []; + + if (isHost || isModerator) { + const filteredApprovedAnswers = this.answers?.filter( + (answer) => answer.status === ANSWER_STATUS.APPROVED, + ); + filteredApprovedAnswers?.forEach((answer) => { + filteredApprovedAnswersArray.push(answer.answer); + }); + return filteredApprovedAnswersArray; + } + + this.approvedAnswers?.forEach((answer) => { + filteredApprovedAnswersArray.push(answer.answer); + }); + return filteredApprovedAnswersArray; + } + + showWordCloud() { + const element = '.word-cloud'; + const words = this.getFilteredApprovedAnswersArray(); + + let wordCloudSize = { + x: this.screenWidth, + y: this.screenWidth, + }; + + if (!words?.length) return; + + //for mobile/small screens + if (this.screenWidth < 500) + return generateWordCloud(words, element, wordCloudSize); + + // for screen >=500px + generateWordCloud(words, element); + } +} diff --git a/app/styles/answer-reply-modal.module.css b/app/styles/answer-reply-modal.module.css new file mode 100644 index 00000000..107bd4c7 --- /dev/null +++ b/app/styles/answer-reply-modal.module.css @@ -0,0 +1,76 @@ +.answer-reply-modal { + width: 31.25rem; + box-sizing: border-box; + background-color: var(--color-white); + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + max-width: 96svw; + border-radius: 25px; + padding: 1.25rem; + padding-right: 40px; +} + +.answer-reply-modal__heading { + text-align: center; + color: var(--color-pink); + font-size: 1.5rem; + text-decoration: underline; + font-style: normal; + font-weight: 800; + line-height: normal; + margin-bottom: 40px; +} + +.answer-reply-modal__answer-label { + font-size: 1.2rem; + font-weight: 600; + color: var(--color-navyblue); +} + +.answer-reply-modal__answer-input { + width: 100%; + padding: 10px; + box-sizing: border-box; + font-size: 1.2rem; + border: 2px solid var(--color-navyblue); + border-radius: 10px; + margin-top: 10px; +} + +.answer-reply-modal__answer-input:focus { + outline: none; + box-shadow: 0 0 0 4px var(--color-light-navyblue); +} + +.answer-reply-modal__info-text { + font-size: 0.8rem; + color: var(--color-lightgrey); +} + +.answer-reply-modal__info-icon { + border: 1px solid var(--color-darkgrey); + padding: 1px 7px; + border-radius: 50%; + font-size: 0.7rem; + color: var(--color-darkgrey); +} + +.answer-reply-modal__actions { + text-align: center; +} + +/* media queries */ +@media only screen and (width <= 425px) { + .answer-reply-modal__submit-button, + .answer-reply-modal__cancel-button { + margin-right: 0; + width: fit-content; + padding: 0 20px; + } + + .answer-reply-modal__submit-button { + padding: 0 34px; + } +} diff --git a/app/styles/answer-view-card.module.css b/app/styles/answer-view-card.module.css new file mode 100644 index 00000000..936a0422 --- /dev/null +++ b/app/styles/answer-view-card.module.css @@ -0,0 +1,48 @@ +.answer-view-card { + box-shadow: 0 0 4px 0 var(--color-lightgrey); + min-height: 5rem; + height: fit-content; + display: flex; + justify-content: space-between; + align-items: center; + flex: 3 1; + border-radius: 10px; + box-sizing: border-box; + padding: 1rem; +} + +.answer-view-card__text { + width: 80%; +} + +.answer-view-card__read-more-button { + all: unset; + color: var(--color-pink); + font-weight: 600; + cursor: pointer; +} + +.answer-view-card--pending { + background-color: #ffffe0; +} + +.answer-view-card--approved { + background-color: #98fb98; +} + +.answer-view-card--rejected { + background-color: var(--color-pink-low-opacity); +} + +@media (width <=1024px) { + .answer-view-card { + flex-direction: column; + gap: 10px; + } + + .answer-view-card__text { + width: 100%; + padding-bottom: 10px; + border-bottom: 1px solid var(--color-lightgrey); + } +} diff --git a/app/styles/app.css b/app/styles/app.css index 090c4937..459dc41c 100644 --- a/app/styles/app.css +++ b/app/styles/app.css @@ -31,6 +31,11 @@ @import url("identity-card.module.css"); @import url("wheel-animations.css"); @import url("goto.module.css"); +@import url("survey-page.module.css"); +@import url("ask-question-modal.module.css"); +@import url("answer-reply-modal.module.css"); +@import url("answer-view-card.module.css"); +@import url("word-cloud.module.css"); @import url("event-card.module.css"); @import url("status-card.module.css"); @import url("tooltip.module.css"); diff --git a/app/styles/ask-question-modal.module.css b/app/styles/ask-question-modal.module.css new file mode 100644 index 00000000..9f859f30 --- /dev/null +++ b/app/styles/ask-question-modal.module.css @@ -0,0 +1,147 @@ +.ask-question-modal { + height: 21rem; + width: 31.25rem; + background-color: var(--color-white); + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border-radius: 10px; + padding: 1.25rem; + max-width: 80svw; +} + +.ask-question-modal__heading { + text-align: center; + color: var(--color-pink); + font-size: 1.5rem; + text-decoration: underline; + font-style: normal; + font-weight: 800; + line-height: normal; +} + +.ask-question-modal__textarea { + width: 100%; + height: 47%; + outline: none; + border: 0; + resize: none; + margin-top: 1.25rem; + font-family: raleway, sans-serif; + font-size: 1.1rem; + font-style: normal; + font-weight: 500; + line-height: normal; + overflow-y: auto; +} + +.ask-question-modal__textarea::-webkit-scrollbar { + width: 5px; +} + +.ask-question-modal__textarea::-webkit-scrollbar-thumb { + background: var(--color-darkgrey); + border-radius: 10px; +} + +.ask-question-modal__checkbox-container { + display: flex; + gap: 0.5rem; +} + +.ask-question-modal__checkbox { + appearance: none; + background-color: #fff; + margin: 0; + font: inherit; + color: currentcolor; + width: 1.15rem; + height: 1.15rem; + border: 0.15em solid var(--color-navyblue); + border-radius: 0.15em; + display: grid; + place-content: center; +} + +.ask-question-modal__checkbox::before { + content: ""; + width: 0.65rem; + height: 0.65rem; + transform: scale(0); + transition: 120ms transform ease-in-out; + box-shadow: inset 1em 1em var(--color-pink); + background-color: CanvasText; + transform-origin: bottom left; + clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); +} + +.ask-question-modal__checkbox:checked { + border: 2px solid var(--color-pink); +} + +.ask-question-modal__checkbox:checked::before { + transform: scale(1); +} + +.ask-question-modal__checkbox-label { + font-size: 1rem; + font-weight: 600; +} + +.ask-question-modal__max-characters-input { + width: 100%; + padding: 10px; + box-sizing: border-box; + font-size: 1.2rem; + border: 2px solid var(--color-navyblue); + border-radius: 10px; + margin-top: 10px; +} + +.ask-question-modal__max-characters-input:focus { + outline: none; + box-shadow: 0 0 0 4px var(--color-light-navyblue); +} + +.ask-question-modal__actions { + text-align: center; +} + +.ask-question-modal__cancel-button { + margin-right: 1rem; +} + +/* media queries */ +@media only screen and (width <=625px) { + .ask-question-modal { + height: 23rem; + padding: 1.25rem 1rem; + } + + .ask-question-modal__checkbox { + width: 1.2rem; + } +} + +@media only screen and (width <=425px) { + .ask-question-modal__checkbox { + width: 1.6rem; + } + + .ask-question-modal__submit-button, + .ask-question-modal__cancel-button { + margin-right: 0; + width: fit-content; + padding: 0 20px; + } +} + +/* utils css */ +.visibility--hidden { + visibility: hidden; +} + +.visibility--visible { + visibility: visible; +} diff --git a/app/styles/button.module.css b/app/styles/button.module.css index 520dafcb..f3a73117 100644 --- a/app/styles/button.module.css +++ b/app/styles/button.module.css @@ -32,6 +32,11 @@ background-color: var(--color-pink); } +.btn-pink:disabled { + opacity: 0.4; + color: var(--color-white); +} + .btn--sm { width: 8rem; } diff --git a/app/styles/icon-button.module.css b/app/styles/icon-button.module.css index 15d81d2b..f0697b8e 100644 --- a/app/styles/icon-button.module.css +++ b/app/styles/icon-button.module.css @@ -27,7 +27,36 @@ transform: translate(50%, 50%); } -@media (width <= 425px) { +.icon-button--sm { + border-radius: 50%; + height: 37px; + width: 38px; + margin-right: 2rem; + position: relative; +} + +.icon-button--sm .iconify.iconify--material-symbols { + height: 50%; + width: 50%; + position: absolute; + bottom: 50%; + right: 50%; + transform: translate(50%, 50%); +} + +.icon-button--green { + background-color: var(--color-green); + color: var(--color-white); + border: none; +} + +.icon-button--red-outlined { + border: 2px solid var(--text-red); + background-color: var(--color-white); + color: var(--text-red); +} + +@media (width <=425px) { .icon-button--md { margin-right: 1rem; } diff --git a/app/styles/input.module.css b/app/styles/input.module.css index 0982e719..6173a8b6 100644 --- a/app/styles/input.module.css +++ b/app/styles/input.module.css @@ -47,3 +47,41 @@ justify-content: space-between; overflow-y: scroll; } + +.user-input--error { + animation: error-animation 0.1s ease-in-out 3; + border: 1px solid red; +} + +.user-input--error.user-input:focus { + box-shadow: 0 0 0 4px var(--color-soft-magenta); +} + +.input-box .input--full-width { + width: 100%; +} + +.input-box__helper-text { + font-size: 0.8rem; + margin-left: 5px; + margin-top: 5px; + color: var(--color-lightgrey); +} + +.input-box__helper-text--error { + color: var(--text-red); +} + +@keyframes error-animation { + 0% { + transform: translateX(-4px); + } + + 50% { + transform: translateX(4px); + } + + 100% { + transform: translateX(-4px); + } +} diff --git a/app/styles/survey-page.module.css b/app/styles/survey-page.module.css new file mode 100644 index 00000000..08e9edfc --- /dev/null +++ b/app/styles/survey-page.module.css @@ -0,0 +1,43 @@ +.survey-page { + width: 100%; + margin-top: 3.1rem; + min-height: 80vh; + box-sizing: border-box; + padding: 0.6rem 1rem; + border-radius: 1rem; +} + +.survey-page__question-container { + display: flex; + align-items: center; + gap: 2rem; +} + +.survey-page__filter { + margin-bottom: 1rem; +} + +.survey-page__answers { + display: flex; + flex-direction: column; + gap: 10px; +} + +.survey-page__answers-heading { + border-radius: 10px; + background: var(--color-light-gray); + color: var(--color-pink); + font-size: 2.5rem; + padding: 0 10px; + line-height: 150%; + margin-bottom: 20px; + box-sizing: border-box; + height: 65px; +} + +@media (width <=768px) { + .survey-page__answers-heading { + font-size: 2rem; + line-height: 191%; + } +} diff --git a/app/styles/word-cloud.module.css b/app/styles/word-cloud.module.css new file mode 100644 index 00000000..9f4ec580 --- /dev/null +++ b/app/styles/word-cloud.module.css @@ -0,0 +1,4 @@ +.word-cloud { + min-height: 400px; + margin-top: 10px; +} diff --git a/app/templates/live.hbs b/app/templates/live.hbs index 38e94fe7..038fbf8b 100644 --- a/app/templates/live.hbs +++ b/app/templates/live.hbs @@ -1,14 +1,25 @@ -{{page-title 'Live'}} +{{page-title "Live"}} -
+
{{#if this.liveService.isJoined}} + {{!-- {{#if true}} --}} {{/if}} - + @@ -33,7 +44,7 @@ @roomCodes={{this.liveService.roomCodesForMaven}} @closeModal={{modal.closeModal}} @newCode={{this.newRoomCode}} - @onInput={{fn this.inputHandler 'newRoomCode'}} + @onInput={{fn this.inputHandler "newRoomCode"}} @createRoomCode={{this.createRoomCodeHandler}} @toast={{this.liveService.toast}} @isLoading={{this.liveService.roomCodeLoading}} @@ -47,34 +58,35 @@ > {{! TODO - add more else if statement instead of only else }} - {{#if (eq this.activeTab 'Screenshare')}} + {{#if (eq this.activeTab "Screenshare")}} {{#if this.liveService.isLoading}} -
- +
+
{{else}} {{#if this.liveService.isJoined}} -
+ {{!-- {{#if true}} --}} +
{{! TODO - add skeleton to the whole structure }} {{#if this.isLoading}} {{else}} - + {{/if}}
-
+
{{#if this.isLoading}} {{else}} @@ -86,7 +98,7 @@ {{/if}}
-
+
{{#if this.isLoading}} {{else}} @@ -100,8 +112,9 @@ /> {{/if}}
+ {{else}} - {{#if (eq this.role '')}} + {{#if (eq this.role "")}} + {{else if (eq this.activeTab "Logs")}} +
+ {{else if (eq this.activeTab "Survey")}} + {{else}} {{! TODO - add the respective component here }} -

Coming Soon!

+

Coming Soon!

{{/if}}
\ No newline at end of file diff --git a/app/utils/common-utils.js b/app/utils/common-utils.js new file mode 100644 index 00000000..971ad825 --- /dev/null +++ b/app/utils/common-utils.js @@ -0,0 +1,11 @@ +export const readMoreFormatter = (string, lengthToDisplay) => { + if (!string) { + return string; + } + + if (string.length > lengthToDisplay) { + return string.slice(0, lengthToDisplay) + '...'; + } else { + return string; + } +}; diff --git a/package.json b/package.json index 0ce7d021..f3330486 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,10 @@ }, "dependencies": { "@100mslive/hms-video-store": "^0.10.6", + "d3-cloud": "^1.2.7", "dotenv": "^16.0.2", + "ember-d3": "^0.5.1", + "exists-sync": "^0.1.0", "fastboot-app-server": "^3.3.2" }, "devDependencies": { diff --git a/tests/integration/components/answer-reply-modal-test.js b/tests/integration/components/answer-reply-modal-test.js new file mode 100644 index 00000000..b7cdb618 --- /dev/null +++ b/tests/integration/components/answer-reply-modal-test.js @@ -0,0 +1,53 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'website-www/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import Service from '@ember/service'; +class MockSurveyService extends Service { + recentQuestion = 'This is mock recent question'; +} +module('Integration | Component | answer-reply-modal', function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function (assert) { + // Registering the mock survey service + this.owner.register('service:survey', MockSurveyService); + const surveyService = this.owner.lookup('service:survey'); + + this.setProperties({ + openModal: () => assert.ok(true, 'openModal working fine!'), + closeModal: () => assert.ok(true, 'closeModal working fine!'), + isOpen: true, + inputHandler: () => assert.ok(true, 'onAnswerInput working fine!'), + answerValue: 'mock answer value', + onSubmit: () => assert.ok(true, 'submitAnswer working fine!'), + validationDetails: { + isError: false, + isHelperTextVisible: true, + helperText: `Minimum character limit is 10 characters`, + }, + submitButtonState: { + isDisabled: true, + isLoading: false, + }, + survey: surveyService, + }); + }); + + test('it renders', async function (assert) { + await render(hbs` + + `); + assert.dom('[data-test-answer-reply-modal]').exists(); + }); +}); diff --git a/tests/integration/components/ask-question-modal-test.js b/tests/integration/components/ask-question-modal-test.js new file mode 100644 index 00000000..d3d482c1 --- /dev/null +++ b/tests/integration/components/ask-question-modal-test.js @@ -0,0 +1,17 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'website-www/tests/helpers'; + +module('Integration | Component | ask-question-modal', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + assert.ok(true); + // TODO - add tests + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.set('myAction', function(val) { ... }); + + // await render(hbs``); + + // assert.dom(this.element).hasText(''); + }); +}); diff --git a/tests/integration/components/events/survey-page-test.js b/tests/integration/components/events/survey-page-test.js new file mode 100644 index 00000000..3697cd14 --- /dev/null +++ b/tests/integration/components/events/survey-page-test.js @@ -0,0 +1,14 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'website-www/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | survey-page', function (hooks) { + setupRenderingTest(hooks); + + test('Events::SurveyPage renders', async function (assert) { + await render(hbs``); + + assert.ok(true, 'survey page tests'); + }); +}); diff --git a/tests/integration/components/word-cloud-test.js b/tests/integration/components/word-cloud-test.js new file mode 100644 index 00000000..0fb321b8 --- /dev/null +++ b/tests/integration/components/word-cloud-test.js @@ -0,0 +1,22 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'website-www/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | word-cloud', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + this.set('isWordCloud', true); + + await render(hbs``); + + assert.dom('[data-test-word-cloud]').exists(); + + this.set('isWordCloud', false); + + assert.dom('[data-test-word-cloud]').doesNotExist(); + }); +}); diff --git a/tests/unit/utils/common-test.js b/tests/unit/utils/common-test.js new file mode 100644 index 00000000..443f0bcd --- /dev/null +++ b/tests/unit/utils/common-test.js @@ -0,0 +1,28 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'website-www/tests/helpers'; +import { readMoreFormatter } from 'website-www/utils/common-utils'; + +module('Unit | Util | readMoreFormatter', function (hooks) { + setupTest(hooks); + + test('should return empty string if empty string is passed', function (assert) { + const result = readMoreFormatter('', 10); + + assert.strictEqual(result, '') + }); + + test('should return the string if length of string is less than the length passed to format the string', function (assert) { + const demoString = 'Lorem Ipsum is simply dummy text'; + + const result = readMoreFormatter(demoString, 32); + assert.strictEqual(result, demoString) + }); + + test('should format the string upto the given length in read more format', function (assert) { + const demoString = + 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.'; + + const result = readMoreFormatter(demoString, 32); + assert.strictEqual(result, 'Lorem Ipsum is simply dummy text...') + }); +}); diff --git a/yarn.lock b/yarn.lock index 889a4772..54091bf3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5571,7 +5571,7 @@ broccoli-merge-trees@^2.0.0: broccoli-plugin "^1.3.0" merge-trees "^1.0.1" -broccoli-merge-trees@^3.0.1, broccoli-merge-trees@^3.0.2: +broccoli-merge-trees@^3.0.0, broccoli-merge-trees@^3.0.1, broccoli-merge-trees@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/broccoli-merge-trees/-/broccoli-merge-trees-3.0.2.tgz#f33b451994225522b5c9bcf27d59decfd8ba537d" integrity sha512-ZyPAwrOdlCddduFbsMyyFzJUrvW6b04pMvDiAQZrCwghlvgowJDY+EfoXn+eR1RRA5nmGHJ+B68T63VnpRiT1A== @@ -6303,6 +6303,11 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +commander@2, commander@^2.20.0, commander@^2.6.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + commander@2.8.x: version "2.8.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4" @@ -6320,11 +6325,6 @@ commander@^10.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== -commander@^2.20.0, commander@^2.6.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - commander@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" @@ -6668,6 +6668,269 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" +d3-array@1, d3-array@^1.1.1, d3-array@^1.2.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" + integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw== + +d3-axis@1: + version "1.0.12" + resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.12.tgz#cdf20ba210cfbb43795af33756886fb3638daac9" + integrity sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ== + +d3-brush@1: + version "1.1.6" + resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.1.6.tgz#b0a22c7372cabec128bdddf9bddc058592f89e9b" + integrity sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA== + dependencies: + d3-dispatch "1" + d3-drag "1" + d3-interpolate "1" + d3-selection "1" + d3-transition "1" + +d3-chord@1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-1.0.6.tgz#309157e3f2db2c752f0280fedd35f2067ccbb15f" + integrity sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA== + dependencies: + d3-array "1" + d3-path "1" + +d3-cloud@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/d3-cloud/-/d3-cloud-1.2.7.tgz#5a733c4bae43238cbb4760bb8f2d15912a8ad7a5" + integrity sha512-8TrgcgwRIpoZYQp7s3fGB7tATWfhckRb8KcVd1bOgqkNdkJRDGWfdSf4HkHHzZxSczwQJdSxvfPudwir5IAJ3w== + dependencies: + d3-dispatch "^1.0.3" + +d3-collection@1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e" + integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A== + +d3-color@1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a" + integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q== + +d3-contour@1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-1.3.2.tgz#652aacd500d2264cb3423cee10db69f6f59bead3" + integrity sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg== + dependencies: + d3-array "^1.1.1" + +d3-dispatch@1, d3-dispatch@^1.0.3: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58" + integrity sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA== + +d3-drag@1: + version "1.2.5" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.5.tgz#2537f451acd39d31406677b7dc77c82f7d988f70" + integrity sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w== + dependencies: + d3-dispatch "1" + d3-selection "1" + +d3-dsv@1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.2.0.tgz#9d5f75c3a5f8abd611f74d3f5847b0d4338b885c" + integrity sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g== + dependencies: + commander "2" + iconv-lite "0.4" + rw "1" + +d3-ease@1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.7.tgz#9a834890ef8b8ae8c558b2fe55bd57f5993b85e2" + integrity sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ== + +d3-fetch@1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-1.2.0.tgz#15ce2ecfc41b092b1db50abd2c552c2316cf7fc7" + integrity sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA== + dependencies: + d3-dsv "1" + +d3-force@1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.2.1.tgz#fd29a5d1ff181c9e7f0669e4bd72bdb0e914ec0b" + integrity sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg== + dependencies: + d3-collection "1" + d3-dispatch "1" + d3-quadtree "1" + d3-timer "1" + +d3-format@1: + version "1.4.5" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4" + integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ== + +d3-geo@1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.12.1.tgz#7fc2ab7414b72e59fbcbd603e80d9adc029b035f" + integrity sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg== + dependencies: + d3-array "1" + +d3-hierarchy@1: + version "1.1.9" + resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83" + integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ== + +d3-interpolate@1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987" + integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA== + dependencies: + d3-color "1" + +d3-path@1: + version "1.0.9" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" + integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== + +d3-polygon@1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.6.tgz#0bf8cb8180a6dc107f518ddf7975e12abbfbd38e" + integrity sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ== + +d3-quadtree@1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-1.0.7.tgz#ca8b84df7bb53763fe3c2f24bd435137f4e53135" + integrity sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA== + +d3-random@1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-1.1.2.tgz#2833be7c124360bf9e2d3fd4f33847cfe6cab291" + integrity sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ== + +d3-scale-chromatic@1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz#54e333fc78212f439b14641fb55801dd81135a98" + integrity sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg== + dependencies: + d3-color "1" + d3-interpolate "1" + +d3-scale@2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f" + integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw== + dependencies: + d3-array "^1.2.0" + d3-collection "1" + d3-format "1" + d3-interpolate "1" + d3-time "1" + d3-time-format "2" + +d3-selection-multi@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/d3-selection-multi/-/d3-selection-multi-1.0.1.tgz#cd6c25413d04a2cb97470e786f2cd877f3e34f58" + integrity sha512-mEnRkJ6A+Otd1LuRPV3az+s/RLDmIEhuz/MnT21O2nPcWNH7wZotdIlRjMA4Wpr+n7AESQ5fD8v1J3nL2Mnw9g== + dependencies: + d3-selection "1" + d3-transition "1" + +d3-selection@1, d3-selection@^1.1.0: + version "1.4.2" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.2.tgz#dcaa49522c0dbf32d6c1858afc26b6094555bc5c" + integrity sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg== + +d3-shape@1: + version "1.3.7" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" + integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw== + dependencies: + d3-path "1" + +d3-time-format@2: + version "2.3.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.3.0.tgz#107bdc028667788a8924ba040faf1fbccd5a7850" + integrity sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ== + dependencies: + d3-time "1" + +d3-time@1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" + integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== + +d3-timer@1: + version "1.0.10" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5" + integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw== + +d3-transition@1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.3.2.tgz#a98ef2151be8d8600543434c1ca80140ae23b398" + integrity sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA== + dependencies: + d3-color "1" + d3-dispatch "1" + d3-ease "1" + d3-interpolate "1" + d3-selection "^1.1.0" + d3-timer "1" + +d3-voronoi@1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.4.tgz#dd3c78d7653d2bb359284ae478645d95944c8297" + integrity sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg== + +d3-zoom@1: + version "1.8.3" + resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.8.3.tgz#b6a3dbe738c7763121cd05b8a7795ffe17f4fc0a" + integrity sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ== + dependencies: + d3-dispatch "1" + d3-drag "1" + d3-interpolate "1" + d3-selection "1" + d3-transition "1" + +d3@^5.0.0: + version "5.16.0" + resolved "https://registry.yarnpkg.com/d3/-/d3-5.16.0.tgz#9c5e8d3b56403c79d4ed42fbd62f6113f199c877" + integrity sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw== + dependencies: + d3-array "1" + d3-axis "1" + d3-brush "1" + d3-chord "1" + d3-collection "1" + d3-color "1" + d3-contour "1" + d3-dispatch "1" + d3-drag "1" + d3-dsv "1" + d3-ease "1" + d3-fetch "1" + d3-force "1" + d3-format "1" + d3-geo "1" + d3-hierarchy "1" + d3-interpolate "1" + d3-path "1" + d3-polygon "1" + d3-quadtree "1" + d3-random "1" + d3-scale "2" + d3-scale-chromatic "1" + d3-selection "1" + d3-shape "1" + d3-time "1" + d3-time-format "2" + d3-timer "1" + d3-transition "1" + d3-voronoi "1" + d3-zoom "1" + dag-map@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/dag-map/-/dag-map-2.0.2.tgz#9714b472de82a1843de2fba9b6876938cab44c68" @@ -7128,7 +7391,7 @@ ember-cli-babel-plugin-helpers@^1.0.0, ember-cli-babel-plugin-helpers@^1.1.1: resolved "https://registry.yarnpkg.com/ember-cli-babel-plugin-helpers/-/ember-cli-babel-plugin-helpers-1.1.1.tgz#5016b80cdef37036c4282eef2d863e1d73576879" integrity sha512-sKvOiPNHr5F/60NLd7SFzMpYPte/nnGkq/tMIfXejfKHIhaiIkYFqX8Z9UFTKWLLn+V7NOaby6niNPZUdvKCRw== -ember-cli-babel@^7.10.0, ember-cli-babel@^7.13.0, ember-cli-babel@^7.18.0, ember-cli-babel@^7.22.1, ember-cli-babel@^7.23.0, ember-cli-babel@^7.23.1, ember-cli-babel@^7.26.11, ember-cli-babel@^7.26.3, ember-cli-babel@^7.26.5, ember-cli-babel@^7.26.6, ember-cli-babel@^7.7.3: +ember-cli-babel@^7.1.2, ember-cli-babel@^7.10.0, ember-cli-babel@^7.13.0, ember-cli-babel@^7.18.0, ember-cli-babel@^7.22.1, ember-cli-babel@^7.23.0, ember-cli-babel@^7.23.1, ember-cli-babel@^7.26.11, ember-cli-babel@^7.26.3, ember-cli-babel@^7.26.5, ember-cli-babel@^7.26.6, ember-cli-babel@^7.7.3: version "7.26.11" resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.26.11.tgz#50da0fe4dcd99aada499843940fec75076249a9f" integrity sha512-JJYeYjiz/JTn34q7F5DSOjkkZqy8qwFOOxXfE6pe9yEJqWGu4qErKxlz8I22JoVEQ/aBUO+OcKTpmctvykM9YA== @@ -7666,6 +7929,17 @@ ember-composable-helpers@^5.0.0: ember-cli-babel "^7.26.3" resolve "^1.10.0" +ember-d3@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/ember-d3/-/ember-d3-0.5.1.tgz#b23ce145863f082b5e73d25d9a43a0f1d9e9f412" + integrity sha512-NyjTUuIOxGxZdyrxLasNwwjqyFgay1pVHGRAWFj7mriwTI44muKsM9ZMl6YeepqixceuFig2fDxHmLLrkQV+QQ== + dependencies: + broccoli-funnel "^2.0.0" + broccoli-merge-trees "^3.0.0" + d3 "^5.0.0" + d3-selection-multi "^1.0.1" + ember-cli-babel "^7.1.2" + ember-data@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/ember-data/-/ember-data-5.3.0.tgz#d7be6b77653a41ae8ed045ffb904f1adbdcb8920" @@ -8469,6 +8743,11 @@ execa@^7.1.1: signal-exit "^3.0.7" strip-final-newline "^3.0.0" +exists-sync@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/exists-sync/-/exists-sync-0.1.0.tgz#318d545213d2b2a31499e92c35f74c94196a22f7" + integrity sha512-qEfFekfBVid4b14FNug/RNY1nv+BADnlzKGHulc+t6ZLqGY4kdHGh1iFha8lnE3sJU/1WzMzKRNxS6EvSakJUg== + exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -9757,7 +10036,7 @@ human-signals@^4.3.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ== -iconv-lite@0.4.24, iconv-lite@^0.4.24: +iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -12790,6 +13069,11 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +rw@1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" + integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== + rxjs@^6.4.0, rxjs@^6.6.0: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"