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
|
juggl/config/config.php
|
||||||
graphics
|
graphics
|
||||||
.vscode
|
.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;
|
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)
|
function addRecordTags(array $record_tags)
|
||||||
{
|
{
|
||||||
if ($record_tags === null) return;
|
if ($record_tags === null) return;
|
||||||
|
|
|
@ -67,6 +67,21 @@ function getProjects($user_id)
|
||||||
return $results;
|
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)
|
function getProjectRecordDerivedData($user_id, $project_id)
|
||||||
{
|
{
|
||||||
$durationAttribute = "SUM(duration) AS total_duration";
|
$durationAttribute = "SUM(duration) AS total_duration";
|
||||||
|
@ -132,6 +147,21 @@ function getRunningRecords($user_id)
|
||||||
return $results;
|
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)
|
function updateEndRecord($user_id, $params)
|
||||||
{
|
{
|
||||||
$record_id = $params->get("record_id");
|
$record_id = $params->get("record_id");
|
||||||
|
@ -196,9 +226,12 @@ function getRecordExternalData($record)
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Duration
|
// Duration and running
|
||||||
if ($record["end_time"] == NULL) {
|
if ($record["end_time"] == NULL) {
|
||||||
$record["duration"] = calcDuration($record["start_time"]);
|
$record["duration"] = calcDuration($record["start_time"]);
|
||||||
|
$record["running"] = true;
|
||||||
|
} else {
|
||||||
|
$record["running"] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tags
|
// 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