diff --git a/DOCS.md b/DOCS.md index e3d5aa2..9200bcc 100644 --- a/DOCS.md +++ b/DOCS.md @@ -31,6 +31,13 @@ cheat sheet, and db routes to save their progress when they pass the test
CodeEditor is a two-panel rendering component for student code. It uses AceEditor for the student panel (left), and a remote rendering iframe for the page preview (right).
The CodeEditor is responsible for handling violation of the rules set out for a given +programming challenge. A list of errors is passed in as a prop - this component is +responsible for creating a readable sentence form of what is wrong with the student's code. +For example, "Your HTML code is missing an
Provides a list of Collaborators for a project provided by props.
The Search component is embedded in Nav and is used to search users and projects. It utilizes postgres trigrams serverside (see readme)
+Component for selecting a location from a database of geo codes in Brazil
+Component for choosing a school from a db of school ids and geos - meant to be used in conjunction with SelectGeo
+Popover Component for sharing a link to a students project/codeblock. Copies url to clipboard.
+Component for sharing facebook links. Due to the fact that xvfb screenshots require a few seconds to render, +this component receives a "screenshotReady" prop from the embedding component, which waits a few seconds +to ensure that Facebook's FIRST capture of the page has the finished screenshot
+Sister component to AuthForm, this component wraps the Canon "signup" action and does appropriate error checking
+InputCode is a slide type that requires the student to complete a coding test The CodeEditor component is embedded with a series of rules, and the slide will @@ -101,9 +128,48 @@ For showing code examples with explanations.
TextImage is text left, image right. Images are stored in /slide_images/{id}.jpg Images are uploaded through the CMS and a translated version is chosen here via locales
A thread is the top-level child of a Discussion. Discussions have many threads, threads have many comments.
+UserCard is a component used on a profile page to display other users that share the logged in user's school or location
+About page - contains translated about text and a simple photoslide component from Datawheel's first visit to Minas
+Contest Component handles the (currently postponed) contest, including all the steps and checks to ensure eligibility. +Page is public-facing (doesn't require login) to gain attention - the first step is creating an account
+The Glossary component retrieves words +It is worth noting that the storage of glossary words is somewhat split-brained. Here in Glossary.jsx, canon's "need" functionality +is used to ensure that words are rendered server-side and therefore indexable by search engines. However, glossary words used to live +in the Redux Store (and in QuillWrapper, this is still how they are loaded). Both places use the same API endpoint - but one +uses canon needs, the other puts the data into redux in App.jsx's mount method.
+Homepage component - mostly a wrapper for other smaller components (cards, features, etc)
+Main Level-viewing component (e.g., Jungle Island). It shows a list of the levels available as well as the ending codeblock test. +Codeblocks by other users are listed underneath the island
+Displays all available islands
Leaderboard is a sortable table that lists users by their in-game progress.
+Simple partners page to link to other online coding projects
+A public-facing listing of all slides and quizzes, to be used by teachers as a lesson plan, +students as a reference guide, and to aid in directing users to the site via search engines. +Lesson plan uses Canon's "Needs" to render the page serverside, so that the page is indexable +by SEO bots
+Class component for a user profile. This is a public page and meant to be shared. @@ -125,6 +191,36 @@ This is shown on the public profile for a user and requires sending This is shown on the public profile for a user and requires sending 1 prop: a ref to the user
Projects is one of the largest Pages in codelife - It is responsible for all CRUD +operations of projects, processing screenshots, and listing user codeblocks for inspiration. +Longer term, this should be refactored into smaller components.
+Very small wrapper class for PasswordReset Component
+The Share Page is a top-level page that does not require login, enabling users to share +their projects or codeblocks on facebook or with others. It looks up the content via name/user +and renders a fullscreen codeeditor for display, essentially acting as a hosting page for +the students' work. Show a Report bar on the bottom for logged in users to report inappropriate content
+The slide component is the wrapper for all the various slidetypes in Codelife. However, +it interacts a great deal with the db and greater site, as reaching the last slide +updates user progress, and each slide has a Discussion board beneath it. It's important +to note that currently a Level must be beaten all at once - the "latestSlideCompleted" +variable in state is not persisted anywhere, and leaving the lesson does not restart the +user halfway through a level. Longer term, more granular tracking of user location would +be a nice enhancement.
+Simple splash page that lists about text for the Codelife Project
+Completed/Deprecated Survey Module from a 2017 Survey that followed a beta test in +Minas Gerais. Consists of Radio buttons and a DB post.
+Given a rule and a block of code, check the Javascript and perform an exact match +check on the regex. Used for things like "code must contain getElementById"
+Given a rule and a block of code, check that a given tag is nested inside another +tag. Used for things like "html nests body." Note that this does not currently +account for subsequent occurences (only checks for first occurences)
+Given a rule and a block of code, use a hard-coded regex to check for a SPECIFIC +pattern. Example include a for block "for (;;) {}", ifelse "if () {} else {}" +or a generic invocation of a function "functionName(){}"
+Given a needle (like h1), an attribute (like color), a value (like red), and a JSON +representation of the code as prepared by himalaya (HTML parser), recursively climb +down the nested json tree, testing at each node for the presence of the needle, +and if provided, whether that node has an attribute, and, if provided, whether that +attribute's value exactly matches the provided value.
+Given a rule and a block of code, search for a self closing tag such as +Optionally run attrCount to check for extra rules (such as requiring "src")
+Given a rule and a block of code, ensure that the given needle (such as ) +occurs once and only once in the code (useful for tags like body, head, html)
+Given a rule and a block of code, check if a given tag (such as
) is included in the +code. Optionally, use attrCount to match any provided attributes or values in the rule.
+Given a rule and a block of code, using the "css" module to turn the css into a crawlable +object. Fold over that generated parsed object and drill down to check if the rule's property +matches the property and value of the css entered by the student.
+Search
](#Search)
+
+* * *
+
+
+
+### search.componentDidUpdate()
+Search is not a top-level component in routes.jsx, so it doesn't have access to the Router object that would indicate
+what page the user is on. This makes it difficult to collapse the search results if the user clicks a link outside the
+results. To get around this, a linkObj is passed down from App to Nav to here, and search is collapsed on URL change.
+
+**Kind**: instance method of [Search
](#Search)
+
+* * *
+
+
+
+### search.onKeyDown()
+onKeyDown is meant to capture inputs - but the arrow up/down aren't currently functional (user can use tabs and
+Enter key to go through links however). This UX could be improved.
+
+**Kind**: instance method of [Search
](#Search)
+
+* * *
+
+
+
+### search.search()
+Send search query to API endpoint
+
+**Kind**: instance method of [Search
](#Search)
+
+* * *
+
+
+
+## SelectGeo
+Component for selecting a location from a database of geo codes in Brazil
+
+**Kind**: global class
+
+* [SelectGeo](#SelectGeo)
+ * [.componentDidMount()](#SelectGeo+componentDidMount)
+ * [.changeState()](#SelectGeo+changeState)
+
+
+* * *
+
+
+
+### selectGeo.componentDidMount()
+On Mount, get the gid (embedded in props via userprofile) and populate the search bar accordingly
+
+**Kind**: instance method of [SelectGeo
](#SelectGeo)
+
+* * *
+
+
+
+### selectGeo.changeState()
+Callback for the select box. Contains some code from what was going to be an "Unspecified" option, but was
+changed to a higher up "Rather not say" option in EditProfile.jsx which cancels out the dialog entirely
+and writes a hard-coded "-1" to the user's geo id.
+
+**Kind**: instance method of [SelectGeo
](#SelectGeo)
+
+* * *
+
+
+
+## SelectSchool
+Component for choosing a school from a db of school ids and geos - meant to be used in conjunction with SelectGeo
+
+**Kind**: global class
+
+* [SelectSchool](#SelectSchool)
+ * [.componentDidMount()](#SelectSchool+componentDidMount)
+ * [.updateSchoolList()](#SelectSchool+updateSchoolList)
+
+
+* * *
+
+
+
+### selectSchool.componentDidMount()
+On Mount, get the sid from props (originally retrieved from userprofile) and populate the dropdown accordingly
+
+**Kind**: instance method of [SelectSchool
](#SelectSchool)
+
+* * *
+
+
+
+### selectSchool.updateSchoolList()
+Callback for dual dropdown - Given a selected Geo, hit the API to retrieve all the schools for that Geo and populate the dropdown.
+
+**Kind**: instance method of [SelectSchool
](#SelectSchool)
+
+* * *
+
+
+
+## ShareDirectLink
+Popover Component for sharing a link to a students project/codeblock. Copies url to clipboard.
+
+**Kind**: global class
+
+* * *
+
+
+
+## ShareFacebookLink
+Component for sharing facebook links. Due to the fact that xvfb screenshots require a few seconds to render,
+this component receives a "screenshotReady" prop from the embedding component, which waits a few seconds
+to ensure that Facebook's FIRST capture of the page has the finished screenshot
+
+**Kind**: global class
+
+* * *
+
+
+
+## SignupForm
+Sister component to AuthForm, this component wraps the Canon "signup" action and does appropriate error checking
+
+**Kind**: global class
+
+* * *
+
+
+
+### signupForm.onSubmit()
+When the user clicks submit, verify some info before calling datawheel-canon's `signup` action
+
+**Kind**: instance method of [SignupForm
](#SignupForm)
+
+* * *
+
## InputCode
@@ -1603,223 +1909,1024 @@ Images are uploaded through the CMS and a translated version is chosen here via
* * *
-
+
-## Island
-Displays all available islands
+## Thread
+A thread is the top-level child of a Discussion. Discussions have many threads, threads have many comments.
**Kind**: global class
-* [Island](#Island)
- * [.componentDidMount()](#Island+componentDidMount)
- * [.hasUserCompleted(milestone)](#Island+hasUserCompleted) ⇒ Boolean
+* [Thread](#Thread)
+ * [.componentDidMount()](#Thread+componentDidMount)
+ * [.newComment()](#Thread+newComment)
+ * [.handleReport()](#Thread+handleReport)
* * *
-
+
-### island.componentDidMount()
-On mount, fetch the progress for the currently logged in user.
+### thread.componentDidMount()
+On Mount, retrieve the thread from props
-**Kind**: instance method of [Island
](#Island)
+**Kind**: instance method of [Thread
](#Thread)
* * *
-
+
-### island.hasUserCompleted(milestone) ⇒ Boolean
-On mount, fetch the progress for the currently logged in user.
+### thread.newComment()
+A thread can only have one text window open at a time for a new comment to be added. Keep the details of
+this comment in state, and post it to the endpoint on submit
-**Kind**: instance method of [Island
](#Island)
-**Returns**: Boolean
- Returns a boolean whether or not the user has completed the provided island ID.
+**Kind**: instance method of [Thread
](#Thread)
-| Param | Type | Description |
-| --- | --- | --- |
-| milestone | String
| An island ID. |
+* * *
+
+
+### thread.handleReport()
+A report, handled by the sub-component ReportBox, uses this callback to tell Thread that the user has submitted a report
+
+**Kind**: instance method of [Thread
](#Thread)
* * *
-
+
-## Profile
-Class component for a user profile.
-This is a public page and meant to be shared.
-If a user is logged in AND this is their profile, show an
-edit button allowing them to edit it.
+## UserCard
+UserCard is a component used on a profile page to display other users that share the logged in user's school or location
**Kind**: global class
-* [Profile](#Profile)
- * [new Profile(loading, error, profileUser)](#new_Profile_new)
- * [.componentDidMount()](#Profile+componentDidMount)
- * [.render()](#Profile+render)
+* * *
+
+
+## About
+About page - contains translated about text and a simple photoslide component from Datawheel's first visit to Minas
+
+**Kind**: global class
* * *
-
+
-### new Profile(loading, error, profileUser)
-Creates the Profile component with its initial state.
+## Contest
+Contest Component handles the (currently postponed) contest, including all the steps and checks to ensure eligibility.
+Page is public-facing (doesn't require login) to gain attention - the first step is creating an account
+**Kind**: global class
-| Param | Type | Description |
-| --- | --- | --- |
-| loading | boolean
| true by defaults gets flipped post AJAX. |
-| error | string
| Gets set if no username matches username URL param. |
-| profileUser | object
| Gets set to full user object from DB. |
+* [Contest](#Contest)
+ * [.componentDidMount()](#Contest+componentDidMount)
+ * [.determineStep()](#Contest+determineStep)
* * *
-
+
-### profile.componentDidMount()
-Grabs username from URL param, makes AJAX call to server and sets error
-state (if no user is found) or profileUser (if one is).
+### contest.componentDidMount()
+On mount, load the user, their progress, and their projects. Use this data to populate state and fill in the
+appropriate steps on the page
-**Kind**: instance method of [Profile
](#Profile)
+**Kind**: instance method of [Contest
](#Contest)
* * *
-
+
-### profile.render()
-3 render states:
-case (loading)
- - show loading
-case (error)
- - show error msg from server
-case (user found)
- - user info
+### contest.determineStep()
+Signing up for the contest is a multi-step progress - use the state to determine where the user is
+so the boxes on the page can be checked accordingly.
-**Kind**: instance method of [Profile
](#Profile)
+**Kind**: instance method of [Contest
](#Contest)
* * *
-
+
-## UserCodeBlocks
-Class component for displaying lists of user's snippets.
-This is shown on the public profile for a user and requires sending
-1 prop: a ref to the user
+## Glossary
+The Glossary component retrieves words
+It is worth noting that the storage of glossary words is somewhat split-brained. Here in Glossary.jsx, canon's "need" functionality
+is used to ensure that words are rendered server-side and therefore indexable by search engines. However, glossary words used to live
+in the Redux Store (and in QuillWrapper, this is still how they are loaded). Both places use the same API endpoint - but one
+uses canon needs, the other puts the data into redux in App.jsx's mount method.
**Kind**: global class
-* [UserCodeBlocks](#UserCodeBlocks)
- * [new UserCodeBlocks(loading, snippets)](#new_UserCodeBlocks_new)
- * [.componentDidMount()](#UserCodeBlocks+componentDidMount)
-
-
* * *
-
-
-### new UserCodeBlocks(loading, snippets)
-Creates the UserSnippets component with initial state.
-
+
-| Param | Type | Description |
-| --- | --- | --- |
-| loading | boolean
| true by defaults gets flipped post AJAX. |
-| snippets | array
| Gets set by AJAX call from DB call. |
+## Home
+Homepage component - mostly a wrapper for other smaller components (cards, features, etc)
+**Kind**: global class
* * *
-
+
-### userCodeBlocks.componentDidMount()
-Grabs user id from user prop, makes AJAX call to server and returns
-the list of snippets.
+### home.componentDidMount()
+On mount, fetch the users progress so that a "continue your adventure" placard can be shown.
+Whether the user is logged or not, fetch the featured cb/projects
-**Kind**: instance method of [UserCodeBlocks
](#UserCodeBlocks)
+**Kind**: instance method of [Home
](#Home)
* * *
-
+
-## UserProjects
-Class component for displaying lists of user's projects.
-This is shown on the public profile for a user and requires sending
-1 prop: a ref to the user
+## Level
+Main Level-viewing component (e.g., Jungle Island). It shows a list of the levels available as well as the ending codeblock test.
+Codeblocks by other users are listed underneath the island
**Kind**: global class
-* [UserProjects](#UserProjects)
- * [new UserProjects(loading, projects)](#new_UserProjects_new)
- * [.componentDidMount()](#UserProjects+componentDidMount)
+* [Level](#Level)
+ * [.loadFromDB()](#Level+loadFromDB)
+ * [.maybeTriggerCodeblock()](#Level+maybeTriggerCodeblock)
+ * [.componentDidUpdate()](#Level+componentDidUpdate)
+ * [.componentDidMount()](#Level+componentDidMount)
+ * [.componentWillUnmount()](#Level+componentWillUnmount)
+ * [.toggleTest()](#Level+toggleTest)
+ * [.handleSave()](#Level+handleSave)
+ * [.onFirstCompletion()](#Level+onFirstCompletion)
+ * [.closeOverlay()](#Level+closeOverlay)
+ * [.hasUserCompleted()](#Level+hasUserCompleted)
+ * [.reportLike()](#Level+reportLike)
+ * [.allLevelsBeaten()](#Level+allLevelsBeaten)
+ * [.promptFinalTest()](#Level+promptFinalTest)
+ * [.saveCheckpoint()](#Level+saveCheckpoint)
+ * [.skipCheckpoint()](#Level+skipCheckpoint)
+ * [.buildCheckpointPopover()](#Level+buildCheckpointPopover)
+ * [.buildWinPopover()](#Level+buildWinPopover)
+ * [.buildTestPopover()](#Level+buildTestPopover)
* * *
-
+
-### new UserProjects(loading, projects)
-Creates the UserProjects component with initial state.
+### level.loadFromDB()
+On Mount, or Update (meaning the user switched islands) Load the necessary progress/codeblock data from the db.
+**Kind**: instance method of [Level
](#Level)
-| Param | Type | Description |
-| --- | --- | --- |
-| loading | boolean
| true by defaults gets flipped post AJAX. |
-| projects | array
| Gets set by AJAX call from DB call. |
+* * *
+
+
+### level.maybeTriggerCodeblock()
+The presence of `/show` in the URL is a permalink to open the codeblock. Was originally intended so that codeblockcards could directly link
+to a user's own codeblock and automatically open it, but this feature was postponed.
+
+**Kind**: instance method of [Level
](#Level)
* * *
-
+
-### userProjects.componentDidMount()
-Grabs user id from user prop, makes AJAX call to server and returns
-the list of projects.
+### level.componentDidUpdate()
+When the user changes pages, flush the state and reload from the database
-**Kind**: instance method of [UserProjects
](#UserProjects)
+**Kind**: instance method of [Level
](#Level)
* * *
-
+
-## UsersList
-Class component for displaying lists of user's snippets.
-This is shown on the public profile for a user and requires sending
-1 prop: a ref to the user
+### level.componentDidMount()
+The code to load from DB already exists in ComponentDidUpdate, this dedupes that logic by just manually calling update on mount.
-**Kind**: global class
+**Kind**: instance method of [Level
](#Level)
-* [UsersList](#UsersList)
- * [new UsersList(loading, snippets)](#new_UsersList_new)
- * [.componentDidMount()](#UsersList+componentDidMount)
+* * *
+
-* * *
+### level.componentWillUnmount()
+A timeout is registered on Codeblock completion to process the screenshot, ensuring that it is complete before allowing fb sharing.
+Clear this timeout if the user leaves the page
-
+**Kind**: instance method of [Level
](#Level)
-### new UsersList(loading, snippets)
-Creates the UserSnippets component with initial state.
+* * *
+
-| Param | Type | Description |
-| --- | --- | --- |
-| loading | boolean
| true by defaults gets flipped post AJAX. |
-| snippets | array
| Gets set by AJAX call from DB call. |
+### level.toggleTest()
+Hide or Show the codeblock test popover. Adjust the URL accordingly
+**Kind**: instance method of [Level
](#Level)
* * *
-
+
-### usersList.componentDidMount()
-Grabs user id from user prop, makes AJAX call to server and returns
-the list of snippets.
+### level.handleSave()
+Callback for CodeBlockEditor on save. The CodeBlockEditor passes its codeblock back out to Level so that its
+Codeblock can be set.
-**Kind**: instance method of [UsersList
](#UsersList)
+**Kind**: instance method of [Level
](#Level)
* * *
-
+
+
+### level.onFirstCompletion()
+Called when the user finishes an island for the first time. Calls a refresh on the data
+to unlock codeblocks, shows the victory message, and invites the user to the next island.
+
+**Kind**: instance method of [Level
](#Level)
+
+* * *
+
+
+
+### level.closeOverlay()
+Upon Closing the winning pop-up, send the player to the next island.
+
+**Kind**: instance method of [Level
](#Level)
+
+* * *
+
+
+
+### level.hasUserCompleted()
+Levels and Islands are mixed together in a single array - so this can be used to test if
+a user has beaten a level (e.g. hello-world) or an entire island (e.g. island-1).
+
+**Kind**: instance method of [Level
](#Level)
+
+* * *
+
+
+
+### level.reportLike()
+The codeblocks underneath the island need to be informed via a callback when they are
+liked or unliked, as this affects the sorting.
+
+**Kind**: instance method of [Level
](#Level)
+
+* * *
+
+
+
+### level.allLevelsBeaten()
+Used to determine if the final test should be shown.
+
+**Kind**: instance method of [Level
](#Level)
+
+* * *
+
+
+
+### level.promptFinalTest()
+If a user has beaten all the levels on this island, but has NOT created a codeblock yet,
+they are in the state were the codeblock need be prompted
+
+**Kind**: instance method of [Level
](#Level)
+
+* * *
+
+
+
+### level.saveCheckpoint()
+Checkpoint is a pop-up that appears after level 1, asking the user to share their
+school. This is the api callback to update their profile
+
+**Kind**: instance method of [Level
](#Level)
+
+* * *
+
+
+
+### level.skipCheckpoint()
+If the user elects not to provide their school, write a hard-coded -1 to their sid.
+This saves the "prefer not to answer" choice and prevents future popups.
+
+**Kind**: instance method of [Level
](#Level)
+
+* * *
+
+
+
+### level.buildCheckpointPopover()
+This was written early in the project, before the Component nesting of React was
+fully put to use. This method encapsulates the checkpoint popover - but this should
+obviously be moved to a component, not a method.
+
+**Kind**: instance method of [Level
](#Level)
+
+* * *
+
+
+
+### level.buildWinPopover()
+This was written early in the project, before the Component nesting of React was
+fully put to use. This method encapsulates the "You Win" popover - but this should
+obviously be moved to a component, not a method.
+
+**Kind**: instance method of [Level
](#Level)
+
+* * *
+
+
+
+### level.buildTestPopover()
+This was written early in the project, before the Component nesting of React was
+fully put to use. This method encapsulates the test popover - but this should
+obviously be moved to a component, not a method.
+
+**Kind**: instance method of [Level
](#Level)
+
+* * *
+
+
+
+## Island
+Displays all available islands
+
+**Kind**: global class
+
+* [Island](#Island)
+ * [.componentDidMount()](#Island+componentDidMount)
+ * [.hasUserCompleted(milestone)](#Island+hasUserCompleted) ⇒ Boolean
+
+
+* * *
+
+
+
+### island.componentDidMount()
+On mount, fetch the progress for the currently logged in user.
+
+**Kind**: instance method of [Island
](#Island)
+
+* * *
+
+
+
+### island.hasUserCompleted(milestone) ⇒ Boolean
+On mount, fetch the progress for the currently logged in user.
+
+**Kind**: instance method of [Island
](#Island)
+**Returns**: Boolean
- Returns a boolean whether or not the user has completed the provided island ID.
+
+| Param | Type | Description |
+| --- | --- | --- |
+| milestone | String
| An island ID. |
+
+
+* * *
+
+
+
+## Leaderboard
+Leaderboard is a sortable table that lists users by their in-game progress.
+
+**Kind**: global class
+
+* [Leaderboard](#Leaderboard)
+ * [.componentDidMount()](#Leaderboard+componentDidMount)
+ * [.handleHeaderClick()](#Leaderboard+handleHeaderClick)
+
+
+* * *
+
+
+
+### leaderboard.componentDidMount()
+On Mount, retrieve stats from the API, calculate what percentage of completion this refers
+to, given the state of currently released islands, and update state.
+
+**Kind**: instance method of [Leaderboard
](#Leaderboard)
+
+* * *
+
+
+
+### leaderboard.handleHeaderClick()
+On selection of a sorting property, sort the users accordingly
+
+**Kind**: instance method of [Leaderboard
](#Leaderboard)
+
+* * *
+
+
+
+## LearnMore
+Simple partners page to link to other online coding projects
+
+**Kind**: global class
+
+* * *
+
+
+
+## LessonPlan
+A public-facing listing of all slides and quizzes, to be used by teachers as a lesson plan,
+students as a reference guide, and to aid in directing users to the site via search engines.
+Lesson plan uses Canon's "Needs" to render the page serverside, so that the page is indexable
+by SEO bots
+
+**Kind**: global class
+
+* * *
+
+
+
+## Profile
+Class component for a user profile.
+This is a public page and meant to be shared.
+If a user is logged in AND this is their profile, show an
+edit button allowing them to edit it.
+
+**Kind**: global class
+
+* [Profile](#Profile)
+ * [new Profile(loading, error, profileUser)](#new_Profile_new)
+ * [.componentDidMount()](#Profile+componentDidMount)
+ * [.render()](#Profile+render)
+
+
+* * *
+
+
+
+### new Profile(loading, error, profileUser)
+Creates the Profile component with its initial state.
+
+
+| Param | Type | Description |
+| --- | --- | --- |
+| loading | boolean
| true by defaults gets flipped post AJAX. |
+| error | string
| Gets set if no username matches username URL param. |
+| profileUser | object
| Gets set to full user object from DB. |
+
+
+* * *
+
+
+
+### profile.componentDidMount()
+Grabs username from URL param, makes AJAX call to server and sets error
+state (if no user is found) or profileUser (if one is).
+
+**Kind**: instance method of [Profile
](#Profile)
+
+* * *
+
+
+
+### profile.render()
+3 render states:
+case (loading)
+ - show loading
+case (error)
+ - show error msg from server
+case (user found)
+ - user info
+
+**Kind**: instance method of [Profile
](#Profile)
+
+* * *
+
+
+
+## UserCodeBlocks
+Class component for displaying lists of user's snippets.
+This is shown on the public profile for a user and requires sending
+1 prop: a ref to the user
+
+**Kind**: global class
+
+* [UserCodeBlocks](#UserCodeBlocks)
+ * [new UserCodeBlocks(loading, snippets)](#new_UserCodeBlocks_new)
+ * [.componentDidMount()](#UserCodeBlocks+componentDidMount)
+
+
+* * *
+
+
+
+### new UserCodeBlocks(loading, snippets)
+Creates the UserSnippets component with initial state.
+
+
+| Param | Type | Description |
+| --- | --- | --- |
+| loading | boolean
| true by defaults gets flipped post AJAX. |
+| snippets | array
| Gets set by AJAX call from DB call. |
+
+
+* * *
+
+
+
+### userCodeBlocks.componentDidMount()
+Grabs user id from user prop, makes AJAX call to server and returns
+the list of snippets.
+
+**Kind**: instance method of [UserCodeBlocks
](#UserCodeBlocks)
+
+* * *
+
+
+
+## UserProjects
+Class component for displaying lists of user's projects.
+This is shown on the public profile for a user and requires sending
+1 prop: a ref to the user
+
+**Kind**: global class
+
+* [UserProjects](#UserProjects)
+ * [new UserProjects(loading, projects)](#new_UserProjects_new)
+ * [.componentDidMount()](#UserProjects+componentDidMount)
+
+
+* * *
+
+
+
+### new UserProjects(loading, projects)
+Creates the UserProjects component with initial state.
+
+
+| Param | Type | Description |
+| --- | --- | --- |
+| loading | boolean
| true by defaults gets flipped post AJAX. |
+| projects | array
| Gets set by AJAX call from DB call. |
+
+
+* * *
+
+
+
+### userProjects.componentDidMount()
+Grabs user id from user prop, makes AJAX call to server and returns
+the list of projects.
+
+**Kind**: instance method of [UserProjects
](#UserProjects)
+
+* * *
+
+
+
+## UsersList
+Class component for displaying lists of user's snippets.
+This is shown on the public profile for a user and requires sending
+1 prop: a ref to the user
+
+**Kind**: global class
+
+* [UsersList](#UsersList)
+ * [new UsersList(loading, snippets)](#new_UsersList_new)
+ * [.componentDidMount()](#UsersList+componentDidMount)
+
+
+* * *
+
+
+
+### new UsersList(loading, snippets)
+Creates the UserSnippets component with initial state.
+
+
+| Param | Type | Description |
+| --- | --- | --- |
+| loading | boolean
| true by defaults gets flipped post AJAX. |
+| snippets | array
| Gets set by AJAX call from DB call. |
+
+
+* * *
+
+
+
+### usersList.componentDidMount()
+Grabs user id from user prop, makes AJAX call to server and returns
+the list of snippets.
+
+**Kind**: instance method of [UsersList
](#UsersList)
+
+* * *
+
+
+
+## Projects
+Projects is one of the largest Pages in codelife - It is responsible for all CRUD
+operations of projects, processing screenshots, and listing user codeblocks for inspiration.
+Longer term, this should be refactored into smaller components.
+
+**Kind**: global class
+
+* [Projects](#Projects)
+ * [.componentDidMount()](#Projects+componentDidMount)
+ * [.openProject()](#Projects+openProject)
+ * [.setExecState()](#Projects+setExecState)
+ * [.createNewProject()](#Projects+createNewProject)
+ * [.clickNewProject()](#Projects+clickNewProject)
+ * [.shareProject()](#Projects+shareProject)
+ * [.showLeaveAlert()](#Projects+showLeaveAlert)
+ * [.leaveCollab()](#Projects+leaveCollab)
+ * [.handleCheckbox()](#Projects+handleCheckbox)
+ * [.deleteProject()](#Projects+deleteProject)
+ * [.onClickProject()](#Projects+onClickProject)
+ * [.saveCodeToDB()](#Projects+saveCodeToDB)
+ * [.closeFirstTimeShare()](#Projects+closeFirstTimeShare)
+ * [.executeCode()](#Projects+executeCode)
+ * [.handleFork()](#Projects+handleFork)
+ * [.changeProjectName()](#Projects+changeProjectName)
+ * [.handleKey()](#Projects+handleKey)
+
+
+* * *
+
+
+
+### projects.componentDidMount()
+On Mount, retrieve all projects by the logged in user, as well as the projects by OTHER users
+with whom the logged in user is listed as a collaborator, and put these in state.
+
+**Kind**: instance method of [Projects
](#Projects)
+
+* * *
+
+
+
+### projects.openProject()
+Given a project id, open the project itself by fetching it from the database and loading it
+into state. Set the URL so it continues to match the open project permalink
+
+**Kind**: instance method of [Projects
](#Projects)
+
+* * *
+
+
+
+### projects.setExecState()
+The embedded CodeEditor is the only component that knows if the user has used javascript
+in their project. When this changes in CodeEditor, it bubbles that up via this callback
+so that Projects can dynamically show and hide an "Execute Code" button
+
+**Kind**: instance method of [Projects
](#Projects)
+
+* * *
+
+
+
+### projects.createNewProject()
+Callback for the create new project button. Trims the name of URL-breakers and whitespace,
+posts an empty project to the API endpoint, and refreshes the project list from that API
+payload so the Project List accurately reflects the new project collection. Update the URL
+when the project is finished opening.
+
+**Kind**: instance method of [Projects
](#Projects)
+
+* * *
+
+
+
+### projects.clickNewProject()
+Opens the popover to name the project (which eventually calles createNewProject)
+
+**Kind**: instance method of [Projects
](#Projects)
+
+* * *
+
+
+
+### projects.shareProject()
+Deprecated / unused function
+
+**Kind**: instance method of [Projects
](#Projects)
+
+* * *
+
+
+
+### projects.showLeaveAlert()
+The alerts in this component have two states, false, or "truthy," that is, leaveAlert=false
+means that the window closed, and setting leaveAlert to *what you want the alert to say*
+makes it truthy, and therefore open. This click callback is the "are you sure" dialogue
+for leaving a collaboration
+
+**Kind**: instance method of [Projects
](#Projects)
+
+* * *
+
+
+
+### projects.leaveCollab()
+Upon confirming that this user wants to leave a collab, remove that user from
+the collabs tabel. Additionally, filter it out in state. Either way, close the
+leaveAlert
+
+**Kind**: instance method of [Projects
](#Projects)
+
+* * *
+
+
+
+### projects.handleCheckbox()
+Though users are normally invited to share new projects on facebook, they may elect to
+opt out and "never show this again" which needs to write to their userprofile
+
+**Kind**: instance method of [Projects
](#Projects)
+
+* * *
+
+
+
+### projects.deleteProject()
+Delete a given project. The argument here is confusing - originally clicking delete would
+delete the project immediately. The addition of a deleteAlert (similar to leaveAlert) stores
+the project to be deleted in the deleteAlert.
+
+**Kind**: instance method of [Projects
](#Projects)
+
+* * *
+
+
+
+### projects.onClickProject()
+When a user clicks a project, attempt to open it. Reach into the CodeEditor and check
+if changes have been made, and if so, block the opening attempt until they save.
+
+**Kind**: instance method of [Projects
](#Projects)
+
+* * *
+
+
+
+### projects.saveCodeToDB()
+Prepare a payload containing the filename, id, content, etc to be sent to the update api.
+If this is the first time the user is saving a project, offer to share it on Facebook.
+Note that this is one of the many places in Codelife where a 5 second timer is used to allow
+the screenshot time to process
+
+**Kind**: instance method of [Projects
](#Projects)
+
+* * *
+
+
+
+### projects.closeFirstTimeShare()
+Callback for closing the share window, save the users preference if they asked
+not to be asked again.
+
+**Kind**: instance method of [Projects
](#Projects)
+
+* * *
+
+
+
+### projects.executeCode()
+Reach into the CodeEditor and call the executeCode function. This requires manual
+execution - otherwise if the user was writing something like "alert()" then it would
+render every single keystroke
+
+**Kind**: instance method of [Projects
](#Projects)
+
+* * *
+
+
+
+### projects.handleFork()
+On most parts of the site, forking a codeblock is as easy as creating the project
+and navigating the user to that page. However, if a user forks a codeblock from HERE
+on the project page, a slightly different behavior is required
+
+**Kind**: instance method of [Projects
](#Projects)
+
+* * *
+
+
+
+### projects.changeProjectName()
+Rename a project. Set title editability to false temporarily and prune URL-breakers
+and leading/trailing whitespace from the new name. Write the project to the db and update state
+
+**Kind**: instance method of [Projects
](#Projects)
+
+* * *
+
+
+
+### projects.handleKey()
+Keyboard callbacks
+
+**Kind**: instance method of [Projects
](#Projects)
+
+* * *
+
+
+
+## ResetPw
+Very small wrapper class for PasswordReset Component
+
+**Kind**: global class
+
+* * *
+
+
+
+## Share
+The Share Page is a top-level page that does not require login, enabling users to share
+their projects or codeblocks on facebook or with others. It looks up the content via name/user
+and renders a fullscreen codeeditor for display, essentially acting as a hosting page for
+the students' work. Show a Report bar on the bottom for logged in users to report inappropriate content
+
+**Kind**: global class
+
+* [Share](#Share)
+ * [.componentDidMount()](#Share+componentDidMount)
+ * [.handleReport()](#Share+handleReport)
+
+
+* * *
+
+
+
+### share.componentDidMount()
+In order to color the ReportBox Button appropriate, it needs to be known if the logged
+in user has reported this content. Fetch the reports to check.
+
+**Kind**: instance method of [Share
](#Share)
+
+* * *
+
+
+
+### share.handleReport()
+The ReportBox component needs a callback to tell this outer component that a report
+has been processed.
+
+**Kind**: instance method of [Share
](#Share)
+
+* * *
+
+
+
+## Slide
+The slide component is the wrapper for all the various slidetypes in Codelife. However,
+it interacts a great deal with the db and greater site, as reaching the last slide
+updates user progress, and each slide has a Discussion board beneath it. It's important
+to note that currently a Level must be beaten all at once - the "latestSlideCompleted"
+variable in state is not persisted anywhere, and leaving the lesson does not restart the
+user halfway through a level. Longer term, more granular tracking of user location would
+be a nice enhancement.
+
+**Kind**: global class
+
+* [Slide](#Slide)
+ * [.unblock()](#Slide+unblock)
+ * [.saveProgress()](#Slide+saveProgress)
+ * [.componentDidUpdate()](#Slide+componentDidUpdate)
+ * [.componentDidMount()](#Slide+componentDidMount)
+ * [.hitDB()](#Slide+hitDB)
+ * [.editSlide()](#Slide+editSlide)
+ * [.advanceLevel()](#Slide+advanceLevel)
+ * [.toggleSkip()](#Slide+toggleSkip)
+ * [.toggleDiscussion()](#Slide+toggleDiscussion)
+ * [.onNewThread()](#Slide+onNewThread)
+
+
+* * *
+
+
+
+### slide.unblock()
+InputCode and Quiz slides are "blockers" in that they do not allow progress until a correct
+answer is provided. This function is called when the user beats a slide.
+
+**Kind**: instance method of [Slide
](#Slide)
+
+* * *
+
+
+
+### slide.saveProgress()
+When the user reaches the final slide, write the level to the userprogress table.
+If the user looks at the discussion board, this level is marked as "skipped", which
+ultimately does not count towards overall completion%. Completing the level without
+help marks the level as completed.
+
+**Kind**: instance method of [Slide
](#Slide)
+
+* * *
+
+
+
+### slide.componentDidUpdate()
+Slide.jsx handles all the transitions from slide to slide, so a lot of work need be done
+when the user changes slides. The simplest case is beating a single slide, but they also
+may have beaten this lesson (db write), reached a blocking slide, or be changing levels entirely
+
+**Kind**: instance method of [Slide
](#Slide)
+
+* * *
+
+
+
+### slide.componentDidMount()
+On mount, hit the DB and add the keyboard listener
+
+**Kind**: instance method of [Slide
](#Slide)
+
+* * *
+
+
+
+### slide.hitDB()
+Given the island / level / slide (lid, mlid, sid) from the URL params
+Fetch the slides and userprogress from the db and start from the first slides
+
+**Kind**: instance method of [Slide
](#Slide)
+
+* * *
+
+
+
+### slide.editSlide()
+Admin-only direct link to the CMS to edit a slide's content
+
+**Kind**: instance method of [Slide
](#Slide)
+
+* * *
+
+
+
+### slide.advanceLevel()
+When the user goes to the next level, push the new URL and hard-reload. This should
+be refactored to a more React-y state reset.
+
+**Kind**: instance method of [Slide
](#Slide)
+
+* * *
+
+
+
+### slide.toggleSkip()
+Show or hide the "show discussion" confirm/deny menu
+
+**Kind**: instance method of [Slide
](#Slide)
+
+* * *
+
+
+
+### slide.toggleDiscussion()
+Reveal or hide the discussion component.
+
+**Kind**: instance method of [Slide
](#Slide)
+
+* * *
+
+
+
+### slide.onNewThread()
+When a Discussion board posts a new thread, Slide needs to know this has happened.
+This callback pushes the new thread onto the list so the "thread count" updates
+
+**Kind**: instance method of [Slide
](#Slide)
+
+* * *
+
+
+
+## Splash
+Simple splash page that lists about text for the Codelife Project
+
+**Kind**: global class
+
+* * *
+
+
+
+## Survey
+Completed/Deprecated Survey Module from a 2017 Survey that followed a beta test in
+Minas Gerais. Consists of Radio buttons and a DB post.
+
+**Kind**: global class
+
+* * *
+
+
+
+### survey.componentWillMount()
+Grabs username from URL param, makes AJAX call to server and sets error
+state (if no user is found) or overrides state (if one is).
+
+**Kind**: instance method of [Survey
](#Survey)
+
+* * *
+
+
## threadInclude
threadsRoute is used for retrieving threads and their associated comments.
@@ -1836,6 +2943,92 @@ however the intention was that discussions could expand to encompass projects/co
* * *
+
+
+## cvMatch
+Given a rule and a block of code, check the Javascript and perform an exact match
+check on the regex. Used for things like "code must contain getElementById"
+
+**Kind**: global constant
+
+* * *
+
+
+
+## cvNests
+Given a rule and a block of code, check that a given tag is nested inside another
+tag. Used for things like "html nests body." Note that this does not currently
+account for subsequent occurences (only checks for first occurences)
+
+**Kind**: global constant
+
+* * *
+
+
+
+## cvUses
+Given a rule and a block of code, use a hard-coded regex to check for a SPECIFIC
+pattern. Example include a for block "for (;;) {}", ifelse "if () {} else {}"
+or a generic invocation of a function "functionName(){}"
+
+**Kind**: global constant
+
+* * *
+
+
+
+## attrCount
+Given a needle (like h1), an attribute (like color), a value (like red), and a JSON
+representation of the code as prepared by himalaya (HTML parser), recursively climb
+down the nested json tree, testing at each node for the presence of the needle,
+and if provided, whether that node has an attribute, and, if provided, whether that
+attribute's value exactly matches the provided value.
+
+**Kind**: global constant
+
+* * *
+
+
+
+## cvContainsSelfClosingTag
+Given a rule and a block of code, search for a self closing tag such as
+Optionally run attrCount to check for extra rules (such as requiring "src")
+
+**Kind**: global constant
+
+* * *
+
+
+
+## cvContainsOne
+Given a rule and a block of code, ensure that the given needle (such as )
+occurs once and only once in the code (useful for tags like body, head, html)
+
+**Kind**: global constant
+
+* * *
+
+
+
+## cvContainsTag
+Given a rule and a block of code, check if a given tag (such as ) is included in the
+code. Optionally, use attrCount to match any provided attributes or values in the rule.
+
+**Kind**: global constant
+
+* * *
+
+
+
+## cvContainsStyle
+Given a rule and a block of code, using the "css" module to turn the css into a crawlable
+object. Fold over that generated parsed object and drill down to check if the rule's property
+matches the property and value of the css entered by the student.
+
+**Kind**: global constant
+
+* * *
+
## flattenCodeBlock(user, cb) ⇒ Object
diff --git a/app/components/CodeEditor/DrawerValidation.jsx b/app/components/CodeEditor/DrawerValidation.jsx
index fa63d93..d0cecce 100644
--- a/app/components/CodeEditor/DrawerValidation.jsx
+++ b/app/components/CodeEditor/DrawerValidation.jsx
@@ -3,6 +3,13 @@ import {translate} from "react-i18next";
import "./DrawerValidation.css";
+/**
+ * The CodeEditor is responsible for handling violation of the rules set out for a given
+ * programming challenge. A list of errors is passed in as a prop - this component is
+ * responsible for creating a readable sentence form of what is wrong with the student's code.
+ * For example, "Your HTML code is missing an
) is included in the + * code. Optionally, use attrCount to match any provided attributes or values in the rule. + */ export const cvContainsTag = (rule, payload) => { const html = payload.theText; const json = payload.theJSON; @@ -92,6 +134,11 @@ export const cvContainsTag = (rule, payload) => { return tagClosed && hasAttr; }; +/** + * Given a rule and a block of code, using the "css" module to turn the css into a crawlable + * object. Fold over that generated parsed object and drill down to check if the rule's property + * matches the property and value of the css entered by the student. + */ export const cvContainsStyle = (rule, payload) => { const haystack = payload.theJSON; const needle = rule.needle; @@ -99,9 +146,11 @@ export const cvContainsStyle = (rule, payload) => { const value = rule.value; let head, html, style = null; let styleContent = ""; + // First, crawl through the students html to find the style tag if (haystack) html = haystack.find(e => e.tagName === "html"); if (html) head = html.children.find(e => e.tagName === "head"); if (head) style = head.children.find(e => e.tagName === "style"); + // Grab the CSS out of the style tag and parse it if (style && style.children && style.children[0]) styleContent = style.children[0].content; if (!styleContent) styleContent = ""; const obj = css.parse(styleContent, {silent: true}); diff --git a/sandbox/sandbox.js b/sandbox/sandbox.js index 62be6f5..a9524c7 100644 --- a/sandbox/sandbox.js +++ b/sandbox/sandbox.js @@ -1,3 +1,18 @@ +/** + * Sandbox is the special landing page where student content is rendered. It is currently + * hosted on codelife.tech/[filename].html, where [filename] is the URL from which the + * request is coming, with dashes instead of dots (e.g., codelife.com -> codelife-com.html). + * The Long List of files you see in this directory are unique landing pages for each of the + * possible URL iterations of Codelife. The reason we need a landing page for each of these + * is because the remote rendering makes use of the HTML method postMessage(), which allows + * a payload to be sent across URLs. postMessage requires that the host be explicitly set - + * each landing page listens ONLY for requests from its host, and uses this file to render + * the code in an iframe. postMessage is also used to return status messages to the requester, + * so that the CodeEditor can show which rules are passing and which are failing based on the + * remote execution of the javascript code. + */ + +// loopProtect is an open source library that catches infinite loops in js code. loopProtect.alias = 'protect'; window.loopProtect = loopProtect; @@ -8,6 +23,8 @@ loopProtect.hit = function (line) { if (source) source.postMessage(["catch", "Potential Infinite Loop found on line " + line], host); } +// isNode, isElement, and isNodeList are all catches for when the user attempts to console.log +// an html element. postMessage cannot directly transmit html Nodes, so they must be stringified function isNode(o) { return ( typeof Node === "object" ? o instanceof Node : @@ -31,8 +48,28 @@ function isNodeList(nodes) { (nodes.length === 0 || (typeof nodes[0] === "object" && nodes[0].nodeType > 0)); } +// listen for postMessage() window.addEventListener("message", receiveMessage, false); +/** + * For a full understanding of what's happening here, check CodeEditor.jsx. Before the + * students code is sent via postMessage, it is edited. One of the edits made is that + * a series of "parent.myPost()" functions are injected to the end of the student's js. + * By the time the student's code arrives HERE and is actually executed, parent.myPost + * will refer to THIS function. The job of this function is to receive the arguments + * from that injected javascript, and use postMessage to send the results of that function + * BACK to the react side. + * As an example - say rule 3 is that the user's javascript sets the value of "total" + * to 10. Before we send the students code here to be rendered in an iframe, we edit the code: + * -------------------------- + * total = undefined; + * [ original student code goes here] + * parent.myPost(rule3, total === 10) + * -------------------------- + * This way, the ACTUAL value of total in the VM can be tested. When the injected student code calls + * parent.myPost, it invokes this function, which then sends the results of that BACK THROUGH + * postMessage to the original window - so that we can tell the student if they are correct or not. + */ window.myPost = function() { const payload = []; for (const a of arguments) { @@ -47,14 +84,26 @@ window.myPost = function() { payload.push(arg); } } + // remember - host is determined by which html file (e.g. codelife-com.html) is embedding sandbox.html source.postMessage(payload, host); } +/** + * Listen for messages from the React host. Most of these will be complete web pages made + * by students, so the default behavior is to just inject it directly into an iframe + */ function receiveMessage(event) { if (event.origin !== host) return; source = event.source; + /** + * It takes some time for the iframe to "wake up" and finish being embedded on the page. + * There was no way on the React side to detect when the iframe was ready for writing, + * so for the first second or two, React sends wakeup messages every ~50ms or so until it + * receives "awake" back from this page. Once that has happened, simply dump the code + * into an iframe every time. + */ if (event.data === "wakeup") { source.postMessage("awake", host); } @@ -66,6 +115,7 @@ function receiveMessage(event) { win.protect = loopProtect; + // if URL is requested with ?screenshot=true, hide scrollbars (smile for the camera!) var code = event.data; if (isScreenshot) { if (code.indexOf("
= 0) {