@ -124,6 +124,30 @@ main = (function(main, global) {
|
||||
Mousetrap.bind('g c', toggleMainChat);
|
||||
});
|
||||
|
||||
function localStorageIO(item = '', input = null) {
|
||||
if (input) {
|
||||
try {
|
||||
input = typeof input === 'string' ? input : JSON.stringify(input);
|
||||
} catch (e) {
|
||||
// Do Nothing
|
||||
}
|
||||
localStorage.setItem(item, input);
|
||||
return input;
|
||||
} else {
|
||||
let data = typeof localStorage.getItem(item)
|
||||
!== 'undefined' && localStorage.getItem(item)
|
||||
!== null ? localStorage.getItem(item) : '';
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
// Do Nothing
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
main.localStorageIO = localStorageIO;
|
||||
|
||||
return main;
|
||||
}(main, window));
|
||||
|
||||
@ -612,4 +636,49 @@ $(document).ready(function() {
|
||||
// Repo
|
||||
window.location = 'https://github.com/freecodecamp/freecodecamp/';
|
||||
});
|
||||
|
||||
function getCurrentBillBoard(cb) {
|
||||
$.ajax({
|
||||
url: '/api/flyers/findOne?'
|
||||
+ 'filter=%7B%22order%22%3A%20%20%22id%20DESC%22%7D',
|
||||
method: 'GET',
|
||||
dataType: 'JSON',
|
||||
data: {'order': 'id DESC'}
|
||||
}).done((resp) => {
|
||||
cb(resp);
|
||||
});
|
||||
$('#dismissBill').on('click', (e) => {
|
||||
const elemData
|
||||
= e.target.parentNode.parentNode.children;
|
||||
|
||||
const res
|
||||
= elemData[Object.keys(elemData).filter((key)=> {
|
||||
return elemData[key].id === 'billContent';
|
||||
|
||||
})[0]].innerHTML;
|
||||
|
||||
main.localStorageIO('lastBillBoardSeen', res);
|
||||
});
|
||||
}
|
||||
|
||||
function handleNewBillBoard(resp) {
|
||||
const data = typeof main.localStorageIO('lastBillBoardSeen')
|
||||
!== 'undefined' && main.localStorageIO('lastBillBoardSeen')
|
||||
!== null ? main.localStorageIO('lastBillBoardSeen') : '';
|
||||
if (
|
||||
data.replace(/\s*/gi, '')
|
||||
.replace(/\&\w*\;/gi, '')
|
||||
.replace(/(\<|\/|\>)/gi, '')
|
||||
!== resp.message
|
||||
.replace(/\s*/gi, '')
|
||||
.replace(/\&\w*\;/gi, '')
|
||||
.replace(/(\<|\/|\>)/gi, '')
|
||||
&& resp.active
|
||||
) {
|
||||
$('#billContent').html(resp.message);
|
||||
$('#billBoard').fadeIn();
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentBillBoard(handleNewBillBoard);
|
||||
});
|
||||
|
34
common/models/flyer.json
Normal file
34
common/models/flyer.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "flyer",
|
||||
"base": "PersistedModel",
|
||||
"idInjection": true,
|
||||
"trackChanges": false,
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"validations": [],
|
||||
"relations": {
|
||||
|
||||
},
|
||||
"acls": [
|
||||
{
|
||||
"accessType": "*",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "DENY"
|
||||
},
|
||||
{
|
||||
"accessType": "READ",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW"
|
||||
}
|
||||
],
|
||||
"methods": []
|
||||
}
|
@ -1,26 +1,26 @@
|
||||
import { Disposable, Observable, Scheduler } from 'rx';
|
||||
import { Observable } from 'rx';
|
||||
import uuid from 'node-uuid';
|
||||
import moment from 'moment';
|
||||
import dedent from 'dedent';
|
||||
import debug from 'debug';
|
||||
import debugFactory from 'debug';
|
||||
|
||||
import { saveUser, observeMethod } from '../../server/utils/rx';
|
||||
import { blacklistedUsernames } from '../../server/utils/constants';
|
||||
|
||||
const log = debug('fcc:user:remote');
|
||||
const debug = debugFactory('fcc:user:remote');
|
||||
const BROWNIEPOINTS_TIMEOUT = [1, 'hour'];
|
||||
|
||||
const aboutFilter = {
|
||||
username: true,
|
||||
bio: true
|
||||
};
|
||||
function getAboutProfile({
|
||||
username,
|
||||
bio,
|
||||
points
|
||||
githubProfile: github,
|
||||
progressTimestamps = [],
|
||||
bio
|
||||
}) {
|
||||
return {
|
||||
username,
|
||||
bio,
|
||||
browniePoints: points
|
||||
github,
|
||||
browniePoints: progressTimestamps.length,
|
||||
bio
|
||||
};
|
||||
}
|
||||
|
||||
@ -54,31 +54,8 @@ module.exports = function(User) {
|
||||
|
||||
User.on('dataSourceAttached', () => {
|
||||
User.findOne$ = Observable.fromNodeCallback(User.findOne, User);
|
||||
User.findById$ = Observable.fromNodeCallback(User.findById, User);
|
||||
User.update$ = Observable.fromNodeCallback(User.updateAll, User);
|
||||
User.count$ = Observable.fromNodeCallback(User.count, User);
|
||||
// getPointsById$(_id: String|ObjectId) => Observable[Number]
|
||||
User.getPointsById$ = function getPointsById$(id) {
|
||||
return Observable.create(observer => {
|
||||
let isDisposed = false;
|
||||
// safe ObjectID creation
|
||||
// MongoID(id: ObjectID|String) => ObjectID
|
||||
// MongoDB requires id's to be of type ObjectID
|
||||
const _id = this.app.dataSources.db.connector.getDefaultIdType()(id);
|
||||
this.app.dataSources.db.connector
|
||||
.collection('user')
|
||||
.aggregate([
|
||||
{ $match: { _id } },
|
||||
{ $project: { points: { $size: '$progressTimestamps' } } }
|
||||
], (err, [ { points = 1 } = {}]) => {
|
||||
if (isDisposed) { return null; }
|
||||
if (err) { return observer.onError(err); }
|
||||
observer.onNext(points);
|
||||
return observer.onCompleted();
|
||||
});
|
||||
return Disposable.create(() => { isDisposed = true; });
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
User.observe('before save', function({ instance: user }, next) {
|
||||
@ -99,7 +76,7 @@ module.exports = function(User) {
|
||||
next();
|
||||
});
|
||||
|
||||
log('setting up user hooks');
|
||||
debug('setting up user hooks');
|
||||
User.afterRemote('confirm', function(ctx) {
|
||||
ctx.req.flash('success', {
|
||||
msg: [
|
||||
@ -151,9 +128,9 @@ module.exports = function(User) {
|
||||
}
|
||||
|
||||
// the email of the requested user
|
||||
log(info.email);
|
||||
debug(info.email);
|
||||
// the temp access token to allow password reset
|
||||
log(info.accessToken.id);
|
||||
debug(info.accessToken.id);
|
||||
// requires AccessToken.belongsTo(User)
|
||||
var mailOptions = {
|
||||
to: info.email,
|
||||
@ -173,7 +150,7 @@ module.exports = function(User) {
|
||||
|
||||
User.app.models.Email.send(mailOptions, function(err) {
|
||||
if (err) { console.error(err); }
|
||||
log('email reset sent');
|
||||
debug('email reset sent');
|
||||
});
|
||||
});
|
||||
|
||||
@ -196,7 +173,7 @@ module.exports = function(User) {
|
||||
};
|
||||
|
||||
if (accessToken && accessToken.id) {
|
||||
log('setting cookies');
|
||||
debug('setting cookies');
|
||||
res.cookie('access_token', accessToken.id, config);
|
||||
res.cookie('userId', accessToken.userId, config);
|
||||
}
|
||||
@ -204,7 +181,7 @@ module.exports = function(User) {
|
||||
return req.logIn({ id: accessToken.userId.toString() }, function(err) {
|
||||
if (err) { return next(err); }
|
||||
|
||||
log('user logged in');
|
||||
debug('user logged in');
|
||||
|
||||
if (req.session && req.session.returnTo) {
|
||||
var redirectTo = req.session.returnTo;
|
||||
@ -240,7 +217,7 @@ module.exports = function(User) {
|
||||
if (!username && !email) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
log('checking existence');
|
||||
debug('checking existence');
|
||||
|
||||
// check to see if username is on blacklist
|
||||
if (username && blacklistedUsernames.indexOf(username) !== -1) {
|
||||
@ -253,7 +230,7 @@ module.exports = function(User) {
|
||||
} else {
|
||||
where.email = email ? email.toLowerCase() : email;
|
||||
}
|
||||
log('where', where);
|
||||
debug('where', where);
|
||||
return User.count(where)
|
||||
.then(count => count > 0);
|
||||
};
|
||||
@ -294,23 +271,16 @@ module.exports = function(User) {
|
||||
));
|
||||
});
|
||||
}
|
||||
username = username.toLowerCase();
|
||||
const filter = {
|
||||
where: { username },
|
||||
fields: { id: true, ...aboutFilter }
|
||||
};
|
||||
return User.findOne$(filter)
|
||||
.doOnNext(user => {
|
||||
if (!user || user.username !== username) {
|
||||
throw new Error(`no user found for ${ username }`);
|
||||
}
|
||||
})
|
||||
.flatMap(user => user.getPoints$().map(user))
|
||||
.map(user => getAboutProfile(user))
|
||||
.subscribe(
|
||||
aboutUser => cb(null, aboutUser),
|
||||
cb
|
||||
);
|
||||
return User.findOne({ where: { username } }, (err, user) => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (!user || user.username !== username) {
|
||||
return cb(new Error(`no user found for ${ username }`));
|
||||
}
|
||||
const aboutUser = getAboutProfile(user);
|
||||
return cb(null, aboutUser);
|
||||
});
|
||||
};
|
||||
|
||||
User.remoteMethod(
|
||||
@ -338,8 +308,7 @@ module.exports = function(User) {
|
||||
|
||||
User.giveBrowniePoints =
|
||||
function giveBrowniePoints(receiver, giver, data = {}, dev = false, cb) {
|
||||
receiver = receiver.toLowerCase();
|
||||
giver = giver.toLowerCase();
|
||||
const findUser = observeMethod(User, 'findOne');
|
||||
if (!receiver) {
|
||||
return nextTick(() => {
|
||||
cb(
|
||||
@ -352,84 +321,63 @@ module.exports = function(User) {
|
||||
cb(new TypeError(`giver should be a string but got ${ giver }`));
|
||||
});
|
||||
}
|
||||
if (giver === receiver) {
|
||||
return nextTick(() => {
|
||||
cb(new Error('giver and receiver must be different users'));
|
||||
});
|
||||
}
|
||||
let temp = moment();
|
||||
const browniePoints = temp
|
||||
.subtract.apply(temp, BROWNIEPOINTS_TIMEOUT)
|
||||
.valueOf();
|
||||
const user$ = User.findOne$({
|
||||
where: { username: receiver },
|
||||
fields: {
|
||||
...aboutFilter,
|
||||
progressTimestamps: true
|
||||
}
|
||||
});
|
||||
const giver$ = User.count$({ username: giver });
|
||||
return Observable.combineLatest(
|
||||
user$,
|
||||
giver$,
|
||||
(user, giver) => ({ doesGiverExist: !!giver, user })
|
||||
)
|
||||
.tapOnNext(({ user, doesGiverExist }) => {
|
||||
const user$ = findUser({ where: { username: receiver }});
|
||||
|
||||
return user$
|
||||
.tapOnNext((user) => {
|
||||
if (!user) {
|
||||
throw new Error(`could not find receiver for ${ receiver }`);
|
||||
}
|
||||
if (!doesGiverExist) {
|
||||
throw new Error(`no user found for giver '${giver}'`);
|
||||
}
|
||||
})
|
||||
.flatMap(({ progressTimestamps = [] }) => {
|
||||
return Observable.from(
|
||||
progressTimestamps,
|
||||
null,
|
||||
null,
|
||||
Scheduler.default
|
||||
);
|
||||
return Observable.from(progressTimestamps);
|
||||
})
|
||||
// filter out non objects
|
||||
.filter((timestamp) => !!timestamp || typeof timestamp === 'object')
|
||||
// filterout timestamps older then an hour
|
||||
.filter(({ timestamp = 0 }) => timestamp >= browniePoints)
|
||||
.filter(({ timestamp = 0 }) => {
|
||||
return timestamp >= browniePoints;
|
||||
})
|
||||
// filter out brownie points given by giver
|
||||
.filter(browniePoint => browniePoint.giver === giver)
|
||||
.filter((browniePoint) => {
|
||||
return browniePoint.giver === giver;
|
||||
})
|
||||
// no results means this is the first brownie point given by giver
|
||||
// so return -1 to indicate receiver should receive point
|
||||
.first({ defaultValue: -1 })
|
||||
.flatMap(browniePointsFromGiver => {
|
||||
.flatMap((browniePointsFromGiver) => {
|
||||
if (browniePointsFromGiver === -1) {
|
||||
const updateData = {
|
||||
$push: {
|
||||
progressTimestamps: {
|
||||
giver,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
}
|
||||
};
|
||||
return user$
|
||||
.flatMap(user => user.update$(updateData).map(user))
|
||||
.doOnNext(user => {
|
||||
user.points = user.progressTimestamps.length + 1;
|
||||
|
||||
return user$.flatMap((user) => {
|
||||
user.progressTimestamps.push({
|
||||
giver,
|
||||
timestamp: Date.now(),
|
||||
...data
|
||||
});
|
||||
return saveUser(user);
|
||||
});
|
||||
}
|
||||
return Observable.throw(
|
||||
new Error(`${ giver } already gave ${ receiver } points`)
|
||||
);
|
||||
})
|
||||
.subscribe(
|
||||
user => cb(
|
||||
null,
|
||||
getAboutProfile(user),
|
||||
dev ?
|
||||
{ giver, receiver, data } :
|
||||
null
|
||||
),
|
||||
e => cb(e, null, dev ? { giver, receiver, data } : null),
|
||||
(user) => {
|
||||
return cb(
|
||||
null,
|
||||
getAboutProfile(user),
|
||||
dev ?
|
||||
{ giver, receiver, data } :
|
||||
null
|
||||
);
|
||||
},
|
||||
(e) => cb(e, null, dev ? { giver, receiver, data } : null),
|
||||
() => {
|
||||
log('brownie points assigned completed');
|
||||
debug('brownie points assigned completed');
|
||||
}
|
||||
);
|
||||
};
|
||||
@ -475,7 +423,7 @@ module.exports = function(User) {
|
||||
}
|
||||
);
|
||||
|
||||
// user.update$(updateData: Object) => Observable[Number]
|
||||
// user.updateTo$(updateData: Object) => Observable[Number]
|
||||
User.prototype.update$ = function update$(updateData) {
|
||||
const id = this.getId();
|
||||
const updateOptions = { allowExtendedOperators: true };
|
||||
@ -493,7 +441,7 @@ module.exports = function(User) {
|
||||
}
|
||||
return this.constructor.update$({ id }, updateData, updateOptions);
|
||||
};
|
||||
User.prototype.getTimestamps = function getTimestamps() {
|
||||
User.prototype.getPoints$ = function getPoints$() {
|
||||
const id = this.getId();
|
||||
const filter = {
|
||||
where: { id },
|
||||
@ -517,11 +465,4 @@ module.exports = function(User) {
|
||||
return user.challengeMap;
|
||||
});
|
||||
};
|
||||
// user.getPoints$() => Observable[Number]
|
||||
User.prototype.getPoints$ = function getPoints$() {
|
||||
const id = this.getId();
|
||||
return this.constructor
|
||||
.getPointsById$(id)
|
||||
.doOnNext(points => { this.points = points; });
|
||||
};
|
||||
};
|
||||
|
@ -585,7 +585,7 @@
|
||||
"</div>"
|
||||
],
|
||||
"tests": [
|
||||
"assert.isTrue((/<em>#target4<\\/em>/gi).test($(\"#target4\").html()), 'message: Italicize the text in your <code>target4</code> button by adding HTML tags.');",
|
||||
"assert.isTrue((/<em>#target4<\\/em>/gi).test($(\"#target4\").html()), 'message: Emphasize the text in your <code>target4</code> button by adding HTML tags.');",
|
||||
"assert($(\"#target4\") && $(\"#target4\").text() === '#target4', 'message: Make sure the text is otherwise unchanged.');",
|
||||
"assert.isFalse((/<em>/gi).test($(\"h3\").html()), 'message: Do not alter any other text.');",
|
||||
"assert(code.match(/\\.html\\(/g), 'message: Make sure you are using <code>.html()</code> and not <code>.text()</code>.');"
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { Observable } from 'rx';
|
||||
import passport from 'passport';
|
||||
import { PassportConfigurator } from 'loopback-component-passport';
|
||||
import passportProviders from './passport-providers';
|
||||
@ -71,21 +70,22 @@ PassportConfigurator.prototype.init = function passportInit(noSession) {
|
||||
});
|
||||
|
||||
passport.deserializeUser((id, done) => {
|
||||
Observable.combineLatest(
|
||||
this.userModel.findById$(id, { fields }),
|
||||
this.userModel.getPointsById$(id),
|
||||
(user, points) => {
|
||||
if (user) { user.points = points; }
|
||||
return user;
|
||||
|
||||
this.userModel.findById(id, { fields }, (err, user) => {
|
||||
if (err || !user) {
|
||||
return done(err, user);
|
||||
}
|
||||
)
|
||||
.doOnNext(user => {
|
||||
if (!user) { throw new Error('deserialize found no user'); }
|
||||
})
|
||||
.subscribe(
|
||||
user => done(null, user),
|
||||
done
|
||||
);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -66,5 +66,9 @@
|
||||
"userIdentity": {
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
},
|
||||
"flyer": {
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
extends ../layout
|
||||
block content
|
||||
include ../partials/flyer
|
||||
script(src="/bower_components/cal-heatmap/cal-heatmap.min.js")
|
||||
script.
|
||||
var challengeName = 'Profile View';
|
||||
@ -170,4 +171,4 @@ block content
|
||||
if (challenge.solution)
|
||||
a(href='/challenges/' + removeOldTerms(challenge.name) + '?solution=' + encodeURIComponent(encodeFcc(challenge.solution)), target='_blank')= removeOldTerms(challenge.name)
|
||||
else
|
||||
a(href='/challenges/' + removeOldTerms(challenge.name))= removeOldTerms(challenge.name)
|
||||
a(href='/challenges/' + removeOldTerms(challenge.name))= removeOldTerms(challenge.name)
|
||||
|
@ -4,6 +4,7 @@ block content
|
||||
link(rel='stylesheet', href='/bower_components/CodeMirror/addon/lint/lint.css')
|
||||
link(rel='stylesheet', href='/bower_components/CodeMirror/theme/monokai.css')
|
||||
link(rel='stylesheet', href='/css/ubuntu.css')
|
||||
include ../partials/flyer
|
||||
.row
|
||||
.col-md-4.col-lg-3
|
||||
.scroll-locker(id = "scroll-locker")
|
||||
|
@ -4,6 +4,7 @@ block content
|
||||
link(rel='stylesheet', href='/bower_components/CodeMirror/addon/lint/lint.css')
|
||||
link(rel='stylesheet', href='/bower_components/CodeMirror/theme/monokai.css')
|
||||
link(rel='stylesheet', href='/css/ubuntu.css')
|
||||
include ../partials/flyer
|
||||
.row
|
||||
.col-md-3.col-lg-3
|
||||
.scroll-locker(id = "scroll-locker")
|
||||
|
@ -4,6 +4,7 @@ block content
|
||||
link(rel='stylesheet', href='/bower_components/CodeMirror/addon/lint/lint.css')
|
||||
link(rel='stylesheet', href='/bower_components/CodeMirror/theme/monokai.css')
|
||||
link(rel='stylesheet', href='/css/ubuntu.css')
|
||||
include ../partials/flyer
|
||||
.row
|
||||
.col-md-4.col-lg-3
|
||||
.scroll-locker(id = "scroll-locker")
|
||||
|
@ -1,5 +1,6 @@
|
||||
extends ../layout-wide
|
||||
block content
|
||||
include ../partials/flyer
|
||||
.row
|
||||
.col-md-8.col-md-offset-2
|
||||
for step, index in description
|
||||
|
@ -1,5 +1,6 @@
|
||||
extends ../layout-wide
|
||||
block content
|
||||
include ../partials/flyer
|
||||
.row
|
||||
.col-xs-12.col-sm-12.col-md-4
|
||||
h4.text-center.challenge-instructions-title= name
|
||||
|
@ -1,5 +1,6 @@
|
||||
extends ../layout-wide
|
||||
block content
|
||||
include ../partials/flyer
|
||||
.row
|
||||
.col-md-4
|
||||
h4.text-center.challenge-instructions-title= name
|
||||
|
@ -6,7 +6,7 @@ html(lang='en')
|
||||
body.top-and-bottom-margins
|
||||
include partials/scripts
|
||||
include partials/navbar
|
||||
include partials/flash
|
||||
.container
|
||||
include partials/flash
|
||||
block content
|
||||
include partials/footer
|
||||
|
@ -1,20 +1,21 @@
|
||||
.row.flashMessage
|
||||
.col-xs-12
|
||||
if (messages.errors || messages.error)
|
||||
.alert.alert-danger.fade.in
|
||||
button.close(type='button', data-dismiss='alert')
|
||||
span.ion-close-circled
|
||||
for error in (messages.errors || messages.error)
|
||||
div!= error.msg || error
|
||||
if messages.info
|
||||
.alert.alert-info.fade.in
|
||||
button.close(type='button', data-dismiss='alert')
|
||||
span.ion-close-circled
|
||||
for info in messages.info
|
||||
div!= info.msg
|
||||
if messages.success
|
||||
.alert.alert-success.fade.in
|
||||
button.close(type='button', data-dismiss='alert')
|
||||
span.ion-close-circled
|
||||
for success in messages.success
|
||||
div!= success.msg
|
||||
.container
|
||||
.row.flashMessage.negative-30
|
||||
.col-xs-12
|
||||
if (messages.errors || messages.error)
|
||||
.alert.alert-danger.fade.in
|
||||
button.close(type='button', data-dismiss='alert')
|
||||
span.ion-close-circled
|
||||
for error in (messages.errors || messages.error)
|
||||
div!= error.msg || error
|
||||
if messages.info
|
||||
.alert.alert-info.fade.in
|
||||
button.close(type='button', data-dismiss='alert')
|
||||
span.ion-close-circled
|
||||
for info in messages.info
|
||||
div!= info.msg
|
||||
if messages.success
|
||||
.alert.alert-success.fade.in
|
||||
button.close(type='button', data-dismiss='alert')
|
||||
span.ion-close-circled
|
||||
for success in messages.success
|
||||
div!= success.msg
|
||||
|
8
server/views/partials/flyer.jade
Normal file
8
server/views/partials/flyer.jade
Normal file
@ -0,0 +1,8 @@
|
||||
if (user && user.points > 5)
|
||||
.container
|
||||
.row.flashMessage.negative-30
|
||||
.col-xs-12
|
||||
#billBoard.alert.alert-info.fade.in(style="display: none;")
|
||||
button.close(type='button', data-dismiss='alert')
|
||||
span.ion-close-circled#dismissBill
|
||||
#billContent
|
Reference in New Issue
Block a user