add ability to update image info
This commit is contained in:
parent
6474c2fd8c
commit
92ed17efa5
12 changed files with 244 additions and 14 deletions
1
build/public/assets/index.0aa9cc2a.js
Normal file
1
build/public/assets/index.0aa9cc2a.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
File diff suppressed because one or more lines are too long
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.1c0291a2.js"></script>
|
<script type="module" crossorigin src="/assets/index.0aa9cc2a.js"></script>
|
||||||
<link rel="modulepreload" href="/assets/vendor.8616a479.js">
|
<link rel="modulepreload" href="/assets/vendor.1ad14f11.js">
|
||||||
<link rel="stylesheet" href="/assets/index.c5b0553c.css">
|
<link rel="stylesheet" href="/assets/index.dc049e4e.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -1925,7 +1925,28 @@ app.get('/api/index-data', (req, res) => {
|
||||||
gamesFinished: games.filter(g => !!g.finished),
|
gamesFinished: games.filter(g => !!g.finished),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.post('/upload', (req, res) => {
|
app.post('/api/save-image', bodyParser.json(), (req, res) => {
|
||||||
|
const data = req.body;
|
||||||
|
db.update('images', {
|
||||||
|
title: data.title,
|
||||||
|
}, {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.send({ ok: true });
|
||||||
|
});
|
||||||
|
app.post('/api/upload', (req, res) => {
|
||||||
upload(req, res, async (err) => {
|
upload(req, res, async (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.log(err);
|
log.log(err);
|
||||||
|
|
|
||||||
132
src/frontend/components/EditImageDialog.vue
Normal file
132
src/frontend/components/EditImageDialog.vue
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
<template>
|
||||||
|
<div class="overlay edit-image-dialog" @click="$emit('bgclick')">
|
||||||
|
<div class="overlay-content" @click.stop="">
|
||||||
|
|
||||||
|
<div class="area-image">
|
||||||
|
<div class="has-image">
|
||||||
|
<responsive-image :src="image.url" :title="image.title" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="area-settings">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td><label>Title</label></td>
|
||||||
|
<td><input type="text" v-model="title" placeholder="Flower by @artist" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
<div class="hint">Feel free to leave a credit to the artist/photographer in the title :)</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<!-- TODO: autocomplete category -->
|
||||||
|
<td><label>Category</label></td>
|
||||||
|
<td><input type="text" v-model="category" placeholder="Plants" /></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="area-buttons">
|
||||||
|
<button class="btn" @click="saveImage">🖼️ Save image</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType } from 'vue'
|
||||||
|
import { Image } from '../../common/GameCommon'
|
||||||
|
|
||||||
|
import ResponsiveImage from './ResponsiveImage.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'edit-image-dialog',
|
||||||
|
components: {
|
||||||
|
ResponsiveImage,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
image: {
|
||||||
|
type: Object as PropType<Image>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
bgclick: null,
|
||||||
|
saveClick: null,
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
title: '',
|
||||||
|
category: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.title = this.image.title
|
||||||
|
this.category = this.image.categories.length > 0 ? this.image.categories[0].title : ''
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
saveImage () {
|
||||||
|
this.$emit('saveClick', {
|
||||||
|
id: this.image.id,
|
||||||
|
title: this.title,
|
||||||
|
category: this.category,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
// TODO: scoped vs .edit-image-dialog
|
||||||
|
<style>
|
||||||
|
.edit-image-dialog .overlay-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 450px;
|
||||||
|
grid-template-rows: auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"image settings"
|
||||||
|
"image buttons";
|
||||||
|
height: 90%;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-image-dialog .area-image {
|
||||||
|
grid-area: image;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
.edit-image-dialog .area-image.no-image {
|
||||||
|
align-content: center;
|
||||||
|
display: grid;
|
||||||
|
text-align: center;
|
||||||
|
border: dashed 6px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.edit-image-dialog .area-image .has-image {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.edit-image-dialog .area-image .has-image .remove {
|
||||||
|
position: absolute;
|
||||||
|
top: .5em;
|
||||||
|
left: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-image-dialog .area-settings {
|
||||||
|
grid-area: settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-image-dialog .area-settings table input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-image-dialog .area-buttons {
|
||||||
|
align-self: end;
|
||||||
|
grid-area: buttons;
|
||||||
|
}
|
||||||
|
.edit-image-dialog .area-buttons button {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: .5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<image-teaser v-for="(i,idx) in images" :image="i" @click="imageClicked(i)" :key="idx" />
|
<image-teaser v-for="(i,idx) in images" :image="i" @click="imageClicked(i)" @editClick="imageEditClicked(i)" :key="idx" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
@ -22,11 +22,15 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
emits: {
|
emits: {
|
||||||
imageClicked: null,
|
imageClicked: null,
|
||||||
|
imageEditClicked: null,
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
imageClicked (image: Image) {
|
imageClicked (image: Image) {
|
||||||
this.$emit('imageClicked', image)
|
this.$emit('imageClicked', image)
|
||||||
},
|
},
|
||||||
|
imageEditClicked (image: Image) {
|
||||||
|
this.$emit('imageEditClicked', image)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="imageteaser" :style="style" @click="onClick"></div>
|
<div
|
||||||
|
class="imageteaser"
|
||||||
|
:style="style"
|
||||||
|
@click="onClick">
|
||||||
|
<div class="btn edit" @click.stop="onEditClick">✏️</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
|
|
@ -20,10 +25,22 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: {
|
||||||
|
click: null,
|
||||||
|
editClick: null,
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onClick() {
|
onClick() {
|
||||||
this.$emit('click')
|
this.$emit('click')
|
||||||
},
|
},
|
||||||
|
onEditClick() {
|
||||||
|
this.$emit('editClick')
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
<style type="css">
|
||||||
|
.imageteaser { position: relative; }
|
||||||
|
.imageteaser .edit { display: none; position: absolute; }
|
||||||
|
.imageteaser:hover .edit { display: inline-block; }
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,9 @@ in jigsawpuzzles.io
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<image-library :images="images" @imageClicked="imageClicked" />
|
<image-library :images="images" @imageClicked="onImageClicked" @imageEditClicked="onImageEditClicked" />
|
||||||
<new-image-dialog v-if="dialog==='new-image'" @bgclick="dialog=''" @postToGalleryClick="postToGalleryClick" @setupGameClick="setupGameClick" />
|
<new-image-dialog v-if="dialog==='new-image'" @bgclick="dialog=''" @postToGalleryClick="postToGalleryClick" @setupGameClick="setupGameClick" />
|
||||||
|
<edit-image-dialog v-if="dialog==='edit-image'" @bgclick="dialog=''" @saveClick="onSaveImageClick" :image="image" />
|
||||||
<new-game-dialog v-if="image && dialog==='new-game'" @bgclick="dialog=''" @newGame="onNewGame" :image="image" />
|
<new-game-dialog v-if="image && dialog==='new-game'" @bgclick="dialog=''" @newGame="onNewGame" :image="image" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -41,6 +42,7 @@ import { defineComponent } from 'vue'
|
||||||
|
|
||||||
import ImageLibrary from './../components/ImageLibrary.vue'
|
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 NewGameDialog from './../components/NewGameDialog.vue'
|
import NewGameDialog from './../components/NewGameDialog.vue'
|
||||||
import { GameSettings, Image } from '../../common/GameCommon'
|
import { GameSettings, Image } from '../../common/GameCommon'
|
||||||
import Util from '../../common/Util'
|
import Util from '../../common/Util'
|
||||||
|
|
@ -49,6 +51,7 @@ export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
ImageLibrary,
|
ImageLibrary,
|
||||||
NewImageDialog,
|
NewImageDialog,
|
||||||
|
EditImageDialog,
|
||||||
NewGameDialog,
|
NewGameDialog,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -86,22 +89,46 @@ export default defineComponent({
|
||||||
async filtersChanged () {
|
async filtersChanged () {
|
||||||
await this.loadImages()
|
await this.loadImages()
|
||||||
},
|
},
|
||||||
imageClicked (image: Image) {
|
onImageClicked (image: Image) {
|
||||||
this.image = image
|
this.image = image
|
||||||
this.dialog = 'new-game'
|
this.dialog = 'new-game'
|
||||||
},
|
},
|
||||||
|
onImageEditClicked (image: Image) {
|
||||||
|
this.image = image
|
||||||
|
this.dialog = 'edit-image'
|
||||||
|
},
|
||||||
async uploadImage (data: any) {
|
async uploadImage (data: any) {
|
||||||
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('category', data.category)
|
||||||
|
|
||||||
const res = await fetch('/upload', {
|
const res = await fetch('/api/upload', {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
body: formData,
|
body: formData,
|
||||||
})
|
})
|
||||||
return await res.json()
|
return await res.json()
|
||||||
},
|
},
|
||||||
|
async saveImage (data: any) {
|
||||||
|
const res = await fetch('/api/save-image', {
|
||||||
|
method: 'post',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: data.id,
|
||||||
|
title: data.title,
|
||||||
|
category: data.category,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
return await res.json()
|
||||||
|
},
|
||||||
|
async onSaveImageClick(data: any) {
|
||||||
|
await this.saveImage(data)
|
||||||
|
this.dialog = ''
|
||||||
|
await this.loadImages()
|
||||||
|
},
|
||||||
async postToGalleryClick(data: any) {
|
async postToGalleryClick(data: any) {
|
||||||
await this.uploadImage(data)
|
await this.uploadImage(data)
|
||||||
this.dialog = ''
|
this.dialog = ''
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,36 @@ app.get('/api/index-data', (req, res) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.post('/upload', (req, res) => {
|
interface SaveImageRequestData {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
category: string
|
||||||
|
}
|
||||||
|
app.post('/api/save-image', bodyParser.json(), (req, res) => {
|
||||||
|
const data = req.body as SaveImageRequestData
|
||||||
|
db.update('images', {
|
||||||
|
title: data.title,
|
||||||
|
}, {
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({ ok: true })
|
||||||
|
})
|
||||||
|
app.post('/api/upload', (req, res) => {
|
||||||
upload(req, res, async (err: any) => {
|
upload(req, res, async (err: any) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.log(err)
|
log.log(err)
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export default vite.defineConfig({
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'^/((api|uploads)/.*|upload)': {
|
'^/(api|uploads)/.*': {
|
||||||
target: `http://${cfg.http.hostname}:${cfg.http.port}`,
|
target: `http://${cfg.http.hostname}:${cfg.http.port}`,
|
||||||
secure: false,
|
secure: false,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue