init commit

master
Keith 2016-04-01 01:06:21 +02:00
commit ae45b2bd3e
73 changed files with 8768 additions and 0 deletions

22
LICENSE Normal file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2016 Keith Irwin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# Tracman
node.js application to display a map with user's location. Live at [tracman.org](https://tracman.org/).
The github for the associated android app is [Tracman-org/Android](https://github.com/tracman-org/android).

55
config/auth.js Normal file
View File

@ -0,0 +1,55 @@
var passport = require('passport'),
secret = require('./secrets.js'),
User = require('./models/user.js'),
GoogleStrategy = require('passport-google-oauth2').Strategy,
GoogleTokenStrategy = require('passport-google-id-token');
passport.use(new GoogleStrategy({
clientID: secret.googleClientId,
clientSecret: secret.googleClientSecret,
callbackURL: secret.url+'/auth/google/callback',
passReqToCallback: true
}, function(req, accessToken, refreshToken, profile, done) {
User.findOne({googleID: profile.id}, function(err, user) {
if(err) {console.log(err);}
if (!err && user !== null) { // Log in
if (!user.name) {
user.name = profile.displayName;
}
user.lastLogin = Date.now();
user.save(function (err, raw) {
if (err) { console.log(err); }
});
done(null, user);
} else { // No existing user with google auth
if (req.session.passport) { // Creating new user
User.findById(req.session.passport.user, function(err, user){
user.googleID = profile.id;
user.lastLogin = Date.now();
user.save(function(err){
if (err) { console.log(err); }
done(null, user, {success: 'Your account has been created. Next maybe you should download the <a href="/android">android app</a>. '});
});
});
} else { // User wasn't invited
done(null,false, {error: 'User not found. Maybe you want to <a href="#" data-scrollto="get">request an invite</a>? '});
}
}
});
}));
passport.use(new GoogleTokenStrategy({
clientID: secret.googleClientId
}, function(parsedToken, googleId, done) {
User.findOne({googleID:googleId}, function(err, user) {
if (err) { console.log(err); }
if (!err && user !== null) { // Log in
user.lastLogin = Date.now();
user.save(function (err) {
if (err) { console.log(err); }
});
return done(err, user);
} else { // No such user
done(null, false);
}
});
}));

61
config/mail.js Normal file
View File

@ -0,0 +1,61 @@
var emailTemplate = require('email-templates').EmailTemplate,
path = require('path');
var secret = require('./secrets.js'),
templateDir = path.join(__dirname, '..', 'res', 'mail');
var mailgun = require('mailgun-js')({
apiKey: secret.mailgunAPI,
domain: 'tracman.org'
});
var renderMail = function(template, params, next) {
new emailTemplate(path.join(templateDir, template))
.render(params, function (err, msg) {
if (err) { console.log(err); }
next(msg);
})
}
module.exports = {
mailgun:mailgun,
sendInvite: function(params, cb){
renderMail('invite', {id:params.id, name:params.name}, function(msg) {
mailgun.messages().send({
from: 'Tracman <invites@tracman.org>',
to: params.email,
subject: 'You are invited to use Tracman beta!',
html: msg.html,
text: msg.text
}, cb);
});
},
sendSuggestion: function(params, cb){
renderMail('suggestion', params, function(msg) {
var name = (params.name)?params.name:'Tracman';
var email = (params.email)?' <'+params.email+'>':' <suggestions@tracman.org>';
mailgun.messages().send({
from: name+email,
to: 'Keith Irwin <suggestions@tracman.org>',
subject: 'A suggestion for Tracman',
html: msg.html,
text: msg.text
}, cb);
});
},
sendBugReport: function(params, cb){
renderMail('suggestion', params, function(msg) {
var name = (params.name)?params.name:'Tracman';
var email = (params.email)?' <'+params.email+'>':' <suggestions@tracman.org>';
mailgun.messages().send({
from: name+email,
to: 'Keith Irwin <bugs@tracman.org>',
subject: 'A Bug Report for Tracman',
html: msg.html,
text: msg.text
}, cb);
});
},
};

10
config/models/request.js Normal file
View File

@ -0,0 +1,10 @@
var mongoose = require('mongoose');
module.exports = mongoose.model('Request', {
name: {type:String, required:true},
email: {type:String, required:true, unique:true},
beg: String,
requestedTime: Date,
granted: Date,
userId: String
});

30
config/models/user.js Normal file
View File

@ -0,0 +1,30 @@
var mongoose = require('mongoose');
module.exports = mongoose.model('User', {
name: {type:String, required:true},
email: String,
slug: {type:String, required:true, unique:true},
requestId: String,
isAdmin: {type:Boolean, required:true, default:false},
isPro: {type:Boolean, required:true, default:false},
created: Date,
lastLogin: Date,
googleID: {type:Number, unique:true},
settings: {
units: {type:String, default:'imperial'},
defaultMap: {type:String, default:'road'},
defaultZoom: {type:Number, default:11},
showSpeed: {type:Boolean, default:false},
showTemp: {type:Boolean, default:false},
showAlt: {type:Boolean, default:false},
showStreetview: {type:Boolean, default:false}
},
last: {
time: Date,
lat: Number,
lon: Number,
dir: Number,
alt: Number,
spd: Number
}
});

349
config/routes.js Normal file
View File

@ -0,0 +1,349 @@
var app = require('express')(),
server = require('../server.js'),
User = require('./models/user.js'),
Request = require('./models/request.js'),
bodyParser = require('body-parser'),
slug = require('slug'),
secret = require('./secrets.js'),
passport = require('passport'),
mail = require('./mail.js');
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(bodyParser.json());
function throwErr(req,err) {
console.log(err);
req.flash('error-message',err);
req.flash('error', (err.message||'')+'<br>Would you like to <a href="/bug">report this error</a>?');
}
function ensureAuth(req,res,next) {
if (req.isAuthenticated()) { return next(); }
else {
req.session.returnTo = req.path;
req.flash('error', 'You must be signed in to do that. <a href="/login">Click here to log in</a>. ');
res.redirect('/');
}
}
function ensureAdmin(req,res,next) {
if (req.user.isAdmin) { return next(); }
else { res.sendStatus(401); }
}
module.exports = function(app){
app.get('/robots.txt', function(req,res){
res.type('text/plain');
res.send("User-agent: *\n"+
"Disallow: /trac\n"+
"Disallow: /dashboard\n"+
"Disallow: /invited"
);
});
app.route('/')
.all(function(req,res,next){
next();
}).get(function(req,res){
if (req.session.passport) {
User.findById(req.session.passport.user, function(err, user){
if (err){ throwErr(req,err); }
if (!user){ next(); }
res.render('index.html', {
user: user,
error: req.flash('error')[0],
success: req.flash('succcess')[0]
});
});
} else {
res.render('index.html', {
error: req.flash('error')[0],
success: req.flash('success')[0],
inviteSuccess: req.flash('request-success')[0],
inviteError: req.flash('request-error')[0]
});
}
}).post(function(req,res){
Request.findOne({email:req.body.email}, function(err, request) {
if (err){ throwErr(req,err); }
if (request){ // Already requested with this email
req.flash('request-error', 'Invite already requested! ');
res.redirect('/#get');
} else { // Send new request
request = new Request({
name: req.body.name,
email: req.body.email,
beg: req.body.why,
requestedTime: Date.now()
}); request.save(function(err) {
if (err){ throwErr(req,err); }
mail.mailgun.messages().send({
from: 'Tracman Requests <requests@tracman.org>',
to: 'Keith Irwin <tracman@keithirwin.us>',
subject: 'New Tracman Invite request',
html: '<p>'+req.body.name+' requested a Tracman invite. </p><p>'+req.body.why+'</p><p><a href="http://tracman.org/admin/requests">See all invites</a></p>',
text: '\n'+req.body.name+' requested a Tracman invite. \n\n'+req.body.why+'\n\nhttp://tracman.org/admin/requests'
}, function(err,body){
if (err){ throwErr(req,err); }
else { req.flash('request-success', 'Invite requested! '); }
res.redirect('/#get');
});
});
}
});
});
app.route('/dashboard')
.all(ensureAuth, function(req,res,next){
next();
}).get(function(req,res){
User.findById(req.session.passport.user, function(err, user){
if (err){ throwErr(req,err); }
if (!user){ next(); }
else if (req.session.returnTo && req.query.rd) {
res.redirect(req.session.returnTo);
delete req.session.returnTo;
} else { res.render('dashboard.html', {
user: user,
success: req.flash('success')[0],
error: req.flash('error')[0]
}); }
});
}).post(function(req,res){
User.findByIdAndUpdate(req.session.passport.user, {$set:{
name: req.body.name,
slug: slug(req.body.slug),
settings: {
units: req.body.units,
defaultMap: req.body.map,
defaultZoom: req.body.zoom,
showSpeed: (req.body.showSpeed)?true:false,
showAlt: (req.body.showAlt)?true:false,
showStreetview: (req.body.showStreet)?true:false
}
}}, function(err, user){
if (err) { throwErr(req,err); }
else { req.flash('success', 'Settings updated. '); }
res.redirect('/dashboard');
});
});
app.get('/validate', function(req,res){
if (req.query.slug) { // validate unique slug
User.findOne({slug:slug(req.query.slug)}, function(err, existingUser){
if (existingUser && existingUser.id!==req.session.passport.user) { res.sendStatus(400); }
else { res.sendStatus(200); }
});
}
});
app.get('/trac', ensureAuth, function(req,res,next){
User.findById(req.session.passport.user, function(err, user){
if (err){ throwErr(req,err); }
if (!user){ next(); }
else { res.redirect('/trac/'+user.slug+((req.url.indexOf('?')<0)?'':('?'+req.url.split('?')[1]))); }
});
});
app.get('/trac/:slug', function(req,res,next){
User.findOne({slug:req.params.slug}, function(err, tracuser) {
if (err){ throwErr(req,err); }
if (!tracuser){ next(); }
else { res.render('trac.html',{
api: secret.mapAPI,
user: req.user,
tracuser: tracuser,
noFooter: '1',
noHeader: (req.query.noheader)?req.query.noheader.match(/\d/)[0]:'',
disp: (req.query.disp)?req.query.disp.match(/\d/)[0]:'' // 0=map, 1=streetview, 2=both
}); }
});
});
app.get('/trac/id/:id', function(req,res){
User.findById(req.params.id, function(err, user){
if (err){ throwErr(req,err); }
if (!user){ next(); }
else { res.redirect('/trac/'+user.slug+((req.url.indexOf('?')<0)?'':('?'+req.url.split('?')[1]))); }
});
});
app.get('/invited/:invite', function(req,res,next){
User.findOne({requestId:req.params.invite}, function(err, existingUser) { // User already accepted invite
if (err) { console.log('routes.js:121 ERROR: '+err); }
if (existingUser) { res.redirect('/login'); }
else {
Request.findById(req.params.invite, function(err, request) { // Check for granted invite
if (err) { throwErr(req,err); }
if (!request) { next(); }
else {
user = new User({ // Create new user
requestId: request._id,
email: '',
slug: request._id,
name: request.name,
created: Date.now(),
settings: {
units: 'imperial',
showSpeed: false,
showTemp: false,
showAlt: false,
showStreetview: true
}
}); user.save(function(err) {
if (err) { throwErr(req,err); }
User.findOne({requestId:request._id}, function(err, user) {
if (err) { throwErr(req,err); }
if (user) {
request.userId = user._id;
request.save(function(err, raw){
if (err){ throwErr(req,err); }
});
req.logIn(user, function(err) {
if (err) { throwErr(req,err); }
user.lastLogin = Date.now();
user.save(function(err, raw) {
if (err) { throwErr(req,err); }
res.redirect('/login');
});
});
}
});
});
}
});
}
});
});
app.get('/android', ensureAuth, function(req,res){
res.redirect('https://play.google.com/store/apps/details?id=us.keithirwin.tracman');
});
app.get('/license', function(req,res){
res.render('license.html', {user:req.user});
});
app.route('/pro')
.all(ensureAuth, function(req,res,next){
next();
}).get(function(req,res){
User.findById(req.session.passport.user, function(err, user){
if (err){ throwErr(req,err); }
if (!user){ next(); }
else { res.render('pro.html', {user:user}); }
});
}).post(function(req,res){
User.findByIdAndUpdate(req.session.passport.user,
{$set:{ isPro:true }},
function(err, user){
if (err){ throwErr(req,err); }
else { req.flash('success','You have been signed up for pro. '); }
res.redirect('/dashboard');
}
);
});
app.route('/suggestion')
.get(function(req,res){
res.render('suggestion.html', {user:req.user});
}).post(function(req,res){
mail.sendSuggestion({
name: (req.body.name)?req.body.name:req.user.name,
email: (req.body.email)?req.body.email:req.user.email,
suggestion: req.body.suggestion
}, function (err, raw) {
if (err){ throwErr(req,err); }
else { req.flash('success','Thanks for the suggestion! '); }
res.redirect('/dashboard');
});
});
app.route('/bug')
.all(ensureAuth, function(req,res,next){
next();
}).get(function(req,res){
res.render('bug.html', {
user: req.user,
errorMessage: req.flash('error-message')
});
}).post(function(req,res){
mail.sendBugReport({
source: (req.query.source)?req.body.name:'web',
name: (req.body.name)?req.body.name:req.user.name,
email: (req.body.email)?req.body.email:req.user.email,
errorMessage: req.body.errorMessage,
recreation: req.body.recreation,
bug: req.body.bug
}, function (err, raw) {
if (err){ throwErr(req,err); }
else { req.flash('success','Thanks for the report! '); }
res.redirect('/dashboard');
});
});
// ADMIN
app.route('/admin/requests')
.all([ensureAuth, ensureAdmin], function(req,res,next){
next();
}).get(function(req,res){
User.findById(req.session.passport.user, function(err, user){
if (err){ req.flash('error', err.message); }
Request.find({}, function(err, requests){
if (err) { req.flash('error', err.message); }
res.render('admin/requests.html', {
user: user,
noFooter: '1',
requests: requests,
success:req.flash('success')[0],
error:req.flash('error')[0]
});
});
});
}).post(function(req,res){
Request.findById(req.body.invite, function(err, request){
if (err){ req.flash('error', err.message); }
mail.sendInvite(request, function (err, raw) {
if (err) { req.flash('error', err.message); }
request.granted = Date.now();
request.save(function(err) {
if (err) { req.flash('error', err.message); }
});
req.flash('success', 'Invitation sent to <i>'+request.name+'</i>.');
res.redirect('/admin/requests');
});
});
});
app.get('/admin/users', [ensureAuth, ensureAdmin], function(req,res){
User.findById(req.session.passport.user, function(err, user){
if (err){ req.flash('error', err.message); }
User.find({}, function(err, users){
if (err) { req.flash('error', err.message); }
res.render('admin/users.html', {
user: user,
users: users,
noFooter: '1',
success:req.flash('success')[0],
error:req.flash('error')[0]
});
});
});
});
// AUTH
app.get('/login', function(req,res){
res.redirect('/auth/google');
});
app.get('/logout', function(req,res){
req.logout();
res.redirect('/');
});
app.get('/auth/google', passport.authenticate('google', { scope: [
'https://www.googleapis.com/auth/plus.login',
'https://www.googleapis.com/auth/plus.profile.emails.read'
] }));
app.get('/auth/google/callback', passport.authenticate('google', {
failureRedirect: '/',
failureFlash: true,
successRedirect: '/dashboard?rd=1',
successFlash: true
} ));
app.get('/auth/google/idtoken', passport.authenticate('google-id-token'), function (req,res) {
if (!req.user) { res.sendStatus(401); }
else { res.send(req.user); }
} );
}

50
package.json Normal file
View File

@ -0,0 +1,50 @@
{
"name": "tracman",
"version": "0.1.5",
"description": "Tracks user's GPS location",
"main": "server.js",
"dependencies": {
"body-parser": "^1.15.0",
"connect-flash": "^0.1.1",
"cookie-parser": "^1.4.1",
"email-templates": "^2.1.0",
"express": "^4.13.3",
"express-crash": "0.0.2",
"express-session": "^1.13.0",
"kerberos": "0.0.17",
"mailgun-js": "^0.7.7",
"moment": "^2.12.0",
"mongodb": "^2.1.4",
"mongoose": "^4.3.5",
"nunjucks": "^2.3.0",
"passport": "^0.3.2",
"passport-google-id-token": "^0.4.0",
"passport-google-oauth2": "^0.1.6",
"slug": "^0.9.1",
"socket.io": "^1.4.4"
},
"devDependencies": {
"chai": "^3.5.0",
"chai-http": "^2.0.1"
},
"scripts": {
"test": "test.js",
"start": "node server.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tracman-org/server.git"
},
"keywords": [
"tracking",
"location",
"map"
],
"author": "Keith Irwin",
"license": "MIT",
"README": "README.md",
"bugs": {
"url": "https://tracman.org/bug"
},
"homepage": "https://tracman.org/"
}

131
server.js Normal file
View File

@ -0,0 +1,131 @@
express = require('express'),
crash = require('express-crash'),
bodyParser = require('body-parser'),
cookieParser = require('cookie-parser'),
session = require('express-session'),
mongoose = require('mongoose'),
nunjucks = require('nunjucks'),
passport = require('passport'),
flash = require('connect-flash'),
secret = require('./config/secrets.js'),
auth = require('./config/auth.js'),
User = require('./config/models/user.js'),
routes = require('./config/routes.js'),
app = express(),
http = require('http').Server(app),
io = require('socket.io')(http);
// SETUP
nunjucks.configure(__dirname+'/views', {
autoescape: true,
express: app
});
app.use(session({
secret: secret.session,
saveUninitialized: true,
resave: true
}));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(cookieParser(secret.cookie));
app.use(flash());
app.use(passport.initialize());
app.use(passport.session());
app.use('/static', express.static(__dirname+'/static'));
routes(app);
mongoose.connect(secret.mongoSetup, {
server:{socketOptions:{ keepAlive:1, connectTimeoutMS:30000 }},
replset:{socketOptions:{ keepAlive:1, connectTimeoutMS:30000 }}
});
// Handle errors
var handle404 = function(err,req,res,next) {
res.render('error.html', {code:404});
};
var handle500 = function(err,req,res,next) {
res.render('error.html', {code:500});
};
if (secret.url.substring(0,16)!='http://localhost') {
app.use(crash.handle404(handle404));
app.use(crash.handle500(handle500));
crash.trapRoute(app);
crash.handle(app, handle404, handle500);
}
// Check for tracking users
function checkForUsers(room) {
if (room) {
io.to('app-'+room).emit('activate',
(io.of("/").adapter.rooms[room])?'true':'false'
);
} else {
User.find({}, function(err, users){
if (err) { console.log(err); }
users.forEach( function(user){
checkForUsers(user.id);
});
});
}
}
// Sockets
io.on('connection', function(socket) {
socket.on('room', function(room) {
socket.join(room);
if (room.slice(0,4)!='app-'){
User.findById({_id:room}, function(err, user) {
if (err) { console.log(err); }
if (user) { io.to('app-'+room).emit('activate','true'); }
});
} else {
checkForUsers(room.slice(4));
}
});
socket.on('app', function(loc){
loc.time = Date.now();
io.to(loc.usr).emit('trac', loc);
User.findByIdAndUpdate(loc.usr, {last:{
lat: parseFloat(loc.lat),
lon: parseFloat(loc.lon),
dir: parseFloat(loc.dir||0),
spd: parseFloat(loc.spd||0),
time: Date.now()
}}, function(err, user) {
if (err) { console.log(err); }
if (!user) { console.log("No user found: "+loc.user); }
});
});
socket.onclose = function(reason){
var closedroom = Object.keys(socket.adapter.sids[socket.id]).slice(1)[0];
setTimeout(function() {
checkForUsers(closedroom);
}, 3000);
Object.getPrototypeOf(this).onclose.call(this,reason);
}
});
// Serialize and deserialize users
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
if(!err) done(null, user);
else done(err, null);
});
});
// SERVE
http.listen(62054, function(){
console.log('Listening for http on port 62054');
checkForUsers();
});
module.exports = app;

