stash
This commit is contained in:
parent
e89c261f56
commit
a5ca19ddf8
5 changed files with 20968 additions and 13 deletions
20695
frontend/src/assets/css/toolshed.css
Normal file
20695
frontend/src/assets/css/toolshed.css
Normal file
File diff suppressed because it is too large
Load diff
|
@ -6,9 +6,129 @@
|
|||
<a class="sidebar-toggle d-flex" @click="toggleSidebar">
|
||||
<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">
|
||||
<UserDropdown/>
|
||||
<Notifications :notifications="notifications"/>
|
||||
<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>
|
||||
|
@ -19,32 +139,49 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
/* global closeAllDropdowns */
|
||||
import {mapGetters, mapMutations, mapState} from 'vuex';
|
||||
import * as BIcons from "bootstrap-icons-vue";
|
||||
import Footer from "@/components/Footer.vue";
|
||||
import Sidebar from "@/components/Sidebar.vue";
|
||||
import UserDropdown from "@/components/UserDropdown.vue";
|
||||
import Notifications from "@/components/Notifications.vue";
|
||||
|
||||
export default {
|
||||
name: 'BaseLayout',
|
||||
components: {
|
||||
UserDropdown,
|
||||
Footer,
|
||||
Sidebar
|
||||
Sidebar,
|
||||
Notifications,
|
||||
...BIcons
|
||||
},
|
||||
props: {
|
||||
hideSearch: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
//...mapState(['notifications']),
|
||||
...mapGetters(['notifications']),
|
||||
username() {
|
||||
return this.$route.params.username
|
||||
},
|
||||
},
|
||||
top_notifications() {
|
||||
return this.notifications.slice(0, 5)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleSidebar() {
|
||||
closeAllDropdowns();
|
||||
document.getElementById("sidebar").classList.toggle("collapsed");
|
||||
},
|
||||
...mapMutations(['logout'])
|
||||
},
|
||||
async mounted() {
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -76,10 +213,6 @@ export default {
|
|||
box-shadow: 0 0 2rem var(--bs-shadow)
|
||||
}
|
||||
|
||||
.navbar-align {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.navbar-bg {
|
||||
background: var(--bs-white);
|
||||
}
|
||||
|
|
90
frontend/src/components/Notifications.vue
Normal file
90
frontend/src/components/Notifications.vue
Normal file
|
@ -0,0 +1,90 @@
|
|||
<template>
|
||||
<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', notificationsColor(top_notifications)]">{{
|
||||
top_notifications.length
|
||||
}}</span>
|
||||
</div>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right py-0"
|
||||
aria-labelledby="alertsDropdown">
|
||||
<div class="dropdown-menu-header">
|
||||
{{ top_notifications.length }} New Notifications
|
||||
</div>
|
||||
<div class="list-group">
|
||||
<a href="#" class="list-group-item" v-for="notification in top_notifications" :key="notification.id">
|
||||
<div class="row g-0 align-items-center">
|
||||
<div class="col-2">
|
||||
<b-icon-exclamation-circle class="bi-valign-middle text-danger"
|
||||
v-if="notification.type === 'error'"></b-icon-exclamation-circle>
|
||||
<b-icon-info-circle class="bi-valign-middle text-info"
|
||||
v-else-if="notification.type === 'info'"></b-icon-info-circle>
|
||||
<b-icon-check-circle class="bi-valign-middle text-success"
|
||||
v-else-if="notification.type === 'success'"></b-icon-check-circle>
|
||||
<b-icon-bell class="bi-valign-middle text-warning"
|
||||
v-else-if="notification.type === 'warning'"></b-icon-bell>
|
||||
<b-icon-house class="bi-valign-middle text-primary"
|
||||
v-else-if="notification.type === 'login'"></b-icon-house>
|
||||
<b-icon-person-plus class="bi-valign-middle text-success"
|
||||
v-else-if="notification.type === 'friend'"></b-icon-person-plus>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<div class="text-dark">{{ notification.title }}</div>
|
||||
<div class="text-muted small mt-1" v-if="notification.msg">{{ notification.msg }}</div>
|
||||
<div class="text-muted small mt-1">{{ humanizeTime(notification.time) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="dropdown-menu-footer">
|
||||
<a href="#" class="text-muted">Show all notifications</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters, mapMutations} from 'vuex';
|
||||
import * as BIcons from "bootstrap-icons-vue";
|
||||
|
||||
export default {
|
||||
name: 'Notifications',
|
||||
components: {
|
||||
...BIcons
|
||||
},
|
||||
props: ['notifications'],
|
||||
computed: {
|
||||
top_notifications() {
|
||||
return this.notifications.sort((a, b) => {
|
||||
return new Date(b.time) - new Date(a.time);
|
||||
}).slice(0, 8);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([]),
|
||||
humanizeTime(time) {
|
||||
return moment(time).fromNow();
|
||||
},
|
||||
notificationsColor(notifications) {
|
||||
if (notifications.length === 0) {
|
||||
return 'invisible';
|
||||
}
|
||||
if (notifications.filter(n => n.type === 'error').length > 0) {
|
||||
return 'bg-danger';
|
||||
}
|
||||
if (notifications.filter(n => n.type === 'warning').length > 0) {
|
||||
return 'bg-warning';
|
||||
}
|
||||
return 'bg-primary';
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -120,7 +120,6 @@ export default createStore({
|
|||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
},
|
||||
async lookupServer({state}, {username}) {
|
||||
const domain = username.split('@')[1]
|
||||
|
@ -338,5 +337,41 @@ export default createStore({
|
|||
return acc.concat(items)
|
||||
}, [])
|
||||
},
|
||||
notifications(state) {
|
||||
// supported types: error, warning, info, login, success, friend
|
||||
const u = state.unreachable_neighbors.list().map(elem => {
|
||||
return {
|
||||
type: 'error',
|
||||
title: elem.domain + ' unreachable',
|
||||
msg: 'The neighbor ' + elem.domain + ' is currently unreachable. Please try again later.',
|
||||
time: elem.time
|
||||
}
|
||||
})
|
||||
return [...u, {
|
||||
type: 'info',
|
||||
title: 'Welcome to the Toolshed',
|
||||
msg: 'This is a federated social network. You can add friends from other servers and share items with them.',
|
||||
time: Date.now()
|
||||
}, {
|
||||
type: 'warning',
|
||||
title: 'Lorem ipsum',
|
||||
msg: 'Aliquam ex eros, imperdiet vulputate hendrerit et.',
|
||||
time: Date.now() - 1000 * 60 * 60 * 2
|
||||
}, {
|
||||
type: 'login',
|
||||
title: 'Login from 192.186.1.8',
|
||||
time: Date.now() - 1000 * 60 * 60 * 5
|
||||
}, {
|
||||
type: 'friend',
|
||||
title: 'New connection',
|
||||
msg: 'Christina accepted your request.',
|
||||
time: Date.now() - 1000 * 60 * 60 * 14
|
||||
}, {
|
||||
type: 'success',
|
||||
title: 'Lorem ipsum',
|
||||
msg: 'Aliquam ex eros, imperdiet vulputate hendrerit et.',
|
||||
time: Date.now() - 1000 * 60 * 60 * 24
|
||||
}]
|
||||
},
|
||||
}
|
||||
})
|
|
@ -115,9 +115,11 @@ export default {
|
|||
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/`))
|
||||
const urls = servers.map(server => server.then(s => {
|
||||
return {host: `http://${s}`, target: "/api/inventory_items/"}
|
||||
}))
|
||||
urls.map(url => url.then(u => this.apiFederatedGet(u).then(items => {
|
||||
this.setInventoryItems({url: u, items})
|
||||
this.setInventoryItems({url: u.domain, items})
|
||||
}).catch(e => {
|
||||
}))) // TODO: handle error
|
||||
} catch (e) {
|
||||
|
|
Loading…
Reference in a new issue