diff --git a/lib/actions/root.js b/lib/actions/root.js
new file mode 100644
index 0000000..5d6ca07
--- /dev/null
+++ b/lib/actions/root.js
@@ -0,0 +1,13 @@
+import { bindActionCreators } from 'redux';
+
+import * as ActionsSession from 'vanth/actions/session';
+import * as ActionsURL from 'vanth/actions/url';
+import * as ActionsUser from 'vanth/actions/user';
+
+import RootStore from 'vanth/store/root';
+
+module.exports = {
+ Session : bindActionCreators(ActionsSession, RootStore.dispatch),
+ URL : bindActionCreators(ActionsURL, RootStore.dispatch),
+ User : bindActionCreators(ActionsUser, RootStore.dispatch),
+}
diff --git a/lib/actions/session.js b/lib/actions/session.js
new file mode 100644
index 0000000..4818888
--- /dev/null
+++ b/lib/actions/session.js
@@ -0,0 +1,35 @@
+import * as ActionTools from 'vanth/actions/tools';
+
+export function createSession(username, password, nextPath=null) {
+ const payload = {
+ password: password,
+ username: username,
+ };
+ return ActionTools.fetchAndDispatch(
+ '/session/',
+ 'SESSION_POST_BEGIN',
+ 'SESSION_POST_COMPLETE',
+ 'SESSION_POST_ERROR',
+ ActionTools.Methods.POST,
+ payload,
+ );
+}
+
+export function get() {
+ return ActionTools.fetchAndDispatch(
+ '/session/',
+ 'SESSION_GET_BEGIN',
+ 'SESSION_GET_COMPLETE',
+ 'SESSION_GET_ERROR'
+ );
+}
+
+export function logout(uri) {
+ return ActionTools.fetchAndDispatch(
+ uri,
+ 'SESSION_DELETE_BEGIN',
+ 'SESSION_DELETE_COMPLETE',
+ 'SESSION_DELETE_ERROR',
+ ActionTools.Methods.DELETE,
+ );
+}
diff --git a/lib/actions/tools.js b/lib/actions/tools.js
new file mode 100644
index 0000000..c3516d0
--- /dev/null
+++ b/lib/actions/tools.js
@@ -0,0 +1,102 @@
+import _ from 'lodash';
+
+import Config from 'vanth/config';
+import { ActionType } from 'vanth/constants';
+import * as Fetch from 'vanth/fetch';
+
+export const Methods = {
+ DELETE : 'delete',
+ GET : 'get',
+ PATCH : 'patch',
+ POST : 'post',
+ PUT : 'put',
+}
+
+export function action(type) {
+ return function(data) {
+ let action = {
+ type: ActionType[type],
+ }
+ if(data != undefined) {
+ action.data = data;
+ }
+ if(!action.type) {
+ throw new Error(`An action type is required. Could not find an action type constant for ${type}`);
+ }
+ return action;
+ }
+}
+
+export function ensureConstantAction(constant) {
+ if(typeof constant === "string") {
+ if(!ActionType[constant]) {
+ let message = `${constant} is not a valid constant - you'll need to add it to constants.js`;
+ console.error(message)
+ throw new Error(message);
+ }
+ return action(constant);
+ }
+ return constant;
+}
+
+export function fetchAndDispatch(url, start, end, failed, method=Methods.GET, payload) {
+ let actionStart = ensureConstantAction(start);
+ let actionEnd = ensureConstantAction(end);
+ let actionFailed = ensureConstantAction(failed);
+
+ if(!Fetch[method]) {
+ throw new Error(`Invalid method for fetcher: ${method}`);
+ }
+
+ if(!payload && (method === Methods.POST || method === Methods.PUT)) {
+ throw new Error(`A payload is required for a ${method} method`);
+ }
+
+ let actionData = {};
+ if(method === Methods.PUT || method === Methods.DELETE) {
+ actionData.uri = url;
+ } else {
+ actionData.url = url;
+ }
+
+ return dispatch => {
+ dispatch(actionStart(actionData));
+ let fullURL = url.indexOf('://') >= 0 ? url : Config.API + url;
+ return Fetch[method].call(this, fullURL, payload)
+ .then(response => {
+ let result = response.json;
+ switch(method) {
+ case Methods.GET:
+ break;
+ case Methods.POST:
+ result = _.assign({}, payload, {
+ uri: response.headers.get('Location')
+ }, actionData);
+ break;
+ case Methods.PUT:
+ result = _.assign({}, payload, {
+ uri: url
+ });
+ break;
+ case Methods.DELETE:
+ result = actionData;
+ break;
+ }
+ let eventType = {
+ [Methods.DELETE] : 'RESOURCE_DELETE',
+ [Methods.GET] : 'RESOURCE_GET',
+ [Methods.PUT] : 'RESOURCE_PUT',
+ [Methods.POST] : 'RESOURCE_POST',
+ }[method];
+ dispatch(ensureConstantAction(eventType)(result));
+ dispatch(actionEnd(result));
+ return result;
+ })
+ .catch(data => {
+ actionData.errors = data.errors ? data.errors : [data];
+ dispatch(actionFailed(actionData));
+ throw data;
+ });
+ }
+}
+
diff --git a/lib/actions/url.js b/lib/actions/url.js
new file mode 100644
index 0000000..950c914
--- /dev/null
+++ b/lib/actions/url.js
@@ -0,0 +1,29 @@
+import { ActionType } from 'vanth/constants';
+
+export function change(oldURL, newURL) {
+ return {
+ type : ActionType.URL_CHANGE,
+ data : {
+ oldURL : oldURL,
+ newURL : newURL,
+ }
+ }
+}
+
+export function replace(newURL) {
+ let oldURL = window.location.href;
+ history.replaceState(null, '', '#' + newURL);
+ return change(oldURL, window.location.href);
+}
+
+export function navigate(newURL, query) {
+ let oldURL = window.location.href;
+ let url = '#' + newURL;
+ if(query) {
+ url = "?nextPath=" + query + url;
+ } else {
+ url = "/" + url;
+ }
+ history.pushState(null, '', url);
+ return change(oldURL, window.location.href);
+}
diff --git a/lib/actions/user.js b/lib/actions/user.js
new file mode 100644
index 0000000..5814fcc
--- /dev/null
+++ b/lib/actions/user.js
@@ -0,0 +1,17 @@
+import * as ActionTools from 'vanth/actions/tools';
+
+export function register(name, username, password) {
+ const payload = {
+ name : name,
+ password: password,
+ username: username,
+ }
+ return ActionTools.fetchAndDispatch(
+ '/user/',
+ 'USER_REGISTER_BEGIN',
+ 'USER_REGISTER_COMPLETE',
+ 'USER_REGISTER_ERROR',
+ ActionTools.Methods.POST,
+ payload
+ );
+}
diff --git a/lib/app.js b/lib/app.js
new file mode 100644
index 0000000..b3cfb33
--- /dev/null
+++ b/lib/app.js
@@ -0,0 +1,38 @@
+import { connect, Provider } from 'react-redux';
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+import Actions from 'vanth/actions/root';
+import RootStore from 'vanth/store/root';
+import Routes from 'vanth/routes';
+
+const App = connect(state => state)(React.createClass({
+ componentWillMount: function() {
+ window.onhashchange = function(event) {
+ if(!event) return;
+ Actions.URL.change(event.oldURL, event.newURL || window.location.hash);
+ }
+ },
+ render: function() {
+ let allProps = _.assign({}, this.props, this.state);
+ return (
+
+ );
+ }
+}));
+
+ReactDOM.render((
+
+
+
+), document.getElementById('container'));
+
+window.onload = function() {
+ let thing = Actions.Session.get()
+ thing.then(session => {
+ console.log(session);
+ }).catch(error => {
+ //Actions.URL.navigate('/login');
+ });
+}
+module.exports = App;
diff --git a/lib/config.js b/lib/config.js
new file mode 100644
index 0000000..aea2928
--- /dev/null
+++ b/lib/config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ API: 'http://www.vanth.com',
+}
diff --git a/lib/constants.js b/lib/constants.js
new file mode 100644
index 0000000..1195e21
--- /dev/null
+++ b/lib/constants.js
@@ -0,0 +1,33 @@
+var make_constants = function(constants) {
+ let result = {};
+ for(var i = 0; i < constants.length; i++) {
+ let constant = constants[i];
+ result[constant] = constant;
+ }
+ return result;
+}
+
+export const ActionType = make_constants([
+ 'RESOURCE_DELETE',
+ 'RESOURCE_GET',
+ 'RESOURCE_POST',
+ 'RESOURCE_PUT',
+
+ 'SESSION_DELETE_BEGIN',
+ 'SESSION_DELETE_COMPLETE',
+ 'SESSION_DELETE_ERROR',
+ 'SESSION_GET_BEGIN',
+ 'SESSION_GET_COMPLETE',
+ 'SESSION_GET_ERROR',
+ 'SESSION_POST_BEGIN',
+ 'SESSION_POST_COMPLETE',
+ 'SESSION_POST_ERROR',
+
+ 'URL_CHANGE',
+ 'URL_NAVIGATE',
+ 'URL_REPLACE',
+
+ 'USER_REGISTER_BEGIN',
+ 'USER_REGISTER_COMPLETE',
+ 'USER_REGISTER_ERROR',
+]);
diff --git a/lib/dashboard.js b/lib/dashboard.js
new file mode 100644
index 0000000..642cb95
--- /dev/null
+++ b/lib/dashboard.js
@@ -0,0 +1,33 @@
+import * as BS from 'react-bootstrap';
+import React from 'react';
+
+import * as Actions from 'vanth/actions/root';
+
+var Dashboard = React.createClass({
+ logout: function() {
+ Actions.Session.logout(this.props.session.uri).then(() => {
+ Actions.URL.navigate('/login');
+ });
+ },
+ render: function() {
+ return (
+
+
+
+
+ Vanth
+
+
+
+ Thing
+
+ Logout
+
+
+
+
+ );
+ }
+});
+
+module.exports = Dashboard
diff --git a/lib/fetch.js b/lib/fetch.js
new file mode 100644
index 0000000..b449335
--- /dev/null
+++ b/lib/fetch.js
@@ -0,0 +1,107 @@
+function FetchError(url, status, errors) {
+ this.errors = errors;
+ this.message = `Status code ${status} was returned from ${url}`;
+ this.name = 'FetchError';
+ this.status = status;
+ this.url = url;
+}
+
+let _handleResults = function(resolve, reject, url, fetchRequest) {
+ fetchRequest
+ .then(response => {
+ if(response.status == 204) {
+ resolve({
+ headers : response.headers,
+ json : null,
+ text : '',
+ });
+ } else if(response.status >= 400) {
+ console.error(`The request to ${url} failed with status ${response.status}`, response);
+ response.json()
+ .then(json => {
+ reject(new FetchError(url, response.status, json.errors));
+ })
+ .catch(reject);
+ } else {
+ if(response.headers.get('Content-Type') == 'application/json') {
+ response.json()
+ .then(json => {
+ resolve({
+ headers : response.headers,
+ json : json,
+ text : null,
+ });
+ }).catch(reject);
+ } else {
+ response.text()
+ .then(text => {
+ resolve({
+ headers : response.headers,
+ json : null,
+ text : text,
+ });
+ }).catch(reject);
+ }
+ }
+ })
+ .catch(error => {
+ if(error instanceof Error) {
+ reject(error);
+ } else {
+ console.error("Unrecognized error raised when fetching", error);
+ reject(new Error("Unknown error occurred during fetch"));
+ }
+ });
+}
+
+export function get(url) {
+ return new Promise(function(resolve, reject) {
+ _handleResults(resolve, reject, url,
+ fetch(url, {
+ credentials : 'include',
+ method : 'GET',
+ })
+ );
+ });
+}
+
+export function post(url, data) {
+ return new Promise(function(resolve, reject) {
+ _handleResults(resolve, reject, url,
+ fetch(url, {
+ credentials : 'include',
+ headers : {
+ "Content-Type": "application/json"
+ },
+ method : 'POST',
+ body : JSON.stringify(data),
+ })
+ );
+ });
+}
+
+export function put(url, data) {
+ return new Promise(function(resolve, reject) {
+ _handleResults(resolve, reject, url,
+ fetch(url, {
+ credentials : 'include',
+ headers : {
+ "Content-Type": "application/json"
+ },
+ method : 'PUT',
+ body : JSON.stringify(data),
+ })
+ );
+ });
+}
+
+module.exports.delete = function(url) {
+ return new Promise(function(resolve, reject) {
+ _handleResults(resolve, reject, url,
+ fetch(url, {
+ credentials : 'include',
+ method : 'DELETE',
+ })
+ );
+ });
+}
diff --git a/lib/login.js b/lib/login.js
new file mode 100644
index 0000000..2659738
--- /dev/null
+++ b/lib/login.js
@@ -0,0 +1,76 @@
+import React from 'react';
+import * as BS from 'react-bootstrap';
+import { bindActionCreators } from 'redux';
+
+import Actions from 'vanth/actions/root';
+
+module.exports = React.createClass({
+ getInitialState() {
+ return {
+ password : null,
+ username : null,
+ }
+ },
+
+ handleChange: function(parameter) {
+ return (e) => {
+ this.setState({
+ [parameter] : e.target.value
+ });
+ }
+ },
+
+ handleSubmit: function(e) {
+ e.preventDefault();
+
+ Actions.Session.createSession(
+ this.state.username,
+ this.state.password,
+ this.props.url.search.nextPath || "/"
+ ).then(result => {
+ Actions.Session.get();
+ Actions.URL.navigate('/');
+ }).catch(error => {
+ console.error(error);
+ });
+ },
+
+ render: function() {
+ const forgotPassword = Forgot Password;
+ const pending = false;
+ return (
+
+
+
+ Login
+
+
+
+
+
+ );
+ }
+});
diff --git a/lib/middleware.js b/lib/middleware.js
new file mode 100644
index 0000000..d941a02
--- /dev/null
+++ b/lib/middleware.js
@@ -0,0 +1,12 @@
+import { applyMiddleware, createStore } from 'redux';
+
+import thunkMiddleware from 'redux-thunk';
+import createLogger from 'redux-logger';
+
+const loggerMiddleware = createLogger();
+var createStoreWithMiddleware = applyMiddleware(
+ thunkMiddleware, // lets us dispatch() functions
+ loggerMiddleware // neat middleware that logs actions
+)(createStore);
+
+module.exports = createStoreWithMiddleware;
diff --git a/lib/register.js b/lib/register.js
new file mode 100644
index 0000000..39099f3
--- /dev/null
+++ b/lib/register.js
@@ -0,0 +1,87 @@
+import React from 'react';
+import * as BS from 'react-bootstrap';
+import { bindActionCreators } from 'redux';
+
+import Actions from 'vanth/actions/root';
+
+
+module.exports = React.createClass({
+ getInitialState() {
+ return {
+ name : null,
+ password : null,
+ username : null,
+ }
+ },
+
+ handleChange: function(parameter) {
+ return (e) => {
+ this.setState({
+ [parameter] : e.target.value
+ });
+ }
+ },
+
+ handleSubmit: function(e) {
+ e.preventDefault();
+
+ Actions.User.register(
+ this.state.name,
+ this.state.username,
+ this.state.password,
+ this.props.url.search.nextPath || "/"
+ ).then(result => {
+ Actions.Session.get();
+ Actions.URL.navigate('/');
+ }).catch(error => {
+ console.error(error);
+ });
+ },
+
+ render: function() {
+ const pending = false;
+ return (
+
+
+
+ Vanth - register a new user
+
+
+
+
+
+ );
+ }
+});
diff --git a/lib/routes.js b/lib/routes.js
new file mode 100644
index 0000000..51892a4
--- /dev/null
+++ b/lib/routes.js
@@ -0,0 +1,63 @@
+import React from 'react';
+import PathToRegexp from 'path-to-regexp';
+
+import Dashboard from 'vanth/dashboard';
+import Login from 'vanth/login';
+import Register from 'vanth/register';
+
+const Router = React.createClass({
+ routes: {
+ "/" : Dashboard,
+ "/login" : Login,
+ "/register" : Register,
+ },
+ render: function() {
+ var toRender = null;
+ for(var path in this.routes) {
+ var element = this.routes[path];
+ var keys = [];
+ var pattern = PathToRegexp(path, keys);
+ var match = pattern.exec(this.props.hash);
+ if(match) {
+ if(!!toRender) {
+ console.warn("Matched more than one route. First route was", toRender.path, " this match is ", path);
+ }
+ var route = {};
+ for(var i = 0; i < keys.length; i++) {
+ let key = keys[i];
+ route[key.name] = match[i+1];
+ }
+ var props = _.assign({}, this.props, {route: route});
+ toRender = {
+ element : React.createElement(element, props),
+ path : path
+ }
+ }
+ }
+ if(!toRender) {
+ return (
+
+ );
+ } else {
+ return (
+
+ {toRender.element}
+
+ );
+ }
+ }
+});
+
+var Routes = React.createClass({
+ render: function() {
+ var hash = this.props.url.location.hash.substr(1);
+
+ return (
+
+ );
+ }
+});
+
+module.exports = Routes
diff --git a/lib/store/root.js b/lib/store/root.js
new file mode 100644
index 0000000..b6e8b7f
--- /dev/null
+++ b/lib/store/root.js
@@ -0,0 +1,14 @@
+import { combineReducers } from 'redux';
+import createStoreWithMiddleware from 'vanth/middleware';
+
+import SessionReducer from 'vanth/store/session';
+import URLReducer from 'vanth/store/url';
+
+const root = combineReducers({
+ session : SessionReducer,
+ url : URLReducer,
+});
+
+const store = createStoreWithMiddleware(root);
+
+module.exports = store;
diff --git a/lib/store/session.js b/lib/store/session.js
new file mode 100644
index 0000000..1654f12
--- /dev/null
+++ b/lib/store/session.js
@@ -0,0 +1,19 @@
+import _ from 'lodash';
+import * as Constants from 'vanth/constants';
+
+const emptyState = {
+ name : null,
+ username : null,
+ uri : null,
+};
+
+var reducer = function(state = emptyState, action) {
+ switch (action.type) {
+ case Constants.ActionType.SESSION_GET_COMPLETE:
+ return _.assign({}, state, action.data);
+ default:
+ return state;
+ }
+}
+
+module.exports = reducer;
diff --git a/lib/store/url.js b/lib/store/url.js
new file mode 100644
index 0000000..e9cc0d6
--- /dev/null
+++ b/lib/store/url.js
@@ -0,0 +1,34 @@
+import urllite from 'urllite';
+
+import { ActionType } from 'vanth/constants';
+
+let _parseSearch = function(location) {
+ let search = {};
+ let query = location.search.substring(1);
+ let vars = query.split('&');
+ for(var i = 0; i < vars.length; i++) {
+ var pair = vars[i].split('=');
+ search[pair[0]] = decodeURIComponent(pair[1]);
+ }
+ return search;
+}
+
+const initialState = {
+ location : urllite(window.location),
+ search : _parseSearch(urllite(window.location)),
+};
+
+var reducer = function(state = initialState, action) {
+ switch (action.type) {
+ case ActionType.URL_CHANGE:
+ let location = urllite(action.data.newURL);
+ return _.assign({}, state, {
+ location: location,
+ search: _parseSearch(location)
+ });
+ default:
+ return state;
+ }
+}
+
+module.exports = reducer;