diff options
| -rw-r--r-- | src/site/components/album/AlbumDetails.vue | 177 | ||||
| -rw-r--r-- | src/site/components/album/AlbumEntry.vue | 179 | ||||
| -rw-r--r-- | src/site/pages/dashboard/albums/index.vue | 312 | ||||
| -rw-r--r-- | src/site/store/albums.js | 56 |
4 files changed, 423 insertions, 301 deletions
diff --git a/src/site/components/album/AlbumDetails.vue b/src/site/components/album/AlbumDetails.vue new file mode 100644 index 0000000..a02fe55 --- /dev/null +++ b/src/site/components/album/AlbumDetails.vue @@ -0,0 +1,177 @@ +<template> + <div class="details"> + <h2>Public links for this album:</h2> + + <b-table + :data="details.links || []" + :mobile-cards="true"> + <template slot-scope="props"> + <b-table-column field="identifier" + label="Link" + centered> + <a :href="`${config.URL}/a/${props.row.identifier}`" + target="_blank"> + {{ props.row.identifier }} + </a> + </b-table-column> + + <b-table-column field="views" + label="Views" + centered> + {{ props.row.views }} + </b-table-column> + + <b-table-column field="enableDownload" + label="Allow download" + centered> + <b-switch v-model="props.row.enableDownload" + @input="linkOptionsChanged(props.row)" /> + </b-table-column> + + <b-table-column field="enabled" + numeric> + <button class="button is-danger" + @click="promptDeleteAlbumLink(props.row.identifier)">Delete link</button> + </b-table-column> + </template> + <template slot="empty"> + <div class="has-text-centered"> + <i class="icon-misc-mood-sad" /> + </div> + <div class="has-text-centered"> + Nothing here + </div> + </template> + <template slot="footer"> + <div class="level is-paddingless"> + <div class="level-left"> + <div class="level-item"> + <button :class="{ 'is-loading': isCreatingLink }" + class="button is-primary" + style="float: left" + @click="createLink(albumId)">Create new link</button> + </div> + <div class="level-item"> + <span class="has-text-default">{{ details.links.length }} / {{ config.maxLinksPerAlbum }} links created</span> + </div> + </div> + + <div class="level-right"> + <div class="level-item"> + <button class="button is-danger" + style="float: right" + @click="promptDeleteAlbum(albumId)">Delete album</button> + </div> + </div> + </div> + </template> + </b-table> + </div> +</template> + +<script> +import { mapState } from 'vuex'; + +export default { + props: { + albumId: { + type: Number, + default: 0 + }, + details: { + type: Object, + default: () => ({}) + }, + }, + data() { + return { + isCreatingLink: false + } + }, + computed: mapState(['config']), + methods: { + promptDeleteAlbum(id) { + this.$buefy.dialog.confirm({ + message: 'Are you sure you want to delete this album?', + onConfirm: () => this.deleteAlbum(id) + }); + }, + async deleteAlbum(id) { + const response = await this.$axios.$delete(`album/${id}`); + this.getAlbums(); + return this.$buefy.toast.open(response.message); + }, + promptDeleteAlbumLink(identifier) { + this.$buefy.dialog.confirm({ + message: 'Are you sure you want to delete this album link?', + onConfirm: () => this.deleteAlbumLink(identifier) + }); + }, + async deleteAlbumLink(identifier) { + const response = await this.$axios.$delete(`album/link/delete/${identifier}`); + return this.$buefy.toast.open(response.message); + }, + async linkOptionsChanged(link) { + const response = await this.$axios.$post(`album/link/edit`, + { + identifier: link.identifier, + enableDownload: link.enableDownload, + enabled: link.enabled + }); + this.$buefy.toast.open(response.message); + }, + async createLink(album) { + album.isCreatingLink = true; + // Since we actually want to change the state even if the call fails, use a try catch + try { + const response = await this.$axios.$post(`album/link/new`, + { albumId: album.id }); + this.$buefy.toast.open(response.message); + album.links.push({ + identifier: response.identifier, + views: 0, + enabled: true, + enableDownload: true, + expiresAt: null + }); + } catch (error) { + // + } finally { + album.isCreatingLink = false; + } + } + } +}; +</script> + +<style lang="scss" scoped> + @import '~/assets/styles/_colors.scss'; + + div.details { + flex: 0 1 100%; + padding-left: 2em; + padding-top: 1em; + min-height: 50px; + + .b-table { + padding: 2em 0em; + + .table-wrapper { + -webkit-box-shadow: $boxShadowLight; + box-shadow: $boxShadowLight; + } + } + } +</style> + + +<style lang="scss"> + @import '~/assets/styles/_colors.scss'; + + .b-table { + .table-wrapper { + -webkit-box-shadow: $boxShadowLight; + box-shadow: $boxShadowLight; + } + } +</style> diff --git a/src/site/components/album/AlbumEntry.vue b/src/site/components/album/AlbumEntry.vue new file mode 100644 index 0000000..4d23d6c --- /dev/null +++ b/src/site/components/album/AlbumEntry.vue @@ -0,0 +1,179 @@ +<template> + <div class="album"> + <div class="arrow-container" + @click="toggleDetails(album)"> + <i :class="{ active: isExpanded }" + class="icon-arrow" /> + </div> + <div class="thumb"> + <figure class="image is-64x64 thumb"> + <img src="~/assets/images/blank_darker.png"> + </figure> + </div> + <div class="info"> + <h4> + <router-link :to="`/dashboard/albums/${album.id}`">{{ album.name }}</router-link> + </h4> + <span>Updated <timeago :since="album.editedAt" /></span> + <span>{{ album.fileCount || 0 }} files</span> + </div> + <div class="latest is-hidden-mobile"> + <template v-if="album.fileCount > 0"> + <div v-for="file of album.files" + :key="file.id" + class="thumb"> + <figure class="image is-64x64"> + <a :href="file.url" + target="_blank"> + <img :src="file.thumbSquare"> + </a> + </figure> + </div> + <div v-if="album.fileCount > 5" + class="thumb more no-background"> + <router-link :to="`/dashboard/albums/${album.id}`">{{ album.fileCount - 5 }}+ more</router-link> + </div> + </template> + <template v-else> + <span class="no-files">Nothing to show here</span> + </template> + </div> + + <AlbumDetails v-if="isExpanded" + :details="getDetails" + :albumId="album.id" /> + </div> +</template> + +<script> +import { mapGetters } from 'vuex'; +import AlbumDetails from '~/components/album/AlbumDetails.vue'; + +export default { + components: { + AlbumDetails + }, + props: { + album: { + type: Object, + default: () => ({}) + } + }, + computed: { + ...mapGetters({ + isExpandedGetter: 'albums/isExpanded', + getDetailsGetter: 'albums/getDetails' + }), + isExpanded() { + return this.isExpandedGetter(this.album.id); + }, + getDetails() { + return this.getDetailsGetter(this.album.id); + } + }, + methods: { + async toggleDetails(album) { + if (!this.isExpanded) { + await this.$store.dispatch('albums/fetchDetails', album.id); + } + this.$store.commit('albums/toggleExpandedState', album.id); + } + } +}; +</script> + +<style lang="scss" scoped> + @import '~/assets/styles/_colors.scss'; + + div.album { + display: flex; + flex-wrap: wrap; + margin-bottom: 10px; + + div.arrow-container { + width: 2em; + height: 64px; + position: relative; + cursor: pointer; + + i { + border: 2px solid $defaultTextColor; + border-right: 0; + border-top: 0; + display: block; + height: 1em; + position: absolute; + transform: rotate(-135deg); + transform-origin: center; + width: 1em; + z-index: 4; + top: 22px; + + -webkit-transition: transform 0.1s linear; + -moz-transition: transform 0.1s linear; + -ms-transition: transform 0.1s linear; + -o-transition: transform 0.1s linear; + transition: transform 0.1s linear; + + &.active { + transform: rotate(-45deg); + } + } + } + + div.thumb { + width: 64px; + height: 64px; + -webkit-box-shadow: $boxShadowLight; + box-shadow: $boxShadowLight; + } + + div.info { + margin-left: 15px; + text-align: left; + h4 { + font-size: 1.5rem; + a { + color: $defaultTextColor; + font-weight: 400; + &:hover { text-decoration: underline; } + } + } + span { display: block; } + span:nth-child(3) { + font-size: 0.9rem; + } + } + + div.latest { + flex-grow: 1; + justify-content: flex-end; + display: flex; + margin-left: 15px; + + span.no-files { + font-size: 1.5em; + color: #b1b1b1; + padding-top: 17px; + } + + div.more { + width: 64px; + height: 64px; + background: white; + display: flex; + align-items: center; + padding: 10px; + text-align: center; + a { + line-height: 1rem; + color: $defaultTextColor; + &:hover { text-decoration: underline; } + } + } + } + } + + div.no-background { background: none !important; } +</style> + diff --git a/src/site/pages/dashboard/albums/index.vue b/src/site/pages/dashboard/albums/index.vue index 065667a..2a54ab8 100644 --- a/src/site/pages/dashboard/albums/index.vue +++ b/src/site/pages/dashboard/albums/index.vue @@ -25,118 +25,9 @@ </div> <div class="view-container"> - <div v-for="album in albums" + <AlbumEntry v-for="album in albums.list" :key="album.id" - class="album"> - <div class="arrow-container" - @click="fetchAlbumDetails(album)"> - <i :class="{ active: album.isDetailsOpen }" - class="icon-arrow" /> - </div> - <div class="thumb"> - <figure class="image is-64x64 thumb"> - <img src="~/assets/images/blank_darker.png"> - </figure> - </div> - <div class="info"> - <h4> - <router-link :to="`/dashboard/albums/${album.id}`">{{ album.name }}</router-link> - </h4> - <span>Updated <timeago :since="album.editedAt" /></span> - <span>{{ album.fileCount || 0 }} files</span> - </div> - <div class="latest is-hidden-mobile"> - <template v-if="album.fileCount > 0"> - <div v-for="file of album.files" - :key="file.id" - class="thumb"> - <figure class="image is-64x64"> - <a :href="file.url" - target="_blank"> - <img :src="file.thumbSquare"> - </a> - </figure> - </div> - <div v-if="album.fileCount > 5" - class="thumb more no-background"> - <router-link :to="`/dashboard/albums/${album.id}`">{{ album.fileCount - 5 }}+ more</router-link> - </div> - </template> - <template v-else> - <span class="no-files">Nothing to show here</span> - </template> - </div> - - <div v-if="album.isDetailsOpen" - class="details"> - <h2>Public links for this album:</h2> - - <b-table - :data="album.links.length ? album.links : []" - :mobile-cards="true"> - <template slot-scope="props"> - <b-table-column field="identifier" - label="Link" - centered> - <a :href="`${config.URL}/a/${props.row.identifier}`" - target="_blank"> - {{ props.row.identifier }} - </a> - </b-table-column> - - <b-table-column field="views" - label="Views" - centered> - {{ props.row.views }} - </b-table-column> - - <b-table-column field="enableDownload" - label="Allow download" - centered> - <b-switch v-model="props.row.enableDownload" - @input="linkOptionsChanged(props.row)" /> - </b-table-column> - - <b-table-column field="enabled" - numeric> - <button class="button is-danger" - @click="promptDeleteAlbumLink(props.row.identifier)">Delete link</button> - </b-table-column> - </template> - <template slot="empty"> - <div class="has-text-centered"> - <i class="icon-misc-mood-sad" /> - </div> - <div class="has-text-centered"> - Nothing here - </div> - </template> - <template slot="footer"> - <div class="level is-paddingless"> - <div class="level-left"> - <div class="level-item"> - <button :class="{ 'is-loading': album.isCreatingLink }" - class="button is-primary" - style="float: left" - @click="createLink(album)">Create new link</button> - </div> - <div class="level-item"> - <span class="has-text-default">{{ album.links.length }} / {{ config.maxLinksPerAlbum }} links created</span> - </div> - </div> - - <div class="level-right"> - <div class="level-item"> - <button class="button is-danger" - style="float: right" - @click="promptDeleteAlbum(album.id)">Delete album</button> - </div> - </div> - </div> - </template> - </b-table> - </div> - </div> + :album="album" /> </div> </div> </div> @@ -146,87 +37,28 @@ </template> <script> +import { mapState } from 'vuex'; import Sidebar from '~/components/sidebar/Sidebar.vue'; +import AlbumEntry from '~/components/album/AlbumEntry.vue'; export default { components: { - Sidebar + Sidebar, + AlbumEntry }, - middleware: 'auth', + middleware: ['auth', ({ store }) => { + store.dispatch('albums/fetch'); + }], data() { return { - albums: [], newAlbumName: null }; }, - computed: { - config() { - return this.$store.state.config; - } - }, + computed: mapState(['config', 'albums']), metaInfo() { return { title: 'Uploads' }; }, - mounted() { - this.getAlbums(); - }, methods: { - async fetchAlbumDetails(album) { - const response = await this.$axios.$get(`album/${album.id}/links`); - album.links = response.links; - album.isDetailsOpen = !album.isDetailsOpen; - this.$forceUpdate(); - }, - promptDeleteAlbum(id) { - this.$buefy.dialog.confirm({ - message: 'Are you sure you want to delete this album?', - onConfirm: () => this.deleteAlbum(id) - }); - }, - async deleteAlbum(id) { - const response = await this.$axios.$delete(`album/${id}`); - this.getAlbums(); - return this.$buefy.toast.open(response.message); - }, - promptDeleteAlbumLink(identifier) { - this.$buefy.dialog.confirm({ - message: 'Are you sure you want to delete this album link?', - onConfirm: () => this.deleteAlbumLink(identifier) - }); - }, - async deleteAlbumLink(identifier) { - const response = await this.$axios.$delete(`album/link/delete/${identifier}`); - return this.$buefy.toast.open(response.message); - }, - async linkOptionsChanged(link) { - const response = await this.$axios.$post(`album/link/edit`, - { - identifier: link.identifier, - enableDownload: link.enableDownload, - enabled: link.enabled - }); - this.$buefy.toast.open(response.message); - }, - async createLink(album) { - album.isCreatingLink = true; - // Since we actually want to change the state even if the call fails, use a try catch - try { - const response = await this.$axios.$post(`album/link/new`, - { albumId: album.id }); - this.$buefy.toast.open(response.message); - album.links.push({ - identifier: response.identifier, - views: 0, - enabled: true, - enableDownload: true, - expiresAt: null - }); - } catch (error) { - // - } finally { - album.isCreatingLink = false; - } - }, async createAlbum() { if (!this.newAlbumName || this.newAlbumName === '') return; const response = await this.$axios.$post(`album/new`, @@ -234,17 +66,11 @@ export default { this.newAlbumName = null; this.$buefy.toast.open(response.message); this.getAlbums(); - }, - async getAlbums() { - const response = await this.$axios.$get(`albums/mini`); - for (const album of response.albums) { - album.isDetailsOpen = false; - } - this.albums = response.albums; } } }; </script> + <style lang="scss" scoped> @import '~/assets/styles/_colors.scss'; div.view-container { @@ -256,121 +82,5 @@ export default { background-color: $base-2; } - div.album { - display: flex; - flex-wrap: wrap; - margin-bottom: 10px; - - div.arrow-container { - width: 2em; - height: 64px; - position: relative; - cursor: pointer; - - i { - border: 2px solid $defaultTextColor; - border-right: 0; - border-top: 0; - display: block; - height: 1em; - position: absolute; - transform: rotate(-135deg); - transform-origin: center; - width: 1em; - z-index: 4; - top: 22px; - - -webkit-transition: transform 0.1s linear; - -moz-transition: transform 0.1s linear; - -ms-transition: transform 0.1s linear; - -o-transition: transform 0.1s linear; - transition: transform 0.1s linear; - - &.active { - transform: rotate(-45deg); - } - } - } - div.thumb { - width: 64px; - height: 64px; - -webkit-box-shadow: $boxShadowLight; - box-shadow: $boxShadowLight; - } - - div.info { - margin-left: 15px; - text-align: left; - h4 { - font-size: 1.5rem; - a { - color: $defaultTextColor; - font-weight: 400; - &:hover { text-decoration: underline; } - } - } - span { display: block; } - span:nth-child(3) { - font-size: 0.9rem; - } - } - - div.latest { - flex-grow: 1; - justify-content: flex-end; - display: flex; - margin-left: 15px; - - span.no-files { - font-size: 1.5em; - color: #b1b1b1; - padding-top: 17px; - } - - div.more { - width: 64px; - height: 64px; - background: white; - display: flex; - align-items: center; - padding: 10px; - text-align: center; - a { - line-height: 1rem; - color: $defaultTextColor; - &:hover { text-decoration: underline; } - } - } - } - - div.details { - flex: 0 1 100%; - padding-left: 2em; - padding-top: 1em; - min-height: 50px; - - .b-table { - padding: 2em 0em; - - .table-wrapper { - -webkit-box-shadow: $boxShadowLight; - box-shadow: $boxShadowLight; - } - } - } - } - div.column > h2.subtitle { padding-top: 1px; } - - div.no-background { background: none !important; } -</style> -<style lang="scss"> - @import '~/assets/styles/_colors.scss'; - - .b-table { - .table-wrapper { - -webkit-box-shadow: $boxShadowLight; - box-shadow: $boxShadowLight; - } - } </style> diff --git a/src/site/store/albums.js b/src/site/store/albums.js new file mode 100644 index 0000000..a33181c --- /dev/null +++ b/src/site/store/albums.js @@ -0,0 +1,56 @@ +/* eslint-disable no-shadow */ +export const state = () => ({ + list: [], + isListLoading: false, + albumDetails: {}, + expandedAlbums: [] +}); + +export const getters = { + isExpanded: state => id => state.expandedAlbums.indexOf(id) > -1, + getDetails: state => id => state.albumDetails[id] || {} +}; + +export const actions = { + async fetch({ commit, dispatch }) { + try { + commit('albumsRequest'); + const response = await this.$axios.$get(`albums/mini`); + + commit('setAlbums', response.albums); + } catch (e) { + dispatch('alert/set', { text: e.message, error: true }, { root: true }); + } + }, + async fetchDetails({ commit }, albumId) { + const response = await this.$axios.$get(`album/${albumId}/links`); + + commit('setDetails', { + id: albumId, + details: { + links: response.links + } + }); + } +}; + +export const mutations = { + albumsRequest(state) { + state.isLoading = true; + }, + setAlbums(state, albums) { + state.list = albums; + state.isLoading = false; + }, + setDetails(state, { id, details }) { + state.albumDetails[id] = details; + }, + toggleExpandedState(state, id) { + const foundIndex = state.expandedAlbums.indexOf(id); + if (foundIndex > -1) { + state.expandedAlbums.splice(foundIndex, 1); + } else { + state.expandedAlbums.push(id); + } + } +}; |