Skip to content

Commit

Permalink
feat: add MFS tutorial (#200)
Browse files Browse the repository at this point in the history
* 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 <alan.shaw@protocol.ai>

* 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
  • Loading branch information
terichadbourne committed Jun 10, 2019
1 parent ff5a776 commit b111973
Show file tree
Hide file tree
Showing 44 changed files with 1,325 additions and 274 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Select the appropriate boilerplate Vue file for your lesson from the `tutorials/
- `boilerplate-file-upload.vue` for a lesson with a coding exercise that requires a file upload
- `boilerplate-no-exercise.vue` for a text-only lesson

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

Example (while in `src/tutorials`):

Expand Down Expand Up @@ -254,7 +254,7 @@ you need to override, as in this example:
```js
} else if (result.error && result.error.message === 'No child name passed to addLink') {
// Forgot the file name and just used a directory as the path
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?' }
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?' }
}
```
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.
Expand Down
4 changes: 2 additions & 2 deletions src/components/File-Lesson.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default {
for (let f of Array.from(event.dataTransfer.items)) {
let isFile = f.getAsEntry ? f.getAsEntry().isFile : (f.webkitGetAsEntry ? f.webkitGetAsEntry().isFile : true)
if (!isFile) {
return alert("Folder upload is not supported. Please select a file or multiple files.")
return alert('Directory upload is not supported. Please select one or more files.')
}
}
this.onFiles(files)
Expand All @@ -35,7 +35,7 @@ export default {
event.preventDefault()
event.stopPropagation()
let elem = document.createElement('input')
elem.setAttribute("type", "file")
elem.setAttribute('type', 'file')
elem.setAttribute('multiple', true)
elem.onchange = () => {
this.onFiles(Array.from(elem.files))
Expand Down
75 changes: 68 additions & 7 deletions src/components/Lesson.vue
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,14 @@
<label for="add-files" class="flex items-center h4 pointer">
<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>
<div class="f5 charcoal">
<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>
<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>
</div>
</label>
</div>
</div>
<div v-else class="mt2">
<span v-on:click="resetFileUpload" class="textLink fr pb1">Start Over</span>
<div class="mb2 pl3 pa2 w-100 br3 h4 shadow-4 bg-white color-navy flex items-center">
<div class="mb2 pl3 pa2 w-100 br3 shadow-4 bg-white color-navy flex items-center">
<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>
<ul class="list pl0">
<li v-for="(file, idx) in uploadedFiles" :key="`file-${idx}`">{{file.name}}</li>
Expand Down Expand Up @@ -151,8 +151,11 @@
<Button v-bind:click="next" class="bg-aqua white" data-cy="next-lesson">Next</Button>
</div>
<div v-else>
<span v-if="isFileLesson && !uploadedFiles" class="disabledButtonWrapper">
<Button v-bind:click="next" class="bg-aqua white" disabled>Submit</Button>
<span v-if="(isFileLesson && !uploadedFiles) || isSubmitting" class="disabledButtonWrapper">
<Button v-bind:click="next" class="bg-aqua white" disabled>
<span v-if="isSubmitting" class="loader"></span>
<span v-else>Submit</span>
</Button>
</span>
<Button v-else v-bind:click="run" class="bg-aqua white" data-cy="submit-answer">Submit</Button>
<div v-if="isFileLesson && !uploadedFiles" class="red lh-copy pt2 o-0">
Expand Down Expand Up @@ -254,6 +257,7 @@ export default {
cachedCode: !!localStorage['cached' + self.$route.path],
code: localStorage[self.cacheKey] || self.$attrs.code || self.defaultCode,
solution: self.$attrs.solution,
isSubmitting: false,
viewSolution: false,
overrideErrors: self.$attrs.overrideErrors,
isFileLesson: self.isFileLesson,
Expand All @@ -265,6 +269,7 @@ export default {
lessonKey: 'passed' + self.$route.path,
lessonPassed: !!localStorage['passed' + self.$route.path],
lessonTitle: self.$attrs.lessonTitle,
createTestFile: self.$attrs.createTestFile,
output: self.output,
expandExercise: false,
dragging: false,
Expand Down Expand Up @@ -337,14 +342,17 @@ export default {
},
methods: {
run: async function (...args) {
this.isSubmitting = true
if (oldIPFS) {
oldIPFS.stop()
oldIPFS = null
}
let output = this.output
let ipfs = await this.createIPFS()
if (this.createTestFile) {
await this.createFile(ipfs)
}
let code = this.editor.getValue()
let modules = {}
if (this.$attrs.modules) modules = this.$attrs.modules
if (this.isFileLesson) args.unshift(this.uploadedFiles)
Expand All @@ -353,6 +361,7 @@ export default {
if (!this.$attrs.overrideErrors && result && result.error) {
Vue.set(output, 'test', result)
this.lessonPassed = !!localStorage[this.lessonKey]
this.isSubmitting = false
return
}
// Hide the solution
Expand All @@ -370,17 +379,27 @@ export default {
localStorage[this.lessonKey] = 'passed'
}
this.lessonPassed = !!localStorage[this.lessonKey]
this.isSubmitting = false
},
createIPFS: function () {
if (this.$attrs.createIPFS) {
return this.$attrs.createIPFS()
} else {
let ipfs = this.IPFSPromise.then(IPFS => {
return new IPFS({repo: Math.random().toString()})
this.ipfsConstructor = IPFS
return new IPFS({ repo: Math.random().toString() })
})
return ipfs
}
},
createFile: function (ipfs) {
new Promise((resolve, reject) => {
ipfs.on('ready', async () => {
await ipfs.add(this.ipfsConstructor.Buffer.from('You did it!'))
resolve()
})
})
},
resetCode: function () {
// TRACK? User chose to reset code
this.code = this.$attrs.code || defaultCode
Expand All @@ -400,7 +419,6 @@ export default {
resetFileUpload: function () {
this.uploadedFiles = false
this.dragging = false
console.log({uploadedFiles: this.uploadedFiles})
},
clearPassed: function () {
delete localStorage[this.lessonKey]
Expand Down Expand Up @@ -568,6 +586,49 @@ div#drop-area * {
border-width: 5px 5px 5px;
margin-top: 5px;
}
.loader,
.loader:before,
.loader:after {
border-radius: 50%;
width: 2em;
height: 2em;
animation-fill-mode: both;
animation: loadAnim 1.5s infinite ease-in-out;
}
.loader {
display: block;
margin: 7px auto;
color: #ffffff;
font-size: 5px;
top: -10px;
position: relative;
animation-delay: -0.15s;
pointer-events: none;
}
.loader:before {
content: '';
position: absolute;
left: -3.5em;
animation-delay: -0.30s;
}
.loader:after {
content: '';
position: absolute;
left: 3.5em;
}
@keyframes loadAnim {
0%, 80%, 100% {
box-shadow: 0 2em 0 -1.3em;
}
40% {
box-shadow: 0 2em 0 0;
}
}
</style>

<style> /* We need this unscoped to override the hljs styles. */
Expand Down
4 changes: 2 additions & 2 deletions src/components/LessonLink.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<template>
<router-link :to="to" class="link db pa3 bb b--white green hover-bg-washed-yellow">
<div class="flex">
<div class="green ttu f6" style="min-width: 72px">Lesson {{index}}</div>
<div class="pr2">
<div class="tc green ttu f6" style="min-width: 92px">Lesson {{index}}</div>
<div class="pr3">
<img v-if="lessonPassed('passed' + to)" src="../static/images/complete.svg" alt="complete" style="height: 1rem;"/>
<img v-else-if="lessonCached('cached' + to)" src="../static/images/in-progress.svg" alt="in progress" style="height: 1rem;"/>
<img v-else src="../static/images/not-started.svg" alt="not yet started" style="height: 0.9rem;"/>
Expand Down
34 changes: 23 additions & 11 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,17 @@ import LessonDataStructures02 from './tutorials/Data-Structures/02.vue'
import LessonDataStructures03 from './tutorials/Data-Structures/03.vue'
import LessonDataStructures04 from './tutorials/Data-Structures/04.vue'
import LessonDataStructures05 from './tutorials/Data-Structures/05.vue'
// import MutableFileSystem01 from './tutorials/Mutable-File-System/01.vue'
// import MutableFileSystem02 from './tutorials/Mutable-File-System/02.vue'
// import MutableFileSystem03 from './tutorials/Mutable-File-System/03.vue'
// import MutableFileSystem04 from './tutorials/Mutable-File-System/04.vue'
// import MutableFileSystem05 from './tutorials/Mutable-File-System/05.vue'
import MutableFileSystem01 from './tutorials/Mutable-File-System/01.vue'
import MutableFileSystem02 from './tutorials/Mutable-File-System/02.vue'
import MutableFileSystem03 from './tutorials/Mutable-File-System/03.vue'
import MutableFileSystem04 from './tutorials/Mutable-File-System/04.vue'
import MutableFileSystem05 from './tutorials/Mutable-File-System/05.vue'
import MutableFileSystem06 from './tutorials/Mutable-File-System/06.vue'
import MutableFileSystem07 from './tutorials/Mutable-File-System/07.vue'
import MutableFileSystem08 from './tutorials/Mutable-File-System/08.vue'
import MutableFileSystem09 from './tutorials/Mutable-File-System/09.vue'
import MutableFileSystem10 from './tutorials/Mutable-File-System/10.vue'
import MutableFileSystem11 from './tutorials/Mutable-File-System/11.vue'

Vue
.use(VueRouter)
Expand Down Expand Up @@ -70,12 +76,18 @@ const routes = [
{ path: '/blog/06', component: LessonBlog06 },
{ path: '/blog/07', component: LessonBlog07 },
// Lessons - MFS
// { path: '/mutable-file-system', component: Landing, props: { tutorialId: 'mutableFileSystem' } },
// { path: '/mutable-file-system/01', component: MutableFileSystem01 },
// { path: '/mutable-file-system/02', component: MutableFileSystem02 },
// { path: '/mutable-file-system/03', component: MutableFileSystem03 },
// { path: '/mutable-file-system/04', component: MutableFileSystem04 },
// { path: '/mutable-file-system/05', component: MutableFileSystem05 },
{ path: '/mutable-file-system', component: Landing, props: { tutorialId: 'mutableFileSystem' } },
{ path: '/mutable-file-system/01', component: MutableFileSystem01 },
{ path: '/mutable-file-system/02', component: MutableFileSystem02 },
{ path: '/mutable-file-system/03', component: MutableFileSystem03 },
{ path: '/mutable-file-system/04', component: MutableFileSystem04 },
{ path: '/mutable-file-system/05', component: MutableFileSystem05 },
{ path: '/mutable-file-system/06', component: MutableFileSystem06 },
{ path: '/mutable-file-system/07', component: MutableFileSystem07 },
{ path: '/mutable-file-system/08', component: MutableFileSystem08 },
{ path: '/mutable-file-system/09', component: MutableFileSystem09 },
{ path: '/mutable-file-system/10', component: MutableFileSystem10 },
{ path: '/mutable-file-system/11', component: MutableFileSystem11 },
// 404
{ path: '*', name: '404' }
]
Expand Down
4 changes: 2 additions & 2 deletions src/static/courses.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"all": ["dataStructures", "basics", "blog"],
"featured": ["dataStructures", "basics", "blog"]
"all": ["mutableFileSystem", "dataStructures", "basics", "blog"],
"featured": ["mutableFileSystem", "dataStructures", "basics", "blog"]
}
20 changes: 13 additions & 7 deletions src/static/tutorials.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,20 @@
},
"mutableFileSystem": {
"project": "IPFS",
"title": "IPFS as a Mutable File System",
"description": "Store, edit, and share files with with the Mutable File System (MFS).",
"title": "IPFS: Mutable File System",
"description": "The Mutable File System (MFS) lets you work with files and directories as if you were using a traditional name-based file system.",
"lessons": [
{ "to": "/mutable-file-system/01", "name": "Working with your IPFS node" },
{ "to": "/mutable-file-system/02", "name": "Working with files in ProtoSchool" },
{ "to": "/mutable-file-system/03", "name": "Add a new file to MFS" },
{ "to": "/mutable-file-system/04", "name": "View files in your directory" },
{ "to": "/mutable-file-system/05", "name": "Create a directory" }
{ "to": "/mutable-file-system/01", "name": "Introducing IPFS" },
{ "to": "/mutable-file-system/02", "name": "Check the status of a directory" },
{ "to": "/mutable-file-system/03", "name": "Working with files in ProtoSchool" },
{ "to": "/mutable-file-system/04", "name": "Add a file to MFS" },
{ "to": "/mutable-file-system/05", "name": "View the contents of a directory" },
{ "to": "/mutable-file-system/06", "name": "See how CIDs change as data changes" },
{ "to": "/mutable-file-system/07", "name": "Create a directory" },
{ "to": "/mutable-file-system/08", "name": "Move a file or directory" },
{ "to": "/mutable-file-system/09", "name": "Copy a file or directory" },
{ "to": "/mutable-file-system/10", "name": "Read the contents of a file" },
{ "to": "/mutable-file-system/11", "name": "Remove a file or directory" }
]
}
}
4 changes: 2 additions & 2 deletions src/tutorials/Data-Structures/04.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ Every `CID` is an identifier that contains the `codec` to interpret the data and

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

Expand Down
4 changes: 2 additions & 2 deletions src/tutorials/Data-Structures/05.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ that we can think of as a link, so a Merkle tree is a collection of linked nodes

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.

Getting lost? Let's imagine this as a set of file folders. If we run folder E through our hashing algorithm
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.
Getting lost? Let's imagine this as a set of directories, or file folders. If we run directory E through our hashing algorithm
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.

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

0 comments on commit b111973

Please sign in to comment.