-
-
Save fabmiz/6c7d22ec7267872003a07747474b506d to your computer and use it in GitHub Desktop.
Coding Challenge
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Controller from '@ember/controller'; | |
import { reads, uniqBy, mapBy } from '@ember/object/computed'; | |
export default Controller.extend({ | |
timeslots: reads('model'), | |
timeslotsWithUniqueDates: uniqBy('timeslots', 'isoDate'), | |
isoDates: mapBy('timeslotsWithUniqueDates', 'isoDate'), | |
}); | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Route from '@ember/routing/route'; | |
import Timeslot, { maybeUnwrapProxy } from 'twiddle/utils'; | |
export default Route.extend({ | |
async model() { | |
const timeslots = await this.store.findAll('timeslot'); | |
return maybeUnwrapProxy(timeslots).map(timeslot => new Timeslot(timeslot)); | |
}, | |
afterModel(model) { | |
const uniqDates = Array.from(new Set(model.map(timeslot => timeslot.isoDate))); | |
return this.replaceWith('date', `${uniqDates[0]}`); | |
}, | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Controller from '@ember/controller'; | |
export default Controller.extend({ | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Route from '@ember/routing/route'; | |
export default class DateRoute extends Route { | |
model({ iso_date: date }) { | |
const timeslots = this.modelFor('application'); | |
return timeslots.filter(timeslot => timeslot.isoDate === date); | |
} | |
}; | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Component from '@glimmer/component'; | |
export default class DayWrapper extends Component { | |
interval = 30 * 60 * 1000; // 30 min in ms | |
get timeAxisLength() { | |
return Date.parse(this.latestEndTime) - Date.parse(this.earliestStartTime); | |
} | |
get timeAxisTickCount() { | |
return this.timeAxisLength && Math.floor(this.timeAxisLength / this.interval); | |
} | |
get earliestStartTime() { | |
return this.args.timeslots[0].startDateTime; | |
} | |
get latestEndTime() { | |
const lastIndex = this.args.timeslots.length - 1; | |
return this.args.timeslots[lastIndex].endDateTime; | |
} | |
get timeAxisTickLabels() { | |
const tickLabels = []; | |
let acc = Date.parse(this.earliestStartTime); | |
for (let count=0; count <= this.timeAxisTickCount; count++) { | |
const minutes = (new Date(acc)).getMinutes(); | |
const hours = (new Date(acc)).getHours(); | |
const paddedMinutes = minutes ? `:${minutes}` : ':00'; | |
const axisTickLabel = `${hours}${paddedMinutes}`; | |
tickLabels.push(axisTickLabel); | |
acc += this.interval; | |
} | |
return tickLabels; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Response } from 'ember-cli-mirage'; | |
export default function() { | |
window.server = this; | |
this.get('/timeslots', function (schema, request) { | |
const { date } = request.queryParams; | |
if (!date) { | |
return schema.timeslots.all(); | |
} | |
return schema.timeslots.where({ date: date }); | |
}); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Model } from 'ember-cli-mirage' | |
export default Model.extend({ | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export default function(server) { | |
// Day 1 timeslots | |
server.create('timeslot',{ | |
date: '2021-06-04', | |
startTime: '10:00', | |
endTime: '11:00', | |
activityName: 'Activity 1', | |
availableSpots: 10, | |
bookedCount: 0, | |
maxGuests: 10 | |
}); | |
server.create('timeslot',{ | |
date: '2021-06-04', | |
startTime: '11:00', | |
endTime: '12:00', | |
activityName: 'Activity 1', | |
availableSpots: 7, | |
bookedCount: 3, | |
maxGuests: 10 | |
}); | |
server.create('timeslot',{ | |
date: '2021-06-04', | |
startTime: '13:00', | |
endTime: '14:00', | |
activityName: 'Activity 2', | |
availableSpots: 14, | |
bookedCount: 1, | |
maxGuests: 15 | |
}); | |
// Day 2 timeslots | |
server.create('timeslot',{ | |
date: '2021-06-05', | |
startTime: '10:00', | |
endTime: '11:00', | |
activityName: 'Activity 1', | |
availableSpots: 9, | |
bookedCount: 1, | |
maxGuests: 10 | |
}); | |
server.create('timeslot',{ | |
date: '2021-06-05', | |
startTime: '10:00', | |
endTime: '12:00', | |
activityName: 'Activity 2', | |
availableSpots: 0, | |
bookedCount: 15, | |
maxGuests: 15 | |
}); | |
server.create('timeslot',{ | |
date: '2021-06-05', | |
startTime: '11:30', | |
endTime: '13:00', | |
activityName: 'Activity 2', | |
availableSpots: 10, | |
bookedCount: 5, | |
maxGuests: 15 | |
}); | |
server.create('timeslot',{ | |
date: '2021-06-05', | |
startTime: '12:30', | |
endTime: '14:00', | |
activityName: 'Activity 1', | |
availableSpots: 0, | |
bookedCount: 10, | |
maxGuests: 10 | |
}); | |
server.create('timeslot',{ | |
date: '2021-06-05', | |
startTime: '15:00', | |
endTime: '16:30', | |
activityName: 'Activity 1', | |
availableSpots: 8, | |
bookedCount: 2, | |
maxGuests: 10 | |
}); | |
// Day 3 timeslots | |
server.create('timeslot',{ | |
date: '2021-06-06', | |
startTime: '09:00', | |
endTime: '12:00', | |
activityName: 'Activity 1', | |
availableSpots: 0, | |
bookedCount: 10, | |
maxGuests: 10 | |
}); | |
server.create('timeslot',{ | |
date: '2021-06-06', | |
startTime: '10:00', | |
endTime: '14:00', | |
activityName: 'Activity 3', | |
availableSpots: 5, | |
bookedCount: 0, | |
maxGuests: 5 | |
}); | |
server.create('timeslot',{ | |
date: '2021-06-06', | |
startTime: '11:00', | |
endTime: '12:00', | |
activityName: 'Activity 2', | |
availableSpots: 13, | |
bookedCount: 2, | |
maxGuests: 15 | |
}); | |
server.create('timeslot',{ | |
date: '2021-06-06', | |
startTime: '13:00', | |
endTime: '16:30', | |
activityName: 'Activity 2', | |
availableSpots: 15, | |
bookedCount: 0, | |
maxGuests: 15 | |
}); | |
server.create('timeslot', { | |
date: '2021-06-06', | |
startTime: '13:00', | |
endTime: '16:00', | |
activityName: 'Activity 1', | |
availableSpots: 4, | |
bookedCount: 6, | |
maxGuests: 10 | |
}); | |
server.create('timeslot',{ | |
date: '2021-06-06', | |
startTime: '16:30', | |
endTime: '18:00', | |
activityName: 'Activity 3', | |
availableSpots: 3, | |
bookedCount: 2, | |
maxGuests: 5 | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Model from 'ember-data/model'; | |
import attr from 'ember-data/attr'; | |
export default class extends Model { | |
// date the activity timeslot takes place | |
@attr('date') date; | |
// hh:mm formatted timeslot start time | |
@attr('string') startTime; | |
// hh:mm formatted timeslot end time | |
@attr('string') endTime; | |
// name of the activity the timeslot is for | |
@attr('string') activityName; | |
// number of spots booked for the activity timeslot | |
@attr('number') bookedCount; | |
// number of spots available for the timeslot | |
@attr('number') availableSpots; | |
// max number of guests allowed on this timeslot | |
@attr('number') maxGuests; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import EmberRouter from '@ember/routing/router'; | |
import config from './config/environment'; | |
const Router = EmberRouter.extend({ | |
location: 'none', | |
rootURL: config.rootURL | |
}); | |
Router.map(function() { | |
this.route('date', { path: '/date/:iso_date' }); | |
}); | |
export default Router; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
margin: 12px 16px; | |
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; | |
font-size: 12pt; | |
} | |
.calendar { | |
display: flex; | |
flex-direction: row; | |
} | |
.calendar-day { | |
margin-right: 10px; | |
} | |
.calendar-day .active { | |
background-color: #E8F0F2; | |
padding: 10px; | |
} | |
.calendar-day-detail { | |
background-color: #A2DBFA; | |
padding: 10px; | |
} | |
.timeslot-card-summary { | |
font-size: .95rem; | |
} | |
.timeslot-card { | |
margin-bottom: 10px; | |
padding: 5px; | |
box-shadow: 0 0 15px rgb(0 0 0 / 10%); | |
background-color: #fff200; | |
opacity: .65; | |
} | |
.timeslot-card.available { | |
background-color: #fff200; | |
opacity: 1; | |
} | |
.day-axis { | |
display: flex; | |
justify-content: space-between; | |
margin-bottom: 10px; | |
} | |
.day-axis-tick { | |
font-size: .85rem; | |
} | |
.progress { | |
height: .50rem; | |
border-radius: 0; | |
background-color: transparent; | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { module, test } from 'qunit'; | |
import { visit, currentURL, findAll, click } from '@ember/test-helpers'; | |
import { setupApplicationTest } from 'ember-qunit'; | |
import { setupMirage } from 'ember-cli-mirage/test-support'; | |
module('Acceptance | date detail view', function(hooks) { | |
setupApplicationTest(hooks); | |
setupMirage(hooks); | |
test('visiting /date', async function(assert) { | |
await visit('/'); | |
assert.equal(currentURL(), '/date/2021-06-04', 'at the correct route; index gets redirected'); | |
assert.dom('[data-test-day-axis]').isVisible('time axis shows'); | |
assert.dom('[data-test-timeslot-activity-name]').exists({ count: 3 }, 'shows 3 activity names'); | |
assert.dom('[data-test-timeslot-card]').exists({ count: 3 }, 'has 3 timeslot cards'); | |
assert.dom('[data-test-links]').exists({ count: 3}, 'has 3 links'); | |
const cards = findAll('[data-test-timeslot-card]'); | |
await click(cards[0]); | |
assert.dom(cards[0]).isFocused('timeslot card is focusable'); | |
assert.equal(cards[0].getAttribute('aria-label'), 'Show timeslot detail', 'timeslot card has aria label'); | |
assert.dom('[data-test-timeslot-detail-popover]').exists('Timeslot detail popover shows on focus'); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Application from '../app'; | |
import config from '../config/environment'; | |
import { setApplication } from '@ember/test-helpers'; | |
import { assign } from '@ember/polyfills'; | |
import { start } from 'ember-qunit'; | |
let attributes = { | |
rootElement: '#ember-testing', | |
autoboot: false | |
}; | |
attributes = assign(attributes, config.APP); | |
let application = Application.create(attributes); | |
setApplication(application); | |
start(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Component from '@glimmer/component'; | |
import { action } from '@ember/object'; | |
import { tracked } from '@glimmer/tracking'; | |
import { guidFor } from '@ember/object/internals'; | |
export default class TimeslotCard extends Component { | |
@tracked showTimeSlotDetails = false; | |
get elementId() { | |
return guidFor(this); | |
} | |
get timeslotIsAvailable() { | |
return this.args.timeslot.isAvailable; | |
} | |
get offset() { | |
return (Date.parse(this.args.timeslot.startDateTime) - Date.parse(this.args.earliestStartTime)) * 100 / this.args.maxTimeslotDuration; | |
} | |
get durationUnit() { | |
return this.args.timeslot.durationInHrs === 1 ? 'hour' : 'hours'; | |
} | |
@action | |
setStyle(element) { | |
const pctWidth = (this.args.timeslot.duration / this.args.maxTimeslotDuration) * 100; | |
element.style.width = `${pctWidth}%`; | |
element.style.marginLeft = `${this.offset}%`; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"version": "0.10.7", | |
"ENV": { | |
"modulePrefix": "twiddle", | |
"ember-cli-mirage": { | |
"enabled": true | |
} | |
}, | |
"EmberENV": { | |
"FEATURES": {}, | |
"_TEMPLATE_ONLY_GLIMMER_COMPONENTS": false, | |
"_APPLICATION_TEMPLATE_WRAPPER": true, | |
"_JQUERY_INTEGRATION": true | |
}, | |
"options": { | |
"use_pods": true, | |
"enable-testing": false | |
}, | |
"dependencies": { | |
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.js", | |
"ember": "3.18.1", | |
"ember-template-compiler": "3.18.1", | |
"ember-testing": "3.18.1" | |
}, | |
"addons": { | |
"@glimmer/component": "1.0.0", | |
"ember-data": "3.18.0", | |
"ember-cli-mirage": "1.1.8", | |
"ember-bootstrap": "3.1.4", | |
"@ember/render-modifiers": "1.0.2" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { get } from '@ember/object'; | |
import { assert } from '@ember/debug'; | |
export default class Timeslot { | |
date; | |
endTime; | |
startTime; | |
availableSpots; | |
activityName; | |
bookedCount; | |
maxGuests; | |
constructor (timeslot) { | |
this.date = timeslot.getAttributeValue('date'); | |
this.availableSpots = timeslot.getAttributeValue('availableSpots'); | |
this.endTime = timeslot.getAttributeValue('endTime'); | |
this.startTime = timeslot.getAttributeValue('startTime'); | |
this.activityName = timeslot.getAttributeValue('activityName'); | |
this.bookedCount = timeslot.getAttributeValue('bookedCount'); | |
this.maxGuests = timeslot.getAttributeValue('maxGuests'); | |
} | |
get endDateTime() { | |
return this.concatTimeToDate(this.endTime); | |
} | |
get startDateTime() { | |
return this.concatTimeToDate(this.startTime); | |
} | |
get duration() { | |
return Date.parse(this.endDateTime) - Date.parse(this.startDateTime); | |
} | |
get isoDate() { | |
return (this.date.toISOString()).split('T')[0]; | |
} | |
get durationInHrs() { | |
const oneHourInMs = 60 * 60 * 1000; | |
return this.duration / oneHourInMs; | |
} | |
get isAvailable() { | |
return Boolean(this.availableSpots); | |
} | |
get bookedPct() { | |
return (this.bookedCount * 100) / this.maxGuests; | |
} | |
concatTimeToDate(time) { | |
assert('time is a required argument!', Boolean(time)); | |
const [hours, minutes] = time.split(':'); | |
const date = new Date(this.date); | |
date.setHours(hours); | |
date.setMinutes(minutes); | |
return date; | |
} | |
} | |
export function maybeUnwrapProxy(proxy) { | |
if (!proxy) { | |
return undefined; | |
} | |
if ('content' in proxy) { | |
return get(proxy, 'content'); | |
} | |
return maybeUnwrapProxy(proxy); | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment