Sync commit
This commit is contained in:
parent
a63b582fe5
commit
ab1514ffc9
31 changed files with 1354 additions and 2 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,3 +3,4 @@ juggl/config/config.path
|
|||
juggl/config/config.php
|
||||
graphics
|
||||
.vscode
|
||||
juggl-vue/package-lock.json
|
||||
|
|
29
juggl-server/api/getRecords.php
Normal file
29
juggl-server/api/getRecords.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . "/services/apiBranch.inc.php");
|
||||
require_once(__DIR__ . "/services/jsonBuilder.inc.php");
|
||||
require_once(__DIR__ . "/services/responses.inc.php");
|
||||
require_once(__DIR__ . "/services/jugglDbApi.inc.php");
|
||||
|
||||
class GetRecordsBranch extends ApiBranch
|
||||
{
|
||||
function get(ParamCleaner $params)
|
||||
{
|
||||
respondStatus(405);
|
||||
}
|
||||
|
||||
function post(ParamCleaner $params)
|
||||
{
|
||||
$user_id = $params->get("user_id");
|
||||
|
||||
$records = getRecords($user_id);
|
||||
|
||||
$json = new JsonBuilder();
|
||||
$json->addRecords($records);
|
||||
|
||||
respondJson($json);
|
||||
}
|
||||
}
|
||||
|
||||
$branch = new GetRecordsBranch();
|
||||
$branch->execute();
|
29
juggl-server/api/getUser.php
Normal file
29
juggl-server/api/getUser.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . "/services/apiBranch.inc.php");
|
||||
require_once(__DIR__ . "/services/jsonBuilder.inc.php");
|
||||
require_once(__DIR__ . "/services/responses.inc.php");
|
||||
require_once(__DIR__ . "/services/jugglDbApi.inc.php");
|
||||
|
||||
class GetUserBranch extends ApiBranch
|
||||
{
|
||||
function get(ParamCleaner $params)
|
||||
{
|
||||
respondStatus(405);
|
||||
}
|
||||
|
||||
function post(ParamCleaner $params)
|
||||
{
|
||||
$user_id = $params->get("user_id");
|
||||
|
||||
$user = getUser($user_id);
|
||||
|
||||
$json = new JsonBuilder();
|
||||
$json->addUsers([$user]);
|
||||
|
||||
respondJson($json);
|
||||
}
|
||||
}
|
||||
|
||||
$branch = new GetUserBranch();
|
||||
$branch->execute();
|
|
@ -57,6 +57,23 @@ class JsonBuilder
|
|||
return $this;
|
||||
}
|
||||
|
||||
function addUsers(array $users)
|
||||
{
|
||||
if ($users === null) return;
|
||||
|
||||
$columns = array(
|
||||
"user_id" => "",
|
||||
"name" => "",
|
||||
"mail_address" => ""
|
||||
);
|
||||
|
||||
$this->jsonData['users'] = array();
|
||||
foreach ($users as $user) {
|
||||
$this->jsonData['users'][] = $this->createJsonArray($user, $columns);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
function addRecordTags(array $record_tags)
|
||||
{
|
||||
if ($record_tags === null) return;
|
||||
|
|
|
@ -67,6 +67,21 @@ function getProjects($user_id)
|
|||
return $results;
|
||||
}
|
||||
|
||||
function getUser($user_id)
|
||||
{
|
||||
$db = new DbOperations();
|
||||
$db->select("users", ["user_id", "name", "mail_address"]);
|
||||
$db->where("user_id", Comparison::EQUAL, $user_id);
|
||||
$result = $db->execute();
|
||||
|
||||
if (count($result) <= 0) {
|
||||
return null;
|
||||
}
|
||||
$result = $result[0];
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
function getProjectRecordDerivedData($user_id, $project_id)
|
||||
{
|
||||
$durationAttribute = "SUM(duration) AS total_duration";
|
||||
|
@ -132,6 +147,21 @@ function getRunningRecords($user_id)
|
|||
return $results;
|
||||
}
|
||||
|
||||
function getRecords($user_id)
|
||||
{
|
||||
$db = new DbOperations();
|
||||
$db->select("time_records");
|
||||
$db->where("user_id", Comparison::EQUAL, $user_id);
|
||||
$results = $db->execute();
|
||||
|
||||
// Is still running?
|
||||
foreach ($results as $key => $record) {
|
||||
$results[$key] = getRecordExternalData($record);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
function updateEndRecord($user_id, $params)
|
||||
{
|
||||
$record_id = $params->get("record_id");
|
||||
|
@ -196,9 +226,12 @@ function getRecordExternalData($record)
|
|||
return null;
|
||||
}
|
||||
|
||||
// Duration
|
||||
// Duration and running
|
||||
if ($record["end_time"] == NULL) {
|
||||
$record["duration"] = calcDuration($record["start_time"]);
|
||||
$record["running"] = true;
|
||||
} else {
|
||||
$record["running"] = false;
|
||||
}
|
||||
|
||||
// Tags
|
||||
|
|
3
juggl-vue/.browserslistrc
Normal file
3
juggl-vue/.browserslistrc
Normal file
|
@ -0,0 +1,3 @@
|
|||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
14
juggl-vue/.eslintrc.js
Normal file
14
juggl-vue/.eslintrc.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
extends: ["plugin:vue/essential", "eslint:recommended", "@vue/prettier"],
|
||||
parserOptions: {
|
||||
parser: "babel-eslint"
|
||||
},
|
||||
rules: {
|
||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off"
|
||||
}
|
||||
};
|
23
juggl-vue/.gitignore
vendored
Normal file
23
juggl-vue/.gitignore
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
24
juggl-vue/README.md
Normal file
24
juggl-vue/README.md
Normal file
|
@ -0,0 +1,24 @@
|
|||
# juggl-vue
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
3
juggl-vue/babel.config.js
Normal file
3
juggl-vue/babel.config.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
presets: ["@vue/cli-plugin-babel/preset"]
|
||||
};
|
35
juggl-vue/package.json
Normal file
35
juggl-vue/package.json
Normal file
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "juggl-vue",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.21.0",
|
||||
"bootstrap": "^4.5.3",
|
||||
"bootstrap-vue": "^2.21.1",
|
||||
"core-js": "^3.6.5",
|
||||
"vue": "^2.6.11",
|
||||
"vue-router": "^3.2.0",
|
||||
"vuex": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"node-sass": "^4.12.0",
|
||||
"prettier": "^1.19.1",
|
||||
"sass-loader": "^8.0.2",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
}
|
||||
}
|
BIN
juggl-vue/public/favicon.ico
Normal file
BIN
juggl-vue/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 103 KiB |
25
juggl-vue/public/index.html
Normal file
25
juggl-vue/public/index.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<!-- TODO: What is htmlWebpackPlugin? Was in title before -->
|
||||
<!-- <%= htmlWebpackPlugin.options.title %> -->
|
||||
<title>
|
||||
Juggl
|
||||
</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
|
||||
Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
|
||||
</html>
|
19
juggl-vue/src/App.vue
Normal file
19
juggl-vue/src/App.vue
Normal file
|
@ -0,0 +1,19 @@
|
|||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "App"
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
// Import custom SASS variable overrides, or alternatively
|
||||
// define your variable overrides here instead
|
||||
@import '@/style/theme.sass'
|
||||
|
||||
// Import Bootstrap and BootstrapVue source SCSS files
|
||||
@import '~bootstrap/scss/bootstrap.scss'
|
||||
@import '~bootstrap-vue/src/index.scss'
|
||||
</style>
|
BIN
juggl-vue/src/assets/logo.png
Normal file
BIN
juggl-vue/src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
42
juggl-vue/src/components/base/BaseContainer.vue
Normal file
42
juggl-vue/src/components/base/BaseContainer.vue
Normal file
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<b-container id="grid" fluid>
|
||||
<b-row>
|
||||
<b-col id="column" :class="[width, {'mx-auto':center}]">
|
||||
<slot />
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "BaseContainer",
|
||||
props: {
|
||||
width: {
|
||||
default: "medium",
|
||||
type: String
|
||||
},
|
||||
center: {
|
||||
default: true,
|
||||
type: Boolean
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
#column.slimmer
|
||||
max-width: 300px
|
||||
|
||||
#column.slim
|
||||
max-width: 450px
|
||||
|
||||
#column.medium
|
||||
max-width: 800px
|
||||
|
||||
#column.wide
|
||||
max-width: 1000px
|
||||
|
||||
#column.open
|
||||
max-width: inherit
|
||||
</style>
|
49
juggl-vue/src/components/base/BaseLogo.vue
Normal file
49
juggl-vue/src/components/base/BaseLogo.vue
Normal file
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<div>
|
||||
<b-link to="/">
|
||||
<b-img
|
||||
:src="require('../../assets/logo.png')"
|
||||
alt="Juggl"
|
||||
:height="heightSize"
|
||||
:center="center"
|
||||
/>
|
||||
</b-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "BaseLogo",
|
||||
props: {
|
||||
size: {
|
||||
default: "small",
|
||||
type: String
|
||||
},
|
||||
center: {
|
||||
default: false,
|
||||
type: Boolean
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
heightSize: function() {
|
||||
let sizes = {
|
||||
mini: "35px",
|
||||
normal: "64px",
|
||||
tiny: "80px",
|
||||
smaller: "110px",
|
||||
small: "150px",
|
||||
medium: "300px",
|
||||
large: "450px",
|
||||
big: "600px",
|
||||
huge: "800px",
|
||||
massive: "960px"
|
||||
};
|
||||
let targetSize = sizes[this.size];
|
||||
if (targetSize === undefined) return sizes["small"];
|
||||
else return targetSize;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style />
|
40
juggl-vue/src/components/base/BaseTitle.vue
Normal file
40
juggl-vue/src/components/base/BaseTitle.vue
Normal file
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<h1 id="title" :class="[size, { center: center }]" class="bold pt-5 pb-3">
|
||||
<slot />
|
||||
</h1>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "BaseTitle",
|
||||
props: {
|
||||
size: {
|
||||
default: "medium",
|
||||
type: String
|
||||
},
|
||||
center: {
|
||||
default: true,
|
||||
type: Boolean
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
#title.tiny
|
||||
font-size: 1em
|
||||
#title.small
|
||||
font-size: 1.1em
|
||||
#title.medium
|
||||
font-size: 1.3em
|
||||
#title.large
|
||||
font-size: 1.7em
|
||||
#title.huge
|
||||
font-size: 2em
|
||||
|
||||
#title.center
|
||||
text-align: center
|
||||
|
||||
.bold
|
||||
font-weight: bold
|
||||
</style>
|
80
juggl-vue/src/components/forms/FormLogin.vue
Normal file
80
juggl-vue/src/components/forms/FormLogin.vue
Normal file
|
@ -0,0 +1,80 @@
|
|||
<template>
|
||||
<b-form @submit="submitForm">
|
||||
<b-form-group id="email-group" label-for="email">
|
||||
<b-form-input
|
||||
id="email"
|
||||
v-model="form.user_id"
|
||||
type="number"
|
||||
required
|
||||
placeholder="User ID"
|
||||
trim
|
||||
>
|
||||
</b-form-input>
|
||||
</b-form-group>
|
||||
<b-form-group id="password-group" label-for="password">
|
||||
<b-form-input
|
||||
id="password"
|
||||
v-model="form.api_key"
|
||||
type="password"
|
||||
required
|
||||
placeholder="API Key"
|
||||
>
|
||||
</b-form-input>
|
||||
</b-form-group>
|
||||
<b-form-invalid-feedback :state="!failed">
|
||||
Password or email invalid.
|
||||
</b-form-invalid-feedback>
|
||||
<b-button variant="primary" type="submit" block>
|
||||
Log in
|
||||
</b-button>
|
||||
</b-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import store from "@/store";
|
||||
|
||||
export default {
|
||||
name: "FormLogin",
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
user_id: null,
|
||||
api_key: null
|
||||
},
|
||||
failed: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Submits the form. Assupmtion: Form is valid, based on required flags.
|
||||
*/
|
||||
submitForm: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Try to login
|
||||
store.dispatch("login")
|
||||
.login(this.form.user_id, this.form.api_key)
|
||||
.then(r => {
|
||||
if (r !== true) {
|
||||
this.failed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// On success redirect to target or dashboard
|
||||
var target = "/";
|
||||
if (this.$route.query.redirect) target = this.$route.query.redirect;
|
||||
this.$router.push(target);
|
||||
})
|
||||
.catch(e => {
|
||||
console.log(e);
|
||||
this.failed = true;
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
44
juggl-vue/src/components/layout/LayoutMinimal.vue
Normal file
44
juggl-vue/src/components/layout/LayoutMinimal.vue
Normal file
|
@ -0,0 +1,44 @@
|
|||
<template>
|
||||
<div>
|
||||
<BaseLogo id="logo" size="smaller" center class="space-top" />
|
||||
<BaseContainer :width="width" center class="space-bottom">
|
||||
<BaseTitle v-if="title" center size="huge" class="centered">
|
||||
{{ title }}
|
||||
</BaseTitle>
|
||||
<slot />
|
||||
</BaseContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseContainer from "@/components/base/BaseContainer";
|
||||
import BaseLogo from "@/components/base/BaseLogo";
|
||||
import BaseTitle from "@/components/base/BaseTitle";
|
||||
|
||||
export default {
|
||||
name: "LayoutMinimal",
|
||||
components: {
|
||||
BaseContainer,
|
||||
BaseLogo,
|
||||
BaseTitle
|
||||
},
|
||||
props: {
|
||||
width: {
|
||||
default: "",
|
||||
type: String
|
||||
},
|
||||
title: {
|
||||
default: "",
|
||||
type: String
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
.space-top
|
||||
margin-top: 5rem
|
||||
|
||||
.space-bottom
|
||||
margin-bottom: 5rem
|
||||
</style>
|
78
juggl-vue/src/components/layout/LayoutNavbarPrivate.vue
Normal file
78
juggl-vue/src/components/layout/LayoutNavbarPrivate.vue
Normal file
|
@ -0,0 +1,78 @@
|
|||
<template>
|
||||
<div>
|
||||
<header>
|
||||
<BaseContainer width="wide" center>
|
||||
<ul id="header-container">
|
||||
<li>
|
||||
<BaseLogo size="normal"/>
|
||||
</li>
|
||||
<!-- <li style="float: right">
|
||||
<input
|
||||
id="user-id"
|
||||
class="public hidden"
|
||||
type="text"
|
||||
placeholder="User ID"
|
||||
/>
|
||||
<input
|
||||
id="api-key"
|
||||
class="public hidden"
|
||||
type="password"
|
||||
placeholder="API Key"
|
||||
/>
|
||||
<button id="auth-btn" onclick="handleAuthBtn()">Log In</button>
|
||||
</li> -->
|
||||
</ul>
|
||||
</BaseContainer>
|
||||
</header>
|
||||
<main>
|
||||
<BaseContainer width="medium" center>
|
||||
<slot />
|
||||
</BaseContainer>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseContainer from "@/components/base/BaseContainer.vue";
|
||||
import BaseLogo from "@/components/base/BaseLogo.vue";
|
||||
|
||||
export default {
|
||||
name: "LayoutNavbarPrivate",
|
||||
components: {
|
||||
BaseContainer,
|
||||
BaseLogo
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
@import '@/style/theme.sass'
|
||||
|
||||
header
|
||||
width: 100%
|
||||
position: fixed
|
||||
top: 0px
|
||||
box-shadow: 0px 0px 2rem 1rem #000D
|
||||
height: $navbar-height
|
||||
background-color: $background-primary
|
||||
|
||||
#header-container
|
||||
padding: 0px 20px
|
||||
margin: auto
|
||||
list-style: none
|
||||
line-height: navbar-height
|
||||
|
||||
img
|
||||
margin-top: 0.5rem
|
||||
height: 3rem
|
||||
width: auto
|
||||
|
||||
*
|
||||
vertical-align: baseline
|
||||
display: inline-block
|
||||
|
||||
main
|
||||
padding: 0px 20px
|
||||
margin-top: 6rem
|
||||
|
||||
</style>
|
17
juggl-vue/src/main.js
Normal file
17
juggl-vue/src/main.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import Vue from "vue";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import store from "./store";
|
||||
import { BootstrapVue, BootstrapVueIcons } from "bootstrap-vue";
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
// Install BootstrapVue
|
||||
Vue.use(BootstrapVue);
|
||||
Vue.use(BootstrapVueIcons);
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
render: (h) => h(App),
|
||||
}).$mount("#app");
|
43
juggl-vue/src/router/index.js
Normal file
43
juggl-vue/src/router/index.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
import Vue from "vue";
|
||||
import VueRouter from "vue-router";
|
||||
import Login from "../views/Login.vue";
|
||||
import Home from "../views/Home.vue";
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/login",
|
||||
name: "Login",
|
||||
component: Login,
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
name: "Home",
|
||||
component: Home,
|
||||
// beforeEnter: requireAuth,
|
||||
},
|
||||
];
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: "history",
|
||||
base: process.env.BASE_URL,
|
||||
routes,
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks authentication before proceeding
|
||||
*/
|
||||
// TODO: Authentication required in router
|
||||
// function requireAuth(to, from, next) {
|
||||
// if (!userService.isLoggedIn()) {
|
||||
// next({
|
||||
// path: "/login",
|
||||
// query: { redirect: to.fullPath },
|
||||
// });
|
||||
// } else {
|
||||
// next();
|
||||
// }
|
||||
// }
|
||||
|
||||
export default router;
|
35
juggl-vue/src/services/api.service.js
Normal file
35
juggl-vue/src/services/api.service.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
import axios from "axios";
|
||||
import { store } from "@/store";
|
||||
|
||||
/**
|
||||
* A wrapper for the used fetch API, currently axios.
|
||||
* Uses some default values from the config (e.g. ApiUrl).
|
||||
*
|
||||
* Authentication already integrated.
|
||||
*
|
||||
* Returns promises.
|
||||
*/
|
||||
export const apiService = {
|
||||
get(resource, options) {
|
||||
return this.getApi().get(resource, options);
|
||||
},
|
||||
|
||||
post(resource, json, options) {
|
||||
return this.getApi().post(resource, json, options);
|
||||
},
|
||||
|
||||
put(resource, json, options) {
|
||||
return this.getApi().put(resource, json, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates an instance of the used api and sets necessary headers
|
||||
*/
|
||||
getApi() {
|
||||
var options = {
|
||||
baseURL: store.getters.apiUrl,
|
||||
};
|
||||
|
||||
return axios.create(options);
|
||||
},
|
||||
};
|
113
juggl-vue/src/services/helper.service.js
Normal file
113
juggl-vue/src/services/helper.service.js
Normal file
|
@ -0,0 +1,113 @@
|
|||
// TODO: Do I need this file?
|
||||
|
||||
/**
|
||||
* Contains a bunch of helper functions.
|
||||
*/
|
||||
export const helperService = {
|
||||
/**
|
||||
* Converts number into a human readable percent integer.
|
||||
*
|
||||
* @param {*} r Floating point percent number
|
||||
*/
|
||||
asPercent(r) {
|
||||
return Math.floor(r * 100);
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts number into a human readable floating point number with a digit behind the delimiter.
|
||||
*
|
||||
* @param {*} r Floating point number
|
||||
*/
|
||||
asFloat(r) {
|
||||
return Math.floor(r * 10) / 10;
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts timestamp into a human readable date format.
|
||||
*
|
||||
* @param {*} d ISO Timestamp
|
||||
*/
|
||||
asDateFromISO(d) {
|
||||
return helperService.asDateFromObj(new Date(d));
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts timestamp into a human readable date format.
|
||||
*
|
||||
* @param {*} d Date object
|
||||
*/
|
||||
asDateFromObj(d) {
|
||||
return new Intl.DateTimeFormat("de").format(d);
|
||||
},
|
||||
|
||||
/**
|
||||
* Makes sure, that the given text is not too long.
|
||||
*
|
||||
* @param {*} text Some text that might be shortened
|
||||
* @param {*} maxLength Max number of characters
|
||||
*
|
||||
* @returns The shortened or same text
|
||||
*/
|
||||
keepItShort(text, maxLength = 20, ellipsis = "...") {
|
||||
if (text.length > maxLength) {
|
||||
// Adding ellipsis
|
||||
var short = text.substring(0, maxLength - ellipsis.length).trim();
|
||||
return short + ellipsis;
|
||||
} else return text;
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts a date object into a date-time-timezone string, and sets times and timezone to zero.
|
||||
*
|
||||
* @source https://stackoverflow.com/a/43528844/7376120
|
||||
*
|
||||
* @param {*} date A date object
|
||||
*
|
||||
* @returns Date as string in the used format
|
||||
*/
|
||||
toISODate(date) {
|
||||
var timezoneOffset = date.getMinutes() + date.getTimezoneOffset();
|
||||
var timestamp = date.getTime() + timezoneOffset * 1000;
|
||||
var correctDate = new Date(timestamp);
|
||||
|
||||
correctDate.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
return correctDate.toISOString();
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds current duration to a record.
|
||||
* Copied from original juggl code.
|
||||
* @param {*} record The record instance to update.
|
||||
*/
|
||||
addDuration(record) {
|
||||
if (record.end_time != null) return record;
|
||||
|
||||
record.duration =
|
||||
(new Date().getTime() - new Date(record.start_time).getTime()) /
|
||||
1000;
|
||||
|
||||
return record;
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts a datetime object into the necessary string format for server requests.
|
||||
* Copied from original juggl code.
|
||||
* @param {*} date
|
||||
*/
|
||||
dateToString(date) {
|
||||
return (
|
||||
date.getFullYear() +
|
||||
"-" +
|
||||
(date.getMonth() + 1) +
|
||||
"-" +
|
||||
date.getDate() +
|
||||
" " +
|
||||
date.getHours() +
|
||||
":" +
|
||||
date.getMinutes() +
|
||||
":" +
|
||||
date.getSeconds()
|
||||
);
|
||||
},
|
||||
};
|
222
juggl-vue/src/services/juggl.service.js
Normal file
222
juggl-vue/src/services/juggl.service.js
Normal file
|
@ -0,0 +1,222 @@
|
|||
import { apiService } from "@/services/api.service";
|
||||
import { helperService } from "@/services/helper.service";
|
||||
|
||||
/**
|
||||
* A collection of functions to retreive and send all user-specific data.
|
||||
*/
|
||||
export const jugglService = {
|
||||
/**
|
||||
* Fetches the user from the API.
|
||||
*
|
||||
* @returns A promise
|
||||
*/
|
||||
getUser() {
|
||||
return apiService.post("/getUser.php");
|
||||
},
|
||||
|
||||
getProjects() {
|
||||
return apiService.post("/getProjects.php");
|
||||
},
|
||||
|
||||
getRecords() {
|
||||
return apiService.post("/getRecords.php");
|
||||
},
|
||||
|
||||
getRunningRecords() {
|
||||
return apiService.post("/getRunningRecords.php");
|
||||
},
|
||||
|
||||
startRecord(projectId, startTime = null) {
|
||||
if (startTime == null) startTime = new Date();
|
||||
return apiService.post("/startRecord.php", {
|
||||
project_id: projectId,
|
||||
start_time: helperService.dateToString(startTime),
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Requests a list of all semesters from the current user.
|
||||
*
|
||||
* @returns A promise
|
||||
*/
|
||||
getSemesterList() {
|
||||
return apiService.get("/semester");
|
||||
},
|
||||
|
||||
/**
|
||||
* Requests all the data from a specific semester from the current user.
|
||||
*
|
||||
* @param {*} semesterNr Nr starting at 1 from the desired semester to gather data from
|
||||
*
|
||||
* @returns A promise
|
||||
*/
|
||||
getSemester(semesterNr) {
|
||||
var path = "/semester/" + semesterNr;
|
||||
return apiService.get(path).then((semester) => {
|
||||
// Add semester nr to modules
|
||||
semester.data.modules.forEach((module) => {
|
||||
module.semester_nr = semester.nr;
|
||||
});
|
||||
|
||||
return semester;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a new semester to the current user.
|
||||
*
|
||||
* @param {*} data An object with {nr, startdate, enddate}
|
||||
*
|
||||
* @returns A promise
|
||||
*/
|
||||
addSemester(data) {
|
||||
var path = "/semester";
|
||||
return apiService.put(path, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a new module to the current user.
|
||||
*
|
||||
* @param {*} semesterNr Associated semester nr
|
||||
* @param {*} data An object with {name, required_total, required_per_sheet}
|
||||
*
|
||||
* @returns A promise
|
||||
*/
|
||||
addModule(semesterNr, data) {
|
||||
var path = "/semester/" + semesterNr + "/module";
|
||||
return apiService.put(path, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a new sheet to the current user.
|
||||
*
|
||||
* @param {*} semesterNr Associated semester nr
|
||||
* @param {*} semesterNr Associated module name
|
||||
* @param {*} data An object with {date, points, total, is_draft, percentage}
|
||||
*
|
||||
* @returns A promise
|
||||
*/
|
||||
addSheet(semesterNr, moduleName, data) {
|
||||
var path =
|
||||
"/semester/" + semesterNr + "/module/" + moduleName + "/sheet";
|
||||
return apiService.put(path, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers a new user.
|
||||
*
|
||||
* @param {*} data An object with {date, points, total, is_draft, percentage}
|
||||
*
|
||||
* @returns A promise
|
||||
*/
|
||||
signUp(data) {
|
||||
var path = "/user/signup";
|
||||
return apiService
|
||||
.post(path, data)
|
||||
.then((r) => {
|
||||
return { success: true, msg: r.data.msg };
|
||||
// Later
|
||||
// if (r.status === 204) return { success: true, msg: "" };
|
||||
// else return { success: false, msg: r.data.msg };
|
||||
})
|
||||
.catch((r) => {
|
||||
return { success: false, msg: r.response.data.msg };
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies a user with a token.
|
||||
*
|
||||
* @param {*} token Server generated token
|
||||
*
|
||||
* @returns A promise
|
||||
*/
|
||||
verify(token) {
|
||||
var path = "/user/verify";
|
||||
return apiService
|
||||
.post(path, { token: token })
|
||||
.then((r) => {
|
||||
return { success: true, msg: r.data.msg };
|
||||
})
|
||||
.catch((r) => {
|
||||
return { success: false, msg: r.response.data.msg };
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Requests all the data from a specific semester from the current user.
|
||||
*
|
||||
* @param {*} semesterNr Nr starting at 1 from the desired semester to gather data from
|
||||
*
|
||||
* @returns A promise
|
||||
*/
|
||||
getModule(semesterNr, moduleName) {
|
||||
var path = "/semester/" + semesterNr + "/module/" + moduleName;
|
||||
|
||||
return apiService.get(path).then((module) => {
|
||||
module.data.semester_nr = semesterNr;
|
||||
|
||||
// Add semester nr and module name to sheets
|
||||
module.data.sheets.forEach((sheet) => {
|
||||
sheet.semester_nr = semesterNr;
|
||||
sheet.module_name = moduleName;
|
||||
});
|
||||
|
||||
return module;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends credentials to the server in case of successful authentication, saves the jwt in localStorage.
|
||||
*
|
||||
* @param {*} email Email of user credentials
|
||||
* @param {*} pass Password of user credentials
|
||||
* @returns A promise with on parameter, which is true in case of successful authentication
|
||||
*/
|
||||
login(email, pass) {
|
||||
// Is already logged in?
|
||||
if (this.isLoggedIn()) {
|
||||
this.logout();
|
||||
}
|
||||
|
||||
// Try to log in
|
||||
var user = {
|
||||
email: email,
|
||||
password: pass,
|
||||
};
|
||||
|
||||
return apiService
|
||||
.post("/user/login", user)
|
||||
.then((r) => {
|
||||
localStorage.token = r.data;
|
||||
return true;
|
||||
})
|
||||
.catch(() => {
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the jwt.
|
||||
*/
|
||||
getToken() {
|
||||
return localStorage.token;
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the stored jwt and therefor prevents access to any page that requires authentication.
|
||||
*/
|
||||
logout() {
|
||||
delete localStorage.token;
|
||||
apiService.get("/user/logout");
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the current authentication state.
|
||||
*/
|
||||
isLoggedIn() {
|
||||
// This !! converts expression to boolean with unchanged value
|
||||
// First ! converts to boolean and inverts value and second ! inverts again
|
||||
return !!localStorage.token;
|
||||
},
|
||||
};
|
144
juggl-vue/src/services/path.service.js
Normal file
144
juggl-vue/src/services/path.service.js
Normal file
|
@ -0,0 +1,144 @@
|
|||
import router from "@/router";
|
||||
import { userService } from "@/services/user.service";
|
||||
|
||||
// TODO: Do I need this file?
|
||||
/**
|
||||
* Contains a bunch of path helper functions.
|
||||
*/
|
||||
export const pathService = {
|
||||
/**
|
||||
* Redirects to the not found page.
|
||||
*
|
||||
* @param {*} reason Optional: A reason to explain to the user, why they got redirected
|
||||
* @param {*} sourceRoute Optional: Adds additional information from the source route to the query
|
||||
*/
|
||||
notFound(reason, sourceRoute) {
|
||||
var payload = {};
|
||||
|
||||
if (reason !== undefined) {
|
||||
payload.reason = reason;
|
||||
}
|
||||
|
||||
if (sourceRoute !== undefined) {
|
||||
payload.source = sourceRoute.path;
|
||||
}
|
||||
|
||||
router.push({
|
||||
path: "/404",
|
||||
query: payload,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Validates and gets all data to a path of the form /dashboard/:sem:-sem
|
||||
*
|
||||
* @param {*} route Object that describes the current path and its parameters
|
||||
*
|
||||
* @returns A promise of the form ({semester: semester})
|
||||
*/
|
||||
validateSemPath(route) {
|
||||
var semesterNr = this.getSemesterNrFromParam(route.params.semesterName);
|
||||
|
||||
// Invalid path?
|
||||
if (semesterNr === undefined) {
|
||||
this.notFound("Invalid URL", route);
|
||||
}
|
||||
|
||||
// Gather more data
|
||||
return userService
|
||||
.getSemester(semesterNr)
|
||||
.then((semester) => {
|
||||
return { semester: semester.data };
|
||||
})
|
||||
.catch(() => {
|
||||
// Semester could not be found
|
||||
this.notFound(semesterNr + ". semester not found", route);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Validates and gets all data to a path of the form /dashboard/:sem:-sem/:module:
|
||||
*
|
||||
* @param {*} route Object that describes the current path and its parameters
|
||||
*
|
||||
* @returns A promise of the form ({semester: semester, module: module})
|
||||
*/
|
||||
validateSemModulePath(route) {
|
||||
// First evaluate semester and than the module
|
||||
return this.validateSemPath(route).then((obj) => {
|
||||
var moduleName = route.params.moduleName;
|
||||
|
||||
// Gather more data
|
||||
return userService
|
||||
.getModule(obj.semester.nr, moduleName)
|
||||
.then((module) => {
|
||||
return { semester: obj.semester, module: module.data };
|
||||
})
|
||||
.catch(() => {
|
||||
// Module could not be found
|
||||
this.notFound(
|
||||
'"' + moduleName + '" module not found',
|
||||
route
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Extracts the semester nr based on the url parameter in the format ":semesterNr:-sem".
|
||||
*
|
||||
* @param {*} semesterParam In the format ":semesterNr:-sem"
|
||||
*
|
||||
* @returns Semester Nr. if valid, otherwise undefined
|
||||
*/
|
||||
getSemesterNrFromParam(semesterParam) {
|
||||
let re = new RegExp("^([1-9][0-9]*)-sem$");
|
||||
var res = semesterParam.match(re);
|
||||
|
||||
// Found a match?
|
||||
if (res === null) {
|
||||
return undefined; // No
|
||||
} else {
|
||||
// First group is semester nr
|
||||
return res[1];
|
||||
}
|
||||
},
|
||||
|
||||
getBreadcrumbs(route) {
|
||||
let sites = route.path.split("/");
|
||||
let siteCount = sites.length;
|
||||
|
||||
return sites.reduce((crumbs, current, index) => {
|
||||
// Ignore empty parts
|
||||
if (current.length <= 0) {
|
||||
return crumbs;
|
||||
}
|
||||
|
||||
var name = current;
|
||||
// Handle semester name
|
||||
var semNr = this.getSemesterNrFromParam(current);
|
||||
if (semNr !== undefined) {
|
||||
name = semNr + ". Semester";
|
||||
}
|
||||
// Handle decoding and capitalization
|
||||
name = decodeURIComponent(name);
|
||||
name = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
|
||||
let part = {
|
||||
text: name,
|
||||
};
|
||||
|
||||
let isActive = index + 1 == siteCount;
|
||||
if (isActive) {
|
||||
part.active = true;
|
||||
} else {
|
||||
part.to = crumbs[crumbs.length - 1]
|
||||
? crumbs[crumbs.length - 1].to + "/" + current
|
||||
: "/" + current;
|
||||
}
|
||||
|
||||
crumbs.push(part);
|
||||
return crumbs;
|
||||
}, []);
|
||||
},
|
||||
};
|
56
juggl-vue/src/store/index.js
Normal file
56
juggl-vue/src/store/index.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
import Vue from "vue";
|
||||
import Vuex from "vuex";
|
||||
import { jugglService } from "@/services/juggl.service.js";
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
apiUrl: "https://juggl.giller.dev/api",
|
||||
apiKey: undefined,
|
||||
user: undefined,
|
||||
projects: [],
|
||||
records: [],
|
||||
},
|
||||
mutations: {
|
||||
setKey(state, key) {
|
||||
state.key = key;
|
||||
},
|
||||
setProjects(state, projects) {
|
||||
state.projects = projects;
|
||||
},
|
||||
setRecords(state, records) {
|
||||
state.records = records;
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
runningRecords: (state) => {
|
||||
return state.records.filter((record) => record.running);
|
||||
},
|
||||
finishedRecords: (state) => {
|
||||
return state.records.filter((record) => !record.running);
|
||||
},
|
||||
apiKey: (state) => state.apiKey,
|
||||
user: (state) => state.user,
|
||||
apiUrl: (state) => state.apiUrl,
|
||||
isLoggedIn: (state) => !!state.apiKey,
|
||||
records: (state) => state.records,
|
||||
projects: (state) => state.projects,
|
||||
getProjectById: (state, getters) => (id) => {
|
||||
return getters.projects.find(
|
||||
(project) => project.project_id === id
|
||||
);
|
||||
},
|
||||
getRecordById: (state, getters) => (id) => {
|
||||
return getters.records.find((record) => record.record_id === id);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
loadProjects({ commit }) {
|
||||
commit("setProjects");
|
||||
},
|
||||
login({ commit, userId, apiKey }) {
|
||||
return jugglService.login();
|
||||
},
|
||||
},
|
||||
});
|
93
juggl-vue/src/style/theme.sass
Normal file
93
juggl-vue/src/style/theme.sass
Normal file
|
@ -0,0 +1,93 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Karla:ital,wght@0,400;0,700;1,400;1,700&display=swap')
|
||||
|
||||
// Color scheme
|
||||
$grey: #BBB
|
||||
$black: #000
|
||||
$white: #fff
|
||||
$primary: #F00
|
||||
$secondary: $grey
|
||||
|
||||
$background-primary: #222
|
||||
$background-secondary: #000
|
||||
// $background-gradient: linear-gradient(165deg, $background-primary 65%, $background-secondary 100%)
|
||||
|
||||
$font-primary: $white
|
||||
$font-secondary: $primary
|
||||
$font-inverted: $black
|
||||
$font-link: $secondary
|
||||
|
||||
// Default text
|
||||
*
|
||||
font-family: "Karla", sans-serif
|
||||
color: $font-primary
|
||||
transition-duration: 0.1s
|
||||
|
||||
::selection
|
||||
background: $primary
|
||||
color: $black
|
||||
|
||||
// Components
|
||||
$navbar-height: 4rem
|
||||
|
||||
body
|
||||
background: $background-primary !important
|
||||
margin: 0;
|
||||
// background: $background-primary !important
|
||||
// background: -moz-linear-gradient(165deg, $background-primary 65%, $background-secondary 100%) !important
|
||||
// background: -webkit-linear-gradient(165deg, $background-primary 65%, $background-secondary 100%) !important
|
||||
// background: linear-gradient(165deg, $background-primary 65%, $background-secondary 100%) !important
|
||||
// background-attachment: fixed !important
|
||||
|
||||
.hidden
|
||||
display: none
|
||||
|
||||
.form-group
|
||||
label
|
||||
color: $font-secondary
|
||||
margin-bottom: -0.3em
|
||||
margin-left: 0.3em
|
||||
input
|
||||
background-color: $background-primary !important
|
||||
color: $font-primary !important
|
||||
|
||||
a
|
||||
color: $font-link !important
|
||||
|
||||
// .custom-checkbox
|
||||
// label
|
||||
// color: $font-primary !important
|
||||
|
||||
// a.btn-secondary, a.btn-primary
|
||||
// color: $font-primary !important
|
||||
|
||||
// #title
|
||||
// font-weight: bold
|
||||
|
||||
// .breadcrumb
|
||||
// background-color: #FFF0 !important
|
||||
|
||||
// .breadcrumb-item + .breadcrumb-item::before
|
||||
// content: ">" !important
|
||||
|
||||
// .breadcrumb-item.active
|
||||
// span
|
||||
// color: $font-secondary !important
|
||||
|
||||
// .modal-content
|
||||
// background-color: $background-primary
|
||||
|
||||
// header.b-calendar-grid-caption, .b-calendar-grid-weekdays *, header.b-calendar-header *
|
||||
// color: $font-inverted !important
|
||||
|
||||
// .b-form-datepicker
|
||||
// background-color: $background-primary !important
|
||||
|
||||
// .b-form-datepicker label:not(.text-muted)
|
||||
// color: $font-primary !important
|
||||
|
||||
// .modal-header button.close
|
||||
// color: $font-primary !important
|
||||
|
||||
// input:disabled
|
||||
// color: $font-secondary !important
|
||||
// border-color: $grey !important
|
21
juggl-vue/src/views/Home.vue
Normal file
21
juggl-vue/src/views/Home.vue
Normal file
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<LayoutNavbarPrivate>
|
||||
Hey there
|
||||
<b-link to="/login">Go to login</b-link>
|
||||
</LayoutNavbarPrivate>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LayoutNavbarPrivate from "@/components/layout/LayoutNavbarPrivate";
|
||||
|
||||
export default {
|
||||
name: "Home",
|
||||
components: {
|
||||
LayoutNavbarPrivate
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
20
juggl-vue/src/views/Login.vue
Normal file
20
juggl-vue/src/views/Login.vue
Normal file
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<LayoutMinimal title="Login" width="slimmer">
|
||||
<FormLogin/>
|
||||
<b-link to="/">Go to home</b-link>
|
||||
</LayoutMinimal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// @ is an alias to /src
|
||||
import LayoutMinimal from "@/components/layout/LayoutMinimal";
|
||||
import FormLogin from "@/components/forms/FormLogin";
|
||||
|
||||
export default {
|
||||
name: "Login",
|
||||
components: {
|
||||
LayoutMinimal,
|
||||
FormLogin
|
||||
}
|
||||
};
|
||||
</script>
|
Loading…
Reference in a new issue