refactor frontend code and build pipeline
This commit is contained in:
parent
800f071d6c
commit
00aa1d606a
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
@ -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
17
.github/workflows/jshint_frontend.yml
vendored
Normal 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
|
29
Dockerfile
29
Dockerfile
@ -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
16
package-lock.json
generated
@ -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",
|
||||
|
10
package.json
10
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
2
public/js/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
bundle.js
|
||||
bundle.js.map
|
@ -4,7 +4,6 @@
|
||||
"esversion": 6,
|
||||
"browser": true,
|
||||
"globals": {
|
||||
"webmail": true,
|
||||
"moment": true,
|
||||
"m": true
|
||||
}
|
||||
|
@ -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
16
public/js/bootstrap.js
vendored
Normal 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);
|
||||
})();
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
@ -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"})
|
||||
])
|
||||
];
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
];
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
})();
|
||||
};
|
||||
|
10
public/js/rollup.config.js
Normal file
10
public/js/rollup.config.js
Normal 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
12
public/js/routes.js
Normal 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
|
||||
};
|
@ -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);
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user