Feat: User Integration (#92)

This commit is contained in:
Stuart Taylor
2018-05-24 19:45:38 +01:00
committed by Mrugesh Mohapatra
parent a296568e97
commit 10341daeb5
39 changed files with 959 additions and 454 deletions

View File

@ -5,11 +5,16 @@
], ],
"plugins": [ "plugins": [
"add-module-exports", "add-module-exports",
"transform-function-bind",
[ [
"transform-imports", { "transform-imports", {
"react-bootstrap": { "react-bootstrap": {
"transform": "react-bootstrap/lib/${member}", "transform": "react-bootstrap/lib/${member}",
"preventFullImport": true "preventFullImport": true
},
"lodash": {
"transform": "lodash/${member}",
"preventFullImport": true
} }
} }
] ]

View File

@ -9,7 +9,7 @@ module.exports = {
siteUrl: 'https://learn.freecodecamp.org' siteUrl: 'https://learn.freecodecamp.org'
}, },
proxy: { proxy: {
prefix: '/api', prefix: '/external',
url: 'http://localhost:3000' url: 'http://localhost:3000'
}, },
plugins: [ plugins: [

View File

@ -102,7 +102,6 @@ exports.createPages = ({ graphql, boundActionCreators }) => {
}); });
}; };
const webpack = require('webpack');
const generateBabelConfig = require('gatsby/dist/utils/babel-config'); const generateBabelConfig = require('gatsby/dist/utils/babel-config');
exports.modifyWebpackConfig = ({ config, stage }) => { exports.modifyWebpackConfig = ({ config, stage }) => {
@ -135,13 +134,6 @@ exports.modifyWebpackConfig = ({ config, stage }) => {
} }
] ]
]); ]);
config.plugin('DefinePlugin', webpack.DefinePlugin, [
{
AUTH0_DOMAIN: JSON.stringify(process.env.AUTH0_DOMAIN),
AUTH0_CLIENT_ID: JSON.stringify(process.env.AUTH0_CLIENT_ID),
AUTH0_NAMESPACE: JSON.stringify(process.env.AUTH0_NAMESPACE)
}
]);
}); });
}; };
/* eslint-disable prefer-object-spread/prefer-object-spread */ /* eslint-disable prefer-object-spread/prefer-object-spread */
@ -151,6 +143,7 @@ exports.modifyBabelrc = ({ babelrc }) =>
[ [
'transform-es2015-arrow-functions', 'transform-es2015-arrow-functions',
'transform-imports', 'transform-imports',
'transform-function-bind',
{ {
'react-bootstrap': { 'react-bootstrap': {
transform: 'react-bootstrap/lib/${member}', transform: 'react-bootstrap/lib/${member}',

View File

@ -12,12 +12,14 @@
"babel-plugin-transform-imports": "^1.5.0", "babel-plugin-transform-imports": "^1.5.0",
"babel-standalone": "^6.26.0", "babel-standalone": "^6.26.0",
"brace": "^0.11.1", "brace": "^0.11.1",
"browser-cookies": "^1.2.0",
"chai": "^4.1.2", "chai": "^4.1.2",
"copy-webpack-plugin": "^4.5.1", "copy-webpack-plugin": "^4.5.1",
"debug": "^3.1.0", "debug": "^3.1.0",
"dotenv": "^5.0.1", "dotenv": "^5.0.1",
"enzyme": "^3.3.0", "enzyme": "^3.3.0",
"enzyme-adapter-react-15": "^1.0.5", "enzyme-adapter-react-15": "^1.0.5",
"fetchr": "^0.5.37",
"gatsby": "^1.9.243", "gatsby": "^1.9.243",
"gatsby-link": "^1.6.39", "gatsby-link": "^1.6.39",
"gatsby-plugin-google-fonts": "^0.0.4", "gatsby-plugin-google-fonts": "^0.0.4",
@ -88,6 +90,7 @@
"devDependencies": { "devDependencies": {
"babel-eslint": "^8.2.2", "babel-eslint": "^8.2.2",
"babel-plugin-transform-es2015-arrow-functions": "^6.22.0", "babel-plugin-transform-es2015-arrow-functions": "^6.22.0",
"babel-plugin-transform-function-bind": "^6.22.0",
"enzyme-adapter-react-16": "^1.1.1", "enzyme-adapter-react-16": "^1.1.1",
"eslint": "^4.19.1", "eslint": "^4.19.1",
"eslint-config-freecodecamp": "^1.1.1", "eslint-config-freecodecamp": "^1.1.1",

View File

@ -1,3 +1,6 @@
AUTH0_DOMAIN=<auth0-tennant>.auth0.com AUTH0_DOMAIN=<auth0-tennant>.auth0.com
AUTH0_CLIENT_ID=this-is-me AUTH0_CLIENT_ID=this-is-me
AUTH0_NAMESPACE='https://auth-ns.freecodecamp.org/' AUTH0_NAMESPACE='https://auth-ns.freecodecamp.org/'
DEV_SERVICE_PATH='http://localhost:3000/services'
PROD_SERVICE_PATH=''

View File

@ -0,0 +1,174 @@
{
"name": "APIs and Microservices Projects",
"order": 4,
"time": "150 hours",
"helpRoom": "HelpBackend",
"challenges": [
{
"id": "bd7158d8c443edefaeb5bdef",
"title": "Timestamp Microservice",
"description": [
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://curse-arrow.glitch.me/' target='_blank'>https://curse-arrow.glitch.me/</a>.",
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but it must be publicly visible for our testing.",
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-timestamp/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-timestamp/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
],
"challengeSeed": [],
"tests": [
{
"text": "I can pass a string as a parameter, and it will check to see whether that string contains either a unix timestamp or a natural language date (example: January 1, 2016).",
"testString": ""
},
{
"text": "If it does, it returns both the Unix timestamp and the natural language form of that date.",
"testString": ""
},
{
"text": "If it does not contain a date or Unix timestamp, it returns null for those properties.",
"testString": ""
}
],
"solutions": [],
"hints": [],
"challengeType": 4,
"isRequired": true,
"releasedOn": "January 1, 2016",
"translations": {
"es": {
"title": "Microservicio de Marca Temporal"
}
}
},
{
"id": "bd7158d8c443edefaeb5bdff",
"title": "Request Header Parser Microservice",
"description": [
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://dandelion-roar.glitch.me/' target='_blank'>https://dandelion-roar.glitch.me/</a>.",
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but it must be publicly visible for our testing.",
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-headerparser/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-headerparser/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
],
"challengeSeed": [],
"tests": [
{
"text": "I can get the IP address, language and operating system for my browser.",
"testString": ""
}
],
"solutions": [],
"hints": [],
"challengeType": 4,
"isRequired": true,
"releasedOn": "January 1, 2016",
"translations": {
"es": {
"title": "Microservicio para analizar el encabezado de una petición"
}
}
},
{
"id": "bd7158d8c443edefaeb5bd0e",
"title": "URL Shortener Microservice",
"description": [
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://thread-paper.glitch.me/' target='_blank'>https://thread-paper.glitch.me/</a>.",
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but it must be publicly visible for our testing.",
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-urlshortener/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-urlshortener/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
],
"challengeSeed": [],
"tests": [
{
"text": "I can pass a URL as a parameter and I will receive a shortened URL in the JSON response.",
"testString": ""
},
{
"text": "If I pass an invalid URL that doesn't follow the valid http://www.example.com format, the JSON response will contain an error instead.",
"testString": ""
},
{
"text": "When I visit that shortened URL, it will redirect me to my original link.",
"testString": ""
}
],
"solutions": [],
"hints": [],
"challengeType": 4,
"isRequired": true,
"releasedOn": "January 1, 2016",
"translations": {
"es": {
"title": "Microservicio para acortar URLs"
}
}
},
{
"id": "5a8b073d06fa14fcfde687aa",
"title": "Exercise Tracker",
"description": [
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://fuschia-custard.glitch.me/' target='_blank'>https://fuschia-custard.glitch.me/</a>.",
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but it must be publicly visible for our testing.",
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-exercisetracker/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-exercisetracker/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
],
"challengeSeed": [],
"tests": [
{
"text": "I can create a user by posting form data username to /api/exercise/new-user and returned will be an object with username and <code>_id</code>.",
"testString": ""
},
{
"text": "I can get an array of all users by getting api/exercise/users with the same info as when creating a user.",
"testString": ""
},
{
"text": "I can add an exercise to any user by posting form data userId(_id), description, duration, and optionally date to /api/exercise/add. If no date supplied it will use current date. App will return the user object with the exercise fields added.",
"testString": ""
},
{
"text": "I can retrieve a full exercise log of any user by getting /api/exercise/log with a parameter of userId(_id). App will return the user object with added array log and count (total exercise count).",
"testString": ""
},
{
"text": "I can retrieve part of the log of any user by also passing along optional parameters of from & to or limit. (Date format yyyy-mm-dd, limit = int)",
"testString": ""
}
],
"solutions": [],
"hints": [],
"challengeType": 4,
"isRequired": true,
"releasedOn": "February 17, 2017",
"translations": {
"es": {
"title": "Capa de abstracción para buscar imágenes"
}
}
},
{
"id": "bd7158d8c443edefaeb5bd0f",
"title": "File Metadata Microservice",
"description": [
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://purple-paladin.glitch.me/' target='_blank'>https://purple-paladin.glitch.me/</a>.",
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but it must be publicly visible for our testing.",
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-filemetadata/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-filemetadata/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
],
"challengeSeed": [],
"tests": [
{
"text": "I can submit a FormData object that includes a file upload.",
"testString": ""
},
{
"text": "When I submit something, I will receive the file size in bytes within the JSON response.",
"testString": ""
}
],
"solutions": [],
"hints": [],
"challengeType": 4,
"isRequired": true,
"releasedOn": "January 1, 2016",
"translations": {
"es": {
"title": "Microservicio de metadatos de archivos"
}
}
}
]
}

View File

@ -0,0 +1,298 @@
{
"name": "Information Security and Quality Assurance Projects",
"order": 4,
"time": "150 hours",
"helpRoom": "HelpBackend",
"challenges": [
{
"id": "587d8249367417b2b2512c41",
"title": "Metric-Imperial Converter",
"description": [
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://hard-twilight.glitch.me/' target='_blank'>https://hard-twilight.glitch.me/</a>.",
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but it must be publicly visible for our testing.",
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-metricimpconverter/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-metricimpconverter/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
],
"challengeSeed": [],
"tests": [
{
"text": "I will prevent the client from trying to guess(sniff) the MIME type.",
"testString": ""
},
{
"text": "I will prevent cross-site scripting (XSS) attacks.",
"testString": ""
},
{
"text": "I can GET /api/convert with a single parameter containing an accepted number and unit and have it converted. (Hint: Split the input by looking for the index of the first character which will mark the start of the unit)",
"testString": ""
},
{
"text": "I can convert 'gal' to 'L' and vice versa. (1 gal to 3.78541 L)",
"testString": ""
},
{
"text": "I can convert 'lbs' to 'kg' and vice versa. (1 lbs to 0.453592 kg)",
"testString": ""
},
{
"text": "I can convert 'mi' to 'km' and vice versa. (1 mi to 1.60934 km)",
"testString": ""
},
{
"text": "If my unit of measurement is invalid, returned will be 'invalid unit'.",
"testString": ""
},
{
"text": "If my number is invalid, returned with will 'invalid number'.",
"testString": ""
},
{
"text": "If both are invalid, return will be 'invalid number and unit'.",
"testString": ""
},
{
"text": "I can use fractions, decimals or both in my parameter(ie. 5, 1/2, 2.5/6), but if nothing is provided it will default to 1.",
"testString": ""
},
{
"text": "My return will consist of the initNum, initUnit, returnNum, returnUnit, and string spelling out units in format '{initNum} {initial_Units} converts to {returnNum} {return_Units}' with the result rounded to 5 decimals in the string.",
"testString": ""
},
{
"text": "All 16 unit tests are complete and passing.",
"testString": ""
},
{
"text": "All 5 functional tests are complete and passing.",
"testString": ""
}
],
"solutions": [],
"hints": [],
"challengeType": 4,
"isRequired": true,
"releasedOn": "January 15, 2017",
"translations": {}
},
{
"id": "587d8249367417b2b2512c42",
"title": "Issue Tracker",
"description": [
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://protective-garage.glitch.me/' target='_blank'>https://protective-garage.glitch.me/</a>.",
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but it must be publicly visible for our testing.",
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-issuetracker/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-issuetracker/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
],
"challengeSeed": [],
"tests": [
{
"text": "Prevent cross site scripting (XSS) attacks.",
"testString": ""
},
{
"text": "I can POST /api/issues/{projectname} with form data containing required issue_title, issue_text, created_by, and optional assigned_to and status_text.",
"testString": ""
},
{
"text": "The object saved (and returned) will include all of those fields (blank for optional no input) and also include created_on(date/time), updated_on(date/time), open(boolean, true for open, false for closed), and _id.",
"testString": ""
},
{
"text": "I can PUT /api/issues/{projectname} with a id and any fields in the object with a value to object said object. Returned will be 'successfully updated' or 'could not update '+id. This should always update updated_on. If no fields are sent return 'no updated field sent'.",
"testString": ""
},
{
"text": "I can DELETE /api/issues/{projectname} with a id to completely delete an issue. If no _id is sent return 'id error', success: 'deleted '+id, failed: 'could not delete '+id.",
"testString": ""
},
{
"text": "I can GET /api/issues/{projectname} for an array of all issues on that specific project with all the information for each issue as was returned when posted.",
"testString": ""
},
{
"text": "I can filter my get request by also passing along any field and value in the query(ie. /api/issues/{project}?open=false). I can pass along as many fields/values as I want.",
"testString": ""
},
{
"text": "All 11 functional tests are complete and passing.",
"testString": ""
}
],
"solutions": [],
"hints": [],
"challengeType": 4,
"isRequired": true,
"releasedOn": "January 15, 2017",
"translations": {}
},
{
"id": "587d824a367417b2b2512c43",
"title": "Personal Library",
"description": [
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://spark-cathedral.glitch.me/' target='_blank'>https://spark-cathedral.glitch.me/</a>.",
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but must be publicly visible for our testing.",
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-library/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-library/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
],
"challengeSeed": [],
"tests": [
{
"text": "Nothing from my website will be cached in my client.",
"testString": ""
},
{
"text": "The headers will say that the site is powered by 'PHP 4.2.0' even though it isn't (as a security measure).",
"testString": ""
},
{
"text": "I can post a title to /api/books to add a book and returned will be the object with the title and a unique _id.",
"testString": ""
},
{
"text": "I can get /api/books to retrieve an array of all books containing title, _id, and commentcount.",
"testString": ""
},
{
"text": "I can get /api/books/{id} to retrieve a single object of a book containing _title, _id, & an array of comments (empty array if no comments present).",
"testString": ""
},
{
"text": "I can post a comment to /api/books/{id} to add a comment to a book and returned will be the books object similar to get /api/books/{id} including the new comment.",
"testString": ""
},
{
"text": "I can delete /api/books/{_id} to delete a book from the collection. Returned will be 'delete successful' if successful.",
"testString": ""
},
{
"text": "If I try to request a book that doesn't exist I will be returned 'no book exists'.",
"testString": ""
},
{
"text": "I can send a delete request to /api/books to delete all books in the database. Returned will be 'complete delete successful' if successful.",
"testString": ""
},
{
"text": "All 6 functional tests required are complete and passing.",
"testString": ""
}
],
"solutions": [],
"hints": [],
"challengeType": 4,
"isRequired": true,
"releasedOn": "January 15, 2017",
"translations": {}
},
{
"id": "587d824a367417b2b2512c44",
"title": "Stock Price Checker",
"description": [
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://giant-chronometer.glitch.me/' target='_blank'>https://giant-chronometer.glitch.me/</a>.",
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but must be publicly visible for our testing.",
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-stockchecker/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-stockchecker/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
],
"challengeSeed": [],
"tests": [
{
"text": "Set the content security policies to only allow loading of scripts and css from your server.",
"testString": ""
},
{
"text": "I can GET /api/stock-prices with form data containing a Nasdaq stock ticker and receive back an object stockData.",
"testString": ""
},
{
"text": "In stockData, I can see the stock(string, the ticker), price(decimal in string format), and likes(int).",
"testString": ""
},
{
"text": "I can also pass along field like as true(boolean) to have my like added to the stock(s). Only 1 like per ip should be accepted.",
"testString": ""
},
{
"text": "If I pass along 2 stocks, the return object will be an array with both stock's info. Instead of likes, it will display rel_likes(the difference between the likes on both stocks) on both.",
"testString": ""
},
{
"text": "A good way to receive current price is the following external API(replacing 'GOOG' with your stock): https://finance.google.com/finance/info?q=NASDAQ%3aGOOG",
"testString": ""
},
{
"text": "All 5 functional tests are complete and passing.",
"testString": ""
}
],
"solutions": [],
"hints": [],
"challengeType": 4,
"isRequired": true,
"releasedOn": "January 15, 2017",
"translations": {}
},
{
"id": "587d824a367417b2b2512c45",
"title": "Anonymous Message Board",
"description": [
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://horn-celery.glitch.me/' target='_blank'>https://horn-celery.glitch.me/</a>.",
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but it must be publicly visible for our testing.",
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-messageboard/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-messageboard/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
],
"challengeSeed": [],
"tests": [
{
"text": "Only allow your site to be loading in an iFrame on your own pages.",
"testString": ""
},
{
"text": "Do not allow DNS prefetching.",
"testString": ""
},
{
"text": "Only allow your site to send the referrer for your own pages.",
"testString": ""
},
{
"text": "I can POST a thread to a specific message board by passing form data text and deletepassword_ to /api/threads/{board}.(Recommend res.redirect to board page /b/{board}) Saved will be at least _id, text, createdon_(date&time), bumpedon_(date&time, starts same as created_on), reported(boolean), deletepassword_, & replies(array).",
"testString": ""
},
{
"text": "I can POST a reply to a thread on a specific board by passing form data text, deletepassword_, & threadid_ to /api/replies/{board} and it will also update the bumped_on date to the comments date.(Recommend res.redirect to thread page /b/{board}/{thread_id}) In the thread's replies array will be saved _id, text, createdon_, deletepassword_, & reported.",
"testString": ""
},
{
"text": "I can GET an array of the most recent 10 bumped threads on the board with only the most recent 3 replies each from /api/threads/{board}. The reported and deletepasswords_ fields will not be sent to the client.",
"testString": ""
},
{
"text": "I can GET an entire thread with all its replies from /api/replies/{board}?thread_id={thread_id}. Also hiding the same fields the client should be see.",
"testString": ""
},
{
"text": "I can delete a thread completely if I send a DELETE request to /api/threads/{board} and pass along the threadid_ & deletepassword_. (Text response will be 'incorrect password' or 'success')",
"testString": ""
},
{
"text": "I can delete a post(just changing the text to '[deleted]' instead of removing completely like a thread) if I send a DELETE request to /api/replies/{board} and pass along the threadid_, replyid_, & deletepassword_. (Text response will be 'incorrect password' or 'success')",
"testString": ""
},
{
"text": "I can report a thread and change its reported value to true by sending a PUT request to /api/threads/{board} and pass along the threadid_. (Text response will be 'success')",
"testString": ""
},
{
"text": "I can report a reply and change its reported value to true by sending a PUT request to /api/replies/{board} and pass along the threadid_ & replyid_. (Text response will be 'success')",
"testString": ""
},
{
"text": "Complete functional tests that wholly test routes and pass.",
"testString": ""
}
],
"solutions": [],
"hints": [],
"challengeType": 4,
"isRequired": true,
"releasedOn": "January 15, 2017",
"translations": {}
}
]
}

View File

@ -0,0 +1 @@
export default ['a', 'c', 'g'];

View File

@ -4,6 +4,7 @@ export default [
slug: '/super-block-one/block-a/challenge-one', slug: '/super-block-one/block-a/challenge-one',
blockName: 'Block A' blockName: 'Block A'
}, },
id: 'a',
block: 'block-a', block: 'block-a',
title: 'Challenge One', title: 'Challenge One',
isRequired: false, isRequired: false,
@ -16,6 +17,7 @@ export default [
slug: '/super-block-one/block-a/challenge-two', slug: '/super-block-one/block-a/challenge-two',
blockName: 'Block A' blockName: 'Block A'
}, },
id: 'b',
block: 'block-a', block: 'block-a',
title: 'Challenge Two', title: 'Challenge Two',
isRequired: false, isRequired: false,
@ -28,6 +30,7 @@ export default [
slug: '/super-block-one/block-b/challenge-one', slug: '/super-block-one/block-b/challenge-one',
blockName: 'Block B' blockName: 'Block B'
}, },
id: 'c',
block: 'block-b', block: 'block-b',
title: 'Challenge One', title: 'Challenge One',
isRequired: false, isRequired: false,
@ -40,6 +43,8 @@ export default [
slug: '/super-block-one/block-b/challenge-two', slug: '/super-block-one/block-b/challenge-two',
blockName: 'Block B' blockName: 'Block B'
}, },
id: 'd',
block: 'block-b', block: 'block-b',
title: 'Challenge Two', title: 'Challenge Two',
isRequired: false, isRequired: false,
@ -52,6 +57,7 @@ export default [
slug: '/super-block-one/block-c/challenge-one', slug: '/super-block-one/block-c/challenge-one',
blockName: 'Block C' blockName: 'Block C'
}, },
id: 'e',
block: 'block-c', block: 'block-c',
title: 'Challenge One', title: 'Challenge One',
isRequired: false, isRequired: false,
@ -64,6 +70,7 @@ export default [
slug: '/super-block-one/block-a/challenge-one', slug: '/super-block-one/block-a/challenge-one',
blockName: 'Block A' blockName: 'Block A'
}, },
id: 'f',
block: 'block-a', block: 'block-a',
title: 'Challenge One', title: 'Challenge One',
isRequired: false, isRequired: false,
@ -76,6 +83,7 @@ export default [
slug: '/super-block-one/block-a/challenge-two', slug: '/super-block-one/block-a/challenge-two',
blockName: 'Block A' blockName: 'Block A'
}, },
id: 'g',
block: 'block-a', block: 'block-a',
title: 'Challenge Two', title: 'Challenge Two',
isRequired: false, isRequired: false,
@ -88,6 +96,7 @@ export default [
slug: '/super-block-one/block-b/challenge-one', slug: '/super-block-one/block-b/challenge-one',
blockName: 'Block B' blockName: 'Block B'
}, },
id: 'h',
block: 'block-b', block: 'block-b',
title: 'Challenge One', title: 'Challenge One',
isRequired: false, isRequired: false,
@ -100,6 +109,7 @@ export default [
slug: '/super-block-one/block-b/challenge-two', slug: '/super-block-one/block-b/challenge-two',
blockName: 'Block B' blockName: 'Block B'
}, },
id: 'i',
block: 'block-b', block: 'block-b',
title: 'Challenge Two', title: 'Challenge Two',
isRequired: false, isRequired: false,

View File

@ -1,30 +0,0 @@
/*
TODO(Bouncey): Placed in ./src/pages when we have auth
*/
import React from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { auth } from '../auth';
import { updateUserSignedIn, fetchUserComplete } from '../redux/app';
const mapStateToProps = () => ({});
const mapDispatchToProps = dispatch =>
bindActionCreators({ updateUserSignedIn, fetchUserComplete }, dispatch);
const propTypes = {
fetchUserComplete: PropTypes.func.isRequired,
updateUserSignedIn: PropTypes.func.isRequired
};
function AuthCallback({ updateUserSignedIn, fetchUserComplete }) {
auth.handleAuthentication({ updateUserSignedIn, fetchUserComplete });
return <h2>One moment whilst we finish signing you in</h2>;
}
AuthCallback.displayName = 'AuthCallback';
AuthCallback.propTypes = propTypes;
export default connect(mapStateToProps, mapDispatchToProps)(AuthCallback);

View File

@ -1,111 +0,0 @@
/* global AUTH0_DOMAIN AUTH0_CLIENT_ID AUTH0_NAMESPACE*/
import auth0 from 'auth0-js/build/auth0';
import { navigateTo } from 'gatsby-link';
const namespace = AUTH0_NAMESPACE;
const domain = AUTH0_DOMAIN;
const clientID = AUTH0_CLIENT_ID;
class Auth {
constructor() {
this.auth0 = new auth0.WebAuth({
domain,
clientID,
redirectUri: `${
typeof window !== 'undefined' ? window.location.origin : ''
}/auth-callback`,
audience: `https://${domain}/api/v2/`,
responseType: 'token id_token',
scope: `openid profile email ${namespace + 'accountLinkId'}`
});
this.getUser = this.getUser.bind(this);
this.getToken = this.getToken.bind(this);
this.handleAuthentication = this.handleAuthentication.bind(this);
this.isAuthenticated = this.isAuthenticated.bind(this);
this.login = this.login.bind(this);
this.logout = this.logout.bind(this);
this.setSession = this.setSession.bind(this);
}
login() {
return this.auth0.authorize();
}
logout() {
return Promise.all([
localStorage.removeItem('access_token'),
localStorage.removeItem('id_token'),
localStorage.removeItem('expires_at'),
localStorage.removeItem('user')
]).then(
() =>
typeof window !== 'undefined'
? window.location.reload()
: navigateTo('/#')
);
}
handleAuthentication({ updateUserSignedIn, fetchUserComplete }) {
if (typeof window !== 'undefined') {
this.auth0.parseHash((err, authResult) => {
if (err) {
console.log(err);
return navigateTo('/strange-place');
}
if (authResult && authResult.accessToken && authResult.idToken) {
return (
this.setSession(authResult)
.then(user => {
updateUserSignedIn(true);
return fetchUserComplete(user);
})
// this could be current-challenge
.then(() => navigateTo('/#'))
);
}
return navigateTo('/strange-place');
});
}
}
isAuthenticated() {
const expiresAt = JSON.parse(localStorage.getItem('expires_at'));
const isAuth = new Date().getTime() < expiresAt;
return isAuth;
}
setSession = authResult => {
const expiresAt = JSON.stringify(
authResult.expiresIn * 1000 + new Date().getTime()
);
localStorage.setItem('access_token', authResult.accessToken);
localStorage.setItem('id_token', authResult.idToken);
localStorage.setItem('expires_at', expiresAt);
return new Promise((resolve, reject) => {
this.auth0.client.userInfo(authResult.accessToken, (err, user) => {
if (err) {
// TODO: Decide what we want to do here
reject(err);
}
localStorage.setItem('user', JSON.stringify(user));
resolve(user);
});
});
};
getUser() {
const user = this.isAuthenticated() && localStorage.getItem('user');
if (user) {
return JSON.parse(user);
}
return null;
}
getToken() {
return localStorage.getItem('id_token');
}
}
export const auth = new Auth();

