Basic site working

This commit is contained in:
Maximilian Giller 2021-01-01 16:35:35 +01:00
parent 1be0761358
commit 045eb7ecc9
16 changed files with 494 additions and 256 deletions

View file

@ -12,7 +12,7 @@
"bootstrap": "^4.5.3",
"bootstrap-vue": "^2.21.1",
"core-js": "^3.6.5",
"vue": "^2.6.11",
"vue": "^2.6.12",
"vue-router": "^3.2.0",
"vuex": "^3.4.0"
},

View file

@ -4,7 +4,7 @@
<b-img
:src="require('../../assets/logo.png')"
alt="Juggl"
:height="heightSize"
:width="widthSize"
:center="center"
/>
</b-link>
@ -25,13 +25,13 @@ export default {
}
},
computed: {
heightSize: function() {
widthSize: function() {
let sizes = {
mini: "35px",
normal: "64px",
tiny: "80px",
smaller: "110px",
small: "150px",
normal: "175px",
medium: "300px",
large: "450px",
big: "600px",

View file

@ -0,0 +1,22 @@
<template>
<b-dropdown id="dropdown" :text=this.username variant="outline-danger" right>
<b-dropdown-item to="/logout">Log out</b-dropdown-item>
</b-dropdown>
</template>
<script>
import store from "@/store";
export default {
name: "BaseUserDropdown",
computed: {
username: () => store.getters.user.name,
},
};
</script>
<style lang="sass" scoped>
#dropdown
*
line-height: 1.5
</style>

View file

@ -1,10 +1,9 @@
<template>
<b-form @submit="submitForm">
<b-form-group id="email-group" label-for="email">
<b-form-group id="username-group" label-for="username">
<b-form-input
id="email"
id="username"
v-model="form.user_id"
type="number"
required
placeholder="User ID"
trim
@ -24,7 +23,8 @@
<b-form-invalid-feedback :state="!failed">
Password or email invalid.
</b-form-invalid-feedback>
<b-button variant="primary" type="submit" block>
<b-button variant="primary" type="submit" block :disabled="working">
<b-spinner v-if="working" small/>
Log in
</b-button>
</b-form>
@ -41,7 +41,8 @@ export default {
user_id: null,
api_key: null
},
failed: false
failed: false,
working: false
};
},
methods: {
@ -50,14 +51,16 @@ export default {
*/
submitForm: function(e) {
e.preventDefault();
this.failed = false;
this.working = true;
// Try to login
store.dispatch("login")
.login(this.form.user_id, this.form.api_key)
store.dispatch("login", { userId: this.form.user_id, apiKey: this.form.api_key})
.then(r => {
if (r !== true) {
this.failed = true;
return;
this.working = false;
return false;
}
// On success redirect to target or dashboard
@ -68,6 +71,7 @@ export default {
.catch(e => {
console.log(e);
this.failed = true;
this.working = false;
});
return false;

View file

@ -0,0 +1,74 @@
<template>
<div id="project-list">
<div v-for="project in projects" :key="project.project_id" @click="() => startProject(project.project_id)">
<h1>{{project.name}}</h1>
<p>{{getDurationTimestamp(project)}}</p>
<p>{{project.record_count}} records</p>
</div>
</div>
</template>
<script>
import store from "@/store"
export default {
name: "JugglProjectPanel",
props: {
projects: {
required: true
}
},
methods: {
getDurationTimestamp: (project) => {
var totalSeconds = project.duration;
// var days = Math.floor(totalSeconds / 86400);
var hours = Math.floor(totalSeconds / 3600); //Math.floor((totalSeconds - (days * 86400)) / 3600);
var minutes = Math.floor((totalSeconds - (hours * 3600)) / 60);
var seconds = totalSeconds - (hours * 3600) - (minutes * 60);
// if (days < 10) {days = "0"+days;}
if (hours < 10) {hours = "0"+hours;}
if (minutes < 10) {minutes = "0"+minutes;}
if (seconds < 10) {seconds = "0"+seconds;}
return hours+':'+minutes+':'+seconds;
},
startProject: function (id) {
store.dispatch("startRecord", id);
}
}
}
</script>
<style lang="sass" scoped>
@import '@/style/theme.sass'
#project-list
display: flex
flex-direction: row
flex-wrap: wrap
justify-content: center
align-content: flex-start
padding: 5px
> *
margin: 5px
border: 1px dashed $grey
border-radius: 5px
padding: 10px
&:hover
border-color: $primary !important
cursor: pointer
h1
font-weight: bold
font-size: 24pt
margin: 0px
padding: 0px
p
font-size: 10pt
color: $grey
margin: 0px
padding: 0px
</style>

View file

@ -0,0 +1,97 @@
<template>
<b-table :items="records" hover :busy="isLoading" :fields="fields">
<template #table-busy>
<div class="text-center">
<b-spinner></b-spinner>
</div>
</template>
<!-- Custom data -->
<template #cell(project)="data">
{{ getProject(data.item.project_id).name }}
</template>
<template #cell(start)="data">
{{ data.item.start_time }}
</template>
<template #cell(duration)="data">
{{ data.item.duration }}
</template>
<template #cell(stop)="data">
<b-button size="sm" @click="stopRecord(data.item.record_id)" variant="outline-success">
<b-icon class="icon-btn" icon="check" />
</b-button>
</template>
<template #cell(abort)="data">
<b-button size="sm" @click="() => abortRecord(data.item.record_id)" variant="outline-danger">
<b-icon class="icon-btn" icon="x" />
</b-button>
</template>
</b-table>
</template>
<script>
import store from "@/store";
export default {
name: "JugglRecordsList",
props: {
records: {
required: true,
},
},
data() {
return {
iconScale: 1.6,
fields: [
{
key: "project",
label: "Project",
},
{
key: "start",
label: "Start",
},
{
key: "duration",
label: "Duration",
},
{
key: "stop",
label: "Stop",
},
{
key: "abort",
label: "Abort",
},
],
};
},
computed: {
isLoading: function () {
return this.records === undefined;
},
},
methods: {
getProject: function (id) {
return store.getters.getProjectById(id);
},
stopRecord: function (id) {
store.dispatch("endRecord", id);
},
abortRecord: function (id) {
console.log(id);
},
},
};
</script>
<style lang="sass">
.icon-btn:hover
color: green
cursor: pointer
</styl>

View file

@ -1,6 +1,6 @@
<template>
<div>
<BaseLogo id="logo" size="smaller" center class="space-top" />
<BaseLogo id="logo" size="medium" center class="space-top" />
<BaseContainer :width="width" center class="space-bottom">
<BaseTitle v-if="title" center size="huge" class="centered">
{{ title }}

View file

@ -4,23 +4,11 @@
<BaseContainer width="wide" center>
<ul id="header-container">
<li>
<BaseLogo size="normal"/>
<BaseLogo id="logo" size="normal"/>
</li>
<li class="right">
<BaseUserDropdown/>
</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>
@ -35,12 +23,14 @@
<script>
import BaseContainer from "@/components/base/BaseContainer.vue";
import BaseLogo from "@/components/base/BaseLogo.vue";
import BaseUserDropdown from "@/components/base/BaseUserDropdown.vue";
export default {
name: "LayoutNavbarPrivate",
components: {
BaseContainer,
BaseLogo
BaseLogo,
BaseUserDropdown
}
};
</script>
@ -62,12 +52,11 @@ header
list-style: none
line-height: navbar-height
img
margin-top: 0.5rem
height: 3rem
width: auto
.right
float: right
*
line-height: $navbar-height
vertical-align: baseline
display: inline-block

View file

@ -1,21 +1,34 @@
import Vue from "vue";
import VueRouter from "vue-router";
import store from "../store";
import Login from "../views/Login.vue";
import Home from "../views/Home.vue";
Vue.use(VueRouter);
const routes = [
{
path: "/",
redirect: "/home"
},
{
path: "/login",
name: "Login",
component: Login,
},
{
path: "/",
path: "/home",
name: "Home",
component: Home,
// beforeEnter: requireAuth,
beforeEnter: requireAuth,
},
{
path: "/logout",
name: "Logout",
beforeEnter: (to, from, next) => {
store.dispatch("logout");
next("/");
},
},
];
@ -28,16 +41,15 @@ const router = new VueRouter({
/**
* 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();
// }
// }
function requireAuth(to, from, next) {
if (!store.getters.isLoggedIn) {
next({
path: "/login",
query: { redirect: to.fullPath },
});
} else {
next();
}
}
export default router;

View file

@ -1,5 +1,5 @@
import axios from "axios";
import { store } from "@/store";
import store from "@/store";
/**
* A wrapper for the used fetch API, currently axios.
@ -15,11 +15,19 @@ export const apiService = {
},
post(resource, json, options) {
return this.getApi().post(resource, json, options);
return this.getApi().post(
resource,
{ ...this.getDefaultJson(), ...json },
options
);
},
put(resource, json, options) {
return this.getApi().put(resource, json, options);
return this.getApi().put(
resource,
{ ...this.getDefaultJson(), ...json },
options
);
},
/**
@ -32,4 +40,10 @@ export const apiService = {
return axios.create(options);
},
getDefaultJson() {
return {
user_id: store.getters.auth.userId,
api_key: store.getters.auth.apiKey,
};
},
};

View file

@ -1,5 +1,3 @@
// TODO: Do I need this file?
/**
* Contains a bunch of helper functions.
*/

View file

@ -11,212 +11,88 @@ export const jugglService = {
* @returns A promise
*/
getUser() {
return apiService.post("/getUser.php");
return apiService.post("/getUser.php").then((r) => {
return {
data: r.data,
msg: "",
};
});
},
getProjects() {
return apiService.post("/getProjects.php");
return apiService.post("/getProjects.php").then((r) => {
return {
data: r.data,
msg: "",
};
});
},
getRecord(recordId) {
return apiService
.post("/getRecord.php", {
record_id: recordId,
})
.then((r) => {
return {
data: processRecords(r.data),
msg: "",
};
});
},
getRecords() {
return apiService.post("/getRecords.php");
return apiService.post("/getRecords.php").then((r) => {
return {
data: processRecords(r.data),
msg: "",
};
});
},
getRunningRecords() {
return apiService.post("/getRunningRecords.php");
return apiService.post("/getRunningRecords.php").then((r) => {
return {
data: processRecords(r.data),
msg: "",
};
});
},
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 };
.post("/startRecord.php", {
project_id: projectId,
start_time: helperService.dateToString(startTime),
})
.catch((r) => {
return { success: false, msg: r.response.data.msg };
.then((r) => {
return {
data: r.data,
msg: "",
};
});
},
/**
* Verifies a user with a token.
*
* @param {*} token Server generated token
*
* @returns A promise
*/
verify(token) {
var path = "/user/verify";
endRecord(recordId, endTime = null) {
if (endTime == null) endTime = new Date();
return apiService
.post(path, { token: token })
.then((r) => {
return { success: true, msg: r.data.msg };
.post("/endRecord.php", {
record_id: recordId,
end_time: helperService.dateToString(endTime),
})
.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;
return {
data: r.data,
msg: "",
};
});
},
/**
* 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;
},
};
function processRecords(data) {
Object.values(data.records).forEach(rec => {
rec.running = rec.end_time === null;
});
return data;
}

View file

@ -7,10 +7,10 @@ Vue.use(Vuex);
export default new Vuex.Store({
state: {
apiUrl: "https://juggl.giller.dev/api",
apiKey: undefined,
projects: {},
records: {},
user: undefined,
projects: [],
records: [],
auth: undefined,
},
mutations: {
setKey(state, key) {
@ -22,35 +22,145 @@ export default new Vuex.Store({
setRecords(state, records) {
state.records = records;
},
setUser(state, user) {
state.user = user;
},
logout(state) {
state.auth = undefined;
// TODO: Doesn't work apparently
localStorage.removeItem("apiKey");
localStorage.removeItem("userId");
},
login(state, { apiKey, userId }) {
state.auth = { apiKey: apiKey, userId: userId };
localStorage.setItem("apiKey", apiKey);
localStorage.setItem("userId", userId);
},
},
getters: {
runningRecords: (state) => {
return state.records.filter((record) => record.running);
return Object.values(state.records).filter((record) => record.running);
},
finishedRecords: (state) => {
return state.records.filter((record) => !record.running);
return Object.values(state.records).filter(
(record) => !record.running
);
},
apiKey: (state) => state.apiKey,
user: (state) => state.user,
auth: (state) => state.auth,
apiUrl: (state) => state.apiUrl,
isLoggedIn: (state) => !!state.apiKey,
user: (state) => state.user,
isLoggedIn: (state) => !!state.auth,
records: (state) => state.records,
projects: (state) => state.projects,
projectIds: (state) => {
var projectIds = [];
Object.values(state.projects).forEach((project) => {
projectIds.push(project.project_id);
});
return projectIds;
},
runningProjectIds: (state, getters) => {
var runningProjectIds = [];
Object.values(getters.runningRecords).forEach((record) => {
var projectId = record.project_id;
if (runningProjectIds.includes(runningProjectIds) === false) {
runningProjectIds.push(projectId);
}
});
return runningProjectIds;
},
finishedProjectIds: (state, getters) => {
var runningProjectIds = getters.runningProjectIds;
return getters.projectIds.filter(
(id) => !runningProjectIds.includes(id)
);
},
finishedProjects: (state, getters) => {
var ids = getters.finishedProjectIds;
return Object.values(state.projects).filter((project) =>
ids.includes(project.project_id)
);
},
runningProjects: (state, getters) => {
var ids = getters.runningProjectIds;
return Object.values(state.projects).filter((project) =>
ids.includes(project.project_id)
);
},
getProjectById: (state, getters) => (id) => {
return getters.projects.find(
return Object.values(getters.projects).find(
(project) => project.project_id === id
);
},
getRecordById: (state, getters) => (id) => {
return getters.records.find((record) => record.record_id === id);
return Object.values(getters.records).find((record) => record.record_id === id);
},
},
actions: {
loadProjects({ commit }) {
commit("setProjects");
jugglService.getProjects().then((r) => {
commit("setProjects", r.data.projects);
});
},
login({ commit, userId, apiKey }) {
return jugglService.login();
loadAllRecords({ commit }) {
jugglService.getRecords().then((r) => {
commit("setRecords", r.data.records);
});
},
loadRunningRecords({ commit, getters }) {
jugglService.getRunningRecords().then((r) => {
var allRecords = {
...getters.finishedRecords,
...r.data.records,
};
commit("setRecords", allRecords);
});
},
login({ commit, getters }, { userId, apiKey }) {
// Is already logged in?
if (getters.isLoggedIn) {
this.dispatch("logout");
}
commit("login", { apiKey: apiKey, userId: userId });
return jugglService
.getUser()
.catch(() => {
this.dispatch("logout");
return false;
})
.then((r) => {
commit("setUser", r.data.users[0]);
return true;
});
},
logout({ commit }) {
commit("setUser", undefined);
commit("logout");
},
endRecord(context, recordId) {
return jugglService
.endRecord(recordId)
.catch(() => {
return false;
})
.then(() => {
this.dispatch("loadRunningRecords");
this.dispatch("loadProjects");
return true;
});
},
startRecord(context, projectId) {
return jugglService
.startRecord(projectId)
.catch(() => {
return false;
})
.then(() => {
this.dispatch("loadRunningRecords");
return true;
});
}
},
});

View file

@ -6,6 +6,7 @@ $black: #000
$white: #fff
$primary: #F00
$secondary: $grey
$red: $primary
$background-primary: #222
$background-secondary: #000
@ -53,6 +54,23 @@ body
a
color: $font-link !important
.b-dropdown, .dropdown
border-radius: 0
background: #0000
ul.dropdown-menu
background-color: $background-primary
border: 1px solid $primary
color: $primary
*:hover
color: $white !important
background: darken($primary, 20)
*:active
color: $white !important
background: $primary
// .custom-checkbox
// label
// color: $font-primary !important

View file

@ -1,21 +1,46 @@
<template>
<LayoutNavbarPrivate>
Hey there
<b-link to="/login">Go to login</b-link>
<div v-if="runningRecords.length > 0">
<h2 class="center">Tracking</h2>
<JugglRecordsList :records="runningRecords"/>
</div>
<div v-if="finishedProjects.length > 0">
<h2 class="center">Projects</h2>
<JugglProjectsPanel :projects="finishedProjects"/>
</div>
</LayoutNavbarPrivate>
</template>
<script>
import LayoutNavbarPrivate from "@/components/layout/LayoutNavbarPrivate";
import JugglProjectsPanel from "@/components/juggl/JugglProjectsPanel";
import JugglRecordsList from "@/components/juggl/JugglRecordsList";
import store from "@/store";
export default {
name: "Home",
components: {
LayoutNavbarPrivate
LayoutNavbarPrivate,
JugglProjectsPanel,
JugglRecordsList
},
computed: {
finishedProjects: () => {
return store.getters.finishedProjects;
},
runningRecords: () => {
return store.getters.runningRecords;
}
},
created: () => {
store.dispatch("loadProjects");
store.dispatch("loadRunningRecords");
}
}
</script>
<style>
<style lang="sass">
.center
text-align: center
font-weight: bold
</style>

View file

@ -1,7 +1,6 @@
<template>
<LayoutMinimal title="Login" width="slimmer">
<FormLogin/>
<b-link to="/">Go to home</b-link>
</LayoutMinimal>
</template>