diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..38adffa --- /dev/null +++ b/frontend/.gitignore @@ -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? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..8d6c5ef --- /dev/null +++ b/frontend/README.md @@ -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 +``` diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..b72e945 --- /dev/null +++ b/frontend/index.html @@ -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> diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..6ab03b4 --- /dev/null +++ b/frontend/package.json @@ -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" + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..54ad06d --- /dev/null +++ b/frontend/src/App.vue @@ -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> diff --git a/frontend/src/components/BaseLayout.vue b/frontend/src/components/BaseLayout.vue new file mode 100644 index 0000000..b5ff611 --- /dev/null +++ b/frontend/src/components/BaseLayout.vue @@ -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> \ No newline at end of file diff --git a/frontend/src/dns.js b/frontend/src/dns.js new file mode 100644 index 0000000..70f2ba5 --- /dev/null +++ b/frontend/src/dns.js @@ -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; \ No newline at end of file diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..095e5e0 --- /dev/null +++ b/frontend/src/main.js @@ -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') +}); diff --git a/frontend/src/neigbors.js b/frontend/src/neigbors.js new file mode 100644 index 0000000..8a205c0 --- /dev/null +++ b/frontend/src/neigbors.js @@ -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; \ No newline at end of file diff --git a/frontend/src/router.js b/frontend/src/router.js new file mode 100644 index 0000000..75ea992 --- /dev/null +++ b/frontend/src/router.js @@ -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 \ No newline at end of file diff --git a/frontend/src/store.js b/frontend/src/store.js new file mode 100644 index 0000000..b3475fc --- /dev/null +++ b/frontend/src/store.js @@ -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) + }, []) + } + } +}) \ No newline at end of file diff --git a/frontend/src/views/Friends.vue b/frontend/src/views/Friends.vue new file mode 100644 index 0000000..3a21812 --- /dev/null +++ b/frontend/src/views/Friends.vue @@ -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> \ No newline at end of file diff --git a/frontend/src/views/Index.vue b/frontend/src/views/Index.vue new file mode 100644 index 0000000..4a5d040 --- /dev/null +++ b/frontend/src/views/Index.vue @@ -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> \ No newline at end of file diff --git a/frontend/src/views/Inventory.vue b/frontend/src/views/Inventory.vue new file mode 100644 index 0000000..bb94db5 --- /dev/null +++ b/frontend/src/views/Inventory.vue @@ -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> \ No newline at end of file diff --git a/frontend/src/views/InventoryDetail.vue b/frontend/src/views/InventoryDetail.vue new file mode 100644 index 0000000..d3bd892 --- /dev/null +++ b/frontend/src/views/InventoryDetail.vue @@ -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> \ No newline at end of file diff --git a/frontend/src/views/InventoryEdit.vue b/frontend/src/views/InventoryEdit.vue new file mode 100644 index 0000000..f054c83 --- /dev/null +++ b/frontend/src/views/InventoryEdit.vue @@ -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> \ No newline at end of file diff --git a/frontend/src/views/InventoryNew.vue b/frontend/src/views/InventoryNew.vue new file mode 100644 index 0000000..758761b --- /dev/null +++ b/frontend/src/views/InventoryNew.vue @@ -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> \ No newline at end of file diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 0000000..3686f6b --- /dev/null +++ b/frontend/src/views/Login.vue @@ -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> \ No newline at end of file diff --git a/frontend/src/views/Profile.vue b/frontend/src/views/Profile.vue new file mode 100644 index 0000000..50ecade --- /dev/null +++ b/frontend/src/views/Profile.vue @@ -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> \ No newline at end of file diff --git a/frontend/src/views/Register.vue b/frontend/src/views/Register.vue new file mode 100644 index 0000000..3396600 --- /dev/null +++ b/frontend/src/views/Register.vue @@ -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> \ No newline at end of file diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue new file mode 100644 index 0000000..d143c7e --- /dev/null +++ b/frontend/src/views/Settings.vue @@ -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> \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..40c16c4 --- /dev/null +++ b/frontend/vite.config.js @@ -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' + + + + } + } +})