diff --git a/.gitignore b/.gitignore index 64c4fad..0041fbb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ juggl/config/config.txt juggl/config/config.path juggl/config/config.php graphics -.vscode \ No newline at end of file +.vscode +juggl-vue/package-lock.json diff --git a/juggl-server/api/getRecords.php b/juggl-server/api/getRecords.php new file mode 100644 index 0000000..2c69466 --- /dev/null +++ b/juggl-server/api/getRecords.php @@ -0,0 +1,29 @@ +get("user_id"); + + $records = getRecords($user_id); + + $json = new JsonBuilder(); + $json->addRecords($records); + + respondJson($json); + } +} + +$branch = new GetRecordsBranch(); +$branch->execute(); diff --git a/juggl-server/api/getUser.php b/juggl-server/api/getUser.php new file mode 100644 index 0000000..888f8bb --- /dev/null +++ b/juggl-server/api/getUser.php @@ -0,0 +1,29 @@ +get("user_id"); + + $user = getUser($user_id); + + $json = new JsonBuilder(); + $json->addUsers([$user]); + + respondJson($json); + } +} + +$branch = new GetUserBranch(); +$branch->execute(); diff --git a/juggl-server/api/services/jsonBuilder.inc.php b/juggl-server/api/services/jsonBuilder.inc.php index 4827fe8..aeaa101 100644 --- a/juggl-server/api/services/jsonBuilder.inc.php +++ b/juggl-server/api/services/jsonBuilder.inc.php @@ -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; diff --git a/juggl-server/api/services/jugglDbApi.inc.php b/juggl-server/api/services/jugglDbApi.inc.php index e5333af..c21765a 100644 --- a/juggl-server/api/services/jugglDbApi.inc.php +++ b/juggl-server/api/services/jugglDbApi.inc.php @@ -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 diff --git a/juggl-vue/.browserslistrc b/juggl-vue/.browserslistrc new file mode 100644 index 0000000..214388f --- /dev/null +++ b/juggl-vue/.browserslistrc @@ -0,0 +1,3 @@ +> 1% +last 2 versions +not dead diff --git a/juggl-vue/.eslintrc.js b/juggl-vue/.eslintrc.js new file mode 100644 index 0000000..5592b18 --- /dev/null +++ b/juggl-vue/.eslintrc.js @@ -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" + } +}; diff --git a/juggl-vue/.gitignore b/juggl-vue/.gitignore new file mode 100644 index 0000000..403adbc --- /dev/null +++ b/juggl-vue/.gitignore @@ -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? diff --git a/juggl-vue/README.md b/juggl-vue/README.md new file mode 100644 index 0000000..822638e --- /dev/null +++ b/juggl-vue/README.md @@ -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/). diff --git a/juggl-vue/babel.config.js b/juggl-vue/babel.config.js new file mode 100644 index 0000000..397abca --- /dev/null +++ b/juggl-vue/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ["@vue/cli-plugin-babel/preset"] +}; diff --git a/juggl-vue/package.json b/juggl-vue/package.json new file mode 100644 index 0000000..bbc1528 --- /dev/null +++ b/juggl-vue/package.json @@ -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" + } +} diff --git a/juggl-vue/public/favicon.ico b/juggl-vue/public/favicon.ico new file mode 100644 index 0000000..a1e7893 Binary files /dev/null and b/juggl-vue/public/favicon.ico differ diff --git a/juggl-vue/public/index.html b/juggl-vue/public/index.html new file mode 100644 index 0000000..0cb0e4a --- /dev/null +++ b/juggl-vue/public/index.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + Juggl + + + + + +
+ + + + \ No newline at end of file diff --git a/juggl-vue/src/App.vue b/juggl-vue/src/App.vue new file mode 100644 index 0000000..5e92239 --- /dev/null +++ b/juggl-vue/src/App.vue @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/juggl-vue/src/assets/logo.png b/juggl-vue/src/assets/logo.png new file mode 100644 index 0000000..eb257ee Binary files /dev/null and b/juggl-vue/src/assets/logo.png differ diff --git a/juggl-vue/src/components/base/BaseContainer.vue b/juggl-vue/src/components/base/BaseContainer.vue new file mode 100644 index 0000000..90ea603 --- /dev/null +++ b/juggl-vue/src/components/base/BaseContainer.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/juggl-vue/src/components/base/BaseLogo.vue b/juggl-vue/src/components/base/BaseLogo.vue new file mode 100644 index 0000000..c6f5f7a --- /dev/null +++ b/juggl-vue/src/components/base/BaseLogo.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/juggl-vue/src/components/forms/FormLogin.vue b/juggl-vue/src/components/forms/FormLogin.vue new file mode 100644 index 0000000..35065e2 --- /dev/null +++ b/juggl-vue/src/components/forms/FormLogin.vue @@ -0,0 +1,80 @@ + + + + + \ No newline at end of file diff --git a/juggl-vue/src/components/layout/LayoutMinimal.vue b/juggl-vue/src/components/layout/LayoutMinimal.vue new file mode 100644 index 0000000..943350d --- /dev/null +++ b/juggl-vue/src/components/layout/LayoutMinimal.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/juggl-vue/src/components/layout/LayoutNavbarPrivate.vue b/juggl-vue/src/components/layout/LayoutNavbarPrivate.vue new file mode 100644 index 0000000..2e0c961 --- /dev/null +++ b/juggl-vue/src/components/layout/LayoutNavbarPrivate.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/juggl-vue/src/main.js b/juggl-vue/src/main.js new file mode 100644 index 0000000..69cdc10 --- /dev/null +++ b/juggl-vue/src/main.js @@ -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"); diff --git a/juggl-vue/src/router/index.js b/juggl-vue/src/router/index.js new file mode 100644 index 0000000..3cb1395 --- /dev/null +++ b/juggl-vue/src/router/index.js @@ -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; diff --git a/juggl-vue/src/services/api.service.js b/juggl-vue/src/services/api.service.js new file mode 100644 index 0000000..b91b380 --- /dev/null +++ b/juggl-vue/src/services/api.service.js @@ -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); + }, +}; diff --git a/juggl-vue/src/services/helper.service.js b/juggl-vue/src/services/helper.service.js new file mode 100644 index 0000000..babd7e8 --- /dev/null +++ b/juggl-vue/src/services/helper.service.js @@ -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() + ); + }, +}; diff --git a/juggl-vue/src/services/juggl.service.js b/juggl-vue/src/services/juggl.service.js new file mode 100644 index 0000000..51bab88 --- /dev/null +++ b/juggl-vue/src/services/juggl.service.js @@ -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; + }, +}; diff --git a/juggl-vue/src/services/path.service.js b/juggl-vue/src/services/path.service.js new file mode 100644 index 0000000..a99edf4 --- /dev/null +++ b/juggl-vue/src/services/path.service.js @@ -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; + }, []); + }, +}; diff --git a/juggl-vue/src/store/index.js b/juggl-vue/src/store/index.js new file mode 100644 index 0000000..73a3d4d --- /dev/null +++ b/juggl-vue/src/store/index.js @@ -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(); + }, + }, +}); diff --git a/juggl-vue/src/style/theme.sass b/juggl-vue/src/style/theme.sass new file mode 100644 index 0000000..063408c --- /dev/null +++ b/juggl-vue/src/style/theme.sass @@ -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 diff --git a/juggl-vue/src/views/Home.vue b/juggl-vue/src/views/Home.vue new file mode 100644 index 0000000..2301d84 --- /dev/null +++ b/juggl-vue/src/views/Home.vue @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/juggl-vue/src/views/Login.vue b/juggl-vue/src/views/Login.vue new file mode 100644 index 0000000..aee528e --- /dev/null +++ b/juggl-vue/src/views/Login.vue @@ -0,0 +1,20 @@ + + + \ No newline at end of file