Merge branch 'master' into remove-angular
Conflicts: bower.json client/main.js server/views/coursewares/showZiplineOrBasejump.jade server/views/partials/scripts.jade
This commit is contained in:
@ -224,8 +224,8 @@
|
|||||||
"no-plusplus": 0,
|
"no-plusplus": 0,
|
||||||
|
|
||||||
"react/display-name": 1,
|
"react/display-name": 1,
|
||||||
"react/jsx-boolean-value": 1,
|
"react/jsx-boolean-value": [1, "always"],
|
||||||
"react/jsx-quotes": [1, "single", "avoid-escape"],
|
"jsx-quotes": [1, "prefer-single"],
|
||||||
"react/jsx-no-undef": 1,
|
"react/jsx-no-undef": 1,
|
||||||
"react/jsx-sort-props": 1,
|
"react/jsx-sort-props": 1,
|
||||||
"react/jsx-uses-react": 1,
|
"react/jsx-uses-react": 1,
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -44,6 +44,8 @@ public/js/commonFramework*
|
|||||||
public/js/sandbox*
|
public/js/sandbox*
|
||||||
public/js/iFrameScripts*
|
public/js/iFrameScripts*
|
||||||
public/js/plugin*
|
public/js/plugin*
|
||||||
|
public/js/vendor*
|
||||||
|
public/js/faux*
|
||||||
public/css/main*
|
public/css/main*
|
||||||
|
|
||||||
server/rev-manifest.json
|
server/rev-manifest.json
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
|
|
||||||
node_js:
|
node_js:
|
||||||
- 'node'
|
- '4.2.1'
|
||||||
- '1.6.4'
|
|
||||||
|
|
||||||
sudo: false
|
sudo: false
|
||||||
|
@ -1,12 +1,28 @@
|
|||||||
We're getting a lot of duplicate issues and bug reports that just aren't reporting actual bugs.
|
## Code Not Working?
|
||||||
So, before you submit your issue, please read the [Help I've Found a Bug](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/Help-I've-Found-a-Bug) wiki page.
|
Do not file an issue until you have:
|
||||||
|
|
||||||
|
1. Read [Help I've Found a Bug](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/Help-I've-Found-a-Bug) wiki page and followed the instructions there.
|
||||||
|
2. Asked for confirmation in the appropriate [Help Room](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/Help-Rooms)
|
||||||
|
3. Please *do not* open an issue without a 3rd party confirmation of your problem.
|
||||||
|
|
||||||
|
## I want to help!
|
||||||
We welcome pull requests from Free Code Camp campers (our students) and seasoned JavaScript developers alike! Follow these steps to contribute:
|
We welcome pull requests from Free Code Camp campers (our students) and seasoned JavaScript developers alike! Follow these steps to contribute:
|
||||||
|
|
||||||
1. Check our [public Waffle Board](https://waffle.io/freecodecamp/freecodecamp).
|
1. Check our [public Waffle Board](https://waffle.io/freecodecamp/freecodecamp).
|
||||||
2. Pick an issue that nobody has claimed and start working on it. If your issue isn't on the board, open an issue. If you think you can fix it yourself, start working on it. Feel free to ask for help in our [Gitter](https://gitter.im/FreeCodeCamp/FreeCodeCamp).
|
2. Pick an issue that nobody has claimed and start working on it. If your issue isn't on the board, open an issue. If you think you can fix it yourself, start working on it. Feel free to ask for help in our [General Gitter Chat Room](https://gitter.im/FreeCodeCamp/FreeCodeCamp), or in our [Help Contributors Chat Room](https://gitter.im/FreeCodeCamp/HelpContributors)
|
||||||
3. Fork the project ([Need help with forking a project?](https://help.github.com/articles/fork-a-repo/)). You'll do all of your work on your forked copy.
|
3. You can find issues we are seeking assistance on by searching for the [Help Wanted](https://github.com/FreeCodeCamp/FreeCodeCamp/labels/help%20wanted) tag.
|
||||||
4. Create a branch specific to the issue or feature you are working on. Push your work to that branch. ([Need help with branching?](https://github.com/Kunena/Kunena-Forum/wiki/Create-a-new-branch-with-git-and-manage-branches))
|
|
||||||
5. Name the branch something like `fix/xxx` or `feature/xxx` where `xxx` is a short description of the changes or feature you are attempting to add. For example `fix/email-login` would be a branch where I fix something specific to email login.
|
## Contribution Guidelines
|
||||||
6. You should have [ESLint running in your editor](http://eslint.org/docs/user-guide/integrations.html), and it will highlight anything doesn't conform to [Free Code Camp's JavaScript Style Guide](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/Free-Code-Camp-JavaScript-Style-Guide) (you can find a summary of those rules [here](https://github.com/FreeCodeCamp/FreeCodeCamp/blob/staging/.eslintrc). Please do not ignore any linting errors, as they are meant to **help** you and to ensure a clean and simple code base. Make sure none of your JavaScript is longer than 80 characters per line.
|
|
||||||
7. Once your code is ready, submit a [pull request](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/Pull-Request-Contribute) from your branch to Free Code Camp's `staging` branch. We'll do a quick code review and give you feedback, then iterate from there. It may also be helpful to read about git [rebasing](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/git-rebase).
|
1. Fork the project: [How To Fork And Maintain a Local Instance of Free Code Camp](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/How-To-Fork-And-Maintain-a-Local-Instance-of-Free-Code-Camp)
|
||||||
|
2. Create a branch specific to the issue or feature you are working on. Push your work to that branch. ([Need help with branching?](https://github.com/Kunena/Kunena-Forum/wiki/Create-a-new-branch-with-git-and-manage-branches))
|
||||||
|
3. Name the branch something like `fix/xxx` or `feature/xxx` where `xxx` is a short description of the changes or feature you are attempting to add. For example `fix/email-login` would be a branch where I fix something specific to email login.
|
||||||
|
4. You should have [ESLint running in your editor](http://eslint.org/docs/user-guide/integrations.html), and it will highlight anything doesn't conform to [Free Code Camp's JavaScript Style Guide](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/Free-Code-Camp-JavaScript-Style-Guide) (you can find a summary of those rules [here](https://github.com/FreeCodeCamp/FreeCodeCamp/blob/staging/.eslintrc). Please do not ignore any linting errors, as they are meant to **help** you and to ensure a clean and simple code base. Make sure none of your JavaScript is longer than 80 characters per line.
|
||||||
|
5. Squash your Commits. Ref: [rebasing](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/git-rebase)
|
||||||
|
|
||||||
|
## Create a Pull Request
|
||||||
|
1. Read our [How to Create a Pull Request for Free Code Camp](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/How-To-Create-A-Pull-Request-for-Free-Code-Camp)
|
||||||
|
2. Submit a [pull request](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/Pull-Request-Contribute) from your branch to Free Code Camp's `staging` branch.
|
||||||
|
- The title (also called the subject) of your PR should be descriptive of your changes. i.e. `fix typo in basic-javascript challenge`
|
||||||
|
- If the PR is meant to fix a specific issue, append to the end of your PR's commit message `closes #1337`. This tells GitHub to close that issue if the PR is merged.
|
||||||
|
- Do NOT add issue numbers to the PR's title. i.e. `minor improvements in basic-javascript challenge`
|
||||||
|
46
README.md
46
README.md
@ -4,23 +4,25 @@
|
|||||||
|
|
||||||
[](https://gitter.im/freecodecamp/freecodecamp?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
[](https://gitter.im/freecodecamp/freecodecamp?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
|
|
||||||
Welcome to Free Code Camp's open source codebase!
|
Welcome to Free Code Camp's open source codebase and curriculum!
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
Free Code Camp is an open-source community of busy people who learn to code and build projects for nonprofits.
|
Free Code Camp is an open-source community where you learn to code and help nonprofits.
|
||||||
|
|
||||||
Our campers (students) start by working through our free, self-paced, browser-based curriculum. Next, they build several practice projects. Finally, we pair two campers together with a stakeholder from a nonprofit organization, and help them build the solution the nonprofit has requested.
|
You start by working through our self-paced, browser-based full stack JavaScript curriculum.
|
||||||
|
|
||||||
|
After you complete the first 400 hours worth of challenges (which involves building 10 single-page apps), you'll earn your Front End Development Certification.
|
||||||
|
|
||||||
|
After you complete the second 400 hours worth of challenges (which involves building and deploying 5 full stack apps), you'll earn your Full Stack Development Certification.
|
||||||
|
|
||||||
|
Then we'll pair you with another camper, an agile project manager, and a stakeholder from a nonprofit organization. Together, you'll plan and build an app that helps that nonprofit carry out its mission more effectively.
|
||||||
|
|
||||||
**We help our campers build job-worthy portfolios of real apps used by real people, while helping nonprofits.**
|
**We help our campers build job-worthy portfolios of real apps used by real people, while helping nonprofits.**
|
||||||
|
|
||||||
80% of our campers are over 25, and nearly a fifth of our campers are women.
|
This code is running live at [FreeCodeCamp.com](http://www.FreeCodeCamp.com). We also have [Gitter](https://gitter.im/FreeCodeCamp/FreeCodeCamp), a [Medium publication](http://medium.freecodecamp.com), and even a [Twitch.tv channel](http://twitch.tv/freecodecamp).
|
||||||
|
|
||||||
This code is running live at [FreeCodeCamp.com](http://www.FreeCodeCamp.com). We also have [Gitter](https://gitter.im/FreeCodeCamp/FreeCodeCamp), a [blog](http://blog.freecodecamp.com), and even a [Twitch.tv channel](http://twitch.tv/freecodecamp).
|
|
||||||
|
|
||||||
[Join our community here](http://www.freecodecamp.com/signin).
|
[Join our community here](http://www.freecodecamp.com/signin).
|
||||||
|
|
||||||
*Note: We're currently very close to moving from Express to Loopback. As such, please keep in mind that the instructions here for setting up and running the project do not directly translate to the staging branch. Additionally, the file structure is quite a bit different. As always, the staging branch is the appropriate place to branch off of to fix/add something.*
|
|
||||||
|
|
||||||
Wiki
|
Wiki
|
||||||
------------
|
------------
|
||||||
|
|
||||||
@ -54,22 +56,26 @@ The easiest way to get started is to clone the repository:
|
|||||||
# Get the latest snapshot
|
# Get the latest snapshot
|
||||||
git clone --depth=1 https://github.com/freecodecamp/freecodecamp.git freecodecamp
|
git clone --depth=1 https://github.com/freecodecamp/freecodecamp.git freecodecamp
|
||||||
|
|
||||||
|
# Change directory
|
||||||
cd freecodecamp
|
cd freecodecamp
|
||||||
|
|
||||||
# Install NPM dependencies
|
# Install NPM dependencies
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
|
# Install Gulp globally
|
||||||
|
npm install -g gulp
|
||||||
|
|
||||||
|
# Install Bower globally
|
||||||
|
npm install -g bower
|
||||||
|
|
||||||
# Install Bower dependencies
|
# Install Bower dependencies
|
||||||
bower install
|
bower install
|
||||||
|
|
||||||
# Create a .env file and populate it with the necessary API keys and secrets:
|
# Create a .env file and populate it with the necessary API keys and secrets:
|
||||||
touch .env
|
touch .env
|
||||||
|
|
||||||
# Install Gulp globally
|
|
||||||
npm install -g gulp
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Edit your `.env` file with the following API keys accordingly (if you only use email login, only the `MONGOHQ_URL`, `SESSION_SECRET`, `MANDRILL_USER` and `MANDRILL_PASSWORD` fields are necessary. Keep in mind if you want to use more services you'll have to get your own API keys for those services.
|
Edit your `.env` file with the following API keys accordingly. If you only use email login, only the `MONGOHQ_URL`, `SESSION_SECRET`, `MANDRILL_USER` and `MANDRILL_PASSWORD` fields are necessary. Keep in mind if you want to use more services you'll have to get your own API keys for those services. If you only use a subset or no OAuth2 authentication methods, you may want to remove them from ```server/passport-providers.js``` - otherwise the server will complain about missing clientIDs at launch.
|
||||||
|
|
||||||
```
|
```
|
||||||
MONGOHQ_URL='mongodb://localhost:27017/freecodecamp'
|
MONGOHQ_URL='mongodb://localhost:27017/freecodecamp'
|
||||||
@ -107,20 +113,20 @@ DEBUG=true
|
|||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start the mongo server
|
# Start the mongo server in a seperate terminal
|
||||||
mongod
|
mongod
|
||||||
|
|
||||||
# Create your mongo database.
|
# Initialize Free Code Camp
|
||||||
# Type "mongo" in your terminal to access the mongo shell
|
# This will seed the database for the first time.
|
||||||
use freecodecamp
|
# This command should only be run once.
|
||||||
# Exit the mongo shell with control + d
|
npm run first-time
|
||||||
|
|
||||||
# Seed your database with the challenges
|
|
||||||
node seed/
|
|
||||||
|
|
||||||
# start the application
|
# start the application
|
||||||
gulp
|
gulp
|
||||||
```
|
```
|
||||||
|
Now navigate to your browser and open http://localhost:3001
|
||||||
|
If the app loads, congratulations - you're all set. Otherwise, let us know by opening a GitHub issue and with your error.
|
||||||
|
|
||||||
|
|
||||||
License
|
License
|
||||||
-------
|
-------
|
||||||
|
12
bower.json
12
bower.json
@ -2,10 +2,6 @@
|
|||||||
"name": "freecodecamp",
|
"name": "freecodecamp",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"homepage": "http://freecodecamp.com",
|
"homepage": "http://freecodecamp.com",
|
||||||
"authors": [
|
|
||||||
"Quincy Larson <michaelqlarson@gmail.com>"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"**/.*",
|
"**/.*",
|
||||||
@ -22,8 +18,10 @@
|
|||||||
"bootstrap": "~3.3.4",
|
"bootstrap": "~3.3.4",
|
||||||
"font-awesome": "~4.3.0",
|
"font-awesome": "~4.3.0",
|
||||||
"moment": "~2.10.2",
|
"moment": "~2.10.2",
|
||||||
"ramda": "~0.13.0",
|
"jshint": "~2.9.0",
|
||||||
"jshint": "~2.7.0",
|
"lightbox2": "~2.8.1",
|
||||||
"lightbox2": "~2.8.1"
|
"rxjs": "~4.0.6",
|
||||||
|
"CodeMirror": "~5.8.0",
|
||||||
|
"chai": "~3.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because one or more lines are too long
17
client/commonFramework/add-loop-protect.js
Normal file
17
client/commonFramework/add-loop-protect.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
window.common = (function(global) {
|
||||||
|
const {
|
||||||
|
loopProtect,
|
||||||
|
common = { init: [] }
|
||||||
|
} = global;
|
||||||
|
|
||||||
|
loopProtect.hit = function hit(line) {
|
||||||
|
var err = `Error: Exiting potential infinite loop at line ${line}.`;
|
||||||
|
console.error(err);
|
||||||
|
};
|
||||||
|
|
||||||
|
common.addLoopProtect = function addLoopProtect(code = '') {
|
||||||
|
return loopProtect(code);
|
||||||
|
};
|
||||||
|
|
||||||
|
return common;
|
||||||
|
})(window);
|
259
client/commonFramework/bindings.js
Normal file
259
client/commonFramework/bindings.js
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
window.common = (function(global) {
|
||||||
|
const {
|
||||||
|
$,
|
||||||
|
Rx: { Observable },
|
||||||
|
common = { init: [] }
|
||||||
|
} = global;
|
||||||
|
|
||||||
|
common.ctrlEnterClickHandler = function ctrlEnterClickHandler(e) {
|
||||||
|
// ctrl + enter or cmd + enter
|
||||||
|
if (
|
||||||
|
e.keyCode === 13 &&
|
||||||
|
(e.metaKey || e.ctrlKey)
|
||||||
|
) {
|
||||||
|
$('#complete-courseware-dialog').off('keydown', ctrlEnterClickHandler);
|
||||||
|
if ($('#submit-challenge').length > 0) {
|
||||||
|
$('#submit-challenge').click();
|
||||||
|
} else {
|
||||||
|
window.location = '/challenges/next-challenge?id=' + common.challengeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
common.init.push(function($) {
|
||||||
|
|
||||||
|
var $marginFix = $('.innerMarginFix');
|
||||||
|
$marginFix.css('min-height', $marginFix.height());
|
||||||
|
|
||||||
|
common.submitBtn$ = Observable.fromEvent($('#submitButton'), 'click');
|
||||||
|
|
||||||
|
common.resetBtn$ = Observable.fromEvent($('#reset-button'), 'click');
|
||||||
|
|
||||||
|
// init modal keybindings on open
|
||||||
|
$('#complete-courseware-dialog').on('shown.bs.modal', function() {
|
||||||
|
$('#complete-courseware-dialog').keydown(common.ctrlEnterClickHandler);
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove modal keybinds on close
|
||||||
|
$('#complete-courseware-dialog').on('hidden.bs.modal', function() {
|
||||||
|
$('#complete-courseware-dialog').off(
|
||||||
|
'keydown',
|
||||||
|
common.ctrlEnterClickHandler
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// video checklist binding
|
||||||
|
$('.challenge-list-checkbox').on('change', function() {
|
||||||
|
var checkboxId = $(this).parent().parent().attr('id');
|
||||||
|
if ($(this).is(':checked')) {
|
||||||
|
$(this).parent().siblings().children().addClass('faded');
|
||||||
|
if (!localStorage || !localStorage[checkboxId]) {
|
||||||
|
localStorage[checkboxId] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$(this).is(':checked')) {
|
||||||
|
$(this).parent().siblings().children().removeClass('faded');
|
||||||
|
if (localStorage[checkboxId]) {
|
||||||
|
localStorage.removeItem(checkboxId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.checklist-element').each(function() {
|
||||||
|
var checklistElementId = $(this).attr('id');
|
||||||
|
if (localStorage[checklistElementId]) {
|
||||||
|
$(this).children().children('li').addClass('faded');
|
||||||
|
$(this).children().children('input').trigger('click');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// video challenge submit
|
||||||
|
$('#next-courseware-button').on('click', function() {
|
||||||
|
$('#next-courseware-button').unbind('click');
|
||||||
|
if ($('.signup-btn-nav').length < 1) {
|
||||||
|
var data;
|
||||||
|
var completedWith = $('#completed-with').val() || null;
|
||||||
|
var publicURL = $('#public-url').val() || null;
|
||||||
|
var githubURL = $('#github-url').val() || null;
|
||||||
|
switch (common.challengeType) {
|
||||||
|
case common.challengeTypes.HTML:
|
||||||
|
case common.challengeTypes.JS:
|
||||||
|
case common.challengeTypes.VIDEO:
|
||||||
|
data = {
|
||||||
|
challengeInfo: {
|
||||||
|
challengeId: common.challengeId,
|
||||||
|
challengeName: common.challengeName
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$.post('/completed-challenge/', data)
|
||||||
|
.success(function(res) {
|
||||||
|
if (!res) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.href = '/challenges/next-challenge?id=' +
|
||||||
|
common.challengeId;
|
||||||
|
})
|
||||||
|
.fail(function() {
|
||||||
|
window.location.href = '/challenges';
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case common.challengeTypes.BASEJUMP:
|
||||||
|
case common.challengeTypes.ZIPLINE:
|
||||||
|
data = {
|
||||||
|
challengeInfo: {
|
||||||
|
challengeId: common.challengeId,
|
||||||
|
challengeName: common.challengeName,
|
||||||
|
completedWith: completedWith,
|
||||||
|
publicURL: publicURL,
|
||||||
|
githubURL: githubURL,
|
||||||
|
challengeType: common.challengeType,
|
||||||
|
verified: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$.post('/completed-zipline-or-basejump/', data)
|
||||||
|
.success(function() {
|
||||||
|
window.location.href = '/challenges/next-challenge?id=' +
|
||||||
|
common.challengeId;
|
||||||
|
})
|
||||||
|
.fail(function() {
|
||||||
|
window.location.replace(window.location.href);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case common.challengeTypes.BONFIRE:
|
||||||
|
window.location.href = '/challenges/next-challenge?id=' +
|
||||||
|
common.challengeId;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log('Happy Coding!');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (common.challengeName) {
|
||||||
|
window.ga('send', 'event', 'Challenge', 'load', common.challengeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#complete-courseware-dialog').on('hidden.bs.modal', function() {
|
||||||
|
if (common.editor.focus) {
|
||||||
|
common.editor.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#trigger-issue-modal').on('click', function() {
|
||||||
|
$('#issue-modal').modal('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#trigger-help-modal').on('click', function() {
|
||||||
|
$('#help-modal').modal('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#trigger-reset-modal').on('click', function() {
|
||||||
|
$('#reset-modal').modal('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#trigger-pair-modal').on('click', function() {
|
||||||
|
$('#pair-modal').modal('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#completed-courseware').on('click', function() {
|
||||||
|
$('#complete-courseware-dialog').modal('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#help-ive-found-a-bug-wiki-article').on('click', function() {
|
||||||
|
window.open(
|
||||||
|
'https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/' +
|
||||||
|
"Help-I've-Found-a-Bug",
|
||||||
|
'_blank'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#search-issue').on('click', function() {
|
||||||
|
var queryIssue = window.location.href.toString().split('?')[0];
|
||||||
|
window.open(
|
||||||
|
'https://github.com/FreeCodeCamp/FreeCodeCamp/issues?q=' +
|
||||||
|
'is:issue is:all ' +
|
||||||
|
(common.challengeName) +
|
||||||
|
' OR ' +
|
||||||
|
queryIssue
|
||||||
|
.substr(queryIssue.lastIndexOf('challenges/') + 11)
|
||||||
|
.replace('/', ''), '_blank');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#gist-share').on('click', function() {
|
||||||
|
var gistWindow = window.open('', '_blank');
|
||||||
|
|
||||||
|
$('#gist-share')
|
||||||
|
.attr('disabled', 'true')
|
||||||
|
.removeClass('btn-danger')
|
||||||
|
.addClass('btn-warning disabled');
|
||||||
|
|
||||||
|
function createCORSRequest(method, url) {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
if ('withCredentials' in xhr) {
|
||||||
|
xhr.open(method, url, true);
|
||||||
|
} else if (typeof XDomainRequest !== 'undefined') {
|
||||||
|
xhr = new XDomainRequest();
|
||||||
|
xhr.open(method, url);
|
||||||
|
} else {
|
||||||
|
xhr = null;
|
||||||
|
}
|
||||||
|
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
|
||||||
|
return xhr;
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = createCORSRequest('post', 'https://api.github.com/gists');
|
||||||
|
if (!request) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onload = function() {
|
||||||
|
if (
|
||||||
|
request.readyState === 4 &&
|
||||||
|
request.status === 201 &&
|
||||||
|
request.statusText === 'Created'
|
||||||
|
) {
|
||||||
|
gistWindow.location.href =
|
||||||
|
JSON.parse(request.responseText)['html_url'];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var description = common.username ?
|
||||||
|
'http://www.freecodecamp.com/' + common.username + ' \'s s' :
|
||||||
|
'S';
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
description: description + 'olution for ' + common.challengeName,
|
||||||
|
public: true,
|
||||||
|
files: {}
|
||||||
|
};
|
||||||
|
var queryIssue = window.location.href.toString().split('?')[0];
|
||||||
|
var filename = queryIssue
|
||||||
|
.substr(queryIssue.lastIndexOf('challenges/') + 11)
|
||||||
|
.replace('/', '') + '.js';
|
||||||
|
|
||||||
|
data.files[filename] = {
|
||||||
|
content: '// ' +
|
||||||
|
common.challengeName +
|
||||||
|
'\n' +
|
||||||
|
(common.username ? '// Author: @' + common.username + '\n' : '') +
|
||||||
|
'// Challenge: ' +
|
||||||
|
queryIssue +
|
||||||
|
'\n' +
|
||||||
|
'// Learn to Code at Free Code Camp (www.freecodecamp.com)' +
|
||||||
|
'\n\n' +
|
||||||
|
common.editor.getValue().trim()
|
||||||
|
};
|
||||||
|
|
||||||
|
request.send(JSON.stringify(data));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return common;
|
||||||
|
}(window));
|
47
client/commonFramework/code-storage.js
Normal file
47
client/commonFramework/code-storage.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// depends on: codeUri
|
||||||
|
window.common = (function(global) {
|
||||||
|
const {
|
||||||
|
localStorage,
|
||||||
|
common = { init: [] }
|
||||||
|
} = global;
|
||||||
|
|
||||||
|
var codeStorage = {
|
||||||
|
getStoredValue(key) {
|
||||||
|
if (
|
||||||
|
!localStorage ||
|
||||||
|
typeof localStorage.getItem !== 'function' ||
|
||||||
|
!key ||
|
||||||
|
typeof key !== 'string'
|
||||||
|
) {
|
||||||
|
console.log('unable to save to storage');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return '' + localStorage.getItem(key + 'Val');
|
||||||
|
},
|
||||||
|
|
||||||
|
isAlive: function(key) {
|
||||||
|
var val = this.getStoredValue(key);
|
||||||
|
return val !== 'null' &&
|
||||||
|
val !== 'undefined' &&
|
||||||
|
(val && val.length > 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateStorage(key, code) {
|
||||||
|
if (
|
||||||
|
!localStorage ||
|
||||||
|
typeof localStorage.setItem !== 'function' ||
|
||||||
|
!key ||
|
||||||
|
typeof key !== 'string'
|
||||||
|
) {
|
||||||
|
console.log('unable to save to storage');
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
localStorage.setItem(key + 'Val', code);
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
common.codeStorage = codeStorage;
|
||||||
|
|
||||||
|
return common;
|
||||||
|
}(window, window.common));
|
144
client/commonFramework/code-uri.js
Normal file
144
client/commonFramework/code-uri.js
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
// store code in the URL
|
||||||
|
window.common = (function(global) {
|
||||||
|
const {
|
||||||
|
encodeURIComponent: encode,
|
||||||
|
decodeURIComponent: decode,
|
||||||
|
location,
|
||||||
|
history,
|
||||||
|
common = { init: [] }
|
||||||
|
} = global;
|
||||||
|
|
||||||
|
const {
|
||||||
|
replaceScriptTags,
|
||||||
|
replaceSafeTags,
|
||||||
|
replaceFormActionAttr,
|
||||||
|
replaceFccfaaAttr
|
||||||
|
} = common;
|
||||||
|
|
||||||
|
function encodeFcc(val) {
|
||||||
|
return replaceScriptTags(replaceFormActionAttr(val));
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeFcc(val) {
|
||||||
|
return replaceSafeTags(replaceFccfaaAttr(val));
|
||||||
|
}
|
||||||
|
|
||||||
|
var codeUri = {
|
||||||
|
encode: function(code) {
|
||||||
|
return encode(code);
|
||||||
|
},
|
||||||
|
decode: function(code) {
|
||||||
|
try {
|
||||||
|
return decode(code);
|
||||||
|
} catch (ignore) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isInQuery: function(query) {
|
||||||
|
var decoded = codeUri.decode(query);
|
||||||
|
if (!decoded || typeof decoded.split !== 'function') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return decoded
|
||||||
|
.split('?')
|
||||||
|
.splice(1)
|
||||||
|
.pop()
|
||||||
|
.split('&')
|
||||||
|
.reduce(function(found, param) {
|
||||||
|
var key = param.split('=')[0];
|
||||||
|
if (key === 'solution') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}, false);
|
||||||
|
},
|
||||||
|
isAlive: function() {
|
||||||
|
return codeUri.enabled &&
|
||||||
|
codeUri.isInQuery(location.search) ||
|
||||||
|
codeUri.isInQuery(location.hash);
|
||||||
|
},
|
||||||
|
getKeyInQuery(query, keyToFind = '') {
|
||||||
|
return query
|
||||||
|
.split('&')
|
||||||
|
.reduce(function(oldValue, param) {
|
||||||
|
var key = param.split('=')[0];
|
||||||
|
var value = param.split('=')[1];
|
||||||
|
if (key === keyToFind) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return oldValue;
|
||||||
|
}, null);
|
||||||
|
},
|
||||||
|
getSolutionFromQuery(query = '') {
|
||||||
|
return decodeFcc(
|
||||||
|
codeUri.decode(codeUri.getKeyInQuery(query, 'solution'))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
parse: function() {
|
||||||
|
if (!codeUri.enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var query;
|
||||||
|
if (location.search && codeUri.isInQuery(location.search)) {
|
||||||
|
query = location.search.replace(/^\?/, '');
|
||||||
|
|
||||||
|
if (history && typeof history.replaceState === 'function') {
|
||||||
|
history.replaceState(
|
||||||
|
history.state,
|
||||||
|
null,
|
||||||
|
location.href.split('?')[0]
|
||||||
|
);
|
||||||
|
location.hash = '#?' + encodeFcc(query);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
query = location.hash.replace(/^\#\?/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getSolutionFromQuery(query);
|
||||||
|
},
|
||||||
|
querify: function(solution) {
|
||||||
|
if (!codeUri.enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (history && typeof history.replaceState === 'function') {
|
||||||
|
// grab the url up to the query
|
||||||
|
// destroy any hash symbols still clinging to life
|
||||||
|
const url = (location.href.split('?')[0]).replace(/(#*)$/, '');
|
||||||
|
history.replaceState(
|
||||||
|
history.state,
|
||||||
|
null,
|
||||||
|
url +
|
||||||
|
'#?' +
|
||||||
|
(codeUri.shouldRun() ? '' : 'run=disabled&') +
|
||||||
|
'solution=' +
|
||||||
|
codeUri.encode(encodeFcc(solution))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
location.hash = '?solution=' +
|
||||||
|
codeUri.encode(encodeFcc(solution));
|
||||||
|
}
|
||||||
|
|
||||||
|
return solution;
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
shouldRun() {
|
||||||
|
return !this.getKeyInQuery(
|
||||||
|
(location.search || location.hash).replace(/^(\?|#\?)/, ''),
|
||||||
|
'run'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
common.init.push(function() {
|
||||||
|
codeUri.parse();
|
||||||
|
});
|
||||||
|
|
||||||
|
common.codeUri = codeUri;
|
||||||
|
common.shouldRun = () => codeUri.shouldRun();
|
||||||
|
|
||||||
|
return common;
|
||||||
|
}(window));
|
113
client/commonFramework/create-editor.js
Normal file
113
client/commonFramework/create-editor.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
window.common = (function(global) {
|
||||||
|
const {
|
||||||
|
Rx: { Subject, Observable },
|
||||||
|
CodeMirror,
|
||||||
|
emmetCodeMirror,
|
||||||
|
common = { init: [] }
|
||||||
|
} = global;
|
||||||
|
|
||||||
|
const { challengeType = '0', challengeTypes } = common;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!CodeMirror ||
|
||||||
|
challengeType === challengeTypes.BASEJUMP ||
|
||||||
|
challengeType === challengeTypes.ZIPLINE ||
|
||||||
|
challengeType === challengeTypes.VIDEO ||
|
||||||
|
challengeType === challengeTypes.STEP ||
|
||||||
|
challengeType === challengeTypes.HIKES
|
||||||
|
) {
|
||||||
|
common.editor = {};
|
||||||
|
return common;
|
||||||
|
}
|
||||||
|
|
||||||
|
var editor = CodeMirror.fromTextArea(
|
||||||
|
document.getElementById('codeEditor'),
|
||||||
|
{
|
||||||
|
lint: true,
|
||||||
|
lineNumbers: true,
|
||||||
|
mode: 'javascript',
|
||||||
|
theme: 'monokai',
|
||||||
|
runnable: true,
|
||||||
|
matchBrackets: true,
|
||||||
|
autoCloseBrackets: true,
|
||||||
|
scrollbarStyle: 'null',
|
||||||
|
lineWrapping: true,
|
||||||
|
gutters: ['CodeMirror-lint-markers']
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
editor.setSize('100%', 'auto');
|
||||||
|
|
||||||
|
common.editorExecute$ = new Subject();
|
||||||
|
common.editorKeyUp$ = Observable.fromEventPattern(
|
||||||
|
(handler) => editor.on('keyup', handler),
|
||||||
|
(handler) => editor.off('keyup', handler)
|
||||||
|
);
|
||||||
|
|
||||||
|
editor.setOption('extraKeys', {
|
||||||
|
Tab: function(cm) {
|
||||||
|
if (cm.somethingSelected()) {
|
||||||
|
cm.indentSelection('add');
|
||||||
|
} else {
|
||||||
|
var spaces = Array(cm.getOption('indentUnit') + 1).join(' ');
|
||||||
|
cm.replaceSelection(spaces);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Shift-Tab': function(cm) {
|
||||||
|
if (cm.somethingSelected()) {
|
||||||
|
cm.indentSelection('subtract');
|
||||||
|
} else {
|
||||||
|
var spaces = Array(cm.getOption('indentUnit') + 1).join(' ');
|
||||||
|
cm.replaceSelection(spaces);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Ctrl-Enter': function() {
|
||||||
|
common.editorExecute$.onNext();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
'Cmd-Enter': function() {
|
||||||
|
common.editorExecute$.onNext();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
var info = editor.getScrollInfo();
|
||||||
|
|
||||||
|
var after = editor.charCoords({
|
||||||
|
line: editor.getCursor().line + 1,
|
||||||
|
ch: 0
|
||||||
|
}, 'local').top;
|
||||||
|
|
||||||
|
if (info.top + info.clientHeight < after) {
|
||||||
|
editor.scrollTo(null, after - info.clientHeight + 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emmetCodeMirror) {
|
||||||
|
emmetCodeMirror(
|
||||||
|
editor,
|
||||||
|
{
|
||||||
|
'Cmd-E': 'emmet.expand_abbreviation',
|
||||||
|
Tab: 'emmet.expand_abbreviation_with_tab',
|
||||||
|
Enter: 'emmet.insert_formatted_line_break_only'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
common.init.push(function() {
|
||||||
|
let editorValue;
|
||||||
|
if (common.codeUri.isAlive()) {
|
||||||
|
editorValue = common.codeUri.parse();
|
||||||
|
} else {
|
||||||
|
editorValue = common.codeStorage.isAlive(common.challengeName) ?
|
||||||
|
common.codeStorage.getStoredValue(common.challengeName) :
|
||||||
|
common.seed;
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.setValue(common.replaceSafeTags(editorValue));
|
||||||
|
editor.refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
common.editor = editor;
|
||||||
|
|
||||||
|
return common;
|
||||||
|
}(window));
|
55
client/commonFramework/detect-unsafe-code-stream.js
Normal file
55
client/commonFramework/detect-unsafe-code-stream.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
window.common = (function(global) {
|
||||||
|
const {
|
||||||
|
Rx: { Observable },
|
||||||
|
common = { init: [] }
|
||||||
|
} = global;
|
||||||
|
|
||||||
|
const detectFunctionCall = /function\s*?\(|function\s+\w+\s*?\(/gi;
|
||||||
|
const detectUnsafeJQ = /\$\s*?\(\s*?\$\s*?\)/gi;
|
||||||
|
const detectUnsafeConsoleCall = /if\s\(null\)\sconsole\.log\(1\);/gi;
|
||||||
|
|
||||||
|
common.detectUnsafeCode$ = function detectUnsafeCode$(code) {
|
||||||
|
const openingComments = code.match(/\/\*/gi);
|
||||||
|
const closingComments = code.match(/\*\//gi);
|
||||||
|
|
||||||
|
// checks if the number of opening comments(/*) matches the number of
|
||||||
|
// closing comments(*/)
|
||||||
|
if (
|
||||||
|
openingComments &&
|
||||||
|
(
|
||||||
|
!closingComments ||
|
||||||
|
openingComments.length > closingComments.length
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
|
||||||
|
return Observable.throw(
|
||||||
|
new Error('SyntaxError: Unfinished multi-line comment')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code.match(detectUnsafeJQ)) {
|
||||||
|
return Observable.throw(
|
||||||
|
new Error('Unsafe $($)')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
code.match(/function/g) &&
|
||||||
|
!code.match(detectFunctionCall)
|
||||||
|
) {
|
||||||
|
return Observable.throw(
|
||||||
|
new Error('SyntaxError: Unsafe or unfinished function declaration')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code.match(detectUnsafeConsoleCall)) {
|
||||||
|
return Observable.throw(
|
||||||
|
new Error('Invalid if (null) console.log(1); detected')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Observable.just(code);
|
||||||
|
};
|
||||||
|
|
||||||
|
return common;
|
||||||
|
}(window));
|
28
client/commonFramework/display-test-results.js
Normal file
28
client/commonFramework/display-test-results.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
window.common = (function({ $, common = { init: [] }}) {
|
||||||
|
|
||||||
|
common.displayTestResults = function displayTestResults(data = []) {
|
||||||
|
$('#testSuite').children().remove();
|
||||||
|
data.forEach(({ err = false, text = '' }) => {
|
||||||
|
var iconClass = err ?
|
||||||
|
'"ion-close-circled big-error-icon"' :
|
||||||
|
'"ion-checkmark-circled big-success-icon"';
|
||||||
|
|
||||||
|
$('<div></div>').html(`
|
||||||
|
<div class='row'>
|
||||||
|
<div class='col-xs-2 text-center'>
|
||||||
|
<i class=${iconClass}></i>
|
||||||
|
</div>
|
||||||
|
<div class='col-xs-10 test-output'>
|
||||||
|
${text.split('message: ').pop().replace(/\'\);/g, '')}
|
||||||
|
</div>
|
||||||
|
<div class='ten-pixel-break'/>
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
.appendTo($('#testSuite'));
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
return common;
|
||||||
|
}(window));
|
170
client/commonFramework/end.js
Normal file
170
client/commonFramework/end.js
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
$(document).ready(function() {
|
||||||
|
const common = window.common;
|
||||||
|
const { Observable } = window.Rx;
|
||||||
|
const {
|
||||||
|
addLoopProtect,
|
||||||
|
challengeName,
|
||||||
|
challengeType,
|
||||||
|
challengeTypes
|
||||||
|
} = common;
|
||||||
|
|
||||||
|
common.init.forEach(function(init) {
|
||||||
|
init($);
|
||||||
|
});
|
||||||
|
|
||||||
|
// only run if editor present
|
||||||
|
if (common.editor.getValue) {
|
||||||
|
const code$ = common.editorKeyUp$
|
||||||
|
.debounce(750)
|
||||||
|
.map(() => common.editor.getValue())
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.shareReplay();
|
||||||
|
|
||||||
|
// update storage
|
||||||
|
code$.subscribe(
|
||||||
|
code => {
|
||||||
|
common.codeStorage.updateStorage(common.challengeName, code);
|
||||||
|
common.codeUri.querify(code);
|
||||||
|
},
|
||||||
|
err => console.error(err)
|
||||||
|
);
|
||||||
|
|
||||||
|
code$
|
||||||
|
// only run for HTML
|
||||||
|
.filter(() => common.challengeType === challengeTypes.HTML)
|
||||||
|
.flatMap(code => {
|
||||||
|
return common.detectUnsafeCode$(code)
|
||||||
|
.map(() => {
|
||||||
|
const combinedCode = common.head + code + common.tail;
|
||||||
|
|
||||||
|
return addLoopProtect(combinedCode);
|
||||||
|
})
|
||||||
|
.flatMap(code => common.updatePreview$(code))
|
||||||
|
.flatMap(() => common.checkPreview$({ code }))
|
||||||
|
.catch(err => Observable.just({ err }));
|
||||||
|
})
|
||||||
|
.subscribe(
|
||||||
|
({ err }) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return common.updatePreview$(`
|
||||||
|
<h1>${err}</h1>
|
||||||
|
`).subscribe(() => {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
err => console.error(err)
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
common.resetBtn$
|
||||||
|
.doOnNext(() => {
|
||||||
|
common.editor.setValue(common.replaceSafeTags(common.seed));
|
||||||
|
})
|
||||||
|
.flatMap(() => {
|
||||||
|
return common.executeChallenge$()
|
||||||
|
.catch(err => Observable.just({ err }));
|
||||||
|
})
|
||||||
|
.subscribe(
|
||||||
|
({ err, output, originalCode }) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return common.updateOutputDisplay('' + err);
|
||||||
|
}
|
||||||
|
common.codeStorage.updateStorage(challengeName, originalCode);
|
||||||
|
common.updateOutputDisplay('' + output);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
common.updateOutputDisplay('' + err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Observable.merge(
|
||||||
|
common.editorExecute$,
|
||||||
|
common.submitBtn$
|
||||||
|
)
|
||||||
|
.flatMap(() => {
|
||||||
|
common.appendToOutputDisplay('\n// testing challenge...');
|
||||||
|
return common.executeChallenge$()
|
||||||
|
.map(({ tests, ...rest }) => {
|
||||||
|
const solved = tests.every(test => !test.err);
|
||||||
|
return { ...rest, tests, solved };
|
||||||
|
})
|
||||||
|
.catch(err => Observable.just({ err }));
|
||||||
|
})
|
||||||
|
.subscribe(
|
||||||
|
({ err, solved, output, tests }) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
if (common.challengeType === common.challengeTypes.HTML) {
|
||||||
|
return common.updatePreview$(`
|
||||||
|
<h1>${err}</h1>
|
||||||
|
`).first().subscribe(() => {});
|
||||||
|
}
|
||||||
|
return common.updateOutputDisplay('' + err);
|
||||||
|
}
|
||||||
|
common.updateOutputDisplay('' + output);
|
||||||
|
common.displayTestResults(tests);
|
||||||
|
if (solved) {
|
||||||
|
common.showCompletion();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
({ err }) => {
|
||||||
|
console.error(err);
|
||||||
|
common.updateOutputDisplay('' + err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// initial challenge run to populate tests
|
||||||
|
if (challengeType === challengeTypes.HTML) {
|
||||||
|
var $preview = $('#preview');
|
||||||
|
return Observable.fromCallback($preview.ready, $preview)()
|
||||||
|
.delay(500)
|
||||||
|
.flatMap(() => common.executeChallenge$())
|
||||||
|
.catch(err => Observable.just({ err }))
|
||||||
|
.subscribe(
|
||||||
|
({ err, tests }) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
if (common.challengeType === common.challengeTypes.HTML) {
|
||||||
|
return common.updatePreview$(`
|
||||||
|
<h1>${err}</h1>
|
||||||
|
`).subscribe(() => {});
|
||||||
|
}
|
||||||
|
return common.updateOutputDisplay('' + err);
|
||||||
|
}
|
||||||
|
common.displayTestResults(tests);
|
||||||
|
},
|
||||||
|
({ err }) => {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
challengeType === challengeTypes.BONFIRE ||
|
||||||
|
challengeType === challengeTypes.JS
|
||||||
|
) {
|
||||||
|
Observable.just({})
|
||||||
|
.delay(500)
|
||||||
|
.flatMap(() => common.executeChallenge$())
|
||||||
|
.catch(err => Observable.just({ err }))
|
||||||
|
.subscribe(
|
||||||
|
({ err, originalCode, tests }) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return common.updateOutputDisplay('' + err);
|
||||||
|
}
|
||||||
|
common.codeStorage.updateStorage(challengeName, originalCode);
|
||||||
|
common.displayTestResults(tests);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
console.error(err);
|
||||||
|
common.updateOutputDisplay('' + err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
61
client/commonFramework/execute-challenge-stream.js
Normal file
61
client/commonFramework/execute-challenge-stream.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
window.common = (function(global) {
|
||||||
|
const {
|
||||||
|
ga,
|
||||||
|
common = { init: [] }
|
||||||
|
} = global;
|
||||||
|
|
||||||
|
const {
|
||||||
|
addLoopProtect,
|
||||||
|
getJsFromHtml,
|
||||||
|
detectUnsafeCode$,
|
||||||
|
updatePreview$,
|
||||||
|
challengeType,
|
||||||
|
challengeTypes
|
||||||
|
} = common;
|
||||||
|
|
||||||
|
let attempts = 0;
|
||||||
|
|
||||||
|
common.executeChallenge$ = function executeChallenge$() {
|
||||||
|
const code = common.editor.getValue();
|
||||||
|
const originalCode = code;
|
||||||
|
const head = common.arrayToNewLineString(common.head);
|
||||||
|
const tail = common.arrayToNewLineString(common.tail);
|
||||||
|
const combinedCode = head + code + tail;
|
||||||
|
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
ga('send', 'event', 'Challenge', 'ran-code', common.challengeName);
|
||||||
|
|
||||||
|
// run checks for unsafe code
|
||||||
|
return detectUnsafeCode$(code)
|
||||||
|
// add head and tail and detect loops
|
||||||
|
.map(() => {
|
||||||
|
if (challengeType !== challengeTypes.HTML) {
|
||||||
|
return `<script>;${addLoopProtect(combinedCode)}/**/</script>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return addLoopProtect(combinedCode);
|
||||||
|
})
|
||||||
|
.flatMap(code => updatePreview$(code))
|
||||||
|
.flatMap(code => {
|
||||||
|
let output;
|
||||||
|
|
||||||
|
if (
|
||||||
|
challengeType === challengeTypes.HTML &&
|
||||||
|
common.hasJs(code)
|
||||||
|
) {
|
||||||
|
output = common.getJsOutput(getJsFromHtml(code));
|
||||||
|
} else if (challengeType !== challengeTypes.HTML) {
|
||||||
|
output = common.getJsOutput(addLoopProtect(combinedCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
return common.runPreviewTests$({
|
||||||
|
tests: common.tests.slice(),
|
||||||
|
originalCode,
|
||||||
|
output
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return common;
|
||||||
|
}(window));
|
23
client/commonFramework/get-iframe.js
Normal file
23
client/commonFramework/get-iframe.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
window.common = (function(global) {
|
||||||
|
const {
|
||||||
|
common = { init: [] },
|
||||||
|
document: doc
|
||||||
|
} = global;
|
||||||
|
|
||||||
|
common.getIframe = function getIframe(id = 'preview') {
|
||||||
|
let previewFrame = doc.getElementById(id);
|
||||||
|
|
||||||
|
// create and append a hidden preview frame
|
||||||
|
if (!previewFrame) {
|
||||||
|
previewFrame = doc.createElement('iframe');
|
||||||
|
previewFrame.id = id;
|
||||||
|
previewFrame.setAttribute('style', 'display: none');
|
||||||
|
doc.body.appendChild(previewFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
return previewFrame.contentDocument ||
|
||||||
|
previewFrame.contentWindow.document;
|
||||||
|
};
|
||||||
|
|
||||||
|
return common;
|
||||||
|
})(window);
|
120
client/commonFramework/init.js
Normal file
120
client/commonFramework/init.js
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
window.common = (function(global) {
|
||||||
|
// common namespace
|
||||||
|
// all classes should be stored here
|
||||||
|
// called at the beginning of dom ready
|
||||||
|
const {
|
||||||
|
Rx: { Disposable, Observable, config },
|
||||||
|
common = { init: [] }
|
||||||
|
} = global;
|
||||||
|
|
||||||
|
config.longStackSupport = true;
|
||||||
|
common.head = common.head || [];
|
||||||
|
common.tail = common.tail || [];
|
||||||
|
common.salt = Math.random();
|
||||||
|
|
||||||
|
common.challengeTypes = {
|
||||||
|
HTML: '0',
|
||||||
|
JS: '1',
|
||||||
|
VIDEO: '2',
|
||||||
|
ZIPLINE: '3',
|
||||||
|
BASEJUMP: '4',
|
||||||
|
BONFIRE: '5',
|
||||||
|
HIKES: '6',
|
||||||
|
STEP: '7'
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
common.arrayToNewLineString = function arrayToNewLineString(seedData) {
|
||||||
|
seedData = Array.isArray(seedData) ? seedData : [seedData];
|
||||||
|
return seedData.reduce(function(seed, line) {
|
||||||
|
return '' + seed + line + '\n';
|
||||||
|
}, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
common.seed = common.arrayToNewLineString(common.challengeSeed);
|
||||||
|
|
||||||
|
common.replaceScriptTags = function replaceScriptTags(value) {
|
||||||
|
return value
|
||||||
|
.replace(/<script>/gi, 'fccss')
|
||||||
|
.replace(/<\/script>/gi, 'fcces');
|
||||||
|
};
|
||||||
|
|
||||||
|
common.replaceSafeTags = function replaceSafeTags(value) {
|
||||||
|
return value
|
||||||
|
.replace(/fccss/gi, '<script>')
|
||||||
|
.replace(/fcces/gi, '</script>');
|
||||||
|
};
|
||||||
|
|
||||||
|
common.replaceFormActionAttr = function replaceFormAction(value) {
|
||||||
|
return value.replace(/<form[^>]*>/, function(val) {
|
||||||
|
return val.replace(/action(\s*?)=/, 'fccfaa$1=');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
common.replaceFccfaaAttr = function replaceFccfaaAttr(value) {
|
||||||
|
return value.replace(/<form[^>]*>/, function(val) {
|
||||||
|
return val.replace(/fccfaa(\s*?)=/, 'action$1=');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
common.scopejQuery = function scopejQuery(str) {
|
||||||
|
return str
|
||||||
|
.replace(/\$/gi, 'j$')
|
||||||
|
.replace(/document/gi, 'jdocument')
|
||||||
|
.replace(/jQuery/gi, 'jjQuery');
|
||||||
|
};
|
||||||
|
|
||||||
|
common.unScopeJQuery = function unScopeJQuery(str) {
|
||||||
|
return str
|
||||||
|
.replace(/j\$/gi, '$')
|
||||||
|
.replace(/jdocument/gi, 'document')
|
||||||
|
.replace(/jjQuery/gi, 'jQuery');
|
||||||
|
};
|
||||||
|
|
||||||
|
const commentRegex = /(\/\*[^(\*\/)]*\*\/)|([ \n]\/\/[^\n]*)/g;
|
||||||
|
common.removeComments = function removeComments(str) {
|
||||||
|
return str.replace(commentRegex, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const logRegex = /(console\.[\w]+\s*\(.*\;)/g;
|
||||||
|
common.removeLogs = function removeLogs(str) {
|
||||||
|
return str.replace(logRegex, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
common.reassembleTest = function reassembleTest(code = '', { line, text }) {
|
||||||
|
var regexp = new RegExp('//' + line + common.salt);
|
||||||
|
return code.replace(regexp, text);
|
||||||
|
};
|
||||||
|
|
||||||
|
common.getScriptContent$ = function getScriptContent$(script) {
|
||||||
|
return Observable.create(function(observer) {
|
||||||
|
const jqXHR = $.get(script, null, null, 'text')
|
||||||
|
.success(data => {
|
||||||
|
observer.onNext(data);
|
||||||
|
observer.onCompleted();
|
||||||
|
})
|
||||||
|
.fail(e => observer.onError(e))
|
||||||
|
.always(() => observer.onCompleted());
|
||||||
|
|
||||||
|
return new Disposable(() => {
|
||||||
|
jqXHR.abort();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openScript = /\<\s?script\s?\>/gi;
|
||||||
|
const closingScript = /\<\s?\/\s?script\s?\>/gi;
|
||||||
|
|
||||||
|
// detects if there is JavaScript in the first script tag
|
||||||
|
common.hasJs = function hasJs(code) {
|
||||||
|
return !!common.getJsFromHtml(code);
|
||||||
|
};
|
||||||
|
|
||||||
|
// grabs the content from the first script tag in the code
|
||||||
|
common.getJsFromHtml = function getJsFromHtml(code) {
|
||||||
|
// grab user javaScript
|
||||||
|
return (code.split(openScript)[1] || '').split(closingScript)[0] || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
return common;
|
||||||
|
})(window);
|
51
client/commonFramework/output-display.js
Normal file
51
client/commonFramework/output-display.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
window.common = (function(global) {
|
||||||
|
const {
|
||||||
|
CodeMirror,
|
||||||
|
document: doc,
|
||||||
|
common = { init: [] }
|
||||||
|
} = global;
|
||||||
|
|
||||||
|
const { challengeTypes, challengeType = '0' } = common;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!CodeMirror ||
|
||||||
|
challengeType !== challengeTypes.JS &&
|
||||||
|
challengeType !== challengeTypes.BONFIRE
|
||||||
|
) {
|
||||||
|
common.updateOutputDisplay = () => {};
|
||||||
|
common.appendToOutputDisplay = () => {};
|
||||||
|
return common;
|
||||||
|
}
|
||||||
|
|
||||||
|
var codeOutput = CodeMirror.fromTextArea(
|
||||||
|
doc.getElementById('codeOutput'),
|
||||||
|
{
|
||||||
|
lineNumbers: false,
|
||||||
|
mode: 'text',
|
||||||
|
theme: 'monokai',
|
||||||
|
readOnly: 'nocursor',
|
||||||
|
lineWrapping: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
codeOutput.setValue(`/**
|
||||||
|
* Your output will go here.
|
||||||
|
* Console.log() -type statements
|
||||||
|
* will appear in your browser\'s
|
||||||
|
* DevTools JavaScript console.
|
||||||
|
*/`);
|
||||||
|
|
||||||
|
codeOutput.setSize('100%', '100%');
|
||||||
|
|
||||||
|
common.updateOutputDisplay = function updateOutputDisplay(str = '') {
|
||||||
|
codeOutput.setValue(str);
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
|
||||||
|
common.appendToOutputDisplay = function appendToOutputDisplay(str = '') {
|
||||||
|
codeOutput.setValue(codeOutput.getValue() + str);
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
|
||||||
|
return common;
|
||||||
|
}(window));
|
132
client/commonFramework/phone-scroll-lock.js
Normal file
132
client/commonFramework/phone-scroll-lock.js
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
window.common = (function({ common = { init: [] }}) {
|
||||||
|
|
||||||
|
common.lockTop = function lockTop() {
|
||||||
|
var magiVal;
|
||||||
|
|
||||||
|
if ($(window).width() >= 990) {
|
||||||
|
if ($('.editorScrollDiv').html()) {
|
||||||
|
|
||||||
|
magiVal = $(window).height() - $('.navbar').height();
|
||||||
|
|
||||||
|
if (magiVal < 0) {
|
||||||
|
magiVal = 0;
|
||||||
|
}
|
||||||
|
$('.editorScrollDiv').css('height', magiVal - 85 + 'px');
|
||||||
|
}
|
||||||
|
|
||||||
|
magiVal = $(window).height() - $('.navbar').height();
|
||||||
|
|
||||||
|
if (magiVal < 0) {
|
||||||
|
magiVal = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.scroll-locker')
|
||||||
|
.css('min-height', $('.editorScrollDiv').height())
|
||||||
|
.css('height', magiVal - 185);
|
||||||
|
} else {
|
||||||
|
$('.editorScrollDiv').css('max-height', 500 + 'px');
|
||||||
|
|
||||||
|
$('.scroll-locker')
|
||||||
|
.css('position', 'inherit')
|
||||||
|
.css('top', 'inherit')
|
||||||
|
.css('width', '100%')
|
||||||
|
.css('max-height', '85%');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
common.init.push(function($) {
|
||||||
|
// fakeiphone positioning hotfix
|
||||||
|
if (
|
||||||
|
$('.iphone-position').html() ||
|
||||||
|
$('.iphone').html()
|
||||||
|
) {
|
||||||
|
var startIphonePosition = parseInt(
|
||||||
|
$('.iphone-position')
|
||||||
|
.css('top')
|
||||||
|
.replace('px', ''),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
var startIphone = parseInt(
|
||||||
|
$('.iphone')
|
||||||
|
.css('top')
|
||||||
|
.replace('px', ''),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
$(window).on('scroll', function() {
|
||||||
|
var courseHeight = $('.courseware-height').height();
|
||||||
|
var courseTop = $('.courseware-height').offset().top;
|
||||||
|
var windowScrollTop = $(window).scrollTop();
|
||||||
|
var phoneHeight = $('.iphone-position').height();
|
||||||
|
|
||||||
|
if (courseHeight + courseTop - windowScrollTop - phoneHeight <= 0) {
|
||||||
|
$('.iphone-position').css(
|
||||||
|
'top',
|
||||||
|
startIphonePosition +
|
||||||
|
courseHeight +
|
||||||
|
courseTop -
|
||||||
|
windowScrollTop -
|
||||||
|
phoneHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
$('.iphone').css(
|
||||||
|
'top',
|
||||||
|
startIphonePosition +
|
||||||
|
courseHeight +
|
||||||
|
courseTop -
|
||||||
|
windowScrollTop -
|
||||||
|
phoneHeight +
|
||||||
|
120
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$('.iphone-position').css('top', startIphonePosition);
|
||||||
|
$('.iphone').css('top', startIphone);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($('.scroll-locker').html()) {
|
||||||
|
|
||||||
|
if ($('.scroll-locker').html()) {
|
||||||
|
common.lockTop();
|
||||||
|
$(window).on('resize', function() {
|
||||||
|
common.lockTop();
|
||||||
|
});
|
||||||
|
$(window).on('scroll', function() {
|
||||||
|
common.lockTop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var execInProgress = false;
|
||||||
|
|
||||||
|
// why is this not $???
|
||||||
|
document
|
||||||
|
.getElementById('scroll-locker')
|
||||||
|
.addEventListener(
|
||||||
|
'previewUpdateSpy',
|
||||||
|
function(e) {
|
||||||
|
if (execInProgress) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
execInProgress = true;
|
||||||
|
setTimeout(function() {
|
||||||
|
if (
|
||||||
|
$($('.scroll-locker').children()[0]).height() - 800 > e.detail
|
||||||
|
) {
|
||||||
|
$('.scroll-locker').scrollTop(e.detail);
|
||||||
|
} else {
|
||||||
|
var scrollTop = $($('.scroll-locker').children()[0]).height();
|
||||||
|
|
||||||
|
$('.scroll-locker').animate({ scrollTop: scrollTop }, 175);
|
||||||
|
}
|
||||||
|
execInProgress = false;
|
||||||
|
}, 750);
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return common;
|
||||||
|
}(window));
|
56
client/commonFramework/report-issue.js
Normal file
56
client/commonFramework/report-issue.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
window.common = (function({ common = { init: [] } }) {
|
||||||
|
common.init.push(function($) {
|
||||||
|
$('#report-issue').on('click', function() {
|
||||||
|
var textMessage = [
|
||||||
|
'Challenge [',
|
||||||
|
(common.challengeName || window.location.pathname),
|
||||||
|
'](',
|
||||||
|
window.location.href,
|
||||||
|
') has an issue.\n',
|
||||||
|
'User Agent is: <code>',
|
||||||
|
navigator.userAgent,
|
||||||
|
'</code>.\n',
|
||||||
|
'Please describe how to reproduce this issue, and include ',
|
||||||
|
'links to screenshots if possible.\n\n'
|
||||||
|
].join('');
|
||||||
|
|
||||||
|
if (
|
||||||
|
common.editor &&
|
||||||
|
typeof common.editor.getValue === 'function' &&
|
||||||
|
common.editor.getValue().trim()
|
||||||
|
) {
|
||||||
|
var type;
|
||||||
|
switch (common.challengeType) {
|
||||||
|
case common.challengeTypes.HTML:
|
||||||
|
type = 'html';
|
||||||
|
break;
|
||||||
|
case common.challengeTypes.JS:
|
||||||
|
case common.challengeTypes.BONFIRE:
|
||||||
|
type = 'javascript';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
type = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
textMessage += [
|
||||||
|
'My code:\n```',
|
||||||
|
type,
|
||||||
|
'\n',
|
||||||
|
common.editor.getValue(),
|
||||||
|
'\n```\n\n'
|
||||||
|
].join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
textMessage = encodeURIComponent(textMessage);
|
||||||
|
|
||||||
|
$('#issue-modal').modal('hide');
|
||||||
|
window.open(
|
||||||
|
'https://github.com/freecodecamp/freecodecamp/issues/new?&body=' +
|
||||||
|
textMessage,
|
||||||
|
'_blank'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return common;
|
||||||
|
}(window));
|
40
client/commonFramework/run-tests-stream.js
Normal file
40
client/commonFramework/run-tests-stream.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
window.common = (function(global) {
|
||||||
|
const {
|
||||||
|
Rx: { Observable },
|
||||||
|
chai,
|
||||||
|
common = { init: [] }
|
||||||
|
} = global;
|
||||||
|
|
||||||
|
common.runTests$ = function runTests$({
|
||||||
|
code,
|
||||||
|
originalCode,
|
||||||
|
userTests,
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
|
|
||||||
|
return Observable.from(userTests)
|
||||||
|
.map(function(test) {
|
||||||
|
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
const assert = chai.assert;
|
||||||
|
const editor = { getValue() { return originalCode; }};
|
||||||
|
/* eslint-enable no-unused-vars */
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (test) {
|
||||||
|
/* eslint-disable no-eval */
|
||||||
|
eval(common.reassembleTest(code, test));
|
||||||
|
/* eslint-enable no-eval */
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
test.err = e.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return test;
|
||||||
|
})
|
||||||
|
.toArray()
|
||||||
|
.map(tests => ({ ...rest, tests }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return common;
|
||||||
|
}(window));
|
70
client/commonFramework/show-completion.js
Normal file
70
client/commonFramework/show-completion.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
window.common = (function(global) {
|
||||||
|
const {
|
||||||
|
$,
|
||||||
|
ga = (() => {}),
|
||||||
|
common = { init: [] }
|
||||||
|
} = global;
|
||||||
|
|
||||||
|
common.showCompletion = function showCompletion() {
|
||||||
|
var time = Math.floor(Date.now() - window.started);
|
||||||
|
|
||||||
|
ga(
|
||||||
|
'send',
|
||||||
|
'event',
|
||||||
|
'Challenge',
|
||||||
|
'solved',
|
||||||
|
common.challengeName + ', Time: ' + time + ', Attempts: ' + 0
|
||||||
|
);
|
||||||
|
|
||||||
|
var bonfireSolution = common.editor.getValue();
|
||||||
|
var didCompleteWith = $('#completed-with').val() || null;
|
||||||
|
|
||||||
|
$('#complete-courseware-dialog').modal('show');
|
||||||
|
$('#complete-courseware-dialog .modal-header').click();
|
||||||
|
|
||||||
|
$('#submit-challenge').click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
$('#submit-challenge')
|
||||||
|
.attr('disabled', 'true')
|
||||||
|
.removeClass('btn-primary')
|
||||||
|
.addClass('btn-warning disabled');
|
||||||
|
|
||||||
|
var $checkmarkContainer = $('#checkmark-container');
|
||||||
|
$checkmarkContainer.css({ height: $checkmarkContainer.innerHeight() });
|
||||||
|
|
||||||
|
$('#challenge-checkmark')
|
||||||
|
.addClass('zoomOutUp')
|
||||||
|
// .removeClass('zoomInDown')
|
||||||
|
.delay(1000)
|
||||||
|
.queue(function(next) {
|
||||||
|
$(this).replaceWith(
|
||||||
|
'<div id="challenge-spinner" ' +
|
||||||
|
'class="animated zoomInUp inner-circles-loader">' +
|
||||||
|
'submitting...</div>'
|
||||||
|
);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
$.post(
|
||||||
|
'/completed-bonfire/', {
|
||||||
|
challengeInfo: {
|
||||||
|
challengeId: common.challengeId,
|
||||||
|
challengeName: common.challengeName,
|
||||||
|
completedWith: didCompleteWith,
|
||||||
|
challengeType: common.challengeType,
|
||||||
|
solution: bonfireSolution
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function(res) {
|
||||||
|
if (res) {
|
||||||
|
window.location =
|
||||||
|
'/challenges/next-challenge?id=' + common.challengeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return common;
|
||||||
|
}(window));
|
200
client/commonFramework/step-challenge.js
Normal file
200
client/commonFramework/step-challenge.js
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
window.common = (function({ $, common = { init: [] }}) {
|
||||||
|
const stepClass = '.challenge-step';
|
||||||
|
const prevBtnClass = '.challenge-step-btn-prev';
|
||||||
|
const nextBtnClass = '.challenge-step-btn-next';
|
||||||
|
const actionBtnClass = '.challenge-step-btn-action';
|
||||||
|
const finishBtnClass = '.challenge-step-btn-finish';
|
||||||
|
const submitBtnId = '#challenge-step-btn-submit';
|
||||||
|
const submitModalId = '#challenge-step-modal';
|
||||||
|
|
||||||
|
function getPreviousStep($challengeSteps) {
|
||||||
|
var $prevStep = false;
|
||||||
|
var prevStepIndex = 0;
|
||||||
|
$challengeSteps.each(function(index) {
|
||||||
|
var $step = $(this);
|
||||||
|
if (!$step.hasClass('hidden')) {
|
||||||
|
prevStepIndex = index - 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$prevStep = $challengeSteps[prevStepIndex];
|
||||||
|
|
||||||
|
return $prevStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextStep($challengeSteps) {
|
||||||
|
var length = $challengeSteps.length;
|
||||||
|
var $nextStep = false;
|
||||||
|
var nextStepIndex = 0;
|
||||||
|
$challengeSteps.each(function(index) {
|
||||||
|
var $step = $(this);
|
||||||
|
if (
|
||||||
|
!$step.hasClass('hidden') &&
|
||||||
|
index + 1 !== length
|
||||||
|
) {
|
||||||
|
nextStepIndex = index + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$nextStep = $challengeSteps[nextStepIndex];
|
||||||
|
|
||||||
|
return $nextStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePrevStepClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var prevStep = getPreviousStep($(stepClass));
|
||||||
|
$(this)
|
||||||
|
.parent()
|
||||||
|
.parent()
|
||||||
|
.removeClass('slideInLeft slideInRight')
|
||||||
|
.addClass('animated fadeOutRight fast-animation')
|
||||||
|
.delay(250)
|
||||||
|
.queue(function(prev) {
|
||||||
|
$(this).addClass('hidden');
|
||||||
|
if (prevStep) {
|
||||||
|
$(prevStep)
|
||||||
|
.removeClass('hidden')
|
||||||
|
.removeClass('fadeOutLeft fadeOutRight')
|
||||||
|
.addClass('animated slideInLeft fast-animation')
|
||||||
|
.delay(500)
|
||||||
|
.queue(function(prev) {
|
||||||
|
prev();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
prev();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNextStepClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var nextStep = getNextStep($(stepClass));
|
||||||
|
$(this)
|
||||||
|
.parent()
|
||||||
|
.parent()
|
||||||
|
.removeClass('slideInRight slideInLeft')
|
||||||
|
.addClass('animated fadeOutLeft fast-animation')
|
||||||
|
.delay(250)
|
||||||
|
.queue(function(next) {
|
||||||
|
$(this).addClass('hidden');
|
||||||
|
if (nextStep) {
|
||||||
|
$(nextStep)
|
||||||
|
.removeClass('hidden')
|
||||||
|
.removeClass('fadeOutRight fadeOutLeft')
|
||||||
|
.addClass('animated slideInRight fast-animation')
|
||||||
|
.delay(500)
|
||||||
|
.queue(function(next) {
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleActionClick(e) {
|
||||||
|
var props = common.challengeSeed[0] ||
|
||||||
|
{ stepIndex: [] };
|
||||||
|
|
||||||
|
var $el = $(this);
|
||||||
|
var index = +$el.attr('id');
|
||||||
|
var propIndex = props.stepIndex.indexOf(index);
|
||||||
|
|
||||||
|
if (propIndex === -1) {
|
||||||
|
return $el
|
||||||
|
.parent()
|
||||||
|
.find('.disabled')
|
||||||
|
.removeClass('disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
// an API action
|
||||||
|
// prevent link from opening
|
||||||
|
e.preventDefault();
|
||||||
|
var prop = props.properties[propIndex];
|
||||||
|
var api = props.apis[propIndex];
|
||||||
|
if (common[prop]) {
|
||||||
|
return $el
|
||||||
|
.parent()
|
||||||
|
.find('.disabled')
|
||||||
|
.removeClass('disabled');
|
||||||
|
}
|
||||||
|
$
|
||||||
|
.post(api)
|
||||||
|
.done(function(data) {
|
||||||
|
// assume a boolean indicates passing
|
||||||
|
if (typeof data === 'boolean') {
|
||||||
|
return $el
|
||||||
|
.parent()
|
||||||
|
.find('.disabled')
|
||||||
|
.removeClass('disabled');
|
||||||
|
}
|
||||||
|
// assume api returns string when fails
|
||||||
|
$el
|
||||||
|
.parent()
|
||||||
|
.find('.disabled')
|
||||||
|
.replaceWith('<p>' + data + '</p>');
|
||||||
|
})
|
||||||
|
.fail(function() {
|
||||||
|
console.log('failed');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFinishClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$(submitModalId).modal('show');
|
||||||
|
$(submitModalId + '.modal-header').click();
|
||||||
|
$(submitBtnId).click(handleSubmitClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmitClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
$('#submit-challenge')
|
||||||
|
.attr('disabled', 'true')
|
||||||
|
.removeClass('btn-primary')
|
||||||
|
.addClass('btn-warning disabled');
|
||||||
|
|
||||||
|
var $checkmarkContainer = $('#checkmark-container');
|
||||||
|
$checkmarkContainer.css({ height: $checkmarkContainer.innerHeight() });
|
||||||
|
|
||||||
|
$('#challenge-checkmark')
|
||||||
|
.addClass('zoomOutUp')
|
||||||
|
.delay(1000)
|
||||||
|
.queue(function(next) {
|
||||||
|
$(this).replaceWith(
|
||||||
|
'<div id="challenge-spinner" ' +
|
||||||
|
'class="animated zoomInUp inner-circles-loader">' +
|
||||||
|
'submitting...</div>'
|
||||||
|
);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
$.post(
|
||||||
|
'/completed-bonfire/', {
|
||||||
|
challengeInfo: {
|
||||||
|
challengeId: common.challengeId,
|
||||||
|
challengeName: common.challengeName,
|
||||||
|
challengeType: common.challengeType
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function(res) {
|
||||||
|
if (res) {
|
||||||
|
window.location =
|
||||||
|
'/challenges/next-challenge?id=' + common.challengeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
common.init.push(function($) {
|
||||||
|
if (common.challengeType !== '7') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(prevBtnClass).click(handlePrevStepClick);
|
||||||
|
$(nextBtnClass).click(handleNextStepClick);
|
||||||
|
$(actionBtnClass).click(handleActionClick);
|
||||||
|
$(finishBtnClass).click(handleFinishClick);
|
||||||
|
});
|
||||||
|
|
||||||
|
return common;
|
||||||
|
}(window));
|
99
client/commonFramework/update-preview.js
Normal file
99
client/commonFramework/update-preview.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
window.common = (function(global) {
|
||||||
|
const {
|
||||||
|
Rx: { BehaviorSubject, Observable },
|
||||||
|
common = { init: [] }
|
||||||
|
} = global;
|
||||||
|
|
||||||
|
// the first script tag here is to proxy jQuery
|
||||||
|
// We use the same jQuery on the main window but we change the
|
||||||
|
// context to that of the iframe.
|
||||||
|
var libraryIncludes = `
|
||||||
|
<script>
|
||||||
|
window.loopProtect = parent.loopProtect;
|
||||||
|
window.__err = null;
|
||||||
|
window.loopProtect.hit = function(line) {
|
||||||
|
window.__err = new Error(
|
||||||
|
'Potential infinite loop at line ' + line
|
||||||
|
);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<link
|
||||||
|
rel='stylesheet'
|
||||||
|
href='//cdnjs.cloudflare.com/ajax/libs/animate.css/3.2.0/animate.min.css'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='stylesheet'
|
||||||
|
href='//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel='stylesheet'
|
||||||
|
href='//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css'
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
body { padding: 0px 3px 0px 3px; }
|
||||||
|
</style>
|
||||||
|
`;
|
||||||
|
const codeDisabledError = `
|
||||||
|
<script>
|
||||||
|
window.__err = new Error('code has been disabled');
|
||||||
|
</script>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const iFrameScript$ =
|
||||||
|
common.getScriptContent$('/js/iFrameScripts.js').shareReplay();
|
||||||
|
const jQueryScript$ = common.getScriptContent$(
|
||||||
|
'/bower_components/jquery/dist/jquery.js'
|
||||||
|
).shareReplay();
|
||||||
|
|
||||||
|
// behavior subject allways remembers the last value
|
||||||
|
// we use this to determine if runPreviewTest$ is defined
|
||||||
|
// and prime it with false
|
||||||
|
common.previewReady$ = new BehaviorSubject(false);
|
||||||
|
|
||||||
|
// These should be set up in the preview window
|
||||||
|
// if this error is seen it is because the function tried to run
|
||||||
|
// before the iframe has completely loaded
|
||||||
|
common.runPreviewTests$ =
|
||||||
|
common.checkPreview$ =
|
||||||
|
() => Observable.throw(new Error('Preview not fully loaded'));
|
||||||
|
|
||||||
|
|
||||||
|
common.updatePreview$ = function updatePreview$(code = '') {
|
||||||
|
const preview = common.getIframe('preview');
|
||||||
|
|
||||||
|
return Observable.combineLatest(
|
||||||
|
iFrameScript$,
|
||||||
|
jQueryScript$,
|
||||||
|
(iframe, jQuery) => ({
|
||||||
|
iframeScript: `<script>${iframe}</script>`,
|
||||||
|
jQuery: `<script>${jQuery}</script>`
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
.flatMap(({ iframeScript, jQuery }) => {
|
||||||
|
// we make sure to override the last value in the
|
||||||
|
// subject to false here.
|
||||||
|
common.previewReady$.onNext(false);
|
||||||
|
preview.open();
|
||||||
|
preview.write(
|
||||||
|
libraryIncludes +
|
||||||
|
jQuery +
|
||||||
|
(common.shouldRun() ? code : codeDisabledError) +
|
||||||
|
'<!-- -->' +
|
||||||
|
iframeScript
|
||||||
|
);
|
||||||
|
preview.close();
|
||||||
|
// now we filter false values and wait for the first true
|
||||||
|
return common.previewReady$
|
||||||
|
.filter(ready => ready)
|
||||||
|
.first()
|
||||||
|
// the delay here is to give code within the iframe
|
||||||
|
// control to run
|
||||||
|
.delay(400);
|
||||||
|
})
|
||||||
|
.map(() => code);
|
||||||
|
};
|
||||||
|
|
||||||
|
return common;
|
||||||
|
}(window));
|
@ -1,23 +1,85 @@
|
|||||||
(function() {
|
/* eslint-disable no-undef, no-unused-vars, no-native-reassign */
|
||||||
var expect = chai.expect;
|
// the $ on the iframe window object is the same
|
||||||
|
// as the one used on the main site, but
|
||||||
|
// uses the iframe document as the context
|
||||||
|
window.$(document).ready(function() {
|
||||||
|
var _ = parent._;
|
||||||
|
var Rx = parent.Rx;
|
||||||
|
var chai = parent.chai;
|
||||||
|
var assert = chai.assert;
|
||||||
var tests = parent.tests;
|
var tests = parent.tests;
|
||||||
var editor = parent.editorValueForIFrame;
|
var common = parent.common;
|
||||||
|
|
||||||
setTimeout(function() {
|
common.getJsOutput = function evalJs(code = '') {
|
||||||
for (var i = 0; i < tests.length; i++) {
|
if (window.__err || !common.shouldRun()) {
|
||||||
var thisTest = true;
|
return window.__err || 'code disabled';
|
||||||
try {
|
|
||||||
eval(parent.tests[i]);
|
|
||||||
} catch (err) {
|
|
||||||
allTestsGood = false;
|
|
||||||
thisTest = false;
|
|
||||||
parent.postError(JSON.stringify(err.message.split(':').shift()));
|
|
||||||
} finally {
|
|
||||||
if (thisTest) {
|
|
||||||
parent.postSuccess(JSON.stringify(tests[i].split(',').pop().replace(
|
|
||||||
/\'/g, '').replace(/\)/, '')));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, 10);
|
let output;
|
||||||
})();
|
try {
|
||||||
|
/* eslint-disable no-eval */
|
||||||
|
output = eval(code);
|
||||||
|
/* eslint-enable no-eval */
|
||||||
|
} catch (e) {
|
||||||
|
window.__err = e;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
common.runPreviewTests$ =
|
||||||
|
function runPreviewTests$({
|
||||||
|
tests = [],
|
||||||
|
originalCode,
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
|
const code = originalCode;
|
||||||
|
const editor = { getValue() { return originalCode; } };
|
||||||
|
if (window.__err) {
|
||||||
|
return Rx.Observable.throw(window.__err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate throught the test one at a time
|
||||||
|
// on new stacks
|
||||||
|
return Rx.Observable.from(tests, null, null, Rx.Scheduler.default)
|
||||||
|
// add delay here for firefox to catch up
|
||||||
|
.delay(100)
|
||||||
|
.map(test => {
|
||||||
|
const userTest = {};
|
||||||
|
try {
|
||||||
|
/* eslint-disable no-eval */
|
||||||
|
eval(test);
|
||||||
|
/* eslint-enable no-eval */
|
||||||
|
} catch (e) {
|
||||||
|
userTest.err = e.message.split(':').shift();
|
||||||
|
} finally {
|
||||||
|
if (!test.match(/message: /g)) {
|
||||||
|
// assumes test does not contain arrays
|
||||||
|
// This is a patch until all test fall into this pattern
|
||||||
|
userTest.text = test
|
||||||
|
.split(',')
|
||||||
|
.pop();
|
||||||
|
userTest.text = 'message: ' + userTest.text + '\');';
|
||||||
|
} else {
|
||||||
|
userTest.text = test;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return userTest;
|
||||||
|
})
|
||||||
|
// gather tests back into an array
|
||||||
|
.toArray()
|
||||||
|
.map(tests => ({ ...rest, tests, originalCode }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// used when updating preview without running tests
|
||||||
|
common.checkPreview$ = function checkPreview$(args) {
|
||||||
|
if (window.__err) {
|
||||||
|
return Rx.Observable.throw(window.__err);
|
||||||
|
}
|
||||||
|
return Rx.Observable.just(args);
|
||||||
|
};
|
||||||
|
|
||||||
|
// now that the runPreviewTest$ is defined
|
||||||
|
// we set the subject to true
|
||||||
|
// this will let the updatePreview
|
||||||
|
// script now that we are ready.
|
||||||
|
common.previewReady$.onNext(true);
|
||||||
|
});
|
||||||
|
@ -22,6 +22,19 @@ const history = createHistory();
|
|||||||
const appLocation = createLocation(
|
const appLocation = createLocation(
|
||||||
location.pathname + location.search
|
location.pathname + location.search
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function location$(history) {
|
||||||
|
return Rx.Observable.create(function(observer) {
|
||||||
|
const dispose = history.listen(function(location) {
|
||||||
|
observer.onNext(location.pathname);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Rx.Disposable.create(() => {
|
||||||
|
dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// returns an observable
|
// returns an observable
|
||||||
app$({ history, location: appLocation })
|
app$({ history, location: appLocation })
|
||||||
.flatMap(
|
.flatMap(
|
||||||
@ -36,6 +49,27 @@ app$({ history, location: appLocation })
|
|||||||
// redirects in the future
|
// redirects in the future
|
||||||
({ nextLocation, props }, appCat) => ({ nextLocation, props, appCat })
|
({ nextLocation, props }, appCat) => ({ nextLocation, props, appCat })
|
||||||
)
|
)
|
||||||
|
.doOnNext(({ appCat }) => {
|
||||||
|
const appActions = appCat.getActions('appActions');
|
||||||
|
|
||||||
|
location$(history)
|
||||||
|
.pluck('pathname')
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.doOnNext(route => debug('route change', route))
|
||||||
|
.subscribe(route => appActions.updateRoute(route));
|
||||||
|
|
||||||
|
appActions.goBack.subscribe(function() {
|
||||||
|
history.goBack();
|
||||||
|
});
|
||||||
|
|
||||||
|
appActions
|
||||||
|
.updateRoute
|
||||||
|
.pluck('route')
|
||||||
|
.doOnNext(route => debug('update route', route))
|
||||||
|
.subscribe(function(route) {
|
||||||
|
history.pushState(null, route);
|
||||||
|
});
|
||||||
|
})
|
||||||
.flatMap(({ props, appCat }) => {
|
.flatMap(({ props, appCat }) => {
|
||||||
props.history = history;
|
props.history = history;
|
||||||
return Render(
|
return Render(
|
||||||
@ -49,7 +83,7 @@ app$({ history, location: appLocation })
|
|||||||
debug('react rendered');
|
debug('react rendered');
|
||||||
},
|
},
|
||||||
err => {
|
err => {
|
||||||
debug('an error has occured', err.stack);
|
throw err;
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
debug('react closed subscription');
|
debug('react closed subscription');
|
||||||
|
24
client/less/chat.less
Normal file
24
client/less/chat.less
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
.chat-embed-main-title {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-left: 31px;
|
||||||
|
padding-top: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gitter-chat-embed {
|
||||||
|
z-index: 100;
|
||||||
|
position: fixed;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 60%;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
transition: transform 0.3s cubic-bezier(0.16, 0.22, 0.22, 1.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gitter-chat-embed.is-collapsed:not(.is-loading) {
|
||||||
|
transform: translateX(110%);
|
||||||
|
}
|
16
client/less/jobs.less
Normal file
16
client/less/jobs.less
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
.jobs-list-highlight {
|
||||||
|
background-color: #ffc
|
||||||
|
}
|
||||||
|
|
||||||
|
a.jobs-list-highlight:hover {
|
||||||
|
background-color: #ffc
|
||||||
|
}
|
||||||
|
|
||||||
|
.jobs-list {
|
||||||
|
cursor: pointer;
|
||||||
|
cursor: hand;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jobs-checkbox-spacer input[type="checkbox"] {
|
||||||
|
margin-left: -23px
|
||||||
|
}
|
@ -424,7 +424,6 @@
|
|||||||
&:hover,
|
&:hover,
|
||||||
&:focus {
|
&:focus {
|
||||||
color: @navbar-default-link-active-color;
|
color: @navbar-default-link-active-color;
|
||||||
background-color: @navbar-default-link-active-bg;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
> .disabled > a {
|
> .disabled > a {
|
||||||
|
@ -9,6 +9,11 @@ html,body,div,span,a,li,td,th {
|
|||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bold {
|
||||||
|
font-family: 'Lato-Bold', sans-serif;
|
||||||
|
font-weight: Bold;
|
||||||
|
}
|
||||||
|
|
||||||
li, .wrappable {
|
li, .wrappable {
|
||||||
white-space: pre; /* CSS 2.0 */
|
white-space: pre; /* CSS 2.0 */
|
||||||
white-space: pre-wrap; /* CSS 2.1 */
|
white-space: pre-wrap; /* CSS 2.1 */
|
||||||
@ -57,7 +62,7 @@ body.top-and-bottom-margins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.no-top-and-bottom-margins {
|
body.no-top-and-bottom-margins {
|
||||||
margin: 70px 20px 50px 20px;
|
margin: 70px 20px 0px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2 {
|
h1, h2 {
|
||||||
@ -96,6 +101,10 @@ h1, h2, h3, h4, h5, h6, p, li {
|
|||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fa:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.img-center {
|
.img-center {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
@ -141,6 +150,12 @@ ul {
|
|||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar > .container {
|
||||||
|
width: auto;
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-height {
|
.nav-height {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
border: none;
|
border: none;
|
||||||
@ -216,8 +231,13 @@ ul {
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.large-li {
|
.link-table td{
|
||||||
font-size: 24px;
|
font-size: 16px;
|
||||||
|
border-top: none !important;
|
||||||
|
@media (min-width: 767px) {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.capitalize {
|
.capitalize {
|
||||||
@ -374,10 +394,6 @@ ul {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
|
||||||
margin-left: -16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead {
|
thead {
|
||||||
font-size: 150%;
|
font-size: 150%;
|
||||||
}
|
}
|
||||||
@ -396,6 +412,19 @@ thead {
|
|||||||
width: 50px;
|
width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.brownie-points-nav {
|
||||||
|
@media (min-width: 991px) and (max-width: 999px) {
|
||||||
|
margin-right: -10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.signin-button-nav {
|
||||||
|
@media (min-width: 991px) and (max-width: 1010px) {
|
||||||
|
margin-left: -10px;
|
||||||
|
margin-right: -5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.navbar-nav a {
|
.navbar-nav a {
|
||||||
color: @gray-lighter;
|
color: @gray-lighter;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
@ -415,6 +444,7 @@ thead {
|
|||||||
margin-top: -2px !important;
|
margin-top: -2px !important;
|
||||||
padding-top: 10px !important;
|
padding-top: 10px !important;
|
||||||
padding-bottom: 10px !important;
|
padding-bottom: 10px !important;
|
||||||
|
margin-right: -12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.public-profile-img {
|
.public-profile-img {
|
||||||
@ -486,15 +516,27 @@ thead {
|
|||||||
color: #009900
|
color: #009900
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.testimonial-image-jobs {
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #009900
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.default-border-radius {
|
.default-border-radius {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.story-section {
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
.testimonial-copy {
|
.testimonial-copy {
|
||||||
font-size: 20px;
|
text-align: justify;
|
||||||
text-align: center;
|
font-size: 18px !important;
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-right: 20px;
|
||||||
@media (min-width: 991px) and (max-width: 1199px) {
|
@media (min-width: 991px) and (max-width: 1199px) {
|
||||||
height: 120px;
|
height: 140px;
|
||||||
}
|
}
|
||||||
@media (min-width: 1200px) {
|
@media (min-width: 1200px) {
|
||||||
height: 90px;
|
height: 90px;
|
||||||
@ -582,6 +624,10 @@ form.code span {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.CodeMirror {
|
||||||
|
line-height: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.CodeMirror-linenumber {
|
.CodeMirror-linenumber {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-family: "Ubuntu Mono";
|
font-family: "Ubuntu Mono";
|
||||||
@ -679,6 +725,24 @@ iframe.iphone {
|
|||||||
transition: background .2s ease-in-out, border .2s ease-in-out;
|
transition: background .2s ease-in-out, border .2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-warning-ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: @brand-warning;
|
||||||
|
|
||||||
|
/* CSS Transition */
|
||||||
|
-webkit-transition: background .2s ease-in-out, border .2s ease-in-out;
|
||||||
|
-moz-transition: background .2s ease-in-out, border .2s ease-in-out;
|
||||||
|
-ms-transition: background .2s ease-in-out, border .2s ease-in-out;
|
||||||
|
-o-transition: background .2s ease-in-out, border .2s ease-in-out;
|
||||||
|
transition: background .2s ease-in-out, border .2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.population-table {
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 991px) {
|
@media (max-width: 991px) {
|
||||||
.navbar-header {
|
.navbar-header {
|
||||||
float: none;
|
float: none;
|
||||||
@ -721,7 +785,7 @@ iframe.iphone {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hamburger {
|
.navbar-toggle {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
@ -781,7 +845,8 @@ iframe.iphone {
|
|||||||
|
|
||||||
.news-box-search {
|
.news-box-search {
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
margin-top: -50px;
|
margin-top: -30px;
|
||||||
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
@ -800,6 +865,10 @@ iframe.iphone {
|
|||||||
margin-top: -5px;
|
margin-top: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.landing-heading {
|
||||||
|
font-size: 50px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.mobile-story-headline {
|
.mobile-story-headline {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
@ -814,15 +883,9 @@ iframe.iphone {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
-moz-border-bottom-colors: none;
|
border: 0;
|
||||||
-moz-border-image: none;
|
height: 1px;
|
||||||
-moz-border-left-colors: none;
|
background-image: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0));
|
||||||
-moz-border-right-colors: none;
|
|
||||||
-moz-border-top-colors: none;
|
|
||||||
border-color: @gray;
|
|
||||||
border-style: solid none;
|
|
||||||
border-width: 1px 0;
|
|
||||||
margin: 18px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.big-spacer {
|
.big-spacer {
|
||||||
@ -945,6 +1008,11 @@ code {
|
|||||||
margin: 0!important;
|
margin: 0!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gitter-chat-embed {
|
||||||
|
z-index: 20000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//uncomment this to see the dimensions of all elements outlined in red
|
//uncomment this to see the dimensions of all elements outlined in red
|
||||||
//* {
|
//* {
|
||||||
// border-color: red;
|
// border-color: red;
|
||||||
@ -1052,3 +1120,6 @@ code {
|
|||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@import "chat.less";
|
||||||
|
@import "jobs.less";
|
||||||
|
496
client/main.js
496
client/main.js
@ -1,19 +1,127 @@
|
|||||||
var mapShareKey = 'map-shares';
|
var main = window.main || {};
|
||||||
|
|
||||||
|
main.mapShareKey = 'map-shares';
|
||||||
|
|
||||||
|
main.ga = window.ga || function() {};
|
||||||
|
|
||||||
|
main = (function(main) {
|
||||||
|
|
||||||
|
// should be set before gitter script loads
|
||||||
|
((window.gitter = {}).chat = {}).options = {
|
||||||
|
disableDefaultChat: true
|
||||||
|
};
|
||||||
|
// wait for sidecar to load
|
||||||
|
|
||||||
|
main.chat = {};
|
||||||
|
main.chat.isOpen = false;
|
||||||
|
main.chat.createHelpChat = function createHelpChat() {
|
||||||
|
throw new Error('Sidecar chat has not initialized');
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('gitter-sidecar-ready', function(e) {
|
||||||
|
main.chat.GitterChat = e.detail.Chat;
|
||||||
|
|
||||||
|
main.chat.createHelpChat = function(room, helpChatBtnClass, roomTitle) {
|
||||||
|
roomTitle = roomTitle || 'Waypoint Help';
|
||||||
|
|
||||||
|
$('body').append(
|
||||||
|
'<aside id="chat-embed-help" class="gitter-chat-embed is-collapsed" />'
|
||||||
|
);
|
||||||
|
|
||||||
|
main.chat.helpChat = new main.chat.GitterChat({
|
||||||
|
room: room,
|
||||||
|
activationElement: false,
|
||||||
|
targetElement: $('#chat-embed-help')
|
||||||
|
});
|
||||||
|
|
||||||
|
$(helpChatBtnClass).on('click', function() {
|
||||||
|
// is button already pressed?
|
||||||
|
// no? open chat
|
||||||
|
// yes? close chat
|
||||||
|
var shouldChatBeOpen = !$(this).hasClass('active');
|
||||||
|
main.chat.helpChat.toggleChat(shouldChatBeOpen);
|
||||||
|
if (shouldChatBeOpen) {
|
||||||
|
$(helpChatBtnClass).addClass('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var helpTitleAdd = false;
|
||||||
|
$('#chat-embed-help').on('gitter-chat-toggle', function(e) {
|
||||||
|
var shouldButtonBePressed = !!e.originalEvent.detail.state;
|
||||||
|
|
||||||
|
if (!helpTitleAdd) {
|
||||||
|
helpTitleAdd = true;
|
||||||
|
$('#chat-embed-help > .gitter-chat-embed-action-bar').prepend(
|
||||||
|
'<div class="chat-embed-main-title">' +
|
||||||
|
'<span>' +
|
||||||
|
roomTitle +
|
||||||
|
'</span>' +
|
||||||
|
'</div>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldButtonBePressed) {
|
||||||
|
return $(helpChatBtnClass).addClass('active');
|
||||||
|
}
|
||||||
|
return $(helpChatBtnClass).removeClass('active');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$('body').append(
|
||||||
|
'<aside id="chat-embed-main" class="gitter-chat-embed is-collapsed" />'
|
||||||
|
);
|
||||||
|
|
||||||
|
main.chat.mainChat = new main.chat.GitterChat({
|
||||||
|
room: 'freecodecamp/freecodecamp',
|
||||||
|
activationElement: false,
|
||||||
|
targetElement: $('#chat-embed-main')
|
||||||
|
});
|
||||||
|
|
||||||
|
var mainChatTitleAdded = false;
|
||||||
|
$('#chat-embed-main').on('gitter-chat-toggle', function() {
|
||||||
|
if (mainChatTitleAdded) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
mainChatTitleAdded = true;
|
||||||
|
|
||||||
|
$('#chat-embed-main > .gitter-chat-embed-action-bar').prepend(
|
||||||
|
'<div class="chat-embed-main-title">' +
|
||||||
|
'<span>Free Code Camp\'s Main Chat</span>' +
|
||||||
|
'</div>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$('#nav-chat-btn').on('click', function() {
|
||||||
|
if (!main.chat.isOpen) {
|
||||||
|
|
||||||
|
main.chat.mainChat.toggleChat(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return main;
|
||||||
|
}(main));
|
||||||
|
|
||||||
var lastCompleted = typeof lastCompleted !== 'undefined' ?
|
var lastCompleted = typeof lastCompleted !== 'undefined' ?
|
||||||
lastCompleted :
|
lastCompleted :
|
||||||
'';
|
'';
|
||||||
|
|
||||||
function getMapShares() {
|
main.getMapShares = function getMapShares() {
|
||||||
var alreadyShared = JSON.parse(localStorage.getItem(mapShareKey) || '[]');
|
var alreadyShared = JSON.parse(
|
||||||
|
localStorage.getItem(main.mapShareKey) ||
|
||||||
|
'[]'
|
||||||
|
);
|
||||||
|
|
||||||
if (!alreadyShared || !Array.isArray(alreadyShared)) {
|
if (!alreadyShared || !Array.isArray(alreadyShared)) {
|
||||||
localStorage.setItem(mapShareKey, JSON.stringify([]));
|
localStorage.setItem(main.mapShareKey, JSON.stringify([]));
|
||||||
alreadyShared = [];
|
alreadyShared = [];
|
||||||
}
|
}
|
||||||
return alreadyShared;
|
return alreadyShared;
|
||||||
}
|
};
|
||||||
|
|
||||||
function setMapShare(id) {
|
main.setMapShare = function setMapShare(id) {
|
||||||
var alreadyShared = getMapShares();
|
var alreadyShared = main.getMapShares();
|
||||||
var found = false;
|
var found = false;
|
||||||
alreadyShared.forEach(function(_id) {
|
alreadyShared.forEach(function(_id) {
|
||||||
if (_id === id) {
|
if (_id === id) {
|
||||||
@ -23,24 +131,12 @@ function setMapShare(id) {
|
|||||||
if (!found) {
|
if (!found) {
|
||||||
alreadyShared.push(id);
|
alreadyShared.push(id);
|
||||||
}
|
}
|
||||||
localStorage.setItem(mapShareKey, JSON.stringify(alreadyShared));
|
localStorage.setItem(main.mapShareKey, JSON.stringify(alreadyShared));
|
||||||
return alreadyShared;
|
return alreadyShared;
|
||||||
}
|
};
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|
||||||
var challengeName = typeof challengeName !== 'undefined' ?
|
|
||||||
challengeName :
|
|
||||||
'Untitled';
|
|
||||||
|
|
||||||
if (challengeName) {
|
|
||||||
ga('send', 'event', 'Challenge', 'load', challengeName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof editor !== 'undefined') {
|
|
||||||
$('#reset-button').on('click', resetEditor);
|
|
||||||
}
|
|
||||||
|
|
||||||
var CSRF_HEADER = 'X-CSRF-Token';
|
var CSRF_HEADER = 'X-CSRF-Token';
|
||||||
|
|
||||||
var setCSRFToken = function(securityToken) {
|
var setCSRFToken = function(securityToken) {
|
||||||
@ -53,247 +149,15 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
setCSRFToken($('meta[name="csrf-token"]').attr('content'));
|
setCSRFToken($('meta[name="csrf-token"]').attr('content'));
|
||||||
|
|
||||||
$('.checklist-element').each(function() {
|
$('img').error(function() {
|
||||||
var checklistElementId = $(this).attr('id');
|
$(this)
|
||||||
if(!!localStorage[checklistElementId]) {
|
.unbind('error')
|
||||||
$(this).children().children('li').addClass('faded');
|
.attr(
|
||||||
$(this).children().children('input').trigger('click');
|
'src',
|
||||||
}
|
'https://s3.amazonaws.com/freecodecamp/camper-image-placeholder.png'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.start-challenge').on('click', function() {
|
|
||||||
$(this).parent().remove();
|
|
||||||
$('.challenge-content')
|
|
||||||
.removeClass('hidden-element')
|
|
||||||
.addClass('animated fadeInDown');
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.challenge-list-checkbox').on('change', function() {
|
|
||||||
var checkboxId = $(this).parent().parent().attr('id');
|
|
||||||
if ($(this).is(":checked")) {
|
|
||||||
$(this).parent().siblings().children().addClass('faded');
|
|
||||||
if (!localStorage || !localStorage[checkboxId]) {
|
|
||||||
localStorage[checkboxId] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!$(this).is(":checked")) {
|
|
||||||
$(this).parent().siblings().children().removeClass('faded');
|
|
||||||
if (localStorage[checkboxId]) {
|
|
||||||
localStorage.removeItem(checkboxId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$("img").error(function () {
|
|
||||||
$(this).unbind("error").attr("src", "https://s3.amazonaws.com/freecodecamp/camper-image-placeholder.png");
|
|
||||||
});
|
|
||||||
|
|
||||||
function reBindModals(){
|
|
||||||
|
|
||||||
$('.close-modal').unbind('click');
|
|
||||||
$('.close-modal').on('click', function(){
|
|
||||||
setTimeout(function() {
|
|
||||||
$('.close-modal').parent().parent().parent().parent().modal('hide');
|
|
||||||
}, 200);
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#search-issue').unbind('click');
|
|
||||||
$('#search-issue').on('click', function() {
|
|
||||||
var queryIssue = window.location.href.toString();
|
|
||||||
window.open('https://github.com/FreeCodeCamp/FreeCodeCamp/issues?q=' +
|
|
||||||
'is:issue is:all ' + (challenge_Name || challengeName) + ' OR ' +
|
|
||||||
queryIssue.substr(queryIssue.lastIndexOf('challenges/') + 11)
|
|
||||||
.replace('/', ''), '_blank');
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#help-ive-found-a-bug-wiki-article').unbind('click');
|
|
||||||
$('#help-ive-found-a-bug-wiki-article').on('click', function() {
|
|
||||||
window.open("https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/Help-I've-Found-a-Bug", '_blank');
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#report-issue').unbind('click');
|
|
||||||
$('#report-issue').on('click', function() {
|
|
||||||
var textMessage = [
|
|
||||||
'Challenge [',
|
|
||||||
(challenge_Name || challengeName || window.location.href),
|
|
||||||
'](',
|
|
||||||
window.location.href,
|
|
||||||
') has an issue.\n',
|
|
||||||
'User Agent is: <code>',
|
|
||||||
navigator.userAgent,
|
|
||||||
'</code>.\n',
|
|
||||||
'Please describe how to reproduce this issue, and include ',
|
|
||||||
'links to screenshots if possible.\n\n'
|
|
||||||
].join('');
|
|
||||||
|
|
||||||
if (editor.getValue().trim()) {
|
|
||||||
var type;
|
|
||||||
switch (challengeType) {
|
|
||||||
case challengeTypes.HTML_CSS_JQ:
|
|
||||||
type = 'html';
|
|
||||||
break;
|
|
||||||
case challengeTypes.JAVASCRIPT:
|
|
||||||
type = 'javascript';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
type = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
textMessage += [
|
|
||||||
'My code:\n```',
|
|
||||||
type,
|
|
||||||
'\n',
|
|
||||||
editor.getValue(),
|
|
||||||
'\n```\n\n'
|
|
||||||
].join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
textMessage = encodeURIComponent(textMessage);
|
|
||||||
|
|
||||||
$('#issue-modal').modal('hide');
|
|
||||||
window.open(
|
|
||||||
'https://github.com/freecodecamp/freecodecamp/issues/new?&body=' +
|
|
||||||
textMessage, '_blank'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#completed-courseware').unbind('click');
|
|
||||||
$('#completed-courseware').on('click', function() {
|
|
||||||
$('#complete-courseware-dialog').modal('show');
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#completed-courseware-editorless').unbind('click');
|
|
||||||
$('#completed-courseware-editorless').on('click', function() {
|
|
||||||
$('#complete-courseware-editorless-dialog').modal('show');
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#trigger-pair-modal').unbind('click');
|
|
||||||
$('#trigger-pair-modal').on('click', function() {
|
|
||||||
$('#pair-modal').modal('show');
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#trigger-reset-modal').unbind('click');
|
|
||||||
$('#trigger-reset-modal').on('click', function() {
|
|
||||||
$('#reset-modal').modal('show');
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#trigger-help-modal').unbind('click');
|
|
||||||
$('#trigger-help-modal').on('click', function() {
|
|
||||||
$('#help-modal').modal('show');
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#trigger-issue-modal').unbind('click');
|
|
||||||
$('#trigger-issue-modal').on('click', function() {
|
|
||||||
$('#issue-modal').modal('show');
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#completed-zipline-or-basejump').unbind('click');
|
|
||||||
$('#completed-zipline-or-basejump').on('click', function() {
|
|
||||||
$('#complete-zipline-or-basejump-dialog').modal('show');
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#next-courseware-button').unbind('click');
|
|
||||||
$('#next-courseware-button').on('click', function() {
|
|
||||||
$('#next-courseware-button').unbind('click');
|
|
||||||
if ($('.signup-btn-nav').length < 1) {
|
|
||||||
switch (challengeType) {
|
|
||||||
case challengeTypes.HTML_CSS_JQ:
|
|
||||||
case challengeTypes.JAVASCRIPT:
|
|
||||||
case challengeTypes.VIDEO:
|
|
||||||
$.post(
|
|
||||||
'/completed-challenge/',
|
|
||||||
{
|
|
||||||
challengeInfo: {
|
|
||||||
challengeId: challenge_Id,
|
|
||||||
challengeName: challenge_Name
|
|
||||||
}
|
|
||||||
}).success(
|
|
||||||
function(res) {
|
|
||||||
if (res) {
|
|
||||||
window.location.href = '/challenges/next-challenge?id=' + challenge_Id;
|
|
||||||
}
|
|
||||||
}).fail(
|
|
||||||
function() {
|
|
||||||
window.location.href="/challenges";
|
|
||||||
}
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case challengeTypes.ZIPLINE:
|
|
||||||
var didCompleteWith = $('#completed-with').val() || null;
|
|
||||||
var publicURL = $('#public-url').val() || null;
|
|
||||||
console.log("debug", didCompleteWith, publicURL);
|
|
||||||
$.post(
|
|
||||||
'/completed-zipline-or-basejump/',
|
|
||||||
{
|
|
||||||
challengeInfo: {
|
|
||||||
challengeId: challenge_Id,
|
|
||||||
challengeName: challenge_Name,
|
|
||||||
completedWith: didCompleteWith,
|
|
||||||
publicURL: publicURL,
|
|
||||||
challengeType: challengeType
|
|
||||||
}
|
|
||||||
}).success(
|
|
||||||
function() {
|
|
||||||
window.location.href = '/challenges/next-challenge?id=' + challenge_Id;
|
|
||||||
}).fail(
|
|
||||||
function() {
|
|
||||||
window.location.href = '/challenges';
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case challengeTypes.BASEJUMP:
|
|
||||||
var didCompleteWith = $('#completed-with').val() || null;
|
|
||||||
var publicURL = $('#public-url').val() || null;
|
|
||||||
var githubURL = $('#github-url').val() || null;
|
|
||||||
$.post(
|
|
||||||
'/completed-zipline-or-basejump/',
|
|
||||||
{
|
|
||||||
challengeInfo: {
|
|
||||||
challengeId: challenge_Id,
|
|
||||||
challengeName: challenge_Name,
|
|
||||||
completedWith: didCompleteWith,
|
|
||||||
publicURL: publicURL,
|
|
||||||
githubURL: githubURL,
|
|
||||||
challengeType: challengeType,
|
|
||||||
verified: false
|
|
||||||
}
|
|
||||||
}).success(function() {
|
|
||||||
window.location.href = '/challenges/next-challenge?id=' + challenge_Id;
|
|
||||||
}).fail(function() {
|
|
||||||
window.location.replace(window.location.href);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case challengeTypes.BONFIRE:
|
|
||||||
window.location.href = '/challenges/next-challenge?id=' + challenge_Id;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#complete-courseware-dialog').on('hidden.bs.modal', function() {
|
|
||||||
editor.focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#complete-zipline-or-basejump').on('hidden.bs.modal', function() {
|
|
||||||
editor.focus();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$(window).resize(function(){
|
|
||||||
reBindModals();
|
|
||||||
});
|
|
||||||
|
|
||||||
reBindModals();
|
|
||||||
|
|
||||||
var challengeTypes = {
|
|
||||||
'HTML_CSS_JQ': '0',
|
|
||||||
'JAVASCRIPT': '1',
|
|
||||||
'VIDEO': '2',
|
|
||||||
'ZIPLINE': '3',
|
|
||||||
'BASEJUMP': '4',
|
|
||||||
'BONFIRE': '5'
|
|
||||||
};
|
|
||||||
|
|
||||||
function upvoteHandler(e) {
|
function upvoteHandler(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var upvoteBtn = this;
|
var upvoteBtn = this;
|
||||||
@ -309,16 +173,19 @@ $(document).ready(function() {
|
|||||||
}
|
}
|
||||||
if (!alreadyUpvoted) {
|
if (!alreadyUpvoted) {
|
||||||
$.post('/stories/upvote', { id: id })
|
$.post('/stories/upvote', { id: id })
|
||||||
.fail(function(xhr, textStatus, errorThrown) {
|
.fail(function() {
|
||||||
$(upvoteBtn).bind('click', upvoteHandler);
|
$(upvoteBtn).bind('click', upvoteHandler);
|
||||||
})
|
})
|
||||||
.done(function(data, textStatus, xhr) {
|
.done(function(data) {
|
||||||
$(upvoteBtn).text('Upvoted!').addClass('disabled');
|
$(upvoteBtn)
|
||||||
|
.text('Upvoted!')
|
||||||
|
.addClass('disabled');
|
||||||
|
|
||||||
$('#storyRank').text(data.rank + " points");
|
$('#storyRank').text(data.rank + ' points');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
$('#story-list').on('click', 'button.btn-upvote', upvoteHandler);
|
$('#story-list').on('click', 'button.btn-upvote', upvoteHandler);
|
||||||
|
|
||||||
var storySubmitButtonHandler = function storySubmitButtonHandler() {
|
var storySubmitButtonHandler = function storySubmitButtonHandler() {
|
||||||
@ -326,95 +193,33 @@ $(document).ready(function() {
|
|||||||
var link = $('#story-url').val();
|
var link = $('#story-url').val();
|
||||||
var headline = $('#story-title').val();
|
var headline = $('#story-title').val();
|
||||||
var description = $('#description-box').val();
|
var description = $('#description-box').val();
|
||||||
|
var data = {
|
||||||
|
data: {
|
||||||
|
link: link,
|
||||||
|
headline: headline,
|
||||||
|
timePosted: Date.now(),
|
||||||
|
description: description,
|
||||||
|
storyMetaDescription: main.storyMetaDescription,
|
||||||
|
rank: 1,
|
||||||
|
image: main.storyImage
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
$('#story-submit').unbind('click');
|
$('#story-submit').unbind('click');
|
||||||
$.post('/stories/',
|
$.post('/stories/', data)
|
||||||
{
|
.fail(function() {
|
||||||
data: {
|
|
||||||
link: link,
|
|
||||||
headline: headline,
|
|
||||||
timePosted: Date.now(),
|
|
||||||
description: description,
|
|
||||||
storyMetaDescription: storyMetaDescription,
|
|
||||||
rank: 1,
|
|
||||||
image: storyImage
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.fail(function (xhr, textStatus, errorThrown) {
|
|
||||||
$('#story-submit').bind('click', storySubmitButtonHandler);
|
$('#story-submit').bind('click', storySubmitButtonHandler);
|
||||||
})
|
})
|
||||||
.done(function(data, textStatus, xhr) {
|
.done(function(data) {
|
||||||
window.location = '/stories/' + data.storyLink;
|
window.location = '/stories/' + data.storyLink;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$('#story-submit').on('click', storySubmitButtonHandler);
|
$('#story-submit').on('click', storySubmitButtonHandler);
|
||||||
//fakeiphone positioning hotfix
|
|
||||||
if($('.iphone-position').html() !==undefined || $('.iphone').html() !== undefined){
|
|
||||||
var startIphonePosition = parseInt($('.iphone-position').css('top').replace('px', ''));
|
|
||||||
var startIphone = parseInt($('.iphone').css('top').replace('px', ''));
|
|
||||||
$(window).on('scroll', function(){
|
|
||||||
if((($('.courseware-height').height() + $('.courseware-height').offset().top)-$(window).scrollTop()-$('.iphone-position').height()) <= 0){
|
|
||||||
$('.iphone-position').css('top', startIphonePosition+(($('.courseware-height').height() + $('.courseware-height').offset().top)-$(window).scrollTop()-$('.iphone-position').height()));
|
|
||||||
$('.iphone').css('top', startIphonePosition+(($('.courseware-height').height() + $('.courseware-height').offset().top)-$(window).scrollTop()-$('.iphone-position').height())+120);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
$('.iphone-position').css('top', startIphonePosition);
|
|
||||||
$('.iphone').css('top', startIphone);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if($('.scroll-locker').html() != undefined){
|
|
||||||
function lockTop(){
|
|
||||||
if ($(window).width() >= 990) {
|
|
||||||
if($('.editorScrollDiv').html() !== 'undefined'){
|
|
||||||
var magiVal = $(window).height()-($('.navbar').height()+$('.footer').height());
|
|
||||||
if(magiVal < 0){
|
|
||||||
magiVal = 0;
|
|
||||||
}
|
|
||||||
$('.editorScrollDiv').css("height", (magiVal-85) + "px");
|
|
||||||
}
|
|
||||||
magiVal = $(window).height()-($('.navbar').height()+$('.footer').height());
|
|
||||||
if(magiVal < 0){
|
|
||||||
magiVal = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$('.scroll-locker').css("min-height", $('.editorScrollDiv').height()).css("height",magiVal-185);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$('.editorScrollDiv').css("max-height", 500 + "px");
|
|
||||||
$('.scroll-locker').css('position', 'inherit').css('top', 'inherit').css('width', '100%').css('max-height', '85%');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($('.scroll-locker').html()){
|
|
||||||
lockTop();
|
|
||||||
$(window).on('resize', function(){
|
|
||||||
lockTop();
|
|
||||||
});
|
|
||||||
$(window).on('scroll', function() {
|
|
||||||
lockTop();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
var execInProgress = false;
|
|
||||||
document.getElementById('scroll-locker').addEventListener('previewUpdateSpy', function(e){
|
|
||||||
if (!execInProgress){
|
|
||||||
execInProgress = true;
|
|
||||||
setTimeout(function(){
|
|
||||||
if($($('.scroll-locker').children()[0]).height()-800 > e.detail){
|
|
||||||
$('.scroll-locker').scrollTop(e.detail);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$('.scroll-locker').animate({"scrollTop":$($('.scroll-locker').children()[0]).height()}, 175);
|
|
||||||
}
|
|
||||||
execInProgress = false;
|
|
||||||
}, 750);
|
|
||||||
}
|
|
||||||
}, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// map sharing
|
// map sharing
|
||||||
var alreadyShared = getMapShares();
|
var alreadyShared = main.getMapShares();
|
||||||
|
|
||||||
if (lastCompleted && alreadyShared.indexOf(lastCompleted) === -1) {
|
if (lastCompleted && alreadyShared.indexOf(lastCompleted) === -1) {
|
||||||
$('div[id="' + lastCompleted + '"]')
|
$('div[id="' + lastCompleted + '"]')
|
||||||
@ -442,11 +247,8 @@ $(document).ready(function() {
|
|||||||
username +
|
username +
|
||||||
'&redirect_uri=http%3A%2F%2Ffreecodecamp%2Ecom%2Fmap';
|
'&redirect_uri=http%3A%2F%2Ffreecodecamp%2Ecom%2Fmap';
|
||||||
|
|
||||||
setMapShare(challengeBlockName);
|
main.setMapShare(challengeBlockName);
|
||||||
|
window.ga('send', 'event', 'FB_LINK', 'SHARE', 'Facebook map share');
|
||||||
window.location.href = link;
|
window.location.href = link;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function defCheck(a){
|
|
||||||
if(a !== 'undefined'){return(true);}else{return(false);}
|
|
||||||
}
|
|
||||||
|
@ -11,23 +11,24 @@ function importScript(url, error) {
|
|||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
function run(code) {
|
function run(code, cb) {
|
||||||
var result = {
|
var err = null;
|
||||||
input: code,
|
var result = {};
|
||||||
output: null,
|
|
||||||
error: null,
|
|
||||||
type: null
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var codeExec = runHidden(code);
|
var codeExec = runHidden(code);
|
||||||
result.type = typeof codeExec;
|
result.type = typeof codeExec;
|
||||||
result.output = stringify(codeExec);
|
result.output = stringify(codeExec);
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
result.error = e.message;
|
err = e.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
cb(err, null);
|
||||||
|
} else {
|
||||||
|
cb(null, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
application.remote.output(result);
|
|
||||||
self.close();
|
self.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,7 +36,7 @@ function run(code) {
|
|||||||
// protects even the worker scope from being accessed
|
// protects even the worker scope from being accessed
|
||||||
function runHidden(code) {
|
function runHidden(code) {
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable no-unused-vars */
|
||||||
var indexedDB = null;
|
var indexedDB = null;
|
||||||
var location = null;
|
var location = null;
|
||||||
var navigator = null;
|
var navigator = null;
|
||||||
@ -58,21 +59,15 @@ function runHidden(code) {
|
|||||||
var dump = null;
|
var dump = null;
|
||||||
var onoffline = null;
|
var onoffline = null;
|
||||||
var ononline = null;
|
var ononline = null;
|
||||||
/* eslint-enable */
|
/* eslint-enable no-unused-vars */
|
||||||
|
|
||||||
var error = null;
|
var error = null;
|
||||||
error = importScript(
|
|
||||||
error,
|
|
||||||
'https://cdnjs.cloudflare.com/ajax/libs/ramda/0.13.0/ramda.min.js'
|
|
||||||
);
|
|
||||||
|
|
||||||
error = importScript(
|
error = importScript(
|
||||||
'https://cdnjs.cloudflare.com/ajax/libs/chai/2.2.0/chai.min.js'
|
'https://cdnjs.cloudflare.com/ajax/libs/chai/2.2.0/chai.min.js'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
/* eslint-disable*/
|
/* eslint-disable*/
|
||||||
var expect = chai.expect;
|
|
||||||
var assert = chai.assert;
|
var assert = chai.assert;
|
||||||
/* eslint-enable */
|
/* eslint-enable */
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ import { contain } from 'thundercats-react';
|
|||||||
import { Row } from 'react-bootstrap';
|
import { Row } from 'react-bootstrap';
|
||||||
|
|
||||||
import { Nav } from './components/Nav';
|
import { Nav } from './components/Nav';
|
||||||
import { Footer } from './components/Footer';
|
|
||||||
|
|
||||||
export default contain(
|
export default contain(
|
||||||
{
|
{
|
||||||
@ -52,7 +51,6 @@ export default contain(
|
|||||||
<Row>
|
<Row>
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
</Row>
|
</Row>
|
||||||
<Footer />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,6 @@ export default Cat()
|
|||||||
cat.register(HikesActions, null, services);
|
cat.register(HikesActions, null, services);
|
||||||
cat.register(HikesStore, null, cat);
|
cat.register(HikesStore, null, cat);
|
||||||
|
|
||||||
cat.register(JobActions, null, services);
|
cat.register(JobActions, null, cat, services);
|
||||||
cat.register(JobsStore, null, cat);
|
cat.register(JobsStore, null, cat);
|
||||||
});
|
});
|
||||||
|
27
common/app/components/Flash/Queue.jsx
Normal file
27
common/app/components/Flash/Queue.jsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { Alert } from 'react-bootstrap';
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
|
displayName: 'FlashQueue',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
messages: PropTypes.array
|
||||||
|
},
|
||||||
|
|
||||||
|
renderMessages(messages) {
|
||||||
|
return messages.map(() => {
|
||||||
|
return (
|
||||||
|
<Alert />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { messages = [] } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{ this.renderMessages(messages) }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
0
common/app/components/Flash/index.jsx
Normal file
0
common/app/components/Flash/index.jsx
Normal file
@ -2,7 +2,7 @@
|
|||||||
{
|
{
|
||||||
"className": "ion-speakerphone",
|
"className": "ion-speakerphone",
|
||||||
"content": " Blog ",
|
"content": " Blog ",
|
||||||
"href": "http://blog.freecodecamp.com",
|
"href": "http://medium.freecodecamp.com",
|
||||||
"target": "_blank"
|
"target": "_blank"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
|
import { LinkContainer } from 'react-router-bootstrap';
|
||||||
import {
|
import {
|
||||||
Col,
|
Col,
|
||||||
CollapsibleNav,
|
|
||||||
Nav,
|
Nav,
|
||||||
|
NavbarBrand,
|
||||||
Navbar,
|
Navbar,
|
||||||
NavItem
|
NavItem
|
||||||
} from 'react-bootstrap';
|
} from 'react-bootstrap';
|
||||||
@ -10,17 +11,8 @@ import {
|
|||||||
import navLinks from './links.json';
|
import navLinks from './links.json';
|
||||||
import FCCNavItem from './NavItem.jsx';
|
import FCCNavItem from './NavItem.jsx';
|
||||||
|
|
||||||
|
const win = typeof window !== 'undefined' ? window : {};
|
||||||
const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg';
|
const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg';
|
||||||
const navElements = navLinks.map((navItem, index) => {
|
|
||||||
return (
|
|
||||||
<NavItem
|
|
||||||
eventKey={ index + 1 }
|
|
||||||
href={ navItem.link }
|
|
||||||
key={ index }>
|
|
||||||
{ navItem.content }
|
|
||||||
</NavItem>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const logoElement = (
|
const logoElement = (
|
||||||
<a href='/'>
|
<a href='/'>
|
||||||
@ -31,38 +23,87 @@ const logoElement = (
|
|||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleButton = (
|
const toggleButtonChild = (
|
||||||
<button className='hamburger'>
|
|
||||||
<Col xs={ 12 }>
|
<Col xs={ 12 }>
|
||||||
<span className='hamburger-text'>Menu</span>
|
<span className='hamburger-text'>Menu</span>
|
||||||
</Col>
|
</Col>
|
||||||
</button>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export default class extends React.Component {
|
function getDashedName() {
|
||||||
constructor(props) {
|
let challengeDashedName;
|
||||||
super(props);
|
if (typeof win.localStorage !== 'undefined') {
|
||||||
|
challengeDashedName = win.localStorage.getItem('currentDashedName');
|
||||||
}
|
}
|
||||||
|
return challengeDashedName && challengeDashedName !== 'undefined' ?
|
||||||
|
challengeDashedName :
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
|
||||||
static displayName = 'Nav'
|
export default React.createClass({
|
||||||
static propTypes = {
|
displayName: 'Nav',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
points: PropTypes.number,
|
points: PropTypes.number,
|
||||||
picture: PropTypes.string,
|
picture: PropTypes.string,
|
||||||
signedIn: PropTypes.bool,
|
signedIn: PropTypes.bool,
|
||||||
username: PropTypes.string
|
username: PropTypes.string
|
||||||
}
|
},
|
||||||
|
|
||||||
|
renderLinks() {
|
||||||
|
return navLinks.map(({ content, link, react, target }, index) => {
|
||||||
|
if (react) {
|
||||||
|
return (
|
||||||
|
<LinkContainer
|
||||||
|
eventKey={ index + 1 }
|
||||||
|
key={ content }
|
||||||
|
to={ link }>
|
||||||
|
<NavItem
|
||||||
|
target={ target || null }>
|
||||||
|
{ content }
|
||||||
|
</NavItem>
|
||||||
|
</LinkContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<NavItem
|
||||||
|
eventKey={ index + 1 }
|
||||||
|
href={ link }
|
||||||
|
key={ content }
|
||||||
|
target={ target || null }>
|
||||||
|
{ content }
|
||||||
|
</NavItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderLearnBtn() {
|
||||||
|
return (
|
||||||
|
<NavItem
|
||||||
|
href='#'
|
||||||
|
onClick={ () => {
|
||||||
|
const challengeDashedName = getDashedName();
|
||||||
|
const goTo = challengeDashedName ?
|
||||||
|
'/challenges/' + challengeDashedName :
|
||||||
|
'/map';
|
||||||
|
win.location = goTo;
|
||||||
|
}}>
|
||||||
|
Learn
|
||||||
|
</NavItem>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
renderPoints(username, points) {
|
renderPoints(username, points) {
|
||||||
if (!username) {
|
if (!username) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<NavItem
|
<FCCNavItem
|
||||||
|
className='brownie-points-nav'
|
||||||
href={ '/' + username }>
|
href={ '/' + username }>
|
||||||
[ { points } ]
|
[ { points } ]
|
||||||
</NavItem>
|
</FCCNavItem>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
|
||||||
renderSignin(username, picture) {
|
renderSignin(username, picture) {
|
||||||
if (username) {
|
if (username) {
|
||||||
@ -80,35 +121,36 @@ export default class extends React.Component {
|
|||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<FCCNavItem
|
<FCCNavItem
|
||||||
className='btn signup-btn signup-btn-nav'
|
className='btn signup-btn signup-btn-nav signin-button-nav'
|
||||||
eventKey={ 2 }
|
eventKey={ 2 }
|
||||||
href='/login'>
|
href='/login'>
|
||||||
Sign In
|
Sign In
|
||||||
</FCCNavItem>
|
</FCCNavItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { username, points, picture } = this.props;
|
const { username, points, picture } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Navbar
|
<Navbar
|
||||||
brand={ logoElement }
|
|
||||||
className='nav-height'
|
className='nav-height'
|
||||||
fixedTop={ true }
|
fixedTop={ true }>
|
||||||
toggleButton={ toggleButton }
|
<NavbarBrand>{ logoElement }</NavbarBrand>
|
||||||
toggleNavKey={ 0 }>
|
<Navbar.Toggle children={ toggleButtonChild } />
|
||||||
<CollapsibleNav eventKey={ 0 }>
|
<Navbar.Collapse eventKey={ 0 }>
|
||||||
<Nav
|
<Nav
|
||||||
className='hamburger-dropdown'
|
className='hamburger-dropdown'
|
||||||
navbar={ true }
|
navbar={ true }
|
||||||
right={ true }>
|
pullRight={ true }>
|
||||||
{ navElements }
|
{ this.renderLearnBtn() }
|
||||||
{ this.renderPoints(username, points)}
|
{ this.renderLinks() }
|
||||||
|
{ this.renderPoints(username, points) }
|
||||||
{ this.renderSignin(username, picture) }
|
{ this.renderSignin(username, picture) }
|
||||||
</Nav>
|
</Nav>
|
||||||
</CollapsibleNav>
|
</Navbar.Collapse>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import BootstrapMixin from 'react-bootstrap/lib/BootstrapMixin';
|
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'FCCNavItem',
|
displayName: 'FCCNavItem',
|
||||||
mixins: [BootstrapMixin],
|
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
active: React.PropTypes.bool,
|
active: React.PropTypes.bool,
|
||||||
'aria-controls': React.PropTypes.string,
|
'aria-controls': React.PropTypes.string,
|
||||||
|
children: React.PropTypes.node,
|
||||||
|
className: React.PropTypes.string,
|
||||||
disabled: React.PropTypes.bool,
|
disabled: React.PropTypes.bool,
|
||||||
eventKey: React.PropTypes.any,
|
eventKey: React.PropTypes.any,
|
||||||
href: React.PropTypes.string,
|
href: React.PropTypes.string,
|
||||||
@ -30,7 +30,11 @@ export default React.createClass({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!this.props.disabled) {
|
if (!this.props.disabled) {
|
||||||
this.props.onSelect(this.props.eventKey, this.props.href, this.props.target);
|
this.props.onSelect(
|
||||||
|
this.props.eventKey,
|
||||||
|
this.props.href,
|
||||||
|
this.props.target
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -50,10 +54,11 @@ export default React.createClass({
|
|||||||
...props
|
...props
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
let classes = {
|
const linkClassName = classNames(className, {
|
||||||
active,
|
// 'active': active, we don't actually use the active class
|
||||||
disabled
|
// but it is used for a11y below
|
||||||
};
|
'disabled': disabled
|
||||||
|
});
|
||||||
|
|
||||||
let linkProps = {
|
let linkProps = {
|
||||||
role,
|
role,
|
||||||
@ -75,9 +80,9 @@ export default React.createClass({
|
|||||||
role='presentation'>
|
role='presentation'>
|
||||||
<a
|
<a
|
||||||
{ ...linkProps }
|
{ ...linkProps }
|
||||||
aria-selected={ active }
|
|
||||||
aria-controls={ ariaControls }
|
aria-controls={ ariaControls }
|
||||||
className={ className }>
|
aria-selected={ active }
|
||||||
|
className={ linkClassName }>
|
||||||
{ children }
|
{ children }
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -3,11 +3,21 @@
|
|||||||
"link": "/map"
|
"link": "/map"
|
||||||
}, {
|
}, {
|
||||||
"content": "Chat",
|
"content": "Chat",
|
||||||
"link": "//gitter.im/FreeCodeCamp/FreeCodeCamp"
|
"link": "//gitter.im/FreeCodeCamp/FreeCodeCamp",
|
||||||
|
"target": "_blank"
|
||||||
},{
|
},{
|
||||||
"content": "News",
|
"content": "News",
|
||||||
"link": "/news"
|
"link": "/news",
|
||||||
|
"target": "_blank"
|
||||||
|
},{
|
||||||
|
"content": "Wiki",
|
||||||
|
"link": "https://github.com/freecodecamp/freecodecamp/wiki/",
|
||||||
|
"target": "_blank"
|
||||||
},{
|
},{
|
||||||
"content": "Jobs",
|
"content": "Jobs",
|
||||||
"link": "/jobs"
|
"link": "/jobs",
|
||||||
|
"react": true
|
||||||
|
},{
|
||||||
|
"content": "Links",
|
||||||
|
"link": "/links"
|
||||||
}]
|
}]
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default class extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
static displayName = 'NotFound'
|
|
||||||
static propTypes = {}
|
|
||||||
componentDidMount() {
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
22
common/app/components/NotFound/index.jsx
Normal file
22
common/app/components/NotFound/index.jsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
|
||||||
|
const win = typeof window !== 'undefined' ? window : {};
|
||||||
|
|
||||||
|
function goToServer(path) {
|
||||||
|
win.location = '/' + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
|
displayName: 'NotFound',
|
||||||
|
propTypes: {
|
||||||
|
params: PropTypes.object
|
||||||
|
},
|
||||||
|
componentWillMount() {
|
||||||
|
goToServer(this.props.params.splat);
|
||||||
|
},
|
||||||
|
componentDidMount() {
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return <span></span>;
|
||||||
|
}
|
||||||
|
});
|
@ -8,15 +8,27 @@ export default Actions({
|
|||||||
return { title: title + '| Free Code Camp' };
|
return { title: title + '| Free Code Camp' };
|
||||||
},
|
},
|
||||||
|
|
||||||
setUser({ username, picture, progressTimestamps = [] }) {
|
setUser({
|
||||||
|
username,
|
||||||
|
picture,
|
||||||
|
progressTimestamps = [],
|
||||||
|
isFrontEndCert,
|
||||||
|
isFullStackCert
|
||||||
|
}) {
|
||||||
return {
|
return {
|
||||||
username,
|
username,
|
||||||
picture,
|
picture,
|
||||||
points: progressTimestamps.length
|
points: progressTimestamps.length,
|
||||||
|
isFrontEndCert,
|
||||||
|
isFullStackCert
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getUser: null
|
getUser: null,
|
||||||
|
updateRoute(route) {
|
||||||
|
return { route };
|
||||||
|
},
|
||||||
|
goBack: null
|
||||||
})
|
})
|
||||||
.refs({ displayName: 'AppActions' })
|
.refs({ displayName: 'AppActions' })
|
||||||
.init(({ instance: appActions, args: [services] }) => {
|
.init(({ instance: appActions, args: [services] }) => {
|
||||||
|
@ -8,13 +8,17 @@ const initValue = {
|
|||||||
points: 0
|
points: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Store(initValue)
|
export default Store({
|
||||||
.refs({ displayName: 'AppStore' })
|
refs: {
|
||||||
.init(({ instance: appStore, args: [cat] }) => {
|
displayName: 'AppStore',
|
||||||
const { setUser, setTitle } = cat.getActions('appActions');
|
value: initValue
|
||||||
|
},
|
||||||
|
init({ instance: appStore, args: [cat] }) {
|
||||||
|
const { updateRoute, setUser, setTitle } = cat.getActions('appActions');
|
||||||
const register = createRegistrar(appStore);
|
const register = createRegistrar(appStore);
|
||||||
|
|
||||||
register(setter(fromMany(setUser, setTitle)));
|
register(setter(fromMany(setUser, setTitle, updateRoute)));
|
||||||
|
|
||||||
return appStore;
|
return appStore;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
var Action = require('thundercats').Action,
|
|
||||||
executeBonfire = require('./executeBonfire'),
|
|
||||||
getModel = require('../../utils/getModel'),
|
|
||||||
debug = require('debug')('freecc:common:bonfires');
|
|
||||||
|
|
||||||
var BonfireActions = Action.createActions([
|
|
||||||
'setUserCode',
|
|
||||||
'testUserCode',
|
|
||||||
'setResults',
|
|
||||||
'setDisplay',
|
|
||||||
'setBonfire',
|
|
||||||
'getBonfire',
|
|
||||||
'handleBonfireError',
|
|
||||||
'openCompletionModal'
|
|
||||||
]);
|
|
||||||
|
|
||||||
BonfireActions
|
|
||||||
.getBonfire
|
|
||||||
.subscribe(function(params) {
|
|
||||||
var Bonfire = getModel('bonfire');
|
|
||||||
var bonfireName = params.bonfireName ?
|
|
||||||
params.bonfireName.replace(/\-/g, ' ') :
|
|
||||||
'meet bonfire';
|
|
||||||
debug('getting bonfire for: ', bonfireName);
|
|
||||||
var regQuery = { name: { like: bonfireName, options: 'i' } };
|
|
||||||
Bonfire.find(
|
|
||||||
{ where: regQuery },
|
|
||||||
function(err, bonfire) {
|
|
||||||
if (err) {
|
|
||||||
return debug('bonfire get err', err);
|
|
||||||
}
|
|
||||||
if (!bonfire || bonfire.length < 1) {
|
|
||||||
return debug('404 no bonfire found for ', bonfireName);
|
|
||||||
}
|
|
||||||
bonfire = bonfire.pop();
|
|
||||||
if (bonfire) {
|
|
||||||
debug(
|
|
||||||
'found bonfire %s for route %s',
|
|
||||||
bonfire.name,
|
|
||||||
bonfireName
|
|
||||||
);
|
|
||||||
}
|
|
||||||
BonfireActions.setBonfire(bonfire);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
BonfireActions
|
|
||||||
.testUserCode
|
|
||||||
.subscribe(function({ userCode, tests }) {
|
|
||||||
debug('test bonfire');
|
|
||||||
executeBonfire(userCode, tests, function(err, { output, results }) {
|
|
||||||
if (err) {
|
|
||||||
debug('error running tests', err);
|
|
||||||
return BonfireActions.setDisplay(err);
|
|
||||||
}
|
|
||||||
BonfireActions.setDisplay(output);
|
|
||||||
BonfireActions.setResults(results);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = BonfireActions;
|
|
@ -1,67 +0,0 @@
|
|||||||
var BonfiresActions = require('./Actions');
|
|
||||||
var { Store, setStateUtil } = require('thundercats');
|
|
||||||
|
|
||||||
var BonfiresStore = Store.create({
|
|
||||||
|
|
||||||
getInitialValue: function() {
|
|
||||||
return {
|
|
||||||
userCode: 'console.log(\'FreeCodeCamp!\')',
|
|
||||||
difficulty: 0,
|
|
||||||
description: [
|
|
||||||
'default state'
|
|
||||||
],
|
|
||||||
tests: [],
|
|
||||||
results: null
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getOperations: function() {
|
|
||||||
var {
|
|
||||||
setBonfire,
|
|
||||||
setUserCode,
|
|
||||||
setResults,
|
|
||||||
setDisplay
|
|
||||||
} = BonfiresActions;
|
|
||||||
|
|
||||||
return [
|
|
||||||
setBonfire
|
|
||||||
.map(function(bonfire) {
|
|
||||||
var {
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
difficulty,
|
|
||||||
tests
|
|
||||||
} = bonfire;
|
|
||||||
var userCode = bonfire.challengeSeed;
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
userCode,
|
|
||||||
tests,
|
|
||||||
description,
|
|
||||||
difficulty
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.map(setStateUtil),
|
|
||||||
|
|
||||||
setUserCode
|
|
||||||
.map(function(userCode) {
|
|
||||||
return { userCode };
|
|
||||||
})
|
|
||||||
.map(setStateUtil),
|
|
||||||
|
|
||||||
setDisplay
|
|
||||||
.map(function(display) {
|
|
||||||
return { display };
|
|
||||||
})
|
|
||||||
.map(setStateUtil),
|
|
||||||
|
|
||||||
setResults
|
|
||||||
.map(function(results) {
|
|
||||||
return { results };
|
|
||||||
})
|
|
||||||
.map(setStateUtil)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = BonfiresStore;
|
|
@ -1,99 +0,0 @@
|
|||||||
var React = require('react'),
|
|
||||||
|
|
||||||
// ## mixins
|
|
||||||
{ ObservableStateMixin } = require('thundercats'),
|
|
||||||
|
|
||||||
// ## components
|
|
||||||
SidePanel = require('./SidePanel.jsx'),
|
|
||||||
Results = require('./Results.jsx'),
|
|
||||||
Display = require('../displayCode'),
|
|
||||||
Editor = require('../editor'),
|
|
||||||
{ Grid, Row, Col } = require('react-bootstrap'),
|
|
||||||
|
|
||||||
// ## flux
|
|
||||||
BonfireActions = require('./Actions'),
|
|
||||||
BonfireStore = require('./Store');
|
|
||||||
|
|
||||||
var Bonfire = React.createClass({
|
|
||||||
|
|
||||||
mixins: [ObservableStateMixin],
|
|
||||||
|
|
||||||
contextTypes: {
|
|
||||||
makePath: React.PropTypes.func.isRequired,
|
|
||||||
replaceWith: React.PropTypes.func.isRequired
|
|
||||||
},
|
|
||||||
|
|
||||||
getObservable: function() {
|
|
||||||
return BonfireStore;
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
// get history object
|
|
||||||
var his = typeof window !== 'undefined' ? window.history : null;
|
|
||||||
// spinal-case bonfireName
|
|
||||||
var bonfireName = this.state.name.toLowerCase().replace(/\s/g, '-');
|
|
||||||
// create proper URI from react-router
|
|
||||||
var path = this.context.makePath('bonfires', { bonfireName: bonfireName });
|
|
||||||
|
|
||||||
// if html5 push state exists, update URI
|
|
||||||
// else we are using hash location and should just cause a re render
|
|
||||||
if (his) {
|
|
||||||
his.replaceState({ path: path }, '', path);
|
|
||||||
} else {
|
|
||||||
this.context.replaceWith('bonfires', { bonfireName: bonfireName});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_onTestBonfire: function() {
|
|
||||||
BonfireActions.testUserCode({
|
|
||||||
userCode: this.state.userCode,
|
|
||||||
tests: this.state.tests
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
var {
|
|
||||||
name,
|
|
||||||
userCode,
|
|
||||||
difficulty,
|
|
||||||
description,
|
|
||||||
results,
|
|
||||||
display
|
|
||||||
} = this.state;
|
|
||||||
var brief = description.slice(0, 1).pop();
|
|
||||||
|
|
||||||
// convert bonfire difficulty from floating point string
|
|
||||||
// to integer.
|
|
||||||
var difficultyInt = Math.floor(+difficulty);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Grid>
|
|
||||||
<Row>
|
|
||||||
<Col
|
|
||||||
xs={ 12 }
|
|
||||||
md={ 4 }>
|
|
||||||
<SidePanel
|
|
||||||
name={ name }
|
|
||||||
brief={ brief }
|
|
||||||
difficulty={ difficultyInt }
|
|
||||||
onTestBonfire={ this._onTestBonfire }
|
|
||||||
description={ description.length > 1 ? description : [] }/>
|
|
||||||
<Display
|
|
||||||
value={ display }/>
|
|
||||||
<Results
|
|
||||||
results={ results }/>
|
|
||||||
</Col>
|
|
||||||
<Col
|
|
||||||
xs={ 12 }
|
|
||||||
md={ 8 }>
|
|
||||||
<Editor
|
|
||||||
onValueChange={ BonfireActions.setUserCode }
|
|
||||||
value={ userCode }/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = Bonfire;
|
|
@ -1,62 +0,0 @@
|
|||||||
var React = require('react'),
|
|
||||||
classNames = require('classnames'),
|
|
||||||
{ Grid, Row, Col } = require('react-bootstrap');
|
|
||||||
|
|
||||||
var Results = React.createClass({
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
results: React.PropTypes.array
|
|
||||||
},
|
|
||||||
|
|
||||||
_renderText: function(text, textClass) {
|
|
||||||
return (
|
|
||||||
<Col
|
|
||||||
xs={ 11 }
|
|
||||||
className={ classNames(textClass) }>
|
|
||||||
{ text }
|
|
||||||
</Col>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
_renderResult: function(results) {
|
|
||||||
return results.map(function(result, idx) {
|
|
||||||
var err = result.err;
|
|
||||||
var iconClass = {
|
|
||||||
'ion-close-circled big-error-icon': err,
|
|
||||||
'ion-checkmark-circled big-success-icon': !err
|
|
||||||
};
|
|
||||||
var textClass = {
|
|
||||||
'test-output wrappable': true,
|
|
||||||
'test-vertical-center': !err
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div key={ idx }>
|
|
||||||
<Row>
|
|
||||||
<Col
|
|
||||||
xs={ 1 }
|
|
||||||
className='text-center'>
|
|
||||||
<i className={ classNames(iconClass) }></i>
|
|
||||||
</Col>
|
|
||||||
{ this._renderText(result.text, textClass) }
|
|
||||||
{ err ? this._renderText(err, textClass) : null }
|
|
||||||
</Row>
|
|
||||||
<div className='ten-pixel-break'></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}.bind(this));
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
var results = this.props.results;
|
|
||||||
if (!results || results.length && results.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Grid>
|
|
||||||
{ this._renderResult(this.props.results) }
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = Results;
|
|
@ -1,129 +0,0 @@
|
|||||||
var React = require('react'),
|
|
||||||
|
|
||||||
// ## components
|
|
||||||
{
|
|
||||||
Well,
|
|
||||||
Row,
|
|
||||||
Col,
|
|
||||||
Button,
|
|
||||||
} = require('react-bootstrap');
|
|
||||||
|
|
||||||
var SidePanel = React.createClass({
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
name: React.PropTypes.string,
|
|
||||||
brief: React.PropTypes.string,
|
|
||||||
description: React.PropTypes.array,
|
|
||||||
difficulty: React.PropTypes.number,
|
|
||||||
onTestBonfire: React.PropTypes.func
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
name: 'Welcome to Bonfires!',
|
|
||||||
difficulty: 5,
|
|
||||||
brief: 'This is a brief description'
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
isMoreInfoOpen: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
_toggleMoreInfo: function() {
|
|
||||||
this.setState({
|
|
||||||
isMoreInfoOpen: !this.state.isMoreInfoOpen
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_renderFlames: function() {
|
|
||||||
var difficulty = this.props.difficulty;
|
|
||||||
|
|
||||||
return [1, 2, 3, 4, 5].map(num => {
|
|
||||||
var className = 'ion-ios-flame';
|
|
||||||
if (num > difficulty) {
|
|
||||||
className += '-outline';
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<i
|
|
||||||
key={ num }
|
|
||||||
className={ className }/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_renderMoreInfo: function(isDescription) {
|
|
||||||
var description = this.props.description.map((sentance, index) => {
|
|
||||||
return <p key={ index }>{ sentance }</p>;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isDescription && this.state.isMoreInfoOpen) {
|
|
||||||
return (
|
|
||||||
<Row>
|
|
||||||
<Col xs={ 12 }>
|
|
||||||
{ description }
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
_renderMoreInfoButton: function(isDescription) {
|
|
||||||
if (isDescription) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
onClick={ this._toggleMoreInfo }
|
|
||||||
bsStyle='primary'
|
|
||||||
block={ true }
|
|
||||||
className='btn-primary-ghost'>
|
|
||||||
<span className='ion-arrow-down-b'></span>
|
|
||||||
More information
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
var isDescription = this.props.description &&
|
|
||||||
this.props.description.length > 1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className='text-center'>{ this.props.name }</h1>
|
|
||||||
<h2 className='text-center'>
|
|
||||||
<div className='bonfire-flames'>
|
|
||||||
Difficulty:
|
|
||||||
{ this._renderFlames() }
|
|
||||||
</div>
|
|
||||||
</h2>
|
|
||||||
<Well>
|
|
||||||
<Row>
|
|
||||||
<Col xs={ 12 }>
|
|
||||||
<div className='bonfire-instructions'>
|
|
||||||
<p>{ this.props.brief }</p>
|
|
||||||
<div>
|
|
||||||
{ this._renderMoreInfo(isDescription) }
|
|
||||||
{ this._renderMoreInfoButton(isDescription) }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Well>
|
|
||||||
<Button
|
|
||||||
bsStyle='primary'
|
|
||||||
block={ true }
|
|
||||||
className='btn-big'
|
|
||||||
onClick={ this.props.onTestBonfire }>
|
|
||||||
Run Code (ctrl + enter)
|
|
||||||
</Button>
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = SidePanel;
|
|
@ -1,27 +0,0 @@
|
|||||||
var debug = require('debug')('freecc:executebonfire');
|
|
||||||
var {
|
|
||||||
addTests,
|
|
||||||
runTests,
|
|
||||||
testCode
|
|
||||||
} = require('../../utils');
|
|
||||||
|
|
||||||
module.exports = executeBonfire;
|
|
||||||
|
|
||||||
function executeBonfire(userCode, tests, cb) {
|
|
||||||
|
|
||||||
// TODO: move this into componentDidMount
|
|
||||||
// ga('send', 'event', 'Bonfire', 'ran-code', bonfireName);
|
|
||||||
var testSalt = Math.random();
|
|
||||||
var { preppedCode, userTests } = addTests(userCode, tests, testSalt);
|
|
||||||
|
|
||||||
debug('sending code to web worker for testing');
|
|
||||||
testCode(preppedCode, function(err, data) {
|
|
||||||
if (err) { return cb(err); }
|
|
||||||
var results = runTests(userTests, data, testSalt);
|
|
||||||
debug('testing complete', results);
|
|
||||||
cb(null, {
|
|
||||||
output: data.output,
|
|
||||||
results
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
export default {
|
|
||||||
path: 'bonfires/(:bonfireName)',
|
|
||||||
getComponents(cb) {
|
|
||||||
// TODO(berks): add bonfire component
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,6 +1,6 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { Button, Col, Row, Panel } from 'react-bootstrap';
|
import { Button, Col, Row, Panel } from 'react-bootstrap';
|
||||||
import { Navigation } from 'react-router';
|
import { History } from 'react-router';
|
||||||
import Vimeo from 'react-vimeo';
|
import Vimeo from 'react-vimeo';
|
||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
|
|
||||||
@ -8,7 +8,7 @@ const debug = debugFactory('freecc:hikes');
|
|||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'Lecture',
|
displayName: 'Lecture',
|
||||||
mixins: [Navigation],
|
mixins: [History],
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
currentHike: PropTypes.object,
|
currentHike: PropTypes.object,
|
||||||
@ -20,7 +20,7 @@ export default React.createClass({
|
|||||||
handleFinish() {
|
handleFinish() {
|
||||||
debug('loading questions');
|
debug('loading questions');
|
||||||
const { dashedName } = this.props.params;
|
const { dashedName } = this.props.params;
|
||||||
this.transitionTo(`/hikes/${dashedName}/questions/1`);
|
this.history.pushState(null, `/hikes/${dashedName}/questions/1`);
|
||||||
},
|
},
|
||||||
|
|
||||||
renderTranscript(transcript, dashedName) {
|
renderTranscript(transcript, dashedName) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { Spring } from 'react-motion';
|
import { Spring } from 'react-motion';
|
||||||
import { Navigation, TransitionHook } from 'react-router';
|
import { History, Lifecycle } from 'react-router';
|
||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -19,8 +19,8 @@ export default React.createClass({
|
|||||||
displayName: 'Question',
|
displayName: 'Question',
|
||||||
|
|
||||||
mixins: [
|
mixins: [
|
||||||
Navigation,
|
History,
|
||||||
TransitionHook
|
Lifecycle
|
||||||
],
|
],
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
@ -150,7 +150,8 @@ export default React.createClass({
|
|||||||
|
|
||||||
postJSON$('/completed-challenge', { id, name }).subscribeOnCompleted(() => {
|
postJSON$('/completed-challenge', { id, name }).subscribeOnCompleted(() => {
|
||||||
if (tests[nextQuestionIndex]) {
|
if (tests[nextQuestionIndex]) {
|
||||||
return this.transitionTo(
|
return this.history.pushState(
|
||||||
|
null,
|
||||||
`/hikes/${ dashedName }/questions/${ nextQuestionIndex + 1 }`
|
`/hikes/${ dashedName }/questions/${ nextQuestionIndex + 1 }`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -168,13 +169,13 @@ export default React.createClass({
|
|||||||
}, null);
|
}, null);
|
||||||
|
|
||||||
if (nextHike) {
|
if (nextHike) {
|
||||||
return this.transitionTo(`/hikes/${ nextHike.dashedName }`);
|
return this.history.pushState(null, `/hikes/${ nextHike.dashedName }`);
|
||||||
}
|
}
|
||||||
debug(
|
debug(
|
||||||
'next Hike was not found, currentHike %s',
|
'next Hike was not found, currentHike %s',
|
||||||
currentHike.dashedName
|
currentHike.dashedName
|
||||||
);
|
);
|
||||||
this.transitionTo('/hikes');
|
this.history.pushState(null, '/hikes');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import debugFactory from 'debug';
|
|||||||
|
|
||||||
const debug = debugFactory('freecc:hikes:actions');
|
const debug = debugFactory('freecc:hikes:actions');
|
||||||
|
|
||||||
function getCurrentHike(hikes =[{}], dashedName, currentHike) {
|
function getCurrentHike(hikes = [{}], dashedName, currentHike) {
|
||||||
if (!dashedName) {
|
if (!dashedName) {
|
||||||
debug('no dashedName');
|
debug('no dashedName');
|
||||||
return hikes[0];
|
return hikes[0];
|
||||||
|
@ -5,12 +5,16 @@ const initialValue = {
|
|||||||
currentHike: {}
|
currentHike: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Store(initialValue)
|
export default Store({
|
||||||
.refs({ displayName: 'HikesStore'})
|
refs: {
|
||||||
.init(({ instance: hikeStore, args: [cat] }) => {
|
displayName: 'HikesStore',
|
||||||
|
value: initialValue
|
||||||
|
},
|
||||||
|
init({ instance: hikeStore, args: [cat] }) {
|
||||||
|
|
||||||
let { setHikes } = cat.getActions('hikesActions');
|
let { setHikes } = cat.getActions('hikesActions');
|
||||||
hikeStore.register(setHikes);
|
hikeStore.register(setHikes);
|
||||||
|
|
||||||
return hikeStore;
|
return hikeStore;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
272
common/app/routes/Jobs/components/GoToPayPal.jsx
Normal file
272
common/app/routes/Jobs/components/GoToPayPal.jsx
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { Button, Input, Col, Panel, Row, Well } from 'react-bootstrap';
|
||||||
|
import { contain } from 'thundercats-react';
|
||||||
|
|
||||||
|
// real paypal buttons
|
||||||
|
// will take your money
|
||||||
|
const paypalIds = {
|
||||||
|
regular: 'Q8Z82ZLAX3Q8N',
|
||||||
|
highlighted: 'VC8QPSKCYMZLN'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default contain(
|
||||||
|
{
|
||||||
|
store: 'JobsStore',
|
||||||
|
actions: [
|
||||||
|
'jobActions',
|
||||||
|
'appActions'
|
||||||
|
],
|
||||||
|
map({
|
||||||
|
job: { id, isHighlighted } = {},
|
||||||
|
buttonId = isHighlighted ?
|
||||||
|
paypalIds.highlighted :
|
||||||
|
paypalIds.regular,
|
||||||
|
price = 1000,
|
||||||
|
discountAmount = 0,
|
||||||
|
promoCode = '',
|
||||||
|
promoApplied = false,
|
||||||
|
promoName
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
isHighlighted,
|
||||||
|
buttonId,
|
||||||
|
price,
|
||||||
|
discountAmount,
|
||||||
|
promoName,
|
||||||
|
promoCode,
|
||||||
|
promoApplied
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
React.createClass({
|
||||||
|
displayName: 'GoToPayPal',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
appActions: PropTypes.object,
|
||||||
|
id: PropTypes.string,
|
||||||
|
isHighlighted: PropTypes.bool,
|
||||||
|
buttonId: PropTypes.string,
|
||||||
|
price: PropTypes.number,
|
||||||
|
discountAmount: PropTypes.number,
|
||||||
|
promoName: PropTypes.string,
|
||||||
|
promoCode: PropTypes.string,
|
||||||
|
promoApplied: PropTypes.bool,
|
||||||
|
jobActions: PropTypes.object
|
||||||
|
},
|
||||||
|
|
||||||
|
goToJobBoard() {
|
||||||
|
const { appActions } = this.props;
|
||||||
|
appActions.updateRoute('/jobs');
|
||||||
|
},
|
||||||
|
|
||||||
|
renderDiscount(discountAmount) {
|
||||||
|
if (!discountAmount) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 3 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<h4>Promo Discount</h4>
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
md={ 3 }>
|
||||||
|
<h4>-{ discountAmount }</h4>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHighlightPrice(isHighlighted) {
|
||||||
|
if (!isHighlighted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 3 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<h4>Highlighting</h4>
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
md={ 3 }>
|
||||||
|
<h4>+ 250</h4>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderPromo() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
promoApplied,
|
||||||
|
promoCode,
|
||||||
|
promoName,
|
||||||
|
isHighlighted,
|
||||||
|
jobActions
|
||||||
|
} = this.props;
|
||||||
|
if (promoApplied) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='spacer' />
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 3 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
{ promoName } applied
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='spacer' />
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 3 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
Have a promo code?
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 3 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<Input
|
||||||
|
onChange={ jobActions.setPromoCode }
|
||||||
|
type='text'
|
||||||
|
value={ promoCode } />
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
md={ 3 }>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
onClick={ () => {
|
||||||
|
jobActions.applyCode({
|
||||||
|
id,
|
||||||
|
code: promoCode,
|
||||||
|
type: isHighlighted ? 'isHighlighted' : null
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
Apply Promo Code
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
isHighlighted,
|
||||||
|
buttonId,
|
||||||
|
price,
|
||||||
|
discountAmount
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 10 }
|
||||||
|
mdOffset={ 1 }
|
||||||
|
sm={ 8 }
|
||||||
|
smOffset={ 2 }
|
||||||
|
xs={ 12 }>
|
||||||
|
<Panel>
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 6 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<h2 className='text-center'>
|
||||||
|
One more step
|
||||||
|
</h2>
|
||||||
|
<div className='spacer' />
|
||||||
|
You're Awesome! just one more step to go.
|
||||||
|
Clicking on the link below will redirect to paypal.
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div className='spacer' />
|
||||||
|
<Well>
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 3 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<h4>Job Posting</h4>
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
md={ 6 }>
|
||||||
|
<h4>+ { price }</h4>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{ this.renderHighlightPrice(isHighlighted) }
|
||||||
|
{ this.renderDiscount(discountAmount) }
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 3 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<h4>Total</h4>
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
md={ 6 }>
|
||||||
|
<h4>${
|
||||||
|
price - discountAmount + (isHighlighted ? 250 : 0)
|
||||||
|
}</h4>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Well>
|
||||||
|
{ this.renderPromo() }
|
||||||
|
<div className='spacer' />
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 6 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<form
|
||||||
|
action='https://www.paypal.com/cgi-bin/webscr'
|
||||||
|
method='post'
|
||||||
|
onClick={ this.goToJobBoard }
|
||||||
|
target='_blank'>
|
||||||
|
<input
|
||||||
|
name='cmd'
|
||||||
|
type='hidden'
|
||||||
|
value='_s-xclick' />
|
||||||
|
<input
|
||||||
|
name='hosted_button_id'
|
||||||
|
type='hidden'
|
||||||
|
value={ buttonId } />
|
||||||
|
<input
|
||||||
|
name='custom'
|
||||||
|
type='hidden'
|
||||||
|
value={ '' + id } />
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='large'
|
||||||
|
className='signup-btn'
|
||||||
|
type='submit'>
|
||||||
|
<i className='fa fa-paypal' />
|
||||||
|
Continue to PayPal
|
||||||
|
</Button>
|
||||||
|
<div className='spacer' />
|
||||||
|
<img
|
||||||
|
alt='An array of credit cards'
|
||||||
|
border='0'
|
||||||
|
src='http://i.imgur.com/Q2SdSZG.png'
|
||||||
|
style={{
|
||||||
|
width: '100%'
|
||||||
|
}} />
|
||||||
|
</form>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div className='spacer' />
|
||||||
|
</Panel>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
31
common/app/routes/Jobs/components/JobNotFound.jsx
Normal file
31
common/app/routes/Jobs/components/JobNotFound.jsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { LinkContainer } from 'react-router-bootstrap';
|
||||||
|
import { Button, Row, Col, Panel } from 'react-bootstrap';
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
|
displayName: 'NoJobFound',
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 6 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<Panel>
|
||||||
|
No job found...
|
||||||
|
<LinkContainer to='/jobs'>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='large'
|
||||||
|
bsStyle='primary'>
|
||||||
|
Go to the job board
|
||||||
|
</Button>
|
||||||
|
</LinkContainer>
|
||||||
|
</Panel>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
@ -1,36 +1,42 @@
|
|||||||
import React, { cloneElement, PropTypes } from 'react';
|
import React, { cloneElement, PropTypes } from 'react';
|
||||||
import { contain } from 'thundercats-react';
|
import { contain } from 'thundercats-react';
|
||||||
import { History } from 'react-router';
|
import { Button, Panel, Row, Col } from 'react-bootstrap';
|
||||||
import { Button, Jumbotron, Row } from 'react-bootstrap';
|
|
||||||
|
|
||||||
import CreateJobModal from './CreateJobModal.jsx';
|
|
||||||
import ListJobs from './List.jsx';
|
import ListJobs from './List.jsx';
|
||||||
|
|
||||||
export default contain(
|
export default contain(
|
||||||
{
|
{
|
||||||
store: 'jobsStore',
|
store: 'jobsStore',
|
||||||
fetchAction: 'jobActions.getJobs',
|
fetchAction: 'jobActions.getJobs',
|
||||||
actions: 'jobActions'
|
actions: [
|
||||||
|
'appActions',
|
||||||
|
'jobActions'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
React.createClass({
|
React.createClass({
|
||||||
displayName: 'Jobs',
|
displayName: 'Jobs',
|
||||||
|
|
||||||
mixins: [History],
|
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
children: PropTypes.element,
|
children: PropTypes.element,
|
||||||
|
numOfFollowers: PropTypes.number,
|
||||||
|
appActions: PropTypes.object,
|
||||||
jobActions: PropTypes.object,
|
jobActions: PropTypes.object,
|
||||||
jobs: PropTypes.array,
|
jobs: PropTypes.array,
|
||||||
showModal: PropTypes.bool
|
showModal: PropTypes.bool
|
||||||
},
|
},
|
||||||
|
|
||||||
handleJobClick(id) {
|
componentDidMount() {
|
||||||
const { jobActions } = this.props;
|
const { jobActions } = this.props;
|
||||||
|
jobActions.getFollowers();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleJobClick(id) {
|
||||||
|
const { appActions, jobActions } = this.props;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
jobActions.findJob(id);
|
jobActions.findJob(id);
|
||||||
this.history.pushState(null, `/jobs/${id}`);
|
appActions.updateRoute(`/jobs/${id}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
renderList(handleJobClick, jobs) {
|
renderList(handleJobClick, jobs) {
|
||||||
@ -55,36 +61,73 @@ export default contain(
|
|||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
jobs,
|
jobs,
|
||||||
showModal,
|
appActions
|
||||||
jobActions
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Panel>
|
||||||
<Row>
|
<Row>
|
||||||
<Jumbotron>
|
<Col
|
||||||
<h1>Free Code Camps' Job Board</h1>
|
md={ 10 }
|
||||||
<p>
|
mdOffset= { 1 }
|
||||||
Need to find the best junior developers?
|
xs={ 12 }>
|
||||||
Want to find dedicated developers eager to join your company?
|
<h1 className='text-center'>
|
||||||
Sign up now to post your job!
|
Hire a JavaScript engineer who's experienced in HTML5,
|
||||||
</p>
|
Node.js, MongoDB, and Agile Development.
|
||||||
<Button
|
</h1>
|
||||||
bsSize='large'
|
<div className='spacer' />
|
||||||
className='signup-btn'
|
<Row className='text-center'>
|
||||||
onClick={ jobActions.openModal }>
|
<Col
|
||||||
Try the first month 20% off!
|
sm={ 8 }
|
||||||
</Button>
|
smOffset={ 2 }
|
||||||
</Jumbotron>
|
xs={ 12 }>
|
||||||
|
<Button
|
||||||
|
className='signup-btn btn-block btn-cta'
|
||||||
|
onClick={ ()=> {
|
||||||
|
appActions.updateRoute('/jobs/new');
|
||||||
|
}}>
|
||||||
|
Post a job: $1000
|
||||||
|
</Button>
|
||||||
|
<div className='spacer' />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div className='spacer' />
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 2 }
|
||||||
|
xs={ 4 }>
|
||||||
|
<img
|
||||||
|
alt={`
|
||||||
|
a photo of Michael Gai, who recently hired a software
|
||||||
|
engineer through Free Code Camp
|
||||||
|
`}
|
||||||
|
className='img-responsive testimonial-image-jobs img-center'
|
||||||
|
src='http://i.imgur.com/tGcAA8H.jpg' />
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
md={ 10 }
|
||||||
|
xs={ 8 }>
|
||||||
|
<blockquote>
|
||||||
|
<p>
|
||||||
|
We hired our last developer out of Free Code Camp
|
||||||
|
and couldn't be happier. Free Code Camp is now
|
||||||
|
our go-to way to bring on pre-screened candidates
|
||||||
|
who are enthusiastic about learning quickly and
|
||||||
|
becoming immediately productive in their new career.
|
||||||
|
</p>
|
||||||
|
<footer>
|
||||||
|
Michael Gai, <cite>CEO at CoNarrative</cite>
|
||||||
|
</footer>
|
||||||
|
</blockquote>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
{ this.renderChild(children, jobs) ||
|
||||||
|
this.renderList(this.handleJobClick, jobs) }
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
</Panel>
|
||||||
{ this.renderChild(children, jobs) ||
|
|
||||||
this.renderList(this.handleJobClick, jobs) }
|
|
||||||
</Row>
|
|
||||||
<CreateJobModal
|
|
||||||
onHide={ jobActions.closeModal }
|
|
||||||
showModal={ showModal } />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { PanelGroup, Thumbnail, Panel, Well } from 'react-bootstrap';
|
import classnames from 'classnames';
|
||||||
import moment from 'moment';
|
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'ListJobs',
|
displayName: 'ListJobs',
|
||||||
@ -10,64 +10,58 @@ export default React.createClass({
|
|||||||
jobs: PropTypes.array
|
jobs: PropTypes.array
|
||||||
},
|
},
|
||||||
|
|
||||||
renderJobs(handleClick, jobs =[]) {
|
addLocation(locale) {
|
||||||
const thumbnailStyle = {
|
if (!locale) {
|
||||||
backgroundColor: 'white',
|
return null;
|
||||||
maxHeight: '100px',
|
}
|
||||||
maxWidth: '100px'
|
return (
|
||||||
};
|
<span className='hidden-xs hidden-sm'>
|
||||||
|
{ locale }
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
return jobs.map((
|
renderJobs(handleClick, jobs = []) {
|
||||||
{
|
return jobs
|
||||||
|
.filter(({ isPaid, isApproved, isFilled }) => {
|
||||||
|
return isPaid && isApproved && !isFilled;
|
||||||
|
})
|
||||||
|
.map(({
|
||||||
id,
|
id,
|
||||||
company,
|
company,
|
||||||
position,
|
position,
|
||||||
isHighlighted,
|
isHighlighted,
|
||||||
description,
|
locale
|
||||||
logo,
|
}) => {
|
||||||
city,
|
|
||||||
state,
|
const className = classnames({
|
||||||
email,
|
'jobs-list': true,
|
||||||
phone,
|
'col-xs-12': true,
|
||||||
postedOn
|
'jobs-list-highlight': isHighlighted
|
||||||
},
|
});
|
||||||
index
|
|
||||||
) => {
|
return (
|
||||||
const header = (
|
<ListGroupItem
|
||||||
<div>
|
className={ className }
|
||||||
<h4 style={{ display: 'inline-block' }}>{ company }</h4>
|
key={ id }
|
||||||
<h5
|
onClick={ () => handleClick(id) }>
|
||||||
className='pull-right hidden-xs hidden-md'
|
<div>
|
||||||
style={{ display: 'inline-block' }}>
|
<h4 className='pull-left' style={{ display: 'inline-block' }}>
|
||||||
{ position }
|
<bold>{ company }</bold>
|
||||||
</h5>
|
{' '}
|
||||||
</div>
|
<span className='hidden-xs hidden-sm'>
|
||||||
);
|
- { position }
|
||||||
return (
|
</span>
|
||||||
<Panel
|
</h4>
|
||||||
bsStyle={ isHighlighted ? 'warning' : 'default' }
|
<h4
|
||||||
collapsible={ true }
|
className='pull-right'
|
||||||
eventKey={ index }
|
style={{ display: 'inline-block' }}>
|
||||||
header={ header }
|
{ this.addLocation(locale) }
|
||||||
key={ id }>
|
</h4>
|
||||||
<Well>
|
</div>
|
||||||
<Thumbnail
|
</ListGroupItem>
|
||||||
alt={ company + 'company logo' }
|
);
|
||||||
src={ logo }
|
});
|
||||||
style={ thumbnailStyle } />
|
|
||||||
<Panel>
|
|
||||||
Position: { position }
|
|
||||||
Location: { city }, { state }
|
|
||||||
<br />
|
|
||||||
Contact: { email || phone || 'N/A' }
|
|
||||||
<br />
|
|
||||||
Posted On: { moment(postedOn).format('MMMM Do, YYYY') }
|
|
||||||
</Panel>
|
|
||||||
<p onClick={ () => handleClick(id) }>{ description }</p>
|
|
||||||
</Well>
|
|
||||||
</Panel>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -77,9 +71,9 @@ export default React.createClass({
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelGroup>
|
<ListGroup>
|
||||||
{ this.renderJobs(handleClick, jobs) }
|
{ this.renderJobs(handleClick, jobs) }
|
||||||
</PanelGroup>
|
</ListGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
import { helpers } from 'rx';
|
||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { History } from 'react-router';
|
import { History } from 'react-router';
|
||||||
import { contain } from 'thundercats-react';
|
import { contain } from 'thundercats-react';
|
||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
|
import dedent from 'dedent';
|
||||||
|
import normalizeUrl from 'normalize-url';
|
||||||
|
|
||||||
import { getDefaults } from '../utils';
|
import { getDefaults } from '../utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -14,13 +18,13 @@ import {
|
|||||||
Col,
|
Col,
|
||||||
Input,
|
Input,
|
||||||
Row,
|
Row,
|
||||||
|
Panel,
|
||||||
Well
|
Well
|
||||||
} from 'react-bootstrap';
|
} from 'react-bootstrap';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isAscii,
|
isAscii,
|
||||||
isEmail,
|
isEmail,
|
||||||
isMobilePhone,
|
|
||||||
isURL
|
isURL
|
||||||
} from 'validator';
|
} from 'validator';
|
||||||
|
|
||||||
@ -31,12 +35,32 @@ const checkValidity = [
|
|||||||
'locale',
|
'locale',
|
||||||
'description',
|
'description',
|
||||||
'email',
|
'email',
|
||||||
'phone',
|
|
||||||
'url',
|
'url',
|
||||||
'logo',
|
'logo',
|
||||||
'name',
|
'company',
|
||||||
'highlight'
|
'isHighlighted',
|
||||||
|
'howToApply'
|
||||||
];
|
];
|
||||||
|
const hightlightCopy = `
|
||||||
|
Highlight my post to make it stand out. (+$250)
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
const isRemoteCopy = `
|
||||||
|
This job can be performed remotely.
|
||||||
|
`;
|
||||||
|
|
||||||
|
const howToApplyCopy = dedent`
|
||||||
|
Examples: click here to apply yourcompany.com/jobs/33
|
||||||
|
Or email jobs@yourcompany.com
|
||||||
|
`;
|
||||||
|
|
||||||
|
const checkboxClass = dedent`
|
||||||
|
text-left
|
||||||
|
jobs-checkbox-spacer
|
||||||
|
col-sm-offset-2
|
||||||
|
col-sm-6 col-md-offset-3
|
||||||
|
`;
|
||||||
|
|
||||||
function formatValue(value, validator, type = 'string') {
|
function formatValue(value, validator, type = 'string') {
|
||||||
const formated = getDefaults(type);
|
const formated = getDefaults(type);
|
||||||
@ -50,12 +74,32 @@ function formatValue(value, validator, type = 'string') {
|
|||||||
return formated;
|
return formated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeOptions = {
|
||||||
|
stripWWW: false
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatUrl(url, shouldKeepTrailingSlash = true) {
|
||||||
|
if (
|
||||||
|
typeof url === 'string' &&
|
||||||
|
url.length > 4 &&
|
||||||
|
url.indexOf('.') !== -1
|
||||||
|
) {
|
||||||
|
// prevent trailing / from being stripped during typing
|
||||||
|
let lastChar = '';
|
||||||
|
if (shouldKeepTrailingSlash && url.substring(url.length - 1) === '/') {
|
||||||
|
lastChar = '/';
|
||||||
|
}
|
||||||
|
return normalizeUrl(url, normalizeOptions) + lastChar;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
function isValidURL(data) {
|
function isValidURL(data) {
|
||||||
return isURL(data, { 'require_protocol': true });
|
return isURL(data, { 'require_protocol': true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidPhone(data) {
|
function makeRequired(validator) {
|
||||||
return isMobilePhone(data, 'en-US');
|
return (val) => !!val && validator(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default contain({
|
export default contain({
|
||||||
@ -67,22 +111,28 @@ export default contain({
|
|||||||
locale,
|
locale,
|
||||||
description,
|
description,
|
||||||
email,
|
email,
|
||||||
phone,
|
|
||||||
url,
|
url,
|
||||||
logo,
|
logo,
|
||||||
name,
|
company,
|
||||||
highlight
|
isFrontEndCert = true,
|
||||||
|
isFullStackCert,
|
||||||
|
isHighlighted,
|
||||||
|
isRemoteOk,
|
||||||
|
howToApply
|
||||||
} = form;
|
} = form;
|
||||||
return {
|
return {
|
||||||
position: formatValue(position, isAscii),
|
position: formatValue(position, makeRequired(isAscii)),
|
||||||
locale: formatValue(locale, isAscii),
|
locale: formatValue(locale, makeRequired(isAscii)),
|
||||||
description: formatValue(description, isAscii),
|
description: formatValue(description, makeRequired(helpers.identity)),
|
||||||
email: formatValue(email, isEmail),
|
email: formatValue(email, makeRequired(isEmail)),
|
||||||
phone: formatValue(phone, isValidPhone),
|
url: formatValue(formatUrl(url), isValidURL),
|
||||||
url: formatValue(url, isValidURL),
|
logo: formatValue(formatUrl(logo), isValidURL),
|
||||||
logo: formatValue(logo, isValidURL),
|
company: formatValue(company, makeRequired(isAscii)),
|
||||||
name: formatValue(name, isAscii),
|
isHighlighted: formatValue(isHighlighted, null, 'bool'),
|
||||||
highlight: formatValue(highlight, null, 'bool')
|
isRemoteOk: formatValue(isRemoteOk, null, 'bool'),
|
||||||
|
howToApply: formatValue(howToApply, makeRequired(isAscii)),
|
||||||
|
isFrontEndCert,
|
||||||
|
isFullStackCert
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
subscribeOnWillMount() {
|
subscribeOnWillMount() {
|
||||||
@ -98,55 +148,66 @@ export default contain({
|
|||||||
locale: PropTypes.object,
|
locale: PropTypes.object,
|
||||||
description: PropTypes.object,
|
description: PropTypes.object,
|
||||||
email: PropTypes.object,
|
email: PropTypes.object,
|
||||||
phone: PropTypes.object,
|
|
||||||
url: PropTypes.object,
|
url: PropTypes.object,
|
||||||
logo: PropTypes.object,
|
logo: PropTypes.object,
|
||||||
name: PropTypes.object,
|
company: PropTypes.object,
|
||||||
highlight: PropTypes.object
|
isHighlighted: PropTypes.object,
|
||||||
|
isRemoteOk: PropTypes.object,
|
||||||
|
isFrontEndCert: PropTypes.bool,
|
||||||
|
isFullStackCert: PropTypes.bool,
|
||||||
|
howToApply: PropTypes.object
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [History],
|
mixins: [History],
|
||||||
|
|
||||||
handleSubmit(e) {
|
handleSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const props = this.props;
|
const pros = this.props;
|
||||||
let valid = true;
|
let valid = true;
|
||||||
checkValidity.forEach((prop) => {
|
checkValidity.forEach((prop) => {
|
||||||
// if value exist, check if it is valid
|
// if value exist, check if it is valid
|
||||||
if (props[prop].value && props[prop].type !== 'boolean') {
|
if (pros[prop].value && pros[prop].type !== 'boolean') {
|
||||||
valid = valid && !!props[prop].valid;
|
valid = valid && !!pros[prop].valid;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid || !pros.isFrontEndCert && !pros.isFullStackCert ) {
|
||||||
debug('form not valid');
|
debug('form not valid');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
jobActions,
|
||||||
|
|
||||||
|
// form values
|
||||||
position,
|
position,
|
||||||
locale,
|
locale,
|
||||||
description,
|
description,
|
||||||
email,
|
email,
|
||||||
phone,
|
|
||||||
url,
|
url,
|
||||||
logo,
|
logo,
|
||||||
name,
|
company,
|
||||||
highlight,
|
isFrontEndCert,
|
||||||
jobActions
|
isFullStackCert,
|
||||||
|
isHighlighted,
|
||||||
|
isRemoteOk,
|
||||||
|
howToApply
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
// sanitize user output
|
// sanitize user output
|
||||||
const jobValues = {
|
const jobValues = {
|
||||||
position: inHTMLData(position.value),
|
position: inHTMLData(position.value),
|
||||||
location: inHTMLData(locale.value),
|
locale: inHTMLData(locale.value),
|
||||||
description: inHTMLData(description.value),
|
description: inHTMLData(description.value),
|
||||||
email: inHTMLData(email.value),
|
email: inHTMLData(email.value),
|
||||||
phone: inHTMLData(phone.value),
|
url: formatUrl(uriInSingleQuotedAttr(url.value), false),
|
||||||
url: uriInSingleQuotedAttr(url.value),
|
logo: formatUrl(uriInSingleQuotedAttr(logo.value), false),
|
||||||
logo: uriInSingleQuotedAttr(logo.value),
|
company: inHTMLData(company.value),
|
||||||
name: inHTMLData(name.value),
|
isHighlighted: !!isHighlighted.value,
|
||||||
highlight: !!highlight.value
|
isRemoteOk: !!isRemoteOk.value,
|
||||||
|
howToApply: inHTMLData(howToApply.value),
|
||||||
|
isFrontEndCert,
|
||||||
|
isFullStackCert
|
||||||
};
|
};
|
||||||
|
|
||||||
const job = Object.keys(jobValues).reduce((accu, prop) => {
|
const job = Object.keys(jobValues).reduce((accu, prop) => {
|
||||||
@ -173,19 +234,35 @@ export default contain({
|
|||||||
handleForm({ [name]: value });
|
handleForm({ [name]: value });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleCertClick(name) {
|
||||||
|
const { jobActions: { handleForm } } = this.props;
|
||||||
|
const otherButton = name === 'isFrontEndCert' ?
|
||||||
|
'isFullStackCert' :
|
||||||
|
'isFrontEndCert';
|
||||||
|
|
||||||
|
handleForm({
|
||||||
|
[name]: true,
|
||||||
|
[otherButton]: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
position,
|
position,
|
||||||
locale,
|
locale,
|
||||||
description,
|
description,
|
||||||
email,
|
email,
|
||||||
phone,
|
|
||||||
url,
|
url,
|
||||||
logo,
|
logo,
|
||||||
name,
|
company,
|
||||||
highlight,
|
isHighlighted,
|
||||||
|
isRemoteOk,
|
||||||
|
howToApply,
|
||||||
|
isFrontEndCert,
|
||||||
|
isFullStackCert,
|
||||||
jobActions: { handleForm }
|
jobActions: { handleForm }
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { handleChange } = this;
|
const { handleChange } = this;
|
||||||
const labelClass = 'col-sm-offset-1 col-sm-2';
|
const labelClass = 'col-sm-offset-1 col-sm-2';
|
||||||
const inputClass = 'col-sm-6';
|
const inputClass = 'col-sm-6';
|
||||||
@ -193,22 +270,71 @@ export default contain({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col
|
||||||
<Well className='text-center'>
|
md={ 10 }
|
||||||
<h1>Create Your Job Post</h1>
|
mdOffset={ 1 }>
|
||||||
|
<Panel className='text-center'>
|
||||||
<form
|
<form
|
||||||
className='form-horizontal'
|
className='form-horizontal'
|
||||||
onSubmit={ this.handleSubmit }>
|
onSubmit={ this.handleSubmit }>
|
||||||
|
|
||||||
<div className='spacer'>
|
<div className='spacer'>
|
||||||
<h2>Job Information</h2>
|
<h2>First, select your ideal applicant: </h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
xs={ 6 }
|
||||||
|
xsOffset={ 3 }>
|
||||||
|
<Row>
|
||||||
|
<Button
|
||||||
|
className={ isFrontEndCert ? 'active' : '' }
|
||||||
|
onClick={ () => {
|
||||||
|
if (!isFrontEndCert) {
|
||||||
|
this.handleCertClick('isFrontEndCert');
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<h4>Front End Development Certified</h4>
|
||||||
|
You can expect each applicant
|
||||||
|
to have a code portfolio using the
|
||||||
|
following technologies:
|
||||||
|
HTML5, CSS, jQuery, API integrations
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
<div className='button-spacer' />
|
||||||
|
<Row>
|
||||||
|
<Button
|
||||||
|
className={ isFullStackCert ? 'active' : ''}
|
||||||
|
onClick={ () => {
|
||||||
|
if (!isFullStackCert) {
|
||||||
|
this.handleCertClick('isFullStackCert');
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<h4>Full Stack Development Certified</h4>
|
||||||
|
You can expect each applicant to have a code
|
||||||
|
portfolio using the following technologies:
|
||||||
|
HTML5, CSS, jQuery, API integrations, MVC Framework,
|
||||||
|
JavaScript, Node.js, MongoDB, Express.js
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div className='spacer'>
|
||||||
|
<h2>Tell us about the position</h2>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
bsStyle={ position.bsStyle }
|
bsStyle={ position.bsStyle }
|
||||||
label='Position'
|
label='Job Title'
|
||||||
labelClassName={ labelClass }
|
labelClassName={ labelClass }
|
||||||
onChange={ (e) => handleChange('position', e) }
|
onChange={ (e) => handleChange('position', e) }
|
||||||
placeholder='Position'
|
placeholder={
|
||||||
|
'e.g. Full Stack Developer, Front End Developer, etc.'
|
||||||
|
}
|
||||||
|
required={ true }
|
||||||
type='text'
|
type='text'
|
||||||
value={ position.value }
|
value={ position.value }
|
||||||
wrapperClassName={ inputClass } />
|
wrapperClassName={ inputClass } />
|
||||||
@ -217,7 +343,8 @@ export default contain({
|
|||||||
label='Location'
|
label='Location'
|
||||||
labelClassName={ labelClass }
|
labelClassName={ labelClass }
|
||||||
onChange={ (e) => handleChange('locale', e) }
|
onChange={ (e) => handleChange('locale', e) }
|
||||||
placeholder='Location'
|
placeholder='e.g. San Francisco, Remote, etc.'
|
||||||
|
required={ true }
|
||||||
type='text'
|
type='text'
|
||||||
value={ locale.value }
|
value={ locale.value }
|
||||||
wrapperClassName={ inputClass } />
|
wrapperClassName={ inputClass } />
|
||||||
@ -226,48 +353,67 @@ export default contain({
|
|||||||
label='Description'
|
label='Description'
|
||||||
labelClassName={ labelClass }
|
labelClassName={ labelClass }
|
||||||
onChange={ (e) => handleChange('description', e) }
|
onChange={ (e) => handleChange('description', e) }
|
||||||
placeholder='Description'
|
required={ true }
|
||||||
rows='10'
|
rows='10'
|
||||||
type='textarea'
|
type='textarea'
|
||||||
value={ description.value }
|
value={ description.value }
|
||||||
wrapperClassName={ inputClass } />
|
wrapperClassName={ inputClass } />
|
||||||
|
<Input
|
||||||
|
checked={ isRemoteOk.value }
|
||||||
|
label={ isRemoteCopy }
|
||||||
|
onChange={
|
||||||
|
({ target: { checked } }) => handleForm({
|
||||||
|
isRemoteOk: !!checked
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type='checkbox'
|
||||||
|
wrapperClassName={ checkboxClass } />
|
||||||
|
<div className='spacer' />
|
||||||
|
<Row>
|
||||||
|
<div>
|
||||||
|
<h2>How should they apply?</h2>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
bsStyle={ howToApply.bsStyle }
|
||||||
|
label=' '
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
onChange={ (e) => handleChange('howToApply', e) }
|
||||||
|
placeholder={ howToApplyCopy }
|
||||||
|
required={ true }
|
||||||
|
rows='2'
|
||||||
|
type='textarea'
|
||||||
|
value={ howToApply.value }
|
||||||
|
wrapperClassName={ inputClass } />
|
||||||
|
</Row>
|
||||||
|
|
||||||
<div className='divider'>
|
<div className='spacer' />
|
||||||
<h2>Company Information</h2>
|
<div>
|
||||||
|
<h2>Tell us about your organization</h2>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
bsStyle={ name.bsStyle }
|
bsStyle={ company.bsStyle }
|
||||||
label='Company Name'
|
label='Company Name'
|
||||||
labelClassName={ labelClass }
|
labelClassName={ labelClass }
|
||||||
onChange={ (e) => handleChange('name', e) }
|
onChange={ (e) => handleChange('company', e) }
|
||||||
placeholder='Foo, INC'
|
|
||||||
type='text'
|
type='text'
|
||||||
value={ name.value }
|
value={ company.value }
|
||||||
wrapperClassName={ inputClass } />
|
wrapperClassName={ inputClass } />
|
||||||
<Input
|
<Input
|
||||||
bsStyle={ email.bsStyle }
|
bsStyle={ email.bsStyle }
|
||||||
label='Email'
|
label='Email'
|
||||||
labelClassName={ labelClass }
|
labelClassName={ labelClass }
|
||||||
onChange={ (e) => handleChange('email', e) }
|
onChange={ (e) => handleChange('email', e) }
|
||||||
placeholder='Email'
|
placeholder='This is how we will contact you'
|
||||||
|
required={ true }
|
||||||
type='email'
|
type='email'
|
||||||
value={ email.value }
|
value={ email.value }
|
||||||
wrapperClassName={ inputClass } />
|
wrapperClassName={ inputClass } />
|
||||||
<Input
|
|
||||||
bsStyle={ phone.bsStyle }
|
|
||||||
label='Phone'
|
|
||||||
labelClassName={ labelClass }
|
|
||||||
onChange={ (e) => handleChange('phone', e) }
|
|
||||||
placeholder='555-123-1234'
|
|
||||||
type='tel'
|
|
||||||
value={ phone.value }
|
|
||||||
wrapperClassName={ inputClass } />
|
|
||||||
<Input
|
<Input
|
||||||
bsStyle={ url.bsStyle }
|
bsStyle={ url.bsStyle }
|
||||||
label='URL'
|
label='URL'
|
||||||
labelClassName={ labelClass }
|
labelClassName={ labelClass }
|
||||||
onChange={ (e) => handleChange('url', e) }
|
onChange={ (e) => handleChange('url', e) }
|
||||||
placeholder='http://freecatphotoapp.com'
|
placeholder='http://yourcompany.com'
|
||||||
type='url'
|
type='url'
|
||||||
value={ url.value }
|
value={ url.value }
|
||||||
wrapperClassName={ inputClass } />
|
wrapperClassName={ inputClass } />
|
||||||
@ -276,27 +422,48 @@ export default contain({
|
|||||||
label='Logo'
|
label='Logo'
|
||||||
labelClassName={ labelClass }
|
labelClassName={ labelClass }
|
||||||
onChange={ (e) => handleChange('logo', e) }
|
onChange={ (e) => handleChange('logo', e) }
|
||||||
placeholder='http://freecatphotoapp.com/logo.png'
|
placeholder='http://yourcompany.com/logo.png'
|
||||||
type='url'
|
type='url'
|
||||||
value={ logo.value }
|
value={ logo.value }
|
||||||
wrapperClassName={ inputClass } />
|
wrapperClassName={ inputClass } />
|
||||||
|
|
||||||
<div className='divider'>
|
|
||||||
<h2>Make it stand out</h2>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
checked={ highlight.value }
|
|
||||||
label='Highlight your ad'
|
|
||||||
labelClassName={ 'col-sm-offset-1 col-sm-6'}
|
|
||||||
onChange={
|
|
||||||
({ target: { checked } }) => handleForm({
|
|
||||||
highlight: !!checked
|
|
||||||
})
|
|
||||||
}
|
|
||||||
type='checkbox' />
|
|
||||||
<div className='spacer' />
|
<div className='spacer' />
|
||||||
|
<Well>
|
||||||
|
<div>
|
||||||
|
<h2>Make it stand out</h2>
|
||||||
|
</div>
|
||||||
|
<div className='spacer' />
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 6 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
Highlight this ad to give it extra attention.
|
||||||
|
<br />
|
||||||
|
Featured listings receive more clicks and more applications.
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div className='spacer' />
|
||||||
|
<Row>
|
||||||
|
<Input
|
||||||
|
bsSize='large'
|
||||||
|
bsStyle='success'
|
||||||
|
checked={ isHighlighted.value }
|
||||||
|
label={ hightlightCopy }
|
||||||
|
onChange={
|
||||||
|
({ target: { checked } }) => handleForm({
|
||||||
|
isHighlighted: !!checked
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type='checkbox'
|
||||||
|
wrapperClassName={
|
||||||
|
checkboxClass.replace('text-left', '')
|
||||||
|
} />
|
||||||
|
</Row>
|
||||||
|
</Well>
|
||||||
|
|
||||||
<Row>
|
<Row>
|
||||||
<Col
|
<Col
|
||||||
|
className='text-left'
|
||||||
lg={ 6 }
|
lg={ 6 }
|
||||||
lgOffset={ 3 }>
|
lgOffset={ 3 }>
|
||||||
<Button
|
<Button
|
||||||
@ -309,7 +476,7 @@ export default contain({
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</form>
|
</form>
|
||||||
</Well>
|
</Panel>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
39
common/app/routes/Jobs/components/NewJobCompleted.jsx
Normal file
39
common/app/routes/Jobs/components/NewJobCompleted.jsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { LinkContainer } from 'react-router-bootstrap';
|
||||||
|
import { Button, Panel, Col, Row } from 'react-bootstrap';
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
|
displayName: 'NewJobCompleted',
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className='text-center'>
|
||||||
|
<Panel>
|
||||||
|
<Row>
|
||||||
|
<h1>
|
||||||
|
Your Position has Been Submitted
|
||||||
|
</h1>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 6 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
We’ll review your listing and email you when it’s live.
|
||||||
|
<br />
|
||||||
|
Thank you for listing this job with Free Code Camp.
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div className='spacer' />
|
||||||
|
<LinkContainer to={ '/jobs' }>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='large'
|
||||||
|
bsStyle='primary'>
|
||||||
|
Go to the job board
|
||||||
|
</Button>
|
||||||
|
</LinkContainer>
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
@ -1,14 +1,85 @@
|
|||||||
// import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
|
import { Lifecycle } from 'react-router';
|
||||||
|
import { Panel, Button, Row, Col } from 'react-bootstrap';
|
||||||
import { contain } from 'thundercats-react';
|
import { contain } from 'thundercats-react';
|
||||||
|
|
||||||
import ShowJob from './ShowJob.jsx';
|
import ShowJob from './ShowJob.jsx';
|
||||||
|
import JobNotFound from './JobNotFound.jsx';
|
||||||
|
|
||||||
export default contain(
|
export default contain(
|
||||||
{
|
{
|
||||||
store: 'JobsStore',
|
store: 'JobsStore',
|
||||||
actions: 'JobActions',
|
actions: [
|
||||||
|
'appActions',
|
||||||
|
'jobActions'
|
||||||
|
],
|
||||||
map({ form: job = {} }) {
|
map({ form: job = {} }) {
|
||||||
return { job };
|
return { job };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ShowJob
|
React.createClass({
|
||||||
|
displayName: 'Preview',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
appActions: PropTypes.object,
|
||||||
|
job: PropTypes.object,
|
||||||
|
jobActions: PropTypes.object
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [Lifecycle],
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { appActions, job } = this.props;
|
||||||
|
// redirect user in client
|
||||||
|
if (!job || !job.position || !job.description) {
|
||||||
|
appActions.updateRoute('/jobs/new');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
routerWillLeave() {
|
||||||
|
const { jobActions } = this.props;
|
||||||
|
jobActions.clearPromo();
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { appActions, job, jobActions } = this.props;
|
||||||
|
|
||||||
|
if (!job || !job.position || !job.description) {
|
||||||
|
return <JobNotFound />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ShowJob job={ job } />
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 10 }
|
||||||
|
mdOffset={ 1 }
|
||||||
|
xs={ 12 }>
|
||||||
|
<Panel>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
className='signup-btn'
|
||||||
|
onClick={ () => {
|
||||||
|
jobActions.clearSavedForm();
|
||||||
|
jobActions.saveJobToDb({
|
||||||
|
goTo: '/jobs/new/check-out',
|
||||||
|
job
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
|
||||||
|
Looks great! Let's Check Out
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
onClick={ () => appActions.goBack() } >
|
||||||
|
Head back and make edits
|
||||||
|
</Button>
|
||||||
|
</Panel>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
@ -1,12 +1,71 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { History } from 'react-router';
|
||||||
import { contain } from 'thundercats-react';
|
import { contain } from 'thundercats-react';
|
||||||
|
|
||||||
import ShowJob from './ShowJob.jsx';
|
import ShowJob from './ShowJob.jsx';
|
||||||
|
import JobNotFound from './JobNotFound.jsx';
|
||||||
|
import { isJobValid } from '../utils';
|
||||||
|
|
||||||
|
function shouldShowApply(
|
||||||
|
{
|
||||||
|
isFrontEndCert: isFrontEndCertReq = false,
|
||||||
|
isFullStackCert: isFullStackCertReq = false
|
||||||
|
}, {
|
||||||
|
isFrontEndCert = false,
|
||||||
|
isFullStackCert = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return (!isFrontEndCertReq && !isFullStackCertReq) ||
|
||||||
|
(isFullStackCertReq && isFullStackCert) ||
|
||||||
|
(isFrontEndCertReq && isFrontEndCert);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateMessage(
|
||||||
|
{
|
||||||
|
isFrontEndCert: isFrontEndCertReq = false,
|
||||||
|
isFullStackCert: isFullStackCertReq = false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isFrontEndCert = false,
|
||||||
|
isFullStackCert = false,
|
||||||
|
isSignedIn = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
|
||||||
|
if (!isSignedIn) {
|
||||||
|
return 'Must be signed in to apply';
|
||||||
|
}
|
||||||
|
if (isFrontEndCertReq && !isFrontEndCert) {
|
||||||
|
return 'This employer requires Free Code Camp’s Front ' +
|
||||||
|
'End Development Certification in order to apply';
|
||||||
|
}
|
||||||
|
if (isFullStackCertReq && !isFullStackCert) {
|
||||||
|
return 'This employer requires Free Code Camp’s Full ' +
|
||||||
|
'Stack Development Certification in order to apply';
|
||||||
|
}
|
||||||
|
if (isFrontEndCertReq && isFrontEndCertReq) {
|
||||||
|
return 'This employer requires the Front End Development Certification. ' +
|
||||||
|
"You've earned it, so feel free to apply.";
|
||||||
|
}
|
||||||
|
return 'This employer requires the Full Stack Development Certification. ' +
|
||||||
|
"You've earned it, so feel free to apply.";
|
||||||
|
}
|
||||||
|
|
||||||
export default contain(
|
export default contain(
|
||||||
{
|
{
|
||||||
store: 'jobsStore',
|
stores: ['appStore', 'jobsStore'],
|
||||||
|
fetchWaitFor: 'jobsStore',
|
||||||
fetchAction: 'jobActions.getJob',
|
fetchAction: 'jobActions.getJob',
|
||||||
map({ currentJob }) {
|
combineLatest(
|
||||||
return { job: currentJob };
|
{ username, isFrontEndCert, isFullStackCert },
|
||||||
|
{ currentJob }
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
username,
|
||||||
|
job: currentJob,
|
||||||
|
isFrontEndCert,
|
||||||
|
isFullStackCert
|
||||||
|
};
|
||||||
},
|
},
|
||||||
getPayload({ params: { id }, job = {} }) {
|
getPayload({ params: { id }, job = {} }) {
|
||||||
return {
|
return {
|
||||||
@ -20,5 +79,57 @@ export default contain(
|
|||||||
return job.id !== id;
|
return job.id !== id;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ShowJob
|
React.createClass({
|
||||||
|
displayName: 'Show',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
job: PropTypes.object,
|
||||||
|
isFullStackCert: PropTypes.bool,
|
||||||
|
isFrontEndCert: PropTypes.bool,
|
||||||
|
username: PropTypes.string
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [History],
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { job } = this.props;
|
||||||
|
// redirect user in client
|
||||||
|
if (!isJobValid(job)) {
|
||||||
|
this.history.pushState(null, '/jobs');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isFullStackCert,
|
||||||
|
isFrontEndCert,
|
||||||
|
job,
|
||||||
|
username
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (!isJobValid(job)) {
|
||||||
|
return <JobNotFound />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSignedIn = !!username;
|
||||||
|
|
||||||
|
const showApply = shouldShowApply(
|
||||||
|
job,
|
||||||
|
{ isFrontEndCert, isFullStackCert }
|
||||||
|
);
|
||||||
|
|
||||||
|
const message = generateMessage(
|
||||||
|
job,
|
||||||
|
{ isFrontEndCert, isFullStackCert, isSignedIn }
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ShowJob
|
||||||
|
message={ message }
|
||||||
|
preview={ false }
|
||||||
|
showApply={ showApply }
|
||||||
|
{ ...this.props }/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { Row, Thumbnail, Panel, Well } from 'react-bootstrap';
|
import { Well, Row, Col, Thumbnail, Panel } from 'react-bootstrap';
|
||||||
import moment from 'moment';
|
import urlRegexFactory from 'url-regex';
|
||||||
|
|
||||||
|
const urlRegex = urlRegexFactory();
|
||||||
|
const defaultImage =
|
||||||
|
'https://s3.amazonaws.com/freecodecamp/camper-image-placeholder.png';
|
||||||
|
|
||||||
const thumbnailStyle = {
|
const thumbnailStyle = {
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
@ -8,11 +12,20 @@ const thumbnailStyle = {
|
|||||||
maxWidth: '100px'
|
maxWidth: '100px'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function addATags(text) {
|
||||||
|
return text.replace(urlRegex, function(match) {
|
||||||
|
return `<a href=${match} target='_blank'>${match}</a>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'ShowJob',
|
displayName: 'ShowJob',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
job: PropTypes.object,
|
job: PropTypes.object,
|
||||||
params: PropTypes.object
|
params: PropTypes.object,
|
||||||
|
showApply: PropTypes.bool,
|
||||||
|
preview: PropTypes.bool,
|
||||||
|
message: PropTypes.string
|
||||||
},
|
},
|
||||||
|
|
||||||
renderHeader({ company, position }) {
|
renderHeader({ company, position }) {
|
||||||
@ -28,38 +41,101 @@ export default React.createClass({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderHowToApply(showApply, preview, message, howToApply) {
|
||||||
|
if (!showApply) {
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 6 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<h4 className='bg-info text-center'>{ message }</h4>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 6 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<Well>
|
||||||
|
<bold>{ preview ? 'How do I apply?' : message }</bold>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<span dangerouslySetInnerHTML={{
|
||||||
|
__html: addATags(howToApply)
|
||||||
|
}} />
|
||||||
|
</Well>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { job = {} } = this.props;
|
const {
|
||||||
|
showApply = true,
|
||||||
|
message,
|
||||||
|
preview = true,
|
||||||
|
job = {}
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
logo,
|
logo,
|
||||||
position,
|
position,
|
||||||
city,
|
city,
|
||||||
company,
|
company,
|
||||||
state,
|
state,
|
||||||
email,
|
locale,
|
||||||
phone,
|
description,
|
||||||
postedOn,
|
howToApply
|
||||||
description
|
|
||||||
} = job;
|
} = job;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Row>
|
<Row>
|
||||||
<Well>
|
<Col
|
||||||
<Thumbnail
|
md={ 10 }
|
||||||
alt={ company + 'company logo' }
|
mdOffset={ 1 }
|
||||||
src={ logo }
|
xs={ 12 }>
|
||||||
style={ thumbnailStyle } />
|
|
||||||
<Panel>
|
<Panel>
|
||||||
Position: { position }
|
<Row>
|
||||||
Location: { city }, { state }
|
<h2 className='text-center'>
|
||||||
<br />
|
{ company }
|
||||||
Contact: { email || phone || 'N/A' }
|
</h2>
|
||||||
<br />
|
</Row>
|
||||||
Posted On: { moment(postedOn).format('MMMM Do, YYYY') }
|
<div className='spacer' />
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 2 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<Thumbnail
|
||||||
|
alt={ logo ? company + 'company logo' : 'stock image' }
|
||||||
|
src={ logo || defaultImage }
|
||||||
|
style={ thumbnailStyle } />
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
md={ 4 }>
|
||||||
|
|
||||||
|
<bold>Position: </bold> { position || 'N/A' }
|
||||||
|
<br />
|
||||||
|
<bold>Location: </bold>
|
||||||
|
{ locale ? locale : `${city}, ${state}` }
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div className='spacer' />
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 6 }
|
||||||
|
mdOffset={ 3 }
|
||||||
|
style={{ whiteSpace: 'pre-line' }}
|
||||||
|
xs={ 12 }>
|
||||||
|
<p>{ description }</p>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{ this.renderHowToApply(showApply, preview, message, howToApply) }
|
||||||
</Panel>
|
</Panel>
|
||||||
<p>{ description }</p>
|
</Col>
|
||||||
</Well>
|
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
33
common/app/routes/Jobs/components/TwitterBtn.jsx
Normal file
33
common/app/routes/Jobs/components/TwitterBtn.jsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { Button } from 'react-bootstrap';
|
||||||
|
|
||||||
|
const followLink = 'https://twitter.com/intent/follow?' +
|
||||||
|
'ref_src=twsrc%5Etfw&region=follow_link&screen_name=CamperJobs&' +
|
||||||
|
'amp;tw_p=followbutton';
|
||||||
|
|
||||||
|
function commify(count) {
|
||||||
|
return Number(count).toLocaleString('en');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
|
|
||||||
|
displayName: 'FollowButton',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
count: PropTypes.number
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { count } = this.props;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='large'
|
||||||
|
bsStyle='primary'
|
||||||
|
href={ followLink }
|
||||||
|
target='__blank'>
|
||||||
|
Join { commify(count) } followers who see our job postings on Twitter.
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
@ -1,6 +1,8 @@
|
|||||||
import { Actions } from 'thundercats';
|
import { Actions } from 'thundercats';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
|
import { jsonp$ } from '../../../../utils/jsonp$';
|
||||||
|
import { postJSON$ } from '../../../../utils/ajax-stream';
|
||||||
|
|
||||||
const debug = debugFactory('freecc:jobs:actions');
|
const debug = debugFactory('freecc:jobs:actions');
|
||||||
const assign = Object.assign;
|
const assign = Object.assign;
|
||||||
@ -31,6 +33,7 @@ export default Actions({
|
|||||||
},
|
},
|
||||||
setError: null,
|
setError: null,
|
||||||
getJob: null,
|
getJob: null,
|
||||||
|
saveJobToDb: null,
|
||||||
getJobs(params) {
|
getJobs(params) {
|
||||||
return { params };
|
return { params };
|
||||||
},
|
},
|
||||||
@ -56,12 +59,47 @@ export default Actions({
|
|||||||
},
|
},
|
||||||
saveForm: null,
|
saveForm: null,
|
||||||
getSavedForm: null,
|
getSavedForm: null,
|
||||||
|
clearSavedForm: null,
|
||||||
setForm(form) {
|
setForm(form) {
|
||||||
return { form };
|
return { form };
|
||||||
|
},
|
||||||
|
getFollowers: null,
|
||||||
|
setFollowersCount(numOfFollowers) {
|
||||||
|
return { numOfFollowers };
|
||||||
|
},
|
||||||
|
setPromoCode({ target: { value = '' }} = {}) {
|
||||||
|
return { promoCode: value.replace(/[^\d\w\s]/, '') };
|
||||||
|
},
|
||||||
|
applyCode: null,
|
||||||
|
clearPromo(foo, undef) {
|
||||||
|
return {
|
||||||
|
price: undef,
|
||||||
|
buttonId: undef,
|
||||||
|
discountAmount: undef,
|
||||||
|
promoCode: undef,
|
||||||
|
promoApplied: false,
|
||||||
|
promoName: undef
|
||||||
|
};
|
||||||
|
},
|
||||||
|
applyPromo({
|
||||||
|
fullPrice: price,
|
||||||
|
buttonId,
|
||||||
|
discountAmount,
|
||||||
|
code: promoCode,
|
||||||
|
name: promoName
|
||||||
|
} = {}) {
|
||||||
|
return {
|
||||||
|
price,
|
||||||
|
buttonId,
|
||||||
|
discountAmount,
|
||||||
|
promoCode,
|
||||||
|
promoApplied: true,
|
||||||
|
promoName
|
||||||
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.refs({ displayName: 'JobActions' })
|
.refs({ displayName: 'JobActions' })
|
||||||
.init(({ instance: jobActions, args: [services] }) => {
|
.init(({ instance: jobActions, args: [cat, services] }) => {
|
||||||
jobActions.getJobs.subscribe(() => {
|
jobActions.getJobs.subscribe(() => {
|
||||||
services.read('jobs', null, null, (err, jobs) => {
|
services.read('jobs', null, null, (err, jobs) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -100,5 +138,58 @@ export default Actions({
|
|||||||
jobActions.setForm(job);
|
jobActions.setForm(job);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jobActions.clearSavedForm.subscribe(() => {
|
||||||
|
store.remove('newJob');
|
||||||
|
});
|
||||||
|
|
||||||
|
jobActions.saveJobToDb.subscribe(({ goTo, job }) => {
|
||||||
|
const appActions = cat.getActions('appActions');
|
||||||
|
services.create('jobs', { job }, null, (err, job) => {
|
||||||
|
if (err) {
|
||||||
|
debug('job services experienced an issue', err);
|
||||||
|
return jobActions.setError(err);
|
||||||
|
}
|
||||||
|
jobActions.setJobs({ job });
|
||||||
|
appActions.updateRoute(goTo);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
jobActions.getFollowers.subscribe(() => {
|
||||||
|
const url = 'https://cdn.syndication.twimg.com/widgets/followbutton/' +
|
||||||
|
'info.json?lang=en&screen_names=CamperJobs' +
|
||||||
|
'&callback=JSONPCallback';
|
||||||
|
|
||||||
|
jsonp$(url)
|
||||||
|
.map(({ response }) => {
|
||||||
|
return response[0]['followers_count'];
|
||||||
|
})
|
||||||
|
.subscribe(
|
||||||
|
count => jobActions.setFollowersCount(count),
|
||||||
|
err => jobActions.setError(err)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
jobActions.applyCode.subscribe(({ id, code = '', type = null}) => {
|
||||||
|
const body = {
|
||||||
|
id,
|
||||||
|
code: code.replace(/[^\d\w\s]/, '')
|
||||||
|
};
|
||||||
|
if (type) {
|
||||||
|
body.type = type;
|
||||||
|
}
|
||||||
|
postJSON$('/api/promos/getButton', body)
|
||||||
|
.pluck('response')
|
||||||
|
.subscribe(
|
||||||
|
({ promo }) => {
|
||||||
|
if (promo && promo.buttonId) {
|
||||||
|
jobActions.applyPromo(promo);
|
||||||
|
}
|
||||||
|
jobActions.setError(new Error('no promo found'));
|
||||||
|
},
|
||||||
|
jobActions.setError
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return jobActions;
|
return jobActions;
|
||||||
});
|
});
|
||||||
|
@ -6,9 +6,12 @@ const {
|
|||||||
transformer
|
transformer
|
||||||
} = Store;
|
} = Store;
|
||||||
|
|
||||||
export default Store({ showModal: false })
|
export default Store({
|
||||||
.refs({ displayName: 'JobsStore' })
|
refs: {
|
||||||
.init(({ instance: jobsStore, args: [cat] }) => {
|
displayName: 'JobsStore',
|
||||||
|
value: { showModal: false }
|
||||||
|
},
|
||||||
|
init({ instance: jobsStore, args: [cat] }) {
|
||||||
const {
|
const {
|
||||||
setJobs,
|
setJobs,
|
||||||
findJob,
|
findJob,
|
||||||
@ -16,7 +19,11 @@ export default Store({ showModal: false })
|
|||||||
openModal,
|
openModal,
|
||||||
closeModal,
|
closeModal,
|
||||||
handleForm,
|
handleForm,
|
||||||
setForm
|
setForm,
|
||||||
|
setFollowersCount,
|
||||||
|
setPromoCode,
|
||||||
|
applyPromo,
|
||||||
|
clearPromo
|
||||||
} = cat.getActions('JobActions');
|
} = cat.getActions('JobActions');
|
||||||
const register = createRegistrar(jobsStore);
|
const register = createRegistrar(jobsStore);
|
||||||
register(setter(setJobs));
|
register(setter(setJobs));
|
||||||
@ -24,7 +31,12 @@ export default Store({ showModal: false })
|
|||||||
register(setter(openModal));
|
register(setter(openModal));
|
||||||
register(setter(closeModal));
|
register(setter(closeModal));
|
||||||
register(setter(setForm));
|
register(setter(setForm));
|
||||||
|
register(setter(setPromoCode));
|
||||||
|
register(setter(applyPromo));
|
||||||
|
register(setter(clearPromo));
|
||||||
|
register(setter(setFollowersCount));
|
||||||
|
|
||||||
register(transformer(findJob));
|
register(transformer(findJob));
|
||||||
register(handleForm);
|
register(handleForm);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
@ -2,6 +2,8 @@ import Jobs from './components/Jobs.jsx';
|
|||||||
import NewJob from './components/NewJob.jsx';
|
import NewJob from './components/NewJob.jsx';
|
||||||
import Show from './components/Show.jsx';
|
import Show from './components/Show.jsx';
|
||||||
import Preview from './components/Preview.jsx';
|
import Preview from './components/Preview.jsx';
|
||||||
|
import GoToPayPal from './components/GoToPayPal.jsx';
|
||||||
|
import NewJobCompleted from './components/NewJobCompleted.jsx';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* index: /jobs list jobs
|
* index: /jobs list jobs
|
||||||
@ -19,6 +21,12 @@ export default {
|
|||||||
}, {
|
}, {
|
||||||
path: 'jobs/new/preview',
|
path: 'jobs/new/preview',
|
||||||
component: Preview
|
component: Preview
|
||||||
|
}, {
|
||||||
|
path: 'jobs/new/check-out',
|
||||||
|
component: GoToPayPal
|
||||||
|
}, {
|
||||||
|
path: 'jobs/new/completed',
|
||||||
|
component: NewJobCompleted
|
||||||
}, {
|
}, {
|
||||||
path: 'jobs/:id',
|
path: 'jobs/:id',
|
||||||
component: Show
|
component: Show
|
||||||
|
@ -20,3 +20,10 @@ export function getDefaults(type, value) {
|
|||||||
}
|
}
|
||||||
return Object.assign({}, defaults[type]);
|
return Object.assign({}, defaults[type]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isJobValid(job) {
|
||||||
|
return job &&
|
||||||
|
!job.isFilled &&
|
||||||
|
job.isApproved &&
|
||||||
|
job.isPaid;
|
||||||
|
}
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
import Jobs from './Jobs';
|
import Jobs from './Jobs';
|
||||||
import Hikes from './Hikes';
|
import Hikes from './Hikes';
|
||||||
|
import NotFound from '../components/NotFound/index.jsx';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
path: '/',
|
path: '/',
|
||||||
childRoutes: [
|
childRoutes: [
|
||||||
Jobs,
|
Jobs,
|
||||||
Hikes
|
Hikes,
|
||||||
|
{
|
||||||
|
path: '*',
|
||||||
|
component: NotFound
|
||||||
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
var React = require('react'),
|
|
||||||
Tailspin = require('tailspin');
|
|
||||||
|
|
||||||
var Editor = React.createClass({
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
value: React.PropTypes.string
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
value: [
|
|
||||||
'/**',
|
|
||||||
'* Your output will go here.',
|
|
||||||
'* Console.log() -type statements',
|
|
||||||
'* will appear in your browser\'s',
|
|
||||||
'* DevTools JavaScript console.',
|
|
||||||
'**/'
|
|
||||||
].join('\n')
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
var value = this.props.value;
|
|
||||||
var options = {
|
|
||||||
lineNumbers: false,
|
|
||||||
lineWrapping: true,
|
|
||||||
mode: 'text',
|
|
||||||
readOnly: 'noCursor',
|
|
||||||
textAreaClassName: 'hide-textarea',
|
|
||||||
theme: 'monokai',
|
|
||||||
value: value
|
|
||||||
};
|
|
||||||
|
|
||||||
var config = {
|
|
||||||
setSize: ['100%', '100%']
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form className='code'>
|
|
||||||
<div className='form-group codeMirrorView'>
|
|
||||||
<Tailspin
|
|
||||||
{ ...options }
|
|
||||||
config={ config }/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = Editor;
|
|
@ -1 +0,0 @@
|
|||||||
module.exports = require('./Display.jsx');
|
|
@ -1,91 +0,0 @@
|
|||||||
var React = require('react'),
|
|
||||||
debug = require('debug')('freecc:comp:editor'),
|
|
||||||
jshint = require('jshint').JSHINT,
|
|
||||||
Tailspin = require('tailspin');
|
|
||||||
|
|
||||||
var Editor = React.createClass({
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
onValueChange: React.PropTypes.func,
|
|
||||||
value: React.PropTypes.string
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
value: 'console.log(\'freeCodeCamp is awesome\')'
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
value: this.props.value
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
var options = {
|
|
||||||
autoCloseBrackets: true,
|
|
||||||
gutters: ['CodeMirror-lint-markers'],
|
|
||||||
lint: true,
|
|
||||||
linter: jshint,
|
|
||||||
lineNumbers: true,
|
|
||||||
lineWrapping: true,
|
|
||||||
mode: 'javascript',
|
|
||||||
matchBrackets: true,
|
|
||||||
runnable: true,
|
|
||||||
scrollbarStyle: 'null',
|
|
||||||
theme: 'monokai',
|
|
||||||
textAreaClassName: 'hide-textarea',
|
|
||||||
value: this.state.value,
|
|
||||||
onChange: e => {
|
|
||||||
this.setState({ value: e.target.value});
|
|
||||||
if (typeof this.props.onValueChange === 'function') {
|
|
||||||
this.props.onValueChange(e.target.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var config = {
|
|
||||||
setSize: ['100%', 'auto'],
|
|
||||||
extraKeys: {
|
|
||||||
Tab: function(cm) {
|
|
||||||
debug('tab pressed');
|
|
||||||
if (cm.somethingSelected()) {
|
|
||||||
cm.indentSelection('add');
|
|
||||||
} else {
|
|
||||||
var spaces = new Array(cm.getOption('indentUnit') + 1).join(' ');
|
|
||||||
cm.replaceSelection(spaces);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'Shift-Tab': function(cm) {
|
|
||||||
debug('shift-tab pressed');
|
|
||||||
if (cm.somethingSelected()) {
|
|
||||||
cm.indentSelection('subtract');
|
|
||||||
} else {
|
|
||||||
var spaces = new Array(cm.getOption('indentUnit') + 1).join(' ');
|
|
||||||
cm.replaceSelection(spaces);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'Ctrl-Enter': function() {
|
|
||||||
debug('C-enter pressed');
|
|
||||||
// execute bonfire action
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id='mainEditorPanel'>
|
|
||||||
<form className='code'>
|
|
||||||
<div className='form-group codeMirrorView'>
|
|
||||||
<Tailspin
|
|
||||||
{ ...options }
|
|
||||||
config={ config }/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = Editor;
|
|
@ -1 +0,0 @@
|
|||||||
module.exports = require('./Editor.jsx');
|
|
@ -44,6 +44,20 @@
|
|||||||
"tests": {
|
"tests": {
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
|
"head": {
|
||||||
|
"type": "array",
|
||||||
|
"default": [],
|
||||||
|
"description": "appended to user code"
|
||||||
|
},
|
||||||
|
"tail": {
|
||||||
|
"type": "array",
|
||||||
|
"default": [],
|
||||||
|
"description": "prepended to user code"
|
||||||
|
},
|
||||||
|
"fileName": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "filename challenge comes from. Used in dev mode"
|
||||||
|
},
|
||||||
"challengeSeed": {
|
"challengeSeed": {
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
@ -82,6 +96,13 @@
|
|||||||
},
|
},
|
||||||
"descriptionPt": {
|
"descriptionPt": {
|
||||||
"type": "array"
|
"type": "array"
|
||||||
|
},
|
||||||
|
"solutions": {
|
||||||
|
"type": "array",
|
||||||
|
"default": []
|
||||||
|
},
|
||||||
|
"releasedOn": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validations": [],
|
"validations": [],
|
||||||
|
@ -22,7 +22,8 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"required": true
|
||||||
},
|
},
|
||||||
"phone": {
|
"phone": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -36,24 +37,61 @@
|
|||||||
"country": {
|
"country": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"locale": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "format: city, state"
|
||||||
|
},
|
||||||
"location": {
|
"location": {
|
||||||
"type": "geopoint"
|
"type": "geopoint",
|
||||||
|
"description": "location in lat, long"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"isApproved": {
|
"isApproved": {
|
||||||
"type": "boolean"
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
},
|
},
|
||||||
"isHighlighted": {
|
"isHighlighted": {
|
||||||
"type": "boolean"
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
},
|
},
|
||||||
"isPaid": {
|
"isPaid": {
|
||||||
"type": "boolean"
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"isFilled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
},
|
},
|
||||||
"postedOn": {
|
"postedOn": {
|
||||||
"type": "date",
|
"type": "date",
|
||||||
"defaultFn": "now"
|
"defaultFn": "now"
|
||||||
|
},
|
||||||
|
"isFrontEndCert": {
|
||||||
|
"type": "boolean",
|
||||||
|
"defaut": false,
|
||||||
|
"description": "Camper must be front end certified to apply"
|
||||||
|
},
|
||||||
|
"isFullStackCert": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Camper must be full stack certified to apply"
|
||||||
|
},
|
||||||
|
"isRemoteOk": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Camper may work remotely"
|
||||||
|
},
|
||||||
|
"howToApply": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "How do campers apply to job"
|
||||||
|
},
|
||||||
|
"promoCodeUsed": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "the promocode, if any, that the job uses"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validations": [],
|
"validations": [],
|
||||||
|
55
common/models/pledge.json
Normal file
55
common/models/pledge.json
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"name": "pledge",
|
||||||
|
"base": "PersistedModel",
|
||||||
|
"idInjection": true,
|
||||||
|
"trackChanges": false,
|
||||||
|
"properties": {
|
||||||
|
"nonprofit": {
|
||||||
|
"type": "string",
|
||||||
|
"index": true
|
||||||
|
},
|
||||||
|
"amount": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"dateStarted": {
|
||||||
|
"type": "date",
|
||||||
|
"defaultFn": "now"
|
||||||
|
},
|
||||||
|
"dateEnded": {
|
||||||
|
"type": "date"
|
||||||
|
},
|
||||||
|
"formerUserId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isOrphaned": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isCompleted": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validations": [],
|
||||||
|
"relations": {
|
||||||
|
"user": {
|
||||||
|
"type": "hasMany",
|
||||||
|
"model": "user",
|
||||||
|
"foreignKey": "userId"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"acls": [
|
||||||
|
{
|
||||||
|
"accessType": "*",
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$everyone",
|
||||||
|
"permission": "DENY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessType": "READ",
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$everyone",
|
||||||
|
"permission": "ALLOW"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"methods": []
|
||||||
|
}
|
81
common/models/promo.js
Normal file
81
common/models/promo.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { isAlphanumeric, isHexadecimal } from 'validator';
|
||||||
|
import debug from 'debug';
|
||||||
|
|
||||||
|
const log = debug('freecc:models:promo');
|
||||||
|
|
||||||
|
export default function promo(Promo) {
|
||||||
|
Promo.getButton = function getButton(id, code, type = 'isNot') {
|
||||||
|
const Job = Promo.app.models.Job;
|
||||||
|
if (!id || !isHexadecimal(id)) {
|
||||||
|
return Promise.reject(new Error(
|
||||||
|
'Must include job id'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isAlphanumeric(code) &&
|
||||||
|
type &&
|
||||||
|
!isAlphanumeric(type)
|
||||||
|
) {
|
||||||
|
return Promise.reject(new Error(
|
||||||
|
'Code or Type should be an alphanumeric'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
where: {
|
||||||
|
and: [{
|
||||||
|
code: type === 'isNot' ? type : 'isHighlighted'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: type.replace(/^\$/g, '')
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Promo.findOne(query)
|
||||||
|
.then(function(promo) {
|
||||||
|
// turn promo model to plain js object;
|
||||||
|
promo = promo.toJSON();
|
||||||
|
return Job.updateAll({ id: id }, { promoCodeUsed: code })
|
||||||
|
.then(function({ count = 0 } = {}) {
|
||||||
|
log('job', count);
|
||||||
|
if (count) {
|
||||||
|
return Object.assign({}, promo, { name: `${code} Discount` });
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(
|
||||||
|
`Job ${id} not found`
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Promo.remoteMethod(
|
||||||
|
'getButton',
|
||||||
|
{
|
||||||
|
description: 'Get button id for promocode',
|
||||||
|
accepts: [
|
||||||
|
{
|
||||||
|
arg: 'id',
|
||||||
|
type: 'string',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'code',
|
||||||
|
type: 'string',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'type',
|
||||||
|
type: 'string'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
returns: [
|
||||||
|
{
|
||||||
|
arg: 'promo',
|
||||||
|
type: 'object'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
59
common/models/promo.json
Normal file
59
common/models/promo.json
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"name": "promo",
|
||||||
|
"base": "PersistedModel",
|
||||||
|
"strict": true,
|
||||||
|
"idInjection": true,
|
||||||
|
"trackChanges": false,
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "The code to unlock the promotional discount"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "The name of the discount"
|
||||||
|
},
|
||||||
|
"buttonId": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "The id of paypal button"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A selector of different types of buttons for the same discount"
|
||||||
|
},
|
||||||
|
"fullPrice": {
|
||||||
|
"type": "number",
|
||||||
|
"required": true,
|
||||||
|
"description": "The original amount"
|
||||||
|
},
|
||||||
|
"discountAmount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "The amount of the discount if applicable"
|
||||||
|
},
|
||||||
|
"discountPercent": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "The amount of discount as a percentage if applicable"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validations": [],
|
||||||
|
"relations": {},
|
||||||
|
"acls": [
|
||||||
|
{
|
||||||
|
"accessType": "*",
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$everyone",
|
||||||
|
"permission": "DENY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessType": "EXECUTE",
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$everyone",
|
||||||
|
"permission": "ALLOW",
|
||||||
|
"property": "getButton"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"methods": []
|
||||||
|
}
|
@ -322,7 +322,7 @@ module.exports = function(User) {
|
|||||||
})
|
})
|
||||||
// no results means this is the first brownie point given by giver
|
// no results means this is the first brownie point given by giver
|
||||||
// so return -1 to indicate receiver should receive point
|
// so return -1 to indicate receiver should receive point
|
||||||
.firstOrDefault(null, -1)
|
.first({ defaultValue: -1 })
|
||||||
.flatMap((browniePointsFromGiver) => {
|
.flatMap((browniePointsFromGiver) => {
|
||||||
if (browniePointsFromGiver === -1) {
|
if (browniePointsFromGiver === -1) {
|
||||||
|
|
||||||
|
@ -166,6 +166,11 @@
|
|||||||
"type": "hasMany",
|
"type": "hasMany",
|
||||||
"model": "userIdentity",
|
"model": "userIdentity",
|
||||||
"foreignKey": ""
|
"foreignKey": ""
|
||||||
|
},
|
||||||
|
"pledge": {
|
||||||
|
"type": "hasOne",
|
||||||
|
"model": "pledge",
|
||||||
|
"foreignKey": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"acls": [
|
"acls": [
|
||||||
|
@ -255,6 +255,7 @@ export function postJSON$(url, body) {
|
|||||||
url,
|
url,
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
responseType: 'json',
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -277,10 +278,7 @@ export function get$(url) {
|
|||||||
* @returns {Observable} The observable sequence which contains the parsed JSON
|
* @returns {Observable} The observable sequence which contains the parsed JSON
|
||||||
*/
|
*/
|
||||||
export function getJSON$(url) {
|
export function getJSON$(url) {
|
||||||
if (!root.JSON && typeof root.JSON.parse !== 'function') {
|
return ajax$({ url: url, responseType: 'json' }).map(function(x) {
|
||||||
throw new TypeError('JSON is not supported in your runtime.');
|
|
||||||
}
|
|
||||||
return ajax$({url: url, responseType: 'json'}).map(function(x) {
|
|
||||||
return x.response;
|
return x.response;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
77
common/utils/jsonp$.js
Normal file
77
common/utils/jsonp$.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { AnonymousObservable, Disposable } from 'rx';
|
||||||
|
|
||||||
|
const root = typeof window !== 'undefined' ? window : {};
|
||||||
|
const trash = 'document' in root && root.document.createElement('div');
|
||||||
|
|
||||||
|
function destroy(element) {
|
||||||
|
trash.appendChild(element);
|
||||||
|
trash.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function jsonp$(options) {
|
||||||
|
let id = 0;
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
options = { url: options };
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AnonymousObservable(function(o) {
|
||||||
|
const settings = Object.assign(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
jsonp: 'JSONPCallback',
|
||||||
|
async: true,
|
||||||
|
jsonpCallback: 'rxjsjsonpCallbackscallback_' + (id++).toString(36)
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
let script = root.document.createElement('script');
|
||||||
|
script.type = 'text/javascript';
|
||||||
|
script.async = settings.async;
|
||||||
|
script.src = settings.url.replace(settings.jsonp, settings.jsonpCallback);
|
||||||
|
|
||||||
|
root[settings.jsonpCallback] = function(data) {
|
||||||
|
root[settings.jsonpCallback].called = true;
|
||||||
|
root[settings.jsonpCallback].data = data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = function(e) {
|
||||||
|
if (e.type === 'load' && !root[settings.jsonpCallback].called) {
|
||||||
|
e = { type: 'error' };
|
||||||
|
}
|
||||||
|
const status = e.type === 'error' ? 400 : 200;
|
||||||
|
const data = root[settings.jsonpCallback].data;
|
||||||
|
|
||||||
|
if (status === 200) {
|
||||||
|
o.onNext({
|
||||||
|
status: status,
|
||||||
|
responseType: 'jsonp',
|
||||||
|
response: data,
|
||||||
|
originalEvent: e
|
||||||
|
});
|
||||||
|
|
||||||
|
o.onCompleted();
|
||||||
|
} else {
|
||||||
|
o.onError({
|
||||||
|
type: 'error',
|
||||||
|
status: status,
|
||||||
|
originalEvent: e
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
script.onload = script.onreadystatechanged = script.onerror = handler;
|
||||||
|
|
||||||
|
const head = root.document.getElementsByTagName('head')[0] ||
|
||||||
|
root.document.documentElement;
|
||||||
|
|
||||||
|
head.insertBefore(script, head.firstChild);
|
||||||
|
|
||||||
|
return Disposable.create(() => {
|
||||||
|
script.onload = script.onreadystatechanged = script.onerror = null;
|
||||||
|
|
||||||
|
destroy(script);
|
||||||
|
script = null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
168
gulpfile.js
168
gulpfile.js
@ -13,9 +13,15 @@ var Rx = require('rx'),
|
|||||||
reduce = require('gulp-reduce-file'),
|
reduce = require('gulp-reduce-file'),
|
||||||
sortKeys = require('sort-keys'),
|
sortKeys = require('sort-keys'),
|
||||||
debug = require('debug')('freecc:gulp'),
|
debug = require('debug')('freecc:gulp'),
|
||||||
|
yargs = require('yargs'),
|
||||||
|
concat = require('gulp-concat'),
|
||||||
|
uglify = require('gulp-uglify'),
|
||||||
|
merge = require('merge-stream'),
|
||||||
|
babel = require('gulp-babel'),
|
||||||
|
sourcemaps = require('gulp-sourcemaps'),
|
||||||
|
|
||||||
// react app
|
// react app
|
||||||
webpack = require('gulp-webpack'),
|
webpack = require('webpack-stream'),
|
||||||
webpackConfig = require('./webpack.config.js'),
|
webpackConfig = require('./webpack.config.js'),
|
||||||
webpackConfigNode = require('./webpack.config.node.js'),
|
webpackConfigNode = require('./webpack.config.node.js'),
|
||||||
|
|
||||||
@ -38,7 +44,7 @@ var Rx = require('rx'),
|
|||||||
|
|
||||||
Rx.config.longStackSupport = true;
|
Rx.config.longStackSupport = true;
|
||||||
|
|
||||||
var __DEV__ = process.env.NODE_ENV !== 'production';
|
var __DEV__ = !yargs.argv.p;
|
||||||
var reloadDelay = 1000;
|
var reloadDelay = 1000;
|
||||||
var reload = sync.reload;
|
var reload = sync.reload;
|
||||||
var paths = {
|
var paths = {
|
||||||
@ -68,17 +74,64 @@ var paths = {
|
|||||||
dest: 'public/js'
|
dest: 'public/js'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
vendorChallenges: [
|
||||||
|
'public/bower_components/jshint/dist/jshint.js',
|
||||||
|
'public/bower_components/chai/chai.js',
|
||||||
|
'public/bower_components/CodeMirror/lib/codemirror.js',
|
||||||
|
'public/bower_components/CodeMirror/addon/edit/closebrackets.js',
|
||||||
|
'public/bower_components/CodeMirror/addon/edit/matchbrackets.js',
|
||||||
|
'public/bower_components/CodeMirror/addon/lint/lint.js',
|
||||||
|
'public/bower_components/CodeMirror/addon/lint/javascript-lint.js',
|
||||||
|
'public/bower_components/CodeMirror/mode/javascript/javascript.js',
|
||||||
|
'public/bower_components/CodeMirror/mode/xml/xml.js',
|
||||||
|
'public/bower_components/CodeMirror/mode/css/css.js',
|
||||||
|
'public/bower_components/CodeMirror/mode/htmlmixed/htmlmixed.js',
|
||||||
|
'node_modules/emmet-codemirror/dist/emmet.js',
|
||||||
|
'public/js/lib/loop-protect/loop-protect.js'
|
||||||
|
],
|
||||||
|
|
||||||
|
vendorMain: [
|
||||||
|
'public/bower_components/jquery/dist/jquery.min.js',
|
||||||
|
'public/bower_components/bootstrap/dist/js/bootstrap.min.js',
|
||||||
|
'public/bower_components/angular/angular.min.js',
|
||||||
|
'public/bower_components/angular-bootstrap/ui-bootstrap.min.js',
|
||||||
|
'public/bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js',
|
||||||
|
'public/bower_components/d3/d3.min.js',
|
||||||
|
'public/bower_components/moment/min/moment.min.js',
|
||||||
|
'public/bower_components/lightbox2/dist/js/lightbox.min.js',
|
||||||
|
'public/bower_components/rxjs/dist/rx.all.min.js'
|
||||||
|
],
|
||||||
|
|
||||||
js: [
|
js: [
|
||||||
'client/main.js',
|
'client/main.js',
|
||||||
'client/iFrameScripts.js',
|
'client/iFrameScripts.js',
|
||||||
'client/plugin.js'
|
'client/plugin.js'
|
||||||
],
|
],
|
||||||
|
|
||||||
dependents: [
|
commonFramework: [
|
||||||
'client/commonFramework.js'
|
'init',
|
||||||
|
'bindings',
|
||||||
|
'add-test-to-string',
|
||||||
|
'code-storage',
|
||||||
|
'code-uri',
|
||||||
|
'add-loop-protect',
|
||||||
|
'get-iframe',
|
||||||
|
'update-preview',
|
||||||
|
'create-editor',
|
||||||
|
'detect-unsafe-code-stream',
|
||||||
|
'display-test-results',
|
||||||
|
'execute-challenge-stream',
|
||||||
|
'output-display',
|
||||||
|
'phone-scroll-lock',
|
||||||
|
'report-issue',
|
||||||
|
'run-tests-stream',
|
||||||
|
'show-completion',
|
||||||
|
'step-challenge',
|
||||||
|
'end'
|
||||||
],
|
],
|
||||||
|
|
||||||
less: './client/less/main.less',
|
less: './client/less/main.less',
|
||||||
|
lessFiles: './client/less/*.less',
|
||||||
|
|
||||||
manifest: 'server/manifests/',
|
manifest: 'server/manifests/',
|
||||||
|
|
||||||
@ -92,8 +145,7 @@ var paths = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
challenges: [
|
challenges: [
|
||||||
'seed/challenges/*.json',
|
'seed/challenges/*.json'
|
||||||
'seed/under-construction/*.json'
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -101,6 +153,12 @@ var webpackOptions = {
|
|||||||
devtool: 'inline-source-map'
|
devtool: 'inline-source-map'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function formatCommonFrameworkPaths() {
|
||||||
|
return this.map(function(script) {
|
||||||
|
return 'client/commonFramework/' + script + '.js';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function errorHandler() {
|
function errorHandler() {
|
||||||
var args = Array.prototype.slice.call(arguments);
|
var args = Array.prototype.slice.call(arguments);
|
||||||
|
|
||||||
@ -177,7 +235,14 @@ gulp.task('sync', syncDepenedents, function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('lint-js', function() {
|
gulp.task('lint-js', function() {
|
||||||
return gulp.src(['public/js/lib/**/*'])
|
return gulp.src([
|
||||||
|
'common/**/*.js',
|
||||||
|
'common/**/*.jsx',
|
||||||
|
'client/**/*.js',
|
||||||
|
'client/**/*.jsx',
|
||||||
|
'server/**/*.js',
|
||||||
|
'config/**/*.js'
|
||||||
|
])
|
||||||
.pipe(eslint())
|
.pipe(eslint())
|
||||||
.pipe(eslint.format());
|
.pipe(eslint.format());
|
||||||
});
|
});
|
||||||
@ -191,6 +256,8 @@ gulp.task('lint-json', function() {
|
|||||||
gulp.task('test-challenges', ['lint-json']);
|
gulp.task('test-challenges', ['lint-json']);
|
||||||
|
|
||||||
gulp.task('pack-client', function() {
|
gulp.task('pack-client', function() {
|
||||||
|
if (!__DEV__) { console.log('\n\nbundling production\n\n'); }
|
||||||
|
|
||||||
var manifestName = 'react-manifest.json';
|
var manifestName = 'react-manifest.json';
|
||||||
var dest = webpackConfig.output.path;
|
var dest = webpackConfig.output.path;
|
||||||
|
|
||||||
@ -201,6 +268,7 @@ gulp.task('pack-client', function() {
|
|||||||
webpackConfig,
|
webpackConfig,
|
||||||
webpackOptions
|
webpackOptions
|
||||||
)))
|
)))
|
||||||
|
.pipe(__DEV__ ? gutil.noop() : uglify())
|
||||||
.pipe(gulp.dest(dest))
|
.pipe(gulp.dest(dest))
|
||||||
.pipe(rev())
|
.pipe(rev())
|
||||||
// copy files to public
|
// copy files to public
|
||||||
@ -231,8 +299,12 @@ var defaultStatsOptions = {
|
|||||||
errorDetails: false
|
errorDetails: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var webpackCalled = false;
|
||||||
gulp.task('pack-watch', function(cb) {
|
gulp.task('pack-watch', function(cb) {
|
||||||
var called = false;
|
if (webpackCalled) {
|
||||||
|
console.log('webpack watching already runnning');
|
||||||
|
return cb();
|
||||||
|
}
|
||||||
gulp.src(webpackConfig.entry)
|
gulp.src(webpackConfig.entry)
|
||||||
.pipe(plumber({ errorHandler: errorHandler }))
|
.pipe(plumber({ errorHandler: errorHandler }))
|
||||||
.pipe(webpack(Object.assign(
|
.pipe(webpack(Object.assign(
|
||||||
@ -245,9 +317,9 @@ gulp.task('pack-watch', function(cb) {
|
|||||||
gutil.log(stats.toString(defaultStatsOptions));
|
gutil.log(stats.toString(defaultStatsOptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!called) {
|
if (!webpackCalled) {
|
||||||
debug('webpack watch completed');
|
debug('webpack init completed');
|
||||||
called = true;
|
webpackCalled = true;
|
||||||
cb();
|
cb();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,7 +327,7 @@ gulp.task('pack-watch', function(cb) {
|
|||||||
.pipe(gulp.dest(webpackConfig.output.path));
|
.pipe(gulp.dest(webpackConfig.output.path));
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('pack-watch-manifest', function() {
|
gulp.task('pack-watch-manifest', ['pack-watch'], function() {
|
||||||
var manifestName = 'react-manifest.json';
|
var manifestName = 'react-manifest.json';
|
||||||
var dest = webpackConfig.output.path;
|
var dest = webpackConfig.output.path;
|
||||||
return gulp.src(dest + '/bundle.js')
|
return gulp.src(dest + '/bundle.js')
|
||||||
@ -285,7 +357,7 @@ gulp.task('less', function() {
|
|||||||
var dest = paths.css;
|
var dest = paths.css;
|
||||||
return gulp.src(paths.less)
|
return gulp.src(paths.less)
|
||||||
.pipe(plumber({ errorHandler: errorHandler }))
|
.pipe(plumber({ errorHandler: errorHandler }))
|
||||||
// copile
|
// compile
|
||||||
.pipe(less({
|
.pipe(less({
|
||||||
paths: [ path.join(__dirname, 'less', 'includes') ]
|
paths: [ path.join(__dirname, 'less', 'includes') ]
|
||||||
}))
|
}))
|
||||||
@ -303,12 +375,50 @@ gulp.task('less', function() {
|
|||||||
.pipe(gulp.dest(paths.manifest));
|
.pipe(gulp.dest(paths.manifest));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getFilesGlob(files) {
|
||||||
|
if (!__DEV__) {
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
return files.map(function(file) {
|
||||||
|
return file
|
||||||
|
.replace('.min.', '.')
|
||||||
|
// moment breaks the pattern
|
||||||
|
.replace('/min/', '/');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
gulp.task('js', function() {
|
gulp.task('js', function() {
|
||||||
var manifestName = 'js-manifest.json';
|
var manifestName = 'js-manifest.json';
|
||||||
var dest = paths.publicJs;
|
var dest = paths.publicJs;
|
||||||
|
|
||||||
return gulp.src(paths.js)
|
var jsFiles = merge(
|
||||||
.pipe(plumber({ errorHandler: errorHandler }))
|
|
||||||
|
gulp.src(getFilesGlob(paths.vendorMain))
|
||||||
|
.pipe(__DEV__ ? sourcemaps.init() : gutil.noop())
|
||||||
|
.pipe(concat('vendor-main.js'))
|
||||||
|
.pipe(
|
||||||
|
__DEV__ ?
|
||||||
|
sourcemaps.write({ sourceRoot: '/vendor' }) :
|
||||||
|
gutil.noop()
|
||||||
|
),
|
||||||
|
|
||||||
|
gulp.src(paths.vendorChallenges)
|
||||||
|
.pipe(__DEV__ ? sourcemaps.init() : gutil.noop())
|
||||||
|
.pipe(__DEV__ ? gutil.noop() : uglify())
|
||||||
|
.pipe(concat('vendor-challenges.js'))
|
||||||
|
.pipe(
|
||||||
|
__DEV__ ?
|
||||||
|
sourcemaps.write({ sourceRoot: '/vendor' }) :
|
||||||
|
gutil.noop()
|
||||||
|
),
|
||||||
|
|
||||||
|
gulp.src(paths.js)
|
||||||
|
.pipe(plumber({ errorHandler: errorHandler }))
|
||||||
|
.pipe(babel())
|
||||||
|
.pipe(__DEV__ ? gutil.noop() : uglify())
|
||||||
|
);
|
||||||
|
|
||||||
|
return jsFiles
|
||||||
.pipe(gulp.dest(dest))
|
.pipe(gulp.dest(dest))
|
||||||
// create registry file
|
// create registry file
|
||||||
.pipe(rev())
|
.pipe(rev())
|
||||||
@ -325,7 +435,7 @@ gulp.task('js', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// commonFramework depend on iFrameScripts
|
// commonFramework depend on iFrameScripts
|
||||||
// sandbox depends on plugin
|
// and faux.js
|
||||||
gulp.task('dependents', ['js'], function() {
|
gulp.task('dependents', ['js'], function() {
|
||||||
var manifestName = 'dependents-manifest.json';
|
var manifestName = 'dependents-manifest.json';
|
||||||
var dest = paths.publicJs;
|
var dest = paths.publicJs;
|
||||||
@ -334,8 +444,17 @@ gulp.task('dependents', ['js'], function() {
|
|||||||
path.join(__dirname, paths.manifest, 'js-manifest.json')
|
path.join(__dirname, paths.manifest, 'js-manifest.json')
|
||||||
);
|
);
|
||||||
|
|
||||||
return gulp.src(paths.dependents)
|
return gulp.src(formatCommonFrameworkPaths.call(paths.commonFramework))
|
||||||
.pipe(plumber({ errorHandler: errorHandler }))
|
.pipe(plumber({ errorHandler: errorHandler }))
|
||||||
|
.pipe(babel())
|
||||||
|
.pipe(__DEV__ ? sourcemaps.init() : gutil.noop())
|
||||||
|
.pipe(concat('commonFramework.js'))
|
||||||
|
.pipe(
|
||||||
|
__DEV__ ?
|
||||||
|
sourcemaps.write({ sourceRoot: '/commonFramework' }) :
|
||||||
|
gutil.noop()
|
||||||
|
)
|
||||||
|
.pipe(__DEV__ ? gutil.noop() : uglify())
|
||||||
.pipe(revReplace({ manifest: manifest }))
|
.pipe(revReplace({ manifest: manifest }))
|
||||||
.pipe(gulp.dest(dest))
|
.pipe(gulp.dest(dest))
|
||||||
.pipe(rev())
|
.pipe(rev())
|
||||||
@ -391,12 +510,20 @@ var watchDependents = [
|
|||||||
'pack-watch-manifest'
|
'pack-watch-manifest'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
gulp.task('reload', function() {
|
||||||
|
notify({ message: 'test changed' });
|
||||||
|
reload();
|
||||||
|
});
|
||||||
|
|
||||||
gulp.task('watch', watchDependents, function() {
|
gulp.task('watch', watchDependents, function() {
|
||||||
gulp.watch(paths.less, ['less']);
|
gulp.watch(paths.lessFiles, ['less']);
|
||||||
gulp.watch(paths.js, ['js']);
|
gulp.watch(paths.js, ['js']);
|
||||||
gulp.watch(paths.challenges, ['test-challenges']);
|
gulp.watch(paths.challenges, ['test-challenges', 'reload']);
|
||||||
gulp.watch(paths.js, ['js', 'dependents']);
|
gulp.watch(paths.js, ['js', 'dependents']);
|
||||||
gulp.watch(paths.dependents, ['dependents']);
|
gulp.watch(
|
||||||
|
formatCommonFrameworkPaths.call(paths.commonFramework),
|
||||||
|
['dependents']
|
||||||
|
);
|
||||||
gulp.watch(paths.manifest + '/*.json', ['build-manifest-watch']);
|
gulp.watch(paths.manifest + '/*.json', ['build-manifest-watch']);
|
||||||
gulp.watch(webpackConfig.output.path + '/bundle.js', ['pack-watch-manifest']);
|
gulp.watch(webpackConfig.output.path + '/bundle.js', ['pack-watch-manifest']);
|
||||||
});
|
});
|
||||||
@ -409,4 +536,3 @@ gulp.task('default', [
|
|||||||
'watch',
|
'watch',
|
||||||
'sync'
|
'sync'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Free Code Camp</title>
|
|
||||||
<META http-equiv="refresh" content="1;URL=http://freecodecamp.github.io/freecodecamp/coverage/lcov-report/index.html">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
190
package.json
190
package.json
@ -6,127 +6,141 @@
|
|||||||
"url": "https://github.com/freecodecamp/freecodecamp.git"
|
"url": "https://github.com/freecodecamp/freecodecamp.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "gulp build",
|
"first-time": "npm run create-rev && echo '\n\nseeding database\n\n' && node seed && node seed/nonprofits",
|
||||||
|
"create-rev": "test ! -e server/rev-manifest.json && echo '\n\ncreating manifest\n\n' && touch server/rev-manifest.json && echo '{}' >> server/rev-manifest.json",
|
||||||
|
"build": "NODE_ENV=production gulp build -p",
|
||||||
"start": "babel-node server/server.js",
|
"start": "babel-node server/server.js",
|
||||||
"prestart-production": "bower cache clean && bower install && gulp build",
|
"prestart-production": "bower cache clean && bower install && gulp build -p",
|
||||||
"start-production": "node pm2Start",
|
"start-production": "node pm2Start",
|
||||||
"lint": "eslint --ext=.js,.jsx .",
|
"lint": "npm run lint-js && npm run lint-json",
|
||||||
"test": "gulp test-challenges"
|
"lint-challenges": "jsonlint -q seed/challenges/*.json",
|
||||||
|
"lint-nonprofits": "jsonlint -q seed/nonprofits.json",
|
||||||
|
"lint-server": "jsonlint -q server/*.json",
|
||||||
|
"lint-resources": "jsonlint -q server/resources/*.json",
|
||||||
|
"lint-utils": "jsonlint -q server/utils/*.json",
|
||||||
|
"lint-js": "eslint --ext=.js,.jsx server/ common/ config/ client/",
|
||||||
|
"lint-json": "npm run lint-server && npm run lint-nonprofits && npm run lint-challenges && npm run lint-resources && npm run lint-utils",
|
||||||
|
"test-challenges": "babel-node seed/test-challenges.js | tnyan",
|
||||||
|
"pretest": "npm run lint",
|
||||||
|
"test": "npm run test-challenges"
|
||||||
},
|
},
|
||||||
"license": "(BSD-3-Clause AND CC-BY-SA-4.0)",
|
"license": "(BSD-3-Clause AND CC-BY-SA-4.0)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.2.5",
|
"accepts": "^1.3.0",
|
||||||
"async": "~0.9.0",
|
"async": "^1.5.0",
|
||||||
"babel": "5.8.23",
|
"babel": "5.8.34",
|
||||||
"babel-core": "5.8.23",
|
"babel-core": "5.8.34",
|
||||||
"babel-eslint": "4.1.1",
|
"babel-eslint": "^4.1.4",
|
||||||
"babel-loader": "5.2.2",
|
"babel-loader": "5.4.0",
|
||||||
"bcrypt-nodejs": "~0.0.3",
|
|
||||||
"body-parser": "^1.13.2",
|
"body-parser": "^1.13.2",
|
||||||
"chai-jquery": "~2.0.0",
|
"chai-jquery": "^2.0.0",
|
||||||
"cheerio": "~0.18.0",
|
"cheerio": "~0.19.0",
|
||||||
"classnames": "^2.1.2",
|
"classnames": "^2.1.2",
|
||||||
"clockwork": "~0.1.1",
|
"compression": "^1.6.0",
|
||||||
"compression": "~1.2.1",
|
"connect-mongo": "~0.8.2",
|
||||||
"connect-mongo": "~0.7.0",
|
"cookie-parser": "^1.4.0",
|
||||||
"cookie-parser": "~1.3.3",
|
"csso": "^1.4.1",
|
||||||
"csso": "~1.3.11",
|
"dateformat": "^1.0.11",
|
||||||
"dateformat": "~1.0.11",
|
"debug": "^2.2.0",
|
||||||
"debug": "~2.1.0",
|
"dedent": "~0.6.0",
|
||||||
"dedent": "^0.4.0",
|
"dotenv": "^1.2.0",
|
||||||
"dotenv": "~0.4.0",
|
"emmet-codemirror": "^1.2.5",
|
||||||
"errorhandler": "~1.3.0",
|
"errorhandler": "^1.4.2",
|
||||||
"es6-map": "^0.1.1",
|
"es6-map": "~0.1.1",
|
||||||
"eslint": "^1.1.0",
|
"eslint": "~1.10.2",
|
||||||
"eslint-plugin-react": "^3.2.1",
|
"eslint-plugin-react": "^3.7.1",
|
||||||
"express": "~4.10.4",
|
"express": "^4.13.3",
|
||||||
"express-flash": "~0.0.2",
|
"express-flash": "~0.0.2",
|
||||||
"express-session": "~1.9.2",
|
"express-session": "^1.12.1",
|
||||||
"express-state": "^1.2.0",
|
"express-state": "^1.2.0",
|
||||||
"express-validator": "~2.8.0",
|
"express-validator": "^2.18.0",
|
||||||
"fetchr": "^0.5.12",
|
"fetchr": "~0.5.12",
|
||||||
"font-awesome": "~4.3.0",
|
"forever": "~0.15.1",
|
||||||
"forever": "~0.14.1",
|
"frameguard": "~0.2.2",
|
||||||
"frameguard": "^0.2.2",
|
"gulp": "^3.9.0",
|
||||||
"github-api": "~0.7.0",
|
"gulp-babel": "^5.3.0",
|
||||||
|
"gulp-concat": "^2.6.0",
|
||||||
|
"gulp-eslint": "^1.1.0",
|
||||||
|
"gulp-inject": "^3.0.0",
|
||||||
|
"gulp-jsonlint": "^1.1.0",
|
||||||
"gulp-less": "^3.0.3",
|
"gulp-less": "^3.0.3",
|
||||||
"gulp-minify-css": "~0.5.1",
|
"gulp-minify-css": "^1.2.1",
|
||||||
|
"gulp-nodemon": "^2.0.3",
|
||||||
|
"gulp-notify": "^2.2.0",
|
||||||
|
"gulp-order": "^1.1.1",
|
||||||
|
"gulp-plumber": "^1.0.1",
|
||||||
"gulp-reduce-file": "0.0.1",
|
"gulp-reduce-file": "0.0.1",
|
||||||
"gulp-rev": "^6.0.1",
|
"gulp-rev": "^6.0.1",
|
||||||
"gulp-rev-replace": "^0.4.2",
|
"gulp-rev-replace": "~0.4.2",
|
||||||
|
"gulp-uglify": "^1.5.1",
|
||||||
"gulp-util": "^3.0.6",
|
"gulp-util": "^3.0.6",
|
||||||
"gulp-webpack": "^1.5.0",
|
"gulp-webpack": "^1.5.0",
|
||||||
"helmet": "~0.9.0",
|
"helmet": "~0.15.0",
|
||||||
"helmet-csp": "^0.2.3",
|
"helmet-csp": "~0.3.0",
|
||||||
"history": "^1.9.0",
|
"history": "^1.9.0",
|
||||||
"jade": "~1.8.0",
|
"jade": "^1.11.0",
|
||||||
"json-loader": "^0.5.2",
|
"json-loader": "~0.5.2",
|
||||||
"less": "~2.5.1",
|
"less": "^2.5.1",
|
||||||
"lodash": "^3.9.3",
|
"lodash": "^3.9.3",
|
||||||
"loopback": "https://github.com/FreeCodeCamp/loopback.git#fix/no-password",
|
"loopback": "^2.22.0",
|
||||||
"loopback-boot": "2.8.2",
|
"loopback-boot": "^2.13.0",
|
||||||
"loopback-component-passport": "https://github.com/FreeCodeCamp/loopback-component-passport.git#feature/flashfailure",
|
"loopback-component-passport": "^1.6.0",
|
||||||
"loopback-connector-mongodb": "^1.10.0",
|
"loopback-connector-mongodb": "^1.10.0",
|
||||||
"lusca": "~1.0.2",
|
"merge-stream": "^1.0.0",
|
||||||
"method-override": "~2.3.0",
|
"method-override": "^2.3.0",
|
||||||
"moment": "~2.10.2",
|
"moment": "^2.10.2",
|
||||||
"mongodb": "^2.0.33",
|
"mongodb": "^2.0.33",
|
||||||
"morgan": "~1.5.0",
|
"morgan": "^1.6.1",
|
||||||
"node-libs-browser": "^0.5.2",
|
|
||||||
"node-slack": "0.0.7",
|
|
||||||
"node-uuid": "^1.4.3",
|
"node-uuid": "^1.4.3",
|
||||||
"nodemailer": "~1.3.0",
|
"nodemailer": "^1.9.0",
|
||||||
"object.assign": "^3.0.0",
|
"normalize-url": "^1.3.1",
|
||||||
|
"object.assign": "^4.0.3",
|
||||||
"passport-facebook": "^2.0.0",
|
"passport-facebook": "^2.0.0",
|
||||||
"passport-github": "^0.1.5",
|
"passport-github": "^1.0.0",
|
||||||
"passport-google-oauth2": "^0.1.6",
|
"passport-google-oauth2": "~0.1.6",
|
||||||
"passport-linkedin-oauth2": "^1.2.1",
|
"passport-linkedin-oauth2": "^1.2.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"passport-oauth": "^1.0.0",
|
"passport-oauth": "^1.0.0",
|
||||||
"passport-twitter": "^1.0.3",
|
"passport-twitter": "^1.0.3",
|
||||||
"pmx": "^0.3.16",
|
"pmx": "~0.5.5",
|
||||||
"ramda": "~0.10.0",
|
"ramda": "~0.18.0",
|
||||||
"react": "^0.13.3",
|
"react": "~0.14.3",
|
||||||
"react-bootstrap": "~0.23.7",
|
"react-bootstrap": "~0.28.1",
|
||||||
"react-motion": "~0.1.0",
|
"react-dom": "~0.14.3",
|
||||||
"react-router": "^1.0.0-rc1",
|
"react-motion": "~0.3.1",
|
||||||
"react-vimeo": "^0.0.3",
|
"react-router": "^1.0.0",
|
||||||
"request": "~2.53.0",
|
"react-router-bootstrap": "https://github.com/FreeCodeCamp/react-router-bootstrap.git#freecodecamp",
|
||||||
|
"react-vimeo": "~0.0.3",
|
||||||
|
"request": "^2.65.0",
|
||||||
"rev-del": "^1.0.5",
|
"rev-del": "^1.0.5",
|
||||||
"rx": "^2.5.3",
|
"rx": "^4.0.0",
|
||||||
"sanitize-html": "~1.6.1",
|
"sanitize-html": "^1.11.1",
|
||||||
"sort-keys": "^1.1.1",
|
"sort-keys": "^1.1.1",
|
||||||
"source-map-support": "^0.3.2",
|
|
||||||
"store": "https://github.com/berkeleytrue/store.js.git#feature/noop-server",
|
"store": "https://github.com/berkeleytrue/store.js.git#feature/noop-server",
|
||||||
"thundercats": "^2.1.0",
|
"thundercats": "^3.0.0",
|
||||||
"thundercats-react": "^0.1.0",
|
"thundercats-react": "~0.4.0",
|
||||||
"twit": "~1.1.20",
|
"twit": "^2.1.1",
|
||||||
"uglify-js": "~2.4.15",
|
"uglify-js": "^2.5.0",
|
||||||
"validator": "^3.22.1",
|
"url-regex": "^3.0.0",
|
||||||
|
"validator": "^4.2.1",
|
||||||
"webpack": "^1.9.12",
|
"webpack": "^1.9.12",
|
||||||
|
"webpack-stream": "^2.1.1",
|
||||||
"xss-filters": "^1.2.6",
|
"xss-filters": "^1.2.6",
|
||||||
"yui": "~3.18.1"
|
"yargs": "^3.30.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"blessed": "~0.0.37",
|
"blessed": "~0.1.81",
|
||||||
"bower-main-files": "~0.0.4",
|
"bower-main-files": "~0.0.4",
|
||||||
"browser-sync": "~1.8.1",
|
"browser-sync": "^2.9.12",
|
||||||
"browserify": "^10.2.4",
|
"chai": "^3.4.0",
|
||||||
"chai": "~1.10.0",
|
|
||||||
"envify": "^3.4.0",
|
"envify": "^3.4.0",
|
||||||
"gulp": "~3.8.8",
|
"gulp-sourcemaps": "^1.6.0",
|
||||||
"gulp-eslint": "~0.9.0",
|
"istanbul": "~0.4.0",
|
||||||
"gulp-inject": "~1.0.2",
|
"jsonlint": "^1.6.2",
|
||||||
"gulp-jsonlint": "^1.1.0",
|
"loopback-component-explorer": "^2.1.1",
|
||||||
"gulp-nodemon": "^2.0.3",
|
|
||||||
"gulp-notify": "^2.2.0",
|
|
||||||
"gulp-plumber": "^1.0.1",
|
|
||||||
"istanbul": "^0.3.15",
|
|
||||||
"loopback-explorer": "^1.7.2",
|
|
||||||
"loopback-testing": "^1.1.0",
|
"loopback-testing": "^1.1.0",
|
||||||
"mocha": "~2.0.1",
|
"mocha": "^2.3.3",
|
||||||
"multiline": "~1.0.1",
|
"tap-nyan": "0.0.2",
|
||||||
"supertest": "~0.15.0",
|
"tape": "^4.2.2"
|
||||||
"vinyl-source-stream": "^1.1.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,3 +7,8 @@
|
|||||||
font-family: "Lato Light";
|
font-family: "Lato Light";
|
||||||
src: url(/fonts/Lato-Light.ttf) format("truetype");
|
src: url(/fonts/Lato-Light.ttf) format("truetype");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Lato Bold";
|
||||||
|
src: url(/fonts/Lato-Bold.ttf) format("truetype");
|
||||||
|
}
|
||||||
|
BIN
public/fonts/Lato-Bold.ttf
Executable file
BIN
public/fonts/Lato-Bold.ttf
Executable file
Binary file not shown.
6
public/js/lib/bootstrap.min.js
vendored
6
public/js/lib/bootstrap.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,231 +0,0 @@
|
|||||||
(function (chaiJquery) {
|
|
||||||
// Module systems magic dance.
|
|
||||||
if (typeof require === "function" && typeof exports === "object" && typeof module === "object") {
|
|
||||||
// NodeJS
|
|
||||||
module.exports = chaiJquery;
|
|
||||||
} else if (typeof define === "function" && define.amd) {
|
|
||||||
// AMD
|
|
||||||
define(['jquery'], function ($) {
|
|
||||||
return function (chai, utils) {
|
|
||||||
return chaiJquery(chai, utils, $);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Other environment (usually <script> tag): plug in to global chai instance directly.
|
|
||||||
chai.use(function (chai, utils) {
|
|
||||||
return chaiJquery(chai, utils, jQuery);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}(function (chai, utils, $) {
|
|
||||||
var inspect = utils.inspect,
|
|
||||||
flag = utils.flag;
|
|
||||||
$ = $ || jQuery;
|
|
||||||
|
|
||||||
var setPrototypeOf = '__proto__' in Object ?
|
|
||||||
function (object, prototype) {
|
|
||||||
object.__proto__ = prototype;
|
|
||||||
} :
|
|
||||||
function (object, prototype) {
|
|
||||||
var excludeNames = /^(?:length|name|arguments|caller)$/;
|
|
||||||
|
|
||||||
function copyProperties(dst, src) {
|
|
||||||
Object.getOwnPropertyNames(src).forEach(function (name) {
|
|
||||||
if (!excludeNames.test(name)) {
|
|
||||||
Object.defineProperty(dst, name,
|
|
||||||
Object.getOwnPropertyDescriptor(src, name));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
copyProperties(object, prototype);
|
|
||||||
copyProperties(object, Object.getPrototypeOf(prototype));
|
|
||||||
};
|
|
||||||
|
|
||||||
$.fn.inspect = function (depth) {
|
|
||||||
var el = $('<div />').append(this.clone());
|
|
||||||
if (depth !== undefined) {
|
|
||||||
var children = el.children();
|
|
||||||
while (depth-- > 0)
|
|
||||||
children = children.children();
|
|
||||||
children.html('...');
|
|
||||||
}
|
|
||||||
return el.html();
|
|
||||||
};
|
|
||||||
|
|
||||||
var props = {attr: 'attribute', css: 'CSS property', prop: 'property'};
|
|
||||||
for (var prop in props) {
|
|
||||||
(function (prop, description) {
|
|
||||||
chai.Assertion.addMethod(prop, function (name, val) {
|
|
||||||
var actual = flag(this, 'object')[prop](name);
|
|
||||||
|
|
||||||
if (!flag(this, 'negate') || undefined === val) {
|
|
||||||
this.assert(
|
|
||||||
undefined !== actual
|
|
||||||
, 'expected #{this} to have a #{exp} ' + description
|
|
||||||
, 'expected #{this} not to have a #{exp} ' + description
|
|
||||||
, name
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (undefined !== val) {
|
|
||||||
this.assert(
|
|
||||||
val === actual
|
|
||||||
, 'expected #{this} to have a ' + inspect(name) + ' ' + description + ' with the value #{exp}, but the value was #{act}'
|
|
||||||
, 'expected #{this} not to have a ' + inspect(name) + ' ' + description + ' with the value #{act}'
|
|
||||||
, val
|
|
||||||
, actual
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
flag(this, 'object', actual);
|
|
||||||
});
|
|
||||||
})(prop, props[prop]);
|
|
||||||
}
|
|
||||||
|
|
||||||
chai.Assertion.addMethod('data', function (name, val) {
|
|
||||||
// Work around a chai bug (https://github.com/logicalparadox/chai/issues/16)
|
|
||||||
if (flag(this, 'negate') && undefined !== val && undefined === flag(this, 'object').data(name)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var assertion = new chai.Assertion(flag(this, 'object').data());
|
|
||||||
if (flag(this, 'negate'))
|
|
||||||
assertion = assertion.not;
|
|
||||||
return assertion.property(name, val);
|
|
||||||
});
|
|
||||||
|
|
||||||
chai.Assertion.addMethod('class', function (className) {
|
|
||||||
this.assert(
|
|
||||||
flag(this, 'object').hasClass(className)
|
|
||||||
, 'expected #{this} to have class #{exp}'
|
|
||||||
, 'expected #{this} not to have class #{exp}'
|
|
||||||
, className
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
chai.Assertion.addMethod('id', function (id) {
|
|
||||||
this.assert(
|
|
||||||
flag(this, 'object').attr('id') === id
|
|
||||||
, 'expected #{this} to have id #{exp}'
|
|
||||||
, 'expected #{this} not to have id #{exp}'
|
|
||||||
, id
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
chai.Assertion.addMethod('html', function (html) {
|
|
||||||
var actual = flag(this, 'object').html();
|
|
||||||
this.assert(
|
|
||||||
actual === html
|
|
||||||
, 'expected #{this} to have HTML #{exp}, but the HTML was #{act}'
|
|
||||||
, 'expected #{this} not to have HTML #{exp}'
|
|
||||||
, html
|
|
||||||
, actual
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
chai.Assertion.addMethod('text', function (text) {
|
|
||||||
var actual = flag(this, 'object').text();
|
|
||||||
this.assert(
|
|
||||||
actual === text
|
|
||||||
, 'expected #{this} to have text #{exp}, but the text was #{act}'
|
|
||||||
, 'expected #{this} not to have text #{exp}'
|
|
||||||
, text
|
|
||||||
, actual
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
chai.Assertion.addMethod('value', function (value) {
|
|
||||||
var actual = flag(this, 'object').val();
|
|
||||||
this.assert(
|
|
||||||
flag(this, 'object').val() === value
|
|
||||||
, 'expected #{this} to have value #{exp}, but the value was #{act}'
|
|
||||||
, 'expected #{this} not to have value #{exp}'
|
|
||||||
, value
|
|
||||||
, actual
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
chai.Assertion.addMethod('descendants', function (selector) {
|
|
||||||
this.assert(
|
|
||||||
flag(this, 'object').has(selector).length > 0
|
|
||||||
, 'expected #{this} to have #{exp}'
|
|
||||||
, 'expected #{this} not to have #{exp}'
|
|
||||||
, selector
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
$.each(['visible', 'hidden', 'selected', 'checked', 'enabled', 'disabled'], function (i, attr) {
|
|
||||||
chai.Assertion.addProperty(attr, function () {
|
|
||||||
this.assert(
|
|
||||||
flag(this, 'object').is(':' + attr)
|
|
||||||
, 'expected #{this} to be ' + attr
|
|
||||||
, 'expected #{this} not to be ' + attr);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
chai.Assertion.overwriteProperty('exist', function (_super) {
|
|
||||||
return function () {
|
|
||||||
var obj = flag(this, 'object');
|
|
||||||
if (obj instanceof $) {
|
|
||||||
this.assert(
|
|
||||||
obj.length > 0
|
|
||||||
, 'expected ' + inspect(obj.selector) + ' to exist'
|
|
||||||
, 'expected ' + inspect(obj.selector) + ' not to exist');
|
|
||||||
} else {
|
|
||||||
_super.apply(this, arguments);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
chai.Assertion.overwriteProperty('empty', function (_super) {
|
|
||||||
return function () {
|
|
||||||
var obj = flag(this, 'object');
|
|
||||||
if (obj instanceof $) {
|
|
||||||
this.assert(
|
|
||||||
obj.is(':empty')
|
|
||||||
, 'expected #{this} to be empty'
|
|
||||||
, 'expected #{this} not to be empty');
|
|
||||||
} else {
|
|
||||||
_super.apply(this, arguments);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
chai.Assertion.overwriteMethod('match', function (_super) {
|
|
||||||
return function (selector) {
|
|
||||||
var obj = flag(this, 'object');
|
|
||||||
if (obj instanceof $) {
|
|
||||||
this.assert(
|
|
||||||
obj.is(selector)
|
|
||||||
, 'expected #{this} to match #{exp}'
|
|
||||||
, 'expected #{this} not to match #{exp}'
|
|
||||||
, selector
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
_super.apply(this, arguments);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
chai.Assertion.overwriteChainableMethod('contain',
|
|
||||||
function (_super) {
|
|
||||||
return function (text) {
|
|
||||||
var obj = flag(this, 'object');
|
|
||||||
if (obj instanceof $) {
|
|
||||||
this.assert(
|
|
||||||
obj.is(':contains(\'' + text + '\')')
|
|
||||||
, 'expected #{this} to contain #{exp}'
|
|
||||||
, 'expected #{this} not to contain #{exp}'
|
|
||||||
, text);
|
|
||||||
} else {
|
|
||||||
_super.apply(this, arguments);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function(_super) {
|
|
||||||
return function() {
|
|
||||||
_super.call(this);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}));
|
|
File diff suppressed because it is too large
Load Diff
@ -1 +0,0 @@
|
|||||||
.DS_Store
|
|
@ -1,3 +0,0 @@
|
|||||||
# CodeMirror Accessible
|
|
||||||
|
|
||||||
Read more at the project page: http://bgrins.github.io/codemirror-accessible/
|
|
@ -1,80 +0,0 @@
|
|||||||
window.onload=function(){
|
|
||||||
if (!window.waitToLoad) {
|
|
||||||
load();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function load() {
|
|
||||||
if (window.loaded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.loaded = true;
|
|
||||||
|
|
||||||
var myTextArea = document.getElementById("editor");
|
|
||||||
var textareaEditor = document.getElementById('textarea-editor');
|
|
||||||
var clonedTextArea = document.getElementById('editor2');
|
|
||||||
|
|
||||||
textareaEditor.value = clonedTextArea.value = myTextArea.value;
|
|
||||||
|
|
||||||
document.getElementById("original-demo").appendChild(clonedTextArea);
|
|
||||||
var myScreenreader = document.getElementById("editor-screenreader");
|
|
||||||
|
|
||||||
var myContentEditable = document.getElementById("content-editable");
|
|
||||||
var button = document.getElementById("toggle-textarea-visibility");
|
|
||||||
|
|
||||||
|
|
||||||
var editor = window.editor = CodeMirror.fromTextArea(myTextArea, {
|
|
||||||
lineNumbers: true,
|
|
||||||
matchBrackets: true,
|
|
||||||
continueComments: "Enter",
|
|
||||||
extraKeys: {"Ctrl-Q": "toggleComment"}
|
|
||||||
});
|
|
||||||
|
|
||||||
var editor2 = window.editor2 = CodeMirrorOriginal.fromTextArea(clonedTextArea, {
|
|
||||||
lineNumbers: true,
|
|
||||||
matchBrackets: true,
|
|
||||||
continueComments: "Enter",
|
|
||||||
extraKeys: {"Ctrl-Q": "toggleComment"}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
var textareaVisible = false;
|
|
||||||
function toggleTextareaDisplay() {
|
|
||||||
|
|
||||||
if (!editor.display.input.getAttribute("backup-style")) {
|
|
||||||
editor.display.input.setAttribute("backup-style", editor.display.input.getAttribute("style"));
|
|
||||||
editor.display.input.parentNode.setAttribute("backup-style", editor.display.input.parentNode.getAttribute("style"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textareaVisible) {
|
|
||||||
editor.display.input.setAttribute("style", editor.display.input.getAttribute("backup-style"));
|
|
||||||
editor.display.input.parentNode.setAttribute("style", editor.display.input.parentNode.getAttribute("backup-style"));
|
|
||||||
editor2.display.input.setAttribute("style", editor.display.input.getAttribute("backup-style"));
|
|
||||||
editor2.display.input.parentNode.setAttribute("style", editor.display.input.parentNode.getAttribute("backup-style"));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
editor.display.input.setAttribute("style", "height: 100px; width: 95%; margin-left: 3%; ");
|
|
||||||
editor.display.input.parentNode.setAttribute("style", "");
|
|
||||||
editor2.display.input.setAttribute("style", "height: 100px; width: 95%; margin-left: 3%; ");
|
|
||||||
editor2.display.input.parentNode.setAttribute("style", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
textareaVisible = !textareaVisible;
|
|
||||||
|
|
||||||
if (window.localStorage) {
|
|
||||||
if (textareaVisible) {
|
|
||||||
window.localStorage["toggleTextareaDisplay"] = "1";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
window.localStorage.removeKey("toggleTextareaDisplay");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button.onclick = toggleTextareaDisplay;
|
|
||||||
|
|
||||||
if (window.localStorage && window.localStorage["toggleTextareaDisplay"]) {
|
|
||||||
toggleTextareaDisplay();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,258 +0,0 @@
|
|||||||
/* BASICS */
|
|
||||||
|
|
||||||
.CodeMirror {
|
|
||||||
/* Set height, width, borders, and global font properties here */
|
|
||||||
font-family: monospace;
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
.CodeMirror-scroll {
|
|
||||||
/* Set scrolling behaviour here */
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* PADDING */
|
|
||||||
|
|
||||||
.CodeMirror-lines {
|
|
||||||
padding: 4px 0; /* Vertical padding around content */
|
|
||||||
}
|
|
||||||
.CodeMirror pre {
|
|
||||||
padding: 0 4px; /* Horizontal padding of content */
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
|
||||||
background-color: white; /* The little square between H and V scrollbars */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* GUTTER */
|
|
||||||
|
|
||||||
.CodeMirror-gutters {
|
|
||||||
border-right: 1px solid #ddd;
|
|
||||||
background-color: #f7f7f7;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.CodeMirror-linenumbers {}
|
|
||||||
.CodeMirror-linenumber {
|
|
||||||
padding: 0 3px 0 5px;
|
|
||||||
min-width: 20px;
|
|
||||||
text-align: right;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* CURSOR */
|
|
||||||
|
|
||||||
.CodeMirror div.CodeMirror-cursor {
|
|
||||||
border-left: 1px solid black;
|
|
||||||
z-index: 3;
|
|
||||||
}
|
|
||||||
/* Shown when moving in bi-directional text */
|
|
||||||
.CodeMirror div.CodeMirror-secondarycursor {
|
|
||||||
border-left: 1px solid silver;
|
|
||||||
}
|
|
||||||
.CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor {
|
|
||||||
width: auto;
|
|
||||||
border: 0;
|
|
||||||
background: #7e7;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
/* Can style cursor different in overwrite (non-insert) mode */
|
|
||||||
.CodeMirror div.CodeMirror-cursor.CodeMirror-overwrite {}
|
|
||||||
|
|
||||||
.cm-tab { display: inline-block; }
|
|
||||||
|
|
||||||
/* DEFAULT THEME */
|
|
||||||
|
|
||||||
.cm-s-default .cm-keyword {color: #708;}
|
|
||||||
.cm-s-default .cm-atom {color: #219;}
|
|
||||||
.cm-s-default .cm-number {color: #164;}
|
|
||||||
.cm-s-default .cm-def {color: #00f;}
|
|
||||||
.cm-s-default .cm-variable {color: black;}
|
|
||||||
.cm-s-default .cm-variable-2 {color: #05a;}
|
|
||||||
.cm-s-default .cm-variable-3 {color: #085;}
|
|
||||||
.cm-s-default .cm-property {color: black;}
|
|
||||||
.cm-s-default .cm-operator {color: black;}
|
|
||||||
.cm-s-default .cm-comment {color: #a50;}
|
|
||||||
.cm-s-default .cm-string {color: #a11;}
|
|
||||||
.cm-s-default .cm-string-2 {color: #f50;}
|
|
||||||
.cm-s-default .cm-meta {color: #555;}
|
|
||||||
.cm-s-default .cm-error {color: #f00;}
|
|
||||||
.cm-s-default .cm-qualifier {color: #555;}
|
|
||||||
.cm-s-default .cm-builtin {color: #30a;}
|
|
||||||
.cm-s-default .cm-bracket {color: #997;}
|
|
||||||
.cm-s-default .cm-tag {color: #170;}
|
|
||||||
.cm-s-default .cm-attribute {color: #00c;}
|
|
||||||
.cm-s-default .cm-header {color: blue;}
|
|
||||||
.cm-s-default .cm-quote {color: #090;}
|
|
||||||
.cm-s-default .cm-hr {color: #999;}
|
|
||||||
.cm-s-default .cm-link {color: #00c;}
|
|
||||||
|
|
||||||
.cm-negative {color: #d44;}
|
|
||||||
.cm-positive {color: #292;}
|
|
||||||
.cm-header, .cm-strong {font-weight: bold;}
|
|
||||||
.cm-em {font-style: italic;}
|
|
||||||
.cm-link {text-decoration: underline;}
|
|
||||||
|
|
||||||
.cm-invalidchar {color: #f00;}
|
|
||||||
|
|
||||||
div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
|
|
||||||
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
|
|
||||||
|
|
||||||
/* STOP */
|
|
||||||
|
|
||||||
/* The rest of this file contains styles related to the mechanics of
|
|
||||||
the editor. You probably shouldn't touch them. */
|
|
||||||
|
|
||||||
.CodeMirror {
|
|
||||||
line-height: 1;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
background: white;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-scroll {
|
|
||||||
/* 30px is the magic margin used to hide the element's real scrollbars */
|
|
||||||
/* See overflow: hidden in .CodeMirror */
|
|
||||||
margin-bottom: -30px; margin-right: -30px;
|
|
||||||
padding-bottom: 30px; padding-right: 30px;
|
|
||||||
height: 100%;
|
|
||||||
outline: none; /* Prevent dragging from highlighting the element */
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.CodeMirror-sizer {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* The fake, visible scrollbars. Used to force redraw during scrolling
|
|
||||||
before actuall scrolling happens, thus preventing shaking and
|
|
||||||
flickering artifacts. */
|
|
||||||
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 6;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.CodeMirror-vscrollbar {
|
|
||||||
right: 0; top: 0;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
.CodeMirror-hscrollbar {
|
|
||||||
bottom: 0; left: 0;
|
|
||||||
overflow-y: hidden;
|
|
||||||
overflow-x: scroll;
|
|
||||||
}
|
|
||||||
.CodeMirror-scrollbar-filler {
|
|
||||||
right: 0; bottom: 0;
|
|
||||||
}
|
|
||||||
.CodeMirror-gutter-filler {
|
|
||||||
left: 0; bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-gutters {
|
|
||||||
position: absolute; left: 0; top: 0;
|
|
||||||
padding-bottom: 30px;
|
|
||||||
z-index: 3;
|
|
||||||
}
|
|
||||||
.CodeMirror-gutter {
|
|
||||||
white-space: normal;
|
|
||||||
height: 100%;
|
|
||||||
padding-bottom: 30px;
|
|
||||||
margin-bottom: -32px;
|
|
||||||
display: inline-block;
|
|
||||||
/* Hack to make IE7 behave */
|
|
||||||
*zoom:1;
|
|
||||||
*display:inline;
|
|
||||||
}
|
|
||||||
.CodeMirror-gutter-elt {
|
|
||||||
position: absolute;
|
|
||||||
cursor: default;
|
|
||||||
z-index: 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-lines {
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
.CodeMirror pre {
|
|
||||||
/* Reset some styles that the rest of the page might have set */
|
|
||||||
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
|
|
||||||
border-width: 0;
|
|
||||||
background: transparent;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
margin: 0;
|
|
||||||
white-space: pre;
|
|
||||||
word-wrap: normal;
|
|
||||||
line-height: inherit;
|
|
||||||
color: inherit;
|
|
||||||
z-index: 2;
|
|
||||||
position: relative;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
.CodeMirror-wrap pre {
|
|
||||||
word-wrap: break-word;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: normal;
|
|
||||||
}
|
|
||||||
.CodeMirror-code pre {
|
|
||||||
border-right: 30px solid transparent;
|
|
||||||
width: -webkit-fit-content;
|
|
||||||
width: -moz-fit-content;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
.CodeMirror-wrap .CodeMirror-code pre {
|
|
||||||
border-right: none;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
.CodeMirror-linebackground {
|
|
||||||
position: absolute;
|
|
||||||
left: 0; right: 0; top: 0; bottom: 0;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-linewidget {
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-widget {
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-wrap .CodeMirror-scroll {
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-measure {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%; height: 0px;
|
|
||||||
overflow: hidden;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
.CodeMirror-measure pre { position: static; }
|
|
||||||
|
|
||||||
.CodeMirror div.CodeMirror-cursor {
|
|
||||||
position: absolute;
|
|
||||||
visibility: hidden;
|
|
||||||
border-right: none;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
.CodeMirror-focused div.CodeMirror-cursor {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-selected { background: #d9d9d9; }
|
|
||||||
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
|
|
||||||
|
|
||||||
.cm-searching {
|
|
||||||
background: #ffa;
|
|
||||||
background: rgba(255, 255, 0, .4);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* IE7 hack to prevent it from returning funny offsetTops on the spans */
|
|
||||||
.CodeMirror span { *vertical-align: text-bottom; }
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
/* Hide the cursor when printing */
|
|
||||||
.CodeMirror div.CodeMirror-cursor {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user