diff options
| author | Pitu <[email protected]> | 2019-03-14 23:14:37 +0900 |
|---|---|---|
| committer | Pitu <[email protected]> | 2019-03-14 23:14:37 +0900 |
| commit | 497a961a3844afccc763ebdfa2d77f107318394a (patch) | |
| tree | 429d0f462107f9352c959c7eaa6b24c73f1f791e /src | |
| parent | Small fixes (diff) | |
| download | host.fuwn.me-497a961a3844afccc763ebdfa2d77f107318394a.tar.xz host.fuwn.me-497a961a3844afccc763ebdfa2d77f107318394a.zip | |
Tags
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/database/migrations/20190221225813_addTags.js | 21 | ||||
| -rw-r--r-- | src/api/routes/tags/tagDELETE.js | 37 | ||||
| -rw-r--r-- | src/api/routes/tags/tagPOST.js | 34 | ||||
| -rw-r--r-- | src/api/routes/tags/tagsGET.js | 31 | ||||
| -rw-r--r-- | src/site/pages/dashboard/tags/index.vue | 284 |
5 files changed, 407 insertions, 0 deletions
diff --git a/src/api/database/migrations/20190221225813_addTags.js b/src/api/database/migrations/20190221225813_addTags.js new file mode 100644 index 0000000..ef71877 --- /dev/null +++ b/src/api/database/migrations/20190221225813_addTags.js @@ -0,0 +1,21 @@ +exports.up = async knex => { + await knex.schema.createTable('tags', table => { + table.increments(); + table.string('uuid'); + table.integer('userId'); + table.string('name'); + table.timestamp('createdAt'); + table.timestamp('editedAt'); + }); + + await knex.schema.createTable('fileTags', table => { + table.increments(); + table.integer('fileId'); + table.integer('tagId'); + }); +}; + +exports.down = async knex => { + await knex.schema.dropTableIfExists('tags'); + await knex.schema.dropTableIfExists('fileTags'); +}; diff --git a/src/api/routes/tags/tagDELETE.js b/src/api/routes/tags/tagDELETE.js new file mode 100644 index 0000000..c03ca64 --- /dev/null +++ b/src/api/routes/tags/tagDELETE.js @@ -0,0 +1,37 @@ +const Route = require('../../structures/Route'); +const Util = require('../../utils/Util'); + +class tagDELETE extends Route { + constructor() { + super('/tag/:id/:purge*?', 'delete'); + } + + async run(req, res, db, user) { + const { id, purge } = req.params; + if (!id) return res.status(400).json({ message: 'Invalid tag supplied' }); + + /* + Check if the tag exists + */ + const tag = await db.table('tags').where({ id, userId: user.id }).first(); + if (!tag) return res.status(400).json({ message: 'The tag doesn\'t exist or doesn\'t belong to the user' }); + + try { + /* + Should we also delete every file of that tag? + */ + if (purge) { + await Util.deleteAllFilesFromTag(id); + } + /* + Delete the tag + */ + await db.table('tags').where({ id }).delete(); + return res.json({ message: 'The tag was deleted successfully' }); + } catch (error) { + return super.error(res, error); + } + } +} + +module.exports = tagDELETE; diff --git a/src/api/routes/tags/tagPOST.js b/src/api/routes/tags/tagPOST.js new file mode 100644 index 0000000..0df36e1 --- /dev/null +++ b/src/api/routes/tags/tagPOST.js @@ -0,0 +1,34 @@ +const Route = require('../../structures/Route'); +const moment = require('moment'); +const util = require('../../utils/Util'); + +class tagPOST extends Route { + constructor() { + super('/tag/new', 'post'); + } + + async run(req, res, db, user) { + if (!req.body) return res.status(400).json({ message: 'No body provided' }); + const { name } = req.body; + if (!name) return res.status(400).json({ message: 'No name provided' }); + + /* + Check that a tag with that name doesn't exist yet + */ + const tag = await db.table('tags').where({ name, userId: user.id }).first(); + if (tag) return res.status(401).json({ message: 'There\'s already a tag with that name' }); + + const now = moment.utc().toDate(); + await db.table('tags').insert({ + name, + uuid: util.uuid(), + userId: user.id, + createdAt: now, + editedAt: now + }); + + return res.json({ message: 'The album was created successfully' }); + } +} + +module.exports = tagPOST; diff --git a/src/api/routes/tags/tagsGET.js b/src/api/routes/tags/tagsGET.js new file mode 100644 index 0000000..871148e --- /dev/null +++ b/src/api/routes/tags/tagsGET.js @@ -0,0 +1,31 @@ +const Route = require('../../structures/Route'); +const Util = require('../../utils/Util'); + +class tagsGET extends Route { + constructor() { + super('/tags', 'get'); + } + + async run(req, res, db, user) { + try { + const tags = await db.table('tags') + .where('userId', user.id); + + for (const tag of tags) { + const files = await db.table('fileTags') + .where({ tagId: tag.id }); + + tag.count = files.length ? files.length : 0; + } + + return res.json({ + message: 'Successfully retrieved tags', + tags + }); + } catch (error) { + return super.error(res, error); + } + } +} + +module.exports = tagsGET; diff --git a/src/site/pages/dashboard/tags/index.vue b/src/site/pages/dashboard/tags/index.vue new file mode 100644 index 0000000..b4c5906 --- /dev/null +++ b/src/site/pages/dashboard/tags/index.vue @@ -0,0 +1,284 @@ +<style lang="scss" scoped> + @import '~/assets/styles/_colors.scss'; + section { background-color: $backgroundLight1 !important; } + section.hero div.hero-body { + align-items: baseline; + } + div.search-container { + display: flex; + justify-content: center; + } + + 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; + h4 { + font-size: 1.5rem; + a { + color: $defaultTextColor; + font-weight: 400; + &:hover { text-decoration: underline; } + } + } + span { display: block; } + span:nth-child(3) { + font-size: 0.9rem; + } + } + + div.latest { + flex-grow: 1; + justify-content: flex-end; + display: flex; + margin-left: 15px; + + span.no-files { + font-size: 1.5em; + color: #b1b1b1; + padding-top: 17px; + } + + div.more { + width: 64px; + height: 64px; + background: white; + display: flex; + align-items: center; + padding: 10px; + text-align: center; + a { + line-height: 1rem; + color: $defaultTextColor; + &:hover { text-decoration: underline; } + } + } + } + + div.details { + flex: 0 1 100%; + padding-left: 2em; + padding-top: 1em; + min-height: 50px; + + .b-table { + padding: 2em 0em; + + .table-wrapper { + -webkit-box-shadow: $boxShadowLight; + box-shadow: $boxShadowLight; + } + } + } + } + + div.column > h2.subtitle { padding-top: 1px; } +</style> +<style lang="scss"> + @import '~/assets/styles/_colors.scss'; + + .b-table { + .table-wrapper { + -webkit-box-shadow: $boxShadowLight; + box-shadow: $boxShadowLight; + } + } +</style> + + +<template> + <section class="hero is-fullheight"> + <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> + + <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="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> + </div> + </div> + </div> + </section> +</template> + +<script> +import Sidebar from '~/components/sidebar/Sidebar.vue'; + +export default { + components: { + Sidebar + }, + data() { + return { + tags: [], + newTagName: null + }; + }, + computed: { + config() { + return this.$store.state.config; + } + }, + metaInfo() { + return { title: 'Tags' }; + }, + mounted() { + this.getTags(); + }, + methods: { + promptDeleteTag(id) { + this.$dialog.confirm({ + message: 'Are you sure you want to delete this tag?', + onConfirm: () => this.promptPurgeTag(id) + }); + }, + promptPurgeTag(id) { + this.$dialog.confirm({ + message: 'Would you like to delete every file associated with this tag?', + cancelText: 'No', + confirmText: 'Yes', + onConfirm: () => this.deleteTag(id, true), + onCancel: () => this.deleteTag(id, false) + }); + }, + async deleteTag(id, purge) { + try { + const response = await this.axios.delete(`${this.config.baseURL}/tags/${id}/${purge ? true : ''}`); + this.getTags(); + return this.$toast.open(response.data.message); + } catch (error) { + return this.$onPromiseError(error); + } + }, + async createTag() { + if (!this.newTagName || this.newTagName === '') return; + try { + const response = await this.axios.post(`${this.config.baseURL}/tag/new`, + { name: this.newTagName }); + this.newTagName = null; + this.$toast.open(response.data.message); + this.getTags(); + } catch (error) { + this.$onPromiseError(error); + } + }, + async getTags() { + try { + const response = await this.axios.get(`${this.config.baseURL}/tags`); + for (const tag of response.data.tags) { + tag.isDetailsOpen = false; + } + this.tags = response.data.tags; + } catch (error) { + this.$onPromiseError(error); + } + } + } +}; +</script> |