Upload project.

This commit is contained in:
StevenJW
2020-06-09 21:28:47 +01:00
parent 36a582d89d
commit 00bf32859f
70 changed files with 38746 additions and 0 deletions

41
Aya-Frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,41 @@
lib-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz
*.swp
pids
logs
results
tmp
# Build
public/css/main.css
# Coverage reports
coverage
# API keys and secrets
.env
# Dependency directory
node_modules
bower_components
# Editors
.idea
*.iml
# OS metadata
.DS_Store
Thumbs.db
# Ignore built ts files
dist/**/*
# ignore yarn.lock
yarn.lock

2
Aya-Frontend/README.md Normal file
View File

@@ -0,0 +1,2 @@
# Aya-Frontend
The main frontend to the Aya language learning webapp, using the VueJS framework.

View File

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

15245
Aya-Frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

88
Aya-Frontend/package.json Normal file
View File

@@ -0,0 +1,88 @@
{
"name": "aya-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.28",
"@fortawesome/free-solid-svg-icons": "^5.13.0",
"@fortawesome/vue-fontawesome": "^0.1.9",
"@types/showdown": "^1.9.3",
"@types/simplemde": "^1.11.7",
"axios": "^0.19.2",
"bootstrap": "^4.4.1",
"bootstrap-vue": "^2.12.0",
"core-js": "^3.6.4",
"easymde": "^2.10.1",
"showdown": "^1.9.1",
"simplemde": "^1.11.2",
"vue": "^2.6.11",
"vue-class-component": "^7.2.3",
"vue-js-modal": "^2.0.0-rc.3",
"vue-property-decorator": "^8.4.1",
"vue-router": "^3.1.6",
"vue-simplemde": "^1.0.4",
"vuex": "^3.3.0"
},
"devDependencies": {
"@types/jest": "^24.0.19",
"@typescript-eslint/eslint-plugin": "^2.26.0",
"@typescript-eslint/parser": "^2.26.0",
"@vue/cli-plugin-babel": "~4.3.0",
"@vue/cli-plugin-eslint": "~4.3.0",
"@vue/cli-plugin-router": "~4.3.0",
"@vue/cli-plugin-typescript": "~4.3.0",
"@vue/cli-plugin-unit-jest": "~4.3.0",
"@vue/cli-service": "~4.3.0",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^5.0.2",
"@vue/test-utils": "1.0.0-beta.31",
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-vue": "^6.2.2",
"prettier": "^1.19.1",
"typescript": "~3.8.3",
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended",
"@vue/typescript/recommended",
"@vue/prettier",
"@vue/prettier/@typescript-eslint"
],
"parserOptions": {
"ecmaVersion": 2020
},
"rules": {},
"overrides": [
{
"files": [
"**/__tests__/*.{j,t}s?(x)",
"**/tests/unit/**/*.spec.{j,t}s?(x)"
],
"env": {
"jest": true
}
}
]
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
],
"jest": {
"preset": "@vue/cli-plugin-unit-jest/presets/typescript-and-babel"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View File

@@ -0,0 +1,17 @@
<!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">
<title><%= htmlWebpackPlugin.options.title %></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>

34
Aya-Frontend/src/App.vue Normal file
View File

@@ -0,0 +1,34 @@
<template>
<div id="app">
<div id="nav">
<HeaderPanel />
</div>
<router-view />
</div>
</template>
<script>
// @ is an alias to /src
import HeaderPanel from "@/components/HeaderPanel.vue";
export default {
name: "Home",
components: {
HeaderPanel
}
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: left;
color: #2c3e50;
}
html, body
{
height: 100%; overflow: hidden;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 KiB

View File

@@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="book-open" class="svg-inline--fa fa-book-open fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M542.22 32.05c-54.8 3.11-163.72 14.43-230.96 55.59-4.64 2.84-7.27 7.89-7.27 13.17v363.87c0 11.55 12.63 18.85 23.28 13.49 69.18-34.82 169.23-44.32 218.7-46.92 16.89-.89 30.02-14.43 30.02-30.66V62.75c.01-17.71-15.35-31.74-33.77-30.7zM264.73 87.64C197.5 46.48 88.58 35.17 33.78 32.05 15.36 31.01 0 45.04 0 62.75V400.6c0 16.24 13.13 29.78 30.02 30.66 49.49 2.6 149.59 12.11 218.77 46.95 10.62 5.35 23.21-1.94 23.21-13.46V100.63c0-5.29-2.62-10.14-7.27-12.99z"></path></svg>

After

Width:  |  Height:  |  Size: 691 B

View File

@@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="book" class="svg-inline--fa fa-book fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M448 360V24c0-13.3-10.7-24-24-24H96C43 0 0 43 0 96v320c0 53 43 96 96 96h328c13.3 0 24-10.7 24-24v-16c0-7.5-3.5-14.3-8.9-18.7-4.2-15.4-4.2-59.3 0-74.7 5.4-4.3 8.9-11.1 8.9-18.6zM128 134c0-3.3 2.7-6 6-6h212c3.3 0 6 2.7 6 6v20c0 3.3-2.7 6-6 6H134c-3.3 0-6-2.7-6-6v-20zm0 64c0-3.3 2.7-6 6-6h212c3.3 0 6 2.7 6 6v20c0 3.3-2.7 6-6 6H134c-3.3 0-6-2.7-6-6v-20zm253.4 250H96c-17.7 0-32-14.3-32-32 0-17.6 14.4-32 32-32h285.4c-1.9 17.1-1.9 46.9 0 64z"></path></svg>

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="plus-square" class="svg-inline--fa fa-plus-square fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-32 252c0 6.6-5.4 12-12 12h-92v92c0 6.6-5.4 12-12 12h-56c-6.6 0-12-5.4-12-12v-92H92c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h92v-92c0-6.6 5.4-12 12-12h56c6.6 0 12 5.4 12 12v92h92c6.6 0 12 5.4 12 12v56z"></path></svg>

After

Width:  |  Height:  |  Size: 551 B

View File

@@ -0,0 +1,97 @@
<template>
<div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import Workpage from "../types/Workpage";
import API from '../data/API';
import UserAPI from "../data/UserAPI"
import User from '../types/User';
@Component
export default class WorkpageExplorer extends Vue {
@Prop() api!: API;
//Watch for a login request, if a request is made (value is set to true), begin login procedure.
@Watch('$store.state.loginRequested')
onLoginRequest(newVal: any, oldVal: any)
{
if (newVal == true)
this.loginDialog()
}
//Check whether a token exists in localstorage, if it does then login with that token.
created()
{
document.title = "Aya";
if (localStorage.token == null || localStorage.token == "")
this.loginDialog();
else
this.loginWithToken();
}
//Show login dialog.
loginDialog()
{
this.$store.state.loginRequested = false;
this.$modal.show("dialog", {
title: "Please Login",
text: "Username: <input type=\"text\" id=\"username\"><br>" +
"Password: <input type=\"password\" id=\"password\">",
buttons: [
{
title: "Create",
default: false,
handler: () => { this.createAccount((document.getElementById("username") as HTMLInputElement).value, (document.getElementById("password") as HTMLInputElement).value); this.$modal.hide("dialog"); }
},
{
title: "Login",
default: true,
handler: () => { this.login((document.getElementById("username") as HTMLInputElement).value, (document.getElementById("password") as HTMLInputElement).value); this.$modal.hide("dialog"); }
}
]
},{ clickToClose: false });
}
//Send request to API to login (returning user object with token).
async login(username: string, password: string)
{
if (this.api == undefined)
return;
const uAPI = new UserAPI(this.api.axios, this.api.rootURL);
const response = await uAPI.login(username, password);
this.$store.state.currentUser = new User(response.data.id, response.data.username, response.data.token);
localStorage.token = response.data.token;
this.$store.state.authed = true;
}
//Send request to API to create a new account (returns the new user with the token).
async createAccount(username: string, password: string)
{
if (this.api == undefined)
return;
const uAPI = new UserAPI(this.api.axios, this.api.rootURL);
const response = await uAPI.create(username, password);
console.log(response);
this.$store.state.currentUser = new User(response.data.id, response.data.username, localStorage.token);
this.$store.state.authed = true;
}
//Send request to API to login with a given token.
async loginWithToken()
{
if (this.api == undefined)
return;
const uAPI = new UserAPI(this.api.axios, this.api.rootURL);
const response = await uAPI.getUser(localStorage.token);
this.$store.state.currentUser = new User(response.data.id, response.data.username, localStorage.token);
this.$store.state.authed = true;
}
}
</script>

View File

@@ -0,0 +1,92 @@
<template>
<div class="topnav">
<img class="logo" src="../assets/Aya_JP_With_Line.png" width="84" height="49">
<a class="username" v-if="logString == 'Logout'" href="#">Logged in as: {{ $store.state.currentUser.Username }}</a>
<a class="Log-Link" href="#" @click="handleButton()"> {{ logString }} </a>
</div>
</template>
<script lang="ts">
//Formatting in this script is odd for some reason, blame VSCode.
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import Workpage from "../types/Workpage";
import API from '../data/API';
import UserAPI from "../data/UserAPI"
import User from '../types/User';
@Component
export default class WorkpageExplorer extends Vue
{
@Prop() private logString = "Login";
//Check if authed has changed, and change the text based on whether authed or not.
@Watch('$store.state.authed')
onAuthChange(newVal: any, oldVal: any)
{
if (newVal == true)
this.logString = "Logout";
else
this.logString = "Login"
}
//Decides whether to request a login or logout, depending on the current state.
handleButton()
{
if (this.logString == "Login")
this.requestLogin();
else
{
this.logout();
}
}
//Sets the global loginRequested state to be true, saying that the login prompt should open.
requestLogin()
{
this.$store.state.loginRequested = true;
}
//Removes the token from the local storage and sets global authed to false. (Simple logout technique).
logout()
{
this.$store.state.authed = false;
localStorage.token = null;
}
}
</script>
<style scoped>
.topnav {
background-color: #333;
overflow: hidden;
}
.topnav a {
float: left;
color: #f2f2f2;
text-align: center;
padding: 14px 16px;
text-decoration: none;
font-size: 17px;
}
.topnav a:hover {
background-color: #ddd;
color: black;
}
.logo {
float: left;
color: #f2f2f2;
text-align: center;
text-decoration: none;
font-size: 17px;
}
.Log-Link
{
position: absolute;
right: 0px;
}
.username
{
position: absolute;
right: 100px;
}
</style>

View File

@@ -0,0 +1,138 @@
<template>
<div>
<ul class="workbookList">
<li v-for="workbook in workbookArray" :key="workbook.id">
<img @click="changeActiveWorkbook(workbook.id)" :src="getBookImage(workbook.id)" width="50" height="50">
</li>
</ul>
<div class="addWorkbook">
<img @click="addWorkbookDialog()" src="../assets/plus-square-solid.svg" width="20" height="20"> Add Workbook
</div>
<v-dialog />
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import Workbook from "../types/Workbook";
import API from '../data/API';
import WorkbookAPI from "../data/WorkbookAPI";
import { findByKey } from "../utils/collections";
@Component
export default class WorkbookSidebar extends Vue {
//Local workbook variable, these are from the API.
@Prop() private workbookArray!: Workbook[];
@Prop() private api!: API;
//When dynamic images are done in this way, they must be required by the compiler.
@Prop() private readonly closedBookSolidImage = require('../assets/book-solid.svg');
@Prop() private readonly openBookSolidImage = require('../assets/book-open-solid.svg');
//When the user is authed, get all workbooks from that user from the api and assign them to the local variable.
//When the user is not authed, empty the array.
@Watch('$store.state.authed')
onAuthed(newVal: any, oldVal: any)
{
if (newVal == true)
this.getAllUserWorkbooksFromAPI();
else
this.emptyWorkbookArray();
}
//Assign the response from the api to the local workbook variable.
private async getAllUserWorkbooksFromAPI()
{
if (this.api == undefined)
return;
const wbAPI = new WorkbookAPI(this.api.axios, this.api.rootURL);
const response = await wbAPI.getAllUserWorkbooks(localStorage.token);
this.workbookArray = response.data;
}
//Get the book image (open/closed) for the specified ID.
//(If the current workbook is the workbook ID passed, the open book image will be used, otherwise the closed image will be used).
private getBookImage(id: string): any
{
if (this.$store.state.workbookActive == id)
return this.openBookSolidImage;
return this.closedBookSolidImage;
};
//Change the store's active workbook to the ID passed.
private changeActiveWorkbook(id: string)
{
this.$store.commit("changeActiveWorkbook", id);
}
//Show the dialog for adding a new workbook.
private addWorkbookDialog()
{
if (this.$store.state.authed == false)
return;
this.$modal.show("dialog", {
title: "Add Workbook",
text: "Please input the name of the new Workbook: <input id=\"new-workbook-name\" type=\"text\" style=\"width: 370px\">",
buttons: [
{
title: "Cancel",
hander: () => { this.$modal.hide("dialog") }
},
{
title: "Create",
default: true,
handler: () => { this.addWorkbook((document.getElementById("new-workbook-name") as HTMLInputElement).value); this.$modal.hide("dialog"); }
}
]
});
}
//Adds a workbook by posting to the API. Then, retrieves all of the workbooks again to update the local version.
private async addWorkbook(name: string)
{
if (this.api == undefined)
return;
const wbAPI = new WorkbookAPI(this.api.axios, this.api.rootURL);
const response = await wbAPI.addWorkbook(name, localStorage.token);
await this.getAllUserWorkbooksFromAPI();
}
//Empties the local workbook array.
private emptyWorkbookArray()
{
this.workbookArray = [];
}
}
</script>
<style scoped>
body
{
background-color: darkslategray;
}
.addWorkbook
{
position : fixed;
bottom : 1vh;
cursor: pointer;
}
.workbookList
{
position : fixed;
left: 0px;
overflow : auto;
height : 90vh;
list-style-type : none;
cursor: pointer;
}
.workbookList ul, .workbookList li
{
margin: 0; padding: 0;
}
</style>

View File

@@ -0,0 +1,109 @@
<template>
<div class="editor">
<h1>{{ workpageTitle }}</h1>
<vue-simplemde ref="markdownEditor" />
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch, Ref } from "vue-property-decorator";
import VueSimplemde from 'vue-simplemde';
import API from "../data/API";
import WorkpageAPI from "../data/WorkpageAPI";
import AutoSave from "../utils/autosave";
@Component
export default class WorkpageEditor extends Vue {
//Local variables for editor.
@Prop() private workpageTitle!: string;
@Prop() private workpageContent!: string;
@Prop() private api!: API;
@Prop() private autoSave!: any;
//Reference to the markdown editor.
@Ref("markdownEditor") readonly mde!: any;
//Watch for authed changing, if the user gets unauthed, then reset/empty all values.
@Watch('$store.state.authed')
onAuthChange(newVal: any, oldVal: any)
{
if (newVal == false)
{
this.workpageContent = "";
this.updateEditorText();
this.workpageTitle = "";
}
}
//Watch for the active workpage changing, as this is when the text changes need to be applied.
@Watch('$store.state.workpageActive')
onWorkPageChange(newVal: any, oldVal: any)
{
this.getWorkpage(newVal);
}
//Create a loop for the autosave function. Creates the object on created (5 seconds per post).
created()
{
this.autoSave = new AutoSave(() => this.updateWorkpage(), 5000);
}
//Creates a hook for when a change is made (for example, text changes), create the timer. If another change is made within the 5 seconds, restart the timer.
//This means the save only starts after 5 seconds of inactivity after a change.
mounted()
{
const self = this.startAutosaveTimer;
this.mde.simplemde.codemirror.on("change", function(){
self();
});
}
//Gets a workpage from the api with the specified ID.
private async getWorkpage(id: string)
{
if (this.api == undefined)
return;
const wpAPI = new WorkpageAPI(this.api.axios, this.api.rootURL);
const response = await wpAPI.getWorkpage(id, localStorage.token);
this.workpageTitle = response.data.name;
this.workpageContent = response.data.content;
this.updateEditorText();
}
//Updates the Editor's text with the workpageContent variable.
private updateEditorText()
{
this.mde.simplemde.value(this.workpageContent);
}
//Updates a workpage to the API based on the content in the editor and the active workpage.
private async updateWorkpage()
{
if (this.api == undefined)
return;
const wpAPI = new WorkpageAPI(this.api.axios, this.api.rootURL);
const response = await wpAPI.updateWorkpage(this.$store.state.workpageActive, this.mde.simplemde.value(), localStorage.token);
}
//Starts the autosave timer.
private startAutosaveTimer()
{
clearInterval(this.autoSave);
this.autoSave = setInterval(this.updateWorkpage, 5000);
}
}
</script>
<style>
@import '~simplemde/dist/simplemde.min.css';
.editor
{
height: 90vh;
}
</style>

View File

@@ -0,0 +1,115 @@
<template>
<div class="workpageList">
<ul style="list-style-type : none;">
<li v-for="workpage in workpages" :key="workpage.id">
<a href="#" @click="changeSelectedWorkpage(workpage.id)">{{ workpage.name }}</a>
</li>
</ul>
<div class="addWorkpage">
<img @click="addWorkpageDialog()" src="../assets/plus-square-solid.svg" width="20" height="20"> Add Workpage
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import Workpage from "../types/Workpage";
import API from '../data/API';
import WorkpageAPI from "../data/WorkpageAPI"
@Component
export default class WorkpageExplorer extends Vue {
//The local variable for the workpages displayed.
@Prop() private workpages!: Workpage[];
@Prop() private api!: API;
//Checks of the workbook changes. When the workbook changes, it gets all pages from that workbook.
@Watch('$store.state.workbookActive')
onWorkbookChange(newVal: any, oldVal: any)
{
this.getAllWorkpagesFromWorkbook(newVal);
}
//Checks if the auth gets changed. If the user gets unauthed, all workpages should be removed from the view.
@Watch('$store.state.authed')
onAuthChange(newVal: any, oldVal: any)
{
if (newVal == false)
{
this.workpages = [];
}
}
//Gets all of the workpages from the workbook id passed.
private async getAllWorkpagesFromWorkbook(id: string)
{
if (this.api == undefined)
return;
const wpAPI = new WorkpageAPI(this.api.axios, this.api.rootURL);
const response = await wpAPI.getAllWorkpagesFromWorkbook(id, localStorage.token);
this.workpages = response.data;
}
//Commits the new selected workpage to the store.
private changeSelectedWorkpage(id: string)
{
this.$store.commit("changeActiveWorkpage", id);
}
//Shows the dialog to add a workpage.
private addWorkpageDialog()
{
this.$modal.show("dialog", {
title: "Add Workpage",
text: "Please input the name of the new Workpage: <input id=\"new-workpage-name\" type=\"text\" style=\"width: 370px\">",
buttons: [
{
title: "Cancel",
hander: () => { this.$modal.hide("dialog") }
},
{
title: "Create",
default: true,
handler: () => { this.addWorkpage((document.getElementById("new-workpage-name") as HTMLInputElement).value); this.$modal.hide("dialog"); }
}
]
});
}
//Adds a workpage to the API with the given name.
private async addWorkpage(name: string)
{
if (this.api == undefined)
return;
const activeWB = this.$store.state.workbookActive;
const wpAPI = new WorkpageAPI(this.api.axios, this.api.rootURL);
const response = await wpAPI.addWorkpage(name, activeWB, localStorage.token);
await this.getAllWorkpagesFromWorkbook(activeWB);
};
}
</script>
<style scoped>
.workpageList
{
text-align: left;
white-space : nowrap;
overflow: auto;
list-style-type : none;
}
.addWorkpage
{
position : fixed;
bottom : 1vh;
cursor: pointer;
}
.workpageList ul, .workpageList li
{
margin: 0; padding: 0;
}
</style>

View File

@@ -0,0 +1,47 @@
export default class API
{
readonly rootURL: string;
readonly axios: any;
constructor(ax: any, root: string)
{
this.axios = ax;
this.rootURL = root;
}
async post(path: string, bodyContent: string)
{
const res = await this.axios.post(this.rootURL + path, bodyContent, { headers: { "Content-Type": "application/json" }});
return res;
}
async postAuthed(path: string, token: string)
{
const res = await this.axios.post(this.rootURL + path, "", { headers: { "Authorization": "Bearer " + token }});
return res;
}
async postAuthedWithBody(path: string, content: string, token: string)
{
const res = await this.axios.post(this.rootURL + path, content, { headers: { "Authorization": "Bearer " + token, "Content-Type": "application/json" }, });
return res;
}
async postWithHeaders(path: string, bodyContent: string, options: any)
{
const res = await this.axios.post(this.rootURL + path, bodyContent, options);
return res;
}
async get(path: string)
{
const res = await this.axios.get(this.rootURL + path);
return res;
}
async getAuthed(path: string, token: string)
{
const res = await this.axios.get(this.rootURL + path, { headers: { "Authorization": "Bearer " + token }});
return res;
}
}

View File

@@ -0,0 +1,26 @@
import API from "./API";
export default class UserAPI extends API
{
private readonly apiPath = "api/user/";
constructor(ax: any, root: string)
{
super(ax, root);
}
async login(username: string, password: string)
{
return await super.post(this.apiPath + "login", JSON.stringify({ Username: username, Password: password }));
}
async getUser(token: string)
{
return await super.getAuthed(this.apiPath + "get", token);
}
async create(username: string, password: string)
{
return await super.post(this.apiPath + "create", JSON.stringify({ Username: username, Password: password }));
}
}

View File

@@ -0,0 +1,22 @@
import API from './API';
export default class WorkbookAPI extends API
{
private readonly apiPath = "api/workbook/";
constructor(ax: any, root: string)
{
super(ax, root);
}
async getAllUserWorkbooks(token: string)
{
return await super.getAuthed(this.apiPath + "userworkbooks", token);
}
async addWorkbook(name: string, token: string)
{
console.log(name + " " + token);
return await super.postAuthed(this.apiPath + "add?name=" + name, token);
}
}

View File

@@ -0,0 +1,31 @@
import API from './API';
export default class WorkpageAPI extends API
{
private readonly apiPath = "api/workpage/";
constructor(ax: any, root: string)
{
super(ax, root);
}
async getAllWorkpagesFromWorkbook(id: string, token: string)
{
return await super.getAuthed(this.apiPath + "fromworkbook?id=" + id, token);
}
async getWorkpage(id: string, token: string)
{
return await super.getAuthed(this.apiPath + "get?id=" + id, token)
}
async addWorkpage(name: string, workbookid: number, token)
{
return await super.postAuthed(this.apiPath + "add?name=" + name + "&workbookid=" + workbookid, token);
}
async updateWorkpage(id: string, content: string, token: string)
{
return await super.postAuthedWithBody(this.apiPath + "updateworkpagecontent?id=" + id, '"' + content + '"', token);
}
}

30
Aya-Frontend/src/main.ts Normal file
View File

@@ -0,0 +1,30 @@
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store/store";
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue';
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap-vue/dist/bootstrap-vue.css';
import { library } from '@fortawesome/fontawesome-svg-core';
import { faUserSecret } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import VueSimplemde from 'vue-simplemde';
import 'simplemde/dist/simplemde.min.css';
import VModal from 'vue-js-modal';
library.add(faUserSecret);
Vue.use(BootstrapVue);
Vue.use(IconsPlugin);
Vue.use(VModal, { dialog: true });
Vue.component('font-awesome-icon', FontAwesomeIcon);
Vue.component('vue-simplemde', VueSimplemde)
Vue.config.productionTip = false;
new Vue({
store,
router,
render: h => h(App)
}).$mount("#app");

View File

@@ -0,0 +1,20 @@
import Vue from "vue";
import VueRouter, { RouteConfig } from "vue-router";
import Home from "../views/Home.vue";
Vue.use(VueRouter);
const routes: Array<RouteConfig> = [
{
path: "/",
name: "Home",
component: Home
}];
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes
});
export default router;

13
Aya-Frontend/src/shims-tsx.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
import Vue, { VNode } from "vue";
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any;
}
}
}

4
Aya-Frontend/src/shims-vue.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module "*.vue" {
import Vue from "vue";
export default Vue;
}

View File

@@ -0,0 +1,27 @@
import Vue from 'vue'
import Vuex from 'vuex'
import User from '@/types/User';
Vue.use(Vuex)
export default new Vuex.Store({
state: {
workbookActive: 0,
workpageActive: 0,
authed: false,
loginRequested: false,
currentUser: User
},
getters: {},
mutations: {
changeActiveWorkbook(state, payload)
{
state.workbookActive = payload;
},
changeActiveWorkpage(state, payload)
{
state.workpageActive = payload;
}
},
actions: {}
});

View File

@@ -0,0 +1,13 @@
export default class User
{
ID!: string;
Username!: string;
Token!: string;
constructor(id: string, username: string, token: string)
{
this.ID = id;
this.Username = username;
this.Token = token;
}
}

View File

@@ -0,0 +1,9 @@
import User from "../types/User";
import Workpage from "../types/Workpage";
export default class Workbook
{
id!: string;
name!: string;
ownerID!: string;
}

View File

@@ -0,0 +1,14 @@
import showdown from "showdown";
export default class Workpage
{
WorkpageID!: string;
WorkpageName!: string;
WorkpageContent!: string;
GetWorkpageContentMarkdown(): string
{
const converter = new showdown.Converter;
return converter.makeHtml(this.WorkpageContent);
}
}

