Skip to content

Commit c3de28b

Browse files
mikealfsdiogo
authored andcommitted
feat: add file upload functionality and error hijacking (#111)
1 parent baded2e commit c3de28b

37 files changed

+3106
-2276
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ cache:
77
before_script:
88
npm install
99
script:
10+
npm test
1011
npm run build
1112
notifications:
1213
email: false

README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,23 @@ Example (while in `tutorials`):
3939

4040
#### Vue file
4141

42-
Select the boilerplate Vue file for your lesson from the `tutorials` directory.
42+
Select the appropriate boilerplate Vue file for your lesson from the `tutorials/boilerplates` directory:
43+
44+
- `boilerplate-standard.vue` for a lesson with an exercise which does not require a file upload
45+
- `boilerplate-file-upload.vue` for a lesson with an exercise that requires a file upload
46+
- `boilerplate-no-exercise.vue` for a text-only lesson
4347

4448
Copy that boilerplate into your tutorial folder and rename it to the 2-digit number of the lesson.
4549

46-
Example:
50+
Example (while in `tutorials`):
4751

4852
```sh
49-
> cp tutorials/boilerplate.vue tutorials/Tutorial-Shortname/01.vue
53+
> cp boilerplate/boilerplate-standard.vue Tutorial-Shortname/01.vue
5054
```
5155

5256
Replace anything in the boilerplate file that reads "REPLACEME".
5357

58+
5459
#### Markdown file
5560

5661
Create a `.md` file alongside your `.vue` and add the markdown formatted text
@@ -100,7 +105,7 @@ lessons in your tutorial.
100105

101106
For example, if you add 3 lessons with the following routes:
102107

103-
```
108+
```js
104109
{ path: '/basics/01', component: LessonBasics01 },
105110
{ path: '/basics/02', component: LessonBasics02 },
106111
{ path: '/basics/03', component: LessonBasics03 },
@@ -111,7 +116,7 @@ your second lesson will display the following under the lesson title:
111116

112117
If you add 5 lesssons with the following routes:
113118

114-
```
119+
```js
115120
{ path: '/data-structures/01', component: LessonDataStructures01 },
116121
{ path: '/data-structures/02', component: LessonDataStructures02 },
117122
{ path: '/data-structures/03', component: LessonDataStructures03 },
@@ -125,6 +130,7 @@ your third lesson will display the following under the lesson title:
125130

126131
Notice how multi-word lesson shortnames are treated here. In filepaths, they are lowercase and hyphenated (e.g. `/data-structures/01`). In component names they are upper camel case (smushed together with the first letter of each word capitalized, e.g. `LessonDataStructures01`).
127132

133+
128134
### Add your tutorial to `tutorials.json` and `courses.json`
129135

130136
In `static/tutorials.json`, add a new key for your tutorial (for example, `tutorialShortname` as shown in the example below) and fill in the appropriate values:
@@ -162,6 +168,7 @@ In `static/courses.json`, add the tutorial key to the `all` array so it will app
162168

163169
The project maintainers will take care of making any updates needed to ensure your project is featured in any relevant course listings.
164170

171+
165172
## Boilerplate Explained
166173

167174
```javascript

package-lock.json

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

src/components/File-Lesson.vue

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<script>
2+
import Lesson from './Lesson.vue'
3+
4+
const defaultCode = `/* globals ipfs */
5+
6+
const run = async (files) => {
7+
// your code goes here!
8+
// be sure this function returns the requested value
9+
}
10+
11+
return run
12+
13+
`
14+
15+
export default {
16+
extends: Lesson,
17+
beforeCreate: function () {
18+
this.isFileLesson = true
19+
this.defaultCode = defaultCode
20+
},
21+
methods: {
22+
onFileDrop: function (event) {
23+
event.preventDefault()
24+
event.stopPropagation()
25+
let files = Array.from(event.dataTransfer.files)
26+
for (let f of Array.from(event.dataTransfer.items)) {
27+
let isFile = f.getAsEntry ? f.getAsEntry().isFile : (f.webkitGetAsEntry ? f.webkitGetAsEntry().isFile : true)
28+
if (!isFile) {
29+
return alert("Folder upload is not supported. Please select a file or multiple files.")
30+
}
31+
}
32+
this.onFiles(files)
33+
return false
34+
},
35+
onFileClick: function (event) {
36+
event.preventDefault()
37+
event.stopPropagation()
38+
let elem = document.createElement('input')
39+
elem.setAttribute("type", "file")
40+
elem.setAttribute('multiple', true)
41+
elem.onchange = () => {
42+
this.onFiles(Array.from(elem.files))
43+
}
44+
elem.click()
45+
},
46+
onFiles: function (files) {
47+
this.uploadedFiles = files
48+
window.uploadedFiles = files
49+
}
50+
}
51+
}
52+
</script>

src/components/Lesson.vue

Lines changed: 87 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,36 @@
5151
</div>
5252
</h2>
5353
<div v-if="exercise" v-html="parsedExercise" class='lh-copy'></div>
54+
<div v-if="isFileLesson">
55+
<div class="f5 fw7 mt4 mb2"> Step 1: Upload file(s)
56+
<span class="pl1"><img v-if="uploadedFiles" src="../static/images/complete.svg" alt="complete" style="height: 1.2rem;" class="v-mid"/></span>
57+
</div>
58+
<div id="drop-area" v-if="!uploadedFiles" v-on:drop="onFileDrop"
59+
v-on:click="onFileClick"
60+
@dragenter="dragging=true" @dragend="dragging=false" @dragleave="dragging=false"
61+
@dragover.prevent v-bind:class="{dragging: dragging}" class="dropfile mb2 pa2 w-100 br3 shadow-4 bg-white color-navy">
62+
<div class="o-80 glow">
63+
<label for="add-files" class="flex items-center h4 pointer">
64+
<svg viewBox="0 0 100 100" class="fill-aqua" height="60px" alt="Add"><path d="M71.13 28.87a29.88 29.88 0 1 0 0 42.26 29.86 29.86 0 0 0 0-42.26zm-18.39 37.6h-5.48V52.74H33.53v-5.48h13.73V33.53h5.48v13.73h13.73v5.48H52.74z"></path></svg>
65+
<div class="f5 charcoal">
66+
<p><strong>Drop one or more files here or click to select.</strong> Folder upload is not supported, but you may select multiple files using Ctrl+Click or Command+Click.</p>
67+
</div>
68+
</label>
69+
</div>
70+
</div>
71+
<div v-else class="mt2">
72+
<span v-on:click="resetFileUpload" class="textLink fr pb1">Start Over</span>
73+
<div class="mb2 pl3 pa2 w-100 br3 h4 shadow-4 bg-white color-navy flex items-center">
74+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" class="fill-aqua" height="60px"><path d="M55.94 19.17H30a4 4 0 0 0-4 4v53.65a4 4 0 0 0 4 4h40.1a4 4 0 0 0 4-4V38.06zm5.28 21.08c-4.33 0-7.47-2.85-7.47-6.77V21l18.13 19.25z"/></svg>
75+
<ul class="list pl0">
76+
<li v-for="(file, idx) in uploadedFiles" :key="`file-${idx}`">{{file.name}}</li>
77+
</ul>
78+
</div>
79+
</div>
80+
<div class="f5 fw7 mt4 mb2">Step 2: Update code
81+
<span class="pl1"><img v-if="cachedCode" src="../static/images/complete.svg" alt="complete" style="height: 1.2rem;" class="v-mid"/></span>
82+
</div>
83+
</div>
5484
</div>
5585
<div>
5686
<span v-if="cachedCode" v-on:click="resetCode" class="textLink fr pb1">Reset Code</span>
@@ -86,7 +116,12 @@
86116
</div>
87117
</div>
88118
<div class="lh-copy pv2 ph3" v-else>
89-
Update the code to complete the exercise. Click <strong>submit</strong> to check your answer.
119+
<div v-if="isFileLesson">
120+
Upload file(s) and update the code to complete the exercise. Click <strong>Submit</strong> to check your answer.
121+
</div>
122+
<div v-else>
123+
Update the code to complete the exercise. Click <strong>Submit</strong> to check your answer.
124+
</div>
90125
</div>
91126
</div>
92127
<div class="pt3 ph2 tr">
@@ -97,7 +132,11 @@
97132
<Button v-bind:click="next" class="bg-aqua white">Next</Button>
98133
</div>
99134
<div v-else>
100-
<Button v-bind:click="run" class="bg-aqua white">Submit</Button>
135+
<span v-if="isFileLesson && !uploadedFiles" class="disabledButtonWrapper"><Button v-bind:click="next" class="bg-aqua white" disabled>Submit</Button></span>
136+
<Button v-else v-bind:click="run" class="bg-aqua white">Submit</Button>
137+
<div v-if="isFileLesson && !uploadedFiles" class="red lh-copy pt2 o-0">
138+
You must upload a file before submitting.
139+
</div>
101140
</div>
102141
</div>
103142
</div>
@@ -129,8 +168,8 @@ import MonacoEditor from 'vue-monaco-editor'
129168
import Explorer from './Explorer.vue'
130169
import Button from './Button.vue'
131170
import Header from './Header.vue'
132-
const CID = require('cids')
133-
const marked = require('marked')
171+
import CID from 'cids'
172+
import marked from 'marked'
134173
135174
const hljs = require('highlight.js/lib/highlight.js')
136175
hljs.registerLanguage('js', require('highlight.js/lib/languages/javascript'))
@@ -143,7 +182,7 @@ marked.setOptions({
143182
}
144183
})
145184
146-
const _eval = async (text, ipfs, modules = {}) => {
185+
const _eval = async (text, ipfs, modules = {}, args = []) => {
147186
await new Promise(resolve => ipfs.on('ready', resolve))
148187
149188
let fn
@@ -161,7 +200,7 @@ const _eval = async (text, ipfs, modules = {}) => {
161200
return modules[name]
162201
}
163202
try {
164-
result = await fn(ipfs, require)()
203+
result = await fn(ipfs, require)(...args)
165204
} catch (e) {
166205
result = {error: e}
167206
}
@@ -193,7 +232,9 @@ export default {
193232
exercise: self.$attrs.exercise,
194233
concepts: self.$attrs.concepts,
195234
cachedCode: !!localStorage['cached' + self.$route.path],
196-
code: localStorage[self.cacheKey] || self.$attrs.code || defaultCode,
235+
code: localStorage[self.cacheKey] || self.$attrs.code || self.defaultCode,
236+
overrideErrors: self.$attrs.overrideErrors,
237+
isFileLesson: self.isFileLesson,
197238
parsedText: marked(self.$attrs.text),
198239
parsedExercise: marked(self.$attrs.exercise || ''),
199240
parsedConcepts: marked(self.$attrs.concepts || ''),
@@ -204,6 +245,8 @@ export default {
204245
lessonTitle: self.$attrs.lessonTitle,
205246
output: self.output,
206247
expandExercise: false,
248+
dragging: false,
249+
uploadedFiles: window.uploadedFiles || false,
207250
options: {
208251
selectOnLineNumbers: false,
209252
lineNumbersMinChars: 3,
@@ -259,7 +302,8 @@ export default {
259302
},
260303
beforeCreate: function () {
261304
this.output = {}
262-
this.IPFSPromise = import('ipfs')
305+
this.defaultCode = defaultCode
306+
this.IPFSPromise = import('ipfs').then(m => m.default)
263307
// doesn't work to set lessonPassed in here because it can't recognize lessonKey yet
264308
},
265309
updated: function () {
@@ -269,7 +313,7 @@ export default {
269313
// runs on every keystroke in editor, NOT on page load, NOT on code submit
270314
},
271315
methods: {
272-
run: async function () {
316+
run: async function (...args) {
273317
if (oldIPFS) {
274318
oldIPFS.stop()
275319
oldIPFS = null
@@ -279,14 +323,16 @@ export default {
279323
let code = this.editor.getValue()
280324
let modules = {}
281325
if (this.$attrs.modules) modules = this.$attrs.modules
282-
let result = await _eval(code, ipfs, modules)
283-
284-
if (result && result.error) {
326+
if (this.isFileLesson) args.unshift(this.uploadedFiles)
327+
// Output external errors or not depending on flag
328+
let result = await _eval(code, ipfs, modules, args)
329+
if (!this.$attrs.overrideErrors && result && result.error) {
285330
Vue.set(output, 'test', result)
286331
this.lessonPassed = !!localStorage[this.lessonKey]
287332
return
288333
}
289-
let test = await this.$attrs.validate(result, ipfs)
334+
// Run the `validate` function in the lesson
335+
let test = await this.$attrs.validate(result, ipfs, args)
290336
Vue.set(output, 'test', test)
291337
if (CID.isCID(result)) {
292338
oldIPFS = ipfs
@@ -304,7 +350,7 @@ export default {
304350
return this.$attrs.createIPFS()
305351
} else {
306352
let ipfs = this.IPFSPromise.then(IPFS => {
307-
return IPFS.createNode({repo: Math.random().toString()})
353+
return new IPFS({repo: Math.random().toString()})
308354
})
309355
return ipfs
310356
}
@@ -316,6 +362,11 @@ export default {
316362
this.editor.setValue(this.code)
317363
this.clearPassed()
318364
},
365+
resetFileUpload: function () {
366+
this.uploadedFiles = false
367+
this.dragging = false
368+
console.log({uploadedFiles: this.uploadedFiles})
369+
},
319370
clearPassed: function () {
320371
delete localStorage[this.lessonKey]
321372
this.lessonPassed = !!localStorage[this.lessonKey]
@@ -380,6 +431,18 @@ export default {
380431
</script>
381432

382433
<style scoped>
434+
435+
button:disabled {
436+
cursor: not-allowed;
437+
}
438+
439+
.disabledButtonWrapper:hover + div {
440+
opacity: 1;
441+
transition: opacity .2s ease-in;
442+
}
443+
.dragging {
444+
border: 5px solid #69c4cd;
445+
}
383446
.editor {
384447
height: 100%;
385448
min-height: 15rem;
@@ -420,4 +483,14 @@ footer a {
420483
margin-left: 93px;
421484
}
422485
}
486+
487+
div.dropfile {
488+
cursor: pointer;
489+
}
490+
div.dropfile input {
491+
display: none;
492+
}
493+
div#drop-area * {
494+
pointer-events: none;
495+
}
423496
</style>

src/components/Navigation.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
</div>
1212
<!-- standard nav -->
1313
<div v-else class="dn flex overflow-auto items-center bg-aqua white pv3 center tc mw7">
14-
<div v-for="link in links">
14+
<div v-for="(link, idx) in links" :key="`desktop-${idx}`">
1515
<router-link :class="[isActive(link) ? 'white' : 'navy ', 'nav-link']" :to="`${link.path}`">{{link.text}}</router-link>
1616
</div>
1717
</div>
@@ -37,7 +37,7 @@
3737
<!-- hamburger displayed when requested -->
3838
<div :class="{ dn: isHamburgerClosed }">
3939
<div class="tc bg-aqua-muted white">
40-
<div v-for="link in links">
40+
<div v-for="(link, idx) in links" :key="`mobile-${idx}`">
4141
<router-link @click.native="toggleHamburger" :class="[isActive(link) || isActiveLesson(link) ? 'white' : 'navy', 'link pa3 fw5 f4 db bb border-aqua']" :to="`${link.path}`">{{link.text}}</router-link>
4242
</div>
4343
</div>

src/main.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ import LessonDataStructures02 from './tutorials/Data-Structures/02.vue'
2929
import LessonDataStructures03 from './tutorials/Data-Structures/03.vue'
3030
import LessonDataStructures04 from './tutorials/Data-Structures/04.vue'
3131
import LessonDataStructures05 from './tutorials/Data-Structures/05.vue'
32+
// import MutableFileSystem01 from './tutorials/Mutable-File-System/01.vue'
33+
// import MutableFileSystem02 from './tutorials/Mutable-File-System/02.vue'
34+
// import MutableFileSystem03 from './tutorials/Mutable-File-System/03.vue'
35+
// import MutableFileSystem04 from './tutorials/Mutable-File-System/04.vue'
36+
// import MutableFileSystem05 from './tutorials/Mutable-File-System/05.vue'
3237

3338
Vue.use(VueRouter)
3439

@@ -61,6 +66,13 @@ const routes = [
6166
{ path: '/blog/05', component: LessonBlog05 },
6267
{ path: '/blog/06', component: LessonBlog06 },
6368
{ path: '/blog/07', component: LessonBlog07 },
69+
// Lessons - MFS
70+
// { path: '/mutable-file-system', component: Landing, props: { tutorialId: 'mutableFileSystem' } },
71+
// { path: '/mutable-file-system/01', component: MutableFileSystem01 },
72+
// { path: '/mutable-file-system/02', component: MutableFileSystem02 },
73+
// { path: '/mutable-file-system/03', component: MutableFileSystem03 },
74+
// { path: '/mutable-file-system/04', component: MutableFileSystem04 },
75+
// { path: '/mutable-file-system/05', component: MutableFileSystem05 },
6476
// 404
6577
{ path: '*', name: '404' }
6678
]

src/static/courses.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"all": ["dataStructures", "basics", "blog"],
33
"featured": ["dataStructures", "basics", "blog"]
4-
}
4+
}

src/static/images/glyph_document.svg

Lines changed: 13 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)