Initial commit.

This commit is contained in:
Jonathan Cremin 2015-07-09 23:01:43 +01:00
commit b48a4e92e1
169 changed files with 7538 additions and 0 deletions

170
web/app.js Normal file
View file

@ -0,0 +1,170 @@
import path from 'path';
import spdy from 'spdy';
import koa from 'koa';
import route from 'koa-route';
import views from 'koa-views';
import logger from 'koa-logger';
import favicon from 'koa-favicon';
import redisStore from 'koa-redis';
import compress from 'koa-compress';
import bodyparser from 'koa-bodyparser';
import session from 'koa-generic-session';
import staticHandler from 'koa-file-server';
import co from 'co';
import redis from 'redis-url';
import coRedis from 'co-redis';
import raven from 'raven';
// waiting for PR to be merged, can remove swig dependency when done
import errors from '../lib/koa-error';
import mongoConnect from '../config/mongo';
import * as index from './routes/index';
import * as file from './routes/file';
import * as pro from './routes/pro';
import * as user from './routes/user';
import mongodb from 'mongodb-promisified';
const objectId = mongodb().ObjectId;
import debugname from 'debug';
const debug = debugname('hostr-web');
if (process.env.SENTRY_DSN) {
const ravenClient = new raven.Client(process.env.SENTRY_DSN);
ravenClient.patchGlobal();
}
const redisUrl = process.env.REDIS_URL || process.env.REDISTOGO_URL || 'redis://localhost:6379';
const app = koa();
app.use(errors({template: path.join(__dirname, 'public', '404.html')}));
app.use(function*(next){
this.set('Server', 'Nintendo 64');
if(this.req.headers['x-forwarded-proto'] === 'http'){
return this.redirect('https://' + this.request.headers.host + this.request.url);
}
yield next;
});
app.use(function*(next){
this.state = {
apiURL: process.env.API_URL,
baseURL: process.env.BASE_URL,
stripePublic: process.env.STRIPE_PUBLIC_KEY
};
yield next;
});
const redisConn = redis.connect(redisUrl);
let coRedisConn = {};
co(function*() {
coRedisConn = coRedis(redisConn);
coRedisConn.on('error', function (err) {
debug('Redis error ' + err);
});
}).catch(function(err) {
debug(err);
});
let mongoConnecting = false;
const mongoDeferred = {};
mongoDeferred.promise = new Promise(function(resolve, reject) {
mongoDeferred.resolve = resolve;
mongoDeferred.reject = reject;
});
function* getMongo() {
if (!mongoConnecting) {
mongoConnecting = true;
const db = yield mongoConnect();
mongoDeferred.resolve(db);
return db;
} else {
return mongoDeferred.promise;
}
}
app.use(compress());
app.use(bodyparser());
app.use(favicon(path.join(__dirname, 'public/images/favicon.png')));
app.use(staticHandler({root: path.join(__dirname, 'public'), maxage: 31536000000}));
app.use(logger());
app.use(views('views', {
default: 'ejs'
}));
app.use(function* setupConnections(next){
this.db = yield getMongo();
this.redis = coRedisConn;
yield next;
});
app.keys = [process.env.KEYS || 'INSECURE'];
app.use(session({
store: redisStore({client: redisConn})
}));
app.use(function* objectIdSession(next) {
if (this.session.user) {
this.session.user.id = objectId(this.session.user.id);
}
yield next;
});
app.use(route.get('/', index.main));
app.use(route.get('/account', index.main));
app.use(route.get('/billing', index.main));
app.use(route.get('/pro', index.main));
app.use(route.get('/signin', user.signin));
app.use(route.post('/signin', user.signin));
app.use(route.get('/signup', user.signup));
app.use(route.post('/signup', user.signup));
app.use(route.get('/logout', user.logout));
app.use(route.post('/logout', user.logout));
app.use(route.get('/forgot', user.forgot));
app.use(route.get('/forgot/:token', user.forgot));
app.use(route.post('/forgot/:token', user.forgot));
app.use(route.post('/forgot', user.forgot));
app.use(route.get('/activate/:code', user.activate));
app.use(route.get('/terms', index.staticPage));
app.use(route.get('/privacy', index.staticPage));
app.use(route.get('/pricing', index.staticPage));
app.use(route.get('/apps', index.staticPage));
app.use(route.get('/stats', index.staticPage));
app.use(route.post('/pro/create', pro.create));
app.use(route.post('/pro/cancel', pro.cancel));
app.use(route.get('/:id', file.landing));
app.use(route.get('/download/:id/:name', function(id) {
this.redirect('/' + id);
}));
app.use(route.get('/file/:id/:name', file.get));
app.use(route.get('/files/:id/:name', file.get));
app.use(route.get('/file/:size/:id/:name', file.resized));
app.use(route.get('/updaters/mac', function() {
this.redirect('/updaters/mac.xml');
}));
app.use(route.get('/updaters/mac/changelog', function() {
this.render('mac-update-changelog');
}));
if (!module.parent) {
if (process.env.LOCALHOST_KEY) {
spdy.createServer({
key: process.env.LOCALHOST_KEY,
cert: process.env.LOCALHOST_CRT
}, app.callback()).listen(4041, function() {
debug('Koa SPDY server listening on port ' + (process.env.PORT || 4041));
});
} else {
app.listen(process.env.PORT || 4041, function() {
debug('Koa HTTP server listening on port ' + (process.env.PORT || 4041));
});
}
}
export default app;

193
web/lib/auth.js Normal file
View file

@ -0,0 +1,193 @@
import crypto from 'crypto';
import passwords from 'passwords';
import uuid from 'node-uuid';
import views from 'co-views';
const render = views('views', { default: 'ejs'});
import debugname from 'debug';
const debug = debugname('hostr-web:auth');
import { Mandrill } from 'mandrill-api/mandrill';
const mandrill = new Mandrill(process.env.MANDRILL_KEY);
export function* authenticate(ctx, email, password) {
const Users = ctx.db.Users;
const Logins = ctx.db.Logins;
const remoteIp = ctx.headers['x-real-ip'] || ctx.ip;
if (!password || password.length < 6){
debug('No password, or password too short');
return new Error('Invalid login details');
}
const count = yield Logins.count({ip: remoteIp, successful: false, at: { '$gt': Math.ceil(Date.now() / 1000) - 600}});
if (count > 25) {
debug('Throttling brute force');
return new Error('Invalid login details');
}
const login = {ip: remoteIp, at: Math.ceil(Date.now() / 1000), successful: null};
yield Logins.save(login);
const user = yield Users.findOne({email: email.toLowerCase(), banned: {'$exists': false}, status: {'$ne': 'deleted'}});
if (user) {
const verified = yield passwords.verify(password, user.salted_password);
if (verified) {
debug('Password verified');
login.successful = true;
yield Logins.updateOne({_id: login._id}, login);
return user;
} else {
debug('Password invalid');
login.successful = false;
yield Logins.updateOne({_id: login._id}, login);
}
} else {
debug('Email invalid');
login.successful = false;
yield Logins.updateOne({_id: login._id}, login);
}
}
export function* setupSession(ctx, user) {
debug('Setting up session');
const token = uuid.v4();
yield ctx.redis.set(token, user._id, 'EX', 604800);
const sessionUser = {
'id': user._id,
'email': user.email,
'dailyUploadAllowance': 15,
'maxFileSize': 20971520,
'joined': user.joined,
'plan': user.type || 'Free',
'uploadsToday': 0,
'token': token,
'md5': crypto.createHash('md5').update(user.email).digest('hex')
};
if (sessionUser.plan === 'Pro') {
sessionUser.maxFileSize = 524288000;
sessionUser.dailyUploadAllowance = 'unlimited';
}
ctx.session.user = sessionUser;
if (ctx.request.body.remember && ctx.request.body.remember === 'on') {
const Remember = ctx.db.Remember;
var rememberToken = uuid();
Remember.save({_id: rememberToken, 'user_id': user.id, created: new Date().getTime()});
ctx.cookies.set('r', rememberToken, { maxAge: 1209600000, httpOnly: true});
}
debug('Session set up');
}
export function* signup(ctx, email, password, ip) {
const Users = ctx.db.Users;
const existingUser = yield Users.findOne({email: email, status: {'$ne': 'deleted'}});
if (existingUser) {
debug('Email already in use.');
return 'Email already in use.';
}
const cryptedPassword = yield passwords.crypt(password);
var user = {
email: email,
'salted_password': cryptedPassword,
joined: Math.round(new Date().getTime() / 1000),
'signup_ip': ip,
activationCode: uuid()
};
Users.insertOne(user);
const html = yield render('email/inlined/activate', {activationUrl: process.env.BASE_URL + '/activate/' + user.activationCode});
const text = `Thanks for signing up to Hostr!
Please confirm your email address by clicking the link below.
${process.env.BASE_URL + '/activate/' + user.activationCode}
Jonathan Cremin, Hostr Founder
`;
mandrill.messages.send({message: {
html: html,
text: text,
subject: 'Welcome to Hostr',
'from_email': 'jonathan@hostr.co',
'from_name': 'Jonathan from Hostr',
to: [{
email: user.email,
type: 'to'
}],
'tags': [
'user-activation'
]
}});
}
export function* sendResetToken(ctx, email) {
const Users = ctx.db.Users;
const Reset = ctx.db.Reset;
const user = yield Users.findOne({email: email});
if (user) {
var token = uuid.v4();
Reset.save({
'_id': user._id,
'token': token,
'created': Math.round(new Date().getTime() / 1000)
});
const html = yield this.render('email/inlined/forgot', {forgotUrl: this.locals.baseUrl + '/forgot/' + token});
const text = `It seems you've forgotten your password :(
Visit ${ctx.locals.baseUrl + '/forgot/' + token} to set a new one.
`;
mandrill.messages.send({message: {
html: html,
text: text,
subject: 'Hostr Password Reset',
'from_email': 'jonathan@hostr.co',
'from_name': 'Jonathan from Hostr',
to: [{
email: user.email,
type: 'to'
}],
'tags': [
'password-reset'
]
}});
} else {
return 'There was an error looking up your email address.';
}
}
export function* fromToken(ctx, token) {
const Users = ctx.db.Users;
const reply = yield ctx.redis.get(token);
return yield Users.findOne({_id: reply});
}
export function* fromCookie(ctx, cookie) {
const Remember = ctx.db.Remember;
const Users = ctx.db.Users;
const remember = yield Remember.findOne({_id: cookie});
return yield Users.findOne({_id: remember.user_id});
}
export function* validateResetToken(ctx) {
const Reset = ctx.db.Reset;
return yield Reset.findOne({token: ctx.params.id});
}
export function* updatePassword(ctx, userId, password) {
const Users = ctx.db.Users;
const cryptedPassword = yield passwords.crypt(password);
yield Users.update({_id: userId}, {'$set': {'salted_password': cryptedPassword}});
}
export function* activateUser(ctx, code) {
const Users = ctx.db.Users;
const user = yield Users.findOne({activationCode: code});
if (user) {
Users.updateOne({_id: user._id}, {'$unset': {activationCode: ''}});
yield setupSession(ctx, user);
}
}

