Skip to content

Commit b111973

Browse files
feat: add MFS tutorial (#200)
* Surface MFS tutorials - Uncomment MFS lessons in `main.js` - Add MFS tutorial to courses in `courses.json` * Grammar fixes * update lesson validation wording * change _solution to solution * add missing solution code to mfs lesson 2 * add solution prop to MFS lessons and boilerplates * Rework lesson order in MFS tutorial Rework lesson order * Update stat lesson content * Fix syntax error and validation in stat lesson * update lesson on stating again * swap order of lessons 2 and 3 * Rework messaging for IPFS introk * Apply new logging feature to file upload demo * Validate directory creation for mkdir (MFS #7) * chore: simplify js object files logging * add validation for missing {parents:true} * add validation for use of ls in stat lesson * update text of lessons 3, 4, 5 * improve formatting of references to root directory * improve example for mkdir lesson * add validation for other errors in mkdir lesson 7 * add blank space around user code area * add validation for matching filenames in #7 * remove console logs * validate ls-ing wrong directory in #7 * improve error msg when returning wrong ls in #7 * add boilerplates for lessons 8-12 * correct relative filepaths in readme * fix typo lesson 7 * add lesson 8 text * fix typo and scope issues lsn 7 * update lesson 8 instructions * add globals to boilerplates * add code and first pass validation for lsn 8 * attempt to test for incorrect use of await * test for mistakenly moving directory * swap order of validation to fix bug * add first draft of files.cp lesson * tweak lesson 9 solution display * first draft of files.read lesson * lesson 10 validation edits * fix: lesson 7 validations * address await problems in lesson 8 * add text for lesson 11 * revamp lesson 8 mv solution * remove lesson 12 * add initial validation for lesson 11 rm * attempt to add test file to non-mfs ipfs * add notes on TBValidated lessons * chore: lessons cleanup * chore: update lesson 09 * feat: lesson 9 now copies file from ipfs * feat: make lesson 10 work * feat: finish lesson 11 * fix: appease linter * fix: LessonLink spacing * fix: grow file upload container * chore: apply suggestions from code review Co-Authored-By: Alan Shaw <[email protected]> * chore: replace folder word with directory * chore: rephrase mfs editing * chore: typos * text edits * make user name file when copying * improve lesson 9 validation * read from success.txt in lesson 10 * improve validation for lesson 11 * update headline and lesson titles * chore: update mfs description * fix: add await to ipfs.files.mv * chore: tidying up * feat: add loading animation
1 parent ff5a776 commit b111973

Some content is hidden

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

44 files changed

+1325
-274
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ Select the appropriate boilerplate Vue file for your lesson from the `tutorials/
9494
- `boilerplate-file-upload.vue` for a lesson with a coding exercise that requires a file upload
9595
- `boilerplate-no-exercise.vue` for a text-only lesson
9696

97-
Copy that boilerplate into your tutorial folder and rename it to the 2-digit number of the lesson.
97+
Copy that boilerplate into your tutorial directory and rename it to the 2-digit number of the lesson.
9898

9999
Example (while in `src/tutorials`):
100100

@@ -254,7 +254,7 @@ you need to override, as in this example:
254254
```js
255255
} else if (result.error && result.error.message === 'No child name passed to addLink') {
256256
// Forgot the file name and just used a directory as the path
257-
return { fail: 'Uh oh. It looks like you created a folder instead of a file. Did you forget to include a filename in your path?' }
257+
return { fail: 'Uh oh. It looks like you created a directory instead of a file. Did you forget to include a filename in your path?' }
258258
}
259259
```
260260
Be sure to adapt your test case so that it works within the context of your other conditionals to meet your validation needs. What is required is that you return an object with the `fail` key and a string as its value; that string is what will be shown to the user.

src/components/File-Lesson.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default {
2525
for (let f of Array.from(event.dataTransfer.items)) {
2626
let isFile = f.getAsEntry ? f.getAsEntry().isFile : (f.webkitGetAsEntry ? f.webkitGetAsEntry().isFile : true)
2727
if (!isFile) {
28-
return alert("Folder upload is not supported. Please select a file or multiple files.")
28+
return alert('Directory upload is not supported. Please select one or more files.')
2929
}
3030
}
3131
this.onFiles(files)
@@ -35,7 +35,7 @@ export default {
3535
event.preventDefault()
3636
event.stopPropagation()
3737
let elem = document.createElement('input')
38-
elem.setAttribute("type", "file")
38+
elem.setAttribute('type', 'file')
3939
elem.setAttribute('multiple', true)
4040
elem.onchange = () => {
4141
this.onFiles(Array.from(elem.files))

src/components/Lesson.vue

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,14 @@
6262
<label for="add-files" class="flex items-center h4 pointer">
6363
<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>
6464
<div class="f5 charcoal">
65-
<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>
65+
<p><strong>Drop one or more files here or click to select.</strong> Directory upload is not supported, but you may select multiple files using Ctrl+Click or Command+Click.</p>
6666
</div>
6767
</label>
6868
</div>
6969
</div>
7070
<div v-else class="mt2">
7171
<span v-on:click="resetFileUpload" class="textLink fr pb1">Start Over</span>
72-
<div class="mb2 pl3 pa2 w-100 br3 h4 shadow-4 bg-white color-navy flex items-center">
72+
<div class="mb2 pl3 pa2 w-100 br3 shadow-4 bg-white color-navy flex items-center">
7373
<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>
7474
<ul class="list pl0">
7575
<li v-for="(file, idx) in uploadedFiles" :key="`file-${idx}`">{{file.name}}</li>
@@ -151,8 +151,11 @@
151151
<Button v-bind:click="next" class="bg-aqua white" data-cy="next-lesson">Next</Button>
152152
</div>
153153
<div v-else>
154-
<span v-if="isFileLesson && !uploadedFiles" class="disabledButtonWrapper">
155-
<Button v-bind:click="next" class="bg-aqua white" disabled>Submit</Button>
154+
<span v-if="(isFileLesson && !uploadedFiles) || isSubmitting" class="disabledButtonWrapper">
155+
<Button v-bind:click="next" class="bg-aqua white" disabled>
156+
<span v-if="isSubmitting" class="loader"></span>
157+
<span v-else>Submit</span>
158+
</Button>
156159
</span>
157160
<Button v-else v-bind:click="run" class="bg-aqua white" data-cy="submit-answer">Submit</Button>
158161
<div v-if="isFileLesson && !uploadedFiles" class="red lh-copy pt2 o-0">
@@ -254,6 +257,7 @@ export default {
254257
cachedCode: !!localStorage['cached' + self.$route.path],
255258
code: localStorage[self.cacheKey] || self.$attrs.code || self.defaultCode,
256259
solution: self.$attrs.solution,
260+
isSubmitting: false,
257261
viewSolution: false,
258262
overrideErrors: self.$attrs.overrideErrors,
259263
isFileLesson: self.isFileLesson,
@@ -265,6 +269,7 @@ export default {
265269
lessonKey: 'passed' + self.$route.path,
266270
lessonPassed: !!localStorage['passed' + self.$route.path],
267271
lessonTitle: self.$attrs.lessonTitle,
272+
createTestFile: self.$attrs.createTestFile,
268273
output: self.output,
269274
expandExercise: false,
270275
dragging: false,
@@ -337,14 +342,17 @@ export default {
337342
},
338343
methods: {
339344
run: async function (...args) {
345+
this.isSubmitting = true
340346
if (oldIPFS) {
341347
oldIPFS.stop()
342348
oldIPFS = null
343349
}
344350
let output = this.output
345351
let ipfs = await this.createIPFS()
352+
if (this.createTestFile) {
353+
await this.createFile(ipfs)
354+
}
346355
let code = this.editor.getValue()
347-
348356
let modules = {}
349357
if (this.$attrs.modules) modules = this.$attrs.modules
350358
if (this.isFileLesson) args.unshift(this.uploadedFiles)
@@ -353,6 +361,7 @@ export default {
353361
if (!this.$attrs.overrideErrors && result && result.error) {
354362
Vue.set(output, 'test', result)
355363
this.lessonPassed = !!localStorage[this.lessonKey]
364+
this.isSubmitting = false
356365
return
357366
}
358367
// Hide the solution
@@ -370,17 +379,27 @@ export default {
370379
localStorage[this.lessonKey] = 'passed'
371380
}
372381
this.lessonPassed = !!localStorage[this.lessonKey]
382+
this.isSubmitting = false
373383
},
374384
createIPFS: function () {
375385
if (this.$attrs.createIPFS) {
376386
return this.$attrs.createIPFS()
377387
} else {
378388
let ipfs = this.IPFSPromise.then(IPFS => {
379-
return new IPFS({repo: Math.random().toString()})
389+
this.ipfsConstructor = IPFS
390+
return new IPFS({ repo: Math.random().toString() })
380391
})
381392
return ipfs
382393
}
383394
},
395+
createFile: function (ipfs) {
396+
new Promise((resolve, reject) => {
397+
ipfs.on('ready', async () => {
398+
await ipfs.add(this.ipfsConstructor.Buffer.from('You did it!'))
399+
resolve()
400+
})
401+
})
402+
},
384403
resetCode: function () {
385404
// TRACK? User chose to reset code
386405
this.code = this.$attrs.code || defaultCode
@@ -400,7 +419,6 @@ export default {
400419
resetFileUpload: function () {
401420
this.uploadedFiles = false
402421
this.dragging = false
403-
console.log({uploadedFiles: this.uploadedFiles})
404422
},
405423
clearPassed: function () {
406424
delete localStorage[this.lessonKey]
@@ -568,6 +586,49 @@ div#drop-area * {
568586
border-width: 5px 5px 5px;
569587
margin-top: 5px;
570588
}
589+
590+
.loader,
591+
.loader:before,
592+
.loader:after {
593+
border-radius: 50%;
594+
width: 2em;
595+
height: 2em;
596+
animation-fill-mode: both;
597+
animation: loadAnim 1.5s infinite ease-in-out;
598+
}
599+
600+
.loader {
601+
display: block;
602+
margin: 7px auto;
603+
color: #ffffff;
604+
font-size: 5px;
605+
top: -10px;
606+
position: relative;
607+
animation-delay: -0.15s;
608+
pointer-events: none;
609+
}
610+
611+
.loader:before {
612+
content: '';
613+
position: absolute;
614+
left: -3.5em;
615+
animation-delay: -0.30s;
616+
}
617+
618+
.loader:after {
619+
content: '';
620+
position: absolute;
621+
left: 3.5em;
622+
}
623+
624+
@keyframes loadAnim {
625+
0%, 80%, 100% {
626+
box-shadow: 0 2em 0 -1.3em;
627+
}
628+
40% {
629+
box-shadow: 0 2em 0 0;
630+
}
631+
}
571632
</style>
572633

573634
<style> /* We need this unscoped to override the hljs styles. */

src/components/LessonLink.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<template>
22
<router-link :to="to" class="link db pa3 bb b--white green hover-bg-washed-yellow">
33
<div class="flex">
4-
<div class="green ttu f6" style="min-width: 72px">Lesson {{index}}</div>
5-
<div class="pr2">
4+
<div class="tc green ttu f6" style="min-width: 92px">Lesson {{index}}</div>
5+
<div class="pr3">
66
<img v-if="lessonPassed('passed' + to)" src="../static/images/complete.svg" alt="complete" style="height: 1rem;"/>
77
<img v-else-if="lessonCached('cached' + to)" src="../static/images/in-progress.svg" alt="in progress" style="height: 1rem;"/>
88
<img v-else src="../static/images/not-started.svg" alt="not yet started" style="height: 0.9rem;"/>

src/main.js

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,17 @@ import LessonDataStructures02 from './tutorials/Data-Structures/02.vue'
3030
import LessonDataStructures03 from './tutorials/Data-Structures/03.vue'
3131
import LessonDataStructures04 from './tutorials/Data-Structures/04.vue'
3232
import LessonDataStructures05 from './tutorials/Data-Structures/05.vue'
33-
// import MutableFileSystem01 from './tutorials/Mutable-File-System/01.vue'
34-
// import MutableFileSystem02 from './tutorials/Mutable-File-System/02.vue'
35-
// import MutableFileSystem03 from './tutorials/Mutable-File-System/03.vue'
36-
// import MutableFileSystem04 from './tutorials/Mutable-File-System/04.vue'
37-
// import MutableFileSystem05 from './tutorials/Mutable-File-System/05.vue'
33+
import MutableFileSystem01 from './tutorials/Mutable-File-System/01.vue'
34+
import MutableFileSystem02 from './tutorials/Mutable-File-System/02.vue'
35+
import MutableFileSystem03 from './tutorials/Mutable-File-System/03.vue'
36+
import MutableFileSystem04 from './tutorials/Mutable-File-System/04.vue'
37+
import MutableFileSystem05 from './tutorials/Mutable-File-System/05.vue'
38+
import MutableFileSystem06 from './tutorials/Mutable-File-System/06.vue'
39+
import MutableFileSystem07 from './tutorials/Mutable-File-System/07.vue'
40+
import MutableFileSystem08 from './tutorials/Mutable-File-System/08.vue'
41+
import MutableFileSystem09 from './tutorials/Mutable-File-System/09.vue'
42+
import MutableFileSystem10 from './tutorials/Mutable-File-System/10.vue'
43+
import MutableFileSystem11 from './tutorials/Mutable-File-System/11.vue'
3844

3945
Vue
4046
.use(VueRouter)
@@ -70,12 +76,18 @@ const routes = [
7076
{ path: '/blog/06', component: LessonBlog06 },
7177
{ path: '/blog/07', component: LessonBlog07 },
7278
// Lessons - MFS
73-
// { path: '/mutable-file-system', component: Landing, props: { tutorialId: 'mutableFileSystem' } },
74-
// { path: '/mutable-file-system/01', component: MutableFileSystem01 },
75-
// { path: '/mutable-file-system/02', component: MutableFileSystem02 },
76-
// { path: '/mutable-file-system/03', component: MutableFileSystem03 },
77-
// { path: '/mutable-file-system/04', component: MutableFileSystem04 },
78-
// { path: '/mutable-file-system/05', component: MutableFileSystem05 },
79+
{ path: '/mutable-file-system', component: Landing, props: { tutorialId: 'mutableFileSystem' } },
80+
{ path: '/mutable-file-system/01', component: MutableFileSystem01 },
81+
{ path: '/mutable-file-system/02', component: MutableFileSystem02 },
82+
{ path: '/mutable-file-system/03', component: MutableFileSystem03 },
83+
{ path: '/mutable-file-system/04', component: MutableFileSystem04 },
84+
{ path: '/mutable-file-system/05', component: MutableFileSystem05 },
85+
{ path: '/mutable-file-system/06', component: MutableFileSystem06 },
86+
{ path: '/mutable-file-system/07', component: MutableFileSystem07 },
87+
{ path: '/mutable-file-system/08', component: MutableFileSystem08 },
88+
{ path: '/mutable-file-system/09', component: MutableFileSystem09 },
89+
{ path: '/mutable-file-system/10', component: MutableFileSystem10 },
90+
{ path: '/mutable-file-system/11', component: MutableFileSystem11 },
7991
// 404
8092
{ path: '*', name: '404' }
8193
]

src/static/courses.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"all": ["dataStructures", "basics", "blog"],
3-
"featured": ["dataStructures", "basics", "blog"]
2+
"all": ["mutableFileSystem", "dataStructures", "basics", "blog"],
3+
"featured": ["mutableFileSystem", "dataStructures", "basics", "blog"]
44
}

src/static/tutorials.json

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,20 @@
3737
},
3838
"mutableFileSystem": {
3939
"project": "IPFS",
40-
"title": "IPFS as a Mutable File System",
41-
"description": "Store, edit, and share files with with the Mutable File System (MFS).",
40+
"title": "IPFS: Mutable File System",
41+
"description": "The Mutable File System (MFS) lets you work with files and directories as if you were using a traditional name-based file system.",
4242
"lessons": [
43-
{ "to": "/mutable-file-system/01", "name": "Working with your IPFS node" },
44-
{ "to": "/mutable-file-system/02", "name": "Working with files in ProtoSchool" },
45-
{ "to": "/mutable-file-system/03", "name": "Add a new file to MFS" },
46-
{ "to": "/mutable-file-system/04", "name": "View files in your directory" },
47-
{ "to": "/mutable-file-system/05", "name": "Create a directory" }
43+
{ "to": "/mutable-file-system/01", "name": "Introducing IPFS" },
44+
{ "to": "/mutable-file-system/02", "name": "Check the status of a directory" },
45+
{ "to": "/mutable-file-system/03", "name": "Working with files in ProtoSchool" },
46+
{ "to": "/mutable-file-system/04", "name": "Add a file to MFS" },
47+
{ "to": "/mutable-file-system/05", "name": "View the contents of a directory" },
48+
{ "to": "/mutable-file-system/06", "name": "See how CIDs change as data changes" },
49+
{ "to": "/mutable-file-system/07", "name": "Create a directory" },
50+
{ "to": "/mutable-file-system/08", "name": "Move a file or directory" },
51+
{ "to": "/mutable-file-system/09", "name": "Copy a file or directory" },
52+
{ "to": "/mutable-file-system/10", "name": "Read the contents of a file" },
53+
{ "to": "/mutable-file-system/11", "name": "Remove a file or directory" }
4854
]
4955
}
5056
}

src/tutorials/Data-Structures/04.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ Every `CID` is an identifier that contains the `codec` to interpret the data and
4040

4141
`CID`s allow us to build data structures that link to other data structures
4242
in completely different formats. Imagine a tree of JSON objects that link
43-
to BSON objects that also link to git commits. (Or imagine a file folder containing
44-
puppy images <em>and</em> kitty videos, with a subfolder containing articles on
43+
to BSON objects that also link to git commits. (Or imagine a directory containing
44+
puppy images <em>and</em> kitty videos, with a subdirectory containing articles on
4545
giraffes. The possiblities are endless!) All the way down this tree we
4646
have cryptographic hashes that allow us to distribute and link the data.
4747

src/tutorials/Data-Structures/05.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ that we can think of as a link, so a Merkle tree is a collection of linked nodes
3737

3838
As previously discussed, all content addresses are unique to the data they represent. In the graph above, `node E` contains a reference to the hash for `node F` and `node G`. This means that the content address (hash) of `node E` is unique to a node containing those addresses.
3939

40-
Getting lost? Let's imagine this as a set of file folders. If we run folder E through our hashing algorithm
41-
while it contains subfolders F and G, the content-derived hash we get back will include references to those two folders. If we remove folder G, it's like Grace removing that whisker from her kitten photo. Folder E doesn't have the same contents anymore, so it gets a new hash.
40+
Getting lost? Let's imagine this as a set of directories, or file folders. If we run directory E through our hashing algorithm
41+
while it contains subdirectories F and G, the content-derived hash we get back will include references to those two directories. If we remove directory G, it's like Grace removing that whisker from her kitten photo. Directory E doesn't have the same contents anymore, so it gets a new hash.
4242

4343
As the tree above is built, the final content address (hash) of the root node is unique to a
4444
tree that contains every node all the way down this tree. If the data in any node were

0 commit comments

Comments
 (0)