Sync commit

This commit is contained in:
Maximilian Giller 2020-12-20 17:27:26 +01:00
parent a63b582fe5
commit ab1514ffc9
31 changed files with 1354 additions and 2 deletions

3
.gitignore vendored
View file

@ -2,4 +2,5 @@ juggl/config/config.txt
juggl/config/config.path
juggl/config/config.php
graphics
.vscode
.vscode
juggl-vue/package-lock.json

View 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();

View 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();

View file

@ -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;

View file

@ -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

View file

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

14
juggl-vue/.eslintrc.js Normal file
View 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
View 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
View 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/).

View file

@ -0,0 +1,3 @@
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"]
};

35
juggl-vue/package.json Normal file
View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View 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
View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View 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>

View 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 />

View 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>

View 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>

View 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>

View 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
View 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");

View 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;

View 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);
},
};

View 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()
);
},
};

View 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;
},
};

View 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;
}, []);
},
};

View 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();
},
},
});

View 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

View 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>

View 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>