change categories to tags in frontend
This commit is contained in:
parent
92ed17efa5
commit
5be099c61c
16 changed files with 196 additions and 112 deletions
|
|
@ -8,7 +8,7 @@ export type EncodedPlayer = Array<any>
|
|||
export type EncodedPiece = Array<any>
|
||||
export type EncodedPieceShape = number
|
||||
|
||||
export interface Category {
|
||||
export interface Tag {
|
||||
id: number
|
||||
slug: string
|
||||
title: string
|
||||
|
|
@ -34,7 +34,7 @@ export interface Image {
|
|||
file: string
|
||||
url: string
|
||||
title: string
|
||||
categories: Array<Category>
|
||||
tags: Array<Tag>
|
||||
created: number
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,9 +20,11 @@
|
|||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<!-- TODO: autocomplete category -->
|
||||
<td><label>Category</label></td>
|
||||
<td><input type="text" v-model="category" placeholder="Plants" /></td>
|
||||
<!-- TODO: autocomplete tags -->
|
||||
<td><label>Tags</label></td>
|
||||
<td>
|
||||
<tags-input v-model="tags" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -36,14 +38,16 @@
|
|||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
import { Image } from '../../common/GameCommon'
|
||||
import { Image, Tag } from '../../common/GameCommon'
|
||||
|
||||
import ResponsiveImage from './ResponsiveImage.vue'
|
||||
import TagsInput from './TagsInput.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'edit-image-dialog',
|
||||
components: {
|
||||
ResponsiveImage,
|
||||
TagsInput,
|
||||
},
|
||||
props: {
|
||||
image: {
|
||||
|
|
@ -58,19 +62,19 @@ export default defineComponent({
|
|||
data () {
|
||||
return {
|
||||
title: '',
|
||||
category: '',
|
||||
tags: [] as string[],
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.title = this.image.title
|
||||
this.category = this.image.categories.length > 0 ? this.image.categories[0].title : ''
|
||||
this.tags = this.image.tags.map((t: Tag) => t.title)
|
||||
},
|
||||
methods: {
|
||||
saveImage () {
|
||||
this.$emit('saveClick', {
|
||||
id: this.image.id,
|
||||
title: this.title,
|
||||
category: this.category,
|
||||
tags: this.tags,
|
||||
})
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,12 +16,8 @@ gallery", if possible!
|
|||
<div v-else>
|
||||
<label class="upload">
|
||||
<input type="file" style="display: none" @change="preview" accept="image/*" />
|
||||
<span class="btn">{{label || 'Upload File'}}</span>
|
||||
<span class="btn">Upload File</span>
|
||||
</label>
|
||||
|
||||
|
||||
<!-- TODO: drop area for image -->
|
||||
<!-- <upload class="upload" @uploaded="mediaImgUploaded($event)" accept="image/*" label="Upload an image" /> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -37,9 +33,11 @@ gallery", if possible!
|
|||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<!-- TODO: autocomplete category -->
|
||||
<td><label>Category</label></td>
|
||||
<td><input type="text" v-model="category" placeholder="Plants" /></td>
|
||||
<!-- TODO: autocomplete tags -->
|
||||
<td><label>Tags</label></td>
|
||||
<td>
|
||||
<tags-input v-model="tags" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -55,14 +53,14 @@ gallery", if possible!
|
|||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import Upload from './Upload.vue'
|
||||
import ResponsiveImage from './ResponsiveImage.vue'
|
||||
import TagsInput from './TagsInput.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'new-image-dialog',
|
||||
components: {
|
||||
Upload,
|
||||
ResponsiveImage,
|
||||
TagsInput,
|
||||
},
|
||||
emits: {
|
||||
bgclick: null,
|
||||
|
|
@ -74,7 +72,7 @@ export default defineComponent({
|
|||
previewUrl: '',
|
||||
file: null as File|null,
|
||||
title: '',
|
||||
category: '',
|
||||
tags: [] as string[],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -103,14 +101,14 @@ export default defineComponent({
|
|||
this.$emit('postToGalleryClick', {
|
||||
file: this.file,
|
||||
title: this.title,
|
||||
category: this.category,
|
||||
tags: this.tags,
|
||||
})
|
||||
},
|
||||
setupGameClick () {
|
||||
this.$emit('setupGameClick', {
|
||||
file: this.file,
|
||||
title: this.title,
|
||||
category: this.category,
|
||||
tags: this.tags,
|
||||
})
|
||||
},
|
||||
},
|
||||
|
|
|
|||
60
src/frontend/components/TagsInput.vue
Normal file
60
src/frontend/components/TagsInput.vue
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<template>
|
||||
<div>
|
||||
<input class="input" type="text" v-model="input" placeholder="Plants, People" @keydown.enter="add" @keyup="onKeyUp" />
|
||||
<span v-for="(tag,idx) in values" :key="idx" class="bit" @click="rm(tag)">{{tag}} ✖</span>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'tags-input',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array as PropType<Array<string>>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
'update:modelValue': null,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
input: '',
|
||||
values: [] as Array<string>,
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.values = this.modelValue
|
||||
},
|
||||
methods: {
|
||||
onKeyUp (ev: KeyboardEvent) {
|
||||
if (ev.key === ',') {
|
||||
this.add()
|
||||
ev.stopPropagation()
|
||||
return false
|
||||
}
|
||||
},
|
||||
add () {
|
||||
const newval = this.input.replace(/,/g, '').trim()
|
||||
if (!newval) {
|
||||
return
|
||||
}
|
||||
if (!this.values.includes(newval)) {
|
||||
this.values.push(newval)
|
||||
}
|
||||
this.input = ''
|
||||
this.$emit('update:modelValue', this.values)
|
||||
},
|
||||
rm (val: string) {
|
||||
this.values = this.values.filter(v => v !== val)
|
||||
this.$emit('update:modelValue', this.values)
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.input {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
--link-color: #808db0;
|
||||
--link-hover-color: #c5cfeb;
|
||||
--highlight-color: #dd7e7e;
|
||||
--positive-color: #64a756;
|
||||
--input-bg-color: #262523;
|
||||
--bg-color: rgba(0,0,0,.7);
|
||||
}
|
||||
|
|
@ -212,6 +213,18 @@ kbd {
|
|||
color: var(--main-darker-color);
|
||||
}
|
||||
|
||||
.bit {
|
||||
background: rgb(59, 55, 55);
|
||||
border-radius: .5em;
|
||||
padding: .25em .5em;
|
||||
display: inline-block;
|
||||
margin: 0 .25em .25em 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.bit.on {
|
||||
color: var(--positive-color);
|
||||
}
|
||||
|
||||
.upload-image-teaser {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,12 +13,13 @@ in jigsawpuzzles.io
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label v-if="categories.length > 0">
|
||||
Category:
|
||||
<select v-model="filters.category" @change="filtersChanged">
|
||||
<label v-if="tags.length > 0">
|
||||
Tags:
|
||||
<span class="bit" v-for="(t,idx) in tags" :key="idx" @click="toggleTag(t)" :class="{on: filters.tags.includes(t.slug)}">{{t.title}}</span>
|
||||
<!-- <select v-model="filters.tags" @change="filtersChanged">
|
||||
<option value="">All</option>
|
||||
<option v-for="(c, idx) in categories" :key="idx" :value="c.slug">{{c.title}}</option>
|
||||
</select>
|
||||
<option v-for="(c, idx) in tags" :key="idx" :value="c.slug">{{c.title}}</option>
|
||||
</select> -->
|
||||
</label>
|
||||
<label>
|
||||
Sort by:
|
||||
|
|
@ -44,7 +45,7 @@ import ImageLibrary from './../components/ImageLibrary.vue'
|
|||
import NewImageDialog from './../components/NewImageDialog.vue'
|
||||
import EditImageDialog from './../components/EditImageDialog.vue'
|
||||
import NewGameDialog from './../components/NewGameDialog.vue'
|
||||
import { GameSettings, Image } from '../../common/GameCommon'
|
||||
import { GameSettings, Image, Tag } from '../../common/GameCommon'
|
||||
import Util from '../../common/Util'
|
||||
|
||||
export default defineComponent({
|
||||
|
|
@ -58,10 +59,10 @@ export default defineComponent({
|
|||
return {
|
||||
filters: {
|
||||
sort: 'date_desc',
|
||||
category: '',
|
||||
tags: [] as string[],
|
||||
},
|
||||
images: [],
|
||||
categories: [],
|
||||
tags: [],
|
||||
|
||||
image: {
|
||||
id: 0,
|
||||
|
|
@ -69,7 +70,7 @@ export default defineComponent({
|
|||
file: '',
|
||||
url: '',
|
||||
title: '',
|
||||
categories: [],
|
||||
tags: [],
|
||||
created: 0,
|
||||
} as Image,
|
||||
|
||||
|
|
@ -80,11 +81,19 @@ export default defineComponent({
|
|||
await this.loadImages()
|
||||
},
|
||||
methods: {
|
||||
toggleTag (t: Tag) {
|
||||
if (this.filters.tags.includes(t.slug)) {
|
||||
this.filters.tags = this.filters.tags.filter(slug => slug !== t.slug)
|
||||
} else {
|
||||
this.filters.tags.push(t.slug)
|
||||
}
|
||||
this.filtersChanged()
|
||||
},
|
||||
async loadImages () {
|
||||
const res = await fetch(`/api/newgame-data${Util.asQueryArgs(this.filters)}`)
|
||||
const json = await res.json()
|
||||
this.images = json.images
|
||||
this.categories = json.categories
|
||||
this.tags = json.tags
|
||||
},
|
||||
async filtersChanged () {
|
||||
await this.loadImages()
|
||||
|
|
@ -101,7 +110,7 @@ export default defineComponent({
|
|||
const formData = new FormData();
|
||||
formData.append('file', data.file, data.file.name);
|
||||
formData.append('title', data.title)
|
||||
formData.append('category', data.category)
|
||||
formData.append('tags', data.tags)
|
||||
|
||||
const res = await fetch('/api/upload', {
|
||||
method: 'post',
|
||||
|
|
@ -119,7 +128,7 @@ export default defineComponent({
|
|||
body: JSON.stringify({
|
||||
id: data.id,
|
||||
title: data.title,
|
||||
category: data.category,
|
||||
tags: data.tags,
|
||||
}),
|
||||
})
|
||||
return await res.json()
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ async function getExifOrientation(imagePath: string) {
|
|||
})
|
||||
}
|
||||
|
||||
const getCategories = (db: Db, imageId: number) => {
|
||||
const getTags = (db: Db, imageId: number) => {
|
||||
const query = `
|
||||
select * from categories c
|
||||
inner join image_x_category ixc on c.id = ixc.category_id
|
||||
|
|
@ -63,12 +63,12 @@ const imageFromDb = (db: Db, imageId: number) => {
|
|||
file: `${UPLOAD_DIR}/${i.filename}`,
|
||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||
title: i.title,
|
||||
categories: getCategories(db, i.id) as any[],
|
||||
tags: getTags(db, i.id) as any[],
|
||||
created: i.created * 1000,
|
||||
}
|
||||
}
|
||||
|
||||
const allImagesFromDb = (db: Db, categorySlug: string, sort: string) => {
|
||||
const allImagesFromDb = (db: Db, tagSlugs: string[], sort: string) => {
|
||||
const sortMap = {
|
||||
alpha_asc: [{filename: 1}],
|
||||
alpha_desc: [{filename: -1}],
|
||||
|
|
@ -78,16 +78,18 @@ const allImagesFromDb = (db: Db, categorySlug: string, sort: string) => {
|
|||
|
||||
// TODO: .... clean up
|
||||
const wheresRaw: Record<string, any> = {}
|
||||
if (categorySlug !== '') {
|
||||
const c = db.get('categories', {slug: categorySlug})
|
||||
if (tagSlugs.length > 0) {
|
||||
const c = db.getMany('categories', {slug: {'$in': tagSlugs}})
|
||||
if (!c) {
|
||||
return []
|
||||
}
|
||||
const where = db._buildWhere({
|
||||
'category_id': {'$in': c.map(x => x.id)}
|
||||
})
|
||||
const ids = db._getMany(`
|
||||
select i.id from image_x_category ixc
|
||||
inner join images i on i.id = ixc.image_id
|
||||
where ixc.category_id = ?;
|
||||
`, [c.id]).map(img => img.id)
|
||||
inner join images i on i.id = ixc.image_id ${where.sql};
|
||||
`, where.values).map(img => img.id)
|
||||
if (ids.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
|
@ -101,12 +103,12 @@ where ixc.category_id = ?;
|
|||
file: `${UPLOAD_DIR}/${i.filename}`,
|
||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||
title: i.title,
|
||||
categories: getCategories(db, i.id) as any[],
|
||||
tags: getTags(db, i.id) as any[],
|
||||
created: i.created * 1000,
|
||||
}))
|
||||
}
|
||||
|
||||
const allImagesFromDisk = (category: string, sort: string) => {
|
||||
const allImagesFromDisk = (tags: string[], sort: string) => {
|
||||
let images = fs.readdirSync(UPLOAD_DIR)
|
||||
.filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/))
|
||||
.map(f => ({
|
||||
|
|
@ -115,7 +117,7 @@ const allImagesFromDisk = (category: string, sort: string) => {
|
|||
file: `${UPLOAD_DIR}/${f}`,
|
||||
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
|
||||
title: f.replace(/\.[a-z]+$/, ''),
|
||||
categories: [] as any[],
|
||||
tags: [] as any[],
|
||||
created: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(),
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -63,9 +63,10 @@ app.get('/api/conf', (req, res) => {
|
|||
|
||||
app.get('/api/newgame-data', (req, res) => {
|
||||
const q = req.query as any
|
||||
const tagSlugs: string[] = q.tags ? q.tags.split(',') : []
|
||||
res.send({
|
||||
images: Images.allImagesFromDb(db, q.category, q.sort),
|
||||
categories: db.getMany('categories', {}, [{ title: 1 }]),
|
||||
images: Images.allImagesFromDb(db, tagSlugs, q.sort),
|
||||
tags: db.getMany('categories', {}, [{ title: 1 }]),
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -93,8 +94,22 @@ app.get('/api/index-data', (req, res) => {
|
|||
interface SaveImageRequestData {
|
||||
id: number
|
||||
title: string
|
||||
category: string
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
const setImageTags = (db: Db, imageId: number, tags: string[]) => {
|
||||
tags.forEach((tag: string) => {
|
||||
const slug = Util.slug(tag)
|
||||
const id = db.upsert('categories', { slug, title: tag }, { slug }, 'id')
|
||||
if (id) {
|
||||
db.insert('image_x_category', {
|
||||
image_id: imageId,
|
||||
category_id: id,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
app.post('/api/save-image', bodyParser.json(), (req, res) => {
|
||||
const data = req.body as SaveImageRequestData
|
||||
db.update('images', {
|
||||
|
|
@ -105,16 +120,8 @@ app.post('/api/save-image', bodyParser.json(), (req, res) => {
|
|||
|
||||
db.delete('image_x_category', { image_id: data.id })
|
||||
|
||||
if (data.category) {
|
||||
const title = data.category
|
||||
const slug = Util.slug(title)
|
||||
const id = db.upsert('categories', { slug, title }, { slug }, 'id')
|
||||
if (id) {
|
||||
db.insert('image_x_category', {
|
||||
image_id: data.id,
|
||||
category_id: id,
|
||||
})
|
||||
}
|
||||
if (data.tags) {
|
||||
setImageTags(db, data.id, data.tags)
|
||||
}
|
||||
|
||||
res.send({ ok: true })
|
||||
|
|
@ -140,16 +147,8 @@ app.post('/api/upload', (req, res) => {
|
|||
created: Time.timestamp(),
|
||||
})
|
||||
|
||||
if (req.body.category) {
|
||||
const title = req.body.category
|
||||
const slug = Util.slug(title)
|
||||
const id = db.upsert('categories', { slug, title }, { slug }, 'id')
|
||||
if (id) {
|
||||
db.insert('image_x_category', {
|
||||
image_id: imageId,
|
||||
category_id: id,
|
||||
})
|
||||
}
|
||||
if (req.body.tags) {
|
||||
setImageTags(db, imageId as number, req.body.tags)
|
||||
}
|
||||
|
||||
res.send(Images.imageFromDb(db, imageId as number))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue