refactor frontend code and build pipeline

This commit is contained in:
BuckarooBanzay 2020-04-21 13:28:57 +02:00
parent 800f071d6c
commit 00aa1d606a
21 changed files with 427 additions and 385 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
node_modules

View File

@ -1,4 +1,4 @@
name: jshint
name: jshint_backend
on: [push, pull_request]
@ -9,12 +9,9 @@ jobs:
steps:
- uses: actions/checkout@v1
- name: apt
run: sudo apt-get install -y nodejs npm
- name: npm install
- name: install
run: npm i
- name: npm test
run: npm test
- name: jshint
run: npm run jshint_backend

17
.github/workflows/jshint_frontend.yml vendored Normal file
View File

@ -0,0 +1,17 @@
name: jshint_frontend
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: apt
run: sudo apt-get install -y nodejs npm
- name: install
run: npm i
- name: jshint
run: npm run jshint_frontend

View File

@ -1,27 +1,30 @@
# Stage 1 testing
FROM node:13.13.0-alpine
FROM node:13.13.0-alpine as builder
COPY package.json /data/
COPY package-lock.json /data/
COPY src /data/src
COPY public /data/public
COPY .git/refs/heads/master /data/public/version.txt
COPY . /data
RUN cd /data && npm i && npm test
# build
RUN cd /data &&\
npm ci &&\
npm test &&\
npm run jshint_backend &&\
npm run jshint_frontend &&\
npm run bundle
# Stage 2 package
FROM node:13.13.0-alpine
COPY package.json /data/
COPY package-lock.json /data/
COPY src /data/src
COPY public /data/public
COPY .git/refs/heads/master /data/public/version.txt
COPY . /data
RUN apk update && apk add curl
RUN cd /data && npm i --only=production
RUN cd /data && npm ci --only=production
COPY --from=builder /data/public /data/public
WORKDIR /data
EXPOSE 8080
HEALTHCHECK --interval=5s --timeout=3s \
CMD curl -f http://localhost:8080/ || exit 1
CMD ["npm", "start"]

16
package-lock.json generated
View File

@ -397,6 +397,13 @@
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"fsevents": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz",
"integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==",
"dev": true,
"optional": true
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
@ -697,6 +704,15 @@
"string_decoder": "~0.10.x"
}
},
"rollup": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.6.1.tgz",
"integrity": "sha512-1RhFDRJeg027YjBO6+JxmVWkEZY0ASztHhoEUEWxOwkh4mjO58TFD6Uo7T7Y3FbmDpRTfKhM5NVxJyimCn0Elg==",
"dev": true,
"requires": {
"fsevents": "~2.1.2"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",

View File

@ -4,8 +4,11 @@
"description": "",
"main": "index.js",
"scripts": {
"test": "cd src && jshint . && cd ../public/js/ && jshint .",
"start": "node src/index.js"
"test": "echo ok",
"jshint_backend": "cd src && jshint .",
"jshint_frontend": "cd public/js && jshint .",
"start": "node src/index.js",
"bundle": "cd public/js && rollup -c rollup.config.js"
},
"author": "",
"license": "ISC",
@ -15,6 +18,7 @@
"jsonwebtoken": "^8.4.0"
},
"devDependencies": {
"jshint": "^2.10.3"
"jshint": "^2.10.3",
"rollup": "^2.6.1"
}
}

View File

@ -14,14 +14,6 @@
<script src="js/lib/mithril.min.js"></script>
<script src="js/lib/moment.js"></script>
<script src="js/state.js"></script>
<script src="js/api.js"></script>
<script src="js/service.js"></script>
<script src="js/nav.js"></script>
<script src="js/login.js"></script>
<script src="js/messages.js"></script>
<script src="js/message_detail.js"></script>
<script src="js/compose.js"></script>
<script src="js/main.js"></script>
<script src="js/bootstrap.js"></script>
</body>
</html>

2
public/js/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
bundle.js
bundle.js.map

View File

@ -4,7 +4,6 @@
"esversion": 6,
"browser": true,
"globals": {
"webmail": true,
"moment": true,
"m": true
}

View File

