aboutsummaryrefslogtreecommitdiff
path: root/src/site/pages/dashboard
diff options
context:
space:
mode:
Diffstat (limited to 'src/site/pages/dashboard')
-rw-r--r--src/site/pages/dashboard/account.vue187
-rw-r--r--src/site/pages/dashboard/admin/file/_id.vue245
-rw-r--r--src/site/pages/dashboard/admin/settings.vue217
-rw-r--r--src/site/pages/dashboard/admin/user/_id.vue140
-rw-r--r--src/site/pages/dashboard/admin/users.vue270
-rw-r--r--src/site/pages/dashboard/albums/_id.vue125
-rw-r--r--src/site/pages/dashboard/albums/index.vue348
-rw-r--r--src/site/pages/dashboard/index.vue138
-rw-r--r--src/site/pages/dashboard/tags/index.vue152
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;
}