Skip to content

Commit 1a9fe1e

Browse files
committed
chore: refactor router to automatically create routes for tutorials
1 parent 7f9ce7e commit 1a9fe1e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

124 files changed

+1226
-1957
lines changed

DEVELOPING_TUTORIALS.md

Lines changed: 85 additions & 152 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 274 additions & 145 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@
4141
"cypress": "^3.8.3",
4242
"eslint": "^5.16.0",
4343
"eslint-config-standard": "^11.0.0",
44-
"eslint-config-standard-babel": "0.0.2",
45-
"eslint-plugin-babel": "^5.1.0",
44+
"eslint-plugin-babel": "^5.3.0",
4645
"eslint-plugin-import": "^2.13.0",
4746
"eslint-plugin-node": "^7.0.1",
4847
"eslint-plugin-promise": "^3.8.0",

public/lesson_sources.png

-487 Bytes
Loading

src/App.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<div id="app">
3-
<router-view></router-view>
3+
<router-view :key="$route.fullPath"></router-view>
44
</div>
55
</template>
66

src/components/Breadcrumbs.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<div class="lh-solid v-mid f4">
3-
<span class="b">{{tutorialShortname}}</span> |
3+
<span class="b">{{tutorial.shortTitle}}</span> |
44
<span v-if="isResources">Resources</span>
55
<span v-else>Lesson {{lessonNumber}} of {{lessonsInTutorial}}</span>
66
<img v-if="lessonPassed" src="../static/images/complete.svg" class="dib ml1 v-mid" alt="complete" />
@@ -11,7 +11,7 @@
1111
export default {
1212
props: {
1313
isResources: Boolean,
14-
tutorialShortname: String,
14+
tutorial: Object,
1515
lessonNumber: Number,
1616
lessonsInTutorial: Number,
1717
lessonPassed: Boolean

src/components/CongratulationsCallout.vue

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,13 @@ import { getTutorialFullUrl } from '../utils/tutorials'
2222
2323
export default {
2424
props: {
25-
tutorial: Object,
26-
tutorialId: String
25+
tutorial: Object
2726
},
2827
computed: {
2928
twitterShareLink: function () {
3029
let href = 'https://twitter.com/intent/tweet?'
3130
href += `text=I just completed the ${this.tutorial.title} tutorial at @ProtoSchool!`
32-
href += `&url=${encodeURIComponent(getTutorialFullUrl(this.tutorialId))}`
31+
href += `&url=${encodeURIComponent(getTutorialFullUrl(this.tutorial.formattedId))}`
3332
href += `&hashtags=${this.tutorial.project}`
3433
3534
return href

src/components/Lesson.vue

Lines changed: 86 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,18 @@
55
<section class="mw7 center mt3 pa3">
66
<div class="flex flex-row justify-between green">
77
<Breadcrumbs
8-
:isResources="isResources"
9-
:tutorialShortname="tutorialShortname"
10-
:lessonNumber="lessonNumber"
11-
:lessonsInTutorial="lessonsInTutorial"
12-
:lessonPassed="lessonPassed" />
8+
:isResources="isResources"
9+
:tutorial="tutorial"
10+
:lessonNumber="lessonId"
11+
:lessonsInTutorial="lessonsInTutorial"
12+
:lessonPassed="lessonPassed" />
1313
<TypeIcon
1414
:lessonId="isResources? 'resources' : lessonId"
15-
:tutorialId="tutorialId"
15+
:tutorialId="tutorial.formattedId"
1616
class="h2 ml3" />
17-
</div>
17+
</div>
1818
<CongratulationsCallout
1919
v-if="isResources && isTutorialPassed"
20-
:tutorialId="tutorialId"
2120
:tutorial="tutorial"
2221
class="mv4"
2322
/>
@@ -47,7 +46,7 @@
4746
v-if="exercise"
4847
:isFileLesson="isFileLesson"
4948
:editorReady="editorReady"
50-
:code="code"
49+
:code="editorCode"
5150
:solution="solution"
5251
:cachedCode="cachedCode"
5352
:onMounted="onMounted"
@@ -84,7 +83,7 @@
8483
:output="output.test"
8584
:isResources="isResources"
8685
:nextLessonIsResources="nextLessonIsResources"
87-
:lessonNumber="lessonNumber"
86+
:lessonNumber="lessonId"
8887
:lessonsInTutorial="lessonsInTutorial"
8988
:expandExercise="expandExercise"
9089
:isSubmitting="isSubmitting"
@@ -111,6 +110,8 @@ import marked from 'marked'
111110
import pTimeout from 'p-timeout'
112111
import newGithubIssueUrl from 'new-github-issue-url'
113112
113+
import { getTutorialByUrl, isTutorialPassed, getLesson } from '../utils/tutorials'
114+
import { EVENTS } from '../static/countly'
114115
import Header from './Header.vue'
115116
import Quiz from './Quiz.vue'
116117
import Resources from './Resources.vue'
@@ -123,9 +124,6 @@ import Output from './Output.vue'
123124
import Info from './Info.vue'
124125
import Validator from './Validator.vue'
125126
import CongratulationsCallout from './CongratulationsCallout.vue'
126-
import { EVENTS } from '../static/countly'
127-
import { deriveShortname } from '../utils/paths'
128-
import { getTutorial, isTutorialPassed, getLesson } from '../utils/tutorials'
129127
import TypeIcon from './TypeIcon.vue'
130128
131129
const MAX_EXEC_TIMEOUT = 5000
@@ -215,101 +213,98 @@ export default {
215213
CongratulationsCallout,
216214
TypeIcon
217215
},
216+
props: {
217+
lessonId: Number,
218+
isResources: Boolean,
219+
resources: Array,
220+
text: String,
221+
exercise: String,
222+
concepts: String,
223+
solution: String,
224+
modules: Object,
225+
validate: Function,
226+
code: String,
227+
overrideErrors: Boolean,
228+
isMultipleChoiceLesson: Boolean,
229+
question: String,
230+
choices: Array,
231+
createTestFile: Boolean,
232+
createTestTree: Boolean
233+
},
218234
data: self => {
219-
const tutorial = getTutorial(self.$attrs.tutorialId)
220-
const resourcesLesson = {
221-
title: 'Resources',
222-
type: 'resources'
223-
}
224-
const lesson = self.$attrs.isResources ? resourcesLesson : getLesson(self.$attrs.tutorialId, self.$attrs.lessonId)
225-
226235
return {
227-
lesson,
228-
tutorial,
229-
lessonId: self.$attrs.lessonId,
230-
tutorialId: self.$attrs.tutorialId,
231-
isResources: self.$attrs.isResources,
232-
resources: self.$attrs.resources,
233-
text: self.$attrs.text,
234-
exercise: self.$attrs.exercise,
235-
concepts: self.$attrs.concepts,
236-
cachedChoice: !!localStorage['cached' + self.$route.path],
237-
choice: localStorage[self.cacheKey] || '',
238-
cachedCode: !!localStorage['cached' + self.$route.path],
239-
code: localStorage[self.cacheKey] || self.$attrs.code || self.defaultCode,
240-
solution: self.$attrs.solution,
241236
isSubmitting: false,
242-
viewSolution: false,
243-
overrideErrors: self.$attrs.overrideErrors,
244-
isFileLesson: self.isFileLesson,
245-
isMultipleChoiceLesson: self.isMultipleChoiceLesson,
246-
question: self.$attrs.question,
247-
choices: self.$attrs.choices,
248-
parsedText: marked(self.$attrs.text || ''),
249-
parsedExercise: marked(self.$attrs.exercise || ''),
250-
parsedConcepts: marked(self.$attrs.concepts || ''),
237+
lessonPassed: !!localStorage['passed' + self.$route.path],
238+
lessonKey: 'passed' + self.$route.path,
251239
cacheKey: 'cached' + self.$route.path,
240+
cachedCode: !!localStorage['cached' + self.$route.path],
241+
viewSolution: false,
252242
cachedStateMsg: '',
253-
tutorialPath: tutorial.url,
254-
tutorialShortname: deriveShortname(self.$route.path),
255-
isTutorialPassed: isTutorialPassed(tutorial),
256-
lessonKey: 'passed' + self.$route.path,
257-
lessonPassed: !!localStorage['passed' + self.$route.path],
258-
createTestFile: self.$attrs.createTestFile,
259-
createTestTree: self.$attrs.createTestTree,
260-
output: self.output,
261243
showUploadInfo: false,
262244
expandExercise: false,
245+
editorReady: false,
246+
isFileLesson: self.isFileLesson,
263247
uploadedFiles: window.uploadedFiles || false,
264-
editorReady: false
248+
choice: localStorage[self.cacheKey] || '',
249+
cachedChoice: !!localStorage['cached' + self.$route.path]
265250
}
266251
},
267252
computed: {
268-
lessonNumber: function () {
269-
return parseInt(this.lessonId, 10)
253+
tutorial: function () {
254+
return getTutorialByUrl(this.$route.params.tutorialUrl)
255+
},
256+
isTutorialPassed: function () {
257+
return isTutorialPassed(this.tutorial)
258+
},
259+
parsedText: function () {
260+
return marked(this.text || '')
261+
},
262+
parsedExercise: function () {
263+
return marked(this.exercise || '')
264+
},
265+
parsedConcepts: function () {
266+
return marked(this.concepts || '')
267+
},
268+
lesson: function () {
269+
return getLesson(this.tutorial.formattedId, this.lessonId)
270270
},
271271
lessonIssueUrl: function () {
272-
return encodeURI(`https://github.com/ProtoSchool/protoschool.github.io/issues/new?assignees=&labels=lesson-feedback&template=lesson-feedback.md&title=Lesson+Feedback%3A+${this.tutorialShortname}+-+Lesson+${this.lessonNumber}+(${this.lesson.title})`)
272+
return encodeURI(`https://github.com/ProtoSchool/protoschool.github.io/issues/new?assignees=&labels=lesson-feedback&template=lesson-feedback.md&title=Lesson+Feedback%3A+${this.tutorial.shortTitle}+-+Lesson+${this.lessonId}+(${this.lesson.title})`)
273273
},
274274
tutorialIssueUrl: function () {
275-
return encodeURI(`https://github.com/ProtoSchool/protoschool.github.io/issues/new?assignees=&labels=tutorial-feedback&template=tutorial-feedback.md&title=Tutorial+Feedback%3A+${this.tutorialShortname}`)
275+
return encodeURI(`https://github.com/ProtoSchool/protoschool.github.io/issues/new?assignees=&labels=tutorial-feedback&template=tutorial-feedback.md&title=Tutorial+Feedback%3A+${this.tutorial.shortTitle}`)
276276
},
277277
lessonsInTutorial: function () {
278-
const basePath = this.$route.path.slice(0, -2)
279-
let number = this.$route.path.slice(-2)
280-
while (this.$router.resolve(basePath + number).route.name !== '404') {
281-
number++
282-
number = number.toString().padStart(2, '0')
283-
}
284-
return parseInt(number) - 1
278+
return this.tutorial.lessons.length
285279
},
286280
nextLessonIsResources: function () {
287281
const basePath = this.$route.path.slice(0, -2)
288282
const hasResources = this.$router.resolve(basePath + 'resources').route.name !== '404'
289-
return this.lessonNumber === this.lessonsInTutorial && hasResources
283+
return this.lessonId === this.lessonsInTutorial && hasResources
284+
},
285+
editorCode: {
286+
get: function () {
287+
return localStorage[this.cacheKey] || this.code || this.defaultCode
288+
},
289+
set: function (newCode) {
290+
localStorage[this.cacheKey] = newCode
291+
}
290292
}
291293
},
292294
beforeCreate: function () {
293295
this.output = {}
294296
this.defaultCode = defaultCode
295297
this.IPFSPromise = import('ipfs').then(m => m.default)
296-
// doesn't work to set lessonPassed in here because it can't recognize lessonKey yet
297298
},
298299
beforeMount: function () {
299300
this.choice = localStorage[this.cacheKey] || ''
300301
},
301-
// updated: function () {
302-
// runs on page load AND every keystroke in editor AND submit
303-
// },
304-
// beforeUpdate: function () {
305-
// runs on every keystroke in editor, NOT on page load, NOT on code submit
306-
// },
307302
methods: {
308303
validationIssueUrl: function (code, validationTimeout) {
309304
return newGithubIssueUrl({
310305
user: 'ProtoSchool',
311306
repo: 'protoschool.github.io',
312-
title: `Validation Error: ${this.tutorialShortname} - Lesson ${this.lessonNumber} (${this.lesson.title})`,
307+
title: `Validation Error: ${this.tutorial.shortTitle} - Lesson ${this.lessonId} (${this.lesson.title})`,
313308
labels: ['lesson-feedback', 'validation-error'],
314309
body: `If you submitted code for a lesson and received feedback indicating a validation error, you may have uncovered a bug in our lesson validation code. We've prepopulated the error type and the last code you submitted below as diagnostic clues. Feel free to add additional feedback about the lesson below before clicking "Submit new issue."
315310
@@ -355,11 +350,11 @@ export default {
355350
this.showUploadInfo = false
356351
}
357352
358-
if (this.$attrs.modules) modules = this.$attrs.modules
353+
if (this.modules) modules = this.modules
359354
if (this.isFileLesson) args.unshift(this.uploadedFiles)
360355
// Output external errors or not depending on flag
361356
const result = await _eval(code, ipfs, modules, args)
362-
if (!this.$attrs.overrideErrors && result instanceof Error) {
357+
if (!this.overrideErrors && result instanceof Error) {
363358
Vue.set(output, 'test', result)
364359
this.lessonPassed = !!localStorage[this.lessonKey]
365360
this.isSubmitting = false
@@ -376,8 +371,9 @@ export default {
376371
377372
// Run the `validate` function in the lesson
378373
try {
379-
test = await this.$attrs.validate(result, ipfs, args)
380-
} catch (err) {
374+
test = await this.validate(result, ipfs, args)
375+
} catch (error) {
376+
console.error(error)
381377
// Something in our validation threw an error, it's probably a bug
382378
test = {
383379
fail: `You may have uncovered a bug in our validation code. Please help us improve this lesson by [**opening an issue**](${this.validationIssueUrl(code, true)}) noting that you encountered a validation timeout error. Then you can click **Reset Code** above the code editor, review the instructions, and try again. Still having trouble? Click **View Solution** below the code editor to see the approach we recommend for this challenge.`
@@ -457,9 +453,9 @@ export default {
457453
},
458454
resetCode: function () {
459455
// TRACK? User chose to reset code
460-
this.code = this.$attrs.code || defaultCode
456+
this.editorCode = this.code || defaultCode
461457
// this ^ triggers onCodeChange which will clear cache
462-
this.editor.setValue(this.code)
458+
this.editor.setValue(this.editorCode)
463459
this.clearPassed()
464460
delete this.output.test
465461
this.showUploadInfo = false
@@ -472,31 +468,30 @@ export default {
472468
},
473469
clearPassed: function () {
474470
delete localStorage[this.lessonKey]
475-
this.lessonPassed = !!localStorage[this.lessonKey]
476-
delete localStorage[`passed/${this.tutorialPath}`]
471+
delete localStorage[`passed/${this.tutorial.url}`]
477472
},
478473
loadCodeFromCache: function () {
479-
this.code = localStorage[this.cacheKey]
480-
this.editor.setValue(this.code)
474+
this.editorCode = localStorage[this.cacheKey]
475+
this.editor.setValue(this.editorCode)
481476
},
482477
updateTutorialState: function () {
483478
for (let i = 1; i <= this.lessonsInTutorial; i++) {
484479
const lessonNr = i.toString().padStart(2, 0)
485-
const lsKey = `passed/${this.tutorialPath}/${lessonNr}`
480+
const lsKey = `passed/${this.tutorial.url}/${lessonNr}`
486481
if (localStorage[lsKey] !== 'passed') {
487482
return false
488483
}
489484
}
490-
localStorage[`passed/${this.tutorialPath}`] = 'passed'
485+
localStorage[`passed/${this.tutorial.url}`] = 'passed'
491486
this.trackEvent(EVENTS.TUTORIAL_PASSED)
492487
return true
493488
},
494489
trackEvent: function (event, opts = {}) {
495490
window.Countly.q.push(['add_event', {
496491
key: event,
497492
segmentation: {
498-
tutorial: this.tutorialShortname,
499-
lessonNumber: this.lessonNumber,
493+
tutorial: this.tutorial.shortTitle,
494+
lessonNumber: this.lessonId,
500495
path: this.$route.path,
501496
...opts
502497
}
@@ -522,14 +517,13 @@ export default {
522517
// TRACK? edited back to default state by chance or by 'reset code'
523518
delete localStorage[this.cacheKey]
524519
this.cachedCode = !!localStorage[this.cacheKey]
525-
} else if (this.code === this.editor.getValue()) {
520+
} else if (this.editorCode === this.editor.getValue()) {
526521
// TRACK? returned to cached lesson in progress
527522
} else {
528-
localStorage[this.cacheKey] = this.editor.getValue()
529-
this.code = this.editor.getValue()
523+
this.editorCode = this.editor.getValue()
530524
this.cachedCode = !!localStorage[this.cacheKey]
531525
this.cachedStateMsg = "We're saving your code as you go."
532-
if (this.code !== this.solution) {
526+
if (this.editorCode !== this.solution) {
533527
this.clearPassed()
534528
delete this.output.test
535529
}
@@ -560,14 +554,13 @@ export default {
560554
Vue.set(this.output, 'test', null)
561555
} else {
562556
localStorage[this.lessonKey] = 'passed'
563-
this.lessonPassed = !!localStorage[this.lessonKey]
564557
// track passed lesson if text only
565558
if (!this.isMultipleChoiceLesson) {
566559
this.trackEvent(EVENTS.LESSON_PASSED)
567560
this.updateTutorialState()
568561
}
569562
}
570-
const current = this.lessonNumber
563+
const current = this.lessonId
571564
572565
const next = this.nextLessonIsResources
573566
? 'resources'
@@ -588,7 +581,7 @@ export default {
588581
this.expandExercise = !this.expandExercise
589582
},
590583
cyReplaceWithSolution: function () {
591-
this.editor.setValue(this.$attrs.solution)
584+
this.editor.setValue(this.solution)
592585
},
593586
parseData: (data) => marked(data)
594587
}

0 commit comments

Comments
 (0)