View File

@@ -0,0 +1,22 @@
export default class AutoSave
{
private currentTimeout!: any;
private delayTime!: number;
private func!: Function;
constructor(f: Function, time: number)
{
this.func = f;
this.delayTime = time;
}
public start(): void
{
this.currentTimeout = setTimeout(this.func, this.delayTime);
}
public stop(): void
{
clearTimeout(this.currentTimeout);
}
}

View File

@@ -0,0 +1,8 @@
//Searches through the array and finds the value with the key, else returns null.
export function findByKey(arr: Array<any>, value: any, key = "id"): any
{
for (const val of arr)
if (val[key] == value)
return val;
return null;
}

View File

@@ -0,0 +1,70 @@
<template>
<div class="container">
<div class="row">
<div class="col-sm WorkbookSidebar">
<WorkbookSidebar :api="api" />
</div>
<div class="col-sm WorkpageExplorer">
<WorkpageExplorer :api="api" />
</div>
<div class="col-sm WorkpageEditor">
<WorkpageEditor :api="api" />
</div>
</div>
<AccountDialog :api="api" />
</div>
</template>
<script>
// @ is an alias to /src
import WorkbookSidebar from "@/components/WorkbookSidebar.vue";
import WorkpageExplorer from "@/components/WorkpageExplorer.vue";
import WorkpageEditor from "@/components/WorkpageEditor.vue";
import AccountDialog from "@/components/AccountDialog.vue";
import API from "@/data/API";
export default {
name: "Home",
components: {
WorkbookSidebar,
WorkpageExplorer,
WorkpageEditor,
AccountDialog
},
data: function() {
return {
api: new API(require("axios"), "https://localhost:5001/")
};
}
};
</script>
<style scoped>
.WorkbookSidebar
{
position: absolute;
left: 0px;
width: 150px;
height: 90vh;
resize : horizontal;
background-color: lightslategray;
}
.WorkpageExplorer
{
text-align: left;
position: absolute;
left: 125px;
width: 300px;
height: 90vh;
resize : horizontal;
background-color: rgb(183, 188, 200);
}
.WorkpageEditor
{
text-align: left;
position: absolute;
height: 90vh;
left: 425px;
background-color: rgb(231, 231, 231);
}
</style>

View File

@@ -0,0 +1,12 @@
import { shallowMount } from "@vue/test-utils";
import HelloWorld from "@/components/HelloWorld.vue";
describe("HelloWorld.vue", () => {
it("renders props.msg when passed", () => {
const msg = "new message";
const wrapper = shallowMount(HelloWorld, {
propsData: { msg }
});
expect(wrapper.text()).toMatch(msg);
});
});

View File

@@ -0,0 +1,40 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": false,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env",
"jest"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}