change categories to tags in frontend
This commit is contained in:
parent
92ed17efa5
commit
5be099c61c
16 changed files with 196 additions and 112 deletions
File diff suppressed because one or more lines are too long
1
build/public/assets/index.3b46c827.js
Normal file
1
build/public/assets/index.3b46c827.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
build/public/assets/index.f09d7623.css
Normal file
1
build/public/assets/index.f09d7623.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
6
build/public/assets/vendor.b622ee49.js
Normal file
6
build/public/assets/vendor.b622ee49.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -4,9 +4,9 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
<title>🧩 jigsaw.hyottoko.club</title>
|
<title>🧩 jigsaw.hyottoko.club</title>
|
||||||
<script type="module" crossorigin src="/assets/index.0aa9cc2a.js"></script>
|
<script type="module" crossorigin src="/assets/index.3b46c827.js"></script>
|
||||||
<link rel="modulepreload" href="/assets/vendor.1ad14f11.js">
|
<link rel="modulepreload" href="/assets/vendor.b622ee49.js">
|
||||||
<link rel="stylesheet" href="/assets/index.dc049e4e.css">
|
<link rel="stylesheet" href="/assets/index.f09d7623.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -1258,7 +1258,7 @@ async function getExifOrientation(imagePath) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const getCategories = (db, imageId) => {
|
const getTags = (db, imageId) => {
|
||||||
const query = `
|
const query = `
|
||||||
select * from categories c
|
select * from categories c
|
||||||
inner join image_x_category ixc on c.id = ixc.category_id
|
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}`,
|
file: `${UPLOAD_DIR}/${i.filename}`,
|
||||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||||
title: i.title,
|
title: i.title,
|
||||||
categories: getCategories(db, i.id),
|
tags: getTags(db, i.id),
|
||||||
created: i.created * 1000,
|
created: i.created * 1000,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const allImagesFromDb = (db, categorySlug, sort) => {
|
const allImagesFromDb = (db, tagSlugs, sort) => {
|
||||||
const sortMap = {
|
const sortMap = {
|
||||||
alpha_asc: [{ filename: 1 }],
|
alpha_asc: [{ filename: 1 }],
|
||||||
alpha_desc: [{ filename: -1 }],
|
alpha_desc: [{ filename: -1 }],
|
||||||
|
|
@ -1286,16 +1286,18 @@ const allImagesFromDb = (db, categorySlug, sort) => {
|
||||||
};
|
};
|
||||||
// TODO: .... clean up
|
// TODO: .... clean up
|
||||||
const wheresRaw = {};
|
const wheresRaw = {};
|
||||||
if (categorySlug !== '') {
|
if (tagSlugs.length > 0) {
|
||||||
const c = db.get('categories', { slug: categorySlug });
|
const c = db.getMany('categories', { slug: { '$in': tagSlugs } });
|
||||||
if (!c) {
|
if (!c) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
const where = db._buildWhere({
|
||||||
|
'category_id': { '$in': c.map(x => x.id) }
|
||||||
|
});
|
||||||
const ids = db._getMany(`
|
const ids = db._getMany(`
|
||||||
select i.id from image_x_category ixc
|
select i.id from image_x_category ixc
|
||||||
inner join images i on i.id = ixc.image_id
|
inner join images i on i.id = ixc.image_id ${where.sql};
|
||||||
where ixc.category_id = ?;
|
`, where.values).map(img => img.id);
|
||||||
`, [c.id]).map(img => img.id);
|
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
@ -1308,11 +1310,11 @@ where ixc.category_id = ?;
|
||||||
file: `${UPLOAD_DIR}/${i.filename}`,
|
file: `${UPLOAD_DIR}/${i.filename}`,
|
||||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||||
title: i.title,
|
title: i.title,
|
||||||
categories: getCategories(db, i.id),
|
tags: getTags(db, i.id),
|
||||||
created: i.created * 1000,
|
created: i.created * 1000,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
const allImagesFromDisk = (category, sort) => {
|
const allImagesFromDisk = (tags, sort) => {
|
||||||
let images = fs.readdirSync(UPLOAD_DIR)
|
let images = fs.readdirSync(UPLOAD_DIR)
|
||||||
.filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/))
|
.filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/))
|
||||||
.map(f => ({
|
.map(f => ({
|
||||||
|
|
@ -1321,7 +1323,7 @@ const allImagesFromDisk = (category, sort) => {
|
||||||
file: `${UPLOAD_DIR}/${f}`,
|
file: `${UPLOAD_DIR}/${f}`,
|
||||||
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
|
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
|
||||||
title: f.replace(/\.[a-z]+$/, ''),
|
title: f.replace(/\.[a-z]+$/, ''),
|
||||||
categories: [],
|
tags: [],
|
||||||
created: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(),
|
created: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(),
|
||||||
}));
|
}));
|
||||||
switch (sort) {
|
switch (sort) {
|
||||||
|
|
@ -1901,9 +1903,10 @@ app.get('/api/conf', (req, res) => {
|
||||||
});
|
});
|
||||||
app.get('/api/newgame-data', (req, res) => {
|
app.get('/api/newgame-data', (req, res) => {
|
||||||
const q = req.query;
|
const q = req.query;
|
||||||
|
const tagSlugs = q.tags ? q.tags.split(',') : [];
|
||||||
res.send({
|
res.send({
|
||||||
images: Images.allImagesFromDb(db, q.category, q.sort),
|
images: Images.allImagesFromDb(db, tagSlugs, q.sort),
|
||||||
categories: db.getMany('categories', {}, [{ title: 1 }]),
|
tags: db.getMany('categories', {}, [{ title: 1 }]),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.get('/api/index-data', (req, res) => {
|
app.get('/api/index-data', (req, res) => {
|
||||||
|
|
@ -1925,6 +1928,18 @@ app.get('/api/index-data', (req, res) => {
|
||||||
gamesFinished: games.filter(g => !!g.finished),
|
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) => {
|
app.post('/api/save-image', bodyParser.json(), (req, res) => {
|
||||||
const data = req.body;
|
const data = req.body;
|
||||||
db.update('images', {
|
db.update('images', {
|
||||||
|
|
@ -1933,16 +1948,8 @@ app.post('/api/save-image', bodyParser.json(), (req, res) => {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
});
|
});
|
||||||
db.delete('image_x_category', { image_id: data.id });
|
db.delete('image_x_category', { image_id: data.id });
|
||||||
if (data.category) {
|
if (data.tags) {
|
||||||
const title = data.category;
|
setImageTags(db, data.id, data.tags);
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
res.send({ ok: true });
|
res.send({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
@ -1965,16 +1972,8 @@ app.post('/api/upload', (req, res) => {
|
||||||
title: req.body.title || '',
|
title: req.body.title || '',
|
||||||
created: Time.timestamp(),
|
created: Time.timestamp(),
|
||||||
});
|
});
|
||||||
if (req.body.category) {
|
if (req.body.tags) {
|
||||||
const title = req.body.category;
|
setImageTags(db, imageId, req.body.tags);
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
res.send(Images.imageFromDb(db, imageId));
|
res.send(Images.imageFromDb(db, imageId));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export type EncodedPlayer = Array<any>
|
||||||
export type EncodedPiece = Array<any>
|
export type EncodedPiece = Array<any>
|
||||||
export type EncodedPieceShape = number
|
export type EncodedPieceShape = number
|
||||||
|
|
||||||
export interface Category {
|
export interface Tag {
|
||||||
id: number
|
id: number
|
||||||
slug: string
|
slug: string
|
||||||
title: string
|
title: string
|
||||||
|
|
@ -34,7 +34,7 @@ export interface Image {
|
||||||
file: string
|
file: string
|
||||||
url: string
|
url: string
|
||||||
title: string
|
title: string
|
||||||
categories: Array<Category>
|
tags: Array<Tag>
|
||||||
created: number
|
created: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,11 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<!-- TODO: autocomplete category -->
|
<!-- TODO: autocomplete tags -->
|
||||||
<td><label>Category</label></td>
|
<td><label>Tags</label></td>
|
||||||
<td><input type="text" v-model="category" placeholder="Plants" /></td>
|
<td>
|
||||||
|
<tags-input v-model="tags" />
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -36,14 +38,16 @@
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType } from 'vue'
|
import { defineComponent, PropType } from 'vue'
|
||||||
import { Image } from '../../common/GameCommon'
|
import { Image, Tag } from '../../common/GameCommon'
|
||||||
|
|
||||||
import ResponsiveImage from './ResponsiveImage.vue'
|
import ResponsiveImage from './ResponsiveImage.vue'
|
||||||
|
import TagsInput from './TagsInput.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'edit-image-dialog',
|
name: 'edit-image-dialog',
|
||||||
components: {
|
components: {
|
||||||
ResponsiveImage,
|
ResponsiveImage,
|
||||||
|
TagsInput,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
image: {
|
image: {
|
||||||
|
|
@ -58,19 +62,19 @@ export default defineComponent({
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
title: '',
|
title: '',
|
||||||
category: '',
|
tags: [] as string[],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
this.title = this.image.title
|
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: {
|
methods: {
|
||||||
saveImage () {
|
saveImage () {
|
||||||
this.$emit('saveClick', {
|
this.$emit('saveClick', {
|
||||||
id: this.image.id,
|
id: this.image.id,
|
||||||
title: this.title,
|
title: this.title,
|
||||||
category: this.category,
|
tags: this.tags,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,8 @@ gallery", if possible!
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<label class="upload">
|
<label class="upload">
|
||||||
<input type="file" style="display: none" @change="preview" accept="image/*" />
|
<input type="file" style="display: none" @change="preview" accept="image/*" />
|
||||||
<span class="btn">{{label || 'Upload File'}}</span>
|
<span class="btn">Upload File</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|
||||||
<!-- TODO: drop area for image -->
|
|
||||||
<!-- <upload class="upload" @uploaded="mediaImgUploaded($event)" accept="image/*" label="Upload an image" /> -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -37,9 +33,11 @@ gallery", if possible!
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<!-- TODO: autocomplete category -->
|
<!-- TODO: autocomplete tags -->
|
||||||
<td><label>Category</label></td>
|
<td><label>Tags</label></td>
|
||||||
<td><input type="text" v-model="category" placeholder="Plants" /></td>
|
<td>
|
||||||
|
<tags-input v-model="tags" />
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -55,14 +53,14 @@ gallery", if possible!
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
import Upload from './Upload.vue'
|
|
||||||
import ResponsiveImage from './ResponsiveImage.vue'
|
import ResponsiveImage from './ResponsiveImage.vue'
|
||||||
|
import TagsInput from './TagsInput.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'new-image-dialog',
|
name: 'new-image-dialog',
|
||||||
components: {
|
components: {
|
||||||
Upload,
|
|
||||||
ResponsiveImage,
|
ResponsiveImage,
|
||||||
|
TagsInput,
|
||||||
},
|
},
|
||||||
emits: {
|
emits: {
|
||||||
bgclick: null,
|
bgclick: null,
|
||||||
|
|
@ -74,7 +72,7 @@ export default defineComponent({
|
||||||
previewUrl: '',
|
previewUrl: '',
|
||||||
file: null as File|null,
|
file: null as File|null,
|
||||||
title: '',
|
title: '',
|
||||||
category: '',
|
tags: [] as string[],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -103,14 +101,14 @@ export default defineComponent({
|
||||||
this.$emit('postToGalleryClick', {
|
this.$emit('postToGalleryClick', {
|
||||||
file: this.file,
|
file: this.file,
|
||||||
title: this.title,
|
title: this.title,
|
||||||
category: this.category,
|
tags: this.tags,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
setupGameClick () {
|
setupGameClick () {
|
||||||
this.$emit('setupGameClick', {
|
this.$emit('setupGameClick', {
|
||||||
file: this.file,
|
file: this.file,
|
||||||
title: this.title,
|
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-color: #808db0;
|
||||||
--link-hover-color: #c5cfeb;
|
--link-hover-color: #c5cfeb;
|
||||||
--highlight-color: #dd7e7e;
|
--highlight-color: #dd7e7e;
|
||||||
|
--positive-color: #64a756;
|
||||||
--input-bg-color: #262523;
|
--input-bg-color: #262523;
|
||||||
--bg-color: rgba(0,0,0,.7);
|
--bg-color: rgba(0,0,0,.7);
|
||||||
}
|
}
|
||||||
|
|
@ -212,6 +213,18 @@ kbd {
|
||||||
color: var(--main-darker-color);
|
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 {
|
.upload-image-teaser {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,13 @@ in jigsawpuzzles.io
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label v-if="categories.length > 0">
|
<label v-if="tags.length > 0">
|
||||||
Category:
|
Tags:
|
||||||
<select v-model="filters.category" @change="filtersChanged">
|
<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 value="">All</option>
|
||||||
<option v-for="(c, idx) in categories" :key="idx" :value="c.slug">{{c.title}}</option>
|
<option v-for="(c, idx) in tags" :key="idx" :value="c.slug">{{c.title}}</option>
|
||||||
</select>
|
</select> -->
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Sort by:
|
Sort by:
|
||||||
|
|
@ -44,7 +45,7 @@ import ImageLibrary from './../components/ImageLibrary.vue'
|
||||||
import NewImageDialog from './../components/NewImageDialog.vue'
|
import NewImageDialog from './../components/NewImageDialog.vue'
|
||||||
import EditImageDialog from './../components/EditImageDialog.vue'
|
import EditImageDialog from './../components/EditImageDialog.vue'
|
||||||
import NewGameDialog from './../components/NewGameDialog.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'
|
import Util from '../../common/Util'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
|
@ -58,10 +59,10 @@ export default defineComponent({
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
sort: 'date_desc',
|
sort: 'date_desc',
|
||||||
category: '',
|
tags: [] as string[],
|
||||||
},
|
},
|
||||||
images: [],
|
images: [],
|
||||||
categories: [],
|
tags: [],
|
||||||
|
|
||||||
image: {
|
image: {
|
||||||
id: 0,
|
id: 0,
|
||||||
|
|
@ -69,7 +70,7 @@ export default defineComponent({
|
||||||
file: '',
|
file: '',
|
||||||
url: '',
|
url: '',
|
||||||
title: '',
|
title: '',
|
||||||
categories: [],
|
tags: [],
|
||||||
created: 0,
|
created: 0,
|
||||||
} as Image,
|
} as Image,
|
||||||
|
|
||||||
|
|
@ -80,11 +81,19 @@ export default defineComponent({
|
||||||
await this.loadImages()
|
await this.loadImages()
|
||||||
},
|
},
|
||||||
methods: {
|
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 () {
|
async loadImages () {
|
||||||
const res = await fetch(`/api/newgame-data${Util.asQueryArgs(this.filters)}`)
|
const res = await fetch(`/api/newgame-data${Util.asQueryArgs(this.filters)}`)
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
this.images = json.images
|
this.images = json.images
|
||||||
this.categories = json.categories
|
this.tags = json.tags
|
||||||
},
|
},
|
||||||
async filtersChanged () {
|
async filtersChanged () {
|
||||||
await this.loadImages()
|
await this.loadImages()
|
||||||
|
|
@ -101,7 +110,7 @@ export default defineComponent({
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', data.file, data.file.name);
|
formData.append('file', data.file, data.file.name);
|
||||||
formData.append('title', data.title)
|
formData.append('title', data.title)
|
||||||
formData.append('category', data.category)
|
formData.append('tags', data.tags)
|
||||||
|
|
||||||
const res = await fetch('/api/upload', {
|
const res = await fetch('/api/upload', {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
|
|
@ -119,7 +128,7 @@ export default defineComponent({
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
id: data.id,
|
id: data.id,
|
||||||
title: data.title,
|
title: data.title,
|
||||||
category: data.category,
|
tags: data.tags,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
return await res.json()
|
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 = `
|
const query = `
|
||||||
select * from categories c
|
select * from categories c
|
||||||
inner join image_x_category ixc on c.id = ixc.category_id
|
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}`,
|
file: `${UPLOAD_DIR}/${i.filename}`,
|
||||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||||
title: i.title,
|
title: i.title,
|
||||||
categories: getCategories(db, i.id) as any[],
|
tags: getTags(db, i.id) as any[],
|
||||||
created: i.created * 1000,
|
created: i.created * 1000,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allImagesFromDb = (db: Db, categorySlug: string, sort: string) => {
|
const allImagesFromDb = (db: Db, tagSlugs: string[], sort: string) => {
|
||||||
const sortMap = {
|
const sortMap = {
|
||||||
alpha_asc: [{filename: 1}],
|
alpha_asc: [{filename: 1}],
|
||||||
alpha_desc: [{filename: -1}],
|
alpha_desc: [{filename: -1}],
|
||||||
|
|
@ -78,16 +78,18 @@ const allImagesFromDb = (db: Db, categorySlug: string, sort: string) => {
|
||||||
|
|
||||||
// TODO: .... clean up
|
// TODO: .... clean up
|
||||||
const wheresRaw: Record<string, any> = {}
|
const wheresRaw: Record<string, any> = {}
|
||||||
if (categorySlug !== '') {
|
if (tagSlugs.length > 0) {
|
||||||
const c = db.get('categories', {slug: categorySlug})
|
const c = db.getMany('categories', {slug: {'$in': tagSlugs}})
|
||||||
if (!c) {
|
if (!c) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
const where = db._buildWhere({
|
||||||
|
'category_id': {'$in': c.map(x => x.id)}
|
||||||
|
})
|
||||||
const ids = db._getMany(`
|
const ids = db._getMany(`
|
||||||
select i.id from image_x_category ixc
|
select i.id from image_x_category ixc
|
||||||
inner join images i on i.id = ixc.image_id
|
inner join images i on i.id = ixc.image_id ${where.sql};
|
||||||
where ixc.category_id = ?;
|
`, where.values).map(img => img.id)
|
||||||
`, [c.id]).map(img => img.id)
|
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
@ -101,12 +103,12 @@ where ixc.category_id = ?;
|
||||||
file: `${UPLOAD_DIR}/${i.filename}`,
|
file: `${UPLOAD_DIR}/${i.filename}`,
|
||||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||||
title: i.title,
|
title: i.title,
|
||||||
categories: getCategories(db, i.id) as any[],
|
tags: getTags(db, i.id) as any[],
|
||||||
created: i.created * 1000,
|
created: i.created * 1000,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const allImagesFromDisk = (category: string, sort: string) => {
|
const allImagesFromDisk = (tags: string[], sort: string) => {
|
||||||
let images = fs.readdirSync(UPLOAD_DIR)
|
let images = fs.readdirSync(UPLOAD_DIR)
|
||||||
.filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/))
|
.filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/))
|
||||||
.map(f => ({
|
.map(f => ({
|
||||||
|
|
@ -115,7 +117,7 @@ const allImagesFromDisk = (category: string, sort: string) => {
|
||||||
file: `${UPLOAD_DIR}/${f}`,
|
file: `${UPLOAD_DIR}/${f}`,
|
||||||
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
|
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
|
||||||
title: f.replace(/\.[a-z]+$/, ''),
|
title: f.replace(/\.[a-z]+$/, ''),
|
||||||
categories: [] as any[],
|
tags: [] as any[],
|
||||||
created: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(),
|
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) => {
|
app.get('/api/newgame-data', (req, res) => {
|
||||||
const q = req.query as any
|
const q = req.query as any
|
||||||
|
const tagSlugs: string[] = q.tags ? q.tags.split(',') : []
|
||||||
res.send({
|
res.send({
|
||||||
images: Images.allImagesFromDb(db, q.category, q.sort),
|
images: Images.allImagesFromDb(db, tagSlugs, q.sort),
|
||||||
categories: db.getMany('categories', {}, [{ title: 1 }]),
|
tags: db.getMany('categories', {}, [{ title: 1 }]),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -93,8 +94,22 @@ app.get('/api/index-data', (req, res) => {
|
||||||
interface SaveImageRequestData {
|
interface SaveImageRequestData {
|
||||||
id: number
|
id: number
|
||||||
title: string
|
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) => {
|
app.post('/api/save-image', bodyParser.json(), (req, res) => {
|
||||||
const data = req.body as SaveImageRequestData
|
const data = req.body as SaveImageRequestData
|
||||||
db.update('images', {
|
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 })
|
db.delete('image_x_category', { image_id: data.id })
|
||||||
|
|
||||||
if (data.category) {
|
if (data.tags) {
|
||||||
const title = data.category
|
setImageTags(db, data.id, data.tags)
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.send({ ok: true })
|
res.send({ ok: true })
|
||||||
|
|
@ -140,16 +147,8 @@ app.post('/api/upload', (req, res) => {
|
||||||
created: Time.timestamp(),
|
created: Time.timestamp(),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (req.body.category) {
|
if (req.body.tags) {
|
||||||
const title = req.body.category
|
setImageTags(db, imageId as number, req.body.tags)
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.send(Images.imageFromDb(db, imageId as number))
|
res.send(Images.imageFromDb(db, imageId as number))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue