diff options
Diffstat (limited to 'src/site/pages/dashboard')
| -rw-r--r-- | src/site/pages/dashboard/account.vue | 187 | ||||
| -rw-r--r-- | src/site/pages/dashboard/admin/file/_id.vue | 245 | ||||
| -rw-r--r-- | src/site/pages/dashboard/admin/settings.vue | 217 | ||||
| -rw-r--r-- | src/site/pages/dashboard/admin/user/_id.vue | 140 | ||||
| -rw-r--r-- | src/site/pages/dashboard/admin/users.vue | 270 | ||||
| -rw-r--r-- | src/site/pages/dashboard/albums/_id.vue | 125 | ||||
| -rw-r--r-- | src/site/pages/dashboard/albums/index.vue | 348 | ||||
| -rw-r--r-- | src/site/pages/dashboard/index.vue | 138 | ||||
| -rw-r--r-- | src/site/pages/dashboard/tags/index.vue | 152 |
9 files changed, 923 insertions, 899 deletions
diff --git a/src/site/pages/dashboard/account.vue b/src/site/pages/dashboard/account.vue index 6ecc885..5610495 100644 --- a/src/site/pages/dashboard/account.vue +++ b/src/site/pages/dashboard/account.vue @@ -1,64 +1,94 @@ <template> - <section class="hero is-fullheight dashboard"> - <div class="hero-body"> - <div class="container"> - <div class="columns"> - <div class="column is-narrow"> - <Sidebar /> - </div> - <div class="column"> - <h2 class="subtitle">Account settings</h2> - <hr> + <section class="section is-fullheight dashboard"> + <div class="container"> + <div class="columns"> + <div class="column is-narrow"> + <Sidebar /> + </div> + <div class="column"> + <h2 class="subtitle"> + Account settings + </h2> + <hr> - <b-field label="Username" - message="Nothing to do here" - horizontal> - <b-input v-model="user.username" - expanded - disabled /> - </b-field> + <b-field + label="Username" + message="Nothing to do here" + horizontal> + <b-input + class="lolisafe-input" + :value="user.username" + expanded + disabled /> + </b-field> - <b-field label="Current password" - message="If you want to change your password input the current one here" - horizontal> - <b-input v-model="user.password" - type="password" - expanded /> - </b-field> + <b-field + label="Current password" + message="If you want to change your password input the current one here" + horizontal> + <b-input + v-model="password" + class="lolisafe-input" + type="password" + expanded /> + </b-field> - <b-field label="New password" - message="Your new password" - horizontal> - <b-input v-model="user.newPassword" - type="password" - expanded /> - </b-field> + <b-field + label="New password" + message="Your new password" + horizontal> + <b-input + v-model="newPassword" + class="lolisafe-input" + type="password" + expanded /> + </b-field> - <b-field label="New password again" - message="Your new password once again" - horizontal> - <b-input v-model="user.reNewPassword" - type="password" - expanded /> - </b-field> + <b-field + label="New password again" + message="Your new password once again" + horizontal> + <b-input + v-model="reNewPassword" + class="lolisafe-input" + type="password" + expanded /> + </b-field> - <div class="mb2 mt2 text-center"> - <button class="button is-primary" - @click="changePassword">Change password</button> - </div> + <div class="mb2 mt2 text-center"> + <b-button + type="is-lolisafe" + @click="changePassword"> + Change password + </b-button> + </div> - <b-field label="Api key" - message="This API key lets you use the service from other apps" - horizontal> - <b-input v-model="user.apiKey" + <b-field + label="API key" + message="This API key lets you use the service from other apps" + horizontal> + <b-field expanded> + <b-input + class="lolisafe-input" + :value="apiKey" expanded disabled /> + <p class="control"> + <b-button + type="is-lolisafe" + @click="copyKey"> + Copy + </b-button> + </p> </b-field> + </b-field> - <div class="mb2 mt2 text-center"> - <button class="button is-primary" - @click="promptNewAPIKey">Request new API key</button> - </div> + <div class="mb2 mt2 text-center"> + <b-button + type="is-lolisafe" + @click="promptNewAPIKey"> + Request new API key + </b-button> </div> </div> </div> @@ -67,51 +97,62 @@ </template> <script> +import { mapState, mapActions, mapGetters } from 'vuex'; import Sidebar from '~/components/sidebar/Sidebar.vue'; export default { components: { Sidebar }, - middleware: 'auth', + middleware: ['auth', ({ store }) => { + store.dispatch('auth/fetchCurrentUser'); + }], data() { return { - user: {} + password: '', + newPassword: '', + reNewPassword: '' }; }, + computed: { + ...mapGetters({ 'apiKey': 'auth/getApiKey' }), + ...mapState({ + user: (state) => state.auth.user + }) + }, metaInfo() { return { title: 'Account' }; }, - mounted() { - this.getUserSetttings(); - }, methods: { - async getUserSetttings() { - const response = await this.$axios.$get(`users/me`); - this.user = response.user; - }, + ...mapActions({ + getUserSetttings: 'auth/fetchCurrentUser' + }), async changePassword() { - if (!this.user.password || !this.user.newPassword || !this.user.reNewPassword) { - this.$store.dispatch('alert', { + const { password, newPassword, reNewPassword } = this; + + if (!password || !newPassword || !reNewPassword) { + this.$store.dispatch('alert/set', { text: 'One or more fields are missing', error: true }); return; } - if (this.user.newPassword !== this.user.reNewPassword) { - this.$store.dispatch('alert', { + if (newPassword !== reNewPassword) { + this.$store.dispatch('alert/set', { text: 'Passwords don\'t match', error: true }); return; } - const response = await this.$axios.$post(`user/password/change`, - { - password: this.user.password, - newPassword: this.user.newPassword - }); - this.$buefy.toast.open(response.message); + const response = await this.$store.dispatch('auth/changePassword', { + password, + newPassword + }); + + if (response) { + this.$buefy.toast.open(response.message); + } }, promptNewAPIKey() { this.$buefy.dialog.confirm({ @@ -120,10 +161,12 @@ export default { onConfirm: () => this.requestNewAPIKey() }); }, + copyKey() { + this.$clipboard(this.apiKey); + this.$notifier.success('API key copied to clipboard'); + }, async requestNewAPIKey() { - const response = await this.$axios.$post(`user/apikey/change`); - this.user.apiKey = response.apiKey; - this.$forceUpdate(); + const response = await this.$store.dispatch('auth/requestAPIKey'); this.$buefy.toast.open(response.message); } } diff --git a/src/site/pages/dashboard/admin/file/_id.vue b/src/site/pages/dashboard/admin/file/_id.vue index 6718b32..d54bf54 100644 --- a/src/site/pages/dashboard/admin/file/_id.vue +++ b/src/site/pages/dashboard/admin/file/_id.vue @@ -2,97 +2,119 @@ .underline { text-decoration: underline; } </style> <template> - <section class="hero is-fullheight dashboard"> - <div class="hero-body"> - <div class="container"> - <div class="columns"> - <div class="column is-narrow"> - <Sidebar /> - </div> - <div class="column"> - <h2 class="subtitle">File details</h2> - <hr> - - <div class="columns"> - <div class="column is-6"> - <b-field label="ID" - horizontal> - <span>{{ file.id }}</span> - </b-field> - - <b-field label="Name" - horizontal> - <span>{{ file.name }}</span> - </b-field> - - <b-field label="Original Name" - horizontal> - <span>{{ file.original }}</span> - </b-field> - - <b-field label="IP" - horizontal> - <span class="underline">{{ file.ip }}</span> - </b-field> - - <b-field label="Link" - horizontal> - <a :href="file.url" - target="_blank">{{ file.url }}</a> - </b-field> - - <b-field label="Size" - horizontal> - <span>{{ formatBytes(file.size) }}</span> - </b-field> - - <b-field label="Hash" - horizontal> - <span>{{ file.hash }}</span> - </b-field> - - <b-field label="Uploaded" - horizontal> - <span><timeago :since="file.createdAt" /></span> - </b-field> - </div> - <div class="column is-6"> - <b-field label="User Id" - horizontal> - <span>{{ user.id }}</span> - </b-field> - - <b-field label="Username" - horizontal> - <span>{{ user.username }}</span> - </b-field> - - <b-field label="Enabled" - horizontal> - <span>{{ user.enabled }}</span> - </b-field> - - <b-field label="Registered" - horizontal> - <span><timeago :since="user.createdAt" /></span> - </b-field> - - <b-field label="Files" - horizontal> - <span> - <nuxt-link :to="`/dashboard/admin/user/${user.id}`">{{ user.fileCount }}</nuxt-link> - </span> - </b-field> - </div> + <section class="section is-fullheight dashboard"> + <div class="container"> + <div class="columns"> + <div class="column is-narrow"> + <Sidebar /> + </div> + <div class="column"> + <h2 class="subtitle"> + File details + </h2> + <hr> + + <div class="columns"> + <div class="column is-6"> + <b-field + label="ID" + horizontal> + <span>{{ admin.file.id }}</span> + </b-field> + + <b-field + label="Name" + horizontal> + <span>{{ admin.file.name }}</span> + </b-field> + + <b-field + label="Original Name" + horizontal> + <span>{{ admin.file.original }}</span> + </b-field> + + <b-field + label="IP" + horizontal> + <span class="underline">{{ admin.file.ip }}</span> + </b-field> + + <b-field + label="Link" + horizontal> + <a + :href="admin.file.url" + target="_blank">{{ admin.file.url }}</a> + </b-field> + + <b-field + label="Size" + horizontal> + <span>{{ formatBytes(admin.file.size) }}</span> + </b-field> + + <b-field + label="Hash" + horizontal> + <span>{{ admin.file.hash }}</span> + </b-field> + + <b-field + label="Uploaded" + horizontal> + <span><timeago :since="admin.file.createdAt" /></span> + </b-field> </div> - - <div class="mb2 mt2 text-center"> - <button class="button is-danger" - @click="promptBanIP">Ban IP</button> - <button class="button is-danger" - @click="promptDisableUser">Disable user</button> + <div class="column is-6"> + <b-field + label="User Id" + horizontal> + <span>{{ admin.user.id }}</span> + </b-field> + + <b-field + label="Username" + horizontal> + <span>{{ admin.user.username }}</span> + </b-field> + + <b-field + label="Enabled" + horizontal> + <span>{{ admin.user.enabled }}</span> + </b-field> + + <b-field + label="Registered" + horizontal> + <span><timeago :since="admin.user.createdAt" /></span> + </b-field> + + <b-field + label="Files" + horizontal> + <span> + <nuxt-link :to="`/dashboard/admin/user/${admin.user.id}`">{{ admin.user.fileCount }}</nuxt-link> + </span> + </b-field> </div> </div> + + <div class="mb2 mt2 text-center"> + <b-button + v-if="admin.user.id !== auth.user.id" + type="is-danger" + @click="promptBanIP"> + Ban IP + </b-button> + <b-button + v-if="admin.user.id !== auth.user.id" + type="is-danger" + @click="promptDisableUser"> + Disable user + </b-button> + </div> </div> </div> </div> @@ -100,59 +122,42 @@ </template> <script> +import { mapState } from 'vuex'; import Sidebar from '~/components/sidebar/Sidebar.vue'; export default { components: { Sidebar }, - middleware: ['auth', 'admin'], - data() { - return { - options: {}, - file: null, - user: null - }; - }, - async asyncData({ $axios, route }) { + middleware: ['auth', 'admin', ({ route, store }) => { try { - const response = await $axios.$get(`file/${route.params.id}`); - return { - file: response.file ? response.file : null, - user: response.user ? response.user : null - }; - } catch (error) { - console.error(error); - return { - file: null, - user: null - }; + store.dispatch('admin/fetchFile', route.params.id); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); } - }, + }], + computed: mapState(['admin', 'auth']), methods: { promptDisableUser() { this.$buefy.dialog.confirm({ + type: 'is-danger', message: 'Are you sure you want to disable the account of the user that uploaded this file?', onConfirm: () => this.disableUser() }); }, - async disableUser() { - const response = await this.$axios.$post('admin/users/disable', { - id: this.user.id - }); - this.$buefy.toast.open(response.message); + disableUser() { + this.$handler.executeAction('admin/disableUser', this.user.id); }, promptBanIP() { this.$buefy.dialog.confirm({ + type: 'is-danger', message: 'Are you sure you want to ban the IP this file was uploaded from?', onConfirm: () => this.banIP() }); }, - async banIP() { - const response = await this.$axios.$post('admin/ban/ip', { - ip: this.file.ip - }); - this.$buefy.toast.open(response.message); + banIP() { + this.$handler.executeAction('admin/banIP', this.file.ip); }, formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes'; @@ -163,7 +168,7 @@ export default { const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; + return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`; } } }; diff --git a/src/site/pages/dashboard/admin/settings.vue b/src/site/pages/dashboard/admin/settings.vue index 052a641..c6a9ade 100644 --- a/src/site/pages/dashboard/admin/settings.vue +++ b/src/site/pages/dashboard/admin/settings.vue @@ -1,94 +1,123 @@ <template> - <section class="hero is-fullheight dashboard"> - <div class="hero-body"> - <div class="container"> - <div class="columns"> - <div class="column is-narrow"> - <Sidebar /> - </div> - <div class="column"> - <h2 class="subtitle">Service settings</h2> - <hr> + <section class="section is-fullheight dashboard"> + <div class="container"> + <div class="columns"> + <div class="column is-narrow"> + <Sidebar /> + </div> + <div class="column"> + <h2 class="subtitle"> + Service settings + </h2> + <hr> - <b-field label="Service name" - message="Please enter the name which this service is gonna be identified as" - horizontal> - <b-input v-model="options.serviceName" - expanded /> - </b-field> + <b-field + label="Service name" + message="Please enter the name which this service is gonna be identified as" + horizontal> + <b-input + v-model="settings.serviceName" + class="lolisafe-input" + expanded /> + </b-field> - <b-field label="Upload folder" - message="Where to store the files relative to the working directory" - horizontal> - <b-input v-model="options.uploadFolder" - expanded /> - </b-field> + <b-field + label="Upload folder" + message="Where to store the files relative to the working directory" + horizontal> + <b-input + v-model="settings.uploadFolder" + class="lolisafe-input" + expanded /> + </b-field> - <b-field label="Links per album" - message="Maximum links allowed per album" - horizontal> - <b-input v-model="options.linksPerAlbum" - type="number" - expanded /> - </b-field> + <b-field + label="Links per album" + message="Maximum links allowed per album" + horizontal> + <b-input + v-model="settings.linksPerAlbum" + class="lolisafe-input" + type="number" + expanded /> + </b-field> - <b-field label="Max upload size" - message="Maximum allowed file size in MB" - horizontal> - <b-input v-model="options.maxUploadSize" - expanded /> - </b-field> + <b-field + label="Max upload size" + message="Maximum allowed file size in MB" + horizontal> + <b-input + v-model="settings.maxUploadSize" + class="lolisafe-input" + expanded /> + </b-field> - <b-field label="Filename length" - message="How many characters long should the generated filenames be" - horizontal> - <b-input v-model="options.filenameLength" - expanded /> - </b-field> + <b-field + label="Filename length" + message="How many characters long should the generated filenames be" + horizontal> + <b-input + v-model="settings.filenameLength" + class="lolisafe-input" + expanded /> + </b-field> - <b-field label="Album link length" - message="How many characters a link for an album should have" - horizontal> - <b-input v-model="options.albumLinkLength" - expanded /> - </b-field> + <b-field + label="Album link length" + message="How many characters a link for an album should have" + horizontal> + <b-input + v-model="settings.albumLinkLength" + class="lolisafe-input" + expanded /> + </b-field> - <b-field label="Generate thumbnails" - message="Generate thumbnails when uploading a file if possible" - horizontal> - <b-switch v-model="options.generateThumbnails" - :true-value="true" - :false-value="false" /> - </b-field> + <b-field + label="Generate thumbnails" + message="Generate thumbnails when uploading a file if possible" + horizontal> + <b-switch + v-model="settings.generateThumbnails" + :true-value="true" + :false-value="false" /> + </b-field> - <b-field label="Generate zips" - message="Allow generating zips to download entire albums" - horizontal> - <b-switch v-model="options.generateZips" - :true-value="true" - :false-value="false" /> - </b-field> + <b-field + label="Generate zips" + message="Allow generating zips to download entire albums" + horizontal> + <b-switch + v-model="settings.generateZips" + :true-value="true" + :false-value="false" /> + </b-field> - <b-field label="Public mode" - message="Enable anonymous uploades" - horizontal> - <b-switch v-model="options.publicMode" - :true-value="true" - :false-value="false" /> - </b-field> + <b-field + label="Public mode" + message="Enable anonymous uploades" + horizontal> + <b-switch + v-model="settings.publicMode" + :true-value="true" + :false-value="false" /> + </b-field> - <b-field label="Enable creating account" - message="Enable creating new accounts in the platform" - horizontal> - <b-switch v-model="options.enableAccounts" - :true-value="true" - :false-value="false" /> - </b-field> + <b-field + label="Enable creating account" + message="Enable creating new accounts in the platform" + horizontal> + <b-switch + v-model="settings.enableAccounts" + :true-value="true" + :false-value="false" /> + </b-field> - <div class="mb2 mt2 text-center"> - <button class="button is-primary" - @click="promptRestartService">Save and restart service</button> - </div> + <div class="mb2 mt2 text-center"> + <button + class="button is-primary" + @click="promptRestartService"> + Save and restart service + </button> </div> </div> </div> @@ -97,38 +126,36 @@ </template> <script> +import { mapState } from 'vuex'; import Sidebar from '~/components/sidebar/Sidebar.vue'; export default { components: { Sidebar }, - middleware: ['auth', 'admin'], - data() { - return { - options: {} - }; - }, + middleware: ['auth', 'admin', ({ store }) => { + try { + store.dispatch('admin/fetchSettings'); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } + }], metaInfo() { return { title: 'Settings' }; }, - mounted() { - this.getSettings(); - }, + computed: mapState({ + settings: (state) => state.admin.settings + }), methods: { - async getSettings() { - const response = await this.$axios.$get(`service/config`); - this.options = response.config; - }, promptRestartService() { this.$buefy.dialog.confirm({ message: 'Keep in mind that restarting only works if you have PM2 or something similar set up. Continue?', onConfirm: () => this.restartService() }); }, - async restartService() { - const response = await this.$axios.$post(`service/restart`); - this.$buefy.toast.open(response.message); + restartService() { + this.$handler.executeAction('admin/restartService'); } } }; diff --git a/src/site/pages/dashboard/admin/user/_id.vue b/src/site/pages/dashboard/admin/user/_id.vue index 7703b1c..484d986 100644 --- a/src/site/pages/dashboard/admin/user/_id.vue +++ b/src/site/pages/dashboard/admin/user/_id.vue @@ -2,50 +2,66 @@ .underline { text-decoration: underline; } </style> <template> - <section class="hero is-fullheight dashboard"> - <div class="hero-body"> - <div class="container"> - <div class="columns"> - <div class="column is-narrow"> - <Sidebar /> - </div> - <div class="column"> - <h2 class="subtitle">User details</h2> - <hr> - - <b-field label="User Id" - horizontal> - <span>{{ user.id }}</span> - </b-field> + <section class="section is-fullheight dashboard"> + <div class="container"> + <div class="columns"> + <div class="column is-narrow"> + <Sidebar /> + </div> + <div class="column"> + <h2 class="subtitle"> + User details + </h2> + <hr> - <b-field label="Username" - horizontal> - <span>{{ user.username }}</span> - </b-field> + <b-field + label="User Id" + horizontal> + <span>{{ user.id }}</span> + </b-field> - <b-field label="Enabled" - horizontal> - <span>{{ user.enabled }}</span> - </b-field> + <b-field + label="Username" + horizontal> + <span>{{ user.username }}</span> + </b-field> - <b-field label="Registered" - horizontal> - <span><timeago :since="user.createdAt" /></span> - </b-field> + <b-field + label="Enabled" + horizontal> + <span>{{ user.enabled }}</span> + </b-field> - <b-field label="Files" - horizontal> - <span>{{ files.length }}</span> - </b-field> + <b-field + label="Registered" + horizontal> + <span><timeago :since="user.createdAt" /></span> + </b-field> - <div class="mb2 mt2 text-center"> - <button class="button is-danger" - @click="promptDisableUser">Disable user</button> - </div> + <b-field + label="Files" + horizontal> + <span>{{ user.files.length }}</span> + </b-field> - <Grid v-if="files.length" - :files="files" /> + <div class="mb2 mt2 text-center"> + <b-button + v-if="user.enabled" + type="is-danger" + @click="promptDisableUser"> + Disable user + </b-button> + <b-button + v-if="!user.enabled" + type="is-success" + @click="promptEnableUser"> + Enable user + </b-button> </div> + + <Grid + v-if="user.files.length" + :files="user.files" /> </div> </div> </div> @@ -53,6 +69,7 @@ </template> <script> +import { mapState } from 'vuex'; import Sidebar from '~/components/sidebar/Sidebar.vue'; import Grid from '~/components/grid/Grid.vue'; @@ -61,41 +78,42 @@ export default { Sidebar, Grid }, - middleware: ['auth', 'admin'], + middleware: ['auth', 'admin', ({ route, store }) => { + try { + store.dispatch('admin/fetchUser', route.params.id); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } + }], data() { return { - options: {}, - files: null, - user: null + options: {} }; }, - async asyncData({ $axios, route }) { - try { - const response = await $axios.$get(`/admin/users/${route.params.id}`); - return { - files: response.files ? response.files : null, - user: response.user ? response.user : null - }; - } catch (error) { - console.error(error); - return { - files: null, - user: null - }; - } - }, + computed: mapState({ + user: (state) => state.admin.user + }), methods: { promptDisableUser() { this.$buefy.dialog.confirm({ - message: 'Are you sure you want to disable the account of the user that uploaded this file?', + type: 'is-danger', + message: 'Are you sure you want to disable the account of this user?', onConfirm: () => this.disableUser() }); }, - async disableUser() { - const response = await this.$axios.$post('admin/users/disable', { - id: this.user.id + promptEnableUser() { + this.$buefy.dialog.confirm({ + type: 'is-danger', + message: 'Are you sure you want to enable the account of this user?', + onConfirm: () => this.enableUser() }); - this.$buefy.toast.open(response.message); + }, + disableUser() { + this.$handler.executeAction('admin/disableUser', this.user.id); + }, + enableUser() { + this.$handler.executeAction('admin/enableUser', this.user.id); } } }; diff --git a/src/site/pages/dashboard/admin/users.vue b/src/site/pages/dashboard/admin/users.vue index 1fefa1e..a13564c 100644 --- a/src/site/pages/dashboard/admin/users.vue +++ b/src/site/pages/dashboard/admin/users.vue @@ -1,3 +1,141 @@ +<template> + <section class="section is-fullheight dashboard"> + <div class="container"> + <div class="columns"> + <div class="column is-narrow"> + <Sidebar /> + </div> + <div class="column"> + <h2 class="subtitle"> + Manage your users + </h2> + <hr> + + <div class="view-container"> + <b-table + :data="users" + :mobile-cards="true"> + <b-table-column + v-slot="props" + field="id" + label="Id" + centered> + {{ props.row.id }} + </b-table-column> + + <b-table-column + v-slot="props" + field="username" + label="Username" + centered> + <nuxt-link :to="`/dashboard/admin/user/${props.row.id}`"> + {{ props.row.username }} + </nuxt-link> + </b-table-column> + + <b-table-column + v-slot="props" + field="enabled" + label="Enabled" + centered> + <b-switch + :value="props.row.enabled" + @input="changeEnabledStatus(props.row)" /> + </b-table-column> + + <b-table-column + v-slot="props" + field="isAdmin" + label="Admin" + centered> + <b-switch + :value="props.row.isAdmin" + @input="changeIsAdmin(props.row)" /> + </b-table-column> + + <b-table-column + v-slot="props" + field="purge" + centered> + <b-button + type="is-danger" + @click="promptPurgeFiles(props.row)"> + Purge files + </b-button> + </b-table-column> + + <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="has-text-right"> + {{ users.length }} users + </div> + </template> + </b-table> + </div> + </div> + </div> + </div> + </section> +</template> + +<script> +import { mapState } from 'vuex'; +import Sidebar from '~/components/sidebar/Sidebar.vue'; + +export default { + components: { + Sidebar + }, + middleware: ['auth', 'admin', ({ route, store }) => { + try { + store.dispatch('admin/fetchUsers', route.params.id); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } + }], + computed: mapState({ + users: (state) => state.admin.users, + config: (state) => state.config + }), + metaInfo() { + return { title: 'Uploads' }; + }, + methods: { + async changeEnabledStatus(row) { + if (row.enabled) { + this.$handler.executeAction('admin/disableUser', row.id); + } else { + this.$handler.executeAction('admin/enableUser', row.id); + } + }, + async changeIsAdmin(row) { + if (row.isAdmin) { + this.$handler.executeAction('admin/demoteUser', row.id); + } else { + this.$handler.executeAction('admin/promoteUser', row.id); + } + }, + promptPurgeFiles(row) { + this.$buefy.dialog.confirm({ + message: 'Are you sure you want to delete this user\'s files?', + onConfirm: () => this.purgeFiles(row) + }); + }, + async purgeFiles(row) { + this.$handler.executeAction('admin/purgeUserFiles', row.id); + } + } +}; +</script> + <style lang="scss" scoped> @import '~/assets/styles/_colors.scss'; div.view-container { @@ -107,9 +245,6 @@ } div.column > h2.subtitle { padding-top: 1px; } -</style> -<style lang="scss"> - @import '~/assets/styles/_colors.scss'; .b-table { .table-wrapper { @@ -118,132 +253,3 @@ } } </style> - - -<template> - <section class="hero is-fullheight dashboard"> - <div class="hero-body"> - <div class="container"> - <div class="columns"> - <div class="column is-narrow"> - <Sidebar /> - </div> - <div class="column"> - <h2 class="subtitle">Manage your users</h2> - <hr> - - <div class="view-container"> - <b-table - :data="users || []" - :mobile-cards="true"> - <template slot-scope="props"> - <b-table-column field="id" - label="Id" - centered> - {{ props.row.id }} - </b-table-column> - - <b-table-column field="username" - label="Username" - centered> - <nuxt-link :to="`/dashboard/admin/user/${props.row.id}`">{{ props.row.username }}</nuxt-link> - </b-table-column> - - <b-table-column field="enabled" - label="Enabled" - centered> - <b-switch v-model="props.row.enabled" - @input="changeEnabledStatus(props.row)" /> - </b-table-column> - - <b-table-column field="isAdmin" - label="Admin" - centered> - <b-switch v-model="props.row.isAdmin" - @input="changeIsAdmin(props.row)" /> - </b-table-column> - - <b-table-column field="purge" - centered> - <button class="button is-primary" - @click="promptPurgeFiles(props.row)">Purge files</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="has-text-right"> - {{ users.length }} users - </div> - </template> - </b-table> - </div> - </div> - </div> - </div> - </div> - </section> -</template> - -<script> -import Sidebar from '~/components/sidebar/Sidebar.vue'; - -export default { - components: { - Sidebar - }, - middleware: ['auth', 'admin'], - data() { - return { - users: [] - }; - }, - computed: { - config() { - return this.$store.state.config; - } - }, - metaInfo() { - return { title: 'Uploads' }; - }, - mounted() { - this.getUsers(); - }, - methods: { - async getUsers() { - const response = await this.$axios.$get(`admin/users`); - this.users = response.users; - }, - async changeEnabledStatus(row) { - const response = await this.$axios.$post(`admin/users/${row.enabled ? 'enable' : 'disable'}`, { - id: row.id - }); - this.$buefy.toast.open(response.message); - }, - async changeIsAdmin(row) { - const response = await this.$axios.$post(`admin/users/${row.isAdmin ? 'promote' : 'demote'}`, { - id: row.id - }); - this.$buefy.toast.open(response.message); - }, - promptPurgeFiles(row) { - this.$buefy.dialog.confirm({ - message: 'Are you sure you want to delete this user\'s files?', - onConfirm: () => this.purgeFiles(row) - }); - }, - async purgeFiles(row) { - const response = await this.$axios.$post(`admin/users/purge`, { - id: row.id - }); - this.$buefy.toast.open(response.message); - } - } -}; -</script> diff --git a/src/site/pages/dashboard/albums/_id.vue b/src/site/pages/dashboard/albums/_id.vue index 1b7c442..cf27a15 100644 --- a/src/site/pages/dashboard/albums/_id.vue +++ b/src/site/pages/dashboard/albums/_id.vue @@ -3,20 +3,58 @@ </style> <template> - <section class="hero is-fullheight dashboard"> - <div class="hero-body"> - <div class="container"> - <div class="columns"> - <div class="column is-narrow"> - <Sidebar /> - </div> - <div class="column"> - <h2 class="subtitle">Files</h2> - <hr> - <!-- TODO: Add a list view so the user can see the files that don't have thumbnails, like text documents --> - <Grid v-if="files.length" - :files="files" /> - </div> + <section class="section is-fullheight dashboard"> + <div class="container"> + <div class="columns"> + <div class="column is-narrow"> + <Sidebar /> + </div> + <div class="column"> + <nav class="level"> + <div class="level-left"> + <div class="level-item"> + <h1 class="title is-3"> + {{ images.albumName }} + </h1> + </div> + <div class="level-item"> + <h2 class="subtitle is-5"> + ({{ totalFiles }} files) + </h2> + </div> + </div> + <div class="level-right"> + <div class="level-item"> + <Search :hidden-hints="['album']" /> + </div> + </div> + </nav> + + <hr> + + <Grid + v-if=" + totalFiles" + :files="images.files" + :total="totalFiles"> + <template v-slot:pagination> + <b-pagination + v-if="shouldPaginate" + :total="totalFiles" + :per-page="limit" + :current.sync="current" + range-before="2" + range-after="2" + class="pagination-slot" + icon-prev="icon-interface-arrow-left" + icon-next="icon-interface-arrow-right" + icon-pack="icon" + aria-next-label="Next page" + aria-previous-label="Previous page" + aria-page-label="Page" + aria-current-label="Current page" /> + </template> + </Grid> </div> </div> </div> @@ -24,34 +62,67 @@ </template> <script> +import { mapState, mapGetters, mapActions } from 'vuex'; + import Sidebar from '~/components/sidebar/Sidebar.vue'; import Grid from '~/components/grid/Grid.vue'; +import Search from '~/components/search/Search.vue'; export default { components: { Sidebar, - Grid + Grid, + Search }, - middleware: 'auth', + middleware: ['auth', ({ route, store }) => { + store.commit('images/resetState'); + store.dispatch('images/fetchByAlbumId', { id: route.params.id }); + }], data() { return { - name: null, - files: [] + current: 1 }; }, + computed: { + ...mapGetters({ + totalFiles: 'images/getTotalFiles', + shouldPaginate: 'images/shouldPaginate', + limit: 'images/getLimit' + }), + ...mapState(['images']), + id() { + return this.$route.params.id; + } + }, metaInfo() { return { title: 'Album' }; }, - async asyncData({ $axios, route }) { - try { - const response = await $axios.$get(`album/${route.params.id}/full`); - return { - files: response.files ? response.files : [] - }; - } catch (error) { - console.error(error); - return { files: [] }; + watch: { + current: 'fetchPaginate' + }, + methods: { + ...mapActions({ + fetch: 'images/fetchByAlbumId' + }), + fetchPaginate() { + this.fetch({ id: this.id, page: this.current }); } } }; </script> + +<style lang="scss" scoped> + div.grid { + margin-bottom: 1rem; + } + + .pagination-slot { + padding: 1rem 0; + } +</style> + +<style lang="scss"> + .pagination-slot > .pagination-previous, .pagination-slot > .pagination-next { + display: none !important; + } +</style> diff --git a/src/site/pages/dashboard/albums/index.vue b/src/site/pages/dashboard/albums/index.vue index 60607ac..896d134 100644 --- a/src/site/pages/dashboard/albums/index.vue +++ b/src/site/pages/dashboard/albums/index.vue @@ -7,130 +7,36 @@ <Sidebar /> </div> <div class="column"> - <h2 class="subtitle">Manage your albums</h2> + <h2 class="subtitle"> + Manage your albums + </h2> <hr> <div class="search-container"> <b-field> - <b-input v-model="newAlbumName" + <b-input + v-model="newAlbumName" + class="lolisafe-input" placeholder="Album name..." type="text" @keyup.enter.native="createAlbum" /> <p class="control"> - <button class="button is-primary" - @click="createAlbum">Create album</button> + <button + outlined + class="button is-black" + :disabled="isCreatingAlbum" + @click="createAlbum"> + Create album + </button> </p> </b-field> </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.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" - label="Actions" - centered> - <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="wrapper"> - <div class="has-text-right"> - <button :class="{ 'is-loading': album.isCreatingLink }" - class="button is-primary" - style="float: left" - @click="createLink(album)">Create new link</button> - {{ album.links.length }} / {{ config.maxLinksPerAlbum }} links created - </div> - - <div class="has-text-left"> - <button class="button is-danger" - style="float: right" - @click="promptDeleteAlbum(album.id)">Delete album</button> - </div> - </div> - </template> - </b-table> - </div> - </div> + :album="album" /> </div> </div> </div> @@ -140,223 +46,65 @@ </template> <script> +import { mapState, mapActions } 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 }) => { + try { + store.dispatch('albums/fetch'); + } catch (e) { + this.alert({ text: e.message, error: true }); + } + }], data() { return { - albums: [], - newAlbumName: null + newAlbumName: null, + isCreatingAlbum: false }; }, - 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; - } - }, + ...mapActions({ + 'alert': 'alert/set' + }), async createAlbum() { if (!this.newAlbumName || this.newAlbumName === '') return; - const response = await this.$axios.$post(`album/new`, - { name: this.newAlbumName }); - 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.isCreatingAlbum = true; + try { + const response = await this.$store.dispatch('albums/createAlbum', this.newAlbumName); + + this.alert({ text: response.message, error: false }); + } catch (e) { + this.alert({ text: e.message, error: true }); + } finally { + this.isCreatingAlbum = false; + this.newAlbumName = null; } - this.albums = response.albums; } } }; </script> + <style lang="scss" scoped> @import '~/assets/styles/_colors.scss'; div.view-container { padding: 2rem; } - 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.search-container { + padding: 1rem 2rem; + background-color: $base-2; } div.column > h2.subtitle { padding-top: 1px; } </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/pages/dashboard/index.vue b/src/site/pages/dashboard/index.vue index b58e567..41605f9 100644 --- a/src/site/pages/dashboard/index.vue +++ b/src/site/pages/dashboard/index.vue @@ -1,19 +1,52 @@ <template> - <section class="hero is-fullheight dashboard"> - <div class="hero-body"> - <div class="container"> - <div class="columns"> - <div class="column is-narrow"> - <Sidebar /> - </div> - <div class="column"> - <h2 class="subtitle">Your uploaded files</h2> - <hr> - <!-- TODO: Add a list view so the user can see the files that don't have thumbnails, like text documents --> - <Grid v-if="files.length" - :files="files" - :enableSearch="false" /> - </div> + <section class="section is-fullheight dashboard"> + <div class="container"> + <div class="columns "> + <div class="column is-narrow"> + <Sidebar /> + </div> + <div class="column"> + <nav class="level"> + <div class="level-left"> + <div class="level-item"> + <h2 class="subtitle"> + Your uploaded files + </h2> + </div> + </div> + <div class="level-right"> + <div class="level-item"> + <Search @search="onSearch" /> + </div> + </div> + </nav> + <hr> + + <!-- <b-loading :active="images.isLoading" /> --> + + <Grid + v-if="totalFiles && !isLoading" + :files="images.files" + :enableSearch="false" + class="grid"> + <template v-slot:pagination> + <b-pagination + v-if="shouldPaginate" + :total="totalFiles" + :per-page="limit" + :current.sync="current" + range-before="2" + range-after="2" + class="pagination-slot" + icon-prev="icon-interface-arrow-left" + icon-next="icon-interface-arrow-right" + icon-pack="icon" + aria-next-label="Next page" + aria-previous-label="Previous page" + aria-page-label="Page" + aria-current-label="Current page" /> + </template> + </Grid> </div> </div> </div> @@ -21,31 +54,90 @@ </template> <script> +import { mapState, mapGetters, mapActions } from 'vuex'; + import Sidebar from '~/components/sidebar/Sidebar.vue'; import Grid from '~/components/grid/Grid.vue'; +import Search from '~/components/search/Search.vue'; export default { components: { Sidebar, - Grid + Grid, + Search }, - middleware: 'auth', + middleware: ['auth', ({ store }) => { + store.commit('images/resetState'); + store.dispatch('images/fetch'); + }], data() { return { - files: [] + current: 1, + isLoading: false, + search: '' }; }, + computed: { + ...mapGetters({ + totalFiles: 'images/getTotalFiles', + shouldPaginate: 'images/shouldPaginate', + limit: 'images/getLimit' + }), + ...mapState(['images']) + }, metaInfo() { return { title: 'Uploads' }; }, - mounted() { - this.getFiles(); + watch: { + current: 'fetchPaginate' + }, + created() { + this.filteredHints = this.hints; // fixes the issue where on pageload, suggestions wont load }, methods: { - async getFiles() { - const response = await this.$axios.$get(`files`); - this.files = response.files; + ...mapActions({ + fetch: 'images/fetch' + }), + async fetchPaginate() { + this.isLoading = true; + await this.fetch(this.current); + this.isLoading = false; + }, + sanitizeQuery(qry) { + // remove spaces between a search type selector `album:` + // and the value (ex `tag: 123` -> `tag:123`) + return (qry || '').replace(/(\w+):\s+/gi, '$1:'); + }, + async onSearch(query) { + this.search = query; + + const sanitizedQ = this.sanitizeQuery(query); + if (!sanitizedQ.length) { + this.current = 1; + await this.fetch(this.current); + } else { + this.$handler.executeAction('images/search', { + q: this.sanitizeQuery(query), + page: this.current + }); + } } } }; </script> + +<style lang="scss" scoped> + div.grid { + margin-bottom: 1rem; + } + + .pagination-slot { + padding: 1rem 0; + } +</style> + +<style lang="scss"> + .pagination-slot > .pagination-previous, .pagination-slot > .pagination-next { + display: none !important; + } +</style> diff --git a/src/site/pages/dashboard/tags/index.vue b/src/site/pages/dashboard/tags/index.vue index bc9ae57..dca8304 100644 --- a/src/site/pages/dashboard/tags/index.vue +++ b/src/site/pages/dashboard/tags/index.vue @@ -107,6 +107,10 @@ } div.column > h2.subtitle { padding-top: 1px; } + + div.no-background { + background: none; + } </style> <style lang="scss"> @import '~/assets/styles/_colors.scss'; @@ -119,77 +123,85 @@ } </style> - <template> - <section class="hero is-fullheight dashboard"> - <div class="hero-body"> - <div class="container"> - <div class="columns"> - <div class="column is-narrow"> - <Sidebar /> - </div> - <div class="column"> - <h2 class="subtitle">Manage your tags</h2> - <hr> + <section class="section is-fullheight dashboard"> + <div class="container"> + <div class="columns"> + <div class="column is-narrow"> + <Sidebar /> + </div> + <div class="column"> + <h2 class="subtitle"> + Manage your tags + </h2> + <hr> - <div class="search-container"> - <b-field> - <b-input v-model="newTagName" - placeholder="Tag name..." - type="text" - @keyup.enter.native="createTag" /> - <p class="control"> - <button class="button is-primary" - @click="createTag">Create tags</button> - </p> - </b-field> - </div> + <div class="search-container"> + <b-field> + <b-input + v-model="newTagName" + class="lolisafe-input" + placeholder="Tag name..." + type="text" + @keyup.enter.native="createTag" /> + <p class="control"> + <b-button + type="is-lolisafe" + @click="createTag"> + Create tags + </b-button> + </p> + </b-field> + </div> - <div class="view-container"> - <div v-for="tag in tags" - :key="tag.id" - class="album"> - <div class="arrow-container" - @click="promptDeleteTag"> - <i class="icon-arrow" /> - </div> - <!-- - <div class="thumb"> - <figure class="image is-64x64 thumb"> - <img src="~/assets/images/blank.png"> - </figure> - </div> - --> - <div class="info"> - <h4> - <router-link :to="`/dashboard/tags/${tag.id}`">{{ tag.name }}</router-link> - </h4> - <span>{{ tag.count || 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.uuid}`">{{ album.fileCount - 5 }}+ more</router-link> - </div> - </template> - <template v-else> - <span class="no-files">Nothing to show here</span> - </template> - </div> - --> + <div class="view-container"> + <div + v-for="tag in tags" + :key="tag.id" + class="album"> + <div + class="arrow-container" + @click="promptDeleteTag"> + <i class="icon-arrow" /> + </div> + <!-- + <div class="thumb"> + <figure class="image is-64x64 thumb"> + <img src="~/assets/images/blank.png"> + </figure> + </div> + --> + <div class="info"> + <h4> + <router-link :to="`/dashboard/tags/${tag.id}`"> + {{ tag.name }} + </router-link> + </h4> + <span>{{ tag.count || 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.uuid}`">{{ album.fileCount - 5 }}+ more</router-link> + </div> + </template> + <template v-else> + <span class="no-files">Nothing to show here</span> + </template> </div> + --> </div> </div> </div> @@ -226,12 +238,14 @@ export default { methods: { promptDeleteTag(id) { this.$buefy.dialog.confirm({ + type: 'is-danger', message: 'Are you sure you want to delete this tag?', onConfirm: () => this.promptPurgeTag(id) }); }, promptPurgeTag(id) { this.$buefy.dialog.confirm({ + type: 'is-danger', message: 'Would you like to delete every file associated with this tag?', cancelText: 'No', confirmText: 'Yes', @@ -246,14 +260,14 @@ export default { }, async createTag() { if (!this.newTagName || this.newTagName === '') return; - const response = await this.$axios.$post(`tag/new`, + const response = await this.$axios.$post('tag/new', { name: this.newTagName }); this.newTagName = null; this.$buefy.toast.open(response.message); this.getTags(); }, async getTags() { - const response = await this.$axios.$get(`tags`); + const response = await this.$axios.$get('tags'); for (const tag of response.tags) { tag.isDetailsOpen = false; } |