Add page with newly uploaded photos

Refactor navbar
pull/10/head
Manuel Kasper 2021-03-07 16:34:43 +01:00
rodzic 73b6f88f90
commit b254f66a8f
10 zmienionych plików z 261 dodań i 128 usunięć

Wyświetl plik

@ -1,11 +1,13 @@
<template>
<b-dropdown v-if="authenticated" position="is-bottom-left" aria-role="menu">
<a class="navbar-item callsign" slot="trigger" role="button"><span>{{ $keycloak.tokenParsed.callsign ? $keycloak.tokenParsed.callsign : $keycloak.userName }}</span><b-icon icon="angle-down"></b-icon></a>
<template #trigger="{ active }">
<b-button class="callsign" :icon-right="active ? 'angle-up' : 'angle-down'" :label="$keycloak.tokenParsed.callsign ? $keycloak.tokenParsed.callsign : $keycloak.userName"></b-button>
</template>
<b-dropdown-item v-if="$keycloak.tokenParsed.callsign" has-link><router-link :to="'/activators/' + $keycloak.tokenParsed.callsign" @click.native="$emit('linkClicked')">My activator page</router-link></b-dropdown-item>
<b-dropdown-item @click="doAccountManagement">Manage account</b-dropdown-item>
<b-dropdown-item @click="doLogout">Logout</b-dropdown-item>
</b-dropdown>
<div v-else class="navbar-item">
<div v-else>
<b-button type="is-info" @click="doLogin">Login</b-button>
</div>
</template>
@ -55,7 +57,4 @@ export default {
.callsign {
font-weight: bold;
}
.dropdown-trigger .icon {
vertical-align: middle;
}
</style>

Wyświetl plik

@ -1,34 +1,30 @@
<template>
<nav class="navbar is-fixed-top">
<div class="container">
<div class="navbar-brand">
<div class="navbar-item">
<router-link to="/about"><img src="../assets/sotlas.svg" alt="Logo"></router-link>
</div>
<div class="navbar-item clock">
<font-awesome-icon :icon="['far', 'clock']" class="faicon" /> {{ clock }}
</div>
<a role="button" :class="{ 'navbar-burger': true, 'burger': true, 'is-active': burgerActive }" aria-label="menu" aria-expanded="false" data-target="navbarMenu" @click="burgerClicked">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbarMenu" :class="{ 'navbar-menu': true, 'is-active': burgerActive }">
<div class="navbar-end">
<div class="navbar-item">
<SearchField :query="query" @search="closeBurger" />
</div>
<router-link v-for="link in links" :key="link.target" :to="link.target" :class="linkClass(link)" :title="link.title" @click.native="closeBurger">
<b-icon v-if="link.icon" :pack="link.iconPack" :icon="link.icon" />
{{ link.text }}
<span v-if="!$mq.desktop">{{ link.mobileText }}</span>
</router-link>
<LoginButton @linkClicked="closeBurger" />
</div>
</div>
</div>
</nav>
<b-navbar wrapper-class="container" :fixed-top="true" :active.sync="burgerActive">
<template #brand>
<b-navbar-item tag="router-link" to="/about"><img src="../assets/sotlas.svg" alt="Logo"></b-navbar-item>
<b-navbar-item class="clock" tag="div">
<font-awesome-icon :icon="['far', 'clock']" class="faicon" /> {{ clock }}
</b-navbar-item>
</template>
<template #end>
<b-navbar-item tag="div">
<SearchField :query="query" @search="closeBurger" />
</b-navbar-item>
<b-navbar-item v-for="link in links" tag="router-link" :key="link.target" :to="link.target" :title="link.title" @click.native="closeBurger">
<b-icon v-if="link.icon" :pack="link.iconPack" :icon="link.icon" />
{{ link.text }}
</b-navbar-item>
<b-navbar-dropdown label="More">
<b-navbar-item v-for="link in moreLinks" tag="router-link" :key="link.target" :to="link.target" :title="link.title" @click.native="closeBurger">
<b-icon v-if="link.icon" :pack="link.iconPack" :icon="link.icon" />
{{ link.text }}
</b-navbar-item>
</b-navbar-dropdown>
<b-navbar-item tag="div">
<LoginButton @linkClicked="closeBurger" />
</b-navbar-item>
</template>
</b-navbar>
</template>
<script>
@ -65,16 +61,6 @@ export default {
clearInterval(this.clockInterval)
},
methods: {
linkClass (link) {
let classes = { 'navbar-item': true }
if (this.$route.path.startsWith(link.target)) {
classes['is-active'] = true
}
return classes
},
burgerClicked () {
this.burgerActive = !this.burgerActive
},
closeBurger () {
this.burgerActive = false
},
@ -107,6 +93,14 @@ export default {
{
target: '/alerts',
text: 'Alerts'
}
]
},
moreLinks () {
return [
{
target: '/new_photos',
text: 'New Photos'
},
{
target: '/activators',
@ -114,7 +108,7 @@ export default {
},
{
target: '/settings',
mobileText: 'Settings',
text: 'Settings',
title: 'Settings',
icon: 'cog',
iconPack: 'fas'
@ -151,9 +145,13 @@ export default {
font-size: 1rem !important;
}
}
a.navbar-item.is-active:not(:focus):not(:hover), .navbar-link.is-active:not(:focus):not(:hover) {
.router-link-active:not(:focus):not(:hover) {
background-color: whitesmoke;
}
.navbar-item .icon {
margin-right: 0.25em !important;
vertical-align: middle;
}
.clock {
opacity: 0.7;
font-size: 1rem;
@ -161,10 +159,4 @@ a.navbar-item.is-active:not(:focus):not(:hover), .navbar-link.is-active:not(:foc
.clock .faicon {
margin-right: 0.3em;
}
.navbar-brand .navbar-item {
line-height: 1;
}
.navbar-menu .navbar-item span {
vertical-align: middle;
}
</style>

Wyświetl plik

@ -1,5 +1,5 @@
<template>
<file-pond name="photo" ref="filePond" :label-idle="labelIdle" :allow-multiple="true" :allow-replace="false" :allow-revert="false" :allow-paste="false" accepted-file-types="image/jpeg, image/png, image/heic" :server="uploadServer()" @processfile="onProcessFile" />
<file-pond name="photo" ref="filePond" class-name="box" :label-idle="labelIdle" :allow-multiple="true" :allow-replace="false" :allow-revert="false" :allow-paste="false" accepted-file-types="image/jpeg, image/png, image/heic" :server="uploadServer()" @processfile="onProcessFile" />
</template>
<script>
@ -92,6 +92,6 @@ export default {
margin-bottom: 0;
}
>>> .filepond--panel-root {
background-color: #f7f7f7;
background-color: #f7f7f7;
}
</style>

Wyświetl plik

@ -1,7 +1,16 @@
<template>
<div>
<div v-for="group in groups" :key="group.key">
<SummitPhotosGroup ref="photosGroup" :photos="group.photos" :title="group.title" :titleLink="group.titleLink" :summit="summit" @editPhoto="onEditPhoto" @deletePhoto="onDeletePhoto" @reorderPhotos="onReorderPhotos" />
<SummitPhotosGroup ref="photosGroup" :photos="group.photos" :title="group.title" :titleLink="group.titleLink" :summit="summit" :editable="editable" :showSummitName="showSummitName" :showWaypointButton="showWaypointButton" @editPhoto="onEditPhoto" @deletePhoto="onDeletePhoto" @reorderPhotos="onReorderPhotos">
<template v-slot:title>
<template v-if="showSummitName">
<router-link :to="group.titleLink">{{ group.title }}</router-link><span class="has-text-weight-normal"> on </span><router-link :to="'/summits/' + summit.code">{{ summit.name }} <span class="has-text-weight-normal">(<AltitudeLabel :altitude="summit.altitude" />, {{ summit.code }})</span></router-link>
</template>
<template v-else>
<router-link :to="group.titleLink">{{ group.title }}</router-link>
</template>
</template>
</SummitPhotosGroup>
</div>
<b-modal :active.sync="isEditorActive" has-modal-card trap-focus aria-role="dialog" aria-modal>
<EditPhoto v-if="editingPhoto" :photo="editingPhoto" :summitCode="summit.code" @photoEdited="$emit('photoEdited')" />
@ -12,6 +21,7 @@
<script>
import SummitPhotosGroup from './SummitPhotosGroup.vue'
import EditPhoto from './EditPhoto.vue'
import AltitudeLabel from './AltitudeLabel.vue'
import utils from '../mixins/utils.js'
import api from '../mixins/api.js'
import moment from 'moment'
@ -19,10 +29,14 @@ import moment from 'moment'
export default {
name: 'SummitPhotos',
props: {
summit: Object
summit: Object,
minDate: Date,
editable: Boolean,
showSummitName: Boolean,
showWaypointButton: Boolean
},
components: {
SummitPhotosGroup, EditPhoto
SummitPhotosGroup, EditPhoto, AltitudeLabel
},
mixins: [utils, api],
computed: {
@ -33,7 +47,13 @@ export default {
// Group photos by author
let authorGroups = new Map()
this.summit.photos.forEach(photo => {
let summitPhotos = this.summit.photos
if (this.minDate) {
summitPhotos = summitPhotos.filter(photo => {
return moment(photo.uploadDate).isSameOrAfter(moment(this.minDate))
})
}
summitPhotos.forEach(photo => {
let authorGroup = authorGroups.get(photo.author)
if (!authorGroup) {
authorGroup = {

Wyświetl plik

@ -1,11 +1,10 @@
<template>
<div class="photo-group">
<div class="photo-group box">
<div class="photo-group-title">
<router-link v-if="titleLink" :to="titleLink">{{ title }}</router-link>
<span v-else>{{ title }}</span>
<slot name="title"></slot>
</div>
<PictureSwipe ref="pictureSwipe" class="photos" :items="swipeItems" :options="swipeOptions" @mouseoverPicture="mouseoverPicture" @mouseleavePicture="mouseleavePicture" @editPicture="onEditPicture" @deletePicture="onDeletePicture" @movePicture="onMovePicture" />
<b-button v-if="waypointPhotos.length > 0 && !$mq.mobile" class="waypoints-button" icon-left="file-download" size="is-small" @click="downloadWaypoints">Photo waypoints (.gpx)</b-button>
<b-button v-if="waypointPhotos.length > 0 && !$mq.mobile && showWaypointButton" class="waypoints-button" icon-left="file-download" size="is-small" @click="downloadWaypoints">Photo waypoints (.gpx)</b-button>
</div>
</template>
@ -20,9 +19,10 @@ export default {
name: 'SummitPhotosGroup',
props: {
photos: Array,
title: String,
titleLink: String,
summit: Object
summit: Object,
editable: Boolean,
showSummitName: Boolean,
showWaypointButton: Boolean
},
components: {
PictureSwipe
@ -51,7 +51,7 @@ export default {
h: largeH,
title: this.makeTitleHtml(photo),
thumbTitle: photo.title,
editable: (this.$keycloak && this.$keycloak.authenticated && this.$keycloak.tokenParsed.callsign === photo.author),
editable: (this.editable && this.$keycloak && this.$keycloak.authenticated && this.$keycloak.tokenParsed.callsign === photo.author),
filename: photo.filename
}
})
@ -172,14 +172,6 @@ export default {
max-height: 128px;
max-width: 256px;
}
@media (max-width: 768px) {
.photos >>> figure {
margin: 0 0.5rem 0.5rem 0;
}
.photos >>> figure img {
max-height: 104px;
}
}
>>> .photo-title {
font-size: 1rem;
}
@ -206,7 +198,19 @@ export default {
.photo-group-title a:hover {
color: #3273dc;
}
@media (max-width: 768px) {
.photos >>> figure {
margin: 0 0.5rem 0.5rem 0;
}
.photos >>> figure img {
max-height: 104px;
}
.photo-group {
padding: 0.25rem 0 0 0.5rem;
}
}
.waypoints-button {
margin-bottom: 0.75rem;
margin-right: 0.75rem;
}
</style>

Wyświetl plik

@ -9,7 +9,7 @@ import MatchMedia from 'vue-match-media/src'
import VueKeyCloak from '@dsb-norge/vue-keycloak-js'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCheck, faCheckCircle, faInfoCircle, faExclamationTriangle, faExclamationCircle, faArrowUp, faPlus, faCheckDouble,
faAngleRight, faAngleLeft, faAngleDown, faEye, faEyeSlash, faCaretUp, faUpload, faLink, faHistory, faThList, faImages,
faAngleRight, faAngleLeft, faAngleDown, faAngleUp, faEye, faEyeSlash, faCaretUp, faUpload, faLink, faHistory, faThList, faImages,
faQuoteRight, faSearch, faMountains, faUser, faClock, faChevronCircleUp, faChevronCircleDown, faChartBar, faFileDownload,
faExchange, faGlobe, faCalendarDay, faTrashAlt, faEdit, faClone, faCheckCircle as farCheckCircle, faArrowsH, faArrowsAlt,
faSnowflake, faWindowMinimize, faWindowMaximize, faWindowClose, faExpandArrows, faLocation, faCalendarCheck, faComment, faSpinner,
@ -24,7 +24,7 @@ import axios from 'axios'
import { SnackbarProgrammatic as Snackbar } from 'buefy/dist/components/snackbar'
library.add(faCheck, faCheckCircle, faInfoCircle, faExclamationTriangle, faExclamationCircle, faArrowUp, faPlus, faCheckDouble,
faAngleRight, faAngleLeft, faAngleDown, faEye, faEyeSlash, faCaretUp, faUpload, faLink, faHistory, faThList, faImages,
faAngleRight, faAngleLeft, faAngleDown, faAngleUp, faEye, faEyeSlash, faCaretUp, faUpload, faLink, faHistory, faThList, faImages,
faQuoteRight, faSearch, faMountains, faUser, faClock, faChevronCircleUp, faChevronCircleDown, faMap, faChartBar, faFileDownload,
faExchange, faGlobe, faCalendarDay, faTrashAlt, faEdit, faClone, farCheckCircle, faArrowsH, faArrowsAlt,
faSnowflake, faWindowMinimize, faWindowMaximize, faWindowClose, faExpandArrows, faLocation, faCalendarCheck, faComment, faSpinner,

Wyświetl plik

@ -16,6 +16,7 @@ import Spots from './views/Spots.vue'
import SotaSpots from './views/SotaSpots.vue'
import RBNSpots from './views/RBNSpots.vue'
import Alerts from './views/Alerts.vue'
import NewPhotos from './views/NewPhotos.vue'
Vue.use(Router)
@ -162,6 +163,10 @@ let router = new Router({
path: '/alerts',
component: Alerts
},
{
path: '/new_photos',
component: NewPhotos
},
{
path: '*',
component: NotFound,

Wyświetl plik

@ -0,0 +1,113 @@
<template>
<PageLayout>
<template v-slot:title>New Photos</template>
<template v-slot:title-right>
<b-field>
<b-dropdown class="control" v-model="selectedAssociations" multiple aria-role="list" :scrollable="$mq.desktop" @change="loadNewPhotos">
<b-button icon-right="angle-down" slot="trigger">
Associations {{ selectedAssociations.length > 0 ? ('(' + selectedAssociations.length + ')') : '' }}
</b-button>
<b-dropdown-item v-for="association in associations" :key="association.code" :value="association.code" aria-role="listitem">
{{ association.code }} {{ association.name }}
</b-dropdown-item>
</b-dropdown>
</b-field>
</template>
<template>
<section v-if="summits !== null && summits.length > 0" class="section">
<div class="container">
<SummitPhotos v-for="summit in summits" class="inline" :key="summit.code" :summit="summit" :minDate="minDate" :showSummitName="true" />
</div>
</section>
<section v-else-if="summits != null" class="section">
<div class="container">
<b-message type="is-info" has-icon>
No recently uploaded photos for summits in the selected associations.
</b-message>
</div>
</section>
</template>
</PageLayout>
</template>
<script>
import axios from 'axios'
import moment from 'moment'
import prefs from '../mixins/prefs.js'
import utils from '../mixins/utils.js'
import PageLayout from '../components/PageLayout.vue'
import SummitPhotos from '../components/SummitPhotos.vue'
export default {
name: 'NewPhotos',
components: { PageLayout, SummitPhotos },
mixins: [prefs, utils],
prefs: {
key: 'newPhotosPrefs',
props: ['selectedAssociations']
},
methods: {
loadNewPhotos () {
this.loadingComponent = this.$buefy.loading.open({ canCancel: true })
let recentPhotosParams = { limit: this.limit }
let associations = '*'
if (this.selectedAssociations.length > 0) {
associations = this.selectedAssociations.join('|')
}
axios.get('https://api.sotl.as/summits/recent_photos/' + associations + '/' + this.days, { params: recentPhotosParams })
.then(response => {
this.loadingComponent.close()
this.summits = response.data
})
},
loadAssociations () {
axios.get('https://api.sotl.as/associations/all')
.then(response => {
this.associations = response.data
})
}
},
computed: {
minDate () {
return moment().subtract(this.days, 'days').toDate()
}
},
mounted () {
document.title = 'New Photos - SOTLAS'
this.loadAssociations()
this.loadNewPhotos()
},
data () {
return {
summits: null,
days: 7,
limit: 20,
associations: null,
selectedAssociations: []
}
}
}
</script>
<style scoped>
.dropdown + .dropdown {
margin-left: 0;
}
@media (max-width: 768px) {
.dropdown-item {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
}
.inline {
display: inline-block;
vertical-align: top;
}
>>> .photo-group {
margin-right: 0.75rem;
}
</style>

Wyświetl plik

@ -3,61 +3,61 @@
<template v-slot:title>Search results</template>
<template>
<section v-if="summits !== null && summits.length > 0" class="section">
<div class="container">
<h4 class="title is-4"><b-icon icon="mountains" />Summits</h4>
<b-field v-if="inactiveCount > 0" grouped>
<b-switch v-model="showInactive">Show inactive ({{ inactiveCount }})</b-switch>
</b-field>
<div class="container">
<h4 class="title is-4"><b-icon icon="mountains" />Summits</h4>
<b-field v-if="inactiveCount > 0" grouped>
<b-switch v-model="showInactive">Show inactive ({{ inactiveCount }})</b-switch>
</b-field>
<SummitList :data="filteredSummits" auto-width />
<SummitList :data="filteredSummits" auto-width />
<b-message v-if="summits !== null && summits.length === this.limit" type="is-warning" has-icon>
More than {{ this.limit }} summits found, so not all summits may be shown. Please make your search input more specific.
</b-message>
</div>
</section>
<b-message v-if="summits !== null && summits.length === this.limit" type="is-warning" has-icon>
More than {{ this.limit }} summits found, so not all summits may be shown. Please make your search input more specific.
</b-message>
</div>
</section>
<section v-if="activators !== null && activators.length > 0" class="section">
<div class="container">
<h4 class="title is-4"><b-icon icon="user" />Activators</h4>
<section v-if="activators !== null && activators.length > 0" class="section">
<div class="container">
<h4 class="title is-4"><b-icon icon="user" />Activators</h4>
<b-table class="auto-width" default-sort="callsign" :narrowed="true" :striped="true" :data="activators" :mobile-cards="false">
<template slot-scope="props">
<b-table-column field="callsign" label="Callsign" sortable>
<router-link :to="makeActivatorLink(props.row.callsign)">{{ props.row.callsign }}</router-link>
</b-table-column>
<b-table-column field="summits" label="Summits" numeric sortable>
{{ props.row.summits }}
</b-table-column>
<b-table-column field="points" :label="$mq.mobile ? 'Pts.' : 'Points'" numeric sortable>
{{ props.row.points }}
</b-table-column>
<b-table-column field="bonusPoints" :label="$mq.mobile ? 'Bonus' : 'Bonus points'" numeric sortable>
{{ props.row.bonusPoints }}
</b-table-column>
<b-table-column field="score" label="Score" numeric sortable>
{{ props.row.score }}
</b-table-column>
<b-table-column v-if="!$mq.mobile" field="avgPoints" label="Avg. points" numeric sortable>
{{ props.row.avgPoints }}
</b-table-column>
</template>
</b-table>
<b-table class="auto-width" default-sort="callsign" :narrowed="true" :striped="true" :data="activators" :mobile-cards="false">
<template slot-scope="props">
<b-table-column field="callsign" label="Callsign" sortable>
<router-link :to="makeActivatorLink(props.row.callsign)">{{ props.row.callsign }}</router-link>
</b-table-column>
<b-table-column field="summits" label="Summits" numeric sortable>
{{ props.row.summits }}
</b-table-column>
<b-table-column field="points" :label="$mq.mobile ? 'Pts.' : 'Points'" numeric sortable>
{{ props.row.points }}
</b-table-column>
<b-table-column field="bonusPoints" :label="$mq.mobile ? 'Bonus' : 'Bonus points'" numeric sortable>
{{ props.row.bonusPoints }}
</b-table-column>
<b-table-column field="score" label="Score" numeric sortable>
{{ props.row.score }}
</b-table-column>
<b-table-column v-if="!$mq.mobile" field="avgPoints" label="Avg. points" numeric sortable>
{{ props.row.avgPoints }}
</b-table-column>
</template>
</b-table>
<b-message v-if="activators !== null && activators.length === this.limit" type="is-warning" has-icon>
More than {{ this.limit }} activators found, so not all activators may be shown. Please make your search input more specific.
</b-message>
</div>
</section>
<b-message v-if="activators !== null && activators.length === this.limit" type="is-warning" has-icon>
More than {{ this.limit }} activators found, so not all activators may be shown. Please make your search input more specific.
</b-message>
</div>
</section>
<section class="section">
<div class="container">
<b-message v-if="activators !== null && activators.length === 0 && summits !== null && summits.length === 0" type="is-info" has-icon>
No matching summits or activators for '{{ $route.query.q }}' found.
</b-message>
</div>
</section>
</template>
<section class="section">
<div class="container">
<b-message v-if="activators !== null && activators.length === 0 && summits !== null && summits.length === 0" type="is-info" has-icon>
No matching summits or activators for '{{ $route.query.q }}' found.
</b-message>
</div>
</section>
</template>
</PageLayout>
</template>

Wyświetl plik

@ -95,10 +95,10 @@
<section v-if="summit" class="section">
<div class="container">
<h4 class="title is-4">Photos</h4>
<SummitPhotos ref="summitPhotos" :summit="summit" @photoDeleted="reloadPhotos" @photoEdited="reloadPhotos" @photosReordered="reloadPhotos" />
<SummitPhotos ref="summitPhotos" :summit="summit" :editable="true" :showWaypointButton="true" @photoDeleted="reloadPhotos" @photoEdited="reloadPhotos" @photosReordered="reloadPhotos" />
<PhotosUploader v-if="$keycloak && $keycloak.authenticated" :summitCode="summitCode" @upload="reloadPhotos" />
<div v-else class="uploader-placeholder"><font-awesome-icon :icon="['far', 'images']" size="lg" /> Log in and upload your photos of this summit!</div>
<div v-else class="uploader-placeholder box"><font-awesome-icon :icon="['far', 'images']" size="lg" /> Log in and upload your photos of this summit!</div>
</div>
</section>