diff options
| author | Zephyrrus <[email protected]> | 2020-07-10 01:17:00 +0300 |
|---|---|---|
| committer | GitHub <[email protected]> | 2020-07-10 01:17:00 +0300 |
| commit | a721681944e9eb06742e5b3f71c71aed9c1c117d (patch) | |
| tree | 93ff9fd13a0434d91fb1ae7ca0da48d6929c4d00 /src/site/components | |
| parent | feat: backend pagination for albums (diff) | |
| parent | refactor: finish refactoring all the components to use vuex (diff) | |
| download | host.fuwn.me-a721681944e9eb06742e5b3f71c71aed9c1c117d.tar.xz host.fuwn.me-a721681944e9eb06742e5b3f71c71aed9c1c117d.zip | |
Merge pull request #1 from Zephyrrus/feature/store_refactor
Feature/store refactor
Diffstat (limited to 'src/site/components')
| -rw-r--r-- | src/site/components/album/AlbumDetails.vue | 211 | ||||
| -rw-r--r-- | src/site/components/album/AlbumEntry.vue | 187 | ||||
| -rw-r--r-- | src/site/components/footer/Footer.vue | 70 | ||||
| -rw-r--r-- | src/site/components/grid/Grid.vue | 550 | ||||
| -rw-r--r-- | src/site/components/grid/waterfall/Waterfall.vue | 261 | ||||
| -rw-r--r-- | src/site/components/grid/waterfall/WaterfallItem.vue | 52 | ||||
| -rw-r--r-- | src/site/components/imageInfo/ImageInfo.vue | 0 | ||||
| -rw-r--r-- | src/site/components/loading/BulmaLoading.vue | 33 | ||||
| -rw-r--r-- | src/site/components/loading/CubeShadow.vue | 17 | ||||
| -rw-r--r-- | src/site/components/loading/Origami.vue | 18 | ||||
| -rw-r--r-- | src/site/components/loading/PingPong.vue | 26 | ||||
| -rw-r--r-- | src/site/components/loading/RotateSquare.vue | 13 | ||||
| -rw-r--r-- | src/site/components/navbar/Navbar.vue | 26 | ||||
| -rw-r--r-- | src/site/components/sidebar/Sidebar.vue | 116 | ||||
| -rw-r--r-- | src/site/components/uploader/Uploader.vue | 75 |
15 files changed, 1044 insertions, 611 deletions
diff --git a/src/site/components/album/AlbumDetails.vue b/src/site/components/album/AlbumDetails.vue new file mode 100644 index 0000000..b411f13 --- /dev/null +++ b/src/site/components/album/AlbumDetails.vue @@ -0,0 +1,211 @@ +<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="updateLinkOptions(albumId, props.row)" /> + </b-table-column> + + <b-table-column + field="enabled" + numeric> + <button + :class="{ 'is-loading': isDeleting(props.row.identifier) }" + class="button is-danger" + :disabled="isDeleting(props.row.identifier)" + @click="promptDeleteAlbumLink(albumId, 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, mapActions } from 'vuex'; + +export default { + props: { + albumId: { + type: Number, + default: 0, + }, + details: { + type: Object, + default: () => ({}), + }, + }, + data() { + return { + isCreatingLink: false, + isDeletingLinks: [], + }; + }, + computed: mapState(['config']), + methods: { + ...mapActions({ + deleteAlbumAction: 'albums/deleteAlbum', + deleteAlbumLinkAction: 'albums/deleteLink', + updateLinkOptionsAction: 'albums/updateLinkOptions', + createLinkAction: 'albums/createLink', + alert: 'alert/set', + }), + promptDeleteAlbum(id) { + this.$buefy.dialog.confirm({ + type: 'is-danger', + message: 'Are you sure you want to delete this album?', + onConfirm: () => this.deleteAlbum(id), + }); + }, + promptDeleteAlbumLink(albumId, identifier) { + this.$buefy.dialog.confirm({ + type: 'is-danger', + message: 'Are you sure you want to delete this album link?', + onConfirm: () => this.deleteAlbumLink(albumId, identifier), + }); + }, + async deleteAlbum(id) { + try { + const response = await this.deleteAlbumAction(id); + + this.alert({ text: response.message, error: false }); + } catch (e) { + this.alert({ text: e.message, error: true }); + } + }, + async deleteAlbumLink(albumId, identifier) { + this.isDeletingLinks.push(identifier); + try { + const response = await this.deleteAlbumLinkAction({ albumId, identifier }); + + this.alert({ text: response.message, error: false }); + } catch (e) { + this.alert({ text: e.message, error: true }); + } finally { + this.isDeletingLinks = this.isDeletingLinks.filter((e) => e !== identifier); + } + }, + async createLink(albumId) { + this.isCreatingLink = true; + try { + const response = await this.createLinkAction(albumId); + + this.alert({ text: response.message, error: false }); + } catch (e) { + this.alert({ text: e.message, error: true }); + } finally { + this.isCreatingLink = false; + } + }, + async updateLinkOptions(albumId, linkOpts) { + try { + const response = await this.updateLinkOptionsAction({ albumId, linkOpts }); + + this.alert({ text: response.message, error: false }); + } catch (e) { + this.alert({ text: e.message, error: true }); + } + }, + isDeleting(identifier) { + return this.isDeletingLinks.indexOf(identifier) > -1; + }, + }, +}; +</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..2723b49 --- /dev/null +++ b/src/site/components/album/AlbumEntry.vue @@ -0,0 +1,187 @@ +<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> + Created <span class="is-inline has-text-weight-semibold"><timeago :since="album.createdAt" /></span> + </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(album.id)" + :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', + getDetails: 'albums/getDetails', + }), + isExpanded() { + return this.isExpandedGetter(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/components/footer/Footer.vue b/src/site/components/footer/Footer.vue index 49f622c..19d18f2 100644 --- a/src/site/components/footer/Footer.vue +++ b/src/site/components/footer/Footer.vue @@ -1,12 +1,18 @@ <template> + <!-- eslint-disable max-len --> <footer> - <svg viewBox="0 0 1920 250" + <svg + viewBox="0 0 1920 250" class="waves"> - <path d="M1920 250H0V0s126.707 78.536 349.975 80.05c177.852 1.203 362.805-63.874 553.803-63.874 290.517 0 383.458 57.712 603.992 61.408 220.527 3.696 278.059-61.408 412.23-17.239" + + <path + d="M1920 250H0V0s126.707 78.536 349.975 80.05c177.852 1.203 362.805-63.874 553.803-63.874 290.517 0 383.458 57.712 603.992 61.408 220.527 3.696 278.059-61.408 412.23-17.239" class="wave-1" /> - <path d="M1920 144s-467.917 116.857-1027.243-17.294C369.986 1.322 0 45.578 0 45.578V250h1920V144z" + <path + d="M1920 144s-467.917 116.857-1027.243-17.294C369.986 1.322 0 45.578 0 45.578V250h1920V144z" class="wave-2" /> - <path d="M0 195.553s208.547-75.581 701.325-20.768c376.707 41.908 520.834-67.962 722.545-67.962 222.926 0 311.553 83.523 496.129 86.394V250H0v-54.447z" + <path + d="M0 195.553s208.547-75.581 701.325-20.768c376.707 41.908 520.834-67.962 722.545-67.962 222.926 0 311.553 83.523 496.129 86.394V250H0v-54.447z" class="wave-3" /> </svg> <div> @@ -15,7 +21,8 @@ <div class="column is-narrow"> <h4>lolisafe</h4> <span>© 2017-2020 - <a href="https://github.com/pitu" + <a + href="https://github.com/pitu" class="no-block">Pitu</a> </span><br> <span>v{{ version }}</span> @@ -24,20 +31,33 @@ <div class="columns is-gapless"> <div class="column" /> <div class="column"> - <nuxt-link to="/">Home</nuxt-link> - <nuxt-link to="/faq">FAQ</nuxt-link> + <nuxt-link to="/"> + Home + </nuxt-link> + <nuxt-link to="/faq"> + FAQ + </nuxt-link> </div> <div class="column"> - <nuxt-link to="/dashboard">Dashboard</nuxt-link> - <nuxt-link to="/dashboard">Files</nuxt-link> - <nuxt-link to="/dashboard/albums">Albums</nuxt-link> - <nuxt-link to="/dashboard/account">Account</nuxt-link> + <nuxt-link to="/dashboard"> + Dashboard + </nuxt-link> + <nuxt-link to="/dashboard"> + Files + </nuxt-link> + <nuxt-link to="/dashboard/albums"> + Albums + </nuxt-link> + <nuxt-link to="/dashboard/account"> + Account + </nuxt-link> </div> <div class="column"> <a href="https://github.com/weebdev/lolisafe">GitHub</a> </div> <div class="column"> - <a v-if="loggedIn" + <a + v-if="loggedIn" @click="createShareXThing">ShareX Config</a> <a href="https://chrome.google.com/webstore/detail/lolisafe-uploader/enkkmplljfjppcdaancckgilmgoiofnj">Chrome Extension</a> </div> @@ -48,27 +68,32 @@ </div> </footer> </template> + <script> +/* eslint-disable no-restricted-globals */ + +import { mapState, mapGetters } from 'vuex'; import { saveAs } from 'file-saver'; + export default { computed: { - loggedIn() { - return this.$store.state.loggedIn; - }, - version() { - return this.$store.state.config.version; - } + ...mapGetters({ loggedIn: 'auth/isLoggedIn' }), + ...mapState({ + version: (state) => state.config.version, + serviceName: (state) => state.config.serviceName, + token: (state) => state.auth.token, + }), }, methods: { createShareXThing() { const sharexFile = `{ - "Name": "${this.$store.state.config.serviceName}", + "Name": "${this.serviceName}", "DestinationType": "ImageUploader, FileUploader", "RequestType": "POST", "RequestURL": "${location.origin}/api/upload", "FileFormName": "files[]", "Headers": { - "authorization": "Bearer ${this.$store.state.token}", + "authorization": "Bearer ${this.token}", "accept": "application/vnd.lolisafe.json" }, "ResponseType": "Text", @@ -77,10 +102,11 @@ export default { }`; const sharexBlob = new Blob([sharexFile], { type: 'application/octet-binary' }); saveAs(sharexBlob, `${location.hostname}.sxcu`); - } - } + }, + }, }; </script> + <style lang="scss" scoped> @import '~/assets/styles/_colors.scss'; footer { diff --git a/src/site/components/grid/Grid.vue b/src/site/components/grid/Grid.vue index b6615be..90c196b 100644 --- a/src/site/components/grid/Grid.vue +++ b/src/site/components/grid/Grid.vue @@ -1,152 +1,136 @@ <template> <div> - <div v-if="enableToolbar" - class="toolbar"> - <div class="block"> - <b-radio v-model="showList" - name="name" - :native-value="true"> - List - </b-radio> - <b-radio v-model="showList" - name="name" - :native-value="false"> - Grid - </b-radio> + <nav class="level"> + <div class="level-left"> + <div class="level-item"> + <slot name="pagination" /> + </div> </div> - </div> + <div v-if="enableToolbar" class="level-right toolbar"> + <div class="level-item"> + <div class="block"> + <b-radio v-model="showList" name="name" :native-value="true"> + List + </b-radio> + <b-radio v-model="showList" name="name" :native-value="false"> + Grid + </b-radio> + </div> + </div> + </div> + </nav> <template v-if="!showList"> - <Waterfall v-if="showWaterfall" + <Waterfall + v-if="showWaterfall" :gutterWidth="10" - :gutterHeight="4"> - <!-- - TODO: Implement search based on originalName, albumName and tags - <input v-if="enableSearch" - v-model="searchTerm" - type="text" - placeholder="Search..." - @input="search()" - @keyup.enter="search()"> - --> - - <!-- TODO: Implement pagination --> - - <WaterfallItem v-for="(item, index) in gridFiles" - :key="item.id" - :width="width" - move-class="item-move"> + :gutterHeight="4" + :options="{fitWidth: true}" + :itemWidth="width" + :items="gridFiles"> + <template v-slot="{item}"> <template v-if="isPublic"> - <a :href="`${item.url}`" - target="_blank"> + <a + :href="`${item.url}`" + class="preview-container" + target="_blank" + @mouseenter.self.stop.prevent="item.preview && mouseOver(item.id)" + @mouseleave.self.stop.prevent="item.preview && mouseOut(item.id)"> + <img :src="item.thumb ? item.thumb : blank"> - <span v-if="!item.thumb && item.name" - class="extension">{{ item.name.split('.').pop() }}</span> + <div v-if="item.preview && isHovered(item.id)" class="preview"> + <video ref="video" class="preview" autoplay loop muted> + <source :src="item.preview" type="video/mp4"> + </video> + </div> + + <span v-if="!item.thumb && item.name" class="extension">{{ + item.name.split('.').pop() + }}</span> </a> </template> <template v-else> <img :src="item.thumb ? item.thumb : blank"> - <span v-if="!item.thumb && item.name" - class="extension">{{ item.name.split('.').pop() }}</span> - <div v-if="!isPublic" + <div v-if="item.preview && isHovered(item.id)" class="preview"> + <video ref="video" class="preview" autoplay loop muted> + <source :src="item.preview" type="video/mp4"> + </video> + </div> + + <span v-if="!item.thumb && item.name" class="extension">{{ item.name.split('.').pop() }}</span> + <div + v-if="!isPublic" :class="{ fixed }" - class="actions"> - <b-tooltip label="Link" - position="is-top"> - <a :href="`${item.url}`" - target="_blank" - class="btn"> + class="actions" + @mouseenter.self.stop.prevent="item.preview && mouseOver(item.id)" + @mouseleave.self.stop.prevent="item.preview && mouseOut(item.id)"> + <b-tooltip label="Link" position="is-top"> + <a :href="`${item.url}`" target="_blank" class="btn"> <i class="icon-web-code" /> </a> </b-tooltip> - <b-tooltip label="Albums" - position="is-top"> - <a class="btn" - @click="openAlbumModal(item)"> - <i class="icon-interface-window" /> + <b-tooltip label="Tags" position="is-top"> + <a class="btn" @click="false && manageTags(item)"> + <i class="icon-ecommerce-tag-c" /> </a> </b-tooltip> - <!-- - <b-tooltip label="Tags" - position="is-top"> - <a @click="manageTags(item)"> - <i class="icon-ecommerce-tag-c" /> + <b-tooltip label="Albums" position="is-top"> + <a class="btn" @click="openAlbumModal(item)"> + <i class="icon-interface-window" /> </a> </b-tooltip> - --> - <b-tooltip label="Delete" - position="is-top"> - <a class="btn" - @click="deleteFile(item, index)"> + <b-tooltip label="Delete" position="is-top"> + <a class="btn" @click="deleteFile(item)"> <i class="icon-editorial-trash-a-l" /> </a> </b-tooltip> - <b-tooltip v-if="user && user.isAdmin" - label="More info" - position="is-top" - class="more"> + <b-tooltip v-if="user && user.isAdmin" label="More info" position="is-top" class="more"> <nuxt-link :to="`/dashboard/admin/file/${item.id}`"> <i class="icon-interface-more" /> </nuxt-link> </b-tooltip> </div> </template> - </WaterfallItem> + </template> </Waterfall> </template> <div v-else> - <b-table - :data="gridFiles || []" - :mobile-cards="true"> + <b-table :data="gridFiles || []" :mobile-cards="true"> <template slot-scope="props"> <template v-if="!props.row.hideFromList"> - <b-table-column field="url" - label="URL"> - <a :href="props.row.url" - target="_blank">{{ props.row.url }}</a> + <b-table-column field="url" label="URL"> + <a :href="props.row.url" target="_blank">{{ props.row.url }}</a> </b-table-column> - <b-table-column field="albums" - label="Albums" - centered> + <b-table-column field="albums" label="Albums" centered> <template v-for="(album, index) in props.row.albums"> - <nuxt-link :key="index" - :to="`/dashboard/albums/${album.id}`"> + <nuxt-link :key="index" :to="`/dashboard/albums/${album.id}`"> {{ album.name }} </nuxt-link> - <template v-if="index < props.row.albums.length - 1">, </template> + <template v-if="index < props.row.albums.length - 1"> + , + </template> </template> {{ props.row.username }} </b-table-column> - <b-table-column field="uploaded" - label="Uploaded" - centered> + <b-table-column field="uploaded" label="Uploaded" centered> <span><timeago :since="props.row.createdAt" /></span> </b-table-column> - <b-table-column field="purge" - centered> - <b-tooltip label="Albums" - position="is-top"> - <a class="btn" - @click="openAlbumModal(props.row)"> + <b-table-column field="purge" centered> + <b-tooltip label="Albums" position="is-top"> + <a class="btn" @click="openAlbumModal(props.row)"> <i class="icon-interface-window" /> </a> </b-tooltip> - <b-tooltip label="Delete" - position="is-top" - class="is-danger"> - <a class="is-danger" - @click="deleteFile(props.row)"> + <b-tooltip label="Delete" position="is-top" class="is-danger"> + <a class="is-danger" @click="deleteFile(props.row)"> <i class="icon-editorial-trash-a-l" /> </a> </b-tooltip> - <b-tooltip v-if="user && user.isAdmin" - label="More info" - position="is-top" - class="more"> + <b-tooltip v-if="user && user.isAdmin" label="More info" position="is-top" class="more"> <nuxt-link :to="`/dashboard/admin/file/${props.row.id}`"> <i class="icon-interface-more" /> </nuxt-link> @@ -164,26 +148,28 @@ </template> <template slot="footer"> <div class="has-text-right has-text-default"> - {{ files.length }} files + Showing {{ files.length }} files ({{ total }} total) </div> </template> </b-table> </div> - <b-modal :active.sync="isAlbumsModalActive" - :width="640" - scroll="keep"> + <b-modal :active.sync="isAlbumsModalActive" :width="640" scroll="keep"> <div class="card albumsModal"> <div class="card-content"> <div class="content"> - <h3 class="subtitle">Select the albums this file should be a part of</h3> + <h3 class="subtitle"> + Select the albums this file should be a part of + </h3> <hr> + <div class="albums-container"> - <div v-for="(album, index) in albums" - :key="index" - class="album"> + <div v-for="album in albums" :key="album.id" class="album"> <div class="field"> - <b-checkbox :value="isAlbumSelected(album.id)" - @input="albumCheckboxClicked($event, album.id)">{{ album.name }}</b-checkbox> + <b-checkbox + :value="isAlbumSelected(album.id)" + @input="albumCheckboxClicked($event, album.id)"> + {{ album.name }} + </b-checkbox> </div> </div> </div> @@ -195,243 +181,297 @@ </template> <script> +import { mapState } from 'vuex'; + import Waterfall from './waterfall/Waterfall.vue'; -import WaterfallItem from './waterfall/WaterfallItem.vue'; export default { components: { Waterfall, - WaterfallItem }, props: { files: { type: Array, - default: () => [] + default: () => [], + }, + total: { + type: Number, + default: 0, }, fixed: { type: Boolean, - default: false + default: false, }, isPublic: { type: Boolean, - default: false + default: false, }, width: { type: Number, - default: 150 + default: 150, }, enableSearch: { type: Boolean, - default: true + default: true, }, enableToolbar: { type: Boolean, - default: true - } + default: true, + }, }, data() { return { showWaterfall: true, searchTerm: null, showList: false, - albums: [], + hoveredItems: [], isAlbumsModalActive: false, showingModalForFile: null, filesOffsetWaterfall: 0, filesOffsetEndWaterfall: 50, - filesPerPageWaterfall: 50 + filesPerPageWaterfall: 50, }; }, computed: { - user() { - return this.$store.state.user; - }, + ...mapState({ + user: (state) => state.auth.user, + albums: (state) => state.albums.tinyDetails, + images: (state) => state.images, + }), blank() { + // eslint-disable-next-line global-require, import/no-unresolved return require('@/assets/images/blank.png'); }, gridFiles() { return this.files; }, }, + created() { + this.getAlbums(); + }, methods: { async search() { - const data = await this.$search.do(this.searchTerm, [ - 'name', - 'original', - 'type', - 'albums:name' - ]); - console.log('> Search result data', data); + const data = await this.$search.do(this.searchTerm, ['name', 'original', 'type', 'albums:name']); + console.log('> Search result data', data); // eslint-disable-line no-console }, - deleteFile(file, index) { + deleteFile(file) { + // this.$emit('delete', file); this.$buefy.dialog.confirm({ title: 'Deleting file', message: 'Are you sure you want to <b>delete</b> this file?', confirmText: 'Delete File', type: 'is-danger', - hasIcon: true, onConfirm: async () => { - const response = await this.$axios.$delete(`file/${file.id}`); - if (this.showList) { - file.hideFromList = true; - this.$forceUpdate(); - } else { - this.showWaterfall = false; - this.files.splice(index, 1); - this.$nextTick(() => { - this.showWaterfall = true; - }); + try { + const response = await this.$store.dispatch('images/deleteFile', file.id); + + this.$buefy.toast.open(response.message); + } catch (e) { + this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true }); } - return this.$buefy.toast.open(response.message); - } + }, }); }, isAlbumSelected(id) { - if (!this.showingModalForFile) return; - const found = this.showingModalForFile.albums.find(el => el.id === id); - return found ? found.id ? true : false : false; + if (!this.showingModalForFile) return false; + const found = this.showingModalForFile.albums.find((el) => el.id === id); + return !!(found && found.id); }, async openAlbumModal(file) { + const { id } = file; this.showingModalForFile = file; this.showingModalForFile.albums = []; - this.isAlbumsModalActive = true; - const response = await this.$axios.$get(`file/${file.id}/albums`); - this.showingModalForFile.albums = response.albums; + try { + await this.$store.dispatch('images/getFileAlbums', id); + } catch (e) { + this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true }); + } + this.showingModalForFile.albums = this.images.fileAlbumsMap[id]; - this.getAlbums(); + this.isAlbumsModalActive = true; }, - async albumCheckboxClicked(value, id) { - const response = await this.$axios.$post(`file/album/${value ? 'add' : 'del'}`, { - albumId: id, - fileId: this.showingModalForFile.id - }); - this.$buefy.toast.open(response.message); + async albumCheckboxClicked(add, id) { + try { + let response; + if (add) { + response = await this.$store.dispatch('images/addToAlbum', { + albumId: id, + fileId: this.showingModalForFile.id, + }); + } else { + response = await this.$store.dispatch('images/removeFromAlbum', { + albumId: id, + fileId: this.showingModalForFile.id, + }); + } - // Not the prettiest solution to refetch on each click but it'll do for now - this.$parent.getFiles(); + this.$buefy.toast.open(response.message); + } catch (e) { + this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true }); + } }, async getAlbums() { - const response = await this.$axios.$get(`albums/dropdown`); - this.albums = response.albums; - this.$forceUpdate(); - } - } + try { + await this.$store.dispatch('albums/getTinyDetails'); + } catch (e) { + this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true }); + } + }, + mouseOver(id) { + const foundIndex = this.hoveredItems.indexOf(id); + if (foundIndex > -1) return; + this.hoveredItems.push(id); + }, + mouseOut(id) { + const foundIndex = this.hoveredItems.indexOf(id); + if (foundIndex > -1) this.hoveredItems.splice(foundIndex, 1); + }, + isHovered(id) { + return this.hoveredItems.includes(id); + }, + }, }; </script> <style lang="scss" scoped> - @import '~/assets/styles/_colors.scss'; - .item-move { - transition: all .25s cubic-bezier(.55,0,.1,1); - } +@import '~/assets/styles/_colors.scss'; +.item-move { + transition: all 0.25s cubic-bezier(0.55, 0, 0.1, 1); +} - div.toolbar { - padding: 1rem; +div.toolbar { + padding: 1rem; - .block { - text-align: right; - } + .block { + text-align: right; } +} - span.extension { - position: absolute; - width: 100%; - height: 100%; - z-index: 0; - top: 0; - left: 0; - display: flex; - align-items: center; - justify-content: center; - font-size: 2rem; - pointer-events: none; - opacity: .75; - max-width: 150px; - } +span.extension { + position: absolute; + width: 100%; + height: 100%; + z-index: 0; + top: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 2rem; + pointer-events: none; + opacity: 0.75; + max-width: 150px; +} - div.actions { - opacity: 0; - -webkit-transition: opacity 0.1s linear; - -moz-transition: opacity 0.1s linear; - -ms-transition: opacity 0.1s linear; - -o-transition: opacity 0.1s linear; - transition: opacity 0.1s linear; - position: absolute; - top: 0px; - left: 0px; - width: 100%; - height: calc(100% - 6px); - background: rgba(0, 0, 0, 0.5); - display: flex; - justify-content: center; - align-items: center; - - span { - padding: 3px; - &.more { - position: absolute; - top: 0; - right: 0; - } +div.preview { + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: calc(100% - 6px); + overflow: hidden; +} - &:nth-child(1), &:nth-child(2) { - align-items: flex-end; - } +.preview-container { + display: inline-block; +} - &:nth-child(1), &:nth-child(3) { - justify-content: flex-end; - } - a { +div.actions { + opacity: 0; + -webkit-transition: opacity 0.1s linear; + -moz-transition: opacity 0.1s linear; + -ms-transition: opacity 0.1s linear; + -o-transition: opacity 0.1s linear; + transition: opacity 0.1s linear; + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: calc(100% - 6px); + // background: rgba(0, 0, 0, 0.5); + background: linear-gradient(to top, rgba(0, 0, 0, 0.5) 0px, rgba(0, 0, 0, 0) 60px), + linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0px, rgba(0, 0, 0, 0) 45px); + display: flex; + justify-content: center; + align-items: flex-end; + + span { + padding: 3px; + + &.more { + position: absolute; + top: 0; + right: 0; + } + + &:nth-child(1), + &:nth-child(2) { + align-items: flex-end; + padding-bottom: 10px; + } + + &:nth-child(3), + &:nth-child(4) { + justify-content: flex-end; + padding-bottom: 10px; + } + + a { + width: 30px; + height: 30px; + color: white; + justify-content: center; + align-items: center; + display: flex; + &.btn:before { + content: ''; width: 30px; height: 30px; - color: white; - justify-content: center; - align-items: center; - display: flex; - &.btn:before { - content: ''; - width: 30px; - height: 30px; - border: 1px solid white; - border-radius: 50%; - position: absolute; - } + border: 1px solid white; + border-radius: 50%; + position: absolute; } } + } - &.fixed { - position: relative; - opacity: 1; - background: none; - - a { - width: auto; - height: auto; - color: $defaultTextColor; - &:before { - display: none; - } - } + &.fixed { + position: relative; + opacity: 1; + background: none; + a { + width: auto; + height: auto; + color: $defaultTextColor; + &:before { + display: none; + } } } +} - .albums-container { - display: flex; - flex-direction: row; - flex-wrap: wrap; - .album { - flex-basis: 33%; - text-align: left; - } +.albums-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + .album { + flex-basis: 33%; + text-align: left; } -</style> +} -<style lang="scss"> - .waterfall-item:hover { - div.actions { - opacity: 1 - } +.hidden { + display: none; +} + +.waterfall { + margin: 0 auto; +} + +.waterfall-item:hover { + div.actions { + opacity: 1; } +} </style> diff --git a/src/site/components/grid/waterfall/Waterfall.vue b/src/site/components/grid/waterfall/Waterfall.vue index 8631ea5..79a330a 100644 --- a/src/site/components/grid/waterfall/Waterfall.vue +++ b/src/site/components/grid/waterfall/Waterfall.vue @@ -1,180 +1,133 @@ -<style> - .waterfall { - position: relative; - } -</style> <template> - <div class="waterfall"> - <slot /> + <div ref="waterfall" class="waterfall"> + <WaterfallItem + v-for="(item, index) in items" + :key="item.id" + :style="{ width: `${itemWidth}px`, marginBottom: `${gutterHeight}px` }" + :width="itemWidth"> + <slot :item="item" /> + </WaterfallItem> </div> </template> <script> -// import {quickSort, getMinIndex, _, sum} from './util' - -const quickSort = (arr, type) => { - const left = []; - const right = []; - if (arr.length <= 1) { - return arr; - } - const povis = arr[0]; - for (let i = 1; i < arr.length; i++) { - if (arr[i][type] < povis[type]) { - left.push(arr[i]); - } else { - right.push(arr[i]); - } - } - return quickSort(left, type).concat(povis, quickSort(right, type)) -}; - -const getMinIndex = arr => { - let pos = 0; - for (let i = 0; i < arr.length; i++) { - if (arr[pos] > arr[i]) { - pos = i; - } - } - return pos; -}; +import WaterfallItem from './WaterfallItem.vue'; -const _ = { - on(el, type, func, capture = false) { - el.addEventListener(type, func, capture); - }, - off(el, type, func, capture = false) { - el.removeEventListener(type, func, capture); - } -}; +const isBrowser = typeof window !== 'undefined'; +const Masonry = isBrowser ? window.Masonry || require('masonry-layout') : null; +const imagesloaded = isBrowser ? require('imagesloaded') : null; -const sum = arr => arr.reduce((sum, val) => sum + val); export default { name: 'Waterfall', + components: { + WaterfallItem, + }, props: { - gutterWidth: { - type: Number, - default: 0 - }, - gutterHeight: { - type: Number, - default: 0 - }, - resizable: { - type: Boolean, - default: true + options: { + type: Object, + default: () => {}, }, - align: { - type: String, - default: 'center' + items: { + type: Array, + default: () => [], }, - fixWidth: { - type: Number + itemWidth: { + type: Number, + default: 150, }, - minCol: { + gutterWidth: { type: Number, - default: 1 + default: 10, }, - maxCol: { - type: Number + gutterHeight: { + type: Number, + default: 4, }, - percent: { - type: Array - } }, - data() { - return { - timer: null, - colNum: 0, - lastWidth: 0, - percentWidthArr: [] - }; + mounted() { + this.initializeMasonry(); + this.imagesLoaded(); }, - created() { - this.$on('itemRender', () => { - if (this.timer) { - clearTimeout(this.timer); - } - this.timer = setTimeout(() => { - this.render(); - }, 0); - }); + updated() { + this.performLayout(); + this.imagesLoaded(); }, - mounted() { - this.resizeHandle(); - this.$watch('resizable', this.resizeHandle); + unmounted() { + this.masonry.destroy(); }, methods: { - calulate(arr) { - let pageWidth = this.fixWidth ? this.fixWidth : this.$el.offsetWidth; - // 百分比布局计算 - if (this.percent) { - this.colNum = this.percent.length; - const total = sum(this.percent); - this.percentWidthArr = this.percent.map(value => (value / total) * pageWidth); - this.lastWidth = 0; - // 正常布局计算 - } else { - this.colNum = parseInt(pageWidth / (arr.width + this.gutterWidth)); - if (this.minCol && this.colNum < this.minCol) { - this.colNum = this.minCol; - this.lastWidth = 0; - } else if (this.maxCol && this.colNum > this.maxCol) { - this.colNum = this.maxCol; - this.lastWidth = pageWidth - (arr.width + this.gutterWidth) * this.colNum + this.gutterWidth; - } else { - this.lastWidth = pageWidth - (arr.width + this.gutterWidth) * this.colNum + this.gutterWidth; - } + imagesLoaded() { + const node = this.$refs.waterfall; + imagesloaded( + node, + () => { + this.masonry.layout(); + }, + ); + }, + performLayout() { + const diff = this.diffDomChildren(); + if (diff.removed.length > 0) { + this.masonry.remove(diff.removed); + this.masonry.reloadItems(); + } + if (diff.appended.length > 0) { + this.masonry.appended(diff.appended); + this.masonry.reloadItems(); + } + if (diff.prepended.length > 0) { + this.masonry.prepended(diff.prepended); } + if (diff.moved.length > 0) { + this.masonry.reloadItems(); + } + this.masonry.layout(); + }, + diffDomChildren() { + const oldChildren = this.domChildren.filter((element) => !!element.parentNode); + const newChildren = this.getNewDomChildren(); + const removed = oldChildren.filter((oldChild) => !newChildren.includes(oldChild)); + const domDiff = newChildren.filter((newChild) => !oldChildren.includes(newChild)); + const prepended = domDiff.filter((newChild, index) => newChildren[index] === newChild); + const appended = domDiff.filter((el) => !prepended.includes(el)); + let moved = []; + if (removed.length === 0) { + moved = oldChildren.filter((child, index) => index !== newChildren.indexOf(child)); + } + this.domChildren = newChildren; + return { + old: oldChildren, + new: newChildren, + removed, + appended, + prepended, + moved, + }; }, - resizeHandle() { - if (this.resizable) { - _.on(window, 'resize', this.render, false); - } else { - _.off(window, 'resize', this.render, false); + initializeMasonry() { + if (!this.masonry) { + this.masonry = new Masonry( + this.$refs.waterfall, + { + columnWidth: this.itemWidth, + gutter: this.gutterWidth, + ...this.options, + }, + ); + this.domChildren = this.getNewDomChildren(); } }, - render() { - // 重新排序 - let childArr = []; - childArr = this.$children.map(child => child.getMeta()); - childArr = quickSort(childArr, 'order'); - // 计算列数 - this.calulate(childArr[0]) - let offsetArr = Array(this.colNum).fill(0); - // 渲染 - childArr.forEach(child => { - let position = getMinIndex(offsetArr); - // 百分比布局渲染 - if (this.percent) { - let left = 0; - child.el.style.width = `${this.percentWidthArr[position]}px`; - if (position === 0) { - left = 0; - } else { - for (let i = 0; i < position; i++) { - left += this.percentWidthArr[i]; - } - } - child.el.style.left = `${left}px`; - // 正常布局渲染 - } else { - if (this.align === 'left') { // eslint-disable-line no-lonely-if - child.el.style.left = `${position * (child.width + this.gutterWidth)}px`; - } else if (this.align === 'right') { - child.el.style.left = `${position * (child.width + this.gutterWidth) + this.lastWidth}px`; - } else { - child.el.style.left = `${position * (child.width + this.gutterWidth) + this.lastWidth / 2}px`; - } - } - if (child.height === 0) { - return; - } - child.el.style.top = `${offsetArr[position]}px`; - offsetArr[position] += (child.height + this.gutterHeight); - this.$el.style.height = `${Math.max.apply(Math, offsetArr)}px`; - }); - this.$emit('rendered', this); - } - } + getNewDomChildren() { + const node = this.$refs.waterfall; + const children = this.options && this.options.itemSelector + ? node.querySelectorAll(this.options.itemSelector) : node.children; + return Array.prototype.slice.call(children); + }, + }, }; </script> + +<style lang="scss" scoped> +.wfi { + +} +</style> diff --git a/src/site/components/grid/waterfall/WaterfallItem.vue b/src/site/components/grid/waterfall/WaterfallItem.vue index a02ea1f..c5cade1 100644 --- a/src/site/components/grid/waterfall/WaterfallItem.vue +++ b/src/site/components/grid/waterfall/WaterfallItem.vue @@ -1,60 +1,10 @@ -<style> - .waterfall-item { - position: absolute; - } -</style> <template> <div class="waterfall-item"> <slot /> </div> </template> <script> -import imagesLoaded from 'imagesloaded'; export default { name: 'WaterfallItem', - props: { - order: { - type: Number, - default: 0 - }, - width: { - type: Number, - default: 150 - } - }, - data() { - return { - itemWidth: 0, - height: 0 - }; - }, - created() { - this.$watch(() => this.height, this.emit); - }, - mounted() { - this.$el.style.display = 'none'; - this.$el.style.width = `${this.width}px`; - this.emit(); - imagesLoaded(this.$el, () => { - this.$el.style.left = '-9999px'; - this.$el.style.top = '-9999px'; - this.$el.style.display = 'block'; - this.height = this.$el.offsetHeight; - this.itemWidth = this.$el.offsetWidth; - }); - }, - methods: { - emit() { - this.$parent.$emit('itemRender'); - }, - getMeta() { - return { - el: this.$el, - height: this.height, - width: this.itemWidth, - order: this.order - }; - } - } -} +}; </script> diff --git a/src/site/components/imageInfo/ImageInfo.vue b/src/site/components/imageInfo/ImageInfo.vue new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/site/components/imageInfo/ImageInfo.vue diff --git a/src/site/components/loading/BulmaLoading.vue b/src/site/components/loading/BulmaLoading.vue new file mode 100644 index 0000000..37cc5a5 --- /dev/null +++ b/src/site/components/loading/BulmaLoading.vue @@ -0,0 +1,33 @@ +<template> + <div class="loader-wrapper"> + <div class="loader is-loading" /> + </div> +</template> + +<style lang="scss" scoped> + .loader-wrapper { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + background: #fff; + opacity: 0; + z-index: -1; + transition: opacity .3s; + display: flex; + justify-content: center; + align-items: center; + border-radius: 6px; + + .loader { + height: 80px; + width: 80px; + } + + &.is-active { + opacity: 1; + z-index: 1; + } + } +</style> diff --git a/src/site/components/loading/CubeShadow.vue b/src/site/components/loading/CubeShadow.vue index af31dac..bbfdb52 100644 --- a/src/site/components/loading/CubeShadow.vue +++ b/src/site/components/loading/CubeShadow.vue @@ -1,5 +1,6 @@ <template> - <div :style="styles" + <div + :style="styles" class="spinner spinner--cube-shadow" /> </template> @@ -8,16 +9,16 @@ export default { props: { size: { type: String, - default: '60px' + default: '60px', }, background: { type: String, - default: '#9C27B0' + default: '#9C27B0', }, duration: { type: String, - default: '1.8s' - } + default: '1.8s', + }, }, computed: { styles() { @@ -25,10 +26,10 @@ export default { width: this.size, height: this.size, backgroundColor: this.background, - animationDuration: this.duration + animationDuration: this.duration, }; - } - } + }, + }, }; </script> diff --git a/src/site/components/loading/Origami.vue b/src/site/components/loading/Origami.vue index d1b523d..cd1c087 100644 --- a/src/site/components/loading/Origami.vue +++ b/src/site/components/loading/Origami.vue @@ -1,7 +1,9 @@ <template> - <div :style="styles" + <div + :style="styles" class="spinner spinner-origami"> - <div :style="innerStyles" + <div + :style="innerStyles" class="spinner-inner loading"> <span class="slice" /> <span class="slice" /> @@ -18,21 +20,21 @@ export default { props: { size: { type: String, - default: '40px' - } + default: '40px', + }, }, computed: { innerStyles() { - let size = parseInt(this.size); + const size = parseInt(this.size, 10); return { transform: `scale(${(size / 60)})` }; }, styles() { return { width: this.size, - height: this.size + height: this.size, }; - } - } + }, + }, }; </script> diff --git a/src/site/components/loading/PingPong.vue b/src/site/components/loading/PingPong.vue index ac33e28..d562e9f 100644 --- a/src/site/components/loading/PingPong.vue +++ b/src/site/components/loading/PingPong.vue @@ -1,12 +1,14 @@ <template> - <div :style="styles" + <div + :style="styles" class="spinner spinner--ping-pong"> - <div :style="innerStyles" + <div + :style="innerStyles" class="spinner-inner"> <div class="board"> - <div class="left"/> - <div class="right"/> - <div class="ball"/> + <div class="left" /> + <div class="right" /> + <div class="ball" /> </div> </div> </div> @@ -17,22 +19,22 @@ export default { props: { size: { type: String, - default: '60px' - } + default: '60px', + }, }, computed: { innerStyles() { - let size = parseInt(this.size); + const size = parseInt(this.size, 10); return { transform: `scale(${size / 250})` }; }, styles() { return { width: this.size, - height: this.size + height: this.size, }; - } - } -} + }, + }, +}; </script> <style lang="scss" scoped> diff --git a/src/site/components/loading/RotateSquare.vue b/src/site/components/loading/RotateSquare.vue index 4da8300..089e01a 100644 --- a/src/site/components/loading/RotateSquare.vue +++ b/src/site/components/loading/RotateSquare.vue @@ -1,5 +1,6 @@ <template> - <div :style="styles" + <div + :style="styles" class="spinner spinner--rotate-square-2" /> </template> @@ -8,18 +9,18 @@ export default { props: { size: { type: String, - default: '40px' - } + default: '40px', + }, }, computed: { styles() { return { width: this.size, height: this.size, - display: 'inline-block' + display: 'inline-block', }; - } - } + }, + }, }; </script> diff --git a/src/site/components/navbar/Navbar.vue b/src/site/components/navbar/Navbar.vue index 47f90cb..fb78631 100644 --- a/src/site/components/navbar/Navbar.vue +++ b/src/site/components/navbar/Navbar.vue @@ -1,5 +1,6 @@ <template> - <b-navbar :class="{ isWhite }" + <b-navbar + :class="{ isWhite }" transparent> <template slot="end"> <b-navbar-item tag="div"> @@ -65,32 +66,31 @@ </template> <script> +import { mapState, mapGetters } from 'vuex'; + export default { props: { isWhite: { type: Boolean, - default: false - } + default: false, + }, }, data() { return { hamburger: false }; }, computed: { - loggedIn() { - return this.$store.state.loggedIn; - }, - config() { - return this.$store.state.config; - } + ...mapGetters({ loggedIn: 'auth/isLoggedIn' }), + ...mapState(['config']), }, methods: { - logOut() { - this.$store.dispatch('logout'); + async logOut() { + await this.$store.dispatch('auth/logout'); this.$router.replace('/login'); - } - } + }, + }, }; </script> + <style lang="scss" scoped> @import '~/assets/styles/_colors.scss'; nav.navbar { diff --git a/src/site/components/sidebar/Sidebar.vue b/src/site/components/sidebar/Sidebar.vue index a2ad3f4..d586122 100644 --- a/src/site/components/sidebar/Sidebar.vue +++ b/src/site/components/sidebar/Sidebar.vue @@ -1,64 +1,80 @@ <template> - <div class="dashboard-menu"> - <router-link to="/dashboard"> - <i class="icon-com-pictures" />Files - </router-link> - <router-link to="/dashboard/albums"> - <i class="icon-interface-window" />Albums - </router-link> - <!-- - <router-link to="/dashboard/tags"> - <i class="icon-ecommerce-tag-c" />Tags - </router-link> - --> - <router-link to="/dashboard/account"> - <i class="icon-ecommerce-tag-c" />Account - </router-link> - <template v-if="user && user.isAdmin"> - <router-link to="/dashboard/admin/users"> - <i class="icon-setting-gear-a" />Users - </router-link> - <!-- - TODO: Dont wanna deal with this now - <router-link to="/dashboard/admin/settings"> - <i class="icon-setting-gear-a" />Settings - </router-link> - --> - </template> - </div> + <b-menu class="dashboard-menu"> + <b-menu-list label="Menu"> + <b-menu-item + class="item" + icon="information-outline" + label="Dashboard" + tag="nuxt-link" + to="/dashboard" + exact /> + <b-menu-item + class="item" + icon="image-multiple-outline" + label="Albums" + tag="nuxt-link" + to="/dashboard/albums" + exact /> + <b-menu-item + class="item" + icon="tag-outline" + label="Tags" + tag="nuxt-link" + to="/dashboard/tags" + exact /> + <b-menu-item icon="settings" expanded> + <template slot="label" slot-scope="props"> + Administration + <b-icon class="is-pulled-right" :icon="props.expanded ? 'menu-down' : 'menu-up'" /> + </template> + <b-menu-item icon="account" label="Users" tag="nuxt-link" to="/dashboard/admin/users" exact /> + <b-menu-item icon="cog-outline" label="Settings" tag="nuxt-link" to="/dashboard/admin/settings" exact /> + </b-menu-item> + <b-menu-item + class="item" + icon="account-cog-outline" + label="My account" + tag="nuxt-link" + to="/dashboard/account" + exact /> + </b-menu-list> + <b-menu-list label="Actions"> + <b-menu-item icon="exit-to-app" label="Logout" tag="nuxt-link" to="/logout" exact /> + </b-menu-list> + </b-menu> </template> <script> +import { mapState } from 'vuex'; + export default { - computed: { - user() { - return this.$store.state.user; - } - } + computed: mapState({ + user: (state) => state.auth.user, + }), + methods: { + isRouteActive(id) { + if (this.$route.path.includes(id)) { + return true; + } + return false; + }, + }, }; + </script> <style lang="scss" scoped> @import '~/assets/styles/_colors.scss'; .dashboard-menu { - padding: 2rem; - border-radius: 8px; + ::v-deep a:hover { + cursor: pointer; + text-decoration: none; + } - a { - display: block; - font-weight: 700; - color: $textColor; - position: relative; - padding-left: 40px; - height: 35px; - &:hover{ - color: white; - } + ::v-deep .icon { + margin-right: 0.5rem; + } - i { - position: absolute; - font-size: 1.5em; - top: -4px; - left: 5px; - } + ::v-deep .icon.is-pulled-right { + margin-right: 0; } hr { margin-top: 0.6em; } diff --git a/src/site/components/uploader/Uploader.vue b/src/site/components/uploader/Uploader.vue index 1b03ff8..7e2d446 100644 --- a/src/site/components/uploader/Uploader.vue +++ b/src/site/components/uploader/Uploader.vue @@ -1,7 +1,9 @@ <template> - <div :class="{ 'has-files': alreadyAddedFiles }" + <div + :class="{ 'has-files': alreadyAddedFiles }" class="uploader-wrapper"> - <b-select v-if="loggedIn" + <b-select + v-if="loggedIn" v-model="selectedAlbum" placeholder="Upload to album" size="is-medium" @@ -13,7 +15,8 @@ {{ album.name }} </option> </b-select> - <dropzone v-if="showDropzone" + <dropzone + v-if="showDropzone" id="dropzone" ref="el" :options="dropzoneOptions" @@ -25,16 +28,22 @@ Add or drop more files </label> - <div id="template" + <div + id="template" ref="template"> <div class="dz-preview dz-file-preview"> <div class="dz-details"> - <div class="dz-filename"><span data-dz-name /></div> - <div class="dz-size"><span data-dz-size /></div> + <div class="dz-filename"> + <span data-dz-name /> + </div> + <div class="dz-size"> + <span data-dz-size /> + </div> </div> <div class="result"> <div class="openLink"> - <a class="link" + <a + class="link" target="_blank"> Link </a> @@ -43,14 +52,16 @@ <div class="error"> <div> <span> - <span class="error-message" + <span + class="error-message" data-dz-errormessage /> <i class="icon-web-warning" /> </span> </div> </div> <div class="dz-progress"> - <span class="dz-upload" + <span + class="dz-upload" data-dz-uploadprogress /> </div> <!-- @@ -64,6 +75,8 @@ </template> <script> +import { mapState, mapGetters } from 'vuex'; + import Dropzone from 'nuxt-dropzone'; import '~/assets/styles/dropzone.scss'; @@ -75,20 +88,15 @@ export default { files: [], dropzoneOptions: {}, showDropzone: false, - albums: [], - selectedAlbum: null + selectedAlbum: null, }; }, computed: { - config() { - return this.$store.state.config; - }, - token() { - return this.$store.state.token; - }, - loggedIn() { - return this.$store.state.loggedIn; - } + ...mapState({ + config: (state) => state.config, + albums: (state) => state.albums.tinyDetails, + }), + ...mapGetters({ loggedIn: 'auth/isLoggedIn', token: 'auth/getToken' }), }, watch: { loggedIn() { @@ -96,7 +104,7 @@ export default { }, selectedAlbum() { this.updateDropzoneConfig(); - } + }, }, mounted() { this.dropzoneOptions = { @@ -119,7 +127,7 @@ export default { maxFilesize: this.config.maxFileSize, previewTemplate: this.$refs.template.innerHTML, dictDefaultMessage: 'Drag & Drop your files or click to browse', - headers: { Accept: 'application/vnd.lolisafe.json' } + headers: { Accept: 'application/vnd.lolisafe.json' }, }; this.showDropzone = true; if (this.loggedIn) this.getAlbums(); @@ -129,8 +137,11 @@ export default { Get all available albums so the user can upload directly to one (or several soon™) of them. */ async getAlbums() { - const response = await this.$axios.$get(`albums/dropdown`); - this.albums = response.albums; + try { + await this.$store.dispatch('albums/getTinyDetails'); + } catch (e) { + this.$store.dispatch('alert/set', { text: e.message, error: true }, { root: true }); + } this.updateDropzoneConfig(); }, @@ -143,14 +154,14 @@ export default { this.$refs.el.setOption('headers', { Accept: 'application/vnd.lolisafe.json', Authorization: this.token ? `Bearer ${this.token}` : '', - albumId: this.selectedAlbum ? this.selectedAlbum : null + albumId: this.selectedAlbum ? this.selectedAlbum : null, }); }, /* Dropzone stuff */ - dropzoneFilesAdded(files) { + dropzoneFilesAdded() { this.alreadyAddedFiles = true; }, dropzoneSuccess(file, response) { @@ -159,8 +170,9 @@ export default { dropzoneError(file, message, xhr) { this.$store.dispatch('alert', { text: 'There was an error uploading this file. Check the console.', - error: true + error: true, }); + // eslint-disable-next-line no-console console.error(file, message, xhr); }, async dropzoneChunksUploaded(file, done) { @@ -170,12 +182,11 @@ export default { original: file.name, size: file.size, type: file.type, - count: file.upload.totalChunkCount - }] + count: file.upload.totalChunkCount, + }], }); this.processResult(file, data); - this.$forceUpdate(); return done(); }, @@ -194,8 +205,8 @@ export default { this.$clipboard(response.url); }); */ - } - } + }, + }, }; </script> <style lang="scss" scoped> |