init commit
|
@ -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.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Tracman
|
||||
node.js application to display a map with user's location
|
||||
The github for the associated android app is [keith24/tracman-android](https://github.com/keith24/tracman-android).
|
|
@ -0,0 +1 @@
|
|||
{ "_id" : { "$oid" : "56b020279f740067540f96e9" }, "googleID" : 1.044820980603112e+20, "slug" : "keith", "email" : "mail@keithirwin.us", "name" : "Keith", "created" : { "$date" : "2016-02-01T22:19:03.062-0500" }, "lastLogin" : { "$date" : "2016-03-11T02:00:47.868-0500" }, "last" : { "time" : { "$date" : "2016-03-11T04:29:49.046-0500" }, "spd" : 0, "dir" : 0, "lon" : -74.8288264, "lat" : 39.9087593 }, "settings" : { "showStreetview" : false, "showAlt" : true, "showSpeed" : true, "defaultZoom" : 11, "defaultMap" : "sat", "units" : "imperial" }, "isAdmin" : true, "isPro" : true }
|
|
@ -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.user) { // 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);
|
||||
}
|
||||
});
|
||||
}));
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
|
||||
};
|
|
@ -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
|
||||
});
|
|
@ -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
|
||||
}
|
||||
});
|
|
@ -0,0 +1,371 @@
|
|||
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());
|
||||
|
||||
// Test authentication/admin
|
||||
function ensureAuthenticated(req,res,next) {
|
||||
if (req.isAuthenticated()) { return next(); }
|
||||
else { 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!=undefined) {
|
||||
User.findById(req.session.passport.user, function(err, user){
|
||||
if (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>?'); }
|
||||
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) { 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>?');
|
||||
res.redirect('/');
|
||||
} else if (!err && request == null) { // 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) { 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>?');
|
||||
res.redirect('/');
|
||||
} else {
|
||||
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){ 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>?');
|
||||
}
|
||||
req.flash('request-success', 'Invite requested! ');
|
||||
res.redirect('/#get');
|
||||
});
|
||||
}
|
||||
});
|
||||
} else { // Already requested with this email
|
||||
req.flash('request-error', 'Invite alreay requested! ');
|
||||
res.redirect('/#get');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.route('/dashboard')
|
||||
.all(ensureAuthenticated, function(req,res,next){
|
||||
next();
|
||||
}).get(function(req,res){
|
||||
User.findById(req.session.passport.user, function(err, user){
|
||||
if (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>?');}
|
||||
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){ 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>?');
|
||||
} 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:req.query.slug}, function(err, existingUser){
|
||||
if (existingUser && existingUser.id!==req.session.passport.user) { res.sendStatus(400); }
|
||||
else { res.sendStatus(200); }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/trac', function(req,res,next){
|
||||
User.findById(req.session.passport.user, function(err, user){
|
||||
if (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>?'); }
|
||||
if (user) { res.redirect('/trac/'+user.slug); }
|
||||
else { next(); }
|
||||
});
|
||||
});
|
||||
app.get('/trac/:slug', function(req,res,next){
|
||||
User.findOne({slug:req.params.slug}, function(err, tracuser) {
|
||||
if (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>?'); }
|
||||
if (!tracuser){ next(); }
|
||||
else {
|
||||
res.render('trac.html', {
|
||||
api: secret.mapAPI,
|
||||
user: req.user,
|
||||
tracuser: tracuser,
|
||||
noHeader: req.query.noheader
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
app.get('/trac/id/:id', function(req,res){
|
||||
User.findById(req.params.id, function(err, user){
|
||||
if (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>?'); }
|
||||
res.redirect('/trac/'+user.slug);
|
||||
});
|
||||
});
|
||||
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) { console.log('routes.js:125 ERROR: '+err); }
|
||||
if (!request.granted) { 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) { console.log('routes.js:141 ERROR: '+err); }
|
||||
User.findOne({requestId:request._id}, function(err, user) {
|
||||
if (err) { console.log('routes.js:143 ERROR: '+err); }
|
||||
if (user) {
|
||||
request.userId = user._id;
|
||||
request.save(function(err, raw){
|
||||
if (err) { console.log('routes.js:147 ERROR: '+err); }
|
||||
});
|
||||
req.logIn(user, function(err) {
|
||||
if (err) { console.log('routes.js:150: logIn() ERROR: '+err); }
|
||||
user.lastLogin = Date.now();
|
||||
user.save(function(err, raw) {
|
||||
if (err) { console.log('routes.js:153 ERROR: '+err); }
|
||||
res.redirect('/login');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/android', ensureAuthenticated, 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(ensureAuthenticated, function(req,res,next){
|
||||
next();
|
||||
}).get(function(req,res){
|
||||
User.findById(req.session.passport.user, function(err, user){
|
||||
if (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>?'); }
|
||||
res.render('pro.html', {user:user});
|
||||
});
|
||||
}).post(function(req,res){
|
||||
User.findByIdAndUpdate(req.session.passport.user,
|
||||
{$set:{ isPro:true }},
|
||||
function(err, user){
|
||||
if (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>?');
|
||||
} 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) { 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>?');
|
||||
} else { req.flash('success', 'Thanks for the suggestion! '); }
|
||||
res.redirect('/dashboard');
|
||||
});
|
||||
});
|
||||
app.route('/bug')
|
||||
.all(ensureAuthenticated, 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) { 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>?');
|
||||
} else { req.flash('success', 'Thanks for the report! '); }
|
||||
res.redirect('/dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
// ADMIN
|
||||
app.route('/admin/requests')
|
||||
.all([ensureAuthenticated, ensureAdmin], function(req,res,next){
|
||||
next();
|
||||
}).get(function(req,res){
|
||||
User.findById(req.session.passport.user, function(err, user){
|
||||
if (err){ console.log(err);
|
||||
req.flash('error', err.message); }
|
||||
Request.find({}, function(err, requests){
|
||||
if (err) { console.log(err);
|
||||
req.flash('error', err.message); }
|
||||
res.render('admin/requests.html', {
|
||||
user: user,
|
||||
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){ console.log(err);
|
||||
req.flash('error', err.message); }
|
||||
mail.sendInvite(request, function (err, raw) {
|
||||
if (err) { console.log(err);
|
||||
req.flash('error', err.message); }
|
||||
request.granted = Date.now();
|
||||
request.save(function(err) {
|
||||
if (err) { console.log(err);
|
||||
req.flash('error', err.message); }
|
||||
});
|
||||
req.flash('success', 'Invitation sent to <i>'+request.name+'</i>.');
|
||||
res.redirect('/admin/requests');
|
||||
});
|
||||
});
|
||||
});
|
||||
app.get('/admin/users', [ensureAuthenticated, ensureAdmin], function(req,res){
|
||||
User.findById(req.session.passport.user, function(err, user){
|
||||
if (err){ console.log(err);
|
||||
req.flash('error', err.message); }
|
||||
User.find({}, function(err, users){
|
||||
if (err) { console.log(err);
|
||||
req.flash('error', err.message); }
|
||||
res.render('admin/users.html', {
|
||||
user: user,
|
||||
users: users,
|
||||
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',
|
||||
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);}
|
||||
} );
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"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": {},
|
||||
"scripts": {
|
||||
"test": "test.js",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/keith24/tracman-server.git"
|
||||
},
|
||||
"keywords": [
|
||||
"tracking",
|
||||
"location",
|
||||
"map"
|
||||
],
|
||||
"author": "Keith Irwin",
|
||||
"license": "MIT",
|
||||
"README": "README.md",
|
||||
"bugs": {
|
||||
"url": "https://github.com/keith24/tracman-server/issues"
|
||||
},
|
||||
"homepage": "http://tracman.org/"
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
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.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});
|
||||
};
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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(room).emit('trac', user.last);
|
||||
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(secret.httpPort, function(){
|
||||
console.log('Listening for http on port '+secret.httpPort.toString());
|
||||
checkForUsers();
|
||||
});
|
||||
|
||||
module.exports = app;
|
|
@ -0,0 +1,194 @@
|
|||
/* 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 {
|
||||
font-size: 40px;
|
||||
line-height: 46px;
|
||||
font-weight: 600;
|
||||
}
|
||||
h2 {
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
font-weight: 600;
|
||||
}
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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:hover {
|
||||
color:inherit;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 391 B |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 731 B |
After Width: | Height: | Size: 870 B |
After Width: | Height: | Size: 1015 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 659 B |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 734 B |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 6.0 KiB |
After Width: | Height: | Size: 681 B |
After Width: | Height: | Size: 9.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 552 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 296 KiB |
After Width: | Height: | Size: 101 KiB |
|
@ -0,0 +1,6 @@
|
|||
$(document).ready(function(){
|
||||
$(".hamburger").click(function(){
|
||||
$(".hamburger").toggleClass("is-active");
|
||||
$('nav').toggleClass('visible');
|
||||
});
|
||||
});
|
|
@ -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(){});
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
{% extends 'templates/base.html' %}
|
||||
{% block title %}Tracman | Invite Requests{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'templates/header.html' %}
|
||||
<style>
|
||||
.container { max-width:90%; }
|
||||
</style>
|
||||
|
||||
<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 %}
|
|
@ -0,0 +1,61 @@
|
|||
{% extends 'templates/base.html' %}
|
||||
{% block title %}Tracman | Users{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'templates/header.html' %}
|
||||
<style>
|
||||
.container { max-width:90%; }
|
||||
</style>
|
||||
|
||||
<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 %}
|
|
@ -0,0 +1,71 @@
|
|||
{% extends 'templates/base.html' %}
|
||||
{% block title %}Tracman | Bug Report{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'templates/header.html' %}
|
||||
|
||||
<section class='dark'>
|
||||
<div class='container'>
|
||||
<h1>Submit a Bug Report</h1>
|
||||
|
||||
<p>You can use this form to submit a bug report. 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>
|
||||
<!--
|
||||
col-xs-4 col-xs-offset-1 col-lg-3 col-lg-offset-2
|
||||
col-xs-4 col-xs-offset-1 col-lg-3 col-lg-offset-1
|
||||
-->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% include 'templates/footer.html' %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,160 @@
|
|||
{% extends 'templates/base.html' %}
|
||||
{% block title %}Tracman | Dashboard{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'templates/header.html' %}
|
||||
<style>
|
||||
.google-play {
|
||||
width:150px;
|
||||
border: #fbc93d solid 1px;
|
||||
border-radius:10px; }
|
||||
</style>
|
||||
|
||||
<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><iframe src="https://tracman.org/trac/{{ user.slug }}?noheader=1" width="90%" style="height:90vh;"></iframe></pre>
|
||||
|
||||
<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> Update location manually</button></p>
|
||||
|
||||
<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>
|
||||
|
||||
{% include 'templates/footer.html' %}
|
||||
<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! ');
|
||||
}, function(err){
|
||||
alert('ERROR: '+err);
|
||||
}, { enableHighAccuracy:true });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,14 @@
|
|||
{% extends 'templates/base.html' %}
|
||||
{% block title %}Tracman | {{ code }} Error{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'templates/header.html' %}
|
||||
|
||||
<section class='dark'>
|
||||
<div class='container'>
|
||||
<img style="width:100%;" src="https://http.cat/{{ code }}.jpg" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% include 'templates/footer.html' %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,139 @@
|
|||
{% extends 'templates/base.html' %}
|
||||
|
||||
{% block main %}
|
||||
<link href="/static/css/index.css" rel="stylesheet">
|
||||
<script src="/static/js/index.js"></script>
|
||||
|
||||
{% include 'templates/header.html' %}
|
||||
|
||||
<section class='splash dark' id='splash' name='splash'>
|
||||
<div class='container'>
|
||||
<h1>Tracman</h1>
|
||||
<h2>Let friends track your GPS location in realtime</h2>
|
||||
<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' name='overview'>
|
||||
<div class='container'>
|
||||
<div>
|
||||
<i class='fa fa-mobile'></i>
|
||||
<h2>Easy-to-use</h2>
|
||||
<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>
|
||||
<h2>Realtime</h2>
|
||||
<p>Your location updates every second for all the world to see. </p>
|
||||
</div>
|
||||
<div>
|
||||
<i class='fa fa-usd'></i>
|
||||
<h2>Free</h2>
|
||||
<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¤cy_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' name='app'>
|
||||
<div class='container'>
|
||||
<img src="/static/img/style/phone.png" alt="Mobile phone">
|
||||
<div>
|
||||
<h1>The App</h1>
|
||||
<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>
|
||||
<h3>On/off switch</h3>
|
||||
<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>
|
||||
<h3>Settings</h3>
|
||||
<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>
|
||||
<h3>Saves energy</h3>
|
||||
<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' name='tracpage'>
|
||||
<div class='container'>
|
||||
<img src="/static/img/style/laptop.png" alt="Laptop">
|
||||
<div>
|
||||
<h1>The Map</h1>
|
||||
<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>
|
||||
<h3>Easy</h3>
|
||||
<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>
|
||||
<h3>Precise</h3>
|
||||
<p>Map updates in realtime using the fancy-pants websockets protocol. </p>
|
||||
</li>
|
||||
<li>
|
||||
<i class='fa fa-cogs'></i>
|
||||
<h3>Customizable</h3>
|
||||
<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' name='disclaimer'>
|
||||
<div class='container'>
|
||||
<h1>Warning! </h1>
|
||||
<h2>I assume no responsibility for anything whatsoever. </h2>
|
||||
<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' name='get'>
|
||||
<div class='container'>
|
||||
<h1>Hook me up!</h1>
|
||||
<h2>Right now, Tracman is invite-only. You can beg me for access here. </h2>
|
||||
<form id='invite-form' role="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</span><textarea id='why' name="why" required></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 %}
|
||||
|
||||
{% include 'templates/footer.html' %}
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,33 @@
|
|||
{% extends 'templates/base.html' %}
|
||||
{% block title %}Tracman | License{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'templates/header.html' %}
|
||||
|
||||
<section class='dark'>
|
||||
<div class='container'>
|
||||
<h2>The MIT License (MIT)</h2>
|
||||
<h3>Copyright © 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>
|
||||
|
||||
{% include 'templates/footer.html' %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,46 @@
|
|||
{% extends 'templates/base.html' %}
|
||||
{% block title %}Tracman | Pro{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'templates/header.html' %}
|
||||
|
||||
<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>
|
||||
|
||||
{% include 'templates/footer.html' %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,52 @@
|
|||
{% extends 'templates/base.html' %}
|
||||
{% block title %}Tracman | Suggestion{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'templates/header.html' %}
|
||||
|
||||
<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>
|
||||
|
||||
{% include 'templates/footer.html' %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,56 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<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>
|
||||
|
||||
</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>
|
||||
|
||||
{% block main %}Loading... {% endblock %}
|
||||
|
||||
<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>
|
|
@ -0,0 +1,17 @@
|
|||
<link href="/static/css/footer.css" rel="stylesheet">
|
||||
|
||||
<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" target="_blank">Fraser Boag</a>. </p>
|
||||
</div>
|
||||
<div class='right'>
|
||||
Share:
|
||||
<a href="https://twitter.com/home?status=Show%20your%20location%20to%20friends%20in%20realtime%20at%20http://tracman.org/" target="_blank"><i class="fa fa-twitter"></i></a>
|
||||
<a href="https://www.facebook.com/sharer/sharer.php?u=http://tracman.org/" target="_blank"><i class="fa fa-facebook"></i></a>
|
||||
<a href="https://www.reddit.com/submit?title=Show%20your%20location%20to%20friends%20in%20realtime%20at%20&url=http://tracman.org/" target="_blank"><i class="fa fa-reddit-alien"></i></a>
|
||||
<br>Donate:
|
||||
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=hypergeek14%40gmail%2ecom&lc=US&item_name=Tracman¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted"><i class="fa fa-paypal"></i></a>
|
||||
<a href="bitcoin:1GXbuM398Y2tYwSaQhNQHXWF1AN6GeRgQh?label=tracman"><i class="fa fa-btc"></i></a>
|
||||
</div>
|
||||
</footer>
|
|
@ -0,0 +1,44 @@
|
|||
<link href="/static/css/header.css" rel="stylesheet">
|
||||
<script src="/static/js/header.js"></script>
|
||||
|
||||
<header>
|
||||
<a href="/"><h1 class='logo'><img src="/static/img/style/logo-28.png" class='icon'>Tracman</h1></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 %}
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
{% extends 'templates/base.html' %}
|
||||
{% block title %}Tracman | {{ tracuser.name }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
<link href="/static/css/trac.css" rel="stylesheet">
|
||||
<style>
|
||||
.wrap { top:{% if not noHeader %}58{% else %}0{% endif %}px;}
|
||||
{% if tracuser.settings.showStreetview %}
|
||||
#map, #pano {display:inline-block;}
|
||||
@media (orientation: landscape) {
|
||||
#map, #pano {
|
||||
width:50%;
|
||||
height:99%;
|
||||
}
|
||||
#pano {float:right;}
|
||||
}
|
||||
@media (orientation: portrait) {
|
||||
#map, #pano {
|
||||
width:100%;
|
||||
height:50%;
|
||||
}
|
||||
#pano {bottom:0}
|
||||
}
|
||||
{% else %}
|
||||
#map {
|
||||
width:100%;
|
||||
height:100%;}
|
||||
#pano {display:none;}
|
||||
{% endif %}
|
||||
</style>
|
||||
|
||||
{% if not noHeader %}
|
||||
{% include 'templates/header.html' %}
|
||||
{% endif %}
|
||||
|
||||
<div class='wrap'>
|
||||
|
||||
{% if not tracuser.last.time %}
|
||||
<div class='centered alert alert-warning'>
|
||||
<strong>No Location Found</strong>
|
||||
<br>This user hasn't updated their location yet!
|
||||
<br>If this is your page, maybe you want to <a href="/android">download the app</a>.
|
||||
</div>
|
||||
{% else %}
|
||||
<div id='map'><i class='loading fa fa-refresh fa-spin'></i></div>
|
||||
{% if tracuser.settings.showStreetview %}
|
||||
<div id='pano'><i class='loading fa fa-refresh fa-spin'></i></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 }}';
|
||||
|
||||
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, pano, dir, lat, lon, spd, alt, newLocation, elevator;
|
||||
function initMap() {
|
||||
map = new google.maps.Map(document.getElementById('map'),{
|
||||
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({
|
||||
map: map,
|
||||
draggable: false
|
||||
});
|
||||
if (settings.showStreetview) {
|
||||
pano = new google.maps.StreetViewPanorama( document.getElementById('pano'), {
|
||||
panControl: false,
|
||||
zoomControl: false,
|
||||
addressControl: false,
|
||||
linksControl: false
|
||||
});
|
||||
}
|
||||
|
||||
// Create iFrame logo
|
||||
if (noHeader) {
|
||||
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 = '';
|
||||
speedLabel.innerHTML = 'SPEED';
|
||||
if (last.spd) {
|
||||
speedText.innerHTML = (settings.units=='imperial')?
|
||||
(parseFloat(last.spd)*2.23694).toFixed():last.spd;
|
||||
}
|
||||
if (settings.units=='imperial') {
|
||||
speedUnit.innerHTML = 'm.p.h.';
|
||||
} else {
|
||||
speedUnit.innerHTML = '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) {
|
||||
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';
|
||||
if (last.lon && settings.showAlt) {
|
||||
newLocation = new google.maps.LatLng(last.lat, last.lon);
|
||||
getAltitude(newLocation, elevator, function(alt) {
|
||||
if (alt) {
|
||||
alt = (settings.units=='imperial')?alt*3.28084:alt;
|
||||
altitudeText.innerHTML = alt.toFixed();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (settings.units=='imperial') {
|
||||
altitudeUnit.innerHTML = 'feet above sea level';
|
||||
} else {
|
||||
altitudeUnit.innerHTML = 'meters above sea level';}
|
||||
altitudeSign.appendChild(altitudeLabel);
|
||||
altitudeSign.appendChild(altitudeText);
|
||||
altitudeSign.appendChild(altitudeUnit);
|
||||
map.controls[google.maps.ControlPosition.TOP_RIGHT].push(altitudeSign);
|
||||
}
|
||||
}
|
||||
|
||||
socket.on('connect', function(){
|
||||
socket.emit('room', tracuserid);
|
||||
});
|
||||
|
||||
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);
|
||||
$('.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 = (settings.units=='imperial')?alt*3.28084:alt;
|
||||
$('.alt').text(alt.toFixed());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
map.setCenter(newLocation);
|
||||
marker.setPosition(newLocation);
|
||||
if (settings.showStreetview) {
|
||||
if (pano.position != newLocation) {
|
||||
pano.setPosition(newLocation);
|
||||
if (spd>0) {
|
||||
pano.setPov({pitch:0,heading:dir});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock %}
|