diff options
Diffstat (limited to 'src/site/pages/dashboard')
| -rw-r--r-- | src/site/pages/dashboard/account.vue | 130 | ||||
| -rw-r--r-- | src/site/pages/dashboard/admin/file/_id.vue | 145 | ||||
| -rw-r--r-- | src/site/pages/dashboard/admin/settings.vue | 109 | ||||
| -rw-r--r-- | src/site/pages/dashboard/admin/user/_id.vue | 96 | ||||
| -rw-r--r-- | src/site/pages/dashboard/admin/users.vue | 264 | ||||
| -rw-r--r-- | src/site/pages/dashboard/albums/_id.vue | 138 | ||||
| -rw-r--r-- | src/site/pages/dashboard/albums/index.vue | 357 | ||||
| -rw-r--r-- | src/site/pages/dashboard/index.vue | 133 | ||||
| -rw-r--r-- | src/site/pages/dashboard/tags/index.vue | 45 |
9 files changed, 668 insertions, 749 deletions
diff --git a/src/site/pages/dashboard/account.vue b/src/site/pages/dashboard/account.vue index 121b2b3..31ec8af 100644 --- a/src/site/pages/dashboard/account.vue +++ b/src/site/pages/dashboard/account.vue @@ -6,57 +6,84 @@ <Sidebar /> </div> <div class="column"> - <h2 class="subtitle">Account settings</h2> + <h2 class="subtitle"> + Account settings + </h2> <hr> - <b-field label="Username" + <b-field + label="Username" message="Nothing to do here" horizontal> - <b-input v-model="user.username" + <b-input + :value="user.username" expanded disabled /> </b-field> - <b-field label="Current password" + <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" + <b-input + v-model="password" type="password" expanded /> </b-field> - <b-field label="New password" + <b-field + label="New password" message="Your new password" horizontal> - <b-input v-model="user.newPassword" + <b-input + v-model="newPassword" type="password" expanded /> </b-field> - <b-field label="New password again" + <b-field + label="New password again" message="Your new password once again" horizontal> - <b-input v-model="user.reNewPassword" + <b-input + v-model="reNewPassword" type="password" expanded /> </b-field> <div class="mb2 mt2 text-center"> - <button class="button is-primary" - @click="changePassword">Change password</button> + <b-button + type="is-lolisafe" + @click="changePassword"> + Change password + </b-button> </div> - <b-field label="Api key" + <b-field + label="API key" message="This API key lets you use the service from other apps" horizontal> - <b-input v-model="user.apiKey" - expanded - disabled /> + <b-field expanded> + <b-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> + <b-button + type="is-lolisafe" + @click="promptNewAPIKey"> + Request new API key + </b-button> </div> </div> </div> @@ -65,65 +92,78 @@ </template> <script> +import { mapState, mapActions, mapGetters } from 'vuex'; import Sidebar from '~/components/sidebar/Sidebar.vue'; export default { components: { - Sidebar + 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 + 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 + 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({ type: 'is-danger', message: 'Are you sure you want to regenerate your API key? Previously generated API keys will stop working. Make sure to write the new key down as this is the only time it will be displayed to you.', - onConfirm: () => this.requestNewAPIKey() + 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); - } - } + }, + }, }; </script> diff --git a/src/site/pages/dashboard/admin/file/_id.vue b/src/site/pages/dashboard/admin/file/_id.vue index 5821292..89afa47 100644 --- a/src/site/pages/dashboard/admin/file/_id.vue +++ b/src/site/pages/dashboard/admin/file/_id.vue @@ -9,87 +9,111 @@ <Sidebar /> </div> <div class="column"> - <h2 class="subtitle">File details</h2> + <h2 class="subtitle"> + File details + </h2> <hr> <div class="columns"> <div class="column is-6"> - <b-field label="ID" + <b-field + label="ID" horizontal> - <span>{{ file.id }}</span> + <span>{{ admin.file.id }}</span> </b-field> - <b-field label="Name" + <b-field + label="Name" horizontal> - <span>{{ file.name }}</span> + <span>{{ admin.file.name }}</span> </b-field> - <b-field label="Original Name" + <b-field + label="Original Name" horizontal> - <span>{{ file.original }}</span> + <span>{{ admin.file.original }}</span> </b-field> - <b-field label="IP" + <b-field + label="IP" horizontal> - <span class="underline">{{ file.ip }}</span> + <span class="underline">{{ admin.file.ip }}</span> </b-field> - <b-field label="Link" + <b-field + label="Link" horizontal> - <a :href="file.url" - target="_blank">{{ file.url }}</a> + <a + :href="admin.file.url" + target="_blank">{{ admin.file.url }}</a> </b-field> - <b-field label="Size" + <b-field + label="Size" horizontal> - <span>{{ formatBytes(file.size) }}</span> + <span>{{ formatBytes(admin.file.size) }}</span> </b-field> - <b-field label="Hash" + <b-field + label="Hash" horizontal> - <span>{{ file.hash }}</span> + <span>{{ admin.file.hash }}</span> </b-field> - <b-field label="Uploaded" + <b-field + label="Uploaded" horizontal> - <span><timeago :since="file.createdAt" /></span> + <span><timeago :since="admin.file.createdAt" /></span> </b-field> </div> <div class="column is-6"> - <b-field label="User Id" + <b-field + label="User Id" horizontal> - <span>{{ user.id }}</span> + <span>{{ admin.user.id }}</span> </b-field> - <b-field label="Username" + <b-field + label="Username" horizontal> - <span>{{ user.username }}</span> + <span>{{ admin.user.username }}</span> </b-field> - <b-field label="Enabled" + <b-field + label="Enabled" horizontal> - <span>{{ user.enabled }}</span> + <span>{{ admin.user.enabled }}</span> </b-field> - <b-field label="Registered" + <b-field + label="Registered" horizontal> - <span><timeago :since="user.createdAt" /></span> + <span><timeago :since="admin.user.createdAt" /></span> </b-field> - <b-field label="Files" + <b-field + label="Files" horizontal> <span> - <nuxt-link :to="`/dashboard/admin/user/${user.id}`">{{ user.fileCount }}</nuxt-link> + <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"> - <button class="button is-danger" - @click="promptBanIP">Ban IP</button> - <button class="button is-danger" - @click="promptDisableUser">Disable user</button> + <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> @@ -98,59 +122,42 @@ </template> <script> +import { mapState } from 'vuex'; import Sidebar from '~/components/sidebar/Sidebar.vue'; export default { components: { - Sidebar + 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() + 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() + 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'; @@ -161,8 +168,8 @@ 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]}`; + }, + }, }; </script> diff --git a/src/site/pages/dashboard/admin/settings.vue b/src/site/pages/dashboard/admin/settings.vue index 2d59fff..7f6a0fe 100644 --- a/src/site/pages/dashboard/admin/settings.vue +++ b/src/site/pages/dashboard/admin/settings.vue @@ -6,87 +6,112 @@ <Sidebar /> </div> <div class="column"> - <h2 class="subtitle">Service settings</h2> + <h2 class="subtitle"> + Service settings + </h2> <hr> - <b-field label="Service name" + <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" + <b-input + v-model="settings.serviceName" expanded /> </b-field> - <b-field label="Upload folder" + <b-field + label="Upload folder" message="Where to store the files relative to the working directory" horizontal> - <b-input v-model="options.uploadFolder" + <b-input + v-model="settings.uploadFolder" expanded /> </b-field> - <b-field label="Links per album" + <b-field + label="Links per album" message="Maximum links allowed per album" horizontal> - <b-input v-model="options.linksPerAlbum" + <b-input + v-model="settings.linksPerAlbum" type="number" expanded /> </b-field> - <b-field label="Max upload size" + <b-field + label="Max upload size" message="Maximum allowed file size in MB" horizontal> - <b-input v-model="options.maxUploadSize" + <b-input + v-model="settings.maxUploadSize" expanded /> </b-field> - <b-field label="Filename length" + <b-field + label="Filename length" message="How many characters long should the generated filenames be" horizontal> - <b-input v-model="options.filenameLength" + <b-input + v-model="settings.filenameLength" expanded /> </b-field> - <b-field label="Album link length" + <b-field + label="Album link length" message="How many characters a link for an album should have" horizontal> - <b-input v-model="options.albumLinkLength" + <b-input + v-model="settings.albumLinkLength" expanded /> </b-field> - <b-field label="Generate thumbnails" + <b-field + label="Generate thumbnails" message="Generate thumbnails when uploading a file if possible" horizontal> - <b-switch v-model="options.generateThumbnails" + <b-switch + v-model="settings.generateThumbnails" :true-value="true" :false-value="false" /> </b-field> - <b-field label="Generate zips" + <b-field + label="Generate zips" message="Allow generating zips to download entire albums" horizontal> - <b-switch v-model="options.generateZips" + <b-switch + v-model="settings.generateZips" :true-value="true" :false-value="false" /> </b-field> - <b-field label="Public mode" + <b-field + label="Public mode" message="Enable anonymous uploades" horizontal> - <b-switch v-model="options.publicMode" + <b-switch + v-model="settings.publicMode" :true-value="true" :false-value="false" /> </b-field> - <b-field label="Enable creating account" + <b-field + label="Enable creating account" message="Enable creating new accounts in the platform" horizontal> - <b-switch v-model="options.enableAccounts" + <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> + <button + class="button is-primary" + @click="promptRestartService"> + Save and restart service + </button> </div> </div> </div> @@ -95,39 +120,37 @@ </template> <script> +import { mapState } from 'vuex'; import Sidebar from '~/components/sidebar/Sidebar.vue'; export default { components: { - Sidebar - }, - middleware: ['auth', 'admin'], - data() { - return { - options: {} - }; + Sidebar, }, + 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() + 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'); + }, + }, }; </script> diff --git a/src/site/pages/dashboard/admin/user/_id.vue b/src/site/pages/dashboard/admin/user/_id.vue index 2a56c34..7814468 100644 --- a/src/site/pages/dashboard/admin/user/_id.vue +++ b/src/site/pages/dashboard/admin/user/_id.vue @@ -9,41 +9,59 @@ <Sidebar /> </div> <div class="column"> - <h2 class="subtitle">User details</h2> + <h2 class="subtitle"> + User details + </h2> <hr> - <b-field label="User Id" + <b-field + label="User Id" horizontal> <span>{{ user.id }}</span> </b-field> - <b-field label="Username" + <b-field + label="Username" horizontal> <span>{{ user.username }}</span> </b-field> - <b-field label="Enabled" + <b-field + label="Enabled" horizontal> <span>{{ user.enabled }}</span> </b-field> - <b-field label="Registered" + <b-field + label="Registered" horizontal> <span><timeago :since="user.createdAt" /></span> </b-field> - <b-field label="Files" + <b-field + label="Files" horizontal> - <span>{{ files.length }}</span> + <span>{{ user.files.length }}</span> </b-field> <div class="mb2 mt2 text-center"> - <button class="button is-danger" - @click="promptDisableUser">Disable user</button> + <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="files.length" - :files="files" /> + <Grid + v-if="user.files.length" + :files="user.files" /> </div> </div> </div> @@ -51,50 +69,52 @@ </template> <script> +import { mapState } from 'vuex'; import Sidebar from '~/components/sidebar/Sidebar.vue'; import Grid from '~/components/grid/Grid.vue'; export default { components: { Sidebar, - Grid + 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 }; }, - 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?', - onConfirm: () => this.disableUser() + 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); + }, + }, }; </script> diff --git a/src/site/pages/dashboard/admin/users.vue b/src/site/pages/dashboard/admin/users.vue index 695cf0b..bed4c2b 100644 --- a/src/site/pages/dashboard/admin/users.vue +++ b/src/site/pages/dashboard/admin/users.vue @@ -1,3 +1,137 @@ +<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"> + <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 + :value="props.row.enabled" + @input="changeEnabledStatus(props.row)" /> + </b-table-column> + + <b-table-column + field="isAdmin" + label="Admin" + centered> + <b-switch + :value="props.row.isAdmin" + @input="changeIsAdmin(props.row)" /> + </b-table-column> + + <b-table-column + field="purge" + centered> + <b-button + type="is-danger" + @click="promptPurgeFiles(props.row)"> + Purge files + </b-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> + </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 +241,6 @@ } div.column > h2.subtitle { padding-top: 1px; } -</style> -<style lang="scss"> - @import '~/assets/styles/_colors.scss'; .b-table { .table-wrapper { @@ -118,130 +249,3 @@ } } </style> - - -<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"> - <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> - </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 47e7057..a909e75 100644 --- a/src/site/pages/dashboard/albums/_id.vue +++ b/src/site/pages/dashboard/albums/_id.vue @@ -10,25 +10,59 @@ <Sidebar /> </div> <div class="column"> - <h2 class="subtitle">Files</h2> - <hr> + <nav class="level"> + <div class="level-left"> + <div class="level-item"> + <h1 class="title is-3"> + {{ images.name }} + </h1> + </div> + <div class="level-item"> + <h2 class="subtitle is-5"> + ({{ totalFiles }} files) + </h2> + </div> + </div> + <div class="level-right"> + <div class="level-item"> + <b-field> + <b-input + placeholder="Search" + type="search" /> + <p class="control"> + <b-button type="is-lolisafe"> + Search + </b-button> + </p> + </b-field> + </div> + </div> + </nav> - <Grid v-if="files.length" - :files="files" /> + <hr> - <b-pagination - v-if="count > perPage" - :total="count" - :per-page="perPage" - :current.sync="current" - class="pagination" - 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" /> + <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> @@ -36,53 +70,65 @@ </template> <script> +import { mapState, mapGetters, mapActions } from 'vuex'; + import Sidebar from '~/components/sidebar/Sidebar.vue'; import Grid from '~/components/grid/Grid.vue'; export default { components: { Sidebar, - Grid + Grid, }, - middleware: 'auth', + middleware: ['auth', ({ route, store }) => { + store.commit('images/resetState'); + store.dispatch('images/fetchByAlbumId', { id: route.params.id }); + }], data() { return { - name: null, - files: [], - count: 0, current: 1, - perPage: 30 }; }, + computed: { + ...mapGetters({ + totalFiles: 'images/getTotalFiles', + shouldPaginate: 'images/shouldPaginate', + limit: 'images/getLimit', + }), + ...mapState(['images']), + id() { + return this.$route.params.id; + }, + }, metaInfo() { return { title: 'Album' }; }, watch: { - current: 'getFiles' - }, - async asyncData({ $axios, route }) { - const perPage = 30; - const current = 1; // current page - - try { - const response = await $axios.$get(`album/${route.params.id}/full`, { params: { page: current, limit: perPage }}); - return { - files: response.files || [], - count: response.count || 0, - current, - perPage - }; - } catch (error) { - console.error(error); - return { files: [] }; - } + current: 'fetchPaginate', }, methods: { - async getFiles() { - const response = await this.$axios.$get(`album/${this.$route.params.id}/full`, { params: { page: this.current, limit: this.perPage }}); - this.files = response.files; - this.count = response.count; - } + ...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 065667a..a2ba522 100644 --- a/src/site/pages/dashboard/albums/index.vue +++ b/src/site/pages/dashboard/albums/index.vue @@ -7,136 +7,35 @@ <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" placeholder="Album name..." type="text" @keyup.enter.native="createAlbum" /> <p class="control"> - <button outlined - 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_darker.png"> - </figure> - </div> - <div class="info"> - <h4> - <router-link :to="`/dashboard/albums/${album.id}`">{{ album.name }}</router-link> - </h4> - <span>Updated <timeago :since="album.editedAt" /></span> - <span>{{ album.fileCount || 0 }} files</span> - </div> - <div class="latest is-hidden-mobile"> - <template v-if="album.fileCount > 0"> - <div v-for="file of album.files" - :key="file.id" - class="thumb"> - <figure class="image is-64x64"> - <a :href="file.url" - target="_blank"> - <img :src="file.thumbSquare"> - </a> - </figure> - </div> - <div v-if="album.fileCount > 5" - class="thumb more no-background"> - <router-link :to="`/dashboard/albums/${album.id}`">{{ album.fileCount - 5 }}+ more</router-link> - </div> - </template> - <template v-else> - <span class="no-files">Nothing to show here</span> - </template> - </div> - - <div v-if="album.isDetailsOpen" - class="details"> - <h2>Public links for this album:</h2> - - <b-table - :data="album.links.length ? album.links : []" - :mobile-cards="true"> - <template slot-scope="props"> - <b-table-column field="identifier" - label="Link" - centered> - <a :href="`${config.URL}/a/${props.row.identifier}`" - target="_blank"> - {{ props.row.identifier }} - </a> - </b-table-column> - - <b-table-column field="views" - label="Views" - centered> - {{ props.row.views }} - </b-table-column> - - <b-table-column field="enableDownload" - label="Allow download" - centered> - <b-switch v-model="props.row.enableDownload" - @input="linkOptionsChanged(props.row)" /> - </b-table-column> - - <b-table-column field="enabled" - numeric> - <button class="button is-danger" - @click="promptDeleteAlbumLink(props.row.identifier)">Delete link</button> - </b-table-column> - </template> - <template slot="empty"> - <div class="has-text-centered"> - <i class="icon-misc-mood-sad" /> - </div> - <div class="has-text-centered"> - Nothing here - </div> - </template> - <template slot="footer"> - <div class="level is-paddingless"> - <div class="level-left"> - <div class="level-item"> - <button :class="{ 'is-loading': album.isCreatingLink }" - class="button is-primary" - style="float: left" - @click="createLink(album)">Create new link</button> - </div> - <div class="level-item"> - <span class="has-text-default">{{ album.links.length }} / {{ config.maxLinksPerAlbum }} links created</span> - </div> - </div> - - <div class="level-right"> - <div class="level-item"> - <button class="button is-danger" - style="float: right" - @click="promptDeleteAlbum(album.id)">Delete album</button> - </div> - </div> - </div> - </template> - </b-table> - </div> - </div> + :album="album" /> </div> </div> </div> @@ -146,105 +45,55 @@ </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 + ...mapActions({ + 'alert': 'alert/set', + }), + async createAlbum() { + if (!this.newAlbumName || this.newAlbumName === '') return; + + this.isCreatingAlbum = true; 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) { - // + 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 { - album.isCreatingLink = false; + this.isCreatingAlbum = false; + this.newAlbumName = null; } }, - 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.albums = response.albums; - } - } + }, }; </script> + <style lang="scss" scoped> @import '~/assets/styles/_colors.scss'; div.view-container { @@ -256,121 +105,5 @@ export default { background-color: $base-2; } - div.album { - display: flex; - flex-wrap: wrap; - margin-bottom: 10px; - - div.arrow-container { - width: 2em; - height: 64px; - position: relative; - cursor: pointer; - - i { - border: 2px solid $defaultTextColor; - border-right: 0; - border-top: 0; - display: block; - height: 1em; - position: absolute; - transform: rotate(-135deg); - transform-origin: center; - width: 1em; - z-index: 4; - top: 22px; - - -webkit-transition: transform 0.1s linear; - -moz-transition: transform 0.1s linear; - -ms-transition: transform 0.1s linear; - -o-transition: transform 0.1s linear; - transition: transform 0.1s linear; - - &.active { - transform: rotate(-45deg); - } - } - } - div.thumb { - width: 64px; - height: 64px; - -webkit-box-shadow: $boxShadowLight; - box-shadow: $boxShadowLight; - } - - div.info { - margin-left: 15px; - text-align: left; - h4 { - font-size: 1.5rem; - a { - color: $defaultTextColor; - font-weight: 400; - &:hover { text-decoration: underline; } - } - } - span { display: block; } - span:nth-child(3) { - font-size: 0.9rem; - } - } - - div.latest { - flex-grow: 1; - justify-content: flex-end; - display: flex; - margin-left: 15px; - - span.no-files { - font-size: 1.5em; - color: #b1b1b1; - padding-top: 17px; - } - - div.more { - width: 64px; - height: 64px; - background: white; - display: flex; - align-items: center; - padding: 10px; - text-align: center; - a { - line-height: 1rem; - color: $defaultTextColor; - &:hover { text-decoration: underline; } - } - } - } - - div.details { - flex: 0 1 100%; - padding-left: 2em; - padding-top: 1em; - min-height: 50px; - - .b-table { - padding: 2em 0em; - - .table-wrapper { - -webkit-box-shadow: $boxShadowLight; - box-shadow: $boxShadowLight; - } - } - } - } - div.column > h2.subtitle { padding-top: 1px; } - - div.no-background { background: none !important; } -</style> -<style lang="scss"> - @import '~/assets/styles/_colors.scss'; - - .b-table { - .table-wrapper { - -webkit-box-shadow: $boxShadowLight; - box-shadow: $boxShadowLight; - } - } </style> diff --git a/src/site/pages/dashboard/index.vue b/src/site/pages/dashboard/index.vue index 0eb9532..08c5166 100644 --- a/src/site/pages/dashboard/index.vue +++ b/src/site/pages/dashboard/index.vue @@ -6,27 +6,56 @@ <Sidebar /> </div> <div class="column"> - <h2 class="subtitle">Your uploaded files</h2> + <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"> + <b-field> + <b-input + placeholder="Search" + type="search" /> + <p class="control"> + <b-button type="is-lolisafe"> + Search + </b-button> + </p> + </b-field> + </div> + </div> + </nav> <hr> - <Grid v-if="count" - :files="files" - :enableSearch="false" - class="grid" /> + <!-- <b-loading :active="images.isLoading" /> --> - <b-pagination - v-if="count > perPage" - :total="count" - :per-page="perPage" - :current.sync="current" - class="pagination" - 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" /> + <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> @@ -34,54 +63,50 @@ </template> <script> +import { mapState, mapGetters, mapActions } from 'vuex'; + import Sidebar from '~/components/sidebar/Sidebar.vue'; import Grid from '~/components/grid/Grid.vue'; export default { components: { Sidebar, - Grid + Grid, }, - middleware: 'auth', + middleware: ['auth', ({ store }) => { + store.commit('images/resetState'); + store.dispatch('images/fetch'); + }], data() { return { - files: [], - count: 0, current: 1, - perPage: 30 + isLoading: false, }; }, + computed: { + ...mapGetters({ + totalFiles: 'images/getTotalFiles', + shouldPaginate: 'images/shouldPaginate', + limit: 'images/getLimit', + }), + ...mapState(['images']), + }, metaInfo() { return { title: 'Uploads' }; }, watch: { - current: 'getFiles' - }, - async asyncData({ $axios, route }) { - const perPage = 30; - const current = 1; // current page - - try { - const response = await $axios.$get(`files`, { params: { page: current, limit: perPage }}); - return { - files: response.files || [], - count: response.count || 0, - current, - perPage - }; - } catch (error) { - console.error(error); - return { files: [] }; - } + current: 'fetchPaginate', }, methods: { - async getFiles() { - // TODO: Cache a few pages once fetched - const response = await this.$axios.$get(`files`, { params: { page: this.current, limit: this.perPage }}); - this.files = response.files; - this.count = response.count; - } - } + ...mapActions({ + fetch: 'images/fetch', + }), + async fetchPaginate() { + this.isLoading = true; + await this.fetch(this.current); + this.isLoading = false; + }, + }, }; </script> @@ -89,4 +114,14 @@ export default { div.grid { margin-bottom: 1rem; } -</style>
\ No newline at end of file + + .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 7c295b7..a9c5756 100644 --- a/src/site/pages/dashboard/tags/index.vue +++ b/src/site/pages/dashboard/tags/index.vue @@ -123,7 +123,6 @@ } </style> - <template> <section class="section is-fullheight dashboard"> <div class="container"> @@ -132,27 +131,35 @@ <Sidebar /> </div> <div class="column"> - <h2 class="subtitle">Manage your tags</h2> + <h2 class="subtitle"> + Manage your tags + </h2> <hr> <div class="search-container"> <b-field> - <b-input v-model="newTagName" + <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> + <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" + <div + v-for="tag in tags" :key="tag.id" class="album"> - <div class="arrow-container" + <div + class="arrow-container" @click="promptDeleteTag"> <i class="icon-arrow" /> </div> @@ -165,7 +172,9 @@ --> <div class="info"> <h4> - <router-link :to="`/dashboard/tags/${tag.id}`">{{ tag.name }}</router-link> + <router-link :to="`/dashboard/tags/${tag.id}`"> + {{ tag.name }} + </router-link> </h4> <span>{{ tag.count || 0 }} files</span> </div> @@ -205,19 +214,19 @@ import Sidebar from '~/components/sidebar/Sidebar.vue'; export default { components: { - Sidebar + Sidebar, }, middleware: 'auth', data() { return { tags: [], - newTagName: null + newTagName: null, }; }, computed: { config() { return this.$store.state.config; - } + }, }, metaInfo() { return { title: 'Tags' }; @@ -228,17 +237,19 @@ 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) + 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', onConfirm: () => this.deleteTag(id, true), - onCancel: () => this.deleteTag(id, false) + onCancel: () => this.deleteTag(id, false), }); }, async deleteTag(id, purge) { @@ -248,19 +259,19 @@ 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; } this.tags = response.tags; - } - } + }, + }, }; </script> |