[Fix #19] CSR client

client
sundowndev 2018-09-08 19:18:26 +02:00
parent 53f5f8f2aa
commit 9896969136
17 changed files with 1043 additions and 960 deletions

3
client/.gitignore vendored
View File

@ -57,5 +57,4 @@ typings/
# dotenv environment variables file
.env
#dist
dist
#dist

123
client/dist/public/app.js vendored Normal file

File diff suppressed because one or more lines are too long

60
client/dist/public/index.html vendored Normal file
View File

@ -0,0 +1,60 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<title>Project manager</title>
<!-- Bootstrap core CSS -->
<link href="https://getbootstrap.com/docs/4.1/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="https://getbootstrap.com/docs/4.1/examples/offcanvas/offcanvas.css" rel="stylesheet">
</head>
<body class="bg-light">
<nav class="navbar navbar-expand-lg fixed-top navbar-dark bg-dark">
<a class="navbar-brand mr-auto mr-lg-0" href="#/">Project manager</a>
<button class="navbar-toggler p-0 border-0" type="button" data-toggle="offcanvas">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse offcanvas-collapse" id="navbarsExampleDefault">
<ul class="navbar-nav mr-auto menu">
<li class="nav-item">
<a class="nav-link" href="#/login">Sign in</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#/register">Register</a>
</li>
</ul>
</div>
</nav>
<!--<div class="nav-scroller bg-white box-shadow">
<nav class="nav nav-underline">
<a class="nav-link" href="#">Tasks</a>
<a class="nav-link" href="#">Users</a>
</nav>
</div>-->
<div id="advert"></div>
<div id="body">
<main role="main" class="container">
<div class="d-flex align-items-center p-3 my-3 text-white-50 bg-info rounded box-shadow">
<img class="mr-3" src="https://image.flaticon.com/icons/svg/138/138839.svg" alt="" width="48" height="48">
<div class="lh-100">
<h6 class="mb-0 text-white lh-100">Task manager</h6>
<small>https://github.com/Sundowndev/interview-v1</small>
</div>
</div>
</main>
</div>
<script src="app.js"></script>
</body>
</html>

1214
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,23 +5,18 @@
"main": "dist/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build:dev": "webpack --config webpack.config.js",
"dev": "tsc-watch src/index.ts --outDir ./dist --onSuccess 'node ./dist/index.js' --onFailure 'echo Beep! Compilation Failed'"
"build:dev": "webpack --config webpack.config.js --mode=development"
},
"author": "Dr. Patrick Bartsch",
"license": "MIT",
"devDependencies": {
"tsc-watch": "^1.0.26",
"uglifyjs-webpack-plugin": "^1.1.6",
"webpack": "^4.16.4"
"webpack": "^4.16.4",
"webpack-cli": "^3.1.0"
},
"dependencies": {
"@types/express": "^4.11.0",
"@sundowndev/router.js": "^1.5.6",
"bootstrap": "^4.0.0-alpha.6",
"express": "^4.16.2",
"jquery": "^3.3.1",
"ts-loader": "^3.2.0",
"twig": "^1.12.0",
"typescript": "^2.6.2"
"jquery": "^3.3.1"
}
}

200
client/src/app.js Normal file
View File

@ -0,0 +1,200 @@
let $ = require("jquery");
let Router = require('@sundowndev/router.js');
let router = new Router();
const api_url = 'http://localhost:8000';
let connected = false;
function sendRequest(method, url, data, callback) {
let request = new XMLHttpRequest();
request.open(method, api_url + url);
request.setRequestHeader("Content-Type", "application/json");
request.setRequestHeader('Accept', 'application/json');
if (localStorage.getItem('sessionToken') !== undefined) {
request.setRequestHeader('Authorization', 'Bearer ' + localStorage.getItem('sessionToken'));
}
request.responseType = 'json';
request.onload = function () {
if (callback) callback(request.response);
};
request.send(JSON.stringify(data));
}
function setAdvert(message) {
$('#advert').html('<div class="alert alert-primary alert-dismissible fade show" role="alert">\n' +
' ' + message +
' <button type="button" class="close" data-dismiss="alert" aria-label="Close">\n' +
' <span aria-hidden="true">&times;</span>\n' +
' </button>\n' +
' </div>');
}
function refreshTaskList() {
sendRequest('GET', '/tasks', null, function (response) {
$('.task-list').html('');
response.data.forEach(function (task) {
let status = (task.status === "0") ? 'Closed' : 'Open';
$('.task-list').append('<div class="media text-muted pt-3">\n' +
' <div class="media-body pb-3 mb-0 small lh-125 border-bottom border-gray">\n' +
' <div class="d-flex justify-content-between align-items-center w-100">\n' +
' <a href="/task/' + task.id + '"><strong class="text-gray-dark">' + task.title + '</strong></a>\n' +
' <span class="badge badge-pill badge-primary">' + status + '</span>\n' +
' </div>\n' +
' <spadatan class="d-block">' + task.description + '</spadatan>\n' +
' </div>\n' +
' </div>');
});
});
}
router.before('*', function () {
$(".close").on('click', () => {
$(".close").alert('close');
});
if (localStorage.getItem('sessionToken') !== null) {
connected = true;
}
if (connected) {
$('.menu').html('<li class="nav-item">\n' +
' <a class="nav-link" id="logout-link" href="/logout">Log out</a>\n' +
' </li>');
$('#logout-link').click((e) => {
e.preventDefault();
sendRequest('POST', '/logout', null, function (res) {
localStorage.removeItem('sessionToken');
router.fetchRoute('home');
});
});
}
});
router.add('home', '/', function () {
$('#body').html('<main role="main" class="container">\n' +
' <div class="d-flex align-items-center p-3 my-3 text-white-50 bg-info rounded box-shadow">\n' +
' <img class="mr-3" src="https://image.flaticon.com/icons/svg/138/138839.svg" alt="" width="48" height="48">\n' +
' <div class="lh-100">\n' +
' <h6 class="mb-0 text-white lh-100">Task manager</h6>\n' +
' <small>https://github.com/Sundowndev/interview-v1</small>\n' +
' </div>\n' +
' </div>\n' +
'\n' +
' <div id="createTaskForm"></div>\n' +
'\n' +
' <div class="my-3 p-3 bg-white rounded box-shadow">\n' +
' <h6 class="border-bottom border-gray pb-2 mb-0">All tasks</h6>\n' +
' <div class="task-list"></div>\n' +
' </div>\n' +
' </main>');
if (connected) {
$('#createTaskForm').html('<div class="my-3 p-3 bg-white rounded box-shadow">\n' +
' <form>\n' +
' <div class="form-group">\n' +
' <label for="title">Title</label>\n' +
' <input type="text" name="title" id="title" class="form-control">\n' +
' </div>\n' +
' <div class="form-group">\n' +
' <label for="description">Description</label>\n' +
' <textarea name="description" id="description" class="form-control"></textarea>\n' +
' </div>\n' +
'\n' +
' <button type="submit" id="createTask" class="btn btn-success">Create task</button>\n' +
' </form>\n' +
' </div>');
}
$('#createTask').click(function (e) {
e.preventDefault();
sendRequest('POST', '/tasks', {
title: $("#title")[0].value,
description: $("#description")[0].value
}, function (data) {
setAdvert(data.message);
refreshTaskList();
});
});
refreshTaskList();
});
router.add('login', '/login', function () {
$('#body').html('<main role="main" class="container">\n' +
' <div class="my-3 p-3 bg-white rounded box-shadow">\n' +
' <form action="#">\n' +
' <div class="form-group">\n' +
' <label for="username">Username</label>\n' +
' <input type="text" name="username" id="username" class="form-control">\n' +
' </div>\n' +
' <div class="form-group">\n' +
' <label for="password">Password</label>\n' +
' <input type="password" name="password" id="password" class="form-control">\n' +
' </div>\n' +
'\n' +
' <button type="submit" id="signin" class="btn btn-success">Sign in</button>\n' +
' </form>\n' +
' </div>\n' +
' </main>');
$('#signin').click(function (e) {
e.preventDefault();
sendRequest('POST', '/auth', {
username: $('#username')[0].value,
password: $('#password')[0].value
}, function (res) {
if (res.data.token !== undefined) {
localStorage.setItem('sessionToken', res.data.token);
router.fetchRoute('home');
} else {
setAdvert(res.message);
}
});
});
});
router.add('register', '/register', function () {
$('#body').html('<main role="main" class="container">\n' +
' <div class="my-3 p-3 bg-white rounded box-shadow">\n' +
' <form action="#">\n' +
' <div class="form-group">\n' +
' <label for="username">Username</label>\n' +
' <input type="text" name="username" id="username" class="form-control">\n' +
' </div>\n' +
'\n' +
' <div class="form-group">\n' +
' <label for="email">Email</label>\n' +
' <input type="text" name="email" id="email" class="form-control">\n' +
' </div>\n' +
'\n' +
' <div class="form-group">\n' +
' <label for="password">Password</label>\n' +
' <input type="password" name="password" id="password" class="form-control">\n' +
' </div>\n' +
'\n' +
' <button type="submit" id="register" class="btn btn-success">Register</button>\n' +
' </form>\n' +
' </div>\n' +
'</main>');
$('#register').click(function (e) {
e.preventDefault();
sendRequest('POST', '/signup', {
username: $('#username')[0].value,
password: $('#password')[0].value,
email: $('#email')[0].value
}, function (res) {
setAdvert(res.message);
});
});
});
router.run();

View File

@ -1,135 +0,0 @@
let $ = require("jquery");
//let bs = require("bootstrap");
let connected = false;
const api_url = 'http://localhost:8000';
$(".close").on('click', () => {
$(".close").alert('close');
});
if (localStorage.getItem('sessionToken') !== null) {
connected = true;
}
if (connected) {
$('.menu').html('<li class="nav-item">\n' +
' <a class="nav-link" id="logout-link" href="/logout">Log out</a>\n' +
' </li>');
$('#logout-link').click((e) => {
e.preventDefault();
sendRequest('POST', '/logout', null, function (res) {
localStorage.removeItem('sessionToken');
window.location.pathname = '/';
});
});
}
function sendRequest(method, url, data, callback) {
let request = new XMLHttpRequest();
request.open(method, api_url + url);
request.setRequestHeader("Content-Type", "application/json");
request.setRequestHeader('Accept', 'application/json');
if (localStorage.getItem('sessionToken') !== undefined) {
request.setRequestHeader('Authorization', 'Bearer ' + localStorage.getItem('sessionToken'));
}
request.responseType = 'json';
request.onload = function () {
if (callback) callback(request.response);
};
request.send(JSON.stringify(data));
}
function setAdvert(message) {
$('#advert').html('<div class="alert alert-primary alert-dismissible fade show" role="alert">\n' +
' ' + message +
' <button type="button" class="close" data-dismiss="alert" aria-label="Close">\n' +
' <span aria-hidden="true">&times;</span>\n' +
' </button>\n' +
' </div>');
}
function refreshTaskList() {
sendRequest('GET', '/tasks', null, function (response) {
$('.task-list').html('');
response.data.forEach(function (task) {
let status = 'Open';
if (task.status === "0") {
status = 'Closed';
}
$('.task-list').append('<div class="media text-muted pt-3">\n' +
' <div class="media-body pb-3 mb-0 small lh-125 border-bottom border-gray">\n' +
' <div class="d-flex justify-content-between align-items-center w-100">\n' +
' <a href="/task/' + task.id + '"><strong class="text-gray-dark">' + task.title + '</strong></a>\n' +
' <span class="badge badge-pill badge-primary">' + status + '</span>\n' +
' </div>\n' +
' <spadatan class="d-block">' + task.description + '</spadatan>\n' +
' </div>\n' +
' </div>');
});
});
}
if (connected) {
$('#createTaskForm').html('<div class="my-3 p-3 bg-white rounded box-shadow">\n' +
' <form>\n' +
' <div class="form-group">\n' +
' <label for="title">Title</label>\n' +
' <input type="text" name="title" id="title" class="form-control">\n' +
' </div>\n' +
' <div class="form-group">\n' +
' <label for="description">Description</label>\n' +
' <textarea name="description" id="description" class="form-control"></textarea>\n' +
' </div>\n' +
'\n' +
' <button type="submit" id="createTask" class="btn btn-success">Create task</button>\n' +
' </form>\n' +
' </div>');
}
$('#createTask').click(function (e) {
e.preventDefault();
sendRequest('POST', '/tasks', {
title: $("#title")[0].value,
description: $("#description")[0].value
}, function (data) {
setAdvert(data.message);
refreshTaskList();
});
});
refreshTaskList();
$('#signin').click(function (e) {
e.preventDefault();
sendRequest('POST', '/auth', {
username: $('#username')[0].value,
password: $('#password')[0].value
}, function (res) {
if (res.data.token !== undefined) {
localStorage.setItem('sessionToken', res.data.token);
window.location.pathname = '/';
} else {
setAdvert(res.message);
}
});
});
$('#register').click(function (e) {
e.preventDefault();
sendRequest('POST', '/signup', {
username: $('#username')[0].value,
password: $('#password')[0].value,
email: $('#email')[0].value
}, function (res) {
setAdvert(res.message);
});
});

View File

View File

@ -1,32 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//import * as bodyParser from "body-parser";
const express = require("express");
const http = require("http");
let path = require("path");
let app = express();
app.set('view engine', 'twig');
app.set('views', __dirname + '/views');
app.use(express.static(path.resolve(__dirname) + '/../dist/public/'));
app.get('/', (req, res) => {
res.render('index');
});
app.get('/login', (req, res) => {
res.render('login');
});
app.get('/register', (req, res) => {
res.render('register');
});
app.get('/task/{id}', (req, res) => {
//res.render('index', {title: 'Hey', message: 'Hello there!'});
});
app.get('/task/{id}/edit', (req, res) => {
//res.render('index', {title: 'Hey', message: 'Hello there!'});
});
let httpPort = 3000;
app.set("port", httpPort);
let httpServer = http.createServer(app);
// listen on provided ports
httpServer.listen(httpPort, (data) => {
console.log(`Listening on port ${httpPort}`);
});

View File

@ -1,41 +0,0 @@
//import * as bodyParser from "body-parser";
import * as express from "express";
import {Request, Response} from "express";
import * as http from 'http';
let path = require("path");
let app: express.Application = express();
app.set('view engine', 'twig');
app.set('views', __dirname + '/views');
app.use(express.static(path.resolve(__dirname)+'/../dist/public/'));
app.get('/', (req: Request, res: Response) => {
res.render('index');
});
app.get('/login', (req: Request, res: Response) => {
res.render('login');
});
app.get('/register', (req: Request, res: Response) => {
res.render('register');
});
app.get('/task/{id}', (req: Request, res: Response) => {
//res.render('index', {title: 'Hey', message: 'Hello there!'});
});
app.get('/task/{id}/edit', (req: Request, res: Response) => {
//res.render('index', {title: 'Hey', message: 'Hello there!'});
});
let httpPort = 3000;
app.set("port", httpPort);
let httpServer = http.createServer(app);
// listen on provided ports
httpServer.listen(httpPort, (data) => {
console.log(`Listening on port ${httpPort}`)
});

View File

@ -1,20 +0,0 @@
{% extends 'layout.twig' %}
{% block body %}
<main role="main" class="container">
<div class="d-flex align-items-center p-3 my-3 text-white-50 bg-info rounded box-shadow">
<img class="mr-3" src="https://image.flaticon.com/icons/svg/138/138839.svg" alt="" width="48" height="48">
<div class="lh-100">
<h6 class="mb-0 text-white lh-100">Task manager</h6>
<small>https://github.com/Sundowndev/interview-v1</small>
</div>
</div>
<div id="createTaskForm"></div>
<div class="my-3 p-3 bg-white rounded box-shadow">
<h6 class="border-bottom border-gray pb-2 mb-0">All tasks</h6>
<div class="task-list"></div>
</div>
</main>
{% endblock %}

View File

@ -1,64 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<link rel="icon" href="/favicon.ico">
<title>{% block title %}Project manager{% endblock %}</title>
<!-- Bootstrap core CSS -->
<link href="https://getbootstrap.com/docs/4.1/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="https://getbootstrap.com/docs/4.1/examples/offcanvas/offcanvas.css" rel="stylesheet">
</head>
<body class="bg-light">
{% block header %}
<nav class="navbar navbar-expand-lg fixed-top navbar-dark bg-dark">
<a class="navbar-brand mr-auto mr-lg-0" href="/">Project manager</a>
<button class="navbar-toggler p-0 border-0" type="button" data-toggle="offcanvas">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse offcanvas-collapse" id="navbarsExampleDefault">
<ul class="navbar-nav mr-auto menu">
<li class="nav-item">
<a class="nav-link" href="/login">Sign in</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/register">Register</a>
</li>
</ul>
</div>
</nav>
<!--<div class="nav-scroller bg-white box-shadow">
<nav class="nav nav-underline">
<a class="nav-link" href="#">Tasks</a>
<a class="nav-link" href="#">Users</a>
</nav>
</div>-->
<div id="advert"></div>
{% endblock %}
{% block body %}
<main role="main" class="container">
<div class="d-flex align-items-center p-3 my-3 text-white-50 bg-info rounded box-shadow">
<img class="mr-3" src="https://image.flaticon.com/icons/svg/138/138839.svg" alt="" width="48" height="48">
<div class="lh-100">
<h6 class="mb-0 text-white lh-100">Task manager</h6>
<small>https://github.com/Sundowndev/interview-v1</small>
</div>
</div>
</main>
{% endblock %}
{% block scripts %}
<script src="/app.js"></script>
{% endblock %}
</body>
</html>

View File

@ -1,20 +0,0 @@
{% extends 'layout.twig' %}
{% block body %}
<main role="main" class="container">
<div class="my-3 p-3 bg-white rounded box-shadow">
<form action="#">
<div class="form-group">
<label for="username">Username</label>
<input type="text" name="username" id="username" class="form-control">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" name="password" id="password" class="form-control">
</div>
<button type="submit" id="signin" class="btn btn-success">Sign in</button>
</form>
</div>
</main>
{% endblock %}

View File

@ -1,26 +0,0 @@
{% extends 'layout.twig' %}
{% block body %}
<main role="main" class="container">
<div class="my-3 p-3 bg-white rounded box-shadow">
<form action="#">
<div class="form-group">
<label for="username">Username</label>
<input type="text" name="username" id="username" class="form-control">
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="text" name="email" id="email" class="form-control">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" name="password" id="password" class="form-control">
</div>
<button type="submit" id="register" class="btn btn-success">Register</button>
</form>
</div>
</main>
{% endblock %}

View File

@ -1,8 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true
}
}

View File

@ -14,7 +14,7 @@ fs.readdirSync('node_modules')
const scriptConfig = {
name: 'app',
entry: {
app: './src/assets/app.js'
app: './src/app.js'
},
target: 'web',
output: {
@ -23,32 +23,4 @@ const scriptConfig = {
},
};
const appConfig = {
name: 'index',
entry: {
index: './src/index.ts',
},
target: 'node',
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
},
devtool: 'source-map',
resolve: {
// Add `.ts` and `.tsx` as a resolvable extension.
extensions: ['.ts', '.tsx', '.js']
},
module: {
rules: [
// all files with a `.ts` or `.tsx` extension will be handled by `ts-loader`
{test: /\.tsx?$/, loader: 'ts-loader'}
]
},
externals: nodeModules,
node: {
__filename: true,
__dirname: true
}
};
module.exports = [scriptConfig, appConfig];
module.exports = [scriptConfig];

View File

@ -43,16 +43,6 @@ services:
ports:
- '443:443'
client:
build:
context: .
dockerfile: ./Dockerfile.client
volumes:
# Comment out the next line in production
- ./client:/usr/src/app:rw
ports:
- 3000:3000
# For development purposes
adminer:
image: adminer