188
static/css/base.css Normal file
View File

@ -0,0 +1,188 @@
/* Resets, Clears & Defaults */
*, *:after, *:before {
box-sizing: border-box;
}
body {
background-color:#000;
color:#eee;
}
body, input, textarea {
padding: 0; margin: 0;
font-family: 'Open Sans', sans-serif;
font-size: 18px;
color: #eee;
font-weight: 600;
}
.flexbox {
width:100%;
display:flex;
justify-content:space-around;
}
.flexbox.stretch { justify-content:space-between; }
pre {
white-space: pre-wrap;
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
word-wrap: break-word;
}
.dark pre {
color: #777;
padding: 1%;
border: 1px solid #ccc;
border-radius: .25rem;
}
.dark .form-control:disabled, .dark .form-control:disabled {
background-color: rgba(255,255,255,0.1);
}
.input-group {
margin-bottom:30px;
}
input[type="checkbox"] {
margin: 8px 0;
}
.form-group#buttons {
width: 100%;
display: flex;
justify-content: space-around;
}
input[type="checkbox"] {
display: inline-block;
}
.help-block {margin-top:-20px;}
.alert {
z-index:10;
}
.alert:not(.alert-dismissible) {
text-align: center;
}
.alert a {
color: inherit;
text-decoration: underline;
}
.alert a:hover {
color: inherit;
text-decoration: none;
}
input:focus, textarea:focus {
outline: 0;
}
h1, h2, h3, p {
margin: 0 0 20px 0;
position: relative;
z-index: 6;
}
h1,h2,h3,h4 { font-weight: 600; }
h1 {
font-size: 48px;
line-height: 46px; }
h2 {
font-size: 40px;
line-height: 36px; }
h3 { font-size: 28px; }
h4 { font-size: 20px; }
.red { color: #fb6e3d; }
a {
color: #fbc93d;
text-decoration: none;
}
a:hover {
color: #fbc93d;
text-decoration: underline;
}
.light a {
color:#111;
text-decoration: underline;
}
.light a:hover {
color:#111;
text-decoration: none;
}
img {
max-width: 100%;
}
p img {
display: block;
margin: auto;
}
input[type="checkbox"] {
width: auto;
margin: 8px;
}
.with-errors {
color: #d9534f;
}
::selection {
background: #999;
}
::-moz-selection {
background: #999;
}
/* End Resets, Clears & Defaults */
.container {
width: 100%;
max-width: 1000px;
margin: 0 auto;
}
.container:after {
content: "";
display: block;
clear: both;
}
section {
padding: 100px 0 50px;
}
.btn {
text-decoration: none;
font-weight:600;
display: inline-block;
padding: 15px 30px;
transition: 200ms;
background: transparent;
cursor: pointer;
}
.dark .btn {
color: #fff;
border: 1px solid #fff;
}
.dark .btn:hover:not(.disabled),
.dark .btn:active:not(.disabled),
.dark .btn:focus:not(.disabled) {
background: rgba(255,255,255,0.1);
}
.light .btn {
color: #222;
text-decoration: none;
border: 1px solid #222;
}
.light .btn:hover:not(.disabled),
.light .btn:active:not(.disabled),
.light .btn:focus:not(.disabled) {
background: rgba(0,0,0,0.1);
}
.btn.yellow {
color: #fbc93d;
border: 1px solid #fbc93d;
}
.dark .btn.yellow:hover:not(.disabled),
.dark .btn.yellow:active:not(.disabled),
.dark .btn.yellow:focus:not(.disabled) {
background: rgba(251,201,61,0.1);
}
.btn.smaller {
padding: 10px 25px;
}
.btn .fa {
margin-left: 10px;
}

6229
static/css/bootstrap.css Normal file

File diff suppressed because it is too large Load Diff

52
static/css/footer.css Normal file
View File

@ -0,0 +1,52 @@
footer {
font-weight: 300;
width:100%;
overflow:auto;
background: #111;
color: #ccc;
padding: 0 20px;
}
footer .left {
float: left;
padding: 15px 0;
}
footer .left p {
margin: 0;
}
footer a {
font-weight:600;
color: #fff;
}
footer a:hover {
text-decoration: none;
}
footer .right {
text-align: right;
float: right;
padding: 15px 0;
}
footer a .fa {
margin-left: 5px;
font-size: 20px;
color: inherit;
}
footer .fa a:hover, footer .fa a:focus {
color: inherit;
}
@media (max-width: 800px) {
footer {
padding: 0 10px;
}
}
@media (max-width: 600px) {
footer {
text-align: center;
}
footer .left, footer .right {
float: none;
}
footer .right {
padding-top: 0;
}
}

162
static/css/header.css Normal file
View File

@ -0,0 +1,162 @@
header {
background: #222;
padding: 0;
position: fixed;
top: 0; left: 0;
width: 100%;
z-index: 200;
}
header .logo {
float: left;
font-family: 'Open Sans', sans-serif;
padding: 13px 23px;
color: #fbc93d;
font-weight: 800;
font-size: 22px;
line-height: 30px;
margin: 0;
}
header a:hover, header a:focus {
color: #fbc93d;
}
header .logo a {
color:inherit;
font:inherit;
text-decoration:inherit;
cursor: pointer;
}
header .logo img {
margin-right: 10px;
position: relative;
width:28px;
height:28px;
}
header nav {
float: right;
}
header nav ul {
padding: 0;
margin: 0;
}
header nav ul li {
display: inline-block;
float: left;
}
header nav ul li a, header nav ul li span {
text-decoration:inherit;
display: inline-block;
padding: 15px 20px;
color: #fff;
transition: 200ms;
}
header nav ul li a:hover,
header nav ul li a:focus,
header nav ul li a.active,
header .logo:hover {
text-decoration: none;
background: rgba(255,255,255,0.1);
}
.alert.header {
position: relative;
border-radius: 0;
top: 58px;
width: 100%;
}
header .hamburger {
display: none;
padding: 5px;
cursor: pointer;
transition-property: opacity, -webkit-filter;
transition-property: opacity, filter;
transition-property: opacity, filter, -webkit-filter;
transition-duration: 0.15s;
transition-timing-function: linear; }
.hamburger:hover {
opacity: 0.7; }
header .hamburger-box {
width: 40px;
height: 24px;
position: relative; }
header .hamburger-inner {
top: 50%;
margin-top: -2px; }
header .hamburger-inner, header .hamburger-inner::before, header .hamburger-inner::after {
width: 40px;
height: 4px;
background-color: #fff;
border-radius: 4px;
position: absolute;
transition-property: -webkit-transform;
transition-property: transform;
transition-property: transform, -webkit-transform;
transition-duration: 0.15s;
transition-timing-function: ease; }
header .hamburger-inner::before, header .hamburger-inner::after {
content: "";
display: block; }
header .hamburger-inner::before {
top: -10px; }
header .hamburger-inner::after {
bottom: -10px; }
header .hamburger--slider .hamburger-inner {
top: 0; }
header .hamburger--slider .hamburger-inner::before {
top: 10px;
transition-property: opacity, -webkit-transform;
transition-property: transform, opacity;
transition-property: transform, opacity, -webkit-transform;
transition-timing-function: ease;
transition-duration: 0.2s; }
header .hamburger--slider .hamburger-inner::after {
top: 20px; }
header .hamburger--slider.is-active .hamburger-inner {
-webkit-transform: translate3d(0, 10px, 0) rotate(45deg);
transform: translate3d(0, 10px, 0) rotate(45deg); }
header .hamburger--slider.is-active .hamburger-inner::before {
-webkit-transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0);
transform: rotate(-45deg) translate3d(-5.71429px, -6px, 0);
opacity: 0; }
header .hamburger--slider.is-active .hamburger-inner::after {
-webkit-transform: translate3d(0, -20px, 0) rotate(-90deg);
transform: translate3d(0, -20px, 0) rotate(-90deg); }
@media (max-width: 800px) {
header { padding:0; }
header nav ul li a { padding:15px; }
}
@media (max-width: 600px) {
header nav {
float: none;
position: fixed;
top: 56px;
right: -300px;
bottom: 0;
width: 100%;
max-width: 300px;
background: #333;
transition: 200ms;
}
header nav.visible {
right: 0px;
}
header nav ul li {
display: block;
float: none;
width: 100%;
}
header nav ul li a {
display: block;
width: 100%;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
header .hamburger {
display: inline-block;
color: #fff;
position: absolute;
right: 10px;
top: 13px;
}
}

306
static/css/index.css Normal file
View File

@ -0,0 +1,306 @@
/* Animations */
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(0.8); }
100% { transform: scale(1); }
}
@keyframes spin {
0% { transform: rotate(30deg); }
100% { transform: rotate(210deg); }
}
@keyframes spin2 {
0% { transform: rotate(150deg); }
100% { transform: rotate(330deg); }
}
/* End Animations */
.btn { border-radius: 50px; }
.splash {
background: #090909;
background-image: url("/static/img/style/map.jpg");
background-size: cover;
color: #FFF;
height: 100vh;
overflow: hidden;
position: relative;
}
.splash:after, .splash:before {
content: "";
display: block;
position: absolute;
top: -40px; right: -40px; bottom: -40px; left: -40px;
}
.splash:after {
background: rgba(255,255,255,0.05);
transform: rotate(30deg);
animation: spin 60s infinite linear;
}
.splash:before {
background: rgba(0,0,0,0.5);
transform: rotate(150deg);
animation: spin2 50s infinite linear;
}
.splash .container {
position: relative;
top: 48%;
transform: translateY(-50%);
z-index: 5;
}
.splash h1 {
color: #fbc93d;
}
.splash h2 {
margin-bottom: 40px;
}
.splash .btn {
margin: 0 20px 10px 0;
}
.splash .btn:hover {
color:#fff;
background: rgba(0,0,0,0.5);
}
.overview {
text-align: center;
}
.overview > div > div {
float: left;
width: 33%;
padding: 0 40px 0 40px;
}
.overview .fa {
display: inline-block;
color:#222;
font-size: 50px;
width: 100px;
height: 100px;
border-radius: 50px;
background: #f6f6f6;
margin-bottom: 20px;
padding-top: 25px;
}
.overview p {
margin-bottom: 0;
}
.feature.app {
background: #111;
}
.feature {
position: relative;
overflow: hidden;
}
.feature img {
position: absolute;
top: 100px;
right: 55%;
}
.feature:nth-of-type(even) img {
right: auto;
left: 55%;
}
.feature > div > div {
width: 50%;
float: right;
}
.feature > div > div > p {
margin-bottom: 40px;
}
.feature:nth-of-type(even) > div > div {
float: left;
}
.feature ul {
margin: 0;
padding: 0;
}
.feature ul li {
display: block;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.feature ul li:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: 0;
}
.feature ul li h3 {
margin: 0 0 5px 0;
}
.feature ul li p:last-child {
margin: 0;
}
.feature ul li .fa {
float: left;
font-size: 30px;
background: #fbc93d;
color: #000;
width: 50px;
height: 50px;
display: inline-block;
text-align: center;
padding-top: 10px;
border-radius: 25px;
margin-right: 20px;
margin-top: 7px;
}
.feature ul li p {
overflow: hidden;
}
.light {
color:#222;
position: relative;
overflow: hidden;
}
.light:after {
content: "";
display: block;
position: absolute;
top: -40px; right: -40px; bottom: -40px; left: -40px;
background: rgba(255,255,255,0.1);
transform: rotate(30deg);
}
.light h2 {
margin-bottom: 40px;
}
.disclaimer {
color: #fb6e3d;
background: #000;
}
.disclaimer .container {
position: relative;
z-index: 10;
}
.disclaimer a, .disclaimer a:hover {
color:#fb6e3d;
}
.get {
background: #fbc93d;
}
.get input, .get textarea {
color:#111;
}
.get .input {
width: 47%;
float: left;
}
.get .submit {
width: 47%;
float:right;
}
.get .input:nth-of-type(odd) {
margin-right: 6%;
}
.get .message {
display: block;
clear: both;
float: none;
padding-top: 10px;
}
.get .input input {
display: inline-block;
float: left;
width: 100%;
background: rgba(255,255,255,0.3);
border: 0;
padding: 10px 15px;
}
.get .message textarea {
display: block;
width: 100%;
height: 200px;
background: rgba(255,255,255,0.3);
border: 0;
padding: 10px 15px;
resize: vertical;
}
.get label {
position: relative;
z-index: 10;
}
.get label.input span, .get label.message span {
display: inline-block;
float: left;
}
.get .submit {
text-align: center;
padding-top: 10px;
}
.get .submit .btn, .get .submit .alert {
position:static;
float:right;
}
@media (max-width: 800px) {
section {
padding: 80px 10px;
}
.splash {
height: auto;
padding: 150px 10px 80px 10px;
text-align: center;
}
.splash .container {
position: relative;
top: 0;
transform: none;
}
.overview > div > div {
padding: 0 20px;
}
.feature img {
right: 65%;
}
.feature:nth-of-type(even) img {
left: 65%;
}
.feature > div > div {
width: 60%;
}
}
@media (max-width: 600px) {
section {
padding: 40px 10px;
}
.splash {
padding: 100px 10px 40px 10px;
}
.overview > div > div {
float: none;
width: 100%;
margin-bottom: 40px;
padding: 0;
}
.overview > div > div:last-child {
margin-bottom: 0;
}
.overview p {
overflow: hidden;
}
.feature img {
display: none;
}
.feature > div > div {
width: 100%;
float: none;
}
.get .input {
display: block;
width: 100%;
float: none;
}
.get .input:nth-of-type(odd) {
margin-right: 0;
}
.get label {
padding-top: 10px;
}
.get label:first-of-type {
padding-top: 0;
}
}