@ -1,32 +1,30 @@
(function(){
import state from './state.js';
var api = {};
api.fetchMails = function(){
export const fetchMails = function(){
return m.request({
url: "api/inbox",
headers: { "authorization": webmail.token }
headers: { "authorization": state.token }
});
};
api.deleteMail = function(index){
export const deleteMail = function(index){
return m.request({
method: "DELETE",
url: "api/inbox/" + index,
headers: { "authorization": webmail.token }
headers: { "authorization": state.token }
});
};
api.markRead = function(index){
export const markRead = function(index){
return m.request({
method: "POST",
url: "api/markread",
data: { index: index },
headers: { "authorization": webmail.token }
headers: { "authorization": state.token }
});
};
api.sendMail = function(recipient, subject, text){
export const sendMail = function(recipient, subject, text){
return m.request({
method: "POST",
url: "api/send",
@ -35,28 +33,21 @@ api.sendMail = function(recipient, subject, text){
subject: subject,
text: text
},
headers: { "authorization": webmail.token }
headers: { "authorization": state.token }
});
};
api.verifyToken = function(){
export const verifyToken = function(){
return m.request({
url: "api/verify",
headers: { "authorization": webmail.token }
headers: { "authorization": state.token }
});
};
api.login = function(username, password){
export const login = function(username, password){
return m.request({
method: "POST",
url: "api/login",
data: { username: username, password: password }
});
};
//publish
window.webmail.api = api;
})();

16
public/js/bootstrap.js vendored Normal file
View File

@ -0,0 +1,16 @@
(function(){
var s = document.createElement("script");
if (location.host === "127.0.0.1:8080") {
//dev
s.setAttribute("src", "js/main.js");
s.setAttribute("type", "module");
} else {
//prod
s.setAttribute("src", "js/bundle.js");
}
document.body.appendChild(s);
})();

View File

@ -1,61 +1,55 @@
(function(){
import state from './state.js';
import { sendMail } from './service.js';
var state = webmail.compose;
const Compose = {
view: function(){
return [
m("div", {class:"row"}, [
m("input[type=text]", {
class:"form-control",
placeholder:"Recipient",
value: state.compose.recipient,
oninput: function(e){ state.compose.recipient = e.target.value; }
})
]),
m("div", {class:"row"}, [
m("input[type=text]", {
class:"form-control",
placeholder:"Subject",
value: state.compose.subject,
oninput: function(e){ state.compose.subject = e.target.value; }
})
]),
m("div", {class:"row"}, [
m("textarea", {
class:"form-control",
placeholder:"Text",
style: "height: 300px;",
value: state.compose.body,
oninput: function(e){ state.compose.body = e.target.value; }
})
]),
m("div", {class:"row"}, [
m("button[type=submit]", {
class:"btn btn-sm btn-block btn-primary",
onclick: sendMail,
disabled: !state.compose.body || !state.compose.subject || !state.compose.recipient
}, "Submit")
])
];
}
};
var Compose = {
view: function(){
return [
m("div", {class:"row"}, [
m("input[type=text]", {
class:"form-control",
placeholder:"Recipient",
value: state.recipient,
oninput: function(e){ state.recipient = e.target.value; }
})
]),
m("div", {class:"row"}, [
m("input[type=text]", {
class:"form-control",
placeholder:"Subject",
value: state.subject,
oninput: function(e){ state.subject = e.target.value; }
})
]),
m("div", {class:"row"}, [
m("textarea", {
class:"form-control",
placeholder:"Text",
style: "height: 300px;",
value: state.body,
oninput: function(e){ state.body = e.target.value; }
})
]),
m("div", {class:"row"}, [
m("button[type=submit]", {
class:"btn btn-sm btn-block btn-primary",
onclick: webmail.service.sendMail,
disabled: !state.body || !state.subject || !state.recipient
}, "Submit")
])
];
}
};
export default {
view: function(){
if (state.loginState.loggedIn)
return m("div", {class:"row"}, [
m("div", {class:"col-md-2"}),
m("form", {class:"col-md-8"}, m(Compose)),
m("div", {class:"col-md-2"})
]);
webmail.routes["/compose"] = {
view: function(){
if (webmail.loginState.loggedIn)
return m("div", {class:"row"}, [
m("div", {class:"col-md-2"}),
m("form", {class:"col-md-8"}, m(Compose)),
m("div", {class:"col-md-2"})
]);
else
return null;
}
};
})();
else
return null;
}
};

View File

@ -1,67 +1,64 @@
(function(){
import state from './state.js';
import { login, logout } from './service.js';
var state = webmail.loginState;
var LoginButton = function(){
var LoginButton = function(){
var infoText = m("span", {class:"badge badge-light"}, state.loginState.errorMsg ? state.loginState.errorMsg : "");
var spinner = state.loginState.busy ? m("i", {class:"fas fa-spinner fa-spin"}) : null;
var infoText = m("span", {class:"badge badge-light"}, state.errorMsg ? state.errorMsg : "");
var spinner = state.busy ? m("i", {class:"fas fa-spinner fa-spin"}) : null;
return m("button", {
class:"btn btn-sm btn-block btn-primary",
disabled: !state.loginState.username || !state.loginState.password,
onclick: function(){ login(state.loginState.username, state.loginState.password); }
}, [spinner, " Login ", infoText]);
};
return m("button", {
class:"btn btn-sm btn-block btn-primary",
disabled: !state.username || !state.password,
onclick: function(){ webmail.service.login(state.username, state.password); }
}, [spinner, " Login ", infoText]);
};
var LogoutButton = function(){
return m("button[type=submit]", {
class:"btn btn-sm btn-block btn-secondary",
onclick: logout
}, "Logout");
};
var LogoutButton = function(){
return m("button[type=submit]", {
class:"btn btn-sm btn-block btn-secondary",
onclick: webmail.service.logout
}, "Logout");
};
var LoginForm = {
view: function(){
return [
m("div", {class:"row"}, [
m("h3", "Webmail login")
]),
m("div", {class:"row"}, [
m("input[type=text]", {
class:"form-control",
placeholder:"Playername",
disabled: state.loginState.loggedIn,
value: state.loginState.username,
oninput: function(e){ state.loginState.username = e.target.value; }
})
]),
m("div", {class:"row"}, [
m("input[type=password]", {
class:"form-control",
placeholder:"Password",
disabled: state.loginState.loggedIn,
value: state.loginState.password,
oninput: function(e){ state.loginState.password = e.target.value; }
})
]),
m("div", {class:"row"}, [
state.loginState.loggedIn ? LogoutButton() : LoginButton()
])
];
}
};
var LoginForm = {
view: function(){
return [
m("div", {class:"row"}, [
m("h3", "Webmail login")
]),
m("div", {class:"row"}, [
m("input[type=text]", {
class:"form-control",
placeholder:"Playername",
disabled: state.loggedIn,
value: state.username,
oninput: function(e){ state.username = e.target.value; }
})
]),
m("div", {class:"row"}, [
m("input[type=password]", {
class:"form-control",
placeholder:"Password",
disabled: state.loggedIn,
value: state.password,
oninput: function(e){ state.password = e.target.value; }
})
]),
m("div", {class:"row"}, [
state.loggedIn ? LogoutButton() : LoginButton()
])
];
}
};
webmail.routes["/login"] = {
view: function(){
return [
m("div", {class:"row"}, [
m("div", {class:"col-md-4"}),
m("form", {class:"col-md-4"}, m(LoginForm)),
m("div", {class:"col-md-4"})
])
];
}
};
})();
export default {
view: function(){
return [
m("div", {class:"row"}, [
m("div", {class:"col-md-4"}),
m("form", {class:"col-md-4"}, m(LoginForm)),
m("div", {class:"col-md-4"})
])
];
}
};

View File

@ -1,5 +1,5 @@
(function(){
import routes from './routes.js';
import Nav from './nav.js';
m.route(document.getElementById("app"), "/login", webmail.routes);
})();
m.route(document.getElementById("app"), "/login", routes);
m.mount(document.getElementById("nav"), Nav);

View File

@ -1,41 +1,40 @@
(function(){
import state from './state.js';
import { readMail, reply } from './service.js';
webmail.routes["/message/:id"] = {
view: function(){
if (!webmail.mails)
return m("div", "Loading...");
export default {
view: function(){
if (!state.mails)
return m("div", "Loading...");
var id = m.route.param("id");
var mail = webmail.service.readMail(id);
var id = m.route.param("id");
var mail = readMail(id);
var timeStr = "";
var timeStr = "";
if (mail.time){
var time_m = moment(mail.time * 1000);
var durationStr = moment.duration(time_m - moment()).humanize(true);
if (mail.time){
var time_m = moment(mail.time * 1000);
var durationStr = moment.duration(time_m - moment()).humanize(true);
timeStr = time_m.format("YYYY-MM-DD HH:mm:ss") + " (" + durationStr + ")";
}
var body = [];
mail.body.split("\n").forEach(function(line){
body.push(line);
body.push( m("br") );
});
var replyBtn = m("button[type=button]", {
onclick: function(){ webmail.service.reply(id); },
class: "btn btn-sm btn-primary"
}, "Reply");
return [
m("h2", mail.subject),
m("h5", [ "From: ", m("b", mail.sender), replyBtn ]),
m("h5", [ "Sent: ", m("b", timeStr) ]),
m("div", body)
];
timeStr = time_m.format("YYYY-MM-DD HH:mm:ss") + " (" + durationStr + ")";
}
};
})();
var body = [];
mail.body.split("\n").forEach(function(line){
body.push(line);
body.push( m("br") );
});
var replyBtn = m("button[type=button]", {
onclick: function(){ reply(id); },
class: "btn btn-sm btn-primary"
}, "Reply");
return [
m("h2", mail.subject),
m("h5", [ "From: ", m("b", mail.sender), replyBtn ]),
m("h5", [ "Sent: ", m("b", timeStr) ]),
m("div", body)
];
}
};

View File

@ -1,79 +1,75 @@
(function(){
import state from './state.js';
import { deleteMail } from './service.js';
var InboxRow = {
view: function(vnode){
function openMail(){
m.route.set("/message/:id", { id: vnode.attrs.row.index });
}
var InboxRow = {
view: function(vnode){
function openMail(){
m.route.set("/message/:id", { id: vnode.attrs.row.index });
}
var timeStr = "";
function deleteMail(){
webmail.service.deleteMail(vnode.attrs.row.index);
}
if (vnode.attrs.row.time){
var time_m = moment(vnode.attrs.row.time * 1000);
var durationStr = moment.duration(time_m - moment()).humanize(true);
var timeStr = "";
timeStr = time_m.format("YYYY-MM-DD HH:mm:ss") + " (" + durationStr + ")";
}
if (vnode.attrs.row.time){
var time_m = moment(vnode.attrs.row.time * 1000);
var durationStr = moment.duration(time_m - moment()).humanize(true);
var rowClass = vnode.attrs.row.unread ? "table-primary" : "";
timeStr = time_m.format("YYYY-MM-DD HH:mm:ss") + " (" + durationStr + ")";
}
var rowClass = vnode.attrs.row.unread ? "table-primary" : "";
return m("tr", {class: rowClass}, [
m("td", vnode.attrs.row.sender),
m("td", vnode.attrs.row.subject),
m("td", timeStr),
m("td", [
m("div", { class: "btn-group" }, [
m("button[type=button]", { class: "btn btn-primary", onclick: openMail },[
m("i", {class:"fa fa-envelope-open"}),
"Open"
]),
m("button[type=button]", { class: "btn btn-danger", onclick: deleteMail },[
m("i", {class:"fa fa-trash"}),
"Remove"
])
return m("tr", {class: rowClass}, [
m("td", vnode.attrs.row.sender),
m("td", vnode.attrs.row.subject),
m("td", timeStr),
m("td", [
m("div", { class: "btn-group" }, [
m("button[type=button]", { class: "btn btn-primary", onclick: openMail },[
m("i", {class:"fa fa-envelope-open"}),
"Open"
]),
m("button[type=button]", {
class: "btn btn-danger",
onclick: () => deleteMail(vnode.attrs.row.index)
},[
m("i", {class:"fa fa-trash"}),
"Remove"
])
])
]);
])
]);
}
};
var InboxTable = {
view: function(){
if (!state.mails){
return m("div", "Loading...");
}
};
var InboxTable = {
view: function(){
if (!webmail.mails){
return m("div", "Loading...");
}
var head = m("thead", m("tr", [
m("th", "Sender"),
m("th", "Subject"),
m("th", "Sent"),
m("th", "Action")
]));
var head = m("thead", m("tr", [
m("th", "Sender"),
m("th", "Subject"),
m("th", "Sent"),
m("th", "Action")
]));
var body = m("tbody", state.mails.map(function(row){
return m(InboxRow, {row: row});
}));
var body = m("tbody", webmail.mails.map(function(row){
return m(InboxRow, {row: row});
}));
return m("table",
{class:"table table-condensed table-striped table-sm"},
[head, body]
);
}
};
return m("table",
{class:"table table-condensed table-striped table-sm"},
[head, body]
);
}
};
webmail.routes["/messages"] = {
view: function(){
if (webmail.loginState.loggedIn)
return m(InboxTable);
else
return null;
}
};
})();
export default {
view: function(){
if (state.loginState.loggedIn)
return m(InboxTable);
else
return null;
}
};

View File

@ -1,37 +1,34 @@
import state from './state.js';
import { countUnread } from './service.js';
(function(){
function NavLinks(){
function NavLinks(){
var links = [];
var links = [];
links.push( m("a", {class:"nav-link", href:"#!/login"}, "Login") );
links.push( m("a", {class:"nav-link", href:"#!/login"}, "Login") );
if (webmail.loginState.loggedIn){
links.push( m("a", {class:"nav-link", href:"#!/messages"}, [
"Messages",
m("span", {class: "badge badge-light"}, webmail.service.countUnread())
])
);
links.push( m("a", {class:"nav-link", href:"#!/compose"}, "Compose") );
}
return m("ul", {class:"navbar-nav"}, links);
if (state.loginState.loggedIn){
links.push( m("a", {class:"nav-link", href:"#!/messages"}, [
"Messages",
m("span", {class: "badge badge-light"}, countUnread())
])
);
links.push( m("a", {class:"nav-link", href:"#!/compose"}, "Compose") );
}
function NavBarContent(){
return m("div", {class:"container"}, [
m("i", {class:"fa fa-envelope"}),
m("a", {class:"navbar-brand", href:"#"}, "Minetest webmail"),
m("div", {class:"navbar-collapse"}, NavLinks())
]);
return m("ul", {class:"navbar-nav"}, links);
}
function NavBarContent(){
return m("div", {class:"container"}, [
m("i", {class:"fa fa-envelope"}),
m("a", {class:"navbar-brand", href:"#"}, "Minetest webmail"),
m("div", {class:"navbar-collapse"}, NavLinks())
]);
}
export default {
view: function(){
return m("nav", {class:"navbar navbar-dark bg-dark fixed-top navbar-expand-lg"}, NavBarContent());
}
m.mount(document.getElementById("nav"), {
view: function(){
return m("nav", {class:"navbar navbar-dark bg-dark fixed-top navbar-expand-lg"}, NavBarContent());
}
});
})();
};

View File

@ -0,0 +1,10 @@
export default [{
input: 'main.js',
output: {
file :'bundle.js',
format: 'umd',
sourcemap: true,
compact: true
}
}];

12
public/js/routes.js Normal file
View File

@ -0,0 +1,12 @@
import Login from './login.js';
import Compose from './compose.js';
import Messages from './messages.js';
import MessageDetail from './message_detail.js';
export default {
"/login": Login,
"/compose": Compose,
"/messages": Messages,
"/message/:id": MessageDetail
};

View File

@ -1,73 +1,79 @@
(function(state){
var service = {};
import state from './state.js';
import {
markRead,
verifyToken,
login as api_login,
fetchMails as api_fetchMails,
sendMail as api_sendMail,
deleteMail as api_deleteMail
} from './api.js';
//verify token if available
if (webmail.token){
webmail.api.verifyToken()
if (state.token){
verifyToken()
.then(function(result){
if (result.username){
state.username = result.username;
state.loggedIn = true;
state.loginState.username = result.loginState.username;
state.loginState.loggedIn = true;
//fetch messages after token alright
service.fetchMails();
fetchMails();
}
});
}
service.login = function(username, password){
export const login = function(username, password){
if (!username || !password)
return;
state.errorMsg = "";
state.busy = true;
state.loginState.errorMsg = "";
state.loginState.busy = true;
webmail.api.login(username, password)
api_login(username, password)
.then(function(result){
state.busy = false;
state.loginState.busy = false;
if (result.success){
state.loggedIn = true;
state.errorMsg = "";
state.loginState.loggedIn = true;
state.loginState.errorMsg = "";
//save token
webmail.token = result.token;
state.token = result.token;
localStorage["webmail-token"] = result.token;
//fetch mails after login
service.fetchMails();
fetchMails();
} else {
state.errorMsg = "Login failed: " + result.message;
state.loginState.errorMsg = "Login failed: " + result.message;
}
})
.catch(function(){
state.errorMsg = "System error!";
state.busy = false;
state.loginState.errorMsg = "System error!";
state.loginState.busy = false;
});
};
service.logout = function(){
state.loggedIn = false;
webmail.mails = [];
export const logout = function(){
state.loginState.loggedIn = false;
state.mails = [];
//clear token
webmail.token = null;
state.token = null;
delete localStorage["webmail-token"];
};
service.fetchMails = function(){
if (!webmail.mails || !webmail.mails.length){
webmail.api.fetchMails()
export const fetchMails = function(){
if (!state.mails || !state.mails.length){
api_fetchMails()
.then(function(result){
webmail.mails = result;
state.mails = result;
});
}
};
service.countUnread = function(){
export const countUnread = function(){
var count = 0;
if (webmail.mails && webmail.mails.length){
webmail.mails.forEach(function(mail){
if (state.mails && state.mails.length){
state.mails.forEach(function(mail){
if (mail.unread)
count++;
});
@ -76,29 +82,23 @@ service.countUnread = function(){
return count;
};
service.sendMail = function(){
webmail.api.sendMail(webmail.compose.recipient, webmail.compose.subject, webmail.compose.body);
webmail.compose.recipient = "";
webmail.compose.subject = "";
webmail.compose.body = "";
export const sendMail = function(){
api_sendMail(state.compose.recipient, state.compose.subject, state.compose.body);
state.compose.recipient = "";
state.compose.subject = "";
state.compose.body = "";
};
service.reply = function(index){
var mail = service.readMail(index);
webmail.compose.recipient = mail.sender;
webmail.compose.subject = "Re: " + mail.subject;
webmail.compose.body = "\n---- Original message ----\n" + mail.body;
m.route.set("/compose");
};
service.readMail = function(index){
if (webmail.mails && webmail.mails.length){
var mail = webmail.mails[index-1];
export const readMail = function(index){
if (state.mails && state.mails.length){
var mail = state.mails[index-1];
//mark as read with api
if (mail.unread){
webmail.api.markRead(index);
markRead(index);
//mark read locally
mail.unread = false;
@ -108,11 +108,20 @@ service.readMail = function(index){
}
};
service.deleteMail = function(index){
return webmail.api.deleteMail(index)
export const reply = function(index){
var mail = readMail(index);
state.compose.recipient = mail.sender;
state.compose.subject = "Re: " + mail.subject;
state.compose.body = "\n---- Original message ----\n" + mail.body;
m.route.set("/compose");
};
export const deleteMail = function(index){
return api_deleteMail(index)
.then(function(){
var new_index = 1;
webmail.mails = webmail.mails
state.mails = state.mails
.filter(function(mail){ return mail.index != index; })
.map(function(mail){
mail.index = new_index++;
@ -120,9 +129,3 @@ service.deleteMail = function(index){
});
});
};
webmail.service = service;
})(webmail.loginState);

View File

@ -1,24 +1,20 @@
(function(){
var webmail = {
routes: {},
token: localStorage["webmail-token"],
loginState: {
username: "",
password: "",
loggedIn: false,
errorMsg: "",
busy: false
},
compose: {
recipient: "",
subject: "",
body: ""
},
mails: null
};
const state = {
routes: {},
token: localStorage["webmail-token"],
loginState: {
username: "",
password: "",
loggedIn: false,
errorMsg: "",
busy: false
},
compose: {
recipient: "",
subject: "",
body: ""
},
mails: null
};
//publish
window.webmail = webmail;
})();
export default state;