diff options
Diffstat (limited to 'src/site/pages')
| -rw-r--r-- | src/site/pages/a/_identifier.vue | 46 | ||||
| -rw-r--r-- | src/site/pages/dashboard/account.vue | 187 | ||||
| -rw-r--r-- | src/site/pages/dashboard/admin/file/_id.vue | 245 | ||||
| -rw-r--r-- | src/site/pages/dashboard/admin/settings.vue | 217 | ||||
| -rw-r--r-- | src/site/pages/dashboard/admin/user/_id.vue | 140 | ||||
| -rw-r--r-- | src/site/pages/dashboard/admin/users.vue | 270 | ||||
| -rw-r--r-- | src/site/pages/dashboard/albums/_id.vue | 125 | ||||
| -rw-r--r-- | src/site/pages/dashboard/albums/index.vue | 348 | ||||
| -rw-r--r-- | src/site/pages/dashboard/index.vue | 138 | ||||
| -rw-r--r-- | src/site/pages/dashboard/tags/index.vue | 152 | ||||
| -rw-r--r-- | src/site/pages/faq.vue | 22 | ||||
| -rw-r--r-- | src/site/pages/index.vue | 23 | ||||
| -rw-r--r-- | src/site/pages/login.vue | 128 | ||||
| -rw-r--r-- | src/site/pages/logout.vue | 8 | ||||
| -rw-r--r-- | src/site/pages/register.vue | 121 |
15 files changed, 1128 insertions, 1042 deletions
diff --git a/src/site/pages/a/_identifier.vue b/src/site/pages/a/_identifier.vue index ea36852..0c6261a 100644 --- a/src/site/pages/a/_identifier.vue +++ b/src/site/pages/a/_identifier.vue @@ -16,42 +16,48 @@ </style> <template> - <section class="hero is-fullheight"> + <section class="section is-fullheight"> <template v-if="files && files.length"> - <div class="hero-body align-top"> + <div class="align-top"> <div class="container"> - <h1 class="title">{{ name }}</h1> - <h2 class="subtitle">Serving {{ files ? files.length : 0 }} files</h2> - <a v-if="downloadLink" + <h1 class="title"> + {{ name }} + </h1> + <h2 class="subtitle"> + Serving {{ files ? files.length : 0 }} files + </h2> + <a + v-if="downloadLink" :href="downloadLink">Download Album</a> <hr> </div> </div> - <div class="hero-body"> - <div class="container"> - <Grid v-if="files && files.length" - :files="files" - :isPublic="true" - :width="200" - :enableSearch="false" - :enableToolbar="false" /> - </div> + <div class="container"> + <Grid + v-if="files && files.length" + :files="files" + :isPublic="true" + :width="200" + :enableSearch="false" + :enableToolbar="false" /> </div> </template> <template v-else> - <div class="hero-body"> - <div class="container"> - <h1 class="title">:(</h1> - <h2 class="subtitle">This album seems to be empty</h2> - </div> + <div class="container"> + <h1 class="title"> + :( + </h1> + <h2 class="subtitle"> + This album seems to be empty + </h2> </div> </template> </section> </template> <script> -import Grid from '~/components/grid/Grid.vue'; import axios from 'axios'; +import Grid from '~/components/grid/Grid.vue'; export default { components: { Grid }, 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; } diff --git a/src/site/pages/faq.vue b/src/site/pages/faq.vue index bd93086..9053072 100644 --- a/src/site/pages/faq.vue +++ b/src/site/pages/faq.vue @@ -1,34 +1,45 @@ <template> + <!-- eslint-disable max-len --> <div class="container has-text-left"> - <h2 class="subtitle">What is lolisafe?</h2> + <h2 class="subtitle"> + What is lolisafe? + </h2> <article class="message"> <div class="message-body"> lolisafe is an easy to use, open source and completely free file upload service. We accept your files, photos, documents, anything, and give you back a shareable link for you to send to others. </div> </article> - <h2 class="subtitle">Can I run my own lolisafe?</h2> + <h2 class="subtitle"> + Can I run my own lolisafe? + </h2> <article class="message"> <div class="message-body"> Definitely. Head to <a target="_blank" href="https://github.com/WeebDev/lolisafe">our GitHub repo</a> and follow the instructions to clone, build and deploy it by yourself. It's super easy too! </div> </article> - <h2 class="subtitle">How can I keep track of my uploads?</h2> + <h2 class="subtitle"> + How can I keep track of my uploads? + </h2> <article class="message"> <div class="message-body"> Simply create a user on the site and every upload will be associated with your account, granting you access to your uploaded files through our dashboard. </div> </article> - <h2 class="subtitle">What are albums?</h2> + <h2 class="subtitle"> + What are albums? + </h2> <article class="message"> <div class="message-body"> Albums are a simple way of sorting uploads together. Right now you can create albums through the dashboard and use them only with <a target="_blank" href="https://chrome.google.com/webstore/detail/loli-safe-uploader/enkkmplljfjppcdaancckgilmgoiofnj">our chrome extension</a> which will enable you to <strong>right click -> send to lolisafe</strong> or to a desired album if you have any. </div> </article> - <h2 class="subtitle">Why should I use this?</h2> + <h2 class="subtitle"> + Why should I use this? + </h2> <article class="message"> <div class="message-body"> There are too many file upload services out there, and a lot of them rely on the foundations of pomf which is ancient. In a desperate and unsuccessful attempt of finding a good file uploader that's easily extendable, lolisafe was born. We give you control over your files, we give you a way to sort your uploads into albums for ease of access and we give you an api to use with ShareX or any other thing that let's you make POST requests. @@ -48,6 +59,7 @@ export default { } }; </script> + <style lang="scss" scoped> @import '~/assets/styles/_colors.scss'; article.message { background-color: #ffffff; } diff --git a/src/site/pages/index.vue b/src/site/pages/index.vue index 0617098..8193b88 100644 --- a/src/site/pages/index.vue +++ b/src/site/pages/index.vue @@ -1,5 +1,5 @@ <template> - <div> + <div class="section"> <div class="container"> <div class="columns"> <div class="column is-3 is-offset-2"> @@ -11,15 +11,18 @@ <div class="content-wrapper"> <h4>Blazing fast file uploader. <br>For real.</h4> <p> + <!-- eslint-disable-next-line max-len --> A <strong>modern</strong> and <strong>self-hosted</strong> file upload service that can handle anything you throw at it. Fast uploads, file manager and sharing capabilities all crafted with a beautiful user experience in mind. </p> </div> </div> </div> </div> - <div class="container"> + <div class="container uploader"> <Uploader v-if="config.publicMode || (!config.publicMode && loggedIn)" /> - <div v-else> + <div + v-else + class="has-text-centered is-size-4 has-text-danger"> This site has disabled public uploads. You need an account. </div> <Links /> @@ -27,6 +30,8 @@ </div> </template> <script> +import { mapState, mapGetters } from 'vuex'; + import Logo from '~/components/logo/Logo.vue'; import Uploader from '~/components/uploader/Uploader.vue'; import Links from '~/components/home/links/Links.vue'; @@ -42,12 +47,8 @@ export default { return { albums: [] }; }, computed: { - loggedIn() { - return this.$store.state.loggedIn; - }, - config() { - return this.$store.state.config; - } + ...mapGetters({ loggedIn: 'auth/isLoggedIn' }), + ...mapState(['config']) } }; </script> @@ -79,4 +80,8 @@ export default { } } } + + .uploader { + margin-top: 2rem; + } </style> diff --git a/src/site/pages/login.vue b/src/site/pages/login.vue index 7a98aa4..6403aa9 100644 --- a/src/site/pages/login.vue +++ b/src/site/pages/login.vue @@ -1,37 +1,57 @@ <template> - <section class="hero is-fullheight is-login"> - <div class="hero-body"> - <div class="container"> - <h1 class="title"> - Dashboard Access - </h1> - <h2 class="subtitle mb5"> - Login to access your files and folders - </h2> - <div class="columns"> - <div class="column is-4 is-offset-4"> - <b-field> - <b-input v-model="username" - type="text" - placeholder="Username" - @keyup.enter.native="login" /> - </b-field> - <b-field> - <b-input v-model="password" - type="password" - placeholder="Password" - password-reveal - @keyup.enter.native="login" /> - </b-field> + <section class="section is-fullheight is-login"> + <div class="container"> + <h1 class="title"> + Dashboard Access + </h1> + <h2 class="subtitle mb5"> + Login to access your files and folders + </h2> + <div class="columns"> + <div class="column is-4 is-offset-4"> + <b-field> + <b-input + v-model="username" + class="lolisafe-input" + type="text" + placeholder="Username" + @keyup.enter.native="login" /> + </b-field> + <b-field> + <b-input + v-model="password" + class="lolisafe-input" + type="password" + placeholder="Password" + password-reveal + @keyup.enter.native="login" /> + </b-field> - <p class="control has-addons is-pulled-right"> - <router-link v-if="config.userAccounts" - to="/register" - class="is-text">Don't have an account?</router-link> - <span v-else>Registration is closed at the moment</span> - <button class="button is-primary big ml1" - @click="login">login</button> - </p> + <p class="control has-addons is-pulled-right" /> + + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <router-link + v-if="config.userAccounts" + to="/register" + class="is-text"> + Don't have an account? + </router-link> + <span v-else>Registration is closed at the moment</span> + </div> + </div> + + <div class="level-right"> + <p class="level-item"> + <b-button + size="is-medium" + type="is-lolisafe" + @click="login"> + Login + </b-button> + </p> + </div> </div> </div> </div> @@ -65,6 +85,8 @@ </template> <script> +import { mapState } from 'vuex'; + export default { name: 'Login', data() { @@ -76,38 +98,33 @@ export default { isLoading: false }; }, - computed: { - config() { - return this.$store.state.config; - } - }, + computed: mapState(['config', 'auth']), metaInfo() { return { title: 'Login' }; }, + created() { + if (this.auth.loggedIn) { + this.redirect(); + } + }, methods: { async login() { if (this.isLoading) return; - if (!this.username || !this.password) { - this.$store.dispatch('alert', { - text: 'Please fill both fields before attempting to log in.', - error: true - }); + + const { username, password } = this; + if (!username || !password) { + this.$notifier.error('Please fill both fields before attempting to log in.'); return; } - this.isLoading = true; try { - const data = await this.$axios.$post(`auth/login`, { - username: this.username, - password: this.password - }); - this.$axios.setToken(data.token, 'Bearer'); - document.cookie = `token=${encodeURIComponent(data.token)}`; - this.$store.dispatch('login', { token: data.token, user: data.user }); - - this.redirect(); - } catch (error) { - // + this.isLoading = true; + await this.$store.dispatch('auth/login', { username, password }); + if (this.auth.loggedIn) { + this.redirect(); + } + } catch (e) { + this.$notifier.error(e.message); } finally { this.isLoading = false; } @@ -126,9 +143,8 @@ export default { this.isLoading = false; this.$onPromiseError(err); }); - },*/ + }, */ redirect() { - this.$store.commit('loggedIn', true); if (typeof this.$route.query.redirect !== 'undefined') { this.$router.push(this.$route.query.redirect); return; diff --git a/src/site/pages/logout.vue b/src/site/pages/logout.vue new file mode 100644 index 0000000..e6adbea --- /dev/null +++ b/src/site/pages/logout.vue @@ -0,0 +1,8 @@ +<script> +export default { + async created() { + await this.$store.dispatch('auth/logout'); + this.$router.replace('/login'); + } +}; +</script> diff --git a/src/site/pages/register.vue b/src/site/pages/register.vue index 2f155c0..64376db 100644 --- a/src/site/pages/register.vue +++ b/src/site/pages/register.vue @@ -1,41 +1,62 @@ <template> - <section class="hero is-fullheight is-register"> - <div class="hero-body"> - <div class="container"> - <h1 class="title"> - Dashboard Access - </h1> - <h2 class="subtitle mb5"> - Register for a new account - </h2> - <div class="columns"> - <div class="column is-4 is-offset-4"> - <b-field> - <b-input v-model="username" - type="text" - placeholder="Username" /> - </b-field> - <b-field> - <b-input v-model="password" - type="password" - placeholder="Password" - password-reveal /> - </b-field> - <b-field> - <b-input v-model="rePassword" - type="password" - placeholder="Re-type Password" - password-reveal - @keyup.enter.native="register" /> - </b-field> + <section class="section is-fullheight is-register"> + <div class="container"> + <h1 class="title"> + Dashboard Access + </h1> + <h2 class="subtitle mb5"> + Register for a new account + </h2> + <div class="columns"> + <div class="column is-4 is-offset-4"> + <b-field> + <b-input + v-model="username" + class="lolisafe-input" + type="text" + placeholder="Username" /> + </b-field> + <b-field> + <b-input + v-model="password" + class="lolisafe-input" + type="password" + placeholder="Password" + password-reveal /> + </b-field> + <b-field> + <b-input + v-model="rePassword" + class="lolisafe-input" + type="password" + placeholder="Re-type Password" + password-reveal + @keyup.enter.native="register" /> + </b-field> - <p class="control has-addons is-pulled-right"> - <router-link to="/login" - class="is-text">Already have an account?</router-link> - <button class="button is-primary big ml1" - :disabled="isLoading" - @click="register">Register</button> - </p> + <div class="level"> + <!-- Left side --> + <div class="level-left"> + <div class="level-item"> + <router-link + to="/login" + class="is-text"> + Already have an account? + </router-link> + </div> + </div> + <!-- Right side --> + <div class="level-right"> + <p class="level-item"> + <b-button + size="is-medium" + type="is-lolisafe" + :disabled="isLoading" + @click="register"> + Register + </b-button> + </p> + </div> </div> </div> </div> @@ -44,6 +65,8 @@ </template> <script> +import { mapState } from 'vuex'; + export default { name: 'Register', data() { @@ -54,43 +77,35 @@ export default { isLoading: false }; }, - computed: { - config() { - return this.$store.state.config; - } - }, + computed: mapState(['config', 'auth']), metaInfo() { return { title: 'Register' }; }, methods: { async register() { if (this.isLoading) return; + if (!this.username || !this.password || !this.rePassword) { - this.$store.dispatch('alert', { - text: 'Please fill all fields before attempting to register.', - error: true - }); + this.$notifier.error('Please fill all fields before attempting to register.'); return; } if (this.password !== this.rePassword) { - this.$store.dispatch('alert', { - text: 'Passwords don\'t match', - error: true - }); + this.$notifier.error('Passwords don\'t match'); return; } this.isLoading = true; try { - const response = await this.$axios.$post(`auth/register`, { + const response = await this.$store.dispatch('auth/register', { username: this.username, password: this.password }); - this.$store.dispatch('alert', { text: response.message }); - return this.$router.push('/login'); + this.$notifier.success(response.message); + this.$router.push('/login'); + return; } catch (error) { - // + this.$notifier.error(error.message); } finally { this.isLoading = false; } |