73
static/css/trac.css Normal file
View File

@ -0,0 +1,73 @@
body {
color: #fff;
width: 100%;
height: 100%;
background: #000;}
.wrap {
position: absolute;
bottom: 0px;
width: 100%;
}
.centered.alert {
text-align:center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#map, #pano {position:relative;}
#pano {float:right;}
.loading {
font-size:7em;
position: absolute;
top: 50%;
left: 50%;
margin: -56px 0 0 -56px;
}
.map-logo {
margin-left: -75px;
background: rgba(0,0,0,.7);
padding: 0 10px 0 75px;
font-size: 2em;
}
.map-logo a { color: #fbc93d; }
.tim {
color: #000;
font-size: 12px;
padding-left: 5px;
padding-right: 5px;
background-color: rgba(255,255,255,.7);
}
.spd {
font-size: 32px;
height: 40px;}
.spd-sign {
color: #000;
text-align: center;
padding: 5px;
border: 2px solid #000;
border-radius: 3px;
margin: 10px;
background-color: #FFF;
}
.alt-unit, .spd-unit { font-size:12px; }
.alt-label, .spd-label {
font-size:18px;
height:18px;}
.alt {
font-size: 32px;
height: 40px;}
.alt-sign {
color: #FFF;
text-align: center;
padding: 5px;
border: 2px solid #FFF;
border-radius: 3px;
margin: 10px;
background-color: #009800;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
static/img/icon/bw/128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
static/img/icon/bw/152.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/img/icon/bw/16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

BIN
static/img/icon/bw/228.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
static/img/icon/bw/32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
static/img/icon/bw/48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
static/img/icon/bw/57.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
static/img/icon/bw/72.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
static/img/icon/by/128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
static/img/icon/by/152.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/img/icon/by/16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

BIN
static/img/icon/by/228.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
static/img/icon/by/32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
static/img/icon/by/48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
static/img/icon/by/57.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
static/img/icon/by/72.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
static/img/style/laptop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
static/img/style/map.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

BIN
static/img/style/phone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

6
static/js/header.js Normal file
View File

@ -0,0 +1,6 @@
$(document).ready(function(){
$(".hamburger").click(function(){
$(".hamburger").toggleClass("is-active");
$('nav').toggleClass('visible');
});
});

18
static/js/index.js Normal file
View File

@ -0,0 +1,18 @@
jQuery.extend(jQuery.easing,{
easeInOutExpo: function(x, t, b, c, d){
if (t==0) return b;
if (t==d) return b+c;
if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b;
return c/2 * (-Math.pow(2, -10 * --t) + 2) + b;
}
});
$(document).ready(function(){
$('a[href=#]').click(function(e){
e.preventDefault();
$('nav').removeClass('visible');
$('html,body').stop().animate({scrollTop: $('.'+$(this).data('scrollto')).offset().top-65 }, 700, 'easeInOutExpo', function(){});
});
});

7
static/js/moment.min.js vendored Normal file

File diff suppressed because one or more lines are too long

9
static/js/validator.min.js vendored Normal file

File diff suppressed because one or more lines are too long

35
test.js Normal file
View File

@ -0,0 +1,35 @@
var chai = require('chai'),
chaiHttp = require('chai-http'),
server = require('./server'),
should = chai.should();
chai.use(chaiHttp);
describe('Routes', function() {
it('Displays homepage', function(done){
chai.request(server)
.get('/')
.end(function(err,res){
res.should.have.status(200);
done();
});
});
it('Displays robots.txt', function(done){
chai.request(server)
.get('/robots.txt')
.end(function(err,res){
res.should.have.status(200);
res.should.be.text;
done();
});
});
it('Displays a tracpage', function(done){
chai.request(server)
.get('/trac/keith')
.end(function(err,res){
res.should.have.status(200);
done();
});
});
});

68
views/admin/requests.html Normal file
View File

@ -0,0 +1,68 @@
{% extends 'templates/base.html' %}
{% block title %}Tracman | Invite Requests{% endblock %}
{% block head %}
{{ super() }}
<style>
.container { max-width:90%; }
</style>
{% endblock %}
{% block main %}
<section class='dark'>
<div class='container'>
<h1>Requests</h1>
<table id='requests-table' class='table table-hover'>
<thead><tr>
<th>Name</th>
<th>Email</th>
<th>Message</th>
<th>Requested</th>
<th>Invited</th>
<th>User</th>
</tr></thead>
<tbody>
{% for request in requests %}
<tr class="table-{% if request.userId %}success{% elif request.granted %}info{% else %}danger{% endif %}">
<td>{{ request.name }}</td>
<td>{{ request.email }}</td>
<td>{{ request.beg | replace("\r\n", "<br>") | safe }}</td>
<td id='{{ request.id }}-requested'></td>
<td id='{{ request.id }}-granted'>
{% if not request.granted %}
<form action="" method="POST">
<button type="submit" class='btn btn-block btn-default' name="invite" value="{{ request.id }}">INVITE</button>
</form>
{% endif %}
</td>
<td>
{% if request.userId %}
User: <a href="/trac/id/{{ request.userId }}">{{ request.userId }}</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<script src="/static/js/moment.min.js"></script>
<script>
{% for request in requests %}
$('#{{ request.id }}-requested').text(
moment("{{ request.requestedTime }}", "ddd MMM DD YYYY HH:mm:ss [GMT]ZZ").fromNow()
// Sun Mar 20 2016 19:21:55 GMT+0100 (CET)
);
{% if request.granted %}
$('#{{ request.id }}-granted').text(
moment("{{ request.granted }}", "ddd MMM DD YYYY HH:mm:ss [GMT]ZZ").fromNow()
);
{% endif %}
{% endfor %}
</script>
{% endblock %}

63
views/admin/users.html Normal file
View File

@ -0,0 +1,63 @@
{% extends 'templates/base.html' %}
{% block title %}Tracman | Users{% endblock %}
{% block head %}
{{ super() }}
<style>
.container { max-width:90%; }
</style>
{% endblock %}
{% block main %}
<section class='dark'>
<div class='container'>
<h1>Users</h1>
<table id='users-table' class='table table-hover'>
<thead><tr>
<th>Name</th>
<th>Slug</th>
<th>Joined</th>
<th>Last login</th>
<th>Moved</th>
</tr></thead>
<tbody>
{% for usr in users %}
<tr>
<td>{{ usr.name }}</td>
<td><a href="/trac/{{ usr.slug }}">/{{ usr.slug }}</a></td>
<td id='{{ usr.id }}-created'></td>
<td id='{{ usr.id }}-logged'></td>
<td id='{{ usr.id }}-moved'></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<script src="/static/js/moment.min.js"></script>
<script>
{% for usr in users %}
{% if usr.created %}
$('#{{ usr.id }}-created').text(
moment("{{ usr.created }}", "ddd MMM DD YYYY HH:mm:ss [GMT]ZZ").format('l')
);
{% endif %}
{% if usr.lastLogin %}
$('#{{ usr.id }}-logged').text(
moment("{{ usr.lastLogin }}", "ddd MMM DD YYYY HH:mm:ss [GMT]ZZ").fromNow()
);
{% endif %}
{% if usr.last %}
$('#{{ usr.id }}-moved').text(
moment("{{ usr.last.time }}", "ddd MMM DD YYYY HH:mm:ss [GMT]ZZ").fromNow()
);
{% endif %}
{% endfor %}
</script>
{% endblock %}

64
views/bug.html Normal file
View File

@ -0,0 +1,64 @@
{% extends 'templates/base.html' %}
{% block title %}{{ super() }} | Bug Report{% endblock %}
{% block main %}
<section class='dark'>
<div class='container'>
<h1>Submit a Bug Report</h1>
<p>You can use this form to submit a bug report. You can also <a href="https://github.com/Tracman-org/Server/issues/new">post the issue on github</a>. Or maybe you'd like to <a href="/suggestion">suggest a feature</a> instead? </p>
<script src="/static/js/validator.min.js"></script>
<form id='suggestions-form' class='col-lg-10 col-lg-offset-1 form-horizontal' role="form" method="POST" data-toggle="validator">
<div class='form-group' id='error-message' title="If you recieved an error message, put it here. An error message may be added automatically. ">
<label class='control-label col-sm-2 col-lg-3' for="errorMessage">Error message (if any) </label>
<div class='input-group col-xs-12 col-sm-10 col-lg-9'>
<textarea class='form-control' name="errorMessage" rows="3" maxlength="2400" {% if errorMessage.length %}disabled{% endif %}>{{ errorMessage }}</textarea>
</div>
<div class='help-block with-errors col-xs-12 col-sm-10 col-sm-offset-2 col-lg-9 col-lg-offset-3'></div>
</div>
<div class='form-group' id='recreation' title="Help me recreate the issue. ">
<label class='control-label col-sm-2 col-lg-3' for="recreation">What were you doing when this happened? </label>
<div class='input-group col-xs-12 col-sm-10 col-lg-9'>
<textarea class='form-control' name="recreation" rows="4" maxlength="2400"></textarea>
</div>
<div class='help-block with-errors col-xs-12 col-sm-10 col-sm-offset-2 col-lg-9 col-lg-offset-3'></div>
</div>
<div class='form-group' id='bug' title="Describe the problem here. ">
<label class='control-label col-sm-2 col-lg-3' for="bug">What happened? </label>
<div class='input-group col-xs-12 col-sm-10 col-lg-9'>
<textarea class='form-control' name="bug" rows="4" maxlength="2400"></textarea>
</div>
<div class='help-block with-errors col-xs-12 col-sm-10 col-sm-offset-2 col-lg-9 col-lg-offset-3'></div>
</div>
<div class='form-group' id='name' title="Put your name here if you want. ">
<label class='control-label col-sm-2 col-lg-3' for="name">Name</label>
<div class='input-group col-xs-12 col-sm-10 col-lg-9'>
<input class='form-control' name="name" type="text" value="{{ user.name }}"
maxlength="160"><br>
</div>
<div class='help-block with-errors col-xs-12 col-sm-10 col-sm-offset-2 col-lg-9 col-lg-offset-3'></div>
</div>
<div class='form-group' id='email' title="Put an email address if you want a reply. ">
<label class='control-label col-sm-2 col-lg-3' for="email">Email</label>
<div class='input-group col-xs-12 col-sm-10 col-lg-9'>
<input class='form-control' name="email" type="email" value="{{ user.email }}"
maxlength="160" data-error="That's not an email address"><br>
</div>
<div class='help-block with-errors col-xs-12 col-sm-10 col-sm-offset-2 col-lg-9 col-lg-offset-3'></div>
</div>
<div class='form-group flexbox' id='buttons'>
<button type="submit" class='btn yellow'>SUBMIT</button>
<a class='btn' href="/dashboard">nevermind</a>
</div>
</form>
</div>
</section>
{% endblock %}

166
views/dashboard.html Normal file
View File

@ -0,0 +1,166 @@
{% extends 'templates/base.html' %}
{% block title %}{{ super() }} | Dashboard{% endblock %}
{% block head %}
{{ super() }}
<style>
.google-play {
width:150px;
border: #fbc93d solid 1px;
border-radius:10px; }
</style>
{% endblock %}
{% block main %}
<section class='dark'>
<div class='container'>
<h1>Welcome{% if user.name %}, {{ user.name }}{% endif %}!</h1>
<p>To view your location, use this link: <a href="/trac/{{ user.slug }}">http://tracman.org/trac/{{ user.slug }}</a></p>
<p>You can also embed a map into your website with this code. Be sure to set the width and height attributes to suit your circumstance. </p>
<pre>&lt;iframe src=&quot;https://tracman.org/trac/{{ user.slug }}?noheader=1&quot; width=&quot;90%&quot; style=&quot;height:90vh;&quot;&gt;&lt;/iframe&gt;</pre>
</div>
<br>
<div class='container'>
<h2>App</h2>
<p>Click the button below to download the app from the google play store, if you haven't already. </p>
<p><a href="https://play.google.com/store/apps/details?id=us.keithirwin.tracman&utm_source=global_co&utm_medium=prtnr&utm_content=Mar2515&utm_campaign=PartBadge&pcampaignid=MKT-AC-global-none-all-co-pr-py-PartBadges-Oct1515-1"><img class='google-play' alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/apps/en-play-badge.png" /></a></p>
<p>Without the app running, your location won't update. But you can also set your location to this device's geolocation by clicking the button below. </p>
<p><button id='manual-set' class='btn' style="display:block; margin:auto;" onclick="setLocation()"><i class="fa fa-map-marker"></i>&emsp;Update location manually</button></p>
</div>
<br>
<div class='container'>
<h2>Settings</h2>
<script src="/static/js/validator.min.js"></script>
<form id='settings-form' class='col-lg-10 col-lg-offset-1 form-horizontal' data-toggle="validator" role="form" method="post">
<div class='form-group' id='name' title="This appears in your page's title. ">
<label class='control-label col-sm-2 col-lg-3' for="name">Name</label>
<div class='input-group col-xs-12 col-sm-10 col-lg-9'>
<input class='form-control' name="name" type="text" value="{{ user.name }}"
maxlength="160" data-error="Invalid input"><br>
</div>
<div class='help-block with-errors col-xs-12 col-sm-10 col-sm-offset-2 col-lg-9 col-lg-offset-3'></div>
</div>
<div class='form-group' id='slug' title="This is the URL which shows your location. Be careful whom you share it with! ">
<label class='control-label col-sm-2 col-lg-3' for="slug">URL</label>
<div class='input-group col-xs-12 col-sm-10 col-lg-9'>
<span class='input-group-addon'>tracman.org/trac/</span>
<input class='form-control' type="text" name="slug" value="{{ user.slug }}" required data-remote="/validate"
maxlength="160" data-remote-error="That URL is already taken. " data-error="Invalid input"><br>
</div>
<div class='help-block with-errors col-xs-12 col-sm-10 col-sm-offset-2 col-lg-9 col-lg-offset-3'></div>
</div>
<div class='form-group col-xs-12' id='units' title="Select imperial units for feet and miles/hour. Select metric units if you are a commie. ">
<label class='control-label col-sm-4 col-lg-3' for="units">Units</label>
<div class='input-group col-sm-8 col-lg-9'>
<div class='radio-inline'><label>
<input type="radio" name="units" value="imperial" {% if user.settings.units == 'imperial' %}checked{% endif %}>
Imperial
</label></div>
<div class='radio-inline'><label>
<input type="radio" name="units" value="metric" {% if user.settings.units == 'metric' %}checked{% endif %}>
Metric
</label></div>
</div>
</div>
<div class='form-group col-xs-12' id='defaultMap' title="Shows whether to show a satellite image or standard google road map as the default on your page. Visitors will have the option to change this. Note that satellite images load slower. ">
<label class='control-label col-sm-4 col-lg-3' for="map">Default map</label>
<div class='input-group col-sm-8 col-lg-9'>
<div class='radio-inline'><label>
<input type="radio" name="map" value="road" {% if user.settings.defaultMap == 'road' %}checked{% endif %}>
Road
</label></div>
<div class='radio-inline'><label>
<input type="radio" name="map" value="sat" {% if user.settings.defaultMap == 'sat' %}checked{% endif %}>
Satellite
</label></div>
</div>
</div>
<div class='form-group col-xs-12' id='defaultZoom' title="Shows the initial map zoom level on your page. A higher number means more zoom. Note that the size of the viewing window will also have an effect on how much of the map a visitor can see. ">
<label class='control-label col-xs-6 col-sm-4 col-lg-3' for="map">Default zoom</label>
<div class='input-group col-xs-6 col-sm-8 col-lg-9'>
<select class='c-select' name="zoom">
<option {% if user.settings.defaultZoom==1 %}selected {% endif %}value="1">1 World</option>
<option {% if user.settings.defaultZoom==2 %}selected {% endif %}value="2">2</option>
<option {% if user.settings.defaultZoom==3 %}selected {% endif %}value="3">3</option>
<option {% if user.settings.defaultZoom==4 %}selected {% endif %}value="4">4</option>
<option {% if user.settings.defaultZoom==5 %}selected {% endif %}value="5">5 Landmass</option>
<option {% if user.settings.defaultZoom==6 %}selected {% endif %}value="6">6</option>
<option {% if user.settings.defaultZoom==7 %}selected {% endif %}value="7">7</option>
<option {% if user.settings.defaultZoom==8 %}selected {% endif %}value="8">8</option>
<option {% if user.settings.defaultZoom==9 %}selected {% endif %}value="9">9</option>
<option {% if user.settings.defaultZoom==10 %}selected {% endif %}value="10">10 City</option>
<option {% if user.settings.defaultZoom==11 %}selected {% endif %}value="11">11</option>
<option {% if user.settings.defaultZoom==12 %}selected {% endif %}value="12">12</option>
<option {% if user.settings.defaultZoom==13 %}selected {% endif %}value="13">13</option>
<option {% if user.settings.defaultZoom==14 %}selected {% endif %}value="14">14</option>
<option {% if user.settings.defaultZoom==15 %}selected {% endif %}value="15">15 Streets</option>
<option {% if user.settings.defaultZoom==16 %}selected {% endif %}value="16">16</option>
<option {% if user.settings.defaultZoom==17 %}selected {% endif %}value="17">17</option>
<option {% if user.settings.defaultZoom==18 %}selected {% endif %}value="18">18</option>
<option {% if user.settings.defaultZoom==19 %}selected {% endif %}value="19">19</option>
<option {% if user.settings.defaultZoom==20 %}selected {% endif %}value="20">20 Buildings</option>
</select>
</div>
</div>
<div class='form-group col-xs-12' id='showSpeed' title="PRO ONLY! Shows a spedometer on the map. ">
<label class='control-label col-xs-6 col-sm-4 col-lg-3' for="showSpeed">Show speed{% if not user.isPro %} <span class='red'>(PRO)</span>{% endif %}</label>
<div class='input-group col-xs-6 col-sm-8 col-lg-9'>
<input class='form-control' name="showSpeed" type="checkbox" {% if not user.isPro %}disabled {% elif user.settings.showSpeed %}checked{% else %}{% endif %}><br>
</div>
</div>
<div class='form-group col-xs-12' id='showAltitude' title="PRO ONLY! Shows the current elevation on the map. ">
<label class='control-label col-xs-6 col-sm-4 col-lg-3' for="showAlt">Show altitude{% if not user.isPro %} <span class='red'>(PRO)</span>{% endif %}</label>
<div class='input-group col-xs-6 col-sm-8 col-lg-9'>
<input class='form-control' name="showAlt" type="checkbox" {% if not user.isPro %}disabled {% elif user.settings.showAlt %}checked{% else %}{% endif %}><br>
</div>
</div>
<div class='form-group col-xs-12' id='showStreet' title="PRO ONLY! Shows a Google street view image from your current location, oriented in the direction of travel. This feature is EXPERIMENTAL. ">
<label class='control-label col-xs-6 col-sm-4 col-lg-3' for="showStreet">Show street view{% if not user.isPro %} <span class='red'>(PRO)</span><br>{% endif %} (experimental)</label>
<div class='input-group col-xs-6 col-sm-8 col-lg-9'>
<input class='form-control' name="showStreet" type="checkbox" {% if not user.isPro %}disabled{% elif user.settings.showStreetview %}checked{% else %}{% endif %}><br>
</div>
</div>
<div class='form-group col-xs-12' style="margin: 30px 0;">
<input class='btn yellow col-xs-10 col-xs-offset-1 col-sm-offset-2 col-sm-4 col-lg-offset-3 col-lg-6' type="submit" value="Save">
</div>
</form>
{% if not user.isPro %}<p style="clear:both">Want to try <a href="/pro">Tracman Pro</a>? It's free during beta testing. </p>{% endif %}
<p style="clear:both">Would you like to submit a <a href="/suggestion">suggestion</a> or <a href="/bug">bug report</a>? </p>
</div>
</section>
<script src="https://cdn.socket.io/socket.io-1.2.0.js"></script>
<script>
var socket = io.connect();
socket.on('connect', function(){
socket.emit('room', 'app-{{ user.id }}');
});
function setLocation() {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function(pos){
socket.emit('app', {
usr: '{{ user.id }}',
lat: pos.coords.latitude,
lon: pos.coords.longitude,
spd: (pos.coords.speed||0)
});
alert('Location updated! ');
location.reload();
}, function(err){
alert('ERROR: '+err);
}, { enableHighAccuracy:true });
} else { alert('Unable to set location. '); }
}
</script>
{% endblock %}

10
views/error.html Normal file
View File

@ -0,0 +1,10 @@
{% extends 'templates/base.html' %}
{% block title %}{{ super() }} | {{ code }} Error{% endblock %}
{% block main %}
<section class='dark'>
<div class='container'>
<img style="width:100%;" src="https://http.cat/{{ code }}.jpg" />
</div>
</section>
{% endblock %}

139
views/index.html Normal file
View File

@ -0,0 +1,139 @@
{% extends 'templates/base.html' %}
{% block head %}
{{ super() }}
<link href="/static/css/index.css" rel="stylesheet">
{% endblock %}
{% block main %}
<script src="/static/js/index.js"></script>
<section class='splash dark' id='splash'>
<div class='container'>
<h1>Tracman</h1>
<h3>Let friends track your GPS location in realtime</h3>
<a class='btn' href="#" data-scrollto="overview">More info<i class='fa fa-angle-down'></i></a>
{% if not user %}
<a class='btn' href="#" data-scrollto="get">Request invite<i class='fa fa-angle-down'></i></a>
{% endif %}
<a class='btn' href="/trac/keith">View example<i class='fa fa-angle-right'></i></a>
{% if user %}
<a class='btn' href="/dashboard">Dashboard<i class='fa fa-angle-right'></i></a>
{% endif %}
</div>
</section>
<section class='overview dark' id='overview'>
<div class='container'>
<div>
<i class='fa fa-mobile'></i>
<h3>Easy-to-use</h3>
<p>Download the android app and log in. Then send your friends a link with a map showing your live location. </p>
</div>
<div>
<i class='fa fa-bolt'></i>
<h3>Realtime</h3>
<p>Your location updates every second for all the world to see. </p>
</div>
<div>
<i class='fa fa-usd'></i>
<h3>Free</h3>
<p>It's free, but you can <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=hypergeek14%40gmail%2ecom&lc=US&item_name=Tracman&currency_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted">donate</a> if you want to help with server expenses. </p>
</div>
</div>
</section>
<section class='feature app dark' id='app'>
<div class='container'>
<img src="/static/img/style/phone.png" alt="Mobile phone">
<div>
<h2>The App</h2>
<p>Tracman uses an android app to recieve GPS location of your device. Sorry, there's no iPhone version yet. </p>
<ul>
<li>
<i class='fa fa-toggle-on'></i>
<h4>On/off switch</h4>
<p>If you need to go undercover, just turn tracman off with the flip of a switch. </p>
</li>
<li>
<i class='fa fa-cog'></i>
<h4>Settings</h4>
<p>Change your settings to show a less accurate location, if you want an air of mystery. </p>
</li>
<li>
<i class='fa fa-battery-3'></i>
<h4>Saves energy</h4>
<p>If nobody's tracking you, tracman won't needlessly drain your battery. </p>
</li>
</ul>
</div>
</div>
</section>
<section class='feature tracpage dark' id='tracpage'>
<div class='container'>
<img src="/static/img/style/laptop.png" alt="Laptop">
<div>
<h2>The Map</h2>
<p>You'll get a simple webpage with a map to send to friends. <a href="/trac/keith">Click here for a demo</a>. </p>
<ul>
<li>
<i class='fa fa-hand-o-right'></i>
<h4>Easy</h4>
<p>Just send a link to whomever you want. Bam, now they know exactly where you are. </p>
</li>
<li>
<i class='fa fa-map-marker'></i>
<h4>Precise</h4>
<p>Map updates in realtime using the fancy-pants websockets protocol. </p>
</li>
<li>
<i class='fa fa-cogs'></i>
<h4>Customizable</h4>
<p>You can change some things. Not a lot yet, but I am always adding features. </p>
</li>
</ul>
</div>
</div>
</section>
<section class='disclaimer light' id='disclaimer'>
<div class='container'>
<h2>Warning! </h2>
<h3>I assume no responsibility for anything whatsoever. </h3>
<p>If you haven't realized it already, there are a lot of reasons why publishing your location online could be a bad idea. </p>
<ul>
<li>You get caught cheating</li>
<li>Your boss knows you aren't at work</li>
<li>The FBI hunts you down</li>
</ul>
<p>I'm not taking any responsibilty for the code either. Tracman is in beta and is very buggy. If it stops working, don't blame me! Heck, I could even accidentally delete all your data. Or it could get hacked by Russians. If using amateur software scares you, DON'T DO IT! </p>
<p>For more information, you can read the <a href="/license">license and copyright information</a>. </p>
</div>
</section>
{% if not user %}
<section class='get light' id='get'>
<div class='container'>
<h2>Hook me up!</h2>
<h3>Right now, Tracman is invite-only. You can beg me for access here. </h3>
<form id='invite-form' method="post">
<label class='input'><span>Name</span><input type="text" name="name" required></label>
<label class='input'><span>Email address</span><input type="email" name="email" required></label>
<label class='message'><span>Why you deserve beta access (optional)</span><textarea id='why' name="why"></textarea></label>
<label class='checkbox'><input type="checkbox" name="disclaimer" required><span>I've read the <a href="#" data-scrollto="disclaimer">scary warning</a>. </span></label>
<label class='submit'>
{% if inviteSuccess.length > 0 %}
<div class='alert alert-success'><i class="fa fa-check-circle"></i> {{ inviteSuccess }}</div>
{% elif inviteError.length > 0 %}
<div class='alert alert-danger'><i class="fa fa-exclamation-circle"></i> {{ inviteError }}</div>
{% else %}
<button type="submit" class='btn'>Request Invite<i class='fa fa-angle-right'></i></button>
{% endif %}
</label>
</form>
</div>
</section>
{% endif %}
{% endblock %}

31
views/license.html Normal file
View File

@ -0,0 +1,31 @@
{% extends 'templates/base.html' %}
{% block title %}{{ super() }} | License{% endblock %}
{% block main %}
<section class='dark'>
<div class='container'>
<h2>The MIT License (MIT)</h2>
<h3>Copyright &copy; 2016 Keith Irwin</h3>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions: </p>
<p>The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software. </p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. </p>
</div>
</section>
{% endblock %}

43
views/pro.html Normal file
View File

@ -0,0 +1,43 @@
{% extends 'templates/base.html' %}
{% block title %}{{ super() }} | Pro{% endblock %}
{% block main %}
<section class='dark'>
<div class='container'>
<h1>Tracman Pro</h1>
<h3>A word from the developer</h3>
<p>Hi Folks, </p>
<p>Glad you're enjoying my website and app. I made the whole thing, from front to backend,
and I'm really proud of it! However, I'm a long-haul trucker by day and coding is just a hobby.
I don't make any money off this website, and I pay the server fees out of my own pocket. Do you
pity me enough to donate some money by <a href="https://www.paypal.com/us/cgi-bin/webscr?cmd=_flow&SESSION=56toCNZRvXj-_J6kC_5D258zMwjyU7hLvxKFQXsIG5_nzVlAhEw2VLFf7K0&dispatch=5885d80a13c0db1f8e263663d3faee8defcd6970d4fd9d661117ac2649af92bb">paypal</a>
or <a href="bitcoin:1GXbuM398Y2tYwSaQhNQHXWF1AN6GeRgQh?label=tracman">bitcoin</a>? </p>
<p>To make a little money off this service, I'm going to be offering a pro version with more
features. It'll be cheap, probably $1 or $2 per month. However, while Tracman is in beta,
you can beta test the pro version too. Be sure to inform me about any <a href="/bug">bugs</a>
you encounter or <a href="/suggestion">suggestions</a> you have. And keep in mind that at some
point, when we launch out of beta, Tracman Pro will <em>not</em> be free and <strong>you will
lose your pro membership</strong> unless start paying for it.
<p>That said, just click the button below to test out the pro features. Keep in mind, they are
as <a href="/#disclaimer">unstable</a> as the rest of this product.
<p>Cheers, <br>
<a href="https://keithirwin.us/">Keith Irwin</a></p>
<form class='row flexbox' action="#" method="POST">
{% if user.isPro %}
<div class='alert alert-success'><i class="fa fa-check-circle"></i> You are already pro! </div>
{% else %}
<button type="submit" class='btn yellow'>GO PRO</button>
{% endif %}
<a class='btn' href="/dashboard">go home</a>
</form>
</div>
</section>
{% endblock %}

48
views/suggestion.html Normal file
View File

@ -0,0 +1,48 @@
{% extends 'templates/base.html' %}
{% block title %}{{ super() }} | Suggestion{% endblock %}
{% block main %}
<section class='dark'>
<div class='container'>
<h1>Suggest a Feature</h1>
<p>You can use this form to suggest new features. Or maybe you need to <a href="/bug">submit a bug report</a> instead? </p>
<script src="/static/js/validator.min.js"></script>
<form id='suggestions-form' class='col-lg-10 col-lg-offset-1 form-horizontal' role="form" method="POST" data-toggle="validator">
<div class='form-group' id='suggestion'>
<label class='control-label col-sm-2 col-lg-3' for="suggestion">What feature could improve Tracman? </label>
<div class='input-group col-xs-12 col-sm-10 col-lg-9'>
<textarea class='form-control' name="suggestion" rows="5" required maxlength="2400" data-error="You have to make a suggestion! "></textarea>
</div>
<div class='help-block with-errors col-xs-12 col-sm-10 col-sm-offset-2 col-lg-9 col-lg-offset-3'></div>
</div>
<div class='form-group' id='name' title="Put your name here if you want. ">
<label class='control-label col-sm-2 col-lg-3' for="name">Name</label>
<div class='input-group col-xs-12 col-sm-10 col-lg-9'>
<input class='form-control' name="name" type="text" value="{{ user.name }}"
maxlength="160"><br>
</div>
<div class='help-block with-errors col-xs-12 col-sm-10 col-sm-offset-2 col-lg-9 col-lg-offset-3'></div>
</div>
<div class='form-group' id='email' title="Put an email address if you want a reply. ">
<label class='control-label col-sm-2 col-lg-3' for="email">Email</label>
<div class='input-group col-xs-12 col-sm-10 col-lg-9'>
<input class='form-control' name="email" type="email" value="{{ user.email }}"
maxlength="160" data-error="That's not an email address"><br>
</div>
<div class='help-block with-errors col-xs-12 col-sm-10 col-sm-offset-2 col-lg-9 col-lg-offset-3'></div>
</div>
<div class='form-group flexbox' id='buttons'>
<button type="submit" class='btn yellow'>SUBMIT</button>
<a class='btn' href="/dashboard">nevermind</a>
</div>
</form>
</div>
</section>
{% endblock %}

62
views/templates/base.html Normal file
View File

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html>
<head>
{% block head %}
<title>{% block title %}Tracman{% endblock %}</title>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0">
<meta name="keywords" content="map, phone, gps, link, location, track, friends, app">
<meta name="description" content="Tracman lets you see and share your phone's exact realtime location">
<meta name="theme-color" content="#222">
<meta name="msapplication-navbutton-color" content="#222">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="shortcut icon" sizes="16x16 32x32 48x48" type="image/x-icon" href="/static/img/icon/by/16-32-48.ico">
<link rel="icon apple-touch-icon" sizes="32x32" type="image/png" href="/static/img/icon/by/32.png">
<link rel="icon apple-touch-icon" sizes="57x57" type="image/png" href="/static/img/icon/by/57.png">
<link rel="icon apple-touch-icon" sizes="72x72" type="image/png" href="/static/img/icon/by/72.png">
<link rel="icon apple-touch-icon" sizes="128x128" type="image/png" href="/static/img/icon/by/128.png">
<link rel="icon apple-touch-icon" sizes="152x152" type="image/png" href="/static/img/icon/by/152.png">
<link rel="icon apple-touch-icon" sizes="228x228" type="image/png" href="/static/img/icon/by/228.png">
<link rel="apple-touch-icon-precomposed" type="image/png" href="/static/img/icon/by/152.png">
<link rel="stylesheet" type="text/css" href="/static/css/bootstrap.css">
<link href="/static/css/base.css" rel="stylesheet">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300,600">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Merriweather:300,700">
<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
{% endblock %}
{% if not noHeader %}<link href="/static/css/header.css" rel="stylesheet">{% endif %}
{% if not noFooter %}<link href="/static/css/footer.css" rel="stylesheet">{% endif %}
</head>
<body>
<noscript>
<div class='header alert alert-danger alert-dismissible'>
<strong>Uh-oh!</strong> You don't have javascript enabled! This page won't load correctly without it. You should really enable it, because many websites won't work properly. Ask your grandchildren if you need help.
<a href="#" class='close' data-dismiss="alert" aria-label="close"><i class='fa fa-times'></i></a>
</div>
</noscript>
{% if not noHeader %}{% include 'templates/header.html' %}{% endif %}
{% block main %}Loading... {% endblock %}
{% if not noFooter %}{% include 'templates/footer.html' %}{% endif %}
<script>
(function(t,r,a,c,m,o,n){t['GoogleAnalyticsObject']=m;t[m]=t[m]||function(){
(t[m].q=t[m].q||[]).push(arguments)},t[m].l=1*new Date();o=r.createElement(a),
n=r.getElementsByTagName(a)[0];o.async=1;o.src=c;n.parentNode.insertBefore(o,n)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-44266909-3', 'auto');
ga('require', 'linkid');
ga('send', 'pageview');
</script>
</body>
</html>

View File

@ -0,0 +1,16 @@
<footer class='footer'>
<div class='left'>
<p>Website and app by <a href="https://keithirwin.us/">Keith Irwin</a>.
<br>Design by <a href="http://boag.online/blog/maglev-free-responsive-website-template">Fraser Boag</a>. </p>
</div>
<div class='right'>
Share:
<a href="https://www.facebook.com/sharer/sharer.php?u=https://tracman.org/" target="_blank"><i class="fa fa-facebook"></i></a>
<a href="https://twitter.com/home?status=Show%20your%20location%20to%20friends%20in%20realtime%20at%20https://tracman.org/" target="_blank"><i class="fa fa-twitter"></i></a>
<a href="https://www.reddit.com/submit?title=Show%20your%20location%20to%20friends%20in%20realtime%20at%20&url=https://tracman.org/" target="_blank"><i class="fa fa-reddit-alien"></i></a>
<br>Contribute:
<a href="https://github.com/Tracman-org/Server"><i class="fa fa-github"></i></a>
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=hypergeek14%40gmail%2ecom&lc=US&item_name=Tracman&currency_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted" target="_blank"><i class="fa fa-paypal"></i></a>
<a href="bitcoin:1GXbuM398Y2tYwSaQhNQHXWF1AN6GeRgQh?label=tracman"><i class="fa fa-btc"></i></a>
</div>
</footer>

View File

@ -0,0 +1,43 @@
<script src="/static/js/header.js"></script>
<header>
<a href="/"><span class='logo'><img class='icon' src="/static/img/style/logo-28.png" alt="+">Tracman</span></a>
<div class='hamburger hamburger--slider' aria-label="Menu" aria-controls="navigation">
<div class='hamburger-box'>
<div class='hamburger-inner'></div>
</div>
</div>
<nav id='navigation'>
<ul>
{% if not user %}
<li><a href="/#overview">About</a></li>
<li><a href="/#get">Request Invite</a></li>
<li><a href="/login">Login</a></li>
{% else %}
{% if user.isAdmin %}
<li><a href="/admin/users">Users</a></li>
<li><a href="/admin/requests">Invite Requests</a></li>
{% endif %}
<li><a href="/dashboard">Dashboard</a></li>
<li><a href="/trac/{{ user.slug }}">Map</a></li>
<li><a href="/logout">Logout</a></li>
{% endif %}
</ul>
</nav>
</header>
{% if error %}
<div class='header alert alert-danger alert-dismissible'>
<strong><i class="fa fa-exclamation-circle"></i> ERROR:</strong> {{ error | safe }}
<a href="#" class='close' data-dismiss="alert" aria-label="close"><i class='fa fa-times'></i></a>
</div>
{% elif success %}
<div class='header alert alert-success alert-dismissible'>
<strong><i class="fa fa-check-circle"></i> Success!</strong> {{ success | safe }}
<a href="#" class='close' data-dismiss="alert" aria-label="close"><i class='fa fa-times'></i></a>
</div>
{% endif %}

219
views/trac.html Normal file
View File

@ -0,0 +1,219 @@
{% extends 'templates/base.html' %}
{% block title %}{{ super() }} | {{ tracuser.name }}{% endblock %}
{% block head %}
{{ super() }}
<link href="/static/css/trac.css" rel="stylesheet">
<style>
.wrap { top:{% if not noHeader %}58{% else %}0{% endif %}px;}
img#panoImg { width:100%; height:100%; }
{% if tracuser.settings.showStreetview and disp!='0' and disp!='1' %}
/* show both */
@media (orientation: landscape) {
#map, #pano {
display:inline-block;
width:50%;
height:99%;
}
#pano { float:right; }
}
@media (orientation: portrait) {
#map, #pano {
width:100%;
height:50%;
}
#pano { bottom:0; }
}
{% elif tracuser.settings.showStreetview and disp=='1' %}
/* show streetview */
#pano {
width:100%;
height:100%;}
#map {display:none;}
{% else %}
/* show map */
#map {
width:100%;
height:100%;}
#pano {display:none;}
{% endif %}
</style>
{% endblock %}
{% block main %}
<div class='wrap'>
{% if not tracuser.last.time %}
<div class='centered alert alert-warning'>
<b>No Location Found</b>
{% if user.id == tracuser.id %}
<br>You can <a href="#" onclick="setLocation()">click here to set use this device's location</a>.
<br>Maybe you also want to <a href="/android">download the android app</a>.
{% else %}
<br>This user hasn't updated their location yet!
{% endif %}
</div>
{% else %}
<div id='map'><i class='loading fa fa-refresh fa-spin'></i></div>
{% if tracuser.settings.showStreetview and not noStreetview.length %}
<div id='pano'><img id='panoImg' src="https://maps.googleapis.com/maps/api/streetview?size=800x800&location={{ tracuser.last.lat }},{{ tracuser.last.lon }}&fov=90&heading={{ tracuser.last.dir|default(0) }}&key={{ api }}" alt="Street view image"></div>
{% endif %}
{% endif %}
</div>
<script src="https://cdn.socket.io/socket.io-1.2.0.js"></script>
<script src="https://maps.googleapis.com/maps/api/js?key={{ api }}&callback=initMap" async defer></script>
<script>
var socket = io.connect();
var tracuserid = {{ tracuser._id | dump | safe }},
settings = JSON.parse('{{ tracuser.settings | dump | safe }}'),
last = JSON.parse('{{ tracuser.last | dump | safe }}'),
noHeader = {{ noHeader | dump | safe }},
disp = {{ disp | dump | safe }};
// Set location manually
function setLocation() {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function(pos){
socket.emit('app', {
usr: '{{ user.id }}',
lat: pos.coords.latitude,
lon: pos.coords.longitude,
spd: (pos.coords.speed||0)
});
alert('Location updated! ');
}, function(err){
alert('ERROR: '+err);
}, { enableHighAccuracy:true });
} else { alert('Unable to set location. '); }
}
var getAltitude = function(location, elevator, cb){
elevator = elevator || new google.maps.ElevationService;
elevator.getElevationForLocations({
'locations': [location]
}, function(results, status) {
if (status === google.maps.ElevationStatus.OK && results[0]) {
cb(results[0].elevation);
}
});
}
var map, marker, elevator;
function initMap() {
// Create map
if (disp!='1' || !settings.showStreetview) {
map = new google.maps.Map( document.getElementById('map'), {
center: new google.maps.LatLng( last.lat, last.lon ),
panControl: false,
draggable: false,
zoom: settings.defaultZoom,
streetViewControl: false,
zoomControlOptions: {position: google.maps.ControlPosition.LEFT_TOP},
mapTypeId: (settings.defaultMap=='road')?google.maps.MapTypeId.ROADMAP:google.maps.MapTypeId.HYBRID
});
marker = new google.maps.Marker({
position: { lat:last.lat, lng:last.lon },
title: {{ tracuser.name | dump | safe }},
map: map,
draggable: false
});
// Create iFrame logo
if (noHeader.length) {
var logoDiv = document.createElement('div');
logoDiv.className = 'map-logo';
logoDiv.innerHTML = '<a href="https://tracman.org/">'+
'<img src="https://tracman.org/static/img/style/logo-28.png" alt="[]">'+
'Tracman</a>';
map.controls[google.maps.ControlPosition.BOTTOM_LEFT].push(logoDiv);
}
// Create update time block
var timeDiv = document.createElement('div');
timeDiv.className = 'tim';
if (last.time) {
timeDiv.innerHTML = 'location updated '+new Date(last.time).toLocaleString();
}
map.controls[google.maps.ControlPosition.RIGHT_BOTTOM].push(timeDiv);
// Create speed block
if (settings.showSpeed) {
var speedSign = document.createElement('div'),
speedLabel = document.createElement('div'),
speedText = document.createElement('div'),
speedUnit = document.createElement('div');
speedLabel.className = 'spd-label';
speedText.className = 'spd';
speedUnit.className = 'spd-unit';
speedSign.className = 'spd-sign';
speedText.innerHTML = (settings.units=='imperial')?(parseFloat(last.spd)*2.23694).toFixed():last.spd.toFixed();
speedLabel.innerHTML = 'SPEED';
speedUnit.innerHTML = (settings.units=='imperial')?'m.p.h.':'k.p.h.';
speedSign.appendChild(speedLabel);
speedSign.appendChild(speedText);
speedSign.appendChild(speedUnit);
map.controls[google.maps.ControlPosition.TOP_RIGHT].push(speedSign);
}
// Create altitude block
if (settings.showAlt) {
var elevator = new google.maps.ElevationService;
var altitudeSign = document.createElement('div'),
altitudeLabel = document.createElement('div'),
altitudeText = document.createElement('div'),
altitudeUnit = document.createElement('div');
altitudeLabel.className = 'alt-label';
altitudeText.className = 'alt';
altitudeUnit.className = 'alt-unit';
altitudeSign.className = 'alt-sign';
altitudeText.innerHTML = '';
altitudeLabel.innerHTML = 'ALTITUDE';
getAltitude(new google.maps.LatLng(last.lat,last.lon), elevator, function(alt) {
if (alt) { altitudeText.innerHTML = (settings.units=='imperial')?(alt*3.28084).toFixed():alt.toFixed(); }
});
altitudeUnit.innerHTML = (settings.units=='imperial')?'feet above sea level':'meters above sea level';
altitudeSign.appendChild(altitudeLabel);
altitudeSign.appendChild(altitudeText);
altitudeSign.appendChild(altitudeUnit);
map.controls[google.maps.ControlPosition.TOP_RIGHT].push(altitudeSign);
}
}
// Create streetview
if (settings.showStreetview && disp!='0') {
document.getElementById('panoImg').src='https://maps.googleapis.com/maps/api/streetview?size='+$('#pano').outerWidth()+'x'+$('#pano').outerHeight()+'&location='+last.lat+','+last.lon+'&fov=90&heading='+(last.dir||0)+'&key={{ api }}';
}
}
// Sockets
socket.on('connect', function(){
socket.emit('room', tracuserid);
});
var dir, lat, lon, spd, alt, newLocation;
socket.on('trac', function(loc){
spd = (settings.units=='imperial')?parseFloat(loc.spd)*2.23694:parseFloat(loc.spd);
dir = parseFloat(loc.dir);
lat = parseFloat(loc.lat);
lon = parseFloat(loc.lon);
newLocation = new google.maps.LatLng(loc.lat,loc.lon)
if (disp!='1' || !settings.showStreetview) {
$('.tim').text('location updated '+new Date(loc.time).toLocaleString());
if (settings.showSpeed) { $('.spd').text(spd.toFixed()); }
if (settings.showAlt) {
getAltitude(newLocation, elevator, function(alt) {
if (alt) { $('.alt').text((settings.units=='imperial')?(alt*3.28084).toFixed():alt.toFixed()); }
});
}
map.setCenter(newLocation);
marker.setPosition(newLocation);
}
if (settings.showStreetview && disp!='0') {
document.getElementById('panoImg').src='https://maps.googleapis.com/maps/api/streetview?size='+$('#pano').width()+'x'+$('#pano').height()+'&location='+loc.lat+','+loc.lon+'&fov=90&heading='+dir+'&key={{ api }}';
}
});
</script>
{% endblock %}