-
+
+
+ {{ title }}
+
@@ -24,13 +31,26 @@
import BaseContainer from "@/components/base/BaseContainer.vue";
import BaseLogo from "@/components/base/BaseLogo.vue";
import BaseUserDropdown from "@/components/base/BaseUserDropdown.vue";
+import BaseTitle from "@/components/base/BaseTitle";
export default {
name: "LayoutNavbarPrivate",
components: {
BaseContainer,
BaseLogo,
- BaseUserDropdown
+ BaseUserDropdown,
+ BaseTitle
+ },
+ props: {
+ title: {
+ default: "",
+ type: String
+ },
+ center: {
+ default: false,
+ required: false,
+ type: Boolean
+ }
}
};
@@ -45,6 +65,7 @@ header
box-shadow: 0px 0px 2rem 1rem #000D
height: $navbar-height
background-color: $background-primary
+ z-index: 100
#header-container
padding: 0px 20px
@@ -63,5 +84,8 @@ header
main
padding: 0px 20px
margin-top: 6rem
+ margin-bottom: 6rem
+.center-content
+ text-align: center
diff --git a/juggl-vue/src/main.js b/src/main.js
similarity index 86%
rename from juggl-vue/src/main.js
rename to src/main.js
index 69cdc10..513b7c1 100644
--- a/juggl-vue/src/main.js
+++ b/src/main.js
@@ -11,7 +11,7 @@ Vue.use(BootstrapVue);
Vue.use(BootstrapVueIcons);
new Vue({
- router,
- store,
- render: (h) => h(App),
+ router,
+ store,
+ render: h => h(App)
}).$mount("#app");
diff --git a/src/router/index.js b/src/router/index.js
new file mode 100644
index 0000000..8f69b2c
--- /dev/null
+++ b/src/router/index.js
@@ -0,0 +1,62 @@
+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";
+import RecordDetails from "../views/RecordDetails.vue";
+
+Vue.use(VueRouter);
+
+const routes = [
+ {
+ path: "/",
+ redirect: "/home"
+ },
+ {
+ path: "/login",
+ name: "Login",
+ component: Login
+ },
+ {
+ path: "/home",
+ name: "Home",
+ component: Home,
+ beforeEnter: requireAuth
+ },
+ {
+ path: "/record/:id",
+ name: "Record Details",
+ component: RecordDetails,
+ beforeEnter: requireAuth
+ },
+ {
+ path: "/logout",
+ name: "Logout",
+ beforeEnter: (to, from, next) => {
+ store.dispatch("logout");
+ next("/");
+ }
+ }
+];
+
+const router = new VueRouter({
+ mode: "history",
+ base: process.env.BASE_URL,
+ routes
+});
+
+/**
+ * Checks authentication before proceeding
+ */
+function requireAuth(to, from, next) {
+ if (!store.getters.isLoggedIn) {
+ next({
+ path: "/login",
+ query: { redirect: to.fullPath }
+ });
+ } else {
+ next();
+ }
+}
+
+export default router;
diff --git a/src/services/api.service.js b/src/services/api.service.js
new file mode 100644
index 0000000..194c7ea
--- /dev/null
+++ b/src/services/api.service.js
@@ -0,0 +1,49 @@
+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,
+ { ...this.getDefaultJson(), ...json },
+ options
+ );
+ },
+
+ put(resource, json, options) {
+ return this.getApi().put(
+ resource,
+ { ...this.getDefaultJson(), ...json },
+ options
+ );
+ },
+
+ /**
+ * Creates an instance of the used api and sets necessary headers
+ */
+ getApi() {
+ var options = {
+ baseURL: store.getters.apiUrl
+ };
+
+ return axios.create(options);
+ },
+ getDefaultJson() {
+ return {
+ user_id: store.getters.auth.userId,
+ api_key: store.getters.auth.apiKey
+ };
+ }
+};
diff --git a/src/services/helper.service.js b/src/services/helper.service.js
new file mode 100644
index 0000000..67a2572
--- /dev/null
+++ b/src/services/helper.service.js
@@ -0,0 +1,140 @@
+/**
+ * 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()
+ );
+ },
+
+ getDurationTimestamp(totalSeconds) {
+ totalSeconds = Math.ceil(totalSeconds);
+ var days = Math.floor(totalSeconds / 86400);
+ var hours = Math.floor((totalSeconds - days * 86400) / 3600);
+ var minutes = Math.floor((totalSeconds - days * 86400 - hours * 3600) / 60);
+ var seconds = totalSeconds - days * 86400 - 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;
+ }
+
+ var timestamp = minutes + ":" + seconds;
+ if (totalSeconds >= 3600) {
+ timestamp = hours + ":" + timestamp;
+ }
+ if (totalSeconds >= 86400) {
+ timestamp = days + ":" + timestamp;
+ }
+ return timestamp;
+ }
+};
diff --git a/src/services/juggl.service.js b/src/services/juggl.service.js
new file mode 100644
index 0000000..7a74d04
--- /dev/null
+++ b/src/services/juggl.service.js
@@ -0,0 +1,140 @@
+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").then(r => {
+ return {
+ data: r.data,
+ msg: ""
+ };
+ });
+ },
+
+ getProjects() {
+ 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: ""
+ };
+ });
+ },
+
+ removeRecord(recordId) {
+ return apiService
+ .post("/removeRecord.php", {
+ record_id: recordId
+ })
+ .then(r => {
+ return {
+ data: r.data,
+ msg: ""
+ };
+ });
+ },
+
+ getRecords({ limit = undefined, finished = undefined}) {
+ var payload = {};
+
+ if (limit !== undefined && limit > 0) {
+ payload.limit = limit;
+ }
+ if (finished !== undefined) {
+ payload.finished = finished;
+ }
+
+ return apiService.post("/getRecords.php", payload).then(r => {
+ return {
+ data: processRecords(r.data),
+ msg: ""
+ };
+ });
+ },
+
+ getRunningRecords() {
+ return apiService.post("/getRunningRecords.php").then(r => {
+ return {
+ data: processRecords(r.data),
+ msg: ""
+ };
+ });
+ },
+
+ addProject(name, startDate = null) {
+ if (startDate == null) startDate = new Date().toISOString();
+ return apiService
+ .post("/addProject.php", {
+ name: name,
+ start_date: startDate
+ })
+ .then(r => {
+ return {
+ data: 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)
+ })
+ .then(r => {
+ return {
+ data: r.data,
+ msg: ""
+ };
+ });
+ },
+
+ endRecord(recordId, endTime = null) {
+ if (endTime == null) endTime = new Date();
+ return apiService
+ .post("/endRecord.php", {
+ record_id: recordId,
+ end_time: helperService.dateToString(endTime)
+ })
+ .then(r => {
+ return {
+ data: r.data,
+ msg: ""
+ };
+ });
+ }
+};
+
+function processRecords(data) {
+ Object.values(data.records).forEach(rec => {
+ rec.running = rec.end_time === null;
+
+ if (rec.running) {
+ rec.duration =
+ (new Date().getTime() - new Date(rec.start_time).getTime()) / 1000;
+ }
+ });
+ return data;
+}
diff --git a/src/services/path.service.js b/src/services/path.service.js
new file mode 100644
index 0000000..e71fdaa
--- /dev/null
+++ b/src/services/path.service.js
@@ -0,0 +1,141 @@
+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;
+ }, []);
+ }
+};
diff --git a/src/store/index.js b/src/store/index.js
new file mode 100644
index 0000000..4914d40
--- /dev/null
+++ b/src/store/index.js
@@ -0,0 +1,17 @@
+import Vue from "vue";
+import Vuex from "vuex";
+import { juggl } from "./modules/juggl";
+import createPersistedState from "vuex-persistedstate";
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ modules: {
+ juggl
+ },
+ plugins: [
+ createPersistedState({
+ storage: window.sessionStorage
+ })
+ ]
+});
diff --git a/juggl-vue/src/store/index.js b/src/store/modules/juggl.js
similarity index 59%
rename from juggl-vue/src/store/index.js
rename to src/store/modules/juggl.js
index 9fc3b14..1994fdb 100644
--- a/juggl-vue/src/store/index.js
+++ b/src/store/modules/juggl.js
@@ -1,16 +1,16 @@
-import Vue from "vue";
-import Vuex from "vuex";
import { jugglService } from "@/services/juggl.service.js";
-Vue.use(Vuex);
-
-export default new Vuex.Store({
+export const juggl = {
state: {
apiUrl: "https://juggl.giller.dev/api",
projects: {},
records: {},
user: undefined,
auth: undefined,
+ usingFinishedRecords: false,
+ usingRunningRecords: false,
+ usingProjects: false,
+ recordsLimit: 0,
},
mutations: {
setKey(state, key) {
@@ -22,12 +22,23 @@ export default new Vuex.Store({
setRecords(state, records) {
state.records = records;
},
+ usingFinishedRecords(state, using) {
+ state.usingFinishedRecords = using;
+ },
+ usingRunningRecords(state, using) {
+ state.usingRunningRecords = using;
+ },
+ usingProjects(state, using) {
+ state.usingProjects = using;
+ },
+ setRecordsLimit(state, limit) {
+ state.recordsLimit = limit;
+ },
setUser(state, user) {
state.user = user;
},
logout(state) {
state.auth = undefined;
- // TODO: Doesn't work apparently
localStorage.removeItem("apiKey");
localStorage.removeItem("userId");
},
@@ -39,7 +50,9 @@ export default new Vuex.Store({
},
getters: {
runningRecords: (state) => {
- return Object.values(state.records).filter((record) => record.running);
+ return Object.values(state.records).filter(
+ (record) => record.running
+ );
},
finishedRecords: (state) => {
return Object.values(state.records).filter(
@@ -93,27 +106,45 @@ export default new Vuex.Store({
);
},
getRecordById: (state, getters) => (id) => {
- return Object.values(getters.records).find((record) => record.record_id === id);
+ return Object.values(getters.records).find(
+ (record) => record.record_id === id
+ );
},
},
actions: {
loadProjects({ commit }) {
- jugglService.getProjects().then((r) => {
+ return jugglService.getProjects().then((r) => {
commit("setProjects", r.data.projects);
+ commit("usingProjects", true);
});
},
- loadAllRecords({ commit }) {
- jugglService.getRecords().then((r) => {
+ loadUser({ commit }) {
+ return jugglService
+ .getUser()
+ .catch(() => {
+ return false;
+ })
+ .then((r) => {
+ commit("setUser", r.data.users[0]);
+ return true;
+ });
+ },
+ loadRecords({ commit, state }, { limit, finished }) {
+ commit("setRecordsLimit", limit);
+ return jugglService.getRecords({ limit: state.recordsLimit, finished: finished }).then((r) => {
commit("setRecords", r.data.records);
+ commit("usingFinishedRecords", true);
+ commit("usingRunningRecords", true);
});
},
loadRunningRecords({ commit, getters }) {
- jugglService.getRunningRecords().then((r) => {
+ return jugglService.getRunningRecords().then((r) => {
var allRecords = {
...getters.finishedRecords,
...r.data.records,
};
commit("setRecords", allRecords);
+ commit("usingRunningRecords", true);
});
},
login({ commit, getters }, { userId, apiKey }) {
@@ -124,15 +155,18 @@ export default new Vuex.Store({
commit("login", { apiKey: apiKey, userId: userId });
- return jugglService
- .getUser()
+ return this.dispatch("loadUser")
.catch(() => {
this.dispatch("logout");
return false;
})
.then((r) => {
- commit("setUser", r.data.users[0]);
- return true;
+ if (r === false) {
+ this.dispatch("logout");
+ return false;
+ } else {
+ return true;
+ }
});
},
logout({ commit }) {
@@ -146,8 +180,18 @@ export default new Vuex.Store({
return false;
})
.then(() => {
- this.dispatch("loadRunningRecords");
- this.dispatch("loadProjects");
+ this.dispatch("updateState");
+ return true;
+ });
+ },
+ addProject(context, { name }) {
+ return jugglService
+ .addProject(name)
+ .catch(() => {
+ return false;
+ })
+ .then(() => {
+ this.dispatch("updateState");
return true;
});
},
@@ -158,9 +202,43 @@ export default new Vuex.Store({
return false;
})
.then(() => {
- this.dispatch("loadRunningRecords");
+ this.dispatch("updateState");
return true;
});
- }
+ },
+ removeRecord(context, recordId) {
+ return jugglService
+ .removeRecord(recordId)
+ .catch(() => {
+ return false;
+ })
+ .then(() => {
+ this.dispatch("updateState");
+ return true;
+ });
+ },
+ updateState({ state }) {
+ if (state.usingProjects) {
+ this.dispatch("loadProjects");
+ }
+ if (state.usingRunningRecords && state.usingFinishedRecords) {
+ this.dispatch("loadRecords");
+ } else if (state.usingRunningRecords) {
+ this.dispatch("loadRunningRecords");
+ }
+ if (state.user === undefined) {
+ this.dispatch("loadUser");
+ }
+ },
+ loadSavedLogin({ commit }) {
+ var userId = localStorage.getItem("userId");
+ var apiKey = localStorage.getItem("apiKey");
+
+ if (userId === undefined || apiKey === undefined) {
+ return;
+ }
+
+ commit("login", { apiKey: apiKey, userId: userId });
+ },
},
-});
+};
diff --git a/juggl-vue/src/style/theme.sass b/src/style/theme.sass
similarity index 52%
rename from juggl-vue/src/style/theme.sass
rename to src/style/theme.sass
index e140814..b9cc666 100644
--- a/juggl-vue/src/style/theme.sass
+++ b/src/style/theme.sass
@@ -32,7 +32,7 @@ $navbar-height: 4rem
body
background: $background-primary !important
- margin: 0;
+ 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
@@ -45,8 +45,8 @@ body
.form-group
label
color: $font-secondary
- margin-bottom: -0.3em
- margin-left: 0.3em
+ margin-bottom: -0.1em
+ margin-left: 0.1em
input
background-color: $background-primary !important
color: $font-primary !important
@@ -58,54 +58,72 @@ a
border-radius: 0
background: #0000
-ul.dropdown-menu
- background-color: $background-primary
- border: 1px solid $primary
+.dropdown-menu
+ background-color: $background-primary !important
+ border: 1px solid $primary !important
color: $primary
-
+
+ .b-time *,
+ background-color: $background-primary !important
+
+#dropdown .dropdown-menu
*:hover
color: $white !important
- background: darken($primary, 20)
-
+ background: darken($primary, 20) !important
+
*:active
color: $white !important
- background: $primary
+ background: $primary !important
-// .custom-checkbox
-// label
-// color: $font-primary !important
+.custom-checkbox
+ label
+ color: $font-primary !important
-// a.btn-secondary, a.btn-primary
-// color: $font-primary !important
+a.btn-secondary, a.btn-primary
+ color: $font-primary !important
-// #title
-// font-weight: bold
+#title
+ font-weight: bold
-// .breadcrumb
-// background-color: #FFF0 !important
+.breadcrumb
+ background-color: #FFF0 !important
-// .breadcrumb-item + .breadcrumb-item::before
-// content: ">" !important
+.breadcrumb-item + .breadcrumb-item::before
+ content: ">" !important
-// .breadcrumb-item.active
-// span
-// color: $font-secondary !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
+header.b-calendar-grid-caption, .b-calendar-grid-weekdays *, header.b-calendar-header *
+ color: $font-inverted !important
+
+.b-form-datepicker, .b-form-timepicker
+ background-color: $background-primary !important
+
+.b-form-datepicker label:not(.text-muted), .b-form-timepicker label:not(.text-muted)
+ color: $font-primary !important
+
+input:disabled
+ color: $font-secondary !important
+ border-color: $grey !important
+
+input
+ color: $font-primary !important
+ border-color: $grey !important
+ background-color: $background-primary !important
+
+select
+ background-color: $background-primary !important
+ color: $font-primary !important
+
+
+// Import Bootstrap and BootstrapVue source SCSS files
+@import '~bootstrap/scss/bootstrap.scss'
+@import '~bootstrap-vue/src/index.scss'
\ No newline at end of file
diff --git a/juggl-vue/src/views/Home.vue b/src/views/Home.vue
similarity index 50%
rename from juggl-vue/src/views/Home.vue
rename to src/views/Home.vue
index 1f3d5b4..f68c219 100644
--- a/juggl-vue/src/views/Home.vue
+++ b/src/views/Home.vue
@@ -1,13 +1,22 @@
-
+
-
+
+
+
+
+
+
+
+
+
+
+
@@ -15,6 +24,7 @@
import LayoutNavbarPrivate from "@/components/layout/LayoutNavbarPrivate";
import JugglProjectsPanel from "@/components/juggl/JugglProjectsPanel";
import JugglRecordsList from "@/components/juggl/JugglRecordsList";
+import FormProjectAdd from "@/components/forms/FormProjectAdd";
import store from "@/store";
export default {
@@ -22,12 +32,16 @@ export default {
components: {
LayoutNavbarPrivate,
JugglProjectsPanel,
- JugglRecordsList
+ JugglRecordsList,
+ FormProjectAdd
},
computed: {
finishedProjects: () => {
return store.getters.finishedProjects;
},
+ finishedRecords: () => {
+ return store.getters.finishedRecords;
+ },
runningRecords: () => {
return store.getters.runningRecords;
}
@@ -35,12 +49,20 @@ export default {
created: () => {
store.dispatch("loadProjects");
store.dispatch("loadRunningRecords");
+ store.dispatch("loadRecords", { limit: 25, finished: true });
}
-}
+};
\ No newline at end of file
+
+section
+ margin-bottom: 4rem
+
+#add-project-form
+ margin-top: 1rem
+ text-align: center
+
diff --git a/juggl-vue/src/views/Login.vue b/src/views/Login.vue
similarity index 92%
rename from juggl-vue/src/views/Login.vue
rename to src/views/Login.vue
index 0ac492f..73e5969 100644
--- a/juggl-vue/src/views/Login.vue
+++ b/src/views/Login.vue
@@ -1,6 +1,6 @@
-
+
@@ -16,4 +16,4 @@ export default {
FormLogin
}
};
-
\ No newline at end of file
+
diff --git a/src/views/RecordDetails.vue b/src/views/RecordDetails.vue
new file mode 100644
index 0000000..4fe1203
--- /dev/null
+++ b/src/views/RecordDetails.vue
@@ -0,0 +1,43 @@
+
+
+
+ {{error}}
+
+
+
+
+
+
+