5
web/public/.eslintrc Normal file
View file

@ -0,0 +1,5 @@
{
"env": {
"browser": true
}
}

46
web/public/404.html Normal file
View file

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title ng-bind="Hostr">Hostr - File not found</title>
<link rel="icon" type="image/png" href="/images/favicon.png">
<link href='//fonts.googleapis.com/css?family=Lato:300,400' rel='stylesheet' type='text/css'>
<link href='//fonts.googleapis.com/css?family=Open+Sans:400,300,600' rel='stylesheet' type='text/css'>
<link href="/styles/app.css" rel="stylesheet" />
</head>
<body>
<section class="container header clearfix">
<div class="row">
<div class="col-md-8 col-md-offset-2" style='text-align: center;'>
<div class="logo">
<a href="/"><img src="/images/logo-dark-r.png" height="22" width="26" alt=""></a>
</div>
</div>
</div>
</section>
<section class="jumbotron error-page">
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<h1>404</h1>
<h2>We can't find the file you're looking for :(</h2>
<p class="lead">The owner may have removed it or it may never have existed in the first place.</p>
<a href="/">Try our homepage instead :)</a>
</div>
</div>
</div>
</section>
<script>
var _gaq=[['_setAccount','UA-66209-2'],['_setDomainName', 'hostr.co'],['_trackPageview']];
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
g.src='//www.google-analytics.com/ga.js';
s.parentNode.insertBefore(g,s)}(document,'script'));
</script>
</body>
</html>

44
web/public/50x.html Normal file
View file

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title ng-bind="Hostr">Hostr - File not found</title>
<link rel="icon" type="image/png" href="/images/favicon.png">
<link href='//fonts.googleapis.com/css?family=Lato:300,400' rel='stylesheet' type='text/css'>
<link href='//fonts.googleapis.com/css?family=Open+Sans:400,300,600' rel='stylesheet' type='text/css'>
<link href="/styles/app.css" rel="stylesheet" />
</head>
<body>
<section class="container header clearfix">
<div class="row">
<div class="col-md-8 col-md-offset-2" style='text-align: center;'>
<div class="logo">
<a href="/"><img src="/images/logo-dark-r.png" height="22" width="26" alt=""></a>
</div>
</div>
</div>
</section>
<section class="jumbotron error-page">
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<h1>Oops!</h1>
<h2>It looks like you've hit an unexpected error :(</h2>
<p class="lead">Refreshing might fix the problem. If not, sit tight! We're on it!</p>
</div>
</div>
</div>
</section>
<script>
var _gaq=[['_setAccount','UA-66209-2'],['_setDomainName', 'hostr.co'],['_trackPageview']];
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
g.src='//www.google-analytics.com/ga.js';
s.parentNode.insertBefore(g,s)}(document,'script'));
</script>
</body>
</html>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="/mstile-70x70.png"/>
<square150x150logo src="/mstile-150x150.png"/>
<square310x310logo src="/mstile-310x310.png"/>
<wide310x150logo src="/mstile-310x150.png"/>
<TileColor>#00aba9</TileColor>
</tile>
</msapplication>
</browserconfig>

78
web/public/config.js Normal file
View file

@ -0,0 +1,78 @@
System.config({
"defaultJSExtensions": true,
"transpiler": "babel",
"babelOptions": {
"optional": [
"runtime"
]
},
"paths": {
"github:*": "jspm_packages/github/*",
"npm:*": "jspm_packages/npm/*"
}
});
System.config({
"map": {
"angular": "npm:angular@1.4.3",
"angular-reconnecting-websocket": "github:adieu/angular-reconnecting-websocket@0.1.1",
"angular-strap": "npm:angular-strap@2.1.2",
"angular/resource": "npm:angular-resource@1.4.3",
"angular/route": "npm:angular-route@1.4.3",
"babel": "npm:babel-core@5.8.5",
"babel-runtime": "npm:babel-runtime@5.8.5",
"bootstrap-sass": "npm:bootstrap-sass@3.3.5",
"cferdinandi/smooth-scroll": "github:cferdinandi/smooth-scroll@5.3.7",
"core-js": "npm:core-js@0.9.18",
"dropzone": "npm:dropzone@4.0.1",
"jquery": "npm:jquery@2.1.4",
"zeroclipboard": "npm:zeroclipboard@2.2.0",
"github:jspm/nodelibs-path@0.1.0": {
"path-browserify": "npm:path-browserify@0.0.0"
},
"github:jspm/nodelibs-process@0.1.1": {
"process": "npm:process@0.10.1"
},
"github:jspm/nodelibs-util@0.1.0": {
"util": "npm:util@0.10.3"
},
"npm:angular-strap@2.1.2": {
"fs": "github:jspm/nodelibs-fs@0.1.2",
"path": "github:jspm/nodelibs-path@0.1.0",
"process": "github:jspm/nodelibs-process@0.1.1",
"systemjs-json": "github:systemjs/plugin-json@0.1.0",
"util": "github:jspm/nodelibs-util@0.1.0"
},
"npm:angular@1.4.3": {
"process": "github:jspm/nodelibs-process@0.1.1"
},
"npm:babel-runtime@5.8.5": {
"process": "github:jspm/nodelibs-process@0.1.1"
},
"npm:core-js@0.9.18": {
"fs": "github:jspm/nodelibs-fs@0.1.2",
"process": "github:jspm/nodelibs-process@0.1.1",
"systemjs-json": "github:systemjs/plugin-json@0.1.0"
},
"npm:dropzone@4.0.1": {
"process": "github:jspm/nodelibs-process@0.1.1"
},
"npm:inherits@2.0.1": {
"util": "github:jspm/nodelibs-util@0.1.0"
},
"npm:jquery@2.1.4": {
"process": "github:jspm/nodelibs-process@0.1.1"
},
"npm:path-browserify@0.0.0": {
"process": "github:jspm/nodelibs-process@0.1.1"
},
"npm:util@0.10.3": {
"inherits": "npm:inherits@2.0.1",
"process": "github:jspm/nodelibs-process@0.1.1"
},
"npm:zeroclipboard@2.2.0": {
"process": "github:jspm/nodelibs-process@0.1.1"
}
}
});

BIN
web/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,022 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

