change categories to tags in frontend

This commit is contained in:
Zutatensuppe 2021-05-22 15:48:13 +02:00
parent 92ed17efa5
commit 5be099c61c
16 changed files with 196 additions and 112 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -4,9 +4,9 @@
<meta charset="UTF-8">
<title>🧩 jigsaw.hyottoko.club</title>
<script type="module" crossorigin src="/assets/index.0aa9cc2a.js"></script>
<link rel="modulepreload" href="/assets/vendor.1ad14f11.js">
<link rel="stylesheet" href="/assets/index.dc049e4e.css">
<script type="module" crossorigin src="/assets/index.3b46c827.js"></script>
<link rel="modulepreload" href="/assets/vendor.b622ee49.js">
<link rel="stylesheet" href="/assets/index.f09d7623.css">
</head>
<body>
<div id="app"></div>

View file

@ -1258,7 +1258,7 @@ async function getExifOrientation(imagePath) {
});
});
}
const getCategories = (db, imageId) => {
const getTags = (db, imageId) => {
const query = `
select * from categories c
inner join image_x_category ixc on c.id = ixc.category_id
@ -1273,11 +1273,11 @@ const imageFromDb = (db, imageId) => {
file: `${UPLOAD_DIR}/${i.filename}`,
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
title: i.title,
categories: getCategories(db, i.id),
tags: getTags(db, i.id),
created: i.created * 1000,
};
};
const allImagesFromDb = (db, categorySlug, sort) => {
const allImagesFromDb = (db, tagSlugs, sort) => {
const sortMap = {
alpha_asc: [{ filename: 1 }],
alpha_desc: [{ filename: -1 }],
@ -1286,16 +1286,18 @@ const allImagesFromDb = (db, categorySlug, sort) => {
};
// TODO: .... clean up
const wheresRaw = {};
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 [];
}
@ -1308,11 +1310,11 @@ where ixc.category_id = ?;
file: `${UPLOAD_DIR}/${i.filename}`,
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
title: i.title,
categories: getCategories(db, i.id),
tags: getTags(db, i.id),
created: i.created * 1000,
}));
};
const allImagesFromDisk = (category, sort) => {
const allImagesFromDisk = (tags, sort) => {
let images = fs.readdirSync(UPLOAD_DIR)
.filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/))
.map(f => ({
@ -1321,7 +1323,7 @@ const allImagesFromDisk = (category, sort) => {
file: `${UPLOAD_DIR}/${f}`,
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
title: f.replace(/\.[a-z]+$/, ''),
categories: [],
tags: [],
created: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(),
}));
switch (sort) {
@ -1901,9 +1903,10 @@ app.get('/api/conf', (req, res) => {
});
app.get('/api/newgame-data', (req, res) => {
const q = req.query;
const tagSlugs = 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 }]),
});
});
app.get('/api/index-data', (req, res) => {
@ -1925,6 +1928,18 @@ app.get('/api/index-data', (req, res) => {
gamesFinished: games.filter(g => !!g.finished),
});
});
const setImageTags = (db, imageId, tags) => {
tags.forEach((tag) => {
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;
db.update('images', {
@ -1933,16 +1948,8 @@ app.post('/api/save-image', bodyParser.json(), (req, res) => {
id: data.id,
});
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 });
});
@ -1965,16 +1972,8 @@ app.post('/api/upload', (req, res) => {
title: req.body.title || '',
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, req.body.tags);
}
res.send(Images.imageFromDb(db, imageId));
});

View file

@ -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
}

View file

@ -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,
})
},
},

View file

@ -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,
})
},
},

View 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>

View file

@ -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;
}

View file

@ -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()

View file

@ -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(),
}))

View file

@ -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))