stash
This commit is contained in:
parent
c02b1588f2
commit
a83083d0eb
22 changed files with 1835 additions and 0 deletions
28
frontend/.gitignore
vendored
Normal file
28
frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
29
frontend/README.md
Normal file
29
frontend/README.md
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# frontend
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
15
frontend/index.html
Normal file
15
frontend/index.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<!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, shrink-to-fit=no">
|
||||||
|
<link rel="shortcut icon" href="/src/assets/icons/toolshed-48x48.png" type="image/png">
|
||||||
|
<title>Toolshed</title>
|
||||||
|
<link href="/src/assets/base.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
23
frontend/package.json
Normal file
23
frontend/package.json
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bootstrap-icons-vue": "^1.10.3",
|
||||||
|
"dns-query": "^0.11.2",
|
||||||
|
"js-nacl": "^1.4.0",
|
||||||
|
"vue": "^3.2.47",
|
||||||
|
"vue-router": "^4.1.6",
|
||||||
|
"vuex": "^4.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
|
"sass": "^1.62.1",
|
||||||
|
"vite": "^4.1.4"
|
||||||
|
}
|
||||||
|
}
|
28
frontend/src/App.vue
Normal file
28
frontend/src/App.vue
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<router-view></router-view>
|
||||||
|
<!-- TODO UI für Freunde liste, add, remove -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
|
||||||
|
import {mapMutations} from 'vuex';
|
||||||
|
import store from '@/store';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
methods: {
|
||||||
|
...mapMutations(['init'])
|
||||||
|
},
|
||||||
|
beforeCreate () {
|
||||||
|
store.commit('init')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
294
frontend/src/components/BaseLayout.vue
Normal file
294
frontend/src/components/BaseLayout.vue
Normal file
|
@ -0,0 +1,294 @@
|
||||||
|
<template>
|
||||||
|
<div class="wrapper">
|
||||||
|
<nav id="sidebar" class="sidebar">
|
||||||
|
<div class="sidebar-content js-simplebar">
|
||||||
|
<router-link to="/" class="sidebar-brand">
|
||||||
|
<span class="align-middle">Toolshed</span>
|
||||||
|
</router-link>
|
||||||
|
<ul class="sidebar-nav">
|
||||||
|
<li class="sidebar-header">
|
||||||
|
Tools & Components
|
||||||
|
</li>
|
||||||
|
<!--
|
||||||
|
<li class="sidebar-item {% if 'icons' in segment %} active {% endif %}">
|
||||||
|
<a class="sidebar-link" href="{% url 'inventory' %}">
|
||||||
|
{% bs_icon 'archive' extra_classes="bi-valign-middle" %}
|
||||||
|
<span class="align-middle">Inventory</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
-->
|
||||||
|
<li class="sidebar-item">
|
||||||
|
<router-link to="/inventory" class="sidebar-link">
|
||||||
|
<b-icon-archive class="bi-valign-middle"></b-icon-archive>
|
||||||
|
<span class="align-middle">Inventory</span>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
<li class="sidebar-item">
|
||||||
|
<router-link to="/friends" class="sidebar-link">
|
||||||
|
<b-icon-people class="bi-valign-middle"></b-icon-people>
|
||||||
|
<span class="align-middle">Friends</span>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="main">
|
||||||
|
<nav class="navbar navbar-expand navbar-light navbar-bg">
|
||||||
|
<a class="sidebar-toggle d-flex">
|
||||||
|
<i class="hamburger align-self-center"></i>
|
||||||
|
</a>
|
||||||
|
<form class="d-none d-sm-inline-block">
|
||||||
|
<div class="input-group input-group-navbar">
|
||||||
|
<input type="text" class="form-control" placeholder="Search…" aria-label="Search">
|
||||||
|
<button class="btn" type="button">
|
||||||
|
<b-icon-search class="bi-valign-middle"></b-icon-search>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="navbar-collapse collapse">
|
||||||
|
<ul class="navbar-nav navbar-align">
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-icon dropdown-toggle" href="#" id="alertsDropdown" data-toggle="dropdown">
|
||||||
|
<div class="position-relative">
|
||||||
|
<b-icon-bell class="bi-valign-middle"></b-icon-bell>
|
||||||
|
<span class="indicator">4</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right py-0"
|
||||||
|
aria-labelledby="alertsDropdown">
|
||||||
|
<div class="dropdown-menu-header">
|
||||||
|
4 New Notifications
|
||||||
|
</div>
|
||||||
|
<div class="list-group">
|
||||||
|
<a href="#" class="list-group-item">
|
||||||
|
<div class="row g-0 align-items-center">
|
||||||
|
<div class="col-2">
|
||||||
|
<b-icon-exclamation-circle
|
||||||
|
class="bi-valign-middle text-danger"></b-icon-exclamation-circle>
|
||||||
|
</div>
|
||||||
|
<div class="col-10">
|
||||||
|
<div class="text-dark">Update completed</div>
|
||||||
|
<div class="text-muted small mt-1">Restart server 12 to complete the
|
||||||
|
update.
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small mt-1">30m ago</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="list-group-item">
|
||||||
|
<div class="row g-0 align-items-center">
|
||||||
|
<div class="col-2">
|
||||||
|
<b-icon-bell class="bi-valign-middle text-warning"></b-icon-bell>
|
||||||
|
</div>
|
||||||
|
<div class="col-10">
|
||||||
|
<div class="text-dark">Lorem ipsum</div>
|
||||||
|
<div class="text-muted small mt-1">Aliquam ex eros, imperdiet vulputate
|
||||||
|
hendrerit
|
||||||
|
et.
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small mt-1">2h ago</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="list-group-item">
|
||||||
|
<div class="row g-0 align-items-center">
|
||||||
|
<div class="col-2">
|
||||||
|
<b-icon-house class="bi-valign-middle text-primary"></b-icon-house>
|
||||||
|
</div>
|
||||||
|
<div class="col-10">
|
||||||
|
<div class="text-dark">Login from 192.186.1.8</div>
|
||||||
|
<div class="text-muted small mt-1">5h ago</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<!--{% for notification in top_notifications %}
|
||||||
|
<a href="#" class="list-group-item">
|
||||||
|
<div class="row g-0 align-items-center">
|
||||||
|
<div class="col-2">
|
||||||
|
{% bs_icon 'person-add' extra_classes="bi-valign-middle text-success" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-10">
|
||||||
|
<div class="text-dark">New connection</div>
|
||||||
|
<div class="text-muted small mt-1">Christina accepted your request.</div>
|
||||||
|
<div class="text-muted small mt-1">14h ago</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}-->
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-menu-footer">
|
||||||
|
<a href="#" class="text-muted">Show all notifications</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-icon dropdown-toggle" href="#" id="messagesDropdown" data-toggle="dropdown">
|
||||||
|
<div class="position-relative">
|
||||||
|
<b-icon-chat-left class="bi-valign-middle"></b-icon-chat-left>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right py-0"
|
||||||
|
aria-labelledby="messagesDropdown">
|
||||||
|
<div class="dropdown-menu-header">
|
||||||
|
<div class="position-relative">
|
||||||
|
4 New Messages
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-group">
|
||||||
|
<a href="#" class="list-group-item">
|
||||||
|
<div class="row g-0 align-items-center">
|
||||||
|
<div class="col-2">
|
||||||
|
<!--<img src="/static/assets/img/avatars/avatar-5.png"
|
||||||
|
class="avatar img-fluid rounded-circle" alt="Vanessa Tucker">-->
|
||||||
|
</div>
|
||||||
|
<div class="col-10 pl-2">
|
||||||
|
<div class="text-dark">Vanessa Tucker</div>
|
||||||
|
<div class="text-muted small mt-1">Nam pretium turpis et arcu. Duis arcu
|
||||||
|
tortor.
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small mt-1">15m ago</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="list-group-item">
|
||||||
|
<div class="row g-0 align-items-center">
|
||||||
|
<div class="col-2">
|
||||||
|
<!--<img src="/static/assets/img/avatars/avatar-2.png"
|
||||||
|
class="avatar img-fluid rounded-circle" alt="William Harris">-->
|
||||||
|
</div>
|
||||||
|
<div class="col-10 pl-2">
|
||||||
|
<div class="text-dark">William Harris</div>
|
||||||
|
<div class="text-muted small mt-1">Curabitur ligula sapien euismod
|
||||||
|
vitae.
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small mt-1">2h ago</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="list-group-item">
|
||||||
|
<div class="row g-0 align-items-center">
|
||||||
|
<div class="col-2">
|
||||||
|
<!--<img src="/static/assets/img/avatars/avatar-4.png"
|
||||||
|
class="avatar img-fluid rounded-circle" alt="Christina Mason">-->
|
||||||
|
</div>
|
||||||
|
<div class="col-10 pl-2">
|
||||||
|
<div class="text-dark">Christina Mason</div>
|
||||||
|
<div class="text-muted small mt-1">Pellentesque auctor neque nec urna.
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small mt-1">4h ago</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<!-- {% for messege in top_messages %}-->
|
||||||
|
<a href="#" class="list-group-item">
|
||||||
|
<div class="row g-0 align-items-center">
|
||||||
|
<div class="col-2">
|
||||||
|
<!--<img src="/static/assets/img/avatars/avatar-3.png"
|
||||||
|
class="avatar img-fluid rounded-circle" alt="Sharon Lessman">-->
|
||||||
|
</div>
|
||||||
|
<div class="col-10 pl-2">
|
||||||
|
<div class="text-dark">Sharon Lessman</div>
|
||||||
|
<div class="text-muted small mt-1">Aenean tellus metus, bibendum sed,
|
||||||
|
posuere ac,
|
||||||
|
mattis non.
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small mt-1">5h ago</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<!--{% endfor %}-->
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-menu-footer">
|
||||||
|
<a href="#" class="text-muted">Show all messages</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-icon dropdown-toggle d-inline-block d-sm-none" href="#"
|
||||||
|
data-toggle="dropdown">
|
||||||
|
<i class="align-middle" data-feather="settings"></i>
|
||||||
|
<b-icon-chat-left class="bi-valign-middle"></b-icon-chat-left>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="nav-link dropdown-toggle d-none d-sm-inline-block" href="#"
|
||||||
|
data-toggle="dropdown">
|
||||||
|
<!--<img src="/static/assets/img/avatars/avatar.png" class="avatar img-fluid rounded mr-1"
|
||||||
|
alt="Charles Hall"/>-->
|
||||||
|
<span class="text-dark">
|
||||||
|
<!--{{ request.user.username }}-->
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="dropdown-menu dropdown-menu-right">
|
||||||
|
<router-link to="/profile" class="dropdown-item">
|
||||||
|
<b-icon-person class="bi-valign-middle mr-1"></b-icon-person>
|
||||||
|
Profile
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/settings" class="dropdown-item">
|
||||||
|
<b-icon-sliders class="bi-valign-middle mr-1"></b-icon-sliders>
|
||||||
|
Settings &
|
||||||
|
Privacy
|
||||||
|
</router-link>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<a class="dropdown-item" href="#" @click="logout"> Log out</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<slot></slot>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row text-muted">
|
||||||
|
<div class="col-6 text-left">
|
||||||
|
<p class="mb-0">
|
||||||
|
<a target="_blank" href="https://www.gnu.org/licenses/gpl-3.0.de.html"
|
||||||
|
class="text-muted">
|
||||||
|
License: <strong>GPL-3.0</strong>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-right">
|
||||||
|
<ul class="list-inline">
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<a class="text-muted"
|
||||||
|
target="_blank" href="https://github.com/gr4yj3d1/toolshed">Dev Docs</a>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<a class="text-muted"
|
||||||
|
target="_blank" href="https://github.com/gr4yj3d1/toolshed">Sources</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapGetters, mapMutations} from 'vuex';
|
||||||
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'BaseLayout',
|
||||||
|
components: {
|
||||||
|
...BIcons
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
username() {
|
||||||
|
return this.$route.params.username
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapMutations(['logout'])
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
30
frontend/src/dns.js
Normal file
30
frontend/src/dns.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import {query} from 'dns-query';
|
||||||
|
|
||||||
|
class FallBackResolver {
|
||||||
|
constructor() {
|
||||||
|
this._servers = ['1.1.1.1', '8.8.8.8'];
|
||||||
|
this._cache = JSON.parse(localStorage.getItem('dns-cache')) || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async query(domain, type) {
|
||||||
|
const key = domain + ':' + type;
|
||||||
|
if (key in this._cache && this._cache[key].time > Date.now() - 1000 * 60 * 60) {
|
||||||
|
const age_seconds = Math.ceil(Date.now() / 1000 - this._cache[key].time / 1000);
|
||||||
|
console.log('cache hit', key, this._cache[key].ttl - age_seconds);
|
||||||
|
return [this._cache[key].data];
|
||||||
|
}
|
||||||
|
const result = await query(
|
||||||
|
{question: {type: type, name: domain}},
|
||||||
|
{
|
||||||
|
endpoints: this._servers,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const first = result.answers[0];
|
||||||
|
this._cache[key] = {time: Date.now(), ...first}; // TODO hadle multiple answers
|
||||||
|
localStorage.setItem('dns-cache', JSON.stringify(this._cache));
|
||||||
|
console.log('cache miss', key, first.ttl);
|
||||||
|
return [first.data];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FallBackResolver;
|
19
frontend/src/main.js
Normal file
19
frontend/src/main.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import {createApp} from 'vue'
|
||||||
|
//import { BootstrapVue, BootstrapVueIcons } from 'bootstrap-vue'
|
||||||
|
import { BootstrapIconsPlugin } from 'bootstrap-icons-vue';
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
import './assets/css/toolshed.css'
|
||||||
|
import './assets/js/app.js'
|
||||||
|
|
||||||
|
import router from './router'
|
||||||
|
import store from './store';
|
||||||
|
|
||||||
|
import _nacl from 'js-nacl';
|
||||||
|
|
||||||
|
const app = createApp(App).use(router).use(store).use(BootstrapIconsPlugin);
|
||||||
|
|
||||||
|
_nacl.instantiate((nacl) => {
|
||||||
|
window.nacl = nacl
|
||||||
|
app.mount('#app')
|
||||||
|
});
|
38
frontend/src/neigbors.js
Normal file
38
frontend/src/neigbors.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
class NeighborsCache {
|
||||||
|
constructor() {
|
||||||
|
//this._max_age = 1000 * 60 * 60; // 1 hour
|
||||||
|
this._max_age = 1000 * 60 * 5; // 5 minutes
|
||||||
|
this._cache = JSON.parse(localStorage.getItem('neighbor-cache')) || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
reachable(domain) {
|
||||||
|
console.log('reachable neighbor ' + domain)
|
||||||
|
if (domain in this._cache) {
|
||||||
|
delete this._cache[domain];
|
||||||
|
localStorage.setItem('neighbor-cache', JSON.stringify(this._cache));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unreachable(domain) {
|
||||||
|
console.log('unreachable neighbor ' + domain)
|
||||||
|
this._cache[domain] = {time: Date.now()};
|
||||||
|
localStorage.setItem('neighbor-cache', JSON.stringify(this._cache));
|
||||||
|
}
|
||||||
|
|
||||||
|
queryUnreachable(domain) {
|
||||||
|
//return false if unreachable
|
||||||
|
if (domain in this._cache) {
|
||||||
|
if (this._cache[domain].time > Date.now() - this._max_age) {
|
||||||
|
console.log('skip unreachable neighbor ' + domain + ' ' + Math.ceil(
|
||||||
|
Date.now()/1000 - this._cache[domain].time/1000) + 's/' + Math.ceil(this._max_age/1000) + 's')
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
delete this._cache[domain];
|
||||||
|
localStorage.setItem('neighbor-cache', JSON.stringify(this._cache));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NeighborsCache;
|
49
frontend/src/router.js
Normal file
49
frontend/src/router.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import {createRouter, createWebHistory} from 'vue-router'
|
||||||
|
import Index from '@/views/Index.vue';
|
||||||
|
import Login from '@/views/Login.vue';
|
||||||
|
import Register from '@/views/Register.vue';
|
||||||
|
import store from '@/store';
|
||||||
|
import Profile from '@/views/Profile.vue';
|
||||||
|
import Settings from '@/views/Settings.vue';
|
||||||
|
import Inventory from '@/views/Inventory.vue';
|
||||||
|
import Friends from "@/views/Friends.vue";
|
||||||
|
import InventoryNew from "@/views/InventoryNew.vue";
|
||||||
|
import InventoryEdit from "@/views/InventoryEdit.vue";
|
||||||
|
import InventoryDetail from "@/views/InventoryDetail.vue";
|
||||||
|
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{path: '/', component: Index, meta: {requiresAuth: true}},
|
||||||
|
{path: '/profile', component: Profile, meta: {requiresAuth: true}},
|
||||||
|
{path: '/settings', component: Settings, meta: {requiresAuth: true}},
|
||||||
|
{path: '/inventory', component: Inventory, meta: {requiresAuth: true}},
|
||||||
|
{path: '/inventory/:id', component: InventoryDetail, meta: {requiresAuth: true}},
|
||||||
|
{path: '/inventory/:id/edit', component: InventoryEdit, meta: {requiresAuth: true}},
|
||||||
|
{path: '/inventory/new', component: InventoryNew, meta: {requiresAuth: true}},
|
||||||
|
{path: '/friends', component: Friends, meta: {requiresAuth: true}},
|
||||||
|
{path: '/login', component: Login, meta: {requiresAuth: false}},
|
||||||
|
{path: '/register', component: Register, meta: {requiresAuth: false}},
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
// 4. Provide the history implementation to use. We are using the hash history for simplicity here.
|
||||||
|
history: createWebHistory(),
|
||||||
|
linkActiveClass: "active",
|
||||||
|
routes, // short for `routes: routes`
|
||||||
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to, from) => {
|
||||||
|
// instead of having to check every route record with
|
||||||
|
// to.matched.some(record => record.meta.requiresAuth)
|
||||||
|
if (to.meta.requiresAuth && !store.getters.isLoggedIn) {
|
||||||
|
// this route requires auth, check if logged in
|
||||||
|
// if not, redirect to login page.
|
||||||
|
return {
|
||||||
|
path: '/login',
|
||||||
|
// save the location we were at to come back later
|
||||||
|
query: {redirect: to.fullPath},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
186
frontend/src/store.js
Normal file
186
frontend/src/store.js
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
import {createStore} from 'vuex';
|
||||||
|
import router from '@/router';
|
||||||
|
import FallBackResolver from "@/dns";
|
||||||
|
import NeighborsCache from "@/neigbors";
|
||||||
|
|
||||||
|
|
||||||
|
export default createStore({
|
||||||
|
state: {
|
||||||
|
user: null,
|
||||||
|
token: null,
|
||||||
|
keypair: null,
|
||||||
|
remember: false,
|
||||||
|
friends: [],
|
||||||
|
item_map: {},
|
||||||
|
resolver: new FallBackResolver(),
|
||||||
|
unreachable_neighbors: new NeighborsCache(),
|
||||||
|
/*wk: new Wellknown({
|
||||||
|
update: true, // Will load latest definitions from updateURL.
|
||||||
|
updateURL: new URL(), // URL to load the latest definitions. (default: project URL)
|
||||||
|
persist: false, // True to persist the loaded definitions (nodejs: in filesystem, browser: localStorage)
|
||||||
|
localStoragePrefix: 'dnsquery_', // Prefix for files persisted.
|
||||||
|
maxAge: 300000, // Max age of persisted data to be used in ms.
|
||||||
|
timeout: 5000 // Timeout when loading updates.
|
||||||
|
})*/
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
setUser(state, user) {
|
||||||
|
state.user = user;
|
||||||
|
if (state.remember)
|
||||||
|
localStorage.setItem('user', user);
|
||||||
|
},
|
||||||
|
setToken(state, token) {
|
||||||
|
state.token = token;
|
||||||
|
if (state.remember)
|
||||||
|
localStorage.setItem('token', token);
|
||||||
|
},
|
||||||
|
setKey(state, keypair) {
|
||||||
|
state.keypair = nacl.crypto_sign_keypair_from_seed(nacl.from_hex(keypair))
|
||||||
|
if (state.remember)
|
||||||
|
localStorage.setItem('keypair', nacl.to_hex(state.keypair.signSk).slice(0, 64))
|
||||||
|
},
|
||||||
|
setRemember(state, remember) {
|
||||||
|
state.remember = remember;
|
||||||
|
if (!remember) {
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('keypair');
|
||||||
|
}
|
||||||
|
localStorage.setItem('remember', remember);
|
||||||
|
},
|
||||||
|
setInventoryItems(state, {url, items}) {
|
||||||
|
state.item_map[url] = items;
|
||||||
|
},
|
||||||
|
logout(state) {
|
||||||
|
state.user = null;
|
||||||
|
state.token = null;
|
||||||
|
state.keypair = null;
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('keypair');
|
||||||
|
router.push('/login');
|
||||||
|
},
|
||||||
|
init(state) {
|
||||||
|
const remember = localStorage.getItem('remember');
|
||||||
|
const user = localStorage.getItem('user');
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const keypair = localStorage.getItem('keypair');
|
||||||
|
if (user && token) {
|
||||||
|
this.commit('setUser', user);
|
||||||
|
this.commit('setToken', token);
|
||||||
|
if (keypair) {
|
||||||
|
this.commit('setKey', keypair)
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async login({commit, dispatch, state}, {username, password, remember}) {
|
||||||
|
//this.setRemember(remember)
|
||||||
|
this.commit('setRemember', remember);
|
||||||
|
/*const response = await fetch('http://10.23.42.128:8000/auth/token/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: username, password: password
|
||||||
|
})
|
||||||
|
})*/
|
||||||
|
const data = await dispatch('apiLocalPost', {
|
||||||
|
target: '/token/', data: {
|
||||||
|
username: username, password: password
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (data.token) {
|
||||||
|
commit('setToken', data.token);
|
||||||
|
commit('setUser', username + '@example.com');
|
||||||
|
const j = await dispatch('apiLocalGet', {target: '/keys/'})
|
||||||
|
const k = j.key
|
||||||
|
this.commit('setKey', k)
|
||||||
|
await router.push({path: '/'});
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
async getFriends(state) {
|
||||||
|
return ['jedi@j3d1.de', 'foobar@example.com', 'foobaz@example.eleon'];
|
||||||
|
},
|
||||||
|
async getFriendServer({state}, {username}) {
|
||||||
|
const domain = username.split('@')[1]
|
||||||
|
if (domain === 'example.eleon')
|
||||||
|
return ['10.23.42.186:8000'];
|
||||||
|
if (domain === 'localhost')
|
||||||
|
return ['127.0.0.1:8000'];
|
||||||
|
if (domain === 'example.com')
|
||||||
|
return ['10.23.42.128:8000'];
|
||||||
|
const request = '_toolshed-server._tcp.' + domain + '.'
|
||||||
|
return await state.resolver.query(request, 'SRV').then(
|
||||||
|
(result) => result.map(
|
||||||
|
(answer) => answer.target + ':' + answer.port))
|
||||||
|
},
|
||||||
|
async apiFederatedGet({state}, url) {
|
||||||
|
if (state.unreachable_neighbors.queryUnreachable(url)) {
|
||||||
|
throw new Error('unreachable neighbor')
|
||||||
|
}
|
||||||
|
const signature = nacl.crypto_sign_detached(nacl.encode_utf8(url), state.keypair.signSk)
|
||||||
|
const auth = 'Signature ' + state.user + ':' + nacl.to_hex(signature)
|
||||||
|
return await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': auth
|
||||||
|
}
|
||||||
|
}).catch( err => state.unreachable_neighbors.unreachable(url)
|
||||||
|
).then(response => response.json())
|
||||||
|
},
|
||||||
|
async apiFederatedPost({state}, {url, data}) {
|
||||||
|
if (state.unreachable_neighbors.queryUnreachable(url)) {
|
||||||
|
throw new Error('unreachable neighbor')
|
||||||
|
}
|
||||||
|
const json = JSON.stringify(data)
|
||||||
|
const signature = nacl.crypto_sign_detached(nacl.encode_utf8(url + json), state.keypair.signSk)
|
||||||
|
const auth = 'Signature ' + state.user + ':' + nacl.to_hex(signature)
|
||||||
|
return await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': auth,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: json
|
||||||
|
}).catch( err => state.unreachable_neighbors.unreachable(url)
|
||||||
|
).then(response => response.json())
|
||||||
|
},
|
||||||
|
async apiLocalGet({state}, {target}) {
|
||||||
|
const auth = state.token ? {'Authorization': 'Token ' + state.token} : {}
|
||||||
|
return await fetch('http://10.23.42.128:8000/auth' + target, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: auth
|
||||||
|
}).then(response => response.json())
|
||||||
|
},
|
||||||
|
async apiLocalPost({state}, {target, data}) {
|
||||||
|
const auth = state.token ? {'Authorization': 'Token ' + state.token} : {}
|
||||||
|
return await fetch('http://10.23.42.128:8000/auth' + target, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...auth
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
}).then(response => response.json())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
isLoggedIn(state) {
|
||||||
|
return state.user !== null && state.token !== null;
|
||||||
|
},
|
||||||
|
inventory_items(state) {
|
||||||
|
return Object.entries(state.item_map).reduce((acc, [url, items]) => {
|
||||||
|
return acc.concat(items)
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
88
frontend/src/views/Friends.vue
Normal file
88
frontend/src/views/Friends.vue
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<main class="content">
|
||||||
|
<div class="container-fluid p-0">
|
||||||
|
<h1 class="h3 mb-3">Friends</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-xl-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title">Foo</h5>
|
||||||
|
<h6 class="card-subtitle text-muted">Bar <code>baz</code>.</h6>
|
||||||
|
</div>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:40%;">Name</th>
|
||||||
|
<th class="d-none d-md-table-cell" style="width:25%">Server</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="friend in friends" :key="friend.name">
|
||||||
|
<td>{{ friend.name }}</td>
|
||||||
|
<td class="d-none d-md-table-cell">{{ friend.server.join(', ') }}</td>
|
||||||
|
<td class="table-action">
|
||||||
|
<a href="#">
|
||||||
|
<b-icon-pencil-square></b-icon-pencil-square>
|
||||||
|
</a>
|
||||||
|
<a href="#">
|
||||||
|
<b-icon-trash></b-icon-trash>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapActions, mapGetters} from "vuex";
|
||||||
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
|
import BaseLayout from "@/components/BaseLayout.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Inventory',
|
||||||
|
components: {
|
||||||
|
BaseLayout,
|
||||||
|
...BIcons
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
friends: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
username() {
|
||||||
|
return this.$route.params.username
|
||||||
|
},
|
||||||
|
inventory_items() {
|
||||||
|
return this.local_items.concat(this.eleon_items)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['getFriends', "getFriendServer"]),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getFriends().then((friends) => {
|
||||||
|
friends.map((friend) => {
|
||||||
|
this.getFriendServer({username: friend}).then((server) => {
|
||||||
|
this.friends.push({
|
||||||
|
name: friend,
|
||||||
|
server: server
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
41
frontend/src/views/Index.vue
Normal file
41
frontend/src/views/Index.vue
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<main class="content">
|
||||||
|
<div class="container-fluid p-0">
|
||||||
|
<h1 class="h3 mb-3">Blank Page</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Empty card</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="logo">
|
||||||
|
<img src="/src/assets/icons/toolshed-48x48.png" alt="Toolshed logo">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapGetters, mapMutations} from 'vuex';
|
||||||
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
|
import BaseLayout from "@/components/BaseLayout.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Index',
|
||||||
|
components: {
|
||||||
|
...BIcons,
|
||||||
|
BaseLayout
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
93
frontend/src/views/Inventory.vue
Normal file
93
frontend/src/views/Inventory.vue
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<main class="content">
|
||||||
|
<div class="container-fluid p-0">
|
||||||
|
<h1 class="h3 mb-3">Inventory Own & Friends"</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-xl-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title">Foo</h5>
|
||||||
|
<h6 class="card-subtitle text-muted">Bar <code>baz</code>.</h6>
|
||||||
|
</div>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:40%;">Name</th>
|
||||||
|
<th style="width:25%">Owner</th>
|
||||||
|
<th class="d-none d-md-table-cell" style="width:25%">Amount</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in inventory_items" :key="item.id">
|
||||||
|
<td>
|
||||||
|
<router-link :to="`/inventory/${item.id}`">{{ item.name }}</router-link>
|
||||||
|
</td>
|
||||||
|
<td>{{ item.owner }}</td>
|
||||||
|
<td class="d-none d-md-table-cell">{{ item.owned_amount }}</td>
|
||||||
|
<td class="table-action">
|
||||||
|
<router-link :to="`/inventory/${item.id}/edit`">
|
||||||
|
<b-icon-pencil-square></b-icon-pencil-square>
|
||||||
|
</router-link>
|
||||||
|
<a :href="`/inventory/${item.id}/delete`" @click.prevent="deleteItem(item.id)">
|
||||||
|
<b-icon-trash></b-icon-trash>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<button class="btn" @click="getInventoryItems">Refresh</button>
|
||||||
|
<router-link to="/inventory/new" class="btn btn-primary">Add</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapActions, mapGetters, mapMutations, mapState} from "vuex";
|
||||||
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
|
import BaseLayout from "@/components/BaseLayout.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Inventory",
|
||||||
|
components: {
|
||||||
|
BaseLayout,
|
||||||
|
...BIcons
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(["inventory_items"]),
|
||||||
|
username() {
|
||||||
|
return this.$route.params.username
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(["apiFederatedGet", "getFriends", "getFriendServer"]),
|
||||||
|
...mapMutations(["setInventoryItems"]),
|
||||||
|
async getInventoryItems() {
|
||||||
|
try {
|
||||||
|
const servers = await this.getFriends().then(friends => friends.map(friend => this.getFriendServer({username: friend})))
|
||||||
|
const urls = servers.map(server => server.then(s => `http://${s}/api/inventory_items/`))
|
||||||
|
urls.map(url => url.then(u => this.apiFederatedGet(u).then(items => {
|
||||||
|
this.setInventoryItems({url: u, items})
|
||||||
|
}).catch(e => {
|
||||||
|
}))) // TODO: handle error
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
await this.getInventoryItems()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
65
frontend/src/views/InventoryDetail.vue
Normal file
65
frontend/src/views/InventoryDetail.vue
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BaseLayout :username="username">
|
||||||
|
<main class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Inventory</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Item</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
<th>Owner</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in inventory_items" :key="item.id">
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td>{{ item.quantity }}</td>
|
||||||
|
<td>{{ item.owner }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="#">
|
||||||
|
<b-icon-pencil-square></b-icon-pencil-square>
|
||||||
|
</a>
|
||||||
|
<a href="#">
|
||||||
|
<b-icon-trash></b-icon-trash>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<button class="btn" @click="getInventoryItems">Refresh</button>
|
||||||
|
<router-link to="/inventory/new" class="btn btn-primary">Add</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
|
import BaseLayout from "@/components/BaseLayout.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "InventoryDetail",
|
||||||
|
components: {
|
||||||
|
BaseLayout,
|
||||||
|
...BIcons
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
65
frontend/src/views/InventoryEdit.vue
Normal file
65
frontend/src/views/InventoryEdit.vue
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BaseLayout :username="username">
|
||||||
|
<main class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Inventory</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Item</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
<th>Owner</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in inventory_items" :key="item.id">
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td>{{ item.quantity }}</td>
|
||||||
|
<td>{{ item.owner }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="#">
|
||||||
|
<b-icon-pencil-square></b-icon-pencil-square>
|
||||||
|
</a>
|
||||||
|
<a href="#">
|
||||||
|
<b-icon-trash></b-icon-trash>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<button class="btn" @click="getInventoryItems">Refresh</button>
|
||||||
|
<router-link to="/inventory/new" class="btn btn-primary">Add</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
|
import BaseLayout from "@/components/BaseLayout.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "InventoryEdit",
|
||||||
|
components: {
|
||||||
|
BaseLayout,
|
||||||
|
...BIcons
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
65
frontend/src/views/InventoryNew.vue
Normal file
65
frontend/src/views/InventoryNew.vue
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BaseLayout :username="username">
|
||||||
|
<main class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Inventory</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Item</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
<th>Owner</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in inventory_items" :key="item.id">
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td>{{ item.quantity }}</td>
|
||||||
|
<td>{{ item.owner }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="#">
|
||||||
|
<b-icon-pencil-square></b-icon-pencil-square>
|
||||||
|
</a>
|
||||||
|
<a href="#">
|
||||||
|
<b-icon-trash></b-icon-trash>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<button class="btn" @click="getInventoryItems">Refresh</button>
|
||||||
|
<router-link to="/inventory/new" class="btn btn-primary">Add</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
|
import BaseLayout from "@/components/BaseLayout.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "InventoryNew",
|
||||||
|
components: {
|
||||||
|
BaseLayout,
|
||||||
|
...BIcons
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
97
frontend/src/views/Login.vue
Normal file
97
frontend/src/views/Login.vue
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
<template>
|
||||||
|
<main class="d-flex w-100">
|
||||||
|
<div class="container d-flex flex-column">
|
||||||
|
<div class="row vh-100">
|
||||||
|
<div class="col-sm-10 col-md-8 col-lg-6 mx-auto d-table h-100">
|
||||||
|
<div class="d-table-cell align-middle">
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<h1 class="h2">
|
||||||
|
Toolshed
|
||||||
|
</h1>
|
||||||
|
<p class="lead" v-if="msg">
|
||||||
|
{{ msg }}
|
||||||
|
</p>
|
||||||
|
<p class="lead" v-else>
|
||||||
|
Sign in to your account to continue
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="m-sm-4">
|
||||||
|
<form role="form" @submit.prevent="do_login">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Username</label>
|
||||||
|
<input class="form-control form-control-lg" type="text"
|
||||||
|
name="username" placeholder="Enter your username"
|
||||||
|
v-model="username"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Password</label>
|
||||||
|
<input class="form-control form-control-lg" type="password"
|
||||||
|
name="password" placeholder="Enter your password"
|
||||||
|
v-model="password"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" value="remember-me"
|
||||||
|
name="remember-me" checked v-model="remember"
|
||||||
|
@change="setRemember(remember)">
|
||||||
|
<span class="form-check-label">
|
||||||
|
Remember me next time
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<button type="submit" name="login" class="btn btn-lg btn-primary">Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<br/>
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="mb-0 text-muted">
|
||||||
|
Don’t have an account?
|
||||||
|
<router-link to="/register">Sign up</router-link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapActions, mapMutations} from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Login',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
msg: 'Welcome to Your Vue.js App',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
remember: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['login']),
|
||||||
|
...mapMutations(['setRemember']),
|
||||||
|
async do_login(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!await this.login({username: this.username, password: this.password, remember: this.remember})) {
|
||||||
|
this.msg = 'Invalid username or password';
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
257
frontend/src/views/Profile.vue
Normal file
257
frontend/src/views/Profile.vue
Normal file
|
@ -0,0 +1,257 @@
|
||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<main class="content">
|
||||||
|
<div class="container-fluid p-0">
|
||||||
|
|
||||||
|
<h1 class="h3 mb-3">Profile</h1>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 col-xl-3">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Profile Details</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<!--<img src="/static/assets/img/avatars/avatar.png"
|
||||||
|
alt="Christina Mason" class="img-fluid rounded-circle mb-2" width="128"
|
||||||
|
height="128"/>-->
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
{{ user.username }}
|
||||||
|
</h5>
|
||||||
|
<div class="text-muted mb-2">
|
||||||
|
{{ user.email }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a class="btn btn-primary btn-sm" href="#">Follow</a>
|
||||||
|
<a class="btn btn-primary btn-sm" href="#"><span
|
||||||
|
data-feather="message-square"></span> Message</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--{% if user.bio %}-->
|
||||||
|
<hr class="my-0"/>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="h6 card-title">Bio</h5>
|
||||||
|
<div class="text-muted mb-2">
|
||||||
|
{{ user.bio }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--{% endif %}-->
|
||||||
|
<hr class="my-0"/>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="h6 card-title">Skills</h5>
|
||||||
|
<a href="#" class="badge bg-primary mr-1 my-1">HTML</a>
|
||||||
|
<a href="#" class="badge bg-primary mr-1 my-1">JavaScript</a>
|
||||||
|
<a href="#" class="badge bg-primary mr-1 my-1">Sass</a>
|
||||||
|
<a href="#" class="badge bg-primary mr-1 my-1">Angular</a>
|
||||||
|
<a href="#" class="badge bg-primary mr-1 my-1">Vue</a>
|
||||||
|
<a href="#" class="badge bg-primary mr-1 my-1">React</a>
|
||||||
|
<a href="#" class="badge bg-primary mr-1 my-1">Redux</a>
|
||||||
|
<a href="#" class="badge bg-primary mr-1 my-1">UI</a>
|
||||||
|
<a href="#" class="badge bg-primary mr-1 my-1">UX</a>
|
||||||
|
</div>
|
||||||
|
<hr class="my-0"/>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="h6 card-title">About</h5>
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
<!--{% if user.location %}-->
|
||||||
|
<li class="mb-1"><span data-feather="home" class="feather-sm mr-1"></span> Lives
|
||||||
|
in <a href="#">{{ user.location }}</a></li>
|
||||||
|
<!--{% endif %}-->
|
||||||
|
|
||||||
|
<li class="mb-1"><span data-feather="briefcase" class="feather-sm mr-1"></span>
|
||||||
|
Works at <a href="#">GitHub</a></li>
|
||||||
|
<li class="mb-1"><span data-feather="map-pin" class="feather-sm mr-1"></span>
|
||||||
|
From <a href="#">Boston</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<hr class="my-0"/>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="h6 card-title">Elsewhere</h5>
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
<li class="mb-1"><span class="fas fa-globe fa-fw mr-1"></span> <a href="#">staciehall.co</a>
|
||||||
|
</li>
|
||||||
|
<li class="mb-1"><span class="fab fa-twitter fa-fw mr-1"></span> <a
|
||||||
|
href="#">Twitter</a>
|
||||||
|
</li>
|
||||||
|
<li class="mb-1"><span class="fab fa-facebook fa-fw mr-1"></span> <a href="#">Facebook</a>
|
||||||
|
</li>
|
||||||
|
<li class="mb-1"><span class="fab fa-instagram fa-fw mr-1"></span> <a href="#">Instagram</a>
|
||||||
|
</li>
|
||||||
|
<li class="mb-1"><span class="fab fa-linkedin fa-fw mr-1"></span> <a href="#">LinkedIn</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-8 col-xl-9">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
|
||||||
|
<h5 class="card-title mb-0">Activities</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body h-100">
|
||||||
|
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<!--<img src="/static/assets/img/avatars/avatar-5.png" width="36" height="36"
|
||||||
|
class="rounded-circle mr-2" alt="Vanessa Tucker">-->
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<small class="float-right text-navy">5m ago</small>
|
||||||
|
<strong>Vanessa Tucker</strong> started following <strong>Christina
|
||||||
|
Mason</strong><br/>
|
||||||
|
<small class="text-muted">Today 7:51 pm</small><br/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<!--<img src="/static/assets/img/avatars/avatar.png" width="36" height="36"
|
||||||
|
class="rounded-circle mr-2" alt="Charles Hall">-->
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<small class="float-right text-navy">30m ago</small>
|
||||||
|
<strong>Charles Hall</strong> posted something on <strong>Christina
|
||||||
|
Mason</strong>'s timeline<br/>
|
||||||
|
<small class="text-muted">Today 7:21 pm</small>
|
||||||
|
|
||||||
|
<div class="border text-sm text-muted p-2 mt-1">
|
||||||
|
Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem
|
||||||
|
quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam
|
||||||
|
nunc, blandit vel, luctus
|
||||||
|
pulvinar, hendrerit id, lorem. Maecenas nec odio et ante tincidunt
|
||||||
|
tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis
|
||||||
|
ante.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="#" class="btn btn-sm btn-danger mt-1"><i class="feather-sm"
|
||||||
|
data-feather="heart"></i>
|
||||||
|
Like</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<!--<img src="/static/assets/img/avatars/avatar-4.png" width="36" height="36"
|
||||||
|
class="rounded-circle mr-2" alt="Christina Mason">-->
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<small class="float-right text-navy">1h ago</small>
|
||||||
|
<strong>Christina Mason</strong> posted a new blog<br/>
|
||||||
|
|
||||||
|
<small class="text-muted">Today 6:35 pm</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<!--<img src="/static/assets/img/avatars/avatar-2.png" width="36" height="36"
|
||||||
|
class="rounded-circle mr-2" alt="William Harris">-->
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<small class="float-right text-navy">3h ago</small>
|
||||||
|
<strong>William Harris</strong> posted two photos on <strong>Christina
|
||||||
|
Mason</strong>'s timeline<br/>
|
||||||
|
<small class="text-muted">Today 5:12 pm</small>
|
||||||
|
|
||||||
|
<div class="row g-0 mt-1">
|
||||||
|
<div class="col-6 col-md-4 col-lg-4 col-xl-3">
|
||||||
|
<!--<img src="/static/assets/img/photos/unsplash-1.jpg"
|
||||||
|
class="img-fluid pr-2" alt="Unsplash">-->
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-md-4 col-lg-4 col-xl-3">
|
||||||
|
<!--<img src="/static/assets/img/photos/unsplash-2.jpg"
|
||||||
|
class="img-fluid pr-2" alt="Unsplash">-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="#" class="btn btn-sm btn-danger mt-1"><i class="feather-sm"
|
||||||
|
data-feather="heart"></i>
|
||||||
|
Like</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<!--<img src="/static/assets/img/avatars/avatar-2.png" width="36" height="36"
|
||||||
|
class="rounded-circle mr-2" alt="William Harris">-->
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<small class="float-right text-navy">1d ago</small>
|
||||||
|
<strong>William Harris</strong> started following <strong>Christina
|
||||||
|
Mason</strong><br/>
|
||||||
|
<small class="text-muted">Yesterday 3:12 pm</small>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-start mt-1">
|
||||||
|
<a class="pr-3" href="#">
|
||||||
|
<!--<img src="/static/assets/img/avatars/avatar-4.png" width="36"
|
||||||
|
height="36" class="rounded-circle mr-2" alt="Christina Mason">-->
|
||||||
|
</a>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="border text-sm text-muted p-2 mt-1">
|
||||||
|
Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id,
|
||||||
|
lorem. Maecenas nec odio et ante tincidunt tempus.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<!--<img src="/static/assets/img/avatars/avatar-4.png" width="36" height="36"
|
||||||
|
class="rounded-circle mr-2" alt="Christina Mason">-->
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<small class="float-right text-navy">1d ago</small>
|
||||||
|
<strong>Christina Mason</strong> posted a new blog<br/>
|
||||||
|
<small class="text-muted">Yesterday 2:43 pm</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<!--<img src="/static/assets/img/avatars/avatar.png" width="36" height="36"
|
||||||
|
class="rounded-circle mr-2" alt="Charles Hall">-->
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<small class="float-right text-navy">1d ago</small>
|
||||||
|
<strong>Charles Hall</strong> started following <strong>Christina
|
||||||
|
Mason</strong><br/>
|
||||||
|
<small class="text-muted">Yesterdag 1:51 pm</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
<a href="#" class="btn btn-primary btn-block">Load more</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BaseLayout from "@/components/BaseLayout.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Profile',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
user: {
|
||||||
|
name: 'John Doe',
|
||||||
|
avatar: '/static/assets/img/avatars/avatar.png',
|
||||||
|
cover: '/static/assets/img/photos/unsplash-1.jpg',
|
||||||
|
occupation: 'Frontend Developer',
|
||||||
|
company: 'Facebook Inc.',
|
||||||
|
email: 'foo@bar.com',
|
||||||
|
phone: '+12 345 678 001',
|
||||||
|
address: 'Boulevard of Broken Dreams, 1234',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {BaseLayout},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
88
frontend/src/views/Register.vue
Normal file
88
frontend/src/views/Register.vue
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<template>
|
||||||
|
<main class="d-flex w-100">
|
||||||
|
<div class="container d-flex flex-column">
|
||||||
|
<div class="row vh-100">
|
||||||
|
<div class="col-sm-10 col-md-8 col-lg-6 mx-auto d-table h-100">
|
||||||
|
<div class="d-table-cell align-middle">
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<h1 class="h2">
|
||||||
|
Toolshed
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p class="lead" v-if="msg">
|
||||||
|
{{ msg }}
|
||||||
|
</p>
|
||||||
|
<p class="lead" v-else>
|
||||||
|
Create an account to get started
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="m-sm-4">
|
||||||
|
<form role="form" method="post" action="">
|
||||||
|
<!--{% csrf_token %}-->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Username</label>
|
||||||
|
<input class="form-control form-control-lg" type="text"
|
||||||
|
name="username" placeholder="Enter your username"/>
|
||||||
|
</div>
|
||||||
|
<span class="text-error">{{ form.username.errors }}</span>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Email</label>
|
||||||
|
<input class="form-control form-control-lg" type="email"
|
||||||
|
name="email" placeholder="Enter your email"/>
|
||||||
|
</div>
|
||||||
|
<span class="text-error">{{ form.email.errors }}</span>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Password</label>
|
||||||
|
<input class="form-control form-control-lg" type="password"
|
||||||
|
name="password1" placeholder="Enter your password"/>
|
||||||
|
</div>
|
||||||
|
<span class="text-error">{{ form.password1.errors }}</span>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Password Check</label>
|
||||||
|
<input class="form-control form-control-lg" type="password"
|
||||||
|
name="password2" placeholder="Enter your password again"/>
|
||||||
|
</div>
|
||||||
|
<span class="text-error">{{ form.password2.errors }}</span>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<button type="submit" name="register" class="btn btn-lg btn-primary">
|
||||||
|
Register
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<br/>
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="mb-0 text-muted">
|
||||||
|
Already have an account? <router-link to="/login">Login</router-link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Register',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
msg: 'Register',
|
||||||
|
form: {
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
password1: '',
|
||||||
|
password2: '',}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
209
frontend/src/views/Settings.vue
Normal file
209
frontend/src/views/Settings.vue
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<main class="content">
|
||||||
|
<div class="container-fluid p-0">
|
||||||
|
|
||||||
|
<h1 class="h3 mb-3">Settings</h1>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 col-xl-2">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Profile Settings</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-group list-group-flush" role="tablist">
|
||||||
|
<a class="list-group-item list-group-item-action active" data-toggle="list"
|
||||||
|
href="#account" role="tab">
|
||||||
|
Account
|
||||||
|
</a>
|
||||||
|
<a class="list-group-item list-group-item-action" data-toggle="list"
|
||||||
|
href="#password" role="tab">
|
||||||
|
Password
|
||||||
|
</a>
|
||||||
|
<a class="list-group-item list-group-item-action" data-toggle="list" href="#"
|
||||||
|
role="tab">
|
||||||
|
Privacy and safety
|
||||||
|
</a>
|
||||||
|
<a class="list-group-item list-group-item-action" data-toggle="list" href="#"
|
||||||
|
role="tab">
|
||||||
|
Email notifications
|
||||||
|
</a>
|
||||||
|
<a class="list-group-item list-group-item-action" data-toggle="list" href="#"
|
||||||
|
role="tab">
|
||||||
|
Web notifications
|
||||||
|
</a>
|
||||||
|
<a class="list-group-item list-group-item-action" data-toggle="list" href="#"
|
||||||
|
role="tab">
|
||||||
|
Widgets
|
||||||
|
</a>
|
||||||
|
<a class="list-group-item list-group-item-action" data-toggle="list" href="#"
|
||||||
|
role="tab">
|
||||||
|
Your data
|
||||||
|
</a>
|
||||||
|
<a class="list-group-item list-group-item-action" data-toggle="list" href="#"
|
||||||
|
role="tab">
|
||||||
|
Delete account
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-9 col-xl-10">
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="tab-pane fade show active" id="account" role="tabpanel">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
|
||||||
|
<h5 class="card-title mb-0">Public info</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"
|
||||||
|
for="inputUsername">Username</label>
|
||||||
|
<input type="text" class="form-control" id="inputUsername"
|
||||||
|
placeholder="Username">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"
|
||||||
|
for="inputUsername">Biography</label>
|
||||||
|
<textarea rows="2" class="form-control" id="inputBio"
|
||||||
|
placeholder="Tell something about yourself"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="text-center">
|
||||||
|
<img alt="Charles Hall"
|
||||||
|
src="/static/assets/img/avatars/avatar.png"
|
||||||
|
class="rounded-circle img-responsive mt-2" width="128"
|
||||||
|
height="128"/>
|
||||||
|
<div class="mt-2">
|
||||||
|
<span class="btn btn-primary"><i
|
||||||
|
class="fas fa-upload"></i> Upload</span>
|
||||||
|
</div>
|
||||||
|
<small>For best results, use an image at least 128px by
|
||||||
|
128px in .jpg format</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
|
||||||
|
<h5 class="card-title mb-0">Private info</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form>
|
||||||
|
<div class="row">
|
||||||
|
<div class="mb-3 col-md-6">
|
||||||
|
<label class="form-label" for="inputFirstName">First
|
||||||
|
name</label>
|
||||||
|
<input type="text" class="form-control" id="inputFirstName"
|
||||||
|
placeholder="First name">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 col-md-6">
|
||||||
|
<label class="form-label" for="inputLastName">Last name</label>
|
||||||
|
<input type="text" class="form-control" id="inputLastName"
|
||||||
|
placeholder="Last name">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="inputEmail4">Email</label>
|
||||||
|
<input type="email" class="form-control" id="inputEmail4"
|
||||||
|
placeholder="Email">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="inputAddress">Address</label>
|
||||||
|
<input type="text" class="form-control" id="inputAddress"
|
||||||
|
placeholder="1234 Main St">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="inputAddress2">Address 2</label>
|
||||||
|
<input type="text" class="form-control" id="inputAddress2"
|
||||||
|
placeholder="Apartment, studio, or floor">
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="mb-3 col-md-6">
|
||||||
|
<label class="form-label" for="inputCity">City</label>
|
||||||
|
<input type="text" class="form-control" id="inputCity">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 col-md-4">
|
||||||
|
<label class="form-label" for="inputState">State</label>
|
||||||
|
<select id="inputState" class="form-control">
|
||||||
|
<option selected>Choose...</option>
|
||||||
|
<option>...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 col-md-2">
|
||||||
|
<label class="form-label" for="inputZip">Zip</label>
|
||||||
|
<input type="text" class="form-control" id="inputZip">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="password" role="tabpanel">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Password</h5>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="inputPasswordCurrent">Current
|
||||||
|
password</label>
|
||||||
|
<input type="password" class="form-control"
|
||||||
|
id="inputPasswordCurrent">
|
||||||
|
<small><a href="#">Forgot your password?</a></small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="inputPasswordNew">New
|
||||||
|
password</label>
|
||||||
|
<input type="password" class="form-control" id="inputPasswordNew">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="inputPasswordNew2">Verify
|
||||||
|
password</label>
|
||||||
|
<input type="password" class="form-control" id="inputPasswordNew2">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BaseLayout from "@/components/BaseLayout.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Settings',
|
||||||
|
components: {BaseLayout},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
28
frontend/vite.config.js
Normal file
28
frontend/vite.config.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import {fileURLToPath, URL} from 'node:url'
|
||||||
|
|
||||||
|
import {defineConfig} from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: true,
|
||||||
|
cors: true,
|
||||||
|
headers: {
|
||||||
|
//allow all origins
|
||||||
|
//'Access-Control-Allow-Origin': 'http://10.23.42.128:8000, http://10.23.42.168:8000',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Origin, Content-Type, X-Auth-Token, Authorization, Accept,charset,boundary,Content-Length',
|
||||||
|
'Access-Control-Allow-Credentials': 'true'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
Loading…
Reference in a new issue