View File

@ -1,20 +1,13 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import Button from 'react-bootstrap/lib/Button';
const propTypes = { function Login() {
login: PropTypes.func.isRequired
};
function Login({ login }) {
return ( return (
<Button bsStyle='default' onClick={login}> <a href='https://beta.freecodecamp.org/signin' target='_blank'>
Login Login
</Button> </a>
); );
} }
Login.displayName = 'Login'; Login.displayName = 'Login';
Login.propTypes = propTypes;
export default Login; export default Login;

View File

@ -1,27 +1,9 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
import MenuItem from 'react-bootstrap/lib/MenuItem';
const propTypes = { function SignedIn() {
email: PropTypes.string, return <a href='https://beta.freecodecamp.org/settings'>Settings</a>;
logout: PropTypes.func.isRequired,
name: PropTypes.string
};
function SignedIn({ email, name, logout }) {
return (
<DropdownButton
bsStyle='default'
id='signedin-dropdown-button'
title={name ? name : email}
>
<MenuItem onClick={logout}>Log Out</MenuItem>
</DropdownButton>
);
} }
SignedIn.displayName = 'SignedIn'; SignedIn.displayName = 'SignedIn';
SignedIn.propTypes = propTypes;
export default SignedIn; export default SignedIn;

View File

@ -1,64 +1,30 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { auth } from '../../../auth'; import { isSignedInSelector } from '../../../redux/app';
import {
fetchUserComplete,
isSignedInSelector,
userSelector,
updateUserSignedIn
} from '../../../redux/app';
import Login from './Login'; import Login from './Login';
import SignedIn from './SignedIn'; import SignedIn from './SignedIn';
const mapStateToProps = createSelector( const mapStateToProps = createSelector(isSignedInSelector, isSignedIn => ({
isSignedInSelector, isSignedIn
userSelector, }));
(isSignedIn, { name, email }) => ({ isSignedIn, name, email })
);
const mapDispatchToProps = dispatch =>
bindActionCreators({ updateUserSignedIn, fetchUserComplete }, dispatch);
const propTypes = { const propTypes = {
email: PropTypes.string, email: PropTypes.string,
fetchUserComplete: PropTypes.func.isRequired,
isSignedIn: PropTypes.bool, isSignedIn: PropTypes.bool,
name: PropTypes.string, name: PropTypes.string
updateUserSignedIn: PropTypes.func.isRequired
}; };
class UserState extends PureComponent { class UserState extends PureComponent {
componentDidMount() {
const isAuth = auth.isAuthenticated();
if (isAuth) {
this.props.fetchUserComplete(auth.getUser());
}
this.props.updateUserSignedIn(isAuth);
}
componentDidUpdate(prevProps) {
const isAuth = auth.isAuthenticated();
if (prevProps.isSignedIn && !isAuth) {
this.props.fetchUserComplete(auth.getUser());
this.props.updateUserSignedIn(isAuth);
}
}
render() { render() {
const { isSignedIn, name, email } = this.props; const { isSignedIn } = this.props;
return isSignedIn && (name || email) ? ( return isSignedIn ? <SignedIn /> : <Login />;
<SignedIn email={email} logout={auth.logout} name={name} />
) : (
<Login login={auth.login} />
);
} }
} }
UserState.displayName = 'UserState'; UserState.displayName = 'UserState';
UserState.propTypes = propTypes; UserState.propTypes = propTypes;
export default connect(mapStateToProps, mapDispatchToProps)(UserState); export default connect(mapStateToProps)(UserState);

View File

@ -45,7 +45,7 @@ header {
margin: 0px 10px; margin: 0px 10px;
} }
#top-right-nav li > a { #top-right-nav li > a, #top-right-nav li > span {
color:#fff; color:#fff;
font-size: 17px; font-size: 17px;
font-weight: 700; font-weight: 700;

View File

@ -3,6 +3,7 @@ import Link from 'gatsby-link';
import FCCSearch from 'react-freecodecamp-search'; import FCCSearch from 'react-freecodecamp-search';
import NavLogo from './components/NavLogo'; import NavLogo from './components/NavLogo';
import UserState from './components/UserState';
import './header.css'; import './header.css';
@ -22,7 +23,7 @@ function Header() {
<a href='https://forum.freecodecamp.org'>Forum</a> <a href='https://forum.freecodecamp.org'>Forum</a>
</li> </li>
<li> <li>
<a href='#'>Log in</a> <UserState />
</li> </li>
</ul> </ul>
</nav> </nav>

View File

@ -46,6 +46,7 @@ exports[`<Map /> snapshot: Map 1`] = `
"blockName": "Block A", "blockName": "Block A",
"slug": "/super-block-one/block-a/challenge-one", "slug": "/super-block-one/block-a/challenge-one",
}, },
"id": "a",
"isPrivate": false, "isPrivate": false,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block One", "superBlock": "Super Block One",
@ -58,6 +59,7 @@ exports[`<Map /> snapshot: Map 1`] = `
"blockName": "Block A", "blockName": "Block A",
"slug": "/super-block-one/block-a/challenge-two", "slug": "/super-block-one/block-a/challenge-two",
}, },
"id": "b",
"isPrivate": false, "isPrivate": false,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block One", "superBlock": "Super Block One",
@ -70,6 +72,7 @@ exports[`<Map /> snapshot: Map 1`] = `
"blockName": "Block B", "blockName": "Block B",
"slug": "/super-block-one/block-b/challenge-one", "slug": "/super-block-one/block-b/challenge-one",
}, },
"id": "c",
"isPrivate": false, "isPrivate": false,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block One", "superBlock": "Super Block One",
@ -82,6 +85,7 @@ exports[`<Map /> snapshot: Map 1`] = `
"blockName": "Block B", "blockName": "Block B",
"slug": "/super-block-one/block-b/challenge-two", "slug": "/super-block-one/block-b/challenge-two",
}, },
"id": "d",
"isPrivate": false, "isPrivate": false,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block One", "superBlock": "Super Block One",
@ -94,6 +98,7 @@ exports[`<Map /> snapshot: Map 1`] = `
"blockName": "Block C", "blockName": "Block C",
"slug": "/super-block-one/block-c/challenge-one", "slug": "/super-block-one/block-c/challenge-one",
}, },
"id": "e",
"isPrivate": true, "isPrivate": true,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block One", "superBlock": "Super Block One",
@ -106,6 +111,7 @@ exports[`<Map /> snapshot: Map 1`] = `
"blockName": "Block A", "blockName": "Block A",
"slug": "/super-block-one/block-a/challenge-one", "slug": "/super-block-one/block-a/challenge-one",
}, },
"id": "f",
"isPrivate": false, "isPrivate": false,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block Two", "superBlock": "Super Block Two",
@ -118,6 +124,7 @@ exports[`<Map /> snapshot: Map 1`] = `
"blockName": "Block A", "blockName": "Block A",
"slug": "/super-block-one/block-a/challenge-two", "slug": "/super-block-one/block-a/challenge-two",
}, },
"id": "g",
"isPrivate": false, "isPrivate": false,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block Two", "superBlock": "Super Block Two",
@ -130,6 +137,7 @@ exports[`<Map /> snapshot: Map 1`] = `
"blockName": "Block B", "blockName": "Block B",
"slug": "/super-block-one/block-b/challenge-one", "slug": "/super-block-one/block-b/challenge-one",
}, },
"id": "h",
"isPrivate": false, "isPrivate": false,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block Two", "superBlock": "Super Block Two",
@ -142,6 +150,7 @@ exports[`<Map /> snapshot: Map 1`] = `
"blockName": "Block B", "blockName": "Block B",
"slug": "/super-block-one/block-b/challenge-two", "slug": "/super-block-one/block-b/challenge-two",
}, },
"id": "i",
"isPrivate": false, "isPrivate": false,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block Two", "superBlock": "Super Block Two",
@ -192,6 +201,7 @@ exports[`<Map /> snapshot: Map 1`] = `
"blockName": "Block A", "blockName": "Block A",
"slug": "/super-block-one/block-a/challenge-one", "slug": "/super-block-one/block-a/challenge-one",
}, },
"id": "a",
"isPrivate": false, "isPrivate": false,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block One", "superBlock": "Super Block One",
@ -204,6 +214,7 @@ exports[`<Map /> snapshot: Map 1`] = `
"blockName": "Block A", "blockName": "Block A",
"slug": "/super-block-one/block-a/challenge-two", "slug": "/super-block-one/block-a/challenge-two",
}, },
"id": "b",
"isPrivate": false, "isPrivate": false,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block One", "superBlock": "Super Block One",
@ -216,6 +227,7 @@ exports[`<Map /> snapshot: Map 1`] = `
"blockName": "Block B", "blockName": "Block B",
"slug": "/super-block-one/block-b/challenge-one", "slug": "/super-block-one/block-b/challenge-one",
}, },
"id": "c",
"isPrivate": false, "isPrivate": false,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block One", "superBlock": "Super Block One",
@ -228,6 +240,7 @@ exports[`<Map /> snapshot: Map 1`] = `
"blockName": "Block B", "blockName": "Block B",
"slug": "/super-block-one/block-b/challenge-two", "slug": "/super-block-one/block-b/challenge-two",
}, },
"id": "d",
"isPrivate": false, "isPrivate": false,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block One", "superBlock": "Super Block One",
@ -240,6 +253,7 @@ exports[`<Map /> snapshot: Map 1`] = `
"blockName": "Block C", "blockName": "Block C",
"slug": "/super-block-one/block-c/challenge-one", "slug": "/super-block-one/block-c/challenge-one",
}, },
"id": "e",
"isPrivate": true, "isPrivate": true,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block One", "superBlock": "Super Block One",
@ -252,6 +266,7 @@ exports[`<Map /> snapshot: Map 1`] = `
"blockName": "Block A", "blockName": "Block A",
"slug": "/super-block-one/block-a/challenge-one", "slug": "/super-block-one/block-a/challenge-one",
}, },
"id": "f",
"isPrivate": false, "isPrivate": false,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block Two", "superBlock": "Super Block Two",
@ -264,6 +279,7 @@ exports[`<Map /> snapshot: Map 1`] = `
"blockName": "Block A", "blockName": "Block A",
"slug": "/super-block-one/block-a/challenge-two", "slug": "/super-block-one/block-a/challenge-two",
}, },
"id": "g",
"isPrivate": false, "isPrivate": false,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block Two", "superBlock": "Super Block Two",
@ -276,6 +292,7 @@ exports[`<Map /> snapshot: Map 1`] = `
"blockName": "Block B", "blockName": "Block B",
"slug": "/super-block-one/block-b/challenge-one", "slug": "/super-block-one/block-b/challenge-one",
}, },
"id": "h",
"isPrivate": false, "isPrivate": false,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block Two", "superBlock": "Super Block Two",
@ -288,6 +305,7 @@ exports[`<Map /> snapshot: Map 1`] = `
"blockName": "Block B", "blockName": "Block B",
"slug": "/super-block-one/block-b/challenge-two", "slug": "/super-block-one/block-b/challenge-two",
}, },
"id": "i",
"isPrivate": false, "isPrivate": false,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block Two", "superBlock": "Super Block Two",

View File

@ -7,15 +7,22 @@ import Link, { navigateTo } from 'gatsby-link';
import ga from '../../../analytics'; import ga from '../../../analytics';
import { makeExpandedBlockSelector, toggleBlock } from '../redux'; import { makeExpandedBlockSelector, toggleBlock } from '../redux';
import { toggleMapModal } from '../../../redux/app'; import { toggleMapModal, userSelector } from '../../../redux/app';
import Caret from '../../icons/Caret'; import Caret from '../../icons/Caret';
/* eslint-disable max-len */
import GreenPass from '../../../templates/Challenges/components/icons/GreenPass';
/* eslint-enable max-len */
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
const expandedSelector = makeExpandedBlockSelector(ownProps.blockDashedName); const expandedSelector = makeExpandedBlockSelector(ownProps.blockDashedName);
return createSelector(expandedSelector, isExpanded => ({ isExpanded }))( return createSelector(
state expandedSelector,
); userSelector,
(isExpanded, { completedChallenges = [] }) => ({
isExpanded,
completedChallenges: completedChallenges.map(({ id }) => id)
})
)(state);
}; };
const mapDispatchToProps = dispatch => const mapDispatchToProps = dispatch =>
@ -24,6 +31,7 @@ const mapDispatchToProps = dispatch =>
const propTypes = { const propTypes = {
blockDashedName: PropTypes.string, blockDashedName: PropTypes.string,
challenges: PropTypes.array, challenges: PropTypes.array,
completedChallenges: PropTypes.arrayOf(PropTypes.string),
intro: PropTypes.shape({ intro: PropTypes.shape({
fields: PropTypes.shape({ slug: PropTypes.string.isRequired }), fields: PropTypes.shape({ slug: PropTypes.string.isRequired }),
frontmatter: PropTypes.shape({ frontmatter: PropTypes.shape({
@ -82,20 +90,36 @@ export class Block extends PureComponent {
> >
{challenge.title || challenge.frontmatter.title} {challenge.title || challenge.frontmatter.title}
</Link> </Link>
{challenge.isCompleted ? (
<span className='badge map-badge'>
<GreenPass style={{ height: '15px' }} />
</span>
) : null}
</li> </li>
)); ));
} }
render() { render() {
const { challenges, isExpanded, intro } = this.props; const { completedChallenges, challenges, isExpanded, intro } = this.props;
const { blockName } = challenges[0].fields; const { blockName } = challenges[0].fields;
const challengesWithCompleted = challenges.map(challenge => {
const { id } = challenge;
const isCompleted = completedChallenges.some(
completedId => id === completedId
);
return { ...challenge, isCompleted };
});
return ( return (
<li className={`block ${isExpanded ? 'open' : ''}`}> <li className={`block ${isExpanded ? 'open' : ''}`}>
<div className='map-title' onClick={this.handleBlockClick}> <div className='map-title' onClick={this.handleBlockClick}>
<Caret /> <Caret />
<h5>{blockName}</h5> <h5>{blockName}</h5>
</div> </div>
<ul>{isExpanded ? this.renderChallenges(intro, challenges) : null}</ul> <ul>
{isExpanded
? this.renderChallenges(intro, challengesWithCompleted)
: null}
</ul>
</li> </li>
); );
} }

View File

@ -9,6 +9,7 @@ import sinon from 'sinon';
import { Block } from './Block'; import { Block } from './Block';
import mockNodes from '../../../__mocks__/map-nodes'; import mockNodes from '../../../__mocks__/map-nodes';
import mockIntroNodes from '../../../__mocks__/intro-nodes'; import mockIntroNodes from '../../../__mocks__/intro-nodes';
import mockCompleted from '../../../__mocks__/completedChallengesMock';
Enzyme.configure({ adapter: new Adapter() }); Enzyme.configure({ adapter: new Adapter() });
const renderer = new ShallowRenderer(); const renderer = new ShallowRenderer();
@ -20,6 +21,7 @@ test('<Block /> not expanded snapshot', () => {
<Block <Block
blockDashedName='block-a' blockDashedName='block-a'
challenges={mockNodes.filter(node => node.block === 'block-a')} challenges={mockNodes.filter(node => node.block === 'block-a')}
completedChallenges={mockCompleted}
intro={mockIntroNodes[0]} intro={mockIntroNodes[0]}
isExpanded={false} isExpanded={false}
toggleBlock={toggleSpy} toggleBlock={toggleSpy}
@ -38,6 +40,7 @@ test('<Block expanded snapshot', () => {
<Block <Block
blockDashedName='block-a' blockDashedName='block-a'
challenges={mockNodes.filter(node => node.block === 'block-a')} challenges={mockNodes.filter(node => node.block === 'block-a')}
completedChallenges={mockCompleted}
intro={mockIntroNodes[0]} intro={mockIntroNodes[0]}
isExpanded={true} isExpanded={true}
toggleBlock={toggleSpy} toggleBlock={toggleSpy}
@ -56,6 +59,7 @@ test('<Block /> should handle toggle clicks correctly', () => {
const props = { const props = {
blockDashedName: 'block-a', blockDashedName: 'block-a',
challenges: mockNodes.filter(node => node.block === 'block-a'), challenges: mockNodes.filter(node => node.block === 'block-a'),
completedChallenges: mockCompleted,
intro: mockIntroNodes[0], intro: mockIntroNodes[0],
isExpanded: false, isExpanded: false,
toggleBlock: toggleSpy, toggleBlock: toggleSpy,

View File

@ -50,6 +50,17 @@ exports[`<Block expanded snapshot: block-expanded 1`] = `
> >
Challenge One Challenge One
</Unknown> </Unknown>
<span
className="badge map-badge"
>
<GreenPass
style={
Object {
"height": "15px",
}
}
/>
</span>
</li> </li>
<li <li
className="map-challenge-title" className="map-challenge-title"
@ -80,6 +91,17 @@ exports[`<Block expanded snapshot: block-expanded 1`] = `
> >
Challenge Two Challenge Two
</Unknown> </Unknown>
<span
className="badge map-badge"
>
<GreenPass
style={
Object {
"height": "15px",
}
}
/>
</span>
</li> </li>
</ul> </ul>
</li> </li>

View File

@ -25,6 +25,7 @@ exports[`<SuperBlock /> expanded snapshot: superBlock-expanded 1`] = `
"blockName": "Block A", "blockName": "Block A",
"slug": "/super-block-one/block-a/challenge-one", "slug": "/super-block-one/block-a/challenge-one",
}, },
"id": "a",
"isPrivate": false, "isPrivate": false,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block One", "superBlock": "Super Block One",
@ -37,6 +38,7 @@ exports[`<SuperBlock /> expanded snapshot: superBlock-expanded 1`] = `
"blockName": "Block A", "blockName": "Block A",
"slug": "/super-block-one/block-a/challenge-two", "slug": "/super-block-one/block-a/challenge-two",
}, },
"id": "b",
"isPrivate": false, "isPrivate": false,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block One", "superBlock": "Super Block One",
@ -67,6 +69,7 @@ exports[`<SuperBlock /> expanded snapshot: superBlock-expanded 1`] = `
"blockName": "Block B", "blockName": "Block B",
"slug": "/super-block-one/block-b/challenge-one", "slug": "/super-block-one/block-b/challenge-one",
}, },
"id": "c",
"isPrivate": false, "isPrivate": false,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block One", "superBlock": "Super Block One",
@ -79,6 +82,7 @@ exports[`<SuperBlock /> expanded snapshot: superBlock-expanded 1`] = `
"blockName": "Block B", "blockName": "Block B",
"slug": "/super-block-one/block-b/challenge-two", "slug": "/super-block-one/block-b/challenge-two",
}, },
"id": "d",
"isPrivate": false, "isPrivate": false,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block One", "superBlock": "Super Block One",
@ -109,6 +113,7 @@ exports[`<SuperBlock /> expanded snapshot: superBlock-expanded 1`] = `
"blockName": "Block C", "blockName": "Block C",
"slug": "/super-block-one/block-c/challenge-one", "slug": "/super-block-one/block-c/challenge-one",
}, },
"id": "e",
"isPrivate": true, "isPrivate": true,
"isRequired": false, "isRequired": false,
"superBlock": "Super Block One", "superBlock": "Super Block One",

View File

@ -1,6 +1,8 @@
/* global graphql */ /* global graphql */
import React, { Fragment, PureComponent } from 'react'; import React, { Fragment, PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import ga from '../analytics'; import ga from '../analytics';
@ -8,6 +10,7 @@ import ga from '../analytics';
import { AllChallengeNode } from '../redux/propTypes'; import { AllChallengeNode } from '../redux/propTypes';
import Header from '../components/Header'; import Header from '../components/Header';
import MapModal from '../components/MapModal'; import MapModal from '../components/MapModal';
import { fetchUser } from '../redux/app';
import './global.css'; import './global.css';
import 'react-reflex/styles.css'; import 'react-reflex/styles.css';
@ -37,11 +40,22 @@ const metaKeywords = [
'programming' 'programming'
]; ];
const mapStateToProps = () => ({});
const mapDispatchToProps = dispatch =>
bindActionCreators({ fetchUser }, dispatch);
const propTypes = {
children: PropTypes.func,
data: AllChallengeNode,
fetchUser: PropTypes.func.isRequired
};
class Layout extends PureComponent { class Layout extends PureComponent {
state = { state = {
location: '' location: ''
}; };
componentDidMount() { componentDidMount() {
this.props.fetchUser();
const url = window.location.pathname + window.location.search; const url = window.location.pathname + window.location.search;
ga.pageview(url); ga.pageview(url);
/* eslint-disable react/no-did-mount-set-state */ /* eslint-disable react/no-did-mount-set-state */
@ -99,12 +113,9 @@ class Layout extends PureComponent {
} }
} }
Layout.propTypes = { Layout.propTypes = propTypes;
children: PropTypes.func,
data: AllChallengeNode
};
export default Layout; export default connect(mapStateToProps, mapDispatchToProps)(Layout);
export const query = graphql` export const query = graphql`
query LayoutQuery { query LayoutQuery {
@ -118,6 +129,7 @@ export const query = graphql`
slug slug
blockName blockName
} }
id
block block
title title
isRequired isRequired

View File

@ -0,0 +1,31 @@
import { of } from 'rxjs/observable/of';
import { ofType } from 'redux-observable';
import { types, fetchUserComplete } from './';
import {
switchMap,
filter,
map,
catchError,
defaultIfEmpty
} from 'rxjs/operators';
import { jwt } from '../cookieVaules';
function fetchUserEpic(action$, _, { services }) {
return action$.pipe(
ofType(types.fetchUser),
filter(() => !!jwt),
switchMap(() => {
return services.readService$({ service: 'user' }).pipe(
filter(({ entities, result }) => entities && !!result),
map(fetchUserComplete),
defaultIfEmpty({ type: 'no-user' }),
catchError(err => {
console.log(err);
return of({ type: 'fetch-user-error' });
})
);
})
);
}
export default fetchUserEpic;

View File

@ -1,34 +1,12 @@
import { createAction, handleActions } from 'redux-actions'; import { createAction, handleActions } from 'redux-actions';
import { createTypes } from '../../../utils/stateManagment'; import { createTypes } from '../../../utils/stateManagment';
import { types as challenge } from '../../templates/Challenges/redux';
import fecthUserEpic from './fetch-user-epic';
const ns = 'app'; const ns = 'app';
function userIdentReplacer(state) { export const epics = [fecthUserEpic];
return {
...state,
[ns]: {
...state[ns],
user: {
...state[ns].user,
about: '**blank**',
email: '**blank**',
facebook: '**blank**',
githubProfile: '**blank**',
linkedin: '**blank**',
location: '**blank**',
name: '**blank**',
picture: '**blank**',
portfolio: '**blank**',
twitter: '**blank**',
username: '**blank**',
website: '**blank**'
}
}
};
}
export const epics = [];
export const types = createTypes( export const types = createTypes(
[ [
@ -42,6 +20,7 @@ export const types = createTypes(
); );
const initialState = { const initialState = {
appUsername: '',
isSignedIn: false, isSignedIn: false,
user: {}, user: {},
showMapModal: false showMapModal: false
@ -57,15 +36,20 @@ export const updateUserSignedIn = createAction(types.updateUserSignedIn);
export const isMapModalOpenSelector = state => state[ns].showMapModal; export const isMapModalOpenSelector = state => state[ns].showMapModal;
export const isSignedInSelector = state => state[ns].isSignedIn; export const isSignedInSelector = state => state[ns].isSignedIn;
export const userSelector = state => state[ns].user; export const userSelector = state => state[ns].user || {};
export const completedChallengesSelector = state =>
export const allAppDataSelector = state => userIdentReplacer(state); state[ns].user.completedChallenges || [];
export const reducer = handleActions( export const reducer = handleActions(
{ {
[types.fetchUserComplete]: (state, { payload }) => ({ [types.fetchUserComplete]: (
state,
{ payload: { entities: { user }, result } }
) => ({
...state, ...state,
user: payload appUsername: result,
user: user[result],
isSignedIn: !!Object.keys(user).length
}), }),
[types.toggleMapModal]: state => ({ [types.toggleMapModal]: state => ({
...state, ...state,
@ -74,6 +58,16 @@ export const reducer = handleActions(
[types.updateUserSignedIn]: (state, { payload }) => ({ [types.updateUserSignedIn]: (state, { payload }) => ({
...state, ...state,
isSignedIn: payload isSignedIn: payload
}),
[challenge.submitComplete]: (state, { payload: { points, id } }) => ({
...state,
user: {
...state.user,
completedChallenges:
points === state.user.points
? state.user.completedChallenges
: [...state.user.completedChallengesSelector, { id }]
}
}) })
}, },
initialState initialState

View File

@ -0,0 +1,5 @@
import cookies from 'browser-cookies';
export const _csrf = typeof window !== 'undefined' && cookies.get('_csrf');
export const jwt =
typeof window !== 'undefined' && cookies.get('jwt_access_token');

View File

@ -0,0 +1,41 @@
import { Observable } from 'rxjs';
import Fetchr from 'fetchr';
function callbackObserver(observer) {
return (err, res) => {
if (err) {
return observer.error(err);
}
observer.next(res);
return observer.complete();
};
}
export default function servicesCreator(options) {
const services = new Fetchr(options);
return {
readService$({ service: resource, params = {} }) {
return Observable.create(observer =>
services
.read(resource)
.params(params)
.end(callbackObserver(observer))
);
}
};
}
// createService$({ service: resource, params, body, config }) {
// return Observable.create(observer => {
// services.create(
// resource,
// params,
// body,
// config,
// callbackObserver(observer)
// );
// return Subscription.create(() => observer.dispose());
// });
// }

View File

@ -15,6 +15,14 @@ import {
epics as challengeEpics epics as challengeEpics
} from '../templates/Challenges/redux'; } from '../templates/Challenges/redux';
import { reducer as map } from '../components/Map/redux'; import { reducer as map } from '../components/Map/redux';
import servicesCreator from './createServices';
import { _csrf } from './cookieVaules';
const serviceOptions = {
context: _csrf ? { _csrf } : {},
xhrPath: '/external/services',
xhrTimeout: 15000
};
const rootReducer = combineReducers({ const rootReducer = combineReducers({
app, app,
@ -29,7 +37,8 @@ const rootEpic = combineEpics(analyticsEpic, ...appEpics, ...challengeEpics);
const epicMiddleware = createEpicMiddleware(rootEpic, { const epicMiddleware = createEpicMiddleware(rootEpic, {
dependencies: { dependencies: {
window: typeof window !== 'undefined' ? window : {}, window: typeof window !== 'undefined' ? window : {},
document: typeof window !== 'undefined' ? document : {} document: typeof window !== 'undefined' ? document : {},
services: servicesCreator(serviceOptions)
} }
}); });

View File

@ -71,7 +71,6 @@ class Editor extends PureComponent {
focusEditor(e) { focusEditor(e) {
// e key to focus editor // e key to focus editor
if (e.keyCode === 69) { if (e.keyCode === 69) {
console.log('focusing');
this._editor.focus(); this._editor.focus();
} }
} }

View File

@ -92,12 +92,14 @@ class ShowClassic extends PureComponent {
initTests, initTests,
updateChallengeMeta, updateChallengeMeta,
updateSuccessMessage, updateSuccessMessage,
data: { challengeNode: { files, title, fields: { tests } } }, data: {
challengeNode: { files, title, fields: { tests }, challengeType }
},
pathContext: { challengeMeta } pathContext: { challengeMeta }
} = this.props; } = this.props;
createFiles(files); createFiles(files);
initTests(tests); initTests(tests);
updateChallengeMeta({ ...challengeMeta, title }); updateChallengeMeta({ ...challengeMeta, title, challengeType });
updateSuccessMessage(randomCompliment()); updateSuccessMessage(randomCompliment());
challengeMounted(challengeMeta.id); challengeMounted(challengeMeta.id);
} }
@ -111,7 +113,12 @@ class ShowClassic extends PureComponent {
updateChallengeMeta, updateChallengeMeta,
updateSuccessMessage, updateSuccessMessage,
data: { data: {
challengeNode: { files, title: currentTitle, fields: { tests } } challengeNode: {
files,
title: currentTitle,
fields: { tests },
challengeType
}
}, },
pathContext: { challengeMeta } pathContext: { challengeMeta }
} = this.props; } = this.props;
@ -119,7 +126,11 @@ class ShowClassic extends PureComponent {
updateSuccessMessage(randomCompliment()); updateSuccessMessage(randomCompliment());
createFiles(files); createFiles(files);
initTests(tests); initTests(tests);
updateChallengeMeta({ ...challengeMeta, title: currentTitle }); updateChallengeMeta({
...challengeMeta,
title: currentTitle,
challengeType
});
challengeMounted(challengeMeta.id); challengeMounted(challengeMeta.id);
} }
} }
@ -142,14 +153,26 @@ class ShowClassic extends PureComponent {
const editors = Object.keys(files) const editors = Object.keys(files)
.map(key => files[key]) .map(key => files[key])
.map((file, index) => ( .map((file, index) => (
<ReflexContainer orientation='horizontal' key={file.key + index}> <ReflexContainer key={file.key + index} orientation='horizontal'>
{index !== 0 && <ReflexSplitter />} {index !== 0 && <ReflexSplitter />}
<ReflexElement flex={1} propagateDimensions={true} renderOnResize={true} renderOnResizeRate={20}> <ReflexElement
flex={1}
propagateDimensions={true}
renderOnResize={true}
renderOnResizeRate={20}
>
<Editor {...file} fileKey={file.key} /> <Editor {...file} fileKey={file.key} />
</ReflexElement> </ReflexElement>
{index + 1 === Object.keys(files).length && <ReflexSplitter propagate={true} />} {index + 1 === Object.keys(files).length && (
<ReflexSplitter propagate={true} />
)}
{index + 1 === Object.keys(files).length ? ( {index + 1 === Object.keys(files).length ? (
<ReflexElement flex={0.25} propagateDimensions={true} renderOnResize={true} renderOnResizeRate={20}> <ReflexElement
flex={0.25}
propagateDimensions={true}
renderOnResize={true}
renderOnResizeRate={20}
>
<Output <Output
defaultOutput={` defaultOutput={`
/** /**
@ -181,9 +204,7 @@ class ShowClassic extends PureComponent {
/> />
</ReflexElement> </ReflexElement>
<ReflexSplitter /> <ReflexSplitter />
<ReflexElement flex={1}> <ReflexElement flex={1}>{editors}</ReflexElement>
{editors}
</ReflexElement>
<ReflexSplitter /> <ReflexSplitter />
<ReflexElement flex={0.5}> <ReflexElement flex={0.5}>
{showPreview ? <Preview className='full-height' /> : null} {showPreview ? <Preview className='full-height' /> : null}

View File

@ -48,10 +48,10 @@ class Output extends PureComponent {
<base href='/' /> <base href='/' />
<MonacoEditor <MonacoEditor
className='challenge-output' className='challenge-output'
editorDidMount={::this.editorDidMount}
height={height} height={height}
options={options} options={options}
value={output ? output : defaultOutput} value={output ? output : defaultOutput}
editorDidMount={::this.editorDidMount}
/> />
</Fragment> </Fragment>
); );

View File

@ -2,16 +2,17 @@ import React from 'react';
const propTypes = {}; const propTypes = {};
function GreenPass() { function GreenPass(props) {
return ( return (
<svg <svg
height='50' height='50'
viewBox='0 0 200 200' viewBox='0 0 200 200'
width='50' width='50'
xmlns='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg'
{...props}
> >
<g> <g>
<title>Test Passed</title> <title>Passed</title>
<circle <circle
cx='100' cx='100'
cy='99' cy='99'

View File

@ -11,7 +11,8 @@ import {
const propTypes = { const propTypes = {
isFrontEnd: PropTypes.bool, isFrontEnd: PropTypes.bool,
isSubmitting: PropTypes.bool, isSubmitting: PropTypes.bool,
openModal: PropTypes.func.isRequired openModal: PropTypes.func.isRequired,
updateProjectForm: PropTypes.func.isRequired
}; };
const frontEndFields = ['solution']; const frontEndFields = ['solution'];
@ -35,9 +36,15 @@ const options = {
}; };
export class ProjectForm extends PureComponent { export class ProjectForm extends PureComponent {
componentDidMount() {
this.props.updateProjectForm({});
}
componentDidUpdate() {
this.props.updateProjectForm({});
}
handleSubmit = values => { handleSubmit = values => {
this.props.openModal('completion'); this.props.openModal('completion');
console.log(values); this.props.updateProjectForm(values);
}; };
render() { render() {

View File

@ -18,7 +18,8 @@ import {
updateChallengeMeta, updateChallengeMeta,
createFiles, createFiles,
updateSuccessMessage, updateSuccessMessage,
openModal openModal,
updateProjectFormValues
} from '../redux'; } from '../redux';
import { frontEndProject } from '../../../../utils/challengeTypes'; import { frontEndProject } from '../../../../utils/challengeTypes';
@ -30,6 +31,7 @@ const mapDispatchToProps = dispatch =>
{ {
updateChallengeMeta, updateChallengeMeta,
createFiles, createFiles,
updateProjectFormValues,
updateSuccessMessage, updateSuccessMessage,
openCompletionModal: () => openModal('completion') openCompletionModal: () => openModal('completion')
}, },
@ -46,6 +48,7 @@ const propTypes = {
challengeMeta: PropTypes.object challengeMeta: PropTypes.object
}), }),
updateChallengeMeta: PropTypes.func.isRequired, updateChallengeMeta: PropTypes.func.isRequired,
updateProjectFormValues: PropTypes.func.isRequired,
updateSuccessMessage: PropTypes.func.isRequired updateSuccessMessage: PropTypes.func.isRequired
}; };
@ -53,21 +56,21 @@ export class Project extends PureComponent {
componentDidMount() { componentDidMount() {
const { const {
createFiles, createFiles,
data: { challengeNode: { title } }, data: { challengeNode: { title, challengeType } },
pathContext: { challengeMeta }, pathContext: { challengeMeta },
updateChallengeMeta, updateChallengeMeta,
updateSuccessMessage updateSuccessMessage
} = this.props; } = this.props;
createFiles({}); createFiles({});
updateSuccessMessage(randomCompliment()); updateSuccessMessage(randomCompliment());
return updateChallengeMeta({ ...challengeMeta, title }); return updateChallengeMeta({ ...challengeMeta, title, challengeType });
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { data: { challengeNode: { title: prevTitle } } } = prevProps; const { data: { challengeNode: { title: prevTitle } } } = prevProps;
const { const {
createFiles, createFiles,
data: { challengeNode: { title: currentTitle } }, data: { challengeNode: { title: currentTitle, challengeType } },
pathContext: { challengeMeta }, pathContext: { challengeMeta },
updateChallengeMeta, updateChallengeMeta,
updateSuccessMessage updateSuccessMessage
@ -75,7 +78,11 @@ export class Project extends PureComponent {
updateSuccessMessage(randomCompliment()); updateSuccessMessage(randomCompliment());
if (prevTitle !== currentTitle) { if (prevTitle !== currentTitle) {
createFiles({}); createFiles({});
updateChallengeMeta({ ...challengeMeta, title: currentTitle }); updateChallengeMeta({
...challengeMeta,
title: currentTitle,
challengeType
});
} }
} }
@ -90,9 +97,11 @@ export class Project extends PureComponent {
guideUrl guideUrl
} }
}, },
openCompletionModal openCompletionModal,
updateProjectFormValues
} = this.props; } = this.props;
const isFrontEnd = challengeType === frontEndProject; const isFrontEnd = challengeType === frontEndProject;
const blockNameTitle = `${blockName} - ${title}`; const blockNameTitle = `${blockName} - ${title}`;
return ( return (
<Fragment> <Fragment>
@ -108,6 +117,7 @@ export class Project extends PureComponent {
<ProjectForm <ProjectForm
isFrontEnd={isFrontEnd} isFrontEnd={isFrontEnd}
openModal={openCompletionModal} openModal={openCompletionModal}
updateProjectForm={updateProjectFormValues}
/> />
</div> </div>
<CompletionModal /> <CompletionModal />

View File

@ -1,180 +1,151 @@
import { of } from 'rxjs/observable/of'; import { of } from 'rxjs/observable/of';
import { switchMap } from 'rxjs/operators'; import { empty } from 'rxjs/observable/empty';
import {
switchMap,
retry,
map,
catchError,
concat,
filter
} from 'rxjs/operators';
import { ofType } from 'redux-observable'; import { ofType } from 'redux-observable';
import { push } from 'react-router-redux'; import { push } from 'react-router-redux';
import { _csrf as csrfToken } from '../../../redux/cookieVaules';
import { import {
// backendFormValuesSelector, backendFormValuesSelector,
// frontendProjectFormValuesSelector, projectFormVaulesSelector,
// backendProjectFormValuesSelector, submitComplete,
// challengeMetaSelector,
// moveToNextChallenge,
// submitChallengeComplete,
// testsSelector,
types, types,
closeModal, challengeMetaSelector,
challengeMetaSelector challengeTestsSelector,
closeModal
} from './'; } from './';
import { userSelector, isSignedInSelector } from '../../../redux/app';
// import { import { postJSON$ } from '../utils/ajax-stream';
// challengeSelector, import { challengeTypes, submitTypes } from '../../../../utils/challengeTypes';
// createErrorObservable,
// csrfSelector,
// userSelector
// } from '../../../redux';
// import { filesSelector } from '../../../files';
// import { backEndProject } from '../../../utils/challengeTypes.js';
// import { makeToast } from '../../../Toasts/redux';
// import { postJSON$ } from '../../../../utils/ajax-stream.js';
// function postChallenge(url, username, _csrf, challengeInfo) { function postChallenge(url, username, _csrf, challengeInfo) {
// return Observable.if( const body = { ...challengeInfo, _csrf };
// () => !!username, const saveChallenge = postJSON$(url, body).pipe(
// Observable.defer(() => { retry(3),
// const body = { ...challengeInfo, _csrf }; map(({ points }) =>
// const saveChallenge = postJSON$(url, body) submitComplete({
// .retry(3) username,
// .map(({ points, lastUpdated, completedDate }) => points,
// submitChallengeComplete(username, points, { ...challengeInfo
// ...challengeInfo, })
// lastUpdated, ),
// completedDate catchError(err => {
// }) console.error(err);
// ) return of({ type: 'here is an error' });
// .catch(createErrorObservable); })
// const challengeCompleted = Observable.of(moveToNextChallenge()); );
// return Observable.merge(saveChallenge, challengeCompleted).startWith({ return saveChallenge;
// type: types.submitChallenge.start }
// });
// }),
// Observable.of(moveToNextChallenge())
// );
// }
// function submitModern(type, state) { function submitModern(type, state) {
// const tests = testsSelector(state); const tests = challengeTestsSelector(state);
// if (tests.length > 0 && tests.every(test => test.pass && !test.err)) { if (tests.length > 0 && tests.every(test => test.pass && !test.err)) {
// if (type === types.checkChallenge) { if (type === types.checkChallenge) {
// return Observable.empty(); return of({ type: 'this was a check challenge' });
// } }
// if (type === types.submitChallenge.toString()) { if (type === types.submitChallenge) {
// const { id } = challengeSelector(state); const { id } = challengeMetaSelector(state);
// const files = filesSelector(state); const { username } = userSelector(state);
// const { username } = userSelector(state); return postChallenge(
// const csrfToken = csrfSelector(state); '/external/modern-challenge-completed',
// return postChallenge( username,
// '/modern-challenge-completed', username, csrfToken, { csrfToken,
// id, {
// files id
// }); }
// } );
// } }
// return Observable.of(makeToast({ message: 'Keep trying.' })); }
// } return empty();
}
// function submitProject(type, state) { function submitProject(type, state) {
// if (type === types.checkChallenge) { if (type === types.checkChallenge) {
// return Observable.empty(); return empty();
// } }
// const {
// solution: frontEndSolution = '' } = frontendProjectFormValuesSelector(
// state
// );
// const {
// solution: backendSolution = '',
// githubLink = ''
// } = backendProjectFormValuesSelector(state);
// const solution = frontEndSolution ? frontEndSolution : backendSolution;
// const { id, challengeType } = challengeSelector(state);
// const { username } = userSelector(state);
// const csrfToken = csrfSelector(state);
// const challengeInfo = { id, challengeType, solution };
// if (challengeType === backEndProject) {
// challengeInfo.githubLink = githubLink;
// }
// return Observable.merge(
// postChallenge('/project-completed', username, csrfToken, challengeInfo),
// Observable.of(closeChallengeModal())
// );
// }
// function submitSimpleChallenge(type, state) { const { solution, githubLink } = projectFormVaulesSelector(state);
// const { id } = challengeSelector(state); const { id, challengeType } = challengeMetaSelector(state);
// const { username } = userSelector(state); const { username } = userSelector(state);
// const csrfToken = csrfSelector(state); const challengeInfo = { id, challengeType, solution };
// const challengeInfo = { id }; if (challengeType === challengeTypes.backEndProject) {
// return postChallenge( challengeInfo.githubLink = githubLink;
// '/challenge-completed', }
// username, return postChallenge(
// csrfToken, '/external/project-completed',
// challengeInfo username,
// ); csrfToken,
// } challengeInfo
);
}
// function submitBackendChallenge(type, state) { function submitBackendChallenge(type, state) {
// const tests = testsSelector(state); const tests = challengeTestsSelector(state);
// if ( if (tests.length > 0 && tests.every(test => test.pass && !test.err)) {
// type === types.checkChallenge && if (type === types.submitChallenge) {
// tests.length > 0 && const { id } = challengeMetaSelector(state);
// tests.every(test => test.pass && !test.err) const { username } = userSelector(state);
// ) { const { solution: { value: solution } } = backendFormValuesSelector(
// /* state
// return Observable.of( );
// makeToast({ const challengeInfo = { id, solution };
// message: `${randomCompliment()} Go to your next challenge.`, return postChallenge(
// action: 'Submit', '/external/backend-challenge-completed',
// actionCreator: 'submitChallenge', username,
// timeout: 10000 csrfToken,
// }) challengeInfo
// ); );
// */ }
// return Observable.empty(); }
// } return empty();
// if (type === types.submitChallenge.toString()) { }
// const { id } = challengeSelector(state);
// const { username } = userSelector(state);
// const csrfToken = csrfSelector(state);
// const { solution } = backendFormValuesSelector(state);
// const challengeInfo = { id, solution };
// return postChallenge(
// '/backend-challenge-completed',
// username,
// csrfToken,
// challengeInfo
// );
// }
// return Observable.of(makeToast({ message: 'Keep trying.' }));
// }
// const submitters = { const submitters = {
// tests: submitModern, tests: submitModern,
// backend: submitBackendChallenge, backend: submitBackendChallenge,
// step: submitSimpleChallenge, 'project.frontEnd': submitProject,
// video: submitSimpleChallenge, 'project.backEnd': submitProject
// quiz: submitSimpleChallenge, };
// 'project.frontEnd': submitProject,
// 'project.backEnd': submitProject,
// 'project.simple': submitSimpleChallenge
// };
export default function completionEpic(action$, { getState }) { export default function completionEpic(action$, { getState }) {
return action$.pipe( return action$.pipe(
ofType(types.submitChallenge), ofType(types.submitChallenge),
switchMap(({ type }) => { switchMap(({ type }) => {
const { nextChallengePath, introPath } = challengeMetaSelector( const state = getState();
getState() const meta = challengeMetaSelector(state);
); const { nextChallengePath, introPath, challengeType } = meta;
// const state = getState(); const next = of(push(introPath ? introPath : nextChallengePath));
// const { submitType } = challengeMetaSelector(state); const closeChallengeModal = of(closeModal('completion'));
// const submitter = submitters[submitType] || (() => Observable.empty()); let submitter = () => of({type: 'no-user-signed-in'});
if (
!(challengeType in submitTypes) ||
!(submitTypes[challengeType] in submitters)
) {
throw new Error(
'Unable to find the correct submit function for challengeType ' +
challengeType
);
}
if (isSignedInSelector(state)) {
submitter = submitters[submitTypes[challengeType]];
}
return type === types.submitChallenge
? of( return submitter(type, state).pipe(
closeModal('completion'), concat(next),
push(introPath ? introPath : nextChallengePath) concat(closeChallengeModal),
) filter(Boolean)
: of({ type: 'PONG' }); );
}) })
); );
} }

View File

@ -27,6 +27,7 @@ const initialState = {
help: false, help: false,
reset: false reset: false
}, },
projectFormVaules: {},
successMessage: 'Happy Coding!' successMessage: 'Happy Coding!'
}; };
@ -49,6 +50,7 @@ export const types = createTypes(
'updateChallengeMeta', 'updateChallengeMeta',
'updateFile', 'updateFile',
'updateJSEnabled', 'updateJSEnabled',
'updateProjectFormValues',
'updateSuccessMessage', 'updateSuccessMessage',
'updateTests', 'updateTests',
@ -95,6 +97,9 @@ export const updateChallengeMeta = createAction(types.updateChallengeMeta);
export const updateFile = createAction(types.updateFile); export const updateFile = createAction(types.updateFile);
export const updateConsole = createAction(types.updateConsole); export const updateConsole = createAction(types.updateConsole);
export const updateJSEnabled = createAction(types.updateJSEnabled); export const updateJSEnabled = createAction(types.updateJSEnabled);
export const updateProjectFormValues = createAction(
types.updateProjectFormValues
);
export const updateSuccessMessage = createAction(types.updateSuccessMessage); export const updateSuccessMessage = createAction(types.updateSuccessMessage);
export const lockCode = createAction(types.lockCode); export const lockCode = createAction(types.lockCode);
@ -116,7 +121,6 @@ export const resetChallenge = createAction(types.resetChallenge);
export const submitChallenge = createAction(types.submitChallenge); export const submitChallenge = createAction(types.submitChallenge);
export const submitComplete = createAction(types.submitComplete); export const submitComplete = createAction(types.submitComplete);
export const backendFormValuesSelector = state => state.form[backendNS];
export const challengeFilesSelector = state => state[ns].challengeFiles; export const challengeFilesSelector = state => state[ns].challengeFiles;
export const challengeMetaSelector = state => state[ns].challengeMeta; export const challengeMetaSelector = state => state[ns].challengeMeta;
export const challengeTestsSelector = state => state[ns].challengeTests; export const challengeTestsSelector = state => state[ns].challengeTests;
@ -129,6 +133,10 @@ export const isResetModalOpenSelector = state => state[ns].modal.reset;
export const isJSEnabledSelector = state => state[ns].isJSEnabled; export const isJSEnabledSelector = state => state[ns].isJSEnabled;
export const successMessageSelector = state => state[ns].successMessage; export const successMessageSelector = state => state[ns].successMessage;
export const backendFormValuesSelector = state => state.form[backendNS];
export const projectFormVaulesSelector = state =>
state[ns].projectFormVaules || {};
export const reducer = handleActions( export const reducer = handleActions(
{ {
[types.createFiles]: (state, { payload }) => ({ [types.createFiles]: (state, { payload }) => ({
@ -195,6 +203,10 @@ export const reducer = handleActions(
})), })),
consoleOut: '' consoleOut: ''
}), }),
[types.updateProjectFormValues]: (state, { payload }) => ({
...state,
projectFormVaules: payload
}),
[types.lockCode]: state => ({ [types.lockCode]: state => ({
...state, ...state,

View File

@ -18,6 +18,7 @@
import debugFactory from 'debug'; import debugFactory from 'debug';
import { Observable, noop } from 'rxjs'; import { Observable, noop } from 'rxjs';
import { map } from 'rxjs/operators';
const debug = debugFactory('fcc:ajax$'); const debug = debugFactory('fcc:ajax$');
const root = typeof window !== 'undefined' ? window : {}; const root = typeof window !== 'undefined' ? window : {};
@ -279,7 +280,7 @@ export function postJSON$(url, body) {
Accept: 'application/json' Accept: 'application/json'
}, },
normalizeError: (e, xhr) => parseXhrResponse('json', xhr) normalizeError: (e, xhr) => parseXhrResponse('json', xhr)
}).map(({ response }) => response); }).pipe(map(({ response }) => response));
} }
// Creates an observable sequence from an Ajax GET Request with the body. // Creates an observable sequence from an Ajax GET Request with the body.

View File

@ -145,7 +145,6 @@ export function setError(error, poly) {
error error
); );
checkPoly(poly); checkPoly(poly);
console.log('SET ERROR!!!!!!!!!!!!!!!!!');
return { return {
...poly, ...poly,
error error

View File

@ -0,0 +1 @@
/external/* https://beta.freecodecamp.org/external/:splat 200

View File

@ -93,8 +93,8 @@
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@freecodecamp/curriculum@^1.0.0": "@freecodecamp/curriculum@^1.0.0":
version "1.0.0" version "1.0.1"
resolved "https://registry.yarnpkg.com/@freecodecamp/curriculum/-/curriculum-1.0.0.tgz#5631db2fc7e28e77b996d8048ccf481e167e808e" resolved "https://registry.yarnpkg.com/@freecodecamp/curriculum/-/curriculum-1.0.1.tgz#e78ce190fac848cf78ea577d394440fbda7f066e"
"@sinonjs/formatio@^2.0.0": "@sinonjs/formatio@^2.0.0":
version "2.0.0" version "2.0.0"
@ -1707,6 +1707,10 @@ brorand@^1.0.1:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
browser-cookies@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/browser-cookies/-/browser-cookies-1.2.0.tgz#fca3ffb9b6a63aadc4d8c0999c6b57d0fa7d29b5"
browser-process-hrtime@^0.1.2: browser-process-hrtime@^0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz#425d68a58d3447f02a04aa894187fce8af8b7b8e" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz#425d68a58d3447f02a04aa894187fce8af8b7b8e"
@ -3319,7 +3323,7 @@ es6-promise@3.2.1:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.2.1.tgz#ec56233868032909207170c39448e24449dd1fc4" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.2.1.tgz#ec56233868032909207170c39448e24449dd1fc4"
es6-promise@^4.0.5, es6-promise@^4.1.0: es6-promise@^4.0.2, es6-promise@^4.0.5, es6-promise@^4.1.0:
version "4.2.4" version "4.2.4"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29"
@ -3783,6 +3787,17 @@ fbjs@^0.8.14, fbjs@^0.8.16, fbjs@^0.8.9:
setimmediate "^1.0.5" setimmediate "^1.0.5"
ua-parser-js "^0.7.9" ua-parser-js "^0.7.9"
fetchr@^0.5.37:
version "0.5.37"
resolved "https://registry.yarnpkg.com/fetchr/-/fetchr-0.5.37.tgz#484dee9f47215a27d5f19b96165be24e0248de62"
dependencies:
debug "^2.6.3"
es6-promise "^4.0.2"
fumble "^0.1.0"
lodash "^4.0.1"
object-assign "^4.0.1"
xhr "^2.4.0"
figures@^2.0.0: figures@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
@ -4092,6 +4107,13 @@ fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
mkdirp ">=0.5 0" mkdirp ">=0.5 0"
rimraf "2" rimraf "2"
fumble@^0.1.0:
version "0.1.3"
resolved "https://registry.yarnpkg.com/fumble/-/fumble-0.1.3.tgz#00c7a97041b85fabcdc2c3bab730b3c90c3b4084"
dependencies:
camelcase "^3.0.0"
http-status "^0.2.0"
function-bind@^1.0.2, function-bind@^1.1.0, function-bind@^1.1.1: function-bind@^1.0.2, function-bind@^1.1.0, function-bind@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@ -5042,6 +5064,10 @@ http-signature@~1.2.0:
jsprim "^1.2.2" jsprim "^1.2.2"
sshpk "^1.7.0" sshpk "^1.7.0"
http-status@^0.2.0:
version "0.2.5"
resolved "https://registry.yarnpkg.com/http-status/-/http-status-0.2.5.tgz#976f91077ea7bfc15277cbcf8c80c4d5c51b49b0"
https-browserify@0.0.1: https-browserify@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
@ -6482,6 +6508,10 @@ lodash@4.11.1:
version "4.11.1" version "4.11.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.11.1.tgz#a32106eb8e2ec8e82c241611414773c9df15f8bc" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.11.1.tgz#a32106eb8e2ec8e82c241611414773c9df15f8bc"
lodash@^4.0.1:
version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
log-symbols@^1.0.2: log-symbols@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
@ -11489,7 +11519,7 @@ xdg-basedir@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
xhr@^2.4.1: xhr@^2.4.0, xhr@^2.4.1:
version "2.5.0" version "2.5.0"
resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.5.0.tgz#bed8d1676d5ca36108667692b74b316c496e49dd" resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.5.0.tgz#bed8d1676d5ca36108667692b74b316c496e49dd"
dependencies: dependencies: