@ -441,4 +441,28 @@ module.exports = function(User) {
|
|||||||
}
|
}
|
||||||
return this.constructor.update$({ id }, updateData, updateOptions);
|
return this.constructor.update$({ id }, updateData, updateOptions);
|
||||||
};
|
};
|
||||||
|
User.prototype.getPoints$ = function getPoints$() {
|
||||||
|
const id = this.getId();
|
||||||
|
const filter = {
|
||||||
|
where: { id },
|
||||||
|
fields: { progressTimestamps: true }
|
||||||
|
};
|
||||||
|
return this.constructor.findOne$(filter)
|
||||||
|
.map(user => {
|
||||||
|
this.progressTimestamps = user.progressTimestamps;
|
||||||
|
return user.progressTimestamps;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
User.prototype.getChallengeMap$ = function getChallengeMap$() {
|
||||||
|
const id = this.getId();
|
||||||
|
const filter = {
|
||||||
|
where: { id },
|
||||||
|
fields: { challengeMap: true }
|
||||||
|
};
|
||||||
|
return this.constructor.findOne$(filter)
|
||||||
|
.map(user => {
|
||||||
|
this.challengeMap = user.challengeMap;
|
||||||
|
return user.challengeMap;
|
||||||
|
});
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -94,6 +94,7 @@
|
|||||||
"normalize-url": "^1.3.1",
|
"normalize-url": "^1.3.1",
|
||||||
"normalizr": "^2.0.0",
|
"normalizr": "^2.0.0",
|
||||||
"object.assign": "^4.0.3",
|
"object.assign": "^4.0.3",
|
||||||
|
"passport": "^0.2.1",
|
||||||
"passport-facebook": "^2.0.0",
|
"passport-facebook": "^2.0.0",
|
||||||
"passport-github": "^1.0.0",
|
"passport-github": "^1.0.0",
|
||||||
"passport-google-oauth2": "~0.1.6",
|
"passport-google-oauth2": "~0.1.6",
|
||||||
|
@ -223,7 +223,7 @@
|
|||||||
"id": "a7bf700cd123b9a54eef01d5",
|
"id": "a7bf700cd123b9a54eef01d5",
|
||||||
"title": "No repeats please",
|
"title": "No repeats please",
|
||||||
"description": [
|
"description": [
|
||||||
"Return the number of total permutations of the provided string that don't have repeated consecutive letters. Assume that duplicate characters are each unique.",
|
"Return the number of total permutations of the provided string that don't have repeated consecutive letters. Assume that all characters in the provided string are each unique.",
|
||||||
"For example, <code>aab</code> should return 2 because it has 6 total permutations (<code>aab</code>, <code>aab</code>, <code>aba</code>, <code>aba</code>, <code>baa</code>, <code>baa</code>), but only 2 of them (<code>aba</code> and <code>aba</code>) don't have the same letter (in this case <code>a</code>) repeating.",
|
"For example, <code>aab</code> should return 2 because it has 6 total permutations (<code>aab</code>, <code>aab</code>, <code>aba</code>, <code>aba</code>, <code>baa</code>, <code>baa</code>), but only 2 of them (<code>aba</code> and <code>aba</code>) don't have the same letter (in this case <code>a</code>) repeating.",
|
||||||
"Remember to use <a href='//github.com/FreeCodeCamp/freecodecamp/wiki/How-to-get-help-when-you-get-stuck' target='_blank'>Read-Search-Ask</a> if you get stuck. Try to pair program. Write your own code."
|
"Remember to use <a href='//github.com/FreeCodeCamp/freecodecamp/wiki/How-to-get-help-when-you-get-stuck' target='_blank'>Read-Search-Ask</a> if you get stuck. Try to pair program. Write your own code."
|
||||||
],
|
],
|
||||||
|
@ -3696,7 +3696,7 @@
|
|||||||
"assert($(\"h1\").hasClass(\"blue-text\"), 'message: Your <code>h1</code> element should have the class <code>blue-text</code>.');",
|
"assert($(\"h1\").hasClass(\"blue-text\"), 'message: Your <code>h1</code> element should have the class <code>blue-text</code>.');",
|
||||||
"assert($(\"h1\").attr(\"id\") === \"orange-text\", 'message: Your <code>h1</code> element should have the id of <code>orange-text</code>.');",
|
"assert($(\"h1\").attr(\"id\") === \"orange-text\", 'message: Your <code>h1</code> element should have the id of <code>orange-text</code>.');",
|
||||||
"assert(code.match(/<h1.*style/gi) && code.match(/<h1.*style.*color\\s*?:/gi), 'message: Your <code>h1</code> element should have the inline style of <code>color: white</code>.');",
|
"assert(code.match(/<h1.*style/gi) && code.match(/<h1.*style.*color\\s*?:/gi), 'message: Your <code>h1</code> element should have the inline style of <code>color: white</code>.');",
|
||||||
"assert(code.match(/pink.*\\!important;/gi), 'message: Your <code>pink-text</code> class should have the <code>!important</code> keyword to override all other declarations.');",
|
"assert(code.match(/\\.pink-text\\s+\\{\\s+color:.*pink.*!important;\\s+\\}/gi), 'message: Your <code>pink-text</code> class declaration should have the <code>!important</code> keyword to override all other declarations.');",
|
||||||
"assert($(\"h1\").css(\"color\") === \"rgb(255, 192, 203)\", 'message: Your <code>h1</code> element should be pink.');"
|
"assert($(\"h1\").css(\"color\") === \"rgb(255, 192, 203)\", 'message: Your <code>h1</code> element should be pink.');"
|
||||||
],
|
],
|
||||||
"descriptionPtBR": [
|
"descriptionPtBR": [
|
||||||
|
@ -546,10 +546,10 @@
|
|||||||
"description": [
|
"description": [
|
||||||
"Using jQuery, you can change the text between the start and end tags of an element. You can even change HTML markup.",
|
"Using jQuery, you can change the text between the start and end tags of an element. You can even change HTML markup.",
|
||||||
"jQuery has a function called <code>.html()</code> that lets you add HTML tags and text within an element. Any content previously within the element will be completely replaced with the content you provide using this function.",
|
"jQuery has a function called <code>.html()</code> that lets you add HTML tags and text within an element. Any content previously within the element will be completely replaced with the content you provide using this function.",
|
||||||
"Here's how you would rewrite and italicize the text of our heading:",
|
"Here's how you would rewrite and emphasize the text of our heading:",
|
||||||
"<code>$(\"h3\").html(\"<em>jQuery Playground</em>\");</code>",
|
"<code>$(\"h3\").html(\"<em>jQuery Playground</em>\");</code>",
|
||||||
"jQuery also has a similar function called <code>.text()</code> that only alters text without adding tags. In other words, this function will not evaluate any HTML tags passed to it, but will instead treat it as text you want to replace with.",
|
"jQuery also has a similar function called <code>.text()</code> that only alters text without adding tags. In other words, this function will not evaluate any HTML tags passed to it, but will instead treat it as text you want to replace with.",
|
||||||
"Change the button with id <code>target4</code> by italicizing its text."
|
"Change the button with id <code>target4</code> by emphasizing its text."
|
||||||
],
|
],
|
||||||
"releasedOn": "November 18, 2015",
|
"releasedOn": "November 18, 2015",
|
||||||
"challengeSeed": [
|
"challengeSeed": [
|
||||||
|
@ -196,6 +196,7 @@
|
|||||||
"id": "bd7153d8c441eddfaeb5bd1f",
|
"id": "bd7153d8c441eddfaeb5bd1f",
|
||||||
"title": "Build Web Apps with Express.js",
|
"title": "Build Web Apps with Express.js",
|
||||||
"description": [
|
"description": [
|
||||||
|
"Before we begin using Express.js, read this article on <a href='http://evanhahn.com/understanding-express/' target='_blank'>Understanding Express.js</a>.",
|
||||||
"We'll build this Waypoint on Cloud 9, a powerful online code editor with a full Ubuntu Linux workspace, all running in the cloud.",
|
"We'll build this Waypoint on Cloud 9, a powerful online code editor with a full Ubuntu Linux workspace, all running in the cloud.",
|
||||||
"If you don't already have Cloud 9 account, create one now at <a href='http://c9.io' target='_blank'>http://c9.io</a>.",
|
"If you don't already have Cloud 9 account, create one now at <a href='http://c9.io' target='_blank'>http://c9.io</a>.",
|
||||||
"Open up <a href='http://c9.io' target='_blank'>http://c9.io</a> and sign in to your account.",
|
"Open up <a href='http://c9.io' target='_blank'>http://c9.io</a> and sign in to your account.",
|
||||||
|
@ -84,7 +84,8 @@ export default function certificate(app) {
|
|||||||
|
|
||||||
function verifyCert(certType, req, res, next) {
|
function verifyCert(certType, req, res, next) {
|
||||||
const { user } = req;
|
const { user } = req;
|
||||||
return certTypeIds[certType]
|
return user.getChallengeMap()
|
||||||
|
.flatMap(() => certTypeIds[certType])
|
||||||
.flatMap(challenge => {
|
.flatMap(challenge => {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
|
@ -142,7 +142,7 @@ function getRenderData$(user, challenge$, origChallengeName, solution) {
|
|||||||
|
|
||||||
return challenge$
|
return challenge$
|
||||||
.map(challenge => challenge.toJSON())
|
.map(challenge => challenge.toJSON())
|
||||||
.filter((challenge) => {
|
.filter(challenge => {
|
||||||
return shouldNotFilterComingSoon(challenge) &&
|
return shouldNotFilterComingSoon(challenge) &&
|
||||||
challenge.type !== 'hike' &&
|
challenge.type !== 'hike' &&
|
||||||
testChallengeName.test(challenge.name);
|
testChallengeName.test(challenge.name);
|
||||||
@ -500,8 +500,17 @@ module.exports = function(app) {
|
|||||||
function showChallenge(req, res, next) {
|
function showChallenge(req, res, next) {
|
||||||
const solution = req.query.solution;
|
const solution = req.query.solution;
|
||||||
const challengeName = req.params.challengeName.replace(challengesRegex, '');
|
const challengeName = req.params.challengeName.replace(challengesRegex, '');
|
||||||
|
const { user } = req;
|
||||||
|
|
||||||
getRenderData$(req.user, challenge$, challengeName, solution)
|
Observable.defer(() => {
|
||||||
|
if (user && user.getChallengeMap$) {
|
||||||
|
return user.getChallengeMap$().map(user);
|
||||||
|
}
|
||||||
|
return Observable.just(null);
|
||||||
|
})
|
||||||
|
.flatMap(user => {
|
||||||
|
return getRenderData$(user, challenge$, challengeName, solution);
|
||||||
|
})
|
||||||
.subscribe(
|
.subscribe(
|
||||||
({ type, redirectUrl, message, data }) => {
|
({ type, redirectUrl, message, data }) => {
|
||||||
if (message) {
|
if (message) {
|
||||||
@ -546,48 +555,46 @@ module.exports = function(app) {
|
|||||||
return res.sendStatus(403);
|
return res.sendStatus(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
const completedDate = Date.now();
|
return req.user.getChallengeMap$()
|
||||||
const {
|
.flatMap(() => {
|
||||||
id,
|
const completedDate = Date.now();
|
||||||
name,
|
const {
|
||||||
challengeType,
|
id,
|
||||||
solution,
|
name,
|
||||||
timezone
|
challengeType,
|
||||||
} = req.body;
|
solution,
|
||||||
|
timezone
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
const { alreadyCompleted, updateData } = buildUserUpdate(
|
const { alreadyCompleted, updateData } = buildUserUpdate(
|
||||||
req.user,
|
req.user,
|
||||||
id,
|
id,
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
challengeType,
|
challengeType,
|
||||||
solution,
|
solution,
|
||||||
name,
|
name,
|
||||||
completedDate
|
completedDate
|
||||||
},
|
},
|
||||||
timezone
|
timezone
|
||||||
);
|
);
|
||||||
|
|
||||||
const user = req.user;
|
const user = req.user;
|
||||||
const points = alreadyCompleted ?
|
const points = alreadyCompleted ? user.points : user.points + 1;
|
||||||
user.progressTimestamps.length :
|
|
||||||
user.progressTimestamps.length + 1;
|
|
||||||
|
|
||||||
return user.update$(updateData)
|
return user.update$(updateData)
|
||||||
.doOnNext(({ count }) => log('%s documents updated', count))
|
.doOnNext(({ count }) => log('%s documents updated', count))
|
||||||
.subscribe(
|
.map(() => {
|
||||||
() => {},
|
if (type === 'json') {
|
||||||
next,
|
return res.json({
|
||||||
function() {
|
points,
|
||||||
if (type === 'json') {
|
alreadyCompleted
|
||||||
return res.json({
|
});
|
||||||
points,
|
}
|
||||||
alreadyCompleted
|
return res.sendStatus(200);
|
||||||
});
|
});
|
||||||
}
|
})
|
||||||
return res.sendStatus(200);
|
.subscribe(() => {}, next);
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function completedZiplineOrBasejump(req, res, next) {
|
function completedZiplineOrBasejump(req, res, next) {
|
||||||
@ -635,31 +642,36 @@ module.exports = function(app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const {
|
return user.getChallengeMap$()
|
||||||
alreadyCompleted,
|
.flatMap(() => {
|
||||||
updateData
|
const {
|
||||||
} = buildUserUpdate(req.user, completedChallenge.id, completedChallenge);
|
alreadyCompleted,
|
||||||
|
updateData
|
||||||
|
} = buildUserUpdate(user, completedChallenge.id, completedChallenge);
|
||||||
|
|
||||||
return user.update$(updateData)
|
return user.update$(updateData)
|
||||||
.doOnNext(({ count }) => log('%s documents updated', count))
|
.doOnNext(({ count }) => log('%s documents updated', count))
|
||||||
.doOnNext(() => {
|
.doOnNext(() => {
|
||||||
if (type === 'json') {
|
if (type === 'json') {
|
||||||
return res.send({
|
return res.send({
|
||||||
alreadyCompleted,
|
alreadyCompleted,
|
||||||
points: alreadyCompleted ?
|
points: alreadyCompleted ? user.points : user.points + 1
|
||||||
user.progressTimestamps.length :
|
});
|
||||||
user.progressTimestamps.length + 1
|
}
|
||||||
|
return res.status(200).send(true);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
return res.status(200).send(true);
|
|
||||||
})
|
})
|
||||||
.subscribe(() => {}, next);
|
.subscribe(() => {}, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMap(showAside, { user = {} }, res, next) {
|
function showMap(showAside, { user }, res, next) {
|
||||||
const { challengeMap = {} } = user;
|
return Observable.defer(() => {
|
||||||
|
if (user && typeof user.getChallengeMap$ === 'function') {
|
||||||
return getSuperBlocks$(challenge$, challengeMap)
|
return user.getChallengeMap$();
|
||||||
|
}
|
||||||
|
return Observable.just({});
|
||||||
|
})
|
||||||
|
.flatMap(challengeMap => getSuperBlocks$(challenge$, challengeMap))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
superBlocks => {
|
superBlocks => {
|
||||||
res.render('map/show', {
|
res.render('map/show', {
|
||||||
|
@ -43,7 +43,6 @@ module.exports = function(app) {
|
|||||||
router.get('/how-nonprofit-projects-work', howNonprofitProjectsWork);
|
router.get('/how-nonprofit-projects-work', howNonprofitProjectsWork);
|
||||||
router.get('/code-of-conduct', codeOfConduct);
|
router.get('/code-of-conduct', codeOfConduct);
|
||||||
router.get('/academic-honesty', academicHonesty);
|
router.get('/academic-honesty', academicHonesty);
|
||||||
router.get('/news', news);
|
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/the-fastest-web-page-on-the-internet',
|
'/the-fastest-web-page-on-the-internet',
|
||||||
@ -285,12 +284,6 @@ module.exports = function(app) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function news(req, res) {
|
|
||||||
res.render('resources/camper-news-deprecated', {
|
|
||||||
title: 'Camper News'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function twitch(req, res) {
|
function twitch(req, res) {
|
||||||
res.redirect('https://twitch.tv/freecodecamp');
|
res.redirect('https://twitch.tv/freecodecamp');
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,11 @@
|
|||||||
var Rx = require('rx'),
|
import moment from 'moment';
|
||||||
assign = require('object.assign'),
|
|
||||||
sanitizeHtml = require('sanitize-html'),
|
|
||||||
moment = require('moment'),
|
|
||||||
debug = require('debug')('fcc:cntr:story'),
|
|
||||||
utils = require('../utils'),
|
|
||||||
observeMethod = require('../utils/rx').observeMethod,
|
|
||||||
saveUser = require('../utils/rx').saveUser,
|
|
||||||
saveInstance = require('../utils/rx').saveInstance,
|
|
||||||
validator = require('validator');
|
|
||||||
|
|
||||||
import {
|
import { unDasherize } from '../utils';
|
||||||
ifNoUser401,
|
import { observeMethod } from '../utils/rx';
|
||||||
ifNoUserRedirectTo
|
|
||||||
} from '../utils/middleware';
|
|
||||||
|
|
||||||
const foundationDate = 1413298800000;
|
const foundationDate = 1413298800000;
|
||||||
const time48Hours = 172800000;
|
const time48Hours = 172800000;
|
||||||
|
|
||||||
const unDasherize = utils.unDasherize;
|
|
||||||
const dasherize = utils.dasherize;
|
|
||||||
const getURLTitle = utils.getURLTitle;
|
|
||||||
const sendNonUserToNews = ifNoUserRedirectTo('/news');
|
|
||||||
|
|
||||||
function hotRank(timeValue, rank) {
|
function hotRank(timeValue, rank) {
|
||||||
/*
|
/*
|
||||||
* Hotness ranking algorithm: http://amix.dk/blog/post/19588
|
* Hotness ranking algorithm: http://amix.dk/blog/post/19588
|
||||||
@ -39,83 +23,61 @@ function sortByRank(a, b) {
|
|||||||
hotRank(a.timePosted - foundationDate, a.rank);
|
hotRank(a.timePosted - foundationDate, a.rank);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanData(data, opts) {
|
|
||||||
var options = assign(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
allowedTags: [],
|
|
||||||
allowedAttributes: []
|
|
||||||
},
|
|
||||||
opts || {}
|
|
||||||
);
|
|
||||||
return sanitizeHtml(data, options).replace(/";/g, '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function(app) {
|
module.exports = function(app) {
|
||||||
var router = app.loopback.Router();
|
const router = app.loopback.Router();
|
||||||
var User = app.models.User;
|
const Story = app.models.Story;
|
||||||
var findUserById = observeMethod(User, 'findById');
|
const findStory = observeMethod(Story, 'find');
|
||||||
|
const findOneStory = observeMethod(Story, 'findOne');
|
||||||
|
const query = {
|
||||||
|
order: 'timePosted DESC',
|
||||||
|
limit: 1000
|
||||||
|
};
|
||||||
|
const storiesData$ = findStory(query)
|
||||||
|
.map(stories => {
|
||||||
|
const sliceVal = stories.length >= 100 ? 100 : stories.length;
|
||||||
|
return stories.sort(sortByRank).slice(0, sliceVal);
|
||||||
|
})
|
||||||
|
.shareReplay();
|
||||||
|
|
||||||
var Story = app.models.Story;
|
const redirectToNews = (req, res) => res.redirect('/news');
|
||||||
var findStory = observeMethod(Story, 'find');
|
const deprecated = (req, res) => res.sendStatus(410);
|
||||||
var findOneStory = observeMethod(Story, 'findOne');
|
router.get('/news', showNews);
|
||||||
var findStoryById = observeMethod(Story, 'findById');
|
router.post('/news/userstories', deprecated);
|
||||||
var countStories = observeMethod(Story, 'count');
|
|
||||||
|
|
||||||
router.post('/news/userstories', userStories);
|
|
||||||
router.get('/news/hot', hotJSON);
|
router.get('/news/hot', hotJSON);
|
||||||
router.get('/news/feed', RSSFeed);
|
router.get('/news/feed', RSSFeed);
|
||||||
router.get('/stories/hotStories', hotJSON);
|
router.get('/stories/hotStories', hotJSON);
|
||||||
router.get(
|
router.get('/stories/submit', redirectToNews);
|
||||||
'/stories/submit',
|
router.get('/stories/submit/new-story', redirectToNews);
|
||||||
sendNonUserToNews,
|
router.post('/stories/preliminary', deprecated);
|
||||||
submitNew
|
router.post('/stories/', deprecated);
|
||||||
);
|
router.post('/stories/search', deprecated);
|
||||||
router.get(
|
|
||||||
'/stories/submit/new-story',
|
|
||||||
sendNonUserToNews,
|
|
||||||
preSubmit
|
|
||||||
);
|
|
||||||
router.post('/stories/preliminary', ifNoUser401, newStory);
|
|
||||||
router.post('/stories/', ifNoUser401, storySubmission);
|
|
||||||
router.post('/stories/search', getStories);
|
|
||||||
router.get('/news/:storyName', returnIndividualStory);
|
router.get('/news/:storyName', returnIndividualStory);
|
||||||
router.post('/stories/upvote/', ifNoUser401, upvote);
|
router.post('/stories/upvote/', deprecated);
|
||||||
router.get('/stories/:storyName', redirectToNews);
|
router.get('/stories/:storyName', replaceStoryWithNews);
|
||||||
|
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
function redirectToNews(req, res) {
|
function showNews(req, res) {
|
||||||
|
res.render('news/deprecated', { title: 'Camper News' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceStoryWithNews(req, res) {
|
||||||
var url = req.originalUrl.replace(/^\/stories/, '/news');
|
var url = req.originalUrl.replace(/^\/stories/, '/news');
|
||||||
return res.redirect(url);
|
return res.redirect(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hotJSON(req, res, next) {
|
function hotJSON(req, res, next) {
|
||||||
var query = {
|
storiesData$.subscribe(
|
||||||
order: 'timePosted DESC',
|
stories => res.json(stories),
|
||||||
limit: 1000
|
|
||||||
};
|
|
||||||
findStory(query).subscribe(
|
|
||||||
function(stories) {
|
|
||||||
var sliceVal = stories.length >= 100 ? 100 : stories.length;
|
|
||||||
var data = stories.sort(sortByRank).slice(0, sliceVal);
|
|
||||||
res.json(data);
|
|
||||||
},
|
|
||||||
next
|
next
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RSSFeed(req, res, next) {
|
function RSSFeed(req, res, next) {
|
||||||
var query = {
|
storiesData$.subscribe(
|
||||||
order: 'timePosted DESC',
|
data => {
|
||||||
limit: 1000
|
|
||||||
};
|
|
||||||
findStory(query).subscribe(
|
|
||||||
function(stories) {
|
|
||||||
var sliceVal = stories.length >= 100 ? 100 : stories.length;
|
|
||||||
var data = stories.sort(sortByRank).slice(0, sliceVal);
|
|
||||||
res.set('Content-Type', 'text/xml');
|
res.set('Content-Type', 'text/xml');
|
||||||
res.render('feed', {
|
res.render('news/feed', {
|
||||||
title: 'FreeCodeCamp Camper News RSS Feed',
|
title: 'FreeCodeCamp Camper News RSS Feed',
|
||||||
description: 'RSS Feed for FreeCodeCamp Top 100 Hot Camper News',
|
description: 'RSS Feed for FreeCodeCamp Top 100 Hot Camper News',
|
||||||
url: 'http://www.freecodecamp.com/news',
|
url: 'http://www.freecodecamp.com/news',
|
||||||
@ -126,51 +88,6 @@ module.exports = function(app) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitNew(req, res) {
|
|
||||||
if (!req.user.isGithubCool) {
|
|
||||||
req.flash('errors', {
|
|
||||||
msg: 'You must link GitHub with your account before you can post' +
|
|
||||||
' on Camper News.'
|
|
||||||
});
|
|
||||||
return res.redirect('/news');
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.render('stories/index', {
|
|
||||||
title: 'Submit a new story to Camper News',
|
|
||||||
page: 'submit'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function preSubmit(req, res) {
|
|
||||||
var data = req.query;
|
|
||||||
if (typeof data.url !== 'string') {
|
|
||||||
req.flash('errors', { msg: 'No URL supplied with story' });
|
|
||||||
return res.redirect('/news');
|
|
||||||
}
|
|
||||||
var cleanedData = cleanData(data.url);
|
|
||||||
|
|
||||||
if (data.url.replace(/&/g, '&') !== cleanedData) {
|
|
||||||
req.flash('errors', {
|
|
||||||
msg: 'The data for this post is malformed'
|
|
||||||
});
|
|
||||||
return res.render('stories/index', {
|
|
||||||
page: 'stories/submit'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var title = data.title || '';
|
|
||||||
var image = data.image || '';
|
|
||||||
var description = data.description || '';
|
|
||||||
return res.render('stories/index', {
|
|
||||||
title: 'Confirm your Camper News story submission',
|
|
||||||
page: 'storySubmission',
|
|
||||||
storyURL: data.url,
|
|
||||||
storyTitle: title,
|
|
||||||
storyImage: image,
|
|
||||||
storyMetaDescription: description
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function returnIndividualStory(req, res, next) {
|
function returnIndividualStory(req, res, next) {
|
||||||
var dashedName = req.params.storyName;
|
var dashedName = req.params.storyName;
|
||||||
var storyName = unDasherize(dashedName);
|
var storyName = unDasherize(dashedName);
|
||||||
@ -193,275 +110,19 @@ module.exports = function(app) {
|
|||||||
return res.redirect('../stories/' + dashedNameFull);
|
return res.redirect('../stories/' + dashedNameFull);
|
||||||
}
|
}
|
||||||
|
|
||||||
var username = req.user ? req.user.username : '';
|
return res.render('news/index', {
|
||||||
// true if any of votes are made by user
|
title: story.headline || 'news',
|
||||||
var userVoted = story.upVotes.some(function(upvote) {
|
|
||||||
return upvote.upVotedByUsername === username;
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.render('stories/index', {
|
|
||||||
title: story.headline,
|
|
||||||
link: story.link,
|
link: story.link,
|
||||||
originalStoryLink: dashedName,
|
originalStoryLink: dashedName,
|
||||||
author: story.author,
|
author: story.author,
|
||||||
rank: story.upVotes.length,
|
rank: story.upVotes.length,
|
||||||
upVotes: story.upVotes,
|
|
||||||
id: story.id,
|
id: story.id,
|
||||||
timeAgo: moment(story.timePosted).fromNow(),
|
timeAgo: moment(story.timePosted).fromNow(),
|
||||||
image: story.image,
|
image: story.image,
|
||||||
page: 'show',
|
storyMetaDescription: story.metaDescription
|
||||||
storyMetaDescription: story.metaDescription,
|
|
||||||
hasUserVoted: userVoted
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
next
|
next
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function userStories({ body: { search = '' } = {} }, res, next) {
|
|
||||||
if (!search || typeof search !== 'string') {
|
|
||||||
return res.sendStatus(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
return app.dataSources.db.connector
|
|
||||||
.collection('story')
|
|
||||||
.find({
|
|
||||||
'author.username': search.toLowerCase().replace('$', '')
|
|
||||||
})
|
|
||||||
.toArray(function(err, items) {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
if (items && items.length !== 0) {
|
|
||||||
return res.json(items.sort(sortByRank));
|
|
||||||
}
|
|
||||||
return res.sendStatus(404);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStories({ body: { search = '' } = {} }, res, next) {
|
|
||||||
if (!search || typeof search !== 'string') {
|
|
||||||
return res.sendStatus(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = {
|
|
||||||
'$text': {
|
|
||||||
// protect against NoSQL injection
|
|
||||||
'$search': search.replace('$', '')
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fields = {
|
|
||||||
headline: 1,
|
|
||||||
timePosted: 1,
|
|
||||||
link: 1,
|
|
||||||
description: 1,
|
|
||||||
rank: 1,
|
|
||||||
upVotes: 1,
|
|
||||||
author: 1,
|
|
||||||
image: 1,
|
|
||||||
storyLink: 1,
|
|
||||||
metaDescription: 1,
|
|
||||||
textScore: {
|
|
||||||
$meta: 'textScore'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
sort: {
|
|
||||||
textScore: {
|
|
||||||
$meta: 'textScore'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return app.dataSources.db.connector
|
|
||||||
.collection('story')
|
|
||||||
.find(query, fields, options)
|
|
||||||
.toArray(function(err, items) {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
if (items && items.length !== 0) {
|
|
||||||
return res.json(items);
|
|
||||||
}
|
|
||||||
return res.sendStatus(404);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function upvote(req, res, next) {
|
|
||||||
const { id } = req.body;
|
|
||||||
var story$ = findStoryById(id).shareReplay();
|
|
||||||
|
|
||||||
story$.flatMap(function(story) {
|
|
||||||
// find story author
|
|
||||||
return findUserById(story.author.userId);
|
|
||||||
})
|
|
||||||
.flatMap(function(user) {
|
|
||||||
// if user deletes account then this will not exist
|
|
||||||
if (user) {
|
|
||||||
user.progressTimestamps.push({
|
|
||||||
timestamp: Date.now()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return saveUser(user);
|
|
||||||
})
|
|
||||||
.flatMap(function() {
|
|
||||||
return story$;
|
|
||||||
})
|
|
||||||
.flatMap(function(story) {
|
|
||||||
debug('upvoting');
|
|
||||||
story.rank += 1;
|
|
||||||
story.upVotes.push({
|
|
||||||
upVotedBy: req.user.id,
|
|
||||||
upVotedByUsername: req.user.username
|
|
||||||
});
|
|
||||||
return saveInstance(story);
|
|
||||||
})
|
|
||||||
.subscribe(
|
|
||||||
function(story) {
|
|
||||||
return res.send(story);
|
|
||||||
},
|
|
||||||
next
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function newStory(req, res, next) {
|
|
||||||
if (!req.user.isGithubCool) {
|
|
||||||
req.flash('errors', {
|
|
||||||
msg: 'You must authenticate with GitHub to post to Camper News'
|
|
||||||
});
|
|
||||||
return res.redirect('/news');
|
|
||||||
}
|
|
||||||
var url = req.body.data.url;
|
|
||||||
|
|
||||||
if (!validator.isURL('' + url)) {
|
|
||||||
req.flash('errors', {
|
|
||||||
msg: "The URL you submitted doesn't appear valid"
|
|
||||||
});
|
|
||||||
return res.json({
|
|
||||||
alreadyPosted: true,
|
|
||||||
storyURL: '/stories/submit'
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
if (url.search(/^https?:\/\//g) === -1) {
|
|
||||||
url = 'http://' + url;
|
|
||||||
}
|
|
||||||
|
|
||||||
return findStory({ where: { link: url } })
|
|
||||||
.map(function(stories) {
|
|
||||||
if (stories.length) {
|
|
||||||
return {
|
|
||||||
alreadyPosted: true,
|
|
||||||
storyURL: '/stories/' + stories.pop().storyLink
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
alreadyPosted: false,
|
|
||||||
storyURL: url
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.flatMap(function(data) {
|
|
||||||
if (data.alreadyPosted) {
|
|
||||||
return Rx.Observable.just(data);
|
|
||||||
}
|
|
||||||
return Rx.Observable.fromNodeCallback(getURLTitle)(data.storyURL)
|
|
||||||
.map(function(story) {
|
|
||||||
return {
|
|
||||||
alreadyPosted: false,
|
|
||||||
storyURL: data.storyURL,
|
|
||||||
storyTitle: story.title,
|
|
||||||
storyImage: story.image,
|
|
||||||
storyMetaDescription: story.description
|
|
||||||
};
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.subscribe(
|
|
||||||
function(story) {
|
|
||||||
if (story.alreadyPosted) {
|
|
||||||
req.flash('errors', {
|
|
||||||
msg: "Someone's already posted that link. Here's the discussion."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
res.json(story);
|
|
||||||
},
|
|
||||||
next
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function storySubmission(req, res, next) {
|
|
||||||
if (req.user.isBanned) {
|
|
||||||
return res.json({
|
|
||||||
isBanned: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
var data = req.body.data;
|
|
||||||
|
|
||||||
var storyLink = data.headline
|
|
||||||
.replace(/[^a-z0-9\s]/gi, '')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.toLowerCase()
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
var link = data.link;
|
|
||||||
|
|
||||||
if (link.search(/^https?:\/\//g) === -1) {
|
|
||||||
link = 'http://' + link;
|
|
||||||
}
|
|
||||||
|
|
||||||
var query = {
|
|
||||||
storyLink: {
|
|
||||||
like: ('^' + storyLink + '(?: [0-9]+)?$'),
|
|
||||||
options: 'i'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var savedStory = countStories(query)
|
|
||||||
.flatMap(function(storyCount) {
|
|
||||||
// if duplicate storyLink add unique number
|
|
||||||
storyLink = (storyCount === 0) ?
|
|
||||||
storyLink :
|
|
||||||
storyLink + ' ' + storyCount;
|
|
||||||
|
|
||||||
var link = data.link;
|
|
||||||
if (link.search(/^https?:\/\//g) === -1) {
|
|
||||||
link = 'http://' + link;
|
|
||||||
}
|
|
||||||
var newStory = new Story({
|
|
||||||
headline: cleanData(data.headline),
|
|
||||||
timePosted: Date.now(),
|
|
||||||
link: link,
|
|
||||||
description: cleanData(data.description),
|
|
||||||
rank: 1,
|
|
||||||
upVotes: [({
|
|
||||||
upVotedBy: req.user.id,
|
|
||||||
upVotedByUsername: req.user.username
|
|
||||||
})],
|
|
||||||
author: {
|
|
||||||
picture: req.user.picture,
|
|
||||||
userId: req.user.id,
|
|
||||||
username: req.user.username
|
|
||||||
},
|
|
||||||
image: data.image,
|
|
||||||
storyLink: storyLink,
|
|
||||||
metaDescription: data.storyMetaDescription
|
|
||||||
});
|
|
||||||
return saveInstance(newStory);
|
|
||||||
});
|
|
||||||
|
|
||||||
req.user.progressTimestamps.push({
|
|
||||||
timestamp: Date.now()
|
|
||||||
});
|
|
||||||
return saveUser(req.user)
|
|
||||||
.flatMap(savedStory)
|
|
||||||
.subscribe(
|
|
||||||
function(story) {
|
|
||||||
res.json({
|
|
||||||
storyLink: dasherize(story.storyLink)
|
|
||||||
});
|
|
||||||
},
|
|
||||||
next
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
@ -420,63 +420,59 @@ module.exports = function(app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleLockdownMode(req, res, next) {
|
function toggleLockdownMode(req, res, next) {
|
||||||
return User.findById(req.accessToken.userId, function(err, user) {
|
const { user } = req;
|
||||||
if (err) { return next(err); }
|
user.update$({ isLocked: !user.isLocked })
|
||||||
return user.updateAttribute('isLocked', !user.isLocked, function(err) {
|
.subscribe(
|
||||||
if (err) { return next(err); }
|
() => {
|
||||||
req.flash('info', {
|
req.flash('info', {
|
||||||
msg: 'We\'ve successfully updated your Privacy preferences.'
|
msg: 'We\'ve successfully updated your Privacy preferences.'
|
||||||
});
|
});
|
||||||
return res.redirect('/settings');
|
return res.redirect('/settings');
|
||||||
});
|
},
|
||||||
});
|
next
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleReceivesAnnouncementEmails(req, res, next) {
|
function toggleReceivesAnnouncementEmails(req, res, next) {
|
||||||
return User.findById(req.accessToken.userId, function(err, user) {
|
const { user } = req;
|
||||||
if (err) { return next(err); }
|
return user.update$({ sendMonthlyEmail: !user.sendMonthlyEmail })
|
||||||
return user.updateAttribute(
|
.subscribe(
|
||||||
'sendMonthlyEmail',
|
() => {
|
||||||
!user.sendMonthlyEmail,
|
|
||||||
(err) => {
|
|
||||||
if (err) { return next(err); }
|
|
||||||
req.flash('info', {
|
req.flash('info', {
|
||||||
msg: 'We\'ve successfully updated your Email preferences.'
|
msg: 'We\'ve successfully updated your Email preferences.'
|
||||||
});
|
});
|
||||||
return res.redirect('/settings');
|
return res.redirect('/settings');
|
||||||
});
|
},
|
||||||
});
|
next
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleReceivesQuincyEmails(req, res, next) {
|
function toggleReceivesQuincyEmails(req, res, next) {
|
||||||
return User.findById(req.accessToken.userId, function(err, user) {
|
const { user } = req;
|
||||||
if (err) { return next(err); }
|
return user.update$({ sendQuincyEmail: !user.sendQuincyEmail })
|
||||||
return user.updateAttribute('sendQuincyEmail', !user.sendQuincyEmail,
|
.subscribe(
|
||||||
(err) => {
|
() => {
|
||||||
if (err) { return next(err); }
|
|
||||||
req.flash('info', {
|
req.flash('info', {
|
||||||
msg: 'We\'ve successfully updated your Email preferences.'
|
msg: 'We\'ve successfully updated your Email preferences.'
|
||||||
});
|
});
|
||||||
return res.redirect('/settings');
|
return res.redirect('/settings');
|
||||||
}
|
},
|
||||||
|
next
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleReceivesNotificationEmails(req, res, next) {
|
function toggleReceivesNotificationEmails(req, res, next) {
|
||||||
return User.findById(req.accessToken.userId, function(err, user) {
|
const { user } = req;
|
||||||
if (err) { return next(err); }
|
return user.update$({ sendNotificationEmail: !user.sendNotificationEmail })
|
||||||
return user.updateAttribute(
|
.subscribe(
|
||||||
'sendNotificationEmail',
|
() => {
|
||||||
!user.sendNotificationEmail,
|
|
||||||
function(err) {
|
|
||||||
if (err) { return next(err); }
|
|
||||||
req.flash('info', {
|
req.flash('info', {
|
||||||
msg: 'We\'ve successfully updated your Email preferences.'
|
msg: 'We\'ve successfully updated your Email preferences.'
|
||||||
});
|
});
|
||||||
return res.redirect('/settings');
|
return res.redirect('/settings');
|
||||||
});
|
},
|
||||||
});
|
next
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function postDeleteAccount(req, res, next) {
|
function postDeleteAccount(req, res, next) {
|
||||||
|
114
server/component-passport.js
Normal file
114
server/component-passport.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import passport from 'passport';
|
||||||
|
import { PassportConfigurator } from 'loopback-component-passport';
|
||||||
|
import passportProviders from './passport-providers';
|
||||||
|
import uuid from 'node-uuid';
|
||||||
|
import { generateKey } from 'loopback-component-passport/lib/models/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
setProfileFromGithub,
|
||||||
|
getSocialProvider,
|
||||||
|
getUsernameFromProvider
|
||||||
|
} from './utils/auth';
|
||||||
|
|
||||||
|
const passportOptions = {
|
||||||
|
emailOptional: true,
|
||||||
|
profileToUser(provider, profile) {
|
||||||
|
var emails = profile.emails;
|
||||||
|
// NOTE(berks): get email or set to null.
|
||||||
|
// MongoDB indexs email but can be sparse(blank)
|
||||||
|
var email = emails && emails[0] && emails[0].value ?
|
||||||
|
emails[0].value :
|
||||||
|
null;
|
||||||
|
|
||||||
|
// create random username
|
||||||
|
// username will be assigned when camper signups for Github
|
||||||
|
var username = 'fcc' + uuid.v4().slice(0, 8);
|
||||||
|
var password = generateKey('password');
|
||||||
|
var userObj = {
|
||||||
|
username: username,
|
||||||
|
password: password
|
||||||
|
};
|
||||||
|
|
||||||
|
if (email) {
|
||||||
|
userObj.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(/github/).test(provider)) {
|
||||||
|
userObj[getSocialProvider(provider)] = getUsernameFromProvider(
|
||||||
|
getSocialProvider(provider),
|
||||||
|
profile
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/github/.test(provider)) {
|
||||||
|
setProfileFromGithub(userObj, profile, profile._json);
|
||||||
|
}
|
||||||
|
return userObj;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fields = {
|
||||||
|
progressTimestamps: false,
|
||||||
|
completedChallenges: false,
|
||||||
|
challengeMap: false
|
||||||
|
};
|
||||||
|
|
||||||
|
PassportConfigurator.prototype.init = function passportInit(noSession) {
|
||||||
|
this.app.middleware('session:after', passport.initialize());
|
||||||
|
|
||||||
|
if (noSession) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.app.middleware('session:after', passport.session());
|
||||||
|
|
||||||
|
// Serialization and deserialization is only required if passport session is
|
||||||
|
// enabled
|
||||||
|
|
||||||
|
passport.serializeUser((user, done) => {
|
||||||
|
done(null, user.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
passport.deserializeUser((id, done) => {
|
||||||
|
|
||||||
|
this.userModel.findById(id, { fields }, (err, user) => {
|
||||||
|
if (err || !user) {
|
||||||
|
return done(err, user);
|
||||||
|
}
|
||||||
|
return this.app.dataSources.db.connector
|
||||||
|
.collection('user')
|
||||||
|
.aggregate([
|
||||||
|
{ $match: { _id: user.id } },
|
||||||
|
{ $project: { points: { $size: '$progressTimestamps' } } }
|
||||||
|
], function(err, [{ points = 1 } = {}]) {
|
||||||
|
if (err) { return done(err); }
|
||||||
|
user.points = points;
|
||||||
|
return done(null, user);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function setupPassport(app) {
|
||||||
|
const configurator = new PassportConfigurator(app);
|
||||||
|
|
||||||
|
configurator.setupModels({
|
||||||
|
userModel: app.models.user,
|
||||||
|
userIdentityModel: app.models.userIdentity,
|
||||||
|
userCredentialModel: app.models.userCredential
|
||||||
|
});
|
||||||
|
|
||||||
|
configurator.init();
|
||||||
|
|
||||||
|
Object.keys(passportProviders).map(function(strategy) {
|
||||||
|
var config = passportProviders[strategy];
|
||||||
|
config.session = config.session !== false;
|
||||||
|
configurator.configureProvider(
|
||||||
|
strategy,
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
...passportOptions
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
@ -3,96 +3,29 @@ var pmx = require('pmx');
|
|||||||
pmx.init();
|
pmx.init();
|
||||||
|
|
||||||
var _ = require('lodash'),
|
var _ = require('lodash'),
|
||||||
uuid = require('node-uuid'),
|
|
||||||
assign = require('lodash').assign,
|
|
||||||
loopback = require('loopback'),
|
loopback = require('loopback'),
|
||||||
boot = require('loopback-boot'),
|
boot = require('loopback-boot'),
|
||||||
expressState = require('express-state'),
|
expressState = require('express-state'),
|
||||||
path = require('path'),
|
path = require('path'),
|
||||||
passportProviders = require('./passport-providers');
|
setupPassport = require('./component-passport');
|
||||||
|
|
||||||
var setProfileFromGithub = require('./utils/auth').setProfileFromGithub;
|
|
||||||
var getSocialProvider = require('./utils/auth').getSocialProvider;
|
|
||||||
var getUsernameFromProvider = require('./utils/auth').getUsernameFromProvider;
|
|
||||||
var generateKey =
|
|
||||||
require('loopback-component-passport/lib/models/utils').generateKey;
|
|
||||||
|
|
||||||
var isBeta = !!process.env.BETA;
|
|
||||||
var app = loopback();
|
var app = loopback();
|
||||||
|
var isBeta = !!process.env.BETA;
|
||||||
|
|
||||||
expressState.extend(app);
|
expressState.extend(app);
|
||||||
app.set('state namespace', '__fcc__');
|
app.set('state namespace', '__fcc__');
|
||||||
|
|
||||||
var PassportConfigurator =
|
|
||||||
require('loopback-component-passport').PassportConfigurator;
|
|
||||||
var passportConfigurator = new PassportConfigurator(app);
|
|
||||||
|
|
||||||
app.set('port', process.env.PORT || 3000);
|
app.set('port', process.env.PORT || 3000);
|
||||||
app.set('views', path.join(__dirname, 'views'));
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
app.set('view engine', 'jade');
|
app.set('view engine', 'jade');
|
||||||
app.use(loopback.token());
|
app.use(loopback.token());
|
||||||
app.disable('x-powered-by');
|
app.disable('x-powered-by');
|
||||||
|
|
||||||
// adds passport initialization after session middleware phase is complete
|
|
||||||
passportConfigurator.init();
|
|
||||||
|
|
||||||
boot(app, {
|
boot(app, {
|
||||||
appRootDir: __dirname,
|
appRootDir: __dirname,
|
||||||
dev: process.env.NODE_ENV
|
dev: process.env.NODE_ENV
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setupPassport(app);
|
||||||
passportConfigurator.setupModels({
|
|
||||||
userModel: app.models.user,
|
|
||||||
userIdentityModel: app.models.userIdentity,
|
|
||||||
userCredentialModel: app.models.userCredential
|
|
||||||
});
|
|
||||||
|
|
||||||
var passportOptions = {
|
|
||||||
emailOptional: true,
|
|
||||||
profileToUser: function(provider, profile) {
|
|
||||||
var emails = profile.emails;
|
|
||||||
// NOTE(berks): get email or set to null.
|
|
||||||
// MongoDB indexs email but can be sparse(blank)
|
|
||||||
var email = emails && emails[0] && emails[0].value ?
|
|
||||||
emails[0].value :
|
|
||||||
null;
|
|
||||||
|
|
||||||
// create random username
|
|
||||||
// username will be assigned when camper signups for GitHub
|
|
||||||
var username = 'fcc' + uuid.v4().slice(0, 8);
|
|
||||||
var password = generateKey('password');
|
|
||||||
var userObj = {
|
|
||||||
username: username,
|
|
||||||
password: password
|
|
||||||
};
|
|
||||||
|
|
||||||
if (email) {
|
|
||||||
userObj.email = email;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(/github/).test(provider)) {
|
|
||||||
userObj[getSocialProvider(provider)] = getUsernameFromProvider(
|
|
||||||
getSocialProvider(provider),
|
|
||||||
profile
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/github/.test(provider)) {
|
|
||||||
setProfileFromGithub(userObj, profile, profile._json);
|
|
||||||
}
|
|
||||||
return userObj;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.keys(passportProviders).map(function(strategy) {
|
|
||||||
var config = passportProviders[strategy];
|
|
||||||
config.session = config.session !== false;
|
|
||||||
passportConfigurator.configureProvider(
|
|
||||||
strategy,
|
|
||||||
assign(config, passportOptions)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.start = _.once(function() {
|
app.start = _.once(function() {
|
||||||
app.listen(app.get('port'), function() {
|
app.listen(app.get('port'), function() {
|
||||||
|
48
server/views/news/index.jade
Normal file
48
server/views/news/index.jade
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
extends ../layout
|
||||||
|
block content
|
||||||
|
h1.text-center Camper News
|
||||||
|
hr
|
||||||
|
.spacer
|
||||||
|
.row
|
||||||
|
.col-xs-12
|
||||||
|
h2 We have discontinued Camper News in favor of our
|
||||||
|
a(href='http://reddit.com/r/freecodecamp')  Subreddit
|
||||||
|
| .
|
||||||
|
h3 Thank you to all of the campers who have contributed links over the past year. Our
|
||||||
|
a(href='http://reddit.com/r/freecodecamp')  Subreddit
|
||||||
|
|   is now the best place to share coding-related links.
|
||||||
|
.spacer
|
||||||
|
hr
|
||||||
|
#search-results
|
||||||
|
.spacer
|
||||||
|
#story-list
|
||||||
|
ul#stories
|
||||||
|
.spacer
|
||||||
|
h3.row
|
||||||
|
.col-xs-2.col-sm-1
|
||||||
|
a(href="/" + author.username)
|
||||||
|
img(src="#{author.picture}", class='img-news')
|
||||||
|
.col-xs-10.col-sm-10
|
||||||
|
.col-xs-12.negative-28
|
||||||
|
a(href="#{link}", target="_blank")
|
||||||
|
h3= title
|
||||||
|
h6
|
||||||
|
.col-xs-12.positive-15.hidden-element#image-display
|
||||||
|
.media
|
||||||
|
.media-left
|
||||||
|
img.url-preview.media-object(src="#{image}", alt="#{storyMetaDescription}")
|
||||||
|
.media-body
|
||||||
|
.col-xs-12.col-sm-12.col-md-6
|
||||||
|
h4= storyMetaDescription
|
||||||
|
.col-xs-12
|
||||||
|
.spacer
|
||||||
|
span#storyRank= rank + (rank > 1 ? " points" : " point")
|
||||||
|
|  · 
|
||||||
|
span Posted #{timeAgo}
|
||||||
|
span  by 
|
||||||
|
a(href="/" + author.username) @#{author.username}
|
||||||
|
|
||||||
|
script.
|
||||||
|
if (image) {
|
||||||
|
$('#image-display').removeClass('hidden-element')
|
||||||
|
}
|
@ -28,7 +28,7 @@ nav.navbar.navbar-default.navbar-fixed-top.nav-height
|
|||||||
a(href='/login') Sign in
|
a(href='/login') Sign in
|
||||||
else
|
else
|
||||||
li.brownie-points-nav
|
li.brownie-points-nav
|
||||||
a(href='/' + user.username) [ #{user.progressTimestamps.length} ]
|
a(href='/' + user.username) [ #{user.points} ]
|
||||||
li.hidden-xs.hidden-sm.avatar
|
li.hidden-xs.hidden-sm.avatar
|
||||||
a(href='/' + user.username)
|
a(href='/' + user.username)
|
||||||
img.profile-picture.float-right(src='#{user.picture}')
|
img.profile-picture.float-right(src='#{user.picture}')
|
||||||
|
@ -1,90 +0,0 @@
|
|||||||
.spacer
|
|
||||||
.spacer
|
|
||||||
#story-list.story-list
|
|
||||||
script.
|
|
||||||
var getLinkedName = function getLinkedName(name) {
|
|
||||||
return name.trim().toLowerCase().replace(/\s/g, '-');
|
|
||||||
}
|
|
||||||
$.ajax({
|
|
||||||
url: '/news/hot',
|
|
||||||
type: 'GET'
|
|
||||||
})
|
|
||||||
.success(
|
|
||||||
function(data) {
|
|
||||||
for (var i = 0; i < data.length; i++) {
|
|
||||||
var div = document.createElement('div');
|
|
||||||
var linkedName = getLinkedName(data[i].storyLink);
|
|
||||||
var rank = data[i].rank;
|
|
||||||
var alreadyUpvoted = false;
|
|
||||||
if (typeof username !== 'undefined') {
|
|
||||||
alreadyUpvoted = data[i].upVotes.some(function(vote) {
|
|
||||||
return vote.upVotedByUsername === username
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$(div)
|
|
||||||
.html(
|
|
||||||
"<div class='visible-xs row'>" +
|
|
||||||
"<div class='visible-xs col-sm-1 col-md-1'>" +
|
|
||||||
"<a href='" + data[i].link + "'>" +
|
|
||||||
"<img class='mobile-story-image img-responsive' src='" + (!!data[i].image ? data[i].image : data[i].author.picture) + "'/>" +
|
|
||||||
"</a>" +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='col-xs-12 mobile-story-headline text-center'>" +
|
|
||||||
"<a href='" + data[i].link + "' target='_blank'>" +
|
|
||||||
data[i].headline +
|
|
||||||
"</a>" +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='col-xs-12 text-center'>" +
|
|
||||||
rank + (rank > 1 ? " points" : " point") + " · posted " +
|
|
||||||
moment(data[i].timePosted).fromNow() +
|
|
||||||
" by <a href='/" + data[i].author.username + "'>@" + data[i].author.username + "</a> " +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='col-xs-12'>" +
|
|
||||||
"<br>" +
|
|
||||||
(typeof username !== 'undefined' ?
|
|
||||||
"<button id='" + data[i].id + "' class='btn btn-no-shadow btn-primary btn-block btn-primary-ghost btn-upvote'>upvote</button>" :
|
|
||||||
"<a href='/signin' class='btn btn-no-shadow btn-primary btn-block btn-primary-ghost btn-upvote'>upvote</a>") +
|
|
||||||
"<a class='btn btn-no-shadow btn-primary btn-block btn-primary-ghost hidden' href='/news/" + linkedName + "'>more info</a>" +
|
|
||||||
"</div>" +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='hidden-xs row media-stories'>" +
|
|
||||||
"<div class='media'>" +
|
|
||||||
"<div class='media-left'>" +
|
|
||||||
"<a href='/" + data[i].author.username + "'>" +
|
|
||||||
"<img class='img-news' src='" + data[i].author.picture + "'/>" +
|
|
||||||
"</a>" +
|
|
||||||
"</div>" +
|
|
||||||
"<h2 class='media-body'>" +
|
|
||||||
"<div class='media-body-wrapper'>" +
|
|
||||||
"<div class='story-headline'>" +
|
|
||||||
"<a href='" + data[i].link + "' target='_blank'>" +
|
|
||||||
data[i].headline +
|
|
||||||
"</a>" +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='story-byline col-xs-12 wrappable'>" +
|
|
||||||
(typeof username !== 'undefined' ?
|
|
||||||
"<button id='" + data[i].id + "' class='btn btn-no-shadow btn-primary btn-xs btn-primary-ghost btn-upvote'>upvote</button>" :
|
|
||||||
"<a href='/signin' class='btn btn-no-shadow btn-primary btn-xs btn-primary-ghost'>upvote</a>") +
|
|
||||||
"<a class='btn btn-no-shadow btn-primary btn-xs btn-primary-ghost hidden' href='/news/" + linkedName + "'>more info</a> " +
|
|
||||||
rank + (rank > 1 ? " points" : " point") + " · posted " +
|
|
||||||
moment(data[i].timePosted).fromNow() +
|
|
||||||
" by <a href='/" + data[i].author.username + "'>@" + data[i].author.username + "</a> " +
|
|
||||||
"</div>" +
|
|
||||||
"</div>" +
|
|
||||||
"</h2>" +
|
|
||||||
"</div>" +
|
|
||||||
"</div>"
|
|
||||||
);
|
|
||||||
$(div).addClass('story-list news-box')
|
|
||||||
$(div).appendTo($('#story-list'));
|
|
||||||
$(div).find('.btn-upvote').each(function(idx, btn) {
|
|
||||||
var $btn = $(btn);
|
|
||||||
if (alreadyUpvoted) {
|
|
||||||
$btn.addClass('disabled');
|
|
||||||
$btn.text('upvoted!');
|
|
||||||
}
|
|
||||||
$btn.data('upVotes', data[i].upVotes);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,32 +0,0 @@
|
|||||||
extends ../layout
|
|
||||||
block content
|
|
||||||
if (user)
|
|
||||||
script.
|
|
||||||
var isLoggedIn = true;
|
|
||||||
var userId = !{JSON.stringify(user.id)};
|
|
||||||
var username = !{JSON.stringify(user.username)};
|
|
||||||
else
|
|
||||||
script.
|
|
||||||
var isLoggedIn = false;
|
|
||||||
script.
|
|
||||||
var challengeName = 'Camper News';
|
|
||||||
var page = !{JSON.stringify(page)};
|
|
||||||
h1.text-center Camper News
|
|
||||||
hr
|
|
||||||
.spacer
|
|
||||||
include news-nav
|
|
||||||
.spacer
|
|
||||||
if (page === 'hot')
|
|
||||||
include hot-stories
|
|
||||||
if (page === 'submit')
|
|
||||||
if (user)
|
|
||||||
include preliminary-submit
|
|
||||||
else
|
|
||||||
.spacer
|
|
||||||
.text-center
|
|
||||||
a.btn.btn-cta.signup-btn.btn-primary(href="/login") Sign in to post your story (it's free)
|
|
||||||
.spacer
|
|
||||||
if (page === 'storySubmission')
|
|
||||||
include submit-story
|
|
||||||
if (page === 'show')
|
|
||||||
include show
|
|
@ -1,127 +0,0 @@
|
|||||||
.row
|
|
||||||
.col-xs-12
|
|
||||||
h2 We are retiring Camper News in favor of our
|
|
||||||
a(href='http://reddit.com/r/freecodecamp') Subreddit
|
|
||||||
| .
|
|
||||||
h3 Thank you to all of the campers who have contributed links over the past year. We will keep Camper News accessible until May. Our
|
|
||||||
a(href='http://reddit.com/r/freecodecamp') Subreddit
|
|
||||||
|   is now the best place to share coding-related links.
|
|
||||||
.spacer
|
|
||||||
hr
|
|
||||||
.spacer
|
|
||||||
.col-xs-12.col-sm-3
|
|
||||||
span
|
|
||||||
a.btn.btn-primary.btn-bigger.btn-block.btn-responsive(href='/stories/submit' class="#{ page === 'hot' ? '' : 'hidden' }") Submit a link
|
|
||||||
span
|
|
||||||
a.btn.btn-success.btn-bigger.btn-block.btn-responsive(href='/news/' class="#{ (page !== 'hot') ? '' : 'hidden' }") All
|
|
||||||
.visible-xs
|
|
||||||
.button-spacer
|
|
||||||
.col-xs-12.col-sm-9
|
|
||||||
.input-group
|
|
||||||
input#searchArea.big-text-field.field-responsive.form-control(type='text', placeholder='Search term or @username')
|
|
||||||
span.input-group-btn
|
|
||||||
button#searchbutton.btn.btn-bigger.btn-primary.btn-responsive(type='button') Search
|
|
||||||
.spacer
|
|
||||||
|
|
||||||
#search-results
|
|
||||||
|
|
||||||
.spacer
|
|
||||||
#story-list
|
|
||||||
ul#stories
|
|
||||||
script.
|
|
||||||
$('#searchArea').keypress(function (event) {
|
|
||||||
if (event.keyCode === 13 || event.which === 13) {
|
|
||||||
executeSearch();
|
|
||||||
$('#searchArea').focus();
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$('#searchbutton').on('click', function () {
|
|
||||||
executeSearch();
|
|
||||||
});
|
|
||||||
function executeSearch() {
|
|
||||||
$('#stories').empty();
|
|
||||||
var searchTerm = $('#searchArea').val(),
|
|
||||||
url = '/stories/search';
|
|
||||||
if (searchTerm.match(/^\@\w+$/)) {
|
|
||||||
url = '/news/userstories';
|
|
||||||
searchTerm = searchTerm.match(/^\@\w+$/)[0].split('@')[1];
|
|
||||||
}
|
|
||||||
var getLinkedName = function getLinkedName(name) {
|
|
||||||
return name.toLowerCase().replace(/\s/g, '-');
|
|
||||||
}
|
|
||||||
$.post(url, { search: searchTerm })
|
|
||||||
.fail(function(xhr, textStatus, errorThrown) {
|
|
||||||
$('#search-results').empty();
|
|
||||||
var div = document.createElement("div");
|
|
||||||
$(div).html("<h3 class='text-center text-warning dotted-underline'><em>No Results Found</em></h3>");
|
|
||||||
$(div).appendTo($('#search-results'));
|
|
||||||
})
|
|
||||||
.done(function(data, textStatus, xhr) {
|
|
||||||
$('#search-results').empty();
|
|
||||||
var spacer = document.createElement('div');
|
|
||||||
$(spacer).html("<div class='spacer'></div>");
|
|
||||||
$(spacer).appendTo($('#search-results'));
|
|
||||||
for (var i = 0; i < data.length; i++) {
|
|
||||||
var div = document.createElement('div');
|
|
||||||
var linkedName = getLinkedName(data[i].storyLink);
|
|
||||||
var rank = data[i].rank;
|
|
||||||
$(div).html(
|
|
||||||
"<div class='visible-xs row'>" +
|
|
||||||
"<div class='visible-xs col-sm-1 col-md-1'>" +
|
|
||||||
"<a href='" + data[i].link + "'>" +
|
|
||||||
"<img class='mobile-story-image img-responsive' src='" + (!!data[i].image ? data[i].image : data[i].author.picture) + "'/>" +
|
|
||||||
"</a>" +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='col-xs-12 mobile-story-headline text-center'>" +
|
|
||||||
"<a href='" + data[i].link + "' target='_blank'>" +
|
|
||||||
data[i].headline +
|
|
||||||
"</a>" +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='visible-xs'>" +
|
|
||||||
"<div class='col-xs-12 text-center'>" +
|
|
||||||
rank + (rank > 1 ? " points" : " point") + " · posted " +
|
|
||||||
moment(data[i].timePosted).fromNow() +
|
|
||||||
" by " +
|
|
||||||
"<a href='/" + data[i].author.username + "'>@" + data[i].author.username +
|
|
||||||
"</a> " +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='col-xs-12'>" +
|
|
||||||
"<br>" +
|
|
||||||
"<a class='btn btn-no-shadow btn-primary btn-block btn-primary-ghost' href='/news/" + linkedName + "'>more info</a>" +
|
|
||||||
"</div>" +
|
|
||||||
"</div>" +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='hidden-xs row media-stories'>" +
|
|
||||||
"<div class='media'>" +
|
|
||||||
"<div class='media-left'>" +
|
|
||||||
"<a href='/" + data[i].author.username + "'>" +
|
|
||||||
"<img class='img-news' src='" + data[i].author.picture + "'/>" +
|
|
||||||
"</a>" +
|
|
||||||
"</div>" +
|
|
||||||
"<h2 class='media-body'>" +
|
|
||||||
"<div class='media-body-wrapper'>" +
|
|
||||||
"<div class='story-headline'>" +
|
|
||||||
"<a href='" + data[i].link + "' target='_blank'>" +
|
|
||||||
data[i].headline +
|
|
||||||
"</a>" +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='story-byline col-xs-12 wrappable'>" +
|
|
||||||
"<a class='btn btn-no-shadow btn-primary btn-xs btn-primary-ghost' href='/news/" + linkedName + "'>more info</a> · " +
|
|
||||||
rank + (rank > 1 ? " points" : " point") + " · posted " +
|
|
||||||
moment(data[i].timePosted).fromNow() +
|
|
||||||
" by <a href='/" + data[i].author.username + "'>@" + data[i].author.username +
|
|
||||||
"</a> " +
|
|
||||||
"</div>" +
|
|
||||||
"</div>" +
|
|
||||||
"</h2>" +
|
|
||||||
"</div>" +
|
|
||||||
"</div>")
|
|
||||||
$(div).addClass('story-list news-box-search')
|
|
||||||
$(div).appendTo($('#search-results'));
|
|
||||||
}
|
|
||||||
var hr = document.createElement("div");
|
|
||||||
$(hr).html("<h3 class='text-center text-success dotted-underline'><em>End search results</em></h3>")
|
|
||||||
$(hr).appendTo($('#search-results'));
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
form.input-group(id='URLSubmit' name='URLSubmit')
|
|
||||||
input#story-url.big-text-field.field-responsive.form-control(placeholder='Paste your link here', name='link', type='url', required, autofocus)
|
|
||||||
span.input-group-btn
|
|
||||||
button#preliminary-story-submit.btn.btn-bigger.btn-primary.btn-responsive(type='submit') Submit
|
|
||||||
.spacer
|
|
||||||
|
|
||||||
script.
|
|
||||||
$('#story-url').on('keypress', function(e) {
|
|
||||||
if (e.keyCode === 13) {
|
|
||||||
$('#preliminary-story-submit').click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
function preliminaryStorySubmit(e) {
|
|
||||||
if (!$('#URLSubmit')[0].checkValidity()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
var storyURL = $('#story-url').val();
|
|
||||||
$('#preliminary-story-submit').attr('disabled', 'disabled');
|
|
||||||
|
|
||||||
$.post('/stories/preliminary',
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
url: storyURL
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.fail(function (xhr, textStatus, errorThrown) {
|
|
||||||
$('#preliminary-story-submit').attr('disabled', false);
|
|
||||||
})
|
|
||||||
.done(function (data, textStatus, xhr) {
|
|
||||||
if (data.alreadyPosted) {
|
|
||||||
window.location = data.storyURL;
|
|
||||||
} else {
|
|
||||||
window.location = '/stories/submit/new-story?url=' +
|
|
||||||
encodeURIComponent(data.storyURL) +
|
|
||||||
'&title=' + encodeURIComponent(data.storyTitle) +
|
|
||||||
'&image=' + encodeURIComponent(data.storyImage) +
|
|
||||||
'&description=' + encodeURIComponent(data.storyMetaDescription);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#preliminary-story-submit').on('click', preliminaryStorySubmit);
|
|
||||||
|
|
||||||
arr = $( "h3 input:checked" )
|
|
||||||
.map(function() {
|
|
||||||
return this.id;
|
|
||||||
})
|
|
||||||
.get()
|
|
||||||
.join('&');
|
|
@ -1,41 +0,0 @@
|
|||||||
script.
|
|
||||||
var storyId = !{JSON.stringify(id)};
|
|
||||||
var originalStoryLink = !{JSON.stringify(originalStoryLink)};
|
|
||||||
var upVotes = !{JSON.stringify(upVotes)};
|
|
||||||
var image = !{JSON.stringify(image)};
|
|
||||||
var hasUserVoted = !{JSON.stringify(hasUserVoted)};
|
|
||||||
|
|
||||||
h3.row
|
|
||||||
.col-xs-2.col-sm-1
|
|
||||||
a(href="/" + author.username)
|
|
||||||
img(src="#{author.picture}", class='img-news')
|
|
||||||
.col-xs-10.col-sm-10
|
|
||||||
.col-xs-12.negative-28
|
|
||||||
a(href="#{link}", target="_blank")
|
|
||||||
h3= title
|
|
||||||
h6
|
|
||||||
.col-xs-12.positive-15.hidden-element#image-display
|
|
||||||
.media
|
|
||||||
.media-left
|
|
||||||
img.url-preview.media-object(src="#{image}", alt="#{storyMetaDescription}")
|
|
||||||
.media-body
|
|
||||||
.col-xs-12.col-sm-12.col-md-6
|
|
||||||
h4= storyMetaDescription
|
|
||||||
.col-xs-12
|
|
||||||
.spacer
|
|
||||||
if !hasUserVoted
|
|
||||||
a#upvote.btn.btn-no-shadow.btn-primary.btn-xs.btn-primary-ghost Upvote
|
|
||||||
|  · 
|
|
||||||
else
|
|
||||||
a#upvote.btn.disabled.btn-no-shadow.btn-primary.btn-xs.btn-primary-ghost Upvoted!
|
|
||||||
|  · 
|
|
||||||
span#storyRank= rank + (rank > 1 ? " points" : " point")
|
|
||||||
|  · 
|
|
||||||
span Posted #{timeAgo}
|
|
||||||
span  by 
|
|
||||||
a(href="/" + author.username) @#{author.username}
|
|
||||||
|
|
||||||
script.
|
|
||||||
if (image) {
|
|
||||||
$('#image-display').removeClass('hidden-element')
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
.spacer
|
|
||||||
.col-xs-12
|
|
||||||
script.
|
|
||||||
var main = window.main || { init: [] };
|
|
||||||
main.storyURL = !{JSON.stringify(storyURL)};
|
|
||||||
main.storyTitle = !{JSON.stringify(storyTitle)};
|
|
||||||
main.storyImage = !{JSON.stringify(storyImage)};
|
|
||||||
main.storyMetaDescription = !{JSON.stringify(storyMetaDescription)};
|
|
||||||
|
|
||||||
form.form-horizontal.control-label-story-submission#story-submission-form(name="submitStory")
|
|
||||||
.col-xs-12
|
|
||||||
.form-group
|
|
||||||
.col-xs-12.col-md-1
|
|
||||||
label.control-label.control-label-story-submission(for='name') Link
|
|
||||||
.col-xs-12.col-md-11
|
|
||||||
input#story-url.form-control(name='Link', disabled, value='#{storyURL}')
|
|
||||||
.form-group
|
|
||||||
.col-xs-12.col-md-1
|
|
||||||
label.control-label.control-label-story-submission(for='name') Title
|
|
||||||
.col-xs-12.col-md-11
|
|
||||||
input#story-title.form-control(value='#{storyTitle}', name='Title', maxlength='90', autocomplete="off", autofocus, required)
|
|
||||||
.form-group
|
|
||||||
.col-xs-12.col-md-offset-1
|
|
||||||
span.pull-left#textarea_feedback
|
|
||||||
.form-group
|
|
||||||
.col-xs-11.col-md-offset-1
|
|
||||||
.hidden-element#image-display
|
|
||||||
.media
|
|
||||||
.media-left
|
|
||||||
img.url-preview.media-object(src="#{storyImage}", alt="#{storyMetaDescription}")
|
|
||||||
.media-body
|
|
||||||
.col-xs-12
|
|
||||||
p= storyMetaDescription
|
|
||||||
.spacer
|
|
||||||
.row
|
|
||||||
.form-group
|
|
||||||
button.btn.btn-bigger.btn-block.btn-primary#story-submit(type='submit', onclick="return false;") Submit
|
|
||||||
script.
|
|
||||||
if (main.storyImage) {
|
|
||||||
$('#image-display').removeClass('hidden-element');
|
|
||||||
}
|
|
Reference in New Issue
Block a user