BIN
web/public/images/apple.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 10 10" enable-background="new 0 0 10 10" xml:space="preserve" height="10" width="10">
<path fill="#c3c3d1" d="M5,5.1L1.9,1.9C1.7,1.8,1.5,1.7,1.3,1.7S0.9,1.8,0.7,1.9L0.2,2.4C0.1,2.6,0,2.8,0,3c0,0.2,0.1,0.4,0.2,0.6l4.2,4.2
C4.6,7.9,4.8,8,5,8s0.4-0.1,0.6-0.2l4.2-4.2C9.9,3.4,10,3.2,10,3c0-0.2-0.1-0.4-0.2-0.6L9.3,1.9C9.1,1.8,8.9,1.7,8.7,1.7
c-0.2,0-0.4,0.1-0.6,0.2L5,5.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" data-icon="cloud-transfer-upload" width="16" height="16" data-container-transform="scale(1 1 ) translate(0 )" viewBox="0 0 16 16">
<path fill="#c3c3d1" d="M8 0c-2.5 0-4.506 1.794-4.906 4.094-1.8.4-3.094 2.006-3.094 3.906 0 2.2 1.8 4 4 4l4-4 4 4h1c1.7 0 3-1.3 3-3s-1.3-3-3-3v-1c0-2.8-2.2-5-5-5zm0 10l-3 3h2v2a1 1 0 1 0 2 0v-2h2l-3-3z" />
</svg>

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1"
id="svg3001" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:dc="http://purl.org/dc/elements/1.1/" inkscape:version="0.48.3.1 r9886" sodipodi:docname="cloud_upload_font_awesome.svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 16 16"
enable-background="new 0 0 16 16" xml:space="preserve" height="16" width="16">
<sodipodi:namedview inkscape:cx="960" inkscape:cy="896" gridtolerance="10" borderopacity="1" id="namedview3007" bordercolor="#666666" objecttolerance="10" inkscape:zoom="0.13169643" showgrid="false" guidetolerance="10" pagecolor="#ffffff" inkscape:current-layer="svg3001" inkscape:window-maximized="0" inkscape:window-y="25" inkscape:window-x="0" inkscape:window-height="16" inkscape:window-width="16" inkscape:pageopacity="0" inkscape:pageshadow="2">
</sodipodi:namedview>
<g id="g3003" transform="matrix(1,0,0,-1,22.779661,1428.2373)">
<path fill="#c3c3d1" id="path3005" inkscape:connector-curvature="0" d="M-12.1,1420c0,0.1,0,0.1-0.1,0.2l-2.9,2.9c-0.1,0-0.1,0.1-0.2,0.1
s-0.1,0-0.2-0.1l-2.9-2.9c-0.1-0.1-0.1-0.1-0.1-0.2s0-0.1,0.1-0.2c0-0.1,0.1-0.1,0.2-0.1h1.9v-2.9c0-0.1,0-0.1,0.1-0.2
s0.1-0.1,0.2-0.1h1.6c0.1,0,0.1,0,0.2,0.1c0.1,0.1,0.1,0.1,0.1,0.2v2.9h1.9c0.1,0,0.1,0,0.2,0.1
C-12.1,1419.9-12.1,1419.9-12.1,1420z M-6.8,1417.6c0-0.9-0.3-1.6-0.9-2.3s-1.4-0.9-2.3-0.9h-9c-1,0-1.9,0.4-2.6,1.1
s-1.1,1.6-1.1,2.6c0,0.7,0.2,1.4,0.6,2s0.9,1.1,1.6,1.4c0,0.2,0,0.3,0,0.4c0,1.2,0.4,2.2,1.2,3s1.8,1.2,3,1.2
c0.9,0,1.7-0.2,2.4-0.7s1.2-1.1,1.6-1.9c0.4,0.3,0.9,0.5,1.4,0.5c0.6,0,1.1-0.2,1.5-0.6s0.6-0.9,0.6-1.5c0-0.4-0.1-0.8-0.3-1.1
c0.7-0.2,1.3-0.5,1.8-1.1C-7,1419-6.8,1418.3-6.8,1417.6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1"
id="svg3001" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:dc="http://purl.org/dc/elements/1.1/" inkscape:version="0.48.3.1 r9886" sodipodi:docname="cloud_upload_font_awesome.svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 16 16"
enable-background="new 0 0 16 16" xml:space="preserve" height="16" width="16">
<sodipodi:namedview inkscape:cx="960" inkscape:cy="896" gridtolerance="10" borderopacity="1" id="namedview3007" bordercolor="#666666" objecttolerance="10" inkscape:zoom="0.13169643" showgrid="false" guidetolerance="10" pagecolor="#ffffff" inkscape:current-layer="svg3001" inkscape:window-maximized="0" inkscape:window-y="25" inkscape:window-x="0" inkscape:window-height="16" inkscape:window-width="16" inkscape:pageopacity="0" inkscape:pageshadow="2">
</sodipodi:namedview>
<g id="g3003" transform="matrix(1,0,0,-1,22.779661,1428.2373)">
<path fill="#a94442" id="path3005" inkscape:connector-curvature="0" d="M-12.1,1420c0,0.1,0,0.1-0.1,0.2l-2.9,2.9c-0.1,0-0.1,0.1-0.2,0.1
s-0.1,0-0.2-0.1l-2.9-2.9c-0.1-0.1-0.1-0.1-0.1-0.2s0-0.1,0.1-0.2c0-0.1,0.1-0.1,0.2-0.1h1.9v-2.9c0-0.1,0-0.1,0.1-0.2
s0.1-0.1,0.2-0.1h1.6c0.1,0,0.1,0,0.2,0.1c0.1,0.1,0.1,0.1,0.1,0.2v2.9h1.9c0.1,0,0.1,0,0.2,0.1
C-12.1,1419.9-12.1,1419.9-12.1,1420z M-6.8,1417.6c0-0.9-0.3-1.6-0.9-2.3s-1.4-0.9-2.3-0.9h-9c-1,0-1.9,0.4-2.6,1.1
s-1.1,1.6-1.1,2.6c0,0.7,0.2,1.4,0.6,2s0.9,1.1,1.6,1.4c0,0.2,0,0.3,0,0.4c0,1.2,0.4,2.2,1.2,3s1.8,1.2,3,1.2
c0.9,0,1.7-0.2,2.4-0.7s1.2-1.1,1.6-1.9c0.4,0.3,0.9,0.5,1.4,0.5c0.6,0,1.1-0.2,1.5-0.6s0.6-0.9,0.6-1.5c0-0.4-0.1-0.8-0.3-1.1
c0.7-0.2,1.3-0.5,1.8-1.1C-7,1419-6.8,1418.3-6.8,1417.6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 919 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

BIN
web/public/images/fb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
web/public/images/file.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

BIN
web/public/images/gear.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
web/public/images/icons.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

BIN
web/public/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

BIN
web/public/images/menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

BIN
web/public/images/music.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

BIN
web/public/images/person.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 B

BIN
web/public/images/plus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
web/public/images/url.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

BIN
web/public/images/user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
web/public/images/video.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

View file

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title ng-bind="Hostr">Hostr - File not found</title>
<link rel="icon" type="image/png" href="/images/favicon.png">
<link href='//fonts.googleapis.com/css?family=Lato:300,400' rel='stylesheet' type='text/css'>
<link href='//fonts.googleapis.com/css?family=Open+Sans:400,300,600' rel='stylesheet' type='text/css'>
<link href="/styles/app.css" rel="stylesheet" />
</head>
<body>
<section class="container header clearfix">
<div class="row">
<div class="col-md-8 col-md-offset-2" style='text-align: center;'>
<div class="logo">
<a href="/"><img src="/images/logo-dark-r.png" height="22" width="26" alt=""></a>
</div>
</div>
</div>
</section>
<section class="jumbotron error-page">
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<h1>BRB</h1>
<h2>We're just performing some upgrades, we'll be right back!</h2>
</div>
</div>
</div>
</section>
<script>
var _gaq=[['_setAccount','UA-66209-2'],['_setDomainName', 'hostr.co'],['_trackPageview']];
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
g.src='//www.google-analytics.com/ga.js';
s.parentNode.insertBefore(g,s)}(document,'script'));
</script>
</body>
</html>

3
web/public/robots.txt Normal file
View file

@ -0,0 +1,3 @@
# hostr.co/
User-agent: *

129
web/public/src/app.js Normal file
View file

@ -0,0 +1,129 @@
import angular from 'angular';
import ngRoute from 'angular/route';
import ngResource from 'angular/resource';
import ReconnectingWebSocket from 'angular-reconnecting-websocket';
import ngDimensions from 'angular-strap/dist/modules/dimensions';
import ngTooltip from 'angular-strap/dist/modules/tooltip';
import { FilesController, FileController, AccountController, ProController, BillingController } from './app/controllers';
import { appHeader, appFooter, menuDropdown, searchShortcut, stripeSubscribe } from './app/directives';
import dropzone from './app/directives/dropzone';
import lazySrc from './app/directives/lazy-src';
import { fileSize, direct } from './app/filters';
import { FileService, UserService, EventService, TransactionService, SettingService } from './app/services';
// Declare app level module which depends on filters, and services
var app = angular.module('hostr', [
'ngRoute',
'ngResource',
'reconnectingWebSocket',
'mgcrea.ngStrap.tooltip'
]);
app.factory('FileService', ['$resource', '$cacheFactory', FileService.factory]);
app.factory('UserService', ['$resource', UserService.factory]);
app.factory('EventService', ['$rootScope', ReconnectingWebSocket, EventService.factory]);
app.factory('TransactionService', ['$resource', '$cacheFactory', TransactionService.factory]);
app.factory('SettingService', ['$http', SettingService.factory]);
app.filter('fileSize', [fileSize]);
app.filter('direct', [direct]);
app.directive('appHeader', [appHeader]);
app.directive('appFooter', [appFooter]);
app.directive('dropzone', ['FileService', '$cacheFactory', '$window', dropzone]);
app.directive('menuDropdown', [menuDropdown]);
app.directive('lazySrc', ['$window', '$document', lazySrc]);
app.directive('searchShortcut', ['$document', searchShortcut]);
app.directive('stripeSubscribe', ['$http', stripeSubscribe]);
app.config(['$routeProvider', '$locationProvider', '$httpProvider', '$tooltipProvider', function($routeProvider, $locationProvider, $httpProvider, $tooltipProvider) {
$tooltipProvider.defaults.template = '/jspm_packages/npm/angular-strap@2.1.2/src/tooltip/tooltip.tpl.html';
if (typeof window.user !== 'undefined') {
$httpProvider.defaults.headers.common.Authorization = ':' + window.user.token;
}
$locationProvider.html5Mode(true);
$httpProvider.interceptors.push(['$q', function($q) {
return {
responseError: function(rejection) {
if (rejection.status === 401) {
window.location = '/logout';
}
return $q.reject(rejection);
}
};
}]);
$routeProvider.when('/', {
templateUrl: '/build/partials/files.html',
controller: FilesController,
title: ' - Files',
resolve: {
files: ['FileService', function(Files) {
return Files.query();
}]
}
})
.when('/apps', {
templateUrl: '/build/partials/apps.html',
title: ' - Apps for Mac and Windows'
})
.when('/pro', {
templateUrl: '/build/partials/pro.html',
controller: ProController,
title: ' - Pro'
})
.when('/account', {
templateUrl: '/build/partials/account.html',
controller: AccountController,
title: ' - Account'
})
.when('/billing', {
templateUrl: '/build/partials/billing.html',
controller: BillingController,
title: ' - Billing'
})
.when('/terms', {
templateUrl: '/build/partials/terms.html',
title: ' - Terms of Service'
})
.when('/privacy', {
templateUrl: '/build/partials/privacy.html',
title: ' - Privacy Policy'
})
.when('/:id', {
templateUrl: '/build/partials/file.html',
controller: FileController,
resolve: {
file: ['$route', 'FileService', function($route, Files) {
return Files.get({id: $route.current.params.id});
}]
}
});
}]);
app.run(['$location', '$rootScope', function($location, $rootScope) {
$rootScope.$on('$routeChangeStart', function(e, curr) {
if (curr.$$route && curr.$$route.resolve) {
// Show a loading message until promises are resolved
$rootScope.loadingView = true;
}
});
$rootScope.$on('$routeChangeSuccess', function (event, current) {
$rootScope.navError = false;
$rootScope.pageTitle = current.$$route.title;
});
$rootScope.$on('$routeChangeError', function () {
$rootScope.loadingView = false;
$rootScope.navError = true;
});
$rootScope.$on('$locationChangeStart', function(event, newUrl) {
if (window.ga) {
window.ga('send', 'pageview', newUrl);
}
});
}]);

View file

@ -0,0 +1,105 @@
export class FilesController {
constructor($scope, UserService, files) {
$scope.$root.user = UserService.get();
files.$promise.then(function() {
$scope.$root.loadingView = false;
});
$scope.header = 'full';
if (!$scope.$root.files) {
$scope.$root.files = files;
}
$scope.remove = function(file) {
$scope.$root.files.some(function(existingFile, index) {
if (file.id === existingFile.id) {
file.$remove(function() {
$scope.$root.showDropdown = false;
$scope.$root.files.splice(index, 1);
});
return true;
}
return false;
});
};
}
}
FilesController.$inject = ['$scope', 'UserService', 'files'];
export class FileController {
constructor ($scope, $rootScope, $routeParams, ReconnectingWebSocket, file) {
file.$promise.then(function() {
$scope.$root.loadingView = false;
$scope.header = 'small';
$scope.file = file;
$scope.direct = '/file/' + file.id + '/' + file.name;
$rootScope.pageTitle = ' - ' + file.name;
if (file.status === 'uploading') {
file.percent = 0;
var ws = new ReconnectingWebSocket('wss://' + window.location.hostname + window.settings.api + '/file/' + file.id);
ws.onmessage = function (msg) {
var evt = JSON.parse(msg.data);
$rootScope.$broadcast(evt.type, evt.data);
};
ws.onopen = function() {
ws.send(JSON.stringify({authorization: window.user.token}));
};
$rootScope.$on('file-progress', function(evt, data) {
$scope.file.percent = data.complete;
});
$rootScope.$on('file-added', function(evt, data) {
$scope.file = data;
});
$rootScope.$on('file-accepted', function(evt, data) {
$scope.file = data;
});
}
}, function() {
$rootScope.navError = true;
$scope.$root.loadingView = false;
});
}
}
FileController.$inject = ['$scope', '$rootScope', '$routeParams', 'WebSocket', 'file'];
export class ProController {
constructor ($scope, $http, UserService) {
$scope.$root.loadingView = false;
$scope.user = UserService.get();
$scope.header = 'full';
$scope.cancel = function() {
$http.post('/pro/cancel').success(function() {
window.location.reload(true);
}).error(function(data) {
console.log(new Error(data));
});
};
}
}
ProController.$inject = ['$scope', '$http', 'UserService'];
export class AccountController {
constructor ($scope, UserService, SettingService) {
$scope.$root.loadingView = false;
$scope.$root.user = UserService.get();
$scope.submit = function(form) {
$scope.updated = false;
$scope.error = false;
SettingService.update(form).then(function() {
$scope.updated = true;
delete $scope.user.new_password;
delete $scope.user.current_password;
}, function(response) {
$scope.error = response.data.error.message;
});
};
}
}
AccountController.$inject = ['$scope', 'UserService', 'SettingService'];
export class BillingController {
constructor ($scope, UserService, TransactionService) {
$scope.$root.loadingView = false;
$scope.$root.user = UserService.get();
$scope.transactions = TransactionService.query();
}
}
BillingController.$inject = ['$scope', 'UserService', 'TransactionService'];

View file

@ -0,0 +1,121 @@
import $ from 'jquery';
export function appHeader() {
return {
restrict: 'E',
templateUrl: '/build/partials/header.html',
replace: true,
link: function postLink(scope) {
scope.userMD5 = window.user.md5;
scope.email = window.user.email;
scope.pro = (window.user.type === 'Pro');
}
};
}
export function appFooter() {
return {
restrict: 'E',
templateUrl: '/build/partials/footer.html',
replace: true,
link: function postLink(scope) {
scope.userMD5 = window.user.md5;
scope.email = window.user.email;
scope.pro = (window.user.type === 'Pro');
}
};
}
export function menuDropdown() {
return function($scope, element) {
$scope.$root.overlayClick = function overlayClick() {
$scope.$root.showDropdown = false;
$('.dropdown').hide();
};
var activeDropdown = $(element).find('.dropdown');
element.on('click', function(e) {
if (activeDropdown.not(':visible').length > 0) {
$('.dropdown').hide();
$scope.$root.showDropdown = true;
activeDropdown.show();
} else if (e.target === element.find('img')[0]) {
$scope.$root.showDropdown = false;
activeDropdown.hide();
}
$scope.$apply();
});
};
}
export function searchShortcut ($document) {
return function($scope, element) {
$document.bind('keypress', function(event) {
if(event.which === 47) {
if (['INPUT', 'TEXTAREA'].indexOf(document.activeElement.tagName) < 0) {
element[0].focus();
event.preventDefault();
}
}
});
};
}
export function stripeSubscribe($http) {
const handler = window.StripeCheckout.configure({
key: window.settings.stripePublic,
image: '/images/stripe-128.png',
token: function(token) {
$http.post('/pro/create', {stripeToken: token})
.success(function(data) {
if (data.status === 'active') {
window.user.plan = 'Pro';
window.location.reload(true);
}
})
.error(function() {
alert('Error upgrading your account');
});
}
});
return function(scope, element) {
element.on('click', function() {
// Open Checkout with further options
handler.open({
name: 'Hostr',
email: window.user.email,
description: 'Hostr Pro Monthly',
amount: 600,
currency: 'USD',
panelLabel: 'Subscribe {{amount}}',
billingAddress: false
});
});
};
}
// angular.module('hostr').directive('clippy', ['files', function factory() {
// return function(scope, element, attrs) {
// element = element[0];
// var client = new ZeroClipboard(element);
// client.on('ready', function(readyEvent) {
// element.addEventListener('click', function(event) {
// event.preventDefault();
// });
//
// client.on( 'aftercopy', function( event ) {
// if (element.innerHTML == 'Copy Link') {
// element.innerHTML = 'Copied!';
// setTimeout(function() {
// if (element) {
// element.innerHTML = 'Copy Link';
// }
// }, 1500);
// }
// });
// });
// }
// }]);

View file

@ -0,0 +1,154 @@
import Dropzone from 'dropzone';
function guid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
export default function dropzone(FileService, $cacheFactory) {
var dropOverlay = document.getElementById('filedrop-overlay');
var dropzoneEl;
var errorTimeout;
return function($scope) {
$scope.$on('$viewContentLoaded', function() {
if (!dropzoneEl) {
$scope.$root.uploadingFiles = [];
var clickable = [].slice.call(document.querySelectorAll('.choose-file'));
dropzoneEl = new Dropzone(document.body, {
url: window.settings.apiURL + '/file',
maxFilesize: window.user.maxFileSize / 1024 / 1024,
maxThumbnailFilesize: 5,
thumbnailWidth: 150,
thumbnailHeight: 98,
parallelUploads: 1,
uploadMultiple: false,
clickable: clickable.length ? clickable : false,
autoDiscover: false,
headers: {'Authorization': ':' + window.user.token},
previewsContainer: false
});
dropzoneEl.on('thumbnail', function(file, thumbnail){
file.thumbnail = thumbnail;
$scope.$apply();
});
dropzoneEl.on('addedfile', function(file){
var id = guid();
file.guid = id;
$scope.$root.uploadingFiles.push(file);
$scope.$apply();
});
dropzoneEl.on('sending', function(file, xhr) {
xhr.setRequestHeader('hostr-guid', file.guid);
});
dropzoneEl.on('uploadprogress', function(file, progress) {
$scope.$root.progress = {
name: file.name,
percent: progress,
status: 'Uploading'
};
if (progress === 100) {
$scope.$root.progress.status = 'Processing';
}
$scope.$apply();
});
dropzoneEl.on('complete', function(file){
delete $scope.$root.progress;
$scope.$apply();
$scope.$root.uploadingFiles.some(function(uploadingFile, index) {
if (uploadingFile.guid === file.guid) {
$scope.$root.uploadingFiles.splice(index, 1);
$scope.$apply();
return true;
}
return false;
});
});
dropzoneEl.on('error', function(evt, error){
if (error.error) {
$scope.$root.uploadError = 'Error uploading file: ' + evt.name + '. ' + error.error.message;
}
else if (evt.name) {
$scope.$root.uploadError = 'Error uploading file: ' + evt.name + '. ' + error;
} else {
if (error[0] !== '<') {
$scope.$root.uploadError = 'Uknown error during upload';
}
}
$scope.$apply();
clearTimeout(errorTimeout);
errorTimeout = setTimeout(function() {
$scope.$root.uploadError = '';
$scope.$apply();
}, 5000);
});
var addFile = function(newFile) {
if (!$scope.$root.files.some(function (file) {
return file.id === newFile.id;
})) {
var cache = $cacheFactory.get('files-cache');
cache.removeAll();
var file = new FileService(newFile);
$scope.$root.files.unshift(file);
$scope.$root.user.uploads_today++;
$scope.$apply();
}
};
dropzoneEl.on('success', function(file, response){
addFile(response);
});
$scope.$on('file-added', function(event, data){
addFile(data);
});
$scope.$on('file-accepted', function(event, data){
$scope.$root.uploadingFiles.some(function(file) {
if (file.guid === data.guid) {
file.id = data.id;
file.href = data.href;
$scope.$apply();
return true;
}
});
});
$scope.$on('file-deleted', function(evt, data) {
$scope.$root.files.forEach(function(file, index) {
if(data.id === file.id) {
delete $scope.$root.files[index];
$scope.$digest();
}
});
});
document.body.addEventListener('dragenter', function(){
dropOverlay.style.display = 'block';
});
dropOverlay.addEventListener('dragleave', function(event){
if (event.target.outerText !== 'Drop files to upload' || event.x === 0) {
dropOverlay.style.display = 'none';
}
});
dropOverlay.addEventListener('drop', function(){
dropOverlay.style.display = 'none';
});
} else {
var clicker = [].slice.call(document.querySelectorAll('.choose-file'));
if (clicker) {
clicker.forEach(function(el) {
el.addEventListener('click', function() {
return dropzoneEl.hiddenFileInput.click();
});
});
}
}
});
};
}

View file

@ -0,0 +1,222 @@
import $ from 'jquery';
export default function lazySrc($window, $document) {
var lazyLoader = (function() {
var images = [];
var renderTimer = null;
var renderDelay = 100;
var win = $($window);
var doc = $($document);
var documentHeight = doc.height();
var documentTimer = null;
var documentDelay = 2000;
var isWatchingWindow = false;
// ---
// PUBLIC METHODS.
// ---
function addImage(image) {
images.push(image);
if (!renderTimer) {
startRenderTimer();
}
if (!isWatchingWindow) {
startWatchingWindow();
}
}
let removeImage = function(image) {
for (let i = 0; i < images.length; i++) {
if (images[i] === image ) {
images.splice(i, 1);
break;
}
}
if ( !images.length ) {
clearRenderTimer();
stopWatchingWindow();
}
};
// ---
// PRIVATE METHODS.
// ---
function clearRenderTimer() {
clearTimeout( renderTimer );
renderTimer = null;
}
function checkImages() {
var visible = [];
var hidden = [];
var windowHeight = win.height();
var scrollTop = win.scrollTop();
var topFoldOffset = scrollTop;
var bottomFoldOffset = ( topFoldOffset + windowHeight );
for (let i = 0; i < images.length; i++) {
var image = images[ i ];
if ( image.isVisible( topFoldOffset, bottomFoldOffset ) ) {
visible.push( image );
} else {
hidden.push( image );
}
}
for (let i = 0; i < visible.length; i++) {
visible[ i ].render();
}
images = hidden;
clearRenderTimer();
if ( !images.length ) {
stopWatchingWindow();
}
}
function startRenderTimer() {
renderTimer = setTimeout( checkImages, renderDelay );
}
function checkDocumentHeight() {
if ( renderTimer ) {
return;
}
var currentDocumentHeight = doc.height();
if ( currentDocumentHeight === documentHeight ) {
return;
}
documentHeight = currentDocumentHeight;
startRenderTimer();
}
function windowChanged() {
if (!renderTimer) {
startRenderTimer();
}
}
function startWatchingWindow() {
isWatchingWindow = true;
win.on( 'resize.lazySrc', windowChanged );
win.on( 'scroll.lazySrc', windowChanged );
documentTimer = setInterval( checkDocumentHeight, documentDelay );
}
function stopWatchingWindow() {
isWatchingWindow = false;
win.off( 'resize.lazySrc' );
win.off( 'scroll.lazySrc' );
clearInterval( documentTimer );
}
return ({
addImage: addImage,
removeImage: removeImage
});
})();
function LazyImage( element ) {
var source = null;
var isRendered = false;
var height = null;
element = $(element);
// ---
// PUBLIC METHODS.
// ---
function isVisible( topFoldOffset, bottomFoldOffset ) {
if (!element.is(':visible')) {
//return( false );
}
bottomFoldOffset = bottomFoldOffset + 50;
if ( height === null ) {
height = element.height();
}
var top = element.offset().top;
var bottom = ( top + height );
return (
(
( top <= bottomFoldOffset ) &&
( top >= topFoldOffset )
)
||
(
( bottom <= bottomFoldOffset ) &&
( bottom >= topFoldOffset )
)
||
(
( top <= topFoldOffset ) &&
( bottom >= bottomFoldOffset )
)
);
}
function renderSource() {
element[ 0 ].src = source;
element[ 0 ].classList.add('loaded');
}
function render() {
isRendered = true;
renderSource();
}
function setSource( newSource ) {
source = newSource;
if ( isRendered ) {
renderSource();
}
}
return ({
isVisible: isVisible,
render: render,
setSource: setSource
});
}
function link( $scope, element, attributes ) {
var lazyImage = new LazyImage( element );
lazyLoader.addImage( lazyImage );
attributes.$observe(
'lazySrc',
function( newSource ) {
lazyImage.setSource( newSource );
}
);
$scope.$on(
'$destroy',
function() {
lazyLoader.removeImage( lazyImage );
}
);
}
return ({
link: link,
restrict: 'A'
});
}

View file

@ -0,0 +1,33 @@
export function fileSize() {
return function(input) {
if (input >= 1073741824) {
input = Math.round((input / 1073741824) * 10) / 10 + 'GB';
} else {
if (input >= 1048576) {
input = Math.round((input / 1048576) * 10) / 10 + 'MB';
} else {
if (input >= 1024) {
input = Math.round((input / 1024) * 10) / 10 + 'KB';
} else {
input = Math.round(input) + 'B';
}
}
}
return input;
};
}
export function direct() {
return function(file) {
if(file.name) {
if (file.direct && file.name.split('.').pop().toLowerCase() === 'psd') {
return file.direct['970x'].replace('/970/', '/').slice(0, -4);
} else if (file.direct && file.direct['970x']) {
return file.direct['970x'].replace('/970/', '/');
} else {
return file.href.replace('hostr.co/', 'hostr.co/file/') + '/' + file.name;
}
}
};
}

View file

@ -0,0 +1,72 @@
export class FileService {
constructor($resource, $cacheFactory) {
var cache = $cacheFactory('files-cache');
return $resource(window.settings.apiURL + '/file/:id', {id: '@id'}, {
query: {method: 'GET', isArray: true, cache: cache,
params: {perpage: 0, all: true}
},
delete: {method: 'DELETE', isArray: true, cache: cache}
});
}
static factory($resource, $cacheFactory) {
return new FileService($resource, $cacheFactory);
}
}
export class UserService {
constructor($resource) {
return $resource(window.settings.apiURL + '/user');
}
static factory($resource) {
return new UserService($resource);
}
}
export class EventService {
constructor($rootScope, ReconnectingWebSocket) {
if (window.user && WebSocket) {
let ws = new ReconnectingWebSocket('wss' + window.settings.apiURL.replace('https', '').replace('http', '') + '/user');
ws.onmessage = function (msg) {
var evt = JSON.parse(msg.data);
$rootScope.$broadcast(evt.type, evt.data);
};
ws.onopen = function() {
ws.send(JSON.stringify({authorization: window.user.token}));
};
}
return true;
}
static factory($rootScope, ReconnectingWebSocket) {
return new EventService($rootScope, ReconnectingWebSocket);
}
}
export class TransactionService {
constructor ($resource, $cacheFactory) {
var cache = $cacheFactory('transaction-cache');
return $resource(window.settings.apiURL + '/user/transaction/:id', {id: '@id'}, {
query: {method: 'GET', isArray: true, cache: cache}
});
}
static factory($resource, $cacheFactory) {
return new TransactionService($resource, $cacheFactory);
}
}
export class SettingService {
constructor ($http) {
var service = {};
service.update = function(data) {
return $http.post(window.settings.apiURL + '/user/settings', data);
};
return service;
}
static factory($http) {
return new SettingService($http);
}
}

View file

@ -0,0 +1,224 @@
/* =============================================================
Smooth Scroll v4.5
Animate scrolling to anchor links, by Chris Ferdinandi.
http://gomakethings.com
Additional contributors:
https://github.com/cferdinandi/smooth-scroll#contributors
Free to use under the MIT License.
http://gomakethings.com/mit/
* ============================================================= */
window.smoothScroll = (function (window, document, undefined) {
'use strict';
// Default settings
// Private {object} variable
var _defaults = {
speed: 500,
easing: 'easeInOutCubic',
offset: 0,
updateURL: false,
callbackBefore: function () {},
callbackAfter: function () {}
};
// Merge default settings with user options
// Private method
// Returns an {object}
var _mergeObjects = function ( original, updates ) {
for (var key in updates) {
original[key] = updates[key];
}
return original;
};
// Calculate the easing pattern
// Private method
// Returns a decimal number
var _easingPattern = function ( type, time ) {
if ( type == 'easeInQuad' ) return time * time; // accelerating from zero velocity
if ( type == 'easeOutQuad' ) return time * (2 - time); // decelerating to zero velocity
if ( type == 'easeInOutQuad' ) return time < 0.5 ? 2 * time * time : -1 + (4 - 2 * time) * time; // acceleration until halfway, then deceleration
if ( type == 'easeInCubic' ) return time * time * time; // accelerating from zero velocity
if ( type == 'easeOutCubic' ) return (--time) * time * time + 1; // decelerating to zero velocity
if ( type == 'easeInOutCubic' ) return time < 0.5 ? 4 * time * time * time : (time - 1) * (2 * time - 2) * (2 * time - 2) + 1; // acceleration until halfway, then deceleration
if ( type == 'easeInQuart' ) return time * time * time * time; // accelerating from zero velocity
if ( type == 'easeOutQuart' ) return 1 - (--time) * time * time * time; // decelerating to zero velocity
if ( type == 'easeInOutQuart' ) return time < 0.5 ? 8 * time * time * time * time : 1 - 8 * (--time) * time * time * time; // acceleration until halfway, then deceleration
if ( type == 'easeInQuint' ) return time * time * time * time * time; // accelerating from zero velocity
if ( type == 'easeOutQuint' ) return 1 + (--time) * time * time * time * time; // decelerating to zero velocity
if ( type == 'easeInOutQuint' ) return time < 0.5 ? 16 * time * time * time * time * time : 1 + 16 * (--time) * time * time * time * time; // acceleration until halfway, then deceleration
return time; // no easing, no acceleration
};
// Calculate how far to scroll
// Private method
// Returns an integer
var _getEndLocation = function ( anchor, headerHeight, offset ) {
var location = 0;
if (anchor.offsetParent) {
do {
location += anchor.offsetTop;
anchor = anchor.offsetParent;
} while (anchor);
}
location = location - headerHeight - offset;
if ( location >= 0 ) {
return location;
} else {
return 0;
}
};
// Determine the document's height
// Private method
// Returns an integer
var _getDocumentHeight = function () {
return Math.max(
document.body.scrollHeight, document.documentElement.scrollHeight,
document.body.offsetHeight, document.documentElement.offsetHeight,
document.body.clientHeight, document.documentElement.clientHeight
);
};
// Convert data-options attribute into an object of key/value pairs
// Private method
// Returns an {object}
var _getDataOptions = function ( options ) {
if ( options === null || options === undefined ) {
return {};
} else {
var settings = {}; // Create settings object
options = options.split(';'); // Split into array of options
// Create a key/value pair for each setting
options.forEach( function(option) {
option = option.trim();
if ( option !== '' ) {
option = option.split(':');
settings[option[0]] = option[1].trim();
}
});
return settings;
}
};
// Update the URL
// Private method
// Runs functions
var _updateURL = function ( anchor, url ) {
if ( (url === true || url === 'true') && history.pushState ) {
history.pushState( {pos:anchor.id}, '', anchor );
}
};
// Start/stop the scrolling animation
// Public method
// Runs functions
var animateScroll = function ( toggle, anchor, options, event ) {
// Options and overrides
options = _mergeObjects( _defaults, options || {} ); // Merge user options with defaults
var overrides = _getDataOptions( toggle ? toggle.getAttribute('data-options') : null );
var speed = parseInt(overrides.speed || options.speed, 10);
var easing = overrides.easing || options.easing;
var offset = parseInt(overrides.offset || options.offset, 10);
var updateURL = overrides.updateURL || options.updateURL;
// Selectors and variables
var fixedHeader = document.querySelector('[data-scroll-header]'); // Get the fixed header
var headerHeight = fixedHeader === null ? 0 : (fixedHeader.offsetHeight + fixedHeader.offsetTop); // Get the height of a fixed header if one exists
var startLocation = window.pageYOffset; // Current location on the page
var endLocation = _getEndLocation( document.querySelector(anchor), headerHeight, offset ); // Scroll to location
var animationInterval; // interval timer
var distance = endLocation - startLocation; // distance to travel
var documentHeight = _getDocumentHeight();
var timeLapsed = 0;
var percentage, position;
// Prevent default click event
if ( toggle && toggle.tagName === 'A' && event ) {
event.preventDefault();
}
// Update URL
_updateURL(anchor, updateURL);
// Stop the scroll animation when it reaches its target (or the bottom/top of page)
// Private method
// Runs functions
var _stopAnimateScroll = function (position, endLocation, animationInterval) {
var currentLocation = window.pageYOffset;
if ( position == endLocation || currentLocation == endLocation || ( (window.innerHeight + currentLocation) >= documentHeight ) ) {
clearInterval(animationInterval);
options.callbackAfter( toggle, anchor ); // Run callbacks after animation complete
}
};
// Loop scrolling animation
// Private method
// Runs functions
var _loopAnimateScroll = function () {
timeLapsed += 16;
percentage = ( timeLapsed / speed );
percentage = ( percentage > 1 ) ? 1 : percentage;
position = startLocation + ( distance * _easingPattern(easing, percentage) );
window.scrollTo( 0, Math.floor(position) );
_stopAnimateScroll(position, endLocation, animationInterval);
};
// Set interval timer
// Private method
// Runs functions
var _startAnimateScroll = function () {
options.callbackBefore( toggle, anchor ); // Run callbacks before animating scroll
animationInterval = setInterval(_loopAnimateScroll, 16);
};
// Reset position to fix weird iOS bug
// https://github.com/cferdinandi/smooth-scroll/issues/45
if ( window.pageYOffset === 0 ) {
window.scrollTo( 0, 0 );
}
// Start scrolling animation
_startAnimateScroll();
};
// Initialize Smooth Scroll
// Public method
// Runs functions
var init = function ( options ) {
// Feature test before initializing
if ( 'querySelector' in document && 'addEventListener' in window && Array.prototype.forEach ) {
// Selectors and variables
options = _mergeObjects( _defaults, options || {} ); // Merge user options with defaults
var toggles = document.querySelectorAll('[data-scroll]'); // Get smooth scroll toggles
// When a toggle is clicked, run the click handler
Array.prototype.forEach.call(toggles, function (toggle, index) {
toggle.addEventListener('click', animateScroll.bind( null, toggle, toggle.getAttribute('href'), options ), false);
});
}
};
// Return public methods
return {
init: init,
animateScroll: animateScroll
};
})(window, document);

View file

@ -0,0 +1,74 @@
<app-header></app-header>
<section class="container admin">
<div class="row">
<div class="col-sm-2">
<ul class="nav nav-pills nav-stacked">
<li class="active"><a href="/account">Account</a>
</li>
<li><a href="/billing">Billing</a>
</li>
</ul>
</div>
<div class="col-sm-10">
<div class="holder">
<div ng-show="user.plan == 'Free'">
<a href="/pro"><p class="lead bg-primary"><strong>Go Pro</strong> and get 500MB per file upload, no daily upload limits, no advertising and more!</p></a>
<p class="info bg-info">
<span>Plan</span>
<span class="type">Free — 20 MB max filesize, 15 file daily upload limit.</span>
<span class="type">
<strong>{{user.uploads_today}}/15</strong> files uploaded today</span>
</p>
</div>
<div ng-show="user.plan != 'Free'">
<p class="info bg-info">
<span>Plan</span>
<span class="type">Pro — 500 MB max filesize, no daily upload limit.</span>
<span class="type">
<strong>0/&infin;</strong> files uploaded today</span>
</p>
</div>
<hr>
<form role="form" ng-submit="submit(user)">
<div class="alert alert-danger" ng-show="error">{{error}}</div>
<div class="alert alert-success" ng-show="updated">Updated your details successfully</div>
<div class="form-group">
<label for="fname">Email</label>
<input type="email" class="form-control" id="fname" value="{{user.email}}" ng-model="user.email">
<span>
<strong>Required.</strong> Password resets will be sent to this address.</span>
</div>
<div class="form-group">
<label for="fname">New Password</label>
<input type="password" class="form-control" id="fname" ng-model="user.new_password">
<span>Leave this field blank unless you want to update your password.</span>
</div>
<hr>
<div class="form-group">
<label for="fname">Current Password</label>
<input type="password" class="form-control" id="fname" ng-model="user.current_password">
<span><strong>Required.</strong> When updating your details we require your current password.</span>
</div>
<button type="submit" href="#" class="btn btn-signup">Save Changes</button>
<!-- <button type="button" href="#" class="btn">Cancel</button> -->
<!-- <button type="button" class="btn btn-danger">Delete Account</button> -->
</form>
</div>
</div>
</section>
<app-footer></app-footer>

View file

@ -0,0 +1,23 @@
<app-header></app-header>
<section class="container apps">
<div class="row">
<div class="col-lg-12 col-lg-offset-0 col-md-6 col-md-offset-3">
<h1>Hostr for Mac and Windows</h1>
<ul class="list-unstyled">
<li>Drag and drop without opening your browser.</li>
<li>Get links instantly without waiting for uploads to finish</li>
<li>Easily capture and share screenshots.</li>
<li>Quick access to recent files.</li>
</ul>
</div>
<div class="col-md-6 col-md-offset-3">
<div class="downloads">
<a href="/apps/mac/Hostr-0.8.0.zip" class="btn btn-primary" target="_self">Download for Mac</a>
<a href="/apps/windows/Hostr-0.7.1.exe" class="btn btn-primary" target="_self">Download for Windows</a>
</div>
</div>
</div>
</section>
<app-footer></app-footer>

View file

@ -0,0 +1,37 @@
<app-header></app-header>
<section class="container admin">
<div class="row">
<div class="col-sm-2">
<ul class="nav nav-pills nav-stacked">
<li><a href="/account">Account</a>
</li>
<li class="active"><a href="#=/billin">Billing</a>
</li>
</ul>
</div>
<div class="col-sm-10">
<div class="holder">
<table class="table" width="100%">
<thead>
<tr>
<th>Date</th>
<th>Product</th>
<th class="amount">Amount</th>
</tr>
</thead>
<tr ng-repeat="transaction in transactions">
<td>{{transaction.date | date : "d MMM yy"}}</td>
<td>{{transaction.description}}</td>
<td align="right">${{transaction.amount}}.00</td>
</tr>
</table>
<div ng-hide="transactions.length > 0" style="text-align:center;min-height: 300px;">
You have no billing history. You should <a href="/pro">check out Pro</a> right now!
<div>
</div>
</div>
</div>
</section>
<app-footer></app-footer>

View file

@ -0,0 +1,70 @@
<section class="container header clearfix">
<img class="logo img-responsive" src="images/logo_dark.png" height="21" width="26" alt="">
<div class="stream-title">
<img src="images/streams.png" height="24" width="27" alt="">
<h3 class="add">Hostr</h3>
</div>
<div class="menu">
<img src="images/menu.png" height="6" width="24" alt="">
<div class="dropdown">
<div class="file-info">
<a href="#">Direct Link to File</a>
<span class="filesize">568kB</span>
<span class="num-downloads">5 downloads</span>
</div>
</div>
</div>
<a href="#" class="download">download (6)</a>
</section>
<section class="container stream image-preview">
<div class="content">
<a href="#" class="prev"></a>
<img class="img-responsive" src="images/large_thumb.png" height="497" width="944" alt="">
<a href="#" class="next"></a>
</div>
<span class="filename">kitty
<span class="filetype">.jpg</span>
</span>
<div class="pagination">
<a href="#">
<img src="images/gallery.png" height="24" width="24" alt="">
</a>
<a href="#">
<img src="images/tiny_thumb.png" height="25" width="24" alt="">
</a>
<a class="active" href="#">
<img src="images/tiny_thumb.png" height="25" width="24" alt="">
</a>
<a href="#">
<img src="images/tiny_thumb.png" height="25" width="24" alt="">
</a>
<a href="#">
<img src="images/tiny_thumb.png" height="25" width="24" alt="">
</a>
</div>
</section>
<section class="container footer navbar-fixed-bottom">
<ul class="nav nav-pills">
<li>
<a href="#">
<strong>Go Pro</strong>
</a>
</li>
<li><a href="#">Terms</a>
</li>
<li><a href="#">Privacy</a>
</li>
<li><a href="#">Contact</a>
</li>
<li><a href="#">API</a>
</li>
<li><a href="#">Blog</a>
</li>
</ul>
</section>

View file

@ -0,0 +1,53 @@
<section class="container header clearfix" ng-if="file" ng-cloak="file">
<div class="logo pull-left">
<a href="/"><img src="/images/logo-dark-r.png" height="22" width="26" alt=""></a>
</div>
<div class="menu" menu-dropdown>
<img src="/images/menu-retina.png" height="6" width="24" alt="" class="dots">
<div class="dropdown">
<div class="file-info">
<a href="{{::file | direct}}" ng-show="file.width" target="_blank">Original File <span class="dimensions">({{:: file.width }}x{{:: file.height }})</span></a>
<div class="meta">
<div class="date">{{::file.added | date : "d MMM yy 'at' h:mm a"}}</div>
<span class="filesize">{{:: file.size | fileSize }}</span>
<span class="num-downloads">{{ file.downloads.toLocaleString() }} <span ng-show="file.width">views</span><span ng-hide="file.width">downloads</span></span>
</div>
</div>
</div>
</div>
<span class="filename" ng-show="file.direct['970x'] || file.status == 'uploading'">{{ ::file.name }}<span class="filetype"></span></span>
</section>
<section class="container" ng-if="file" ng-show="file.status == 'uploading'" ng-cloak="file">
<div class="file-progress">
<span>File has not finished uploading&hellip;</span>
<span>Please wait.</span>
<span class="percent"><h3>{{file.percent}}%</h3></span>
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="{{file.percent}}" aria-valuemin="0" aria-valuemax="100" style="width: {{file.percent}}%;">
{{file.percent}}%
</div>
</div>
</div>
</section>
<section ng-if="file.status == 'active'" class="container image-preview" ng-show="file.direct['970x']">
<img class="img-responsive" ng-src="{{:: file.direct['970x'] }}">
</section>
<section ng-if="file.status == 'active'" class="container file-preview" ng-show="file.href && !file.direct['970x']">
<span class="file-icon"></span>
<span class="filename">{{:: file.name }}<span class="filetype"></span></span>
<form action="{{::file | direct}}" method="get" onsubmit="this.submit()">
<div class="file-warning">
<p>This file has been scanned for viruses but may still not be safe.</p>
<p class="agree"><label for="warning"><input type="checkbox" id="warning" name="warning" ng-model="warning"> I understand and want to download</label>
</div>
<button type="submit" class="btn btn-download btn-lg" ng-disabled="!warning">Download</button>
</form>
</section>

View file

@ -0,0 +1,81 @@
<app-header></app-header>
<section class="container files">
<div class="row" >
<div class="col-sm-12">
<header class="add">
<h3>Files</h3><a class="choose-file"><img src="/images/plus.png"></a>
</header>
<div ng-repeat="uploadingFile in uploadingFiles">
<div class="file" ng-show="uploadingFile.href" bs-tooltip data-title="Click file to copy link!" data-container="body">
<a ng-href="{{uploadingFile.href}}" title="{{ uploadingFile.name }}" data-clipboard-text="{{uploadingFile.href}}" clippy><div class="uploading">
<div class="throbber">
</div>
</div></a>
<span class="title truncate"><a ng-href="{{uploadingFile.href}}" title="{{ uploadingFile.name }}">{{ uploadingFile.name }}</a>
</span>
<div class="menu" menu-dropdown>
<img src="/images/gear.png" height="13" width="13" alt="">
<div class="dropdown">
<div class="file-info">
<ul class="list-unstyled">
<li><a data-clipboard-text="{{uploadingFile.href}}" clippy>Copy Link</a></li>
</ul>
</div>
</div>
</div>
</div>
<div class="file" ng-hide="uploadingFile.href">
<div class="image">
<img src="/images/clock-25.png" width="25">
</div>
<span class="title truncate"><a ng-href="{{uploadingFile.href}}" title="{{ uploadingFile.name }}">{{ uploadingFile.name }}</a>
</span>
</div>
</div>
<div class="file" ng-repeat="file in files | filter:{trashed:'false'} | filter:searchText">
<div class="image">
<a href="{{::file.href}}">
<img lazy-src="{{:: file.direct['150x'] ? (file.direct['150x']) : '/images/file-adjusted.png'}}" width="160">
</a>
</div>
<span class="title truncate"><a href="{{::file.href}}" title="{{:: file.name }}">{{:: file.name }}</a>
</span>
<div class="menu" menu-dropdown>
<img src="/images/gear.png" height="13" width="13" alt="">
<div class="dropdown">
<div class="file-info">
<ul class="list-unstyled">
<li><a data-clipboard-text="{{::file.href}}" clippy>Copy Link</a></li>
</ul>
<ul class="list-unstyled">
<li>{{:: file.added | date : "d MMM yy 'at' h:mm a"}}</li>
<li>{{:: file.size | fileSize }}</li>
<li class="sep">{{ file.downloads }} <span ng-show="::file.direct['970x']">views</span><span ng-hide="::file.direct['970x']">downloads</span></li>
</ul>
<ul class="list-unstyled">
<li><a ng-click="remove(file)">Delete</a></div></li>
</ul>
</div>
</div>
</div>
</div>
<div class="ng-cloak jumbotron info" ng-show="uploadingFiles.length === 0 && files.length === 0 && files.$resolved">
<div class="drop-zone">
<div>
<p>Right now you have no files!</p>
<p class="plus">Drop a file onto the page or click <a class="choose-file"><img src="/images/plus.png"></a> to begin.</p>
<p>For even easier uploading and sharing <a href="/apps">download our apps</a> for Mac and Windows.</a>
</div>
</div>
</div>
</div>
</div>
</section>
<app-footer></app-footer>

View file

@ -0,0 +1,20 @@
<section class="container footer">
<img src="/images/bullet-r.png" height="8" width="8" alt="">
<ul class="nav nav-pills">
<li><a href="/apps">Apps</a>
</li>
<li><a href="mailto:support@hostr.co">Support</a>
</li>
<li><a href="/terms">Terms</a>
</li>
<li><a href="/privacy">Privacy</a>
</li>
<li><a href="https://twitter.com/gethostr" target="_blank">Twitter</a>
</li>
<li>
<a href="/pro">
<strong>Go Pro</strong>
</a>
</li>
</ul>
</section>

View file

@ -0,0 +1,35 @@
<section class="container header clearfix" ng-keyup="keypress">
<div class="row">
<div class="col-sm-4 col-sm-push-4 col-xs-12">
<div class="logo">
<a href="/"><img src="/images/logo-dark-r.png" height="22" width="26" alt=""></a>
</div>
</div>
<div class="col-sm-4 col-sm-pull-4 col-xs-12">
<span class="user menu" menu-dropdown ng-click="dropdownClick()">
<span class="chevron"><img ng-src="//www.gravatar.com/avatar/{{userMD5}}?s=28&d=blank" height="28" width="28" alt="" class="avatar"></span>
{{email}}
<div class="dropdown left">
<div class="file-info">
<a href="/account">Account</a>
<a href="/logout" target="_self">Logout</a>
<hr>
<a href="/apps" target="_self">Get the app</a>
</div>
</div>
</span>
<span ng-if="user.plan =='Free'"
class="remaining"
ng-class="{limit: user.uploads_today >= user.daily_upload_allowance}"
bs-tooltip
data-placement="right"
data-title="You've uploaded {{user.uploads_today}} files today. You can upload {{user.daily_upload_allowance - user.uploads_today}} more for free.">
{{user.uploads_today}}</span>
</div>
<div class="col-sm-4 col-xs-12">
<form id="search" name="search" file-search>
<input name="query" type="search" ng-model="searchText" value="" search-shortcut placeholder="Search&hellip;">
</form>
</div>
</div>
</section>

View file

@ -0,0 +1,52 @@
<app-header></app-header>
<section class="container">
<div class="row">
<div class="col-md-offset-2 col-md-8 col-sm-12">
<h2>Privacy Policy</h2>
<h3>Introduction</h3>
<p>Hostr (we" or "us") values its visitors' privacy. This privacy policy is effective 16th February 2013; it summarizes what information we might collect from a registered user or other visitor ("you"), and what we will and will not do with it.
Please note that this privacy policy does not govern the collection and use of information by companies that Hostr does not control, nor by individuals not employed or managed by Hostr. If you visit a Web site that we mention or link to, be sure to review its privacy policy before providing the site with information.</p>
<h3>What we do with your personally identifiable information</h3>
<p>It is always up to you whether to disclose personally identifiable information to us, although if you elect not to do so, we reserve the right not to register you as a user or provide you with any products or services. "Personally identifiable information" means information that can be used to identify you as an individual, such as, for example:</p>
<ul>
<li>your name, company, email address, phone number, billing address, and shipping address your Hostr user ID and password</li>
<li>credit card information</li>
<li>any account-preference information you provide us</li>
<li>your computer's domain name and IP address, indicating</li>
<li>where your computer is located on the Internet</li>
<li>session data for your login session, so that our computer can talk' to yours while you are logged in</li>
</ul>
<p>If you do provide personally identifiable information to us, either directly or through a reseller or other business partner, we will:
not sell or rent it to a third party without your permission — although unless you opt out (see below), we may use your contact information to provide you with information we believe you need to know or may find useful, such as (for example) news about our services and products and modifications to the Terms of Service; take commercially reasonable precautions to protect the information from loss, misuse and unauthorized access, disclosure, alteration and destruction;
not use or disclose the information except:</p>
<ul>
<li>as necessary to provide services or products you have ordered, such as (for example) by providing it to a carrier to deliver products you have ordered;</li>
<li>in other ways described in this privacy policy or to which you have otherwise consented; in the aggregate with other information in such a way so that your identity cannot reasonably be determined (for example, statistical compilations);</li>
<li>as required by law, for example, in response to a subpoena or search warrant;</li>
<li>to outside auditors who have agreed to keep the information confidential;</li>
<li>as necessary to enforce the Terms of Service;</li>
<li>as necessary to protect the rights, safety, or property of Hostr, its users, or others; this may include (for example) exchanging information with other organizations for fraud protection and/or risk reduction.</li>
</ul>
<h3>Other information we collect</h3>
<p>We may collect other information that cannot be readily used to identify you, such as (for example) the domain name and IP address of your computer. We may use this information, individually or in the aggregate, for technical administration of our Web site(s); research and development; customer- and account administration; and to help us focus our marketing efforts more precisely.</p>
<h3>Cookies</h3>
<p>Hostr uses "cookies" to store personal data on your computer. We may also link information stored on your computer in cookies with personal data about specific individuals stored on our servers. If you set up your Web browser (for example, Internet Explorer or Firefox) so that cookies are not allowed, you might not be able to use some or all of the features of our Web site(s).</p>
<h3>External data storage sites</h3>
<p>We may store your data on servers provided by third party hosting vendors with whom we have contracted.</p>
<h3>Your privacy responsibilities</h3>
<p>To help protect your privacy, be sure:</p>
<ul>
<li>not to share your user ID or password with anyone else;</li>
<li>to take customary precautions to guard against "malware" (viruses, Trojan horses, bots, etc.), for example by installing and updating suitable anti-virus software.</li>
</ul>
<h3>Changes to this privacy policy</h3>
<p>We reserve the right to change this privacy policy as we deem necessary or appropriate because of legal compliance requirements or changes in our business practices. If you have provided us with an email address, we will endeavor to notify you, by email to that address, of any material change to how we will use personally identifiable information.</p>
<h3>Questions or comments?</h3>
<p>If you have questions or comments about Hostr's privacy policy, send an email to support@hostr.com.</p>
<p>Thank you for choosing Hostr!</p>
</div>
</div>
</section>
<app-footer></app-footer>

View file

@ -0,0 +1,67 @@
<app-header></app-header>
<section class="container pro">
<div class="row">
<div class="col-md-3 col-md-offset-3">
<h1>Hostr Free</h1>
<h2>Free!</h2>
<ul class="list-unstyled">
<li>Simple, Secure file sharing</li>
<li>15 Uploads per day</li>
<li>Share 25MB files</li>
<li>Ads on popular files</li>
</ul>
<button ng-hide="user.plan == 'Free'" class="btn btn-primary" ng-click="cancel()">Downgrade</button>
<button ng-show="user.plan == 'Free'" class="btn btn-primary" disabled>You</button>
</div>
<div class="col-md-3">
<h1>Hostr Pro</h1>
<h2>$6/month</h2>
<ul class="list-unstyled">
<li>All the features of Free</li>
<li>Unlimited file sharing</li>
<li>Share 500MB files</li>
<li>No ads, for you or your files</li>
</ul>
<button ng-hide="user.plan == 'Free'" class="btn btn-primary" disabled>You</button>
<button ng-show="user.plan == 'Free'" id="upgrade" class="btn btn-primary" stripe-subscribe>Go Pro</button>
</div>
</div>
<div class="row questions">
<div class="col-md-12">
<h2>Questions?</h2>
</div>
</div>
<div class="row">
<div class="col-md-3 col-md-offset-3">
<h3>Is there a minimum contract?</h3>
<p>You can cancel whenever you like. We will downgrade your account at the end of your billing cycle.</p>
</div>
<div class="col-md-3">
<h3>Can I pay yearly?</h3>
<p>Not yet, but we do plan to offer it in the near future.</p>
</div>
</div>
<div class="row">
<div class="col-md-3 col-md-offset-3">
<h3>How secure is my credit card?</h3>
<p>Every step of the transaction is protected by 256-bit SSL. We use Stripe for our credit card transactions, meaning your card details are never transmitted to us.</p>
</div>
<div class="col-md-3">
<h3>What payment methods do you accept?</h3>
<p>We accept Visa, MasterCard, and American Express.</p>
</div>
</div>
<div class="row">
<div class="col-md-3 col-md-offset-3">
<h3>Got more questions?</h3>
<p>Hit us up on <a href="mailto:support@hostr.co?subject=Hostr%20Pro%20Question">support@hostr.co</a> or click the support link below.</p>
</div>
</div>
</section>
<app-footer></app-footer>

Some files were not shown because too many files have changed in this diff Show more