categorys and titles for images
This commit is contained in:
parent
8abbb13fcc
commit
239c879649
22 changed files with 964 additions and 86 deletions
1
build/public/assets/index.1c0291a2.js
Normal file
1
build/public/assets/index.1c0291a2.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
|
|
@ -4,9 +4,9 @@
|
|||
<meta charset="UTF-8">
|
||||
|
||||
<title>🧩 jigsaw.hyottoko.club</title>
|
||||
<script type="module" crossorigin src="/assets/index.b49dadca.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index.1c0291a2.js"></script>
|
||||
<link rel="modulepreload" href="/assets/vendor.8616a479.js">
|
||||
<link rel="stylesheet" href="/assets/index.6c4f6859.css">
|
||||
<link rel="stylesheet" href="/assets/index.c5b0553c.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import exif from 'exif';
|
|||
import sharp from 'sharp';
|
||||
import bodyParser from 'body-parser';
|
||||
import v8 from 'v8';
|
||||
import bsqlite from 'better-sqlite3';
|
||||
|
||||
class Rng {
|
||||
constructor(seed) {
|
||||
|
|
@ -50,6 +51,12 @@ class Rng {
|
|||
}
|
||||
}
|
||||
|
||||
const slug = (str) => {
|
||||
let tmp = str.toLowerCase();
|
||||
tmp = tmp.replace(/[^a-z0-9]+/g, '-');
|
||||
tmp = tmp.replace(/^-|-$/, '');
|
||||
return tmp;
|
||||
};
|
||||
const pad = (x, pad) => {
|
||||
const str = `${x}`;
|
||||
if (str.length >= pad.length) {
|
||||
|
|
@ -194,6 +201,7 @@ function asQueryArgs(data) {
|
|||
}
|
||||
var Util = {
|
||||
hash,
|
||||
slug,
|
||||
uniqId,
|
||||
encodeShape,
|
||||
decodeShape,
|
||||
|
|
@ -207,7 +215,7 @@ var Util = {
|
|||
asQueryArgs,
|
||||
};
|
||||
|
||||
const log$4 = logger('WebSocketServer.js');
|
||||
const log$5 = logger('WebSocketServer.js');
|
||||
/*
|
||||
Example config
|
||||
|
||||
|
|
@ -245,12 +253,12 @@ class WebSocketServer {
|
|||
this._websocketserver.on('connection', (socket, request) => {
|
||||
const pathname = new URL(this.config.connectstring).pathname;
|
||||
if (request.url.indexOf(pathname) !== 0) {
|
||||
log$4.log('bad request url: ', request.url);
|
||||
log$5.log('bad request url: ', request.url);
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
socket.on('message', (data) => {
|
||||
log$4.log(`ws`, socket.protocol, data);
|
||||
log$5.log(`ws`, socket.protocol, data);
|
||||
this.evt.dispatch('message', { socket, data });
|
||||
});
|
||||
socket.on('close', () => {
|
||||
|
|
@ -1164,8 +1172,10 @@ const DATA_DIR = `${BASE_DIR}/data`;
|
|||
const UPLOAD_DIR = `${BASE_DIR}/data/uploads`;
|
||||
const UPLOAD_URL = `/uploads`;
|
||||
const PUBLIC_DIR = `${BASE_DIR}/build/public/`;
|
||||
const DB_PATCHES_DIR = `${BASE_DIR}/src/dbpatches`;
|
||||
const DB_FILE = `${BASE_DIR}/data/db.sqlite`;
|
||||
|
||||
const log$3 = logger('GameLog.js');
|
||||
const log$4 = logger('GameLog.js');
|
||||
const filename = (gameId) => `${DATA_DIR}/log_${gameId}.log`;
|
||||
const create = (gameId) => {
|
||||
const file = filename(gameId);
|
||||
|
|
@ -1196,8 +1206,8 @@ const get = (gameId) => {
|
|||
return JSON.parse(line);
|
||||
}
|
||||
catch (e) {
|
||||
log$3.log(line);
|
||||
log$3.log(e);
|
||||
log$4.log(line);
|
||||
log$4.log(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -1248,16 +1258,71 @@ async function getExifOrientation(imagePath) {
|
|||
});
|
||||
});
|
||||
}
|
||||
const allImages = (sort) => {
|
||||
const getCategories = (db, imageId) => {
|
||||
const query = `
|
||||
select * from categories c
|
||||
inner join image_x_category ixc on c.id = ixc.category_id
|
||||
where ixc.image_id = ?`;
|
||||
return db._getMany(query, [imageId]);
|
||||
};
|
||||
const imageFromDb = (db, imageId) => {
|
||||
const i = db.get('images', { id: imageId });
|
||||
return {
|
||||
id: i.id,
|
||||
filename: i.filename,
|
||||
file: `${UPLOAD_DIR}/${i.filename}`,
|
||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||
title: i.title,
|
||||
categories: getCategories(db, i.id),
|
||||
created: i.created * 1000,
|
||||
};
|
||||
};
|
||||
const allImagesFromDb = (db, categorySlug, sort) => {
|
||||
const sortMap = {
|
||||
alpha_asc: [{ filename: 1 }],
|
||||
alpha_desc: [{ filename: -1 }],
|
||||
date_asc: [{ created: 1 }],
|
||||
date_desc: [{ created: -1 }],
|
||||
};
|
||||
// TODO: .... clean up
|
||||
const wheresRaw = {};
|
||||
if (categorySlug !== '') {
|
||||
const c = db.get('categories', { slug: categorySlug });
|
||||
if (!c) {
|
||||
return [];
|
||||
}
|
||||
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);
|
||||
if (ids.length === 0) {
|
||||
return [];
|
||||
}
|
||||
wheresRaw['id'] = { '$in': ids };
|
||||
}
|
||||
const images = db.getMany('images', wheresRaw, sortMap[sort]);
|
||||
return images.map(i => ({
|
||||
id: i.id,
|
||||
filename: i.filename,
|
||||
file: `${UPLOAD_DIR}/${i.filename}`,
|
||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||
title: i.title,
|
||||
categories: getCategories(db, i.id),
|
||||
created: i.created * 1000,
|
||||
}));
|
||||
};
|
||||
const allImagesFromDisk = (category, sort) => {
|
||||
let images = fs.readdirSync(UPLOAD_DIR)
|
||||
.filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/))
|
||||
.map(f => ({
|
||||
id: 0,
|
||||
filename: f,
|
||||
file: `${UPLOAD_DIR}/${f}`,
|
||||
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
|
||||
title: '',
|
||||
category: '',
|
||||
ts: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(),
|
||||
title: f.replace(/\.[a-z]+$/, ''),
|
||||
categories: [],
|
||||
created: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(),
|
||||
}));
|
||||
switch (sort) {
|
||||
case 'alpha_asc':
|
||||
|
|
@ -1272,13 +1337,13 @@ const allImages = (sort) => {
|
|||
break;
|
||||
case 'date_asc':
|
||||
images = images.sort((a, b) => {
|
||||
return a.ts > b.ts ? 1 : -1;
|
||||
return a.created > b.created ? 1 : -1;
|
||||
});
|
||||
break;
|
||||
case 'date_desc':
|
||||
default:
|
||||
images = images.sort((a, b) => {
|
||||
return a.ts < b.ts ? 1 : -1;
|
||||
return a.created < b.created ? 1 : -1;
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
|
@ -1298,7 +1363,9 @@ async function getDimensions(imagePath) {
|
|||
return dimensions;
|
||||
}
|
||||
var Images = {
|
||||
allImages,
|
||||
allImagesFromDisk,
|
||||
imageFromDb,
|
||||
allImagesFromDb,
|
||||
resizeImage,
|
||||
getDimensions,
|
||||
};
|
||||
|
|
@ -1477,7 +1544,7 @@ const determinePuzzleInfo = (w, h, targetTiles) => {
|
|||
};
|
||||
};
|
||||
|
||||
const log$2 = logger('GameStorage.js');
|
||||
const log$3 = logger('GameStorage.js');
|
||||
const DIRTY_GAMES = {};
|
||||
function setDirty(gameId) {
|
||||
DIRTY_GAMES[gameId] = true;
|
||||
|
|
@ -1504,7 +1571,7 @@ function loadGame(gameId) {
|
|||
game = JSON.parse(contents);
|
||||
}
|
||||
catch {
|
||||
log$2.log(`[ERR] unable to load game from file ${file}`);
|
||||
log$3.log(`[ERR] unable to load game from file ${file}`);
|
||||
}
|
||||
if (typeof game.puzzle.data.started === 'undefined') {
|
||||
game.puzzle.data.started = Math.round(fs.statSync(file).ctimeMs);
|
||||
|
|
@ -1549,7 +1616,7 @@ function persistGame(gameId) {
|
|||
players: game.players,
|
||||
scoreMode: game.scoreMode,
|
||||
}));
|
||||
log$2.info(`[INFO] persisted game ${game.id}`);
|
||||
log$3.info(`[INFO] persisted game ${game.id}`);
|
||||
}
|
||||
var GameStorage = {
|
||||
loadGames,
|
||||
|
|
@ -1615,7 +1682,7 @@ var Game = {
|
|||
getFinishTs: GameCommon.getFinishTs,
|
||||
};
|
||||
|
||||
const log$1 = logger('GameSocket.js');
|
||||
const log$2 = logger('GameSocket.js');
|
||||
// Map<gameId, Socket[]>
|
||||
const SOCKETS = {};
|
||||
function socketExists(gameId, socket) {
|
||||
|
|
@ -1629,8 +1696,8 @@ function removeSocket(gameId, socket) {
|
|||
return;
|
||||
}
|
||||
SOCKETS[gameId] = SOCKETS[gameId].filter((s) => s !== socket);
|
||||
log$1.log('removed socket: ', gameId, socket.protocol);
|
||||
log$1.log('socket count: ', Object.keys(SOCKETS[gameId]).length);
|
||||
log$2.log('removed socket: ', gameId, socket.protocol);
|
||||
log$2.log('socket count: ', Object.keys(SOCKETS[gameId]).length);
|
||||
}
|
||||
function addSocket(gameId, socket) {
|
||||
if (!(gameId in SOCKETS)) {
|
||||
|
|
@ -1638,8 +1705,8 @@ function addSocket(gameId, socket) {
|
|||
}
|
||||
if (!SOCKETS[gameId].includes(socket)) {
|
||||
SOCKETS[gameId].push(socket);
|
||||
log$1.log('added socket: ', gameId, socket.protocol);
|
||||
log$1.log('socket count: ', Object.keys(SOCKETS[gameId]).length);
|
||||
log$2.log('added socket: ', gameId, socket.protocol);
|
||||
log$2.log('socket count: ', Object.keys(SOCKETS[gameId]).length);
|
||||
}
|
||||
}
|
||||
function getSockets(gameId) {
|
||||
|
|
@ -1655,6 +1722,155 @@ var GameSockets = {
|
|||
getSockets,
|
||||
};
|
||||
|
||||
const log$1 = logger('Db.ts');
|
||||
class Db {
|
||||
constructor(file, patchesDir) {
|
||||
this.file = file;
|
||||
this.patchesDir = patchesDir;
|
||||
this.dbh = bsqlite(this.file);
|
||||
}
|
||||
close() {
|
||||
this.dbh.close();
|
||||
}
|
||||
patch(verbose = true) {
|
||||
if (!this.get('sqlite_master', { type: 'table', name: 'db_patches' })) {
|
||||
this.run('CREATE TABLE db_patches ( id TEXT PRIMARY KEY);', []);
|
||||
}
|
||||
const files = fs.readdirSync(this.patchesDir);
|
||||
const patches = (this.getMany('db_patches')).map(row => row.id);
|
||||
for (const f of files) {
|
||||
if (patches.includes(f)) {
|
||||
if (verbose) {
|
||||
log$1.info(`➡ skipping already applied db patch: ${f}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const contents = fs.readFileSync(`${this.patchesDir}/${f}`, 'utf-8');
|
||||
const all = contents.split(';').map(s => s.trim()).filter(s => !!s);
|
||||
try {
|
||||
this.dbh.transaction((all) => {
|
||||
for (const q of all) {
|
||||
if (verbose) {
|
||||
log$1.info(`Running: ${q}`);
|
||||
}
|
||||
this.run(q);
|
||||
}
|
||||
this.insert('db_patches', { id: f });
|
||||
})(all);
|
||||
log$1.info(`✓ applied db patch: ${f}`);
|
||||
}
|
||||
catch (e) {
|
||||
log$1.error(`✖ unable to apply patch: ${f} ${e}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
_buildWhere(where) {
|
||||
const wheres = [];
|
||||
const values = [];
|
||||
for (const k of Object.keys(where)) {
|
||||
if (where[k] === null) {
|
||||
wheres.push(k + ' IS NULL');
|
||||
continue;
|
||||
}
|
||||
if (typeof where[k] === 'object') {
|
||||
let prop = '$nin';
|
||||
if (where[k][prop]) {
|
||||
if (where[k][prop].length > 0) {
|
||||
wheres.push(k + ' NOT IN (' + where[k][prop].map((_) => '?') + ')');
|
||||
values.push(...where[k][prop]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
prop = '$in';
|
||||
if (where[k][prop]) {
|
||||
if (where[k][prop].length > 0) {
|
||||
wheres.push(k + ' IN (' + where[k][prop].map((_) => '?') + ')');
|
||||
values.push(...where[k][prop]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// TODO: implement rest of mongo like query args ($eq, $lte, $in...)
|
||||
throw new Error('not implemented: ' + JSON.stringify(where[k]));
|
||||
}
|
||||
wheres.push(k + ' = ?');
|
||||
values.push(where[k]);
|
||||
}
|
||||
return {
|
||||
sql: wheres.length > 0 ? ' WHERE ' + wheres.join(' AND ') : '',
|
||||
values,
|
||||
};
|
||||
}
|
||||
_buildOrderBy(orderBy) {
|
||||
const sorts = [];
|
||||
for (const s of orderBy) {
|
||||
const k = Object.keys(s)[0];
|
||||
sorts.push(k + ' COLLATE NOCASE ' + (s[k] > 0 ? 'ASC' : 'DESC'));
|
||||
}
|
||||
return sorts.length > 0 ? ' ORDER BY ' + sorts.join(', ') : '';
|
||||
}
|
||||
_get(query, params = []) {
|
||||
return this.dbh.prepare(query).get(...params);
|
||||
}
|
||||
run(query, params = []) {
|
||||
return this.dbh.prepare(query).run(...params);
|
||||
}
|
||||
_getMany(query, params = []) {
|
||||
return this.dbh.prepare(query).all(...params);
|
||||
}
|
||||
get(table, whereRaw = {}, orderBy = []) {
|
||||
const where = this._buildWhere(whereRaw);
|
||||
const orderBySql = this._buildOrderBy(orderBy);
|
||||
const sql = 'SELECT * FROM ' + table + where.sql + orderBySql;
|
||||
return this._get(sql, where.values);
|
||||
}
|
||||
getMany(table, whereRaw = {}, orderBy = []) {
|
||||
const where = this._buildWhere(whereRaw);
|
||||
const orderBySql = this._buildOrderBy(orderBy);
|
||||
const sql = 'SELECT * FROM ' + table + where.sql + orderBySql;
|
||||
return this._getMany(sql, where.values);
|
||||
}
|
||||
delete(table, whereRaw = {}) {
|
||||
const where = this._buildWhere(whereRaw);
|
||||
const sql = 'DELETE FROM ' + table + where.sql;
|
||||
return this.run(sql, where.values);
|
||||
}
|
||||
exists(table, whereRaw) {
|
||||
return !!this.get(table, whereRaw);
|
||||
}
|
||||
upsert(table, data, check, idcol = null) {
|
||||
if (!this.exists(table, check)) {
|
||||
return this.insert(table, data);
|
||||
}
|
||||
this.update(table, data, check);
|
||||
if (idcol === null) {
|
||||
return 0; // dont care about id
|
||||
}
|
||||
return this.get(table, check)[idcol]; // get id manually
|
||||
}
|
||||
insert(table, data) {
|
||||
const keys = Object.keys(data);
|
||||
const values = keys.map(k => data[k]);
|
||||
const sql = 'INSERT INTO ' + table
|
||||
+ ' (' + keys.join(',') + ')'
|
||||
+ ' VALUES (' + keys.map(k => '?').join(',') + ')';
|
||||
return this.run(sql, values).lastInsertRowid;
|
||||
}
|
||||
update(table, data, whereRaw = {}) {
|
||||
const keys = Object.keys(data);
|
||||
if (keys.length === 0) {
|
||||
return;
|
||||
}
|
||||
const values = keys.map(k => data[k]);
|
||||
const setSql = ' SET ' + keys.join(' = ?,') + ' = ?';
|
||||
const where = this._buildWhere(whereRaw);
|
||||
const sql = 'UPDATE ' + table + setSql + where.sql;
|
||||
this.run(sql, [...values, ...where.values]);
|
||||
}
|
||||
}
|
||||
|
||||
const db = new Db(DB_FILE, DB_PATCHES_DIR);
|
||||
db.patch();
|
||||
let configFile = '';
|
||||
let last = '';
|
||||
for (const val of process.argv) {
|
||||
|
|
@ -1686,8 +1902,8 @@ app.get('/api/conf', (req, res) => {
|
|||
app.get('/api/newgame-data', (req, res) => {
|
||||
const q = req.query;
|
||||
res.send({
|
||||
images: Images.allImages(q.sort),
|
||||
categories: [],
|
||||
images: Images.allImagesFromDb(db, q.category, q.sort),
|
||||
categories: db.getMany('categories', {}, [{ title: 1 }]),
|
||||
});
|
||||
});
|
||||
app.get('/api/index-data', (req, res) => {
|
||||
|
|
@ -1722,12 +1938,24 @@ app.post('/upload', (req, res) => {
|
|||
log.log(err);
|
||||
res.status(400).send("Something went wrong!");
|
||||
}
|
||||
res.send({
|
||||
image: {
|
||||
file: `${UPLOAD_DIR}/${req.file.filename}`,
|
||||
url: `${UPLOAD_URL}/${req.file.filename}`,
|
||||
},
|
||||
const imageId = db.insert('images', {
|
||||
filename: req.file.filename,
|
||||
filename_original: req.file.originalname,
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
res.send(Images.imageFromDb(db, imageId));
|
||||
});
|
||||
});
|
||||
app.post('/newgame', bodyParser.json(), async (req, res) => {
|
||||
|
|
|
|||
232
package-lock.json
generated
232
package-lock.json
generated
|
|
@ -5,6 +5,7 @@
|
|||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^7.4.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"exif": "^0.6.0",
|
||||
"express": "^4.17.1",
|
||||
|
|
@ -16,6 +17,7 @@
|
|||
"ws": "^7.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^5.4.1",
|
||||
"@types/exif": "^0.6.2",
|
||||
"@types/express": "^4.17.11",
|
||||
"@types/multer": "^1.4.5",
|
||||
|
|
@ -1482,6 +1484,15 @@
|
|||
"@babel/types": "^7.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/better-sqlite3": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-5.4.1.tgz",
|
||||
"integrity": "sha512-8hje3Rhsg/9veTkALfCwiWn7VMrP1QDwHhBSgerttYPABEvrHsMQnU9dlqoM6QX3x4uw3Y06dDVz8uDQo1J4Ng==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/integer": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
|
||||
|
|
@ -1548,6 +1559,12 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/integer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/integer/-/integer-4.0.0.tgz",
|
||||
"integrity": "sha512-2U1i6bIRiqizl6O+ETkp2HhUZIxg7g+burUabh9tzGd0qcszfNaFRaY9bGNlQKgEU7DCsH5qMajRDW5QamWQbw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
|
||||
|
|
@ -2509,6 +2526,17 @@
|
|||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-7.4.0.tgz",
|
||||
"integrity": "sha512-hXwwaFvtYwRfjBSGP6+woB95qbwSnfpXyy/kDFzgOMoDttzyaWsBGcU3FGuRbzhbRv0qpKRCJQ6Hru2pQ8adxg==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^6.0.1",
|
||||
"tar": "^6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/big.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
||||
|
|
@ -2519,6 +2547,14 @@
|
|||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"dependencies": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
|
|
@ -4180,6 +4216,11 @@
|
|||
"bser": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
|
|
@ -4331,6 +4372,17 @@
|
|||
"node": ">=6 <7 || >=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
|
|
@ -7372,6 +7424,39 @@
|
|||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz",
|
||||
"integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/mixin-deep": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
|
||||
|
|
@ -9982,6 +10067,22 @@
|
|||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz",
|
||||
"integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==",
|
||||
"dependencies": {
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
"minipass": "^3.0.0",
|
||||
"minizlib": "^2.1.1",
|
||||
"mkdirp": "^1.0.3",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
|
||||
|
|
@ -10048,6 +10149,30 @@
|
|||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/terminal-link": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz",
|
||||
|
|
@ -12107,6 +12232,15 @@
|
|||
"@babel/types": "^7.3.0"
|
||||
}
|
||||
},
|
||||
"@types/better-sqlite3": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-5.4.1.tgz",
|
||||
"integrity": "sha512-8hje3Rhsg/9veTkALfCwiWn7VMrP1QDwHhBSgerttYPABEvrHsMQnU9dlqoM6QX3x4uw3Y06dDVz8uDQo1J4Ng==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/integer": "*"
|
||||
}
|
||||
},
|
||||
"@types/body-parser": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
|
||||
|
|
@ -12173,6 +12307,12 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/integer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/integer/-/integer-4.0.0.tgz",
|
||||
"integrity": "sha512-2U1i6bIRiqizl6O+ETkp2HhUZIxg7g+burUabh9tzGd0qcszfNaFRaY9bGNlQKgEU7DCsH5qMajRDW5QamWQbw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
|
||||
|
|
@ -12971,6 +13111,16 @@
|
|||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"better-sqlite3": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-7.4.0.tgz",
|
||||
"integrity": "sha512-hXwwaFvtYwRfjBSGP6+woB95qbwSnfpXyy/kDFzgOMoDttzyaWsBGcU3FGuRbzhbRv0qpKRCJQ6Hru2pQ8adxg==",
|
||||
"requires": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^6.0.1",
|
||||
"tar": "^6.1.0"
|
||||
}
|
||||
},
|
||||
"big.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
||||
|
|
@ -12978,6 +13128,14 @@
|
|||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"requires": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
|
|
@ -14304,6 +14462,11 @@
|
|||
"bser": "2.1.1"
|
||||
}
|
||||
},
|
||||
"file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
|
|
@ -14421,6 +14584,14 @@
|
|||
"universalify": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"fs-minipass": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||
"requires": {
|
||||
"minipass": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
|
|
@ -16743,6 +16914,37 @@
|
|||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
},
|
||||
"minipass": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz",
|
||||
"integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==",
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"minizlib": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||
"requires": {
|
||||
"minipass": "^3.0.0",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"mixin-deep": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
|
||||
|
|
@ -18801,6 +19003,36 @@
|
|||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"dev": true
|
||||
},
|
||||
"tar": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz",
|
||||
"integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==",
|
||||
"requires": {
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
"minipass": "^3.0.0",
|
||||
"minizlib": "^2.1.1",
|
||||
"mkdirp": "^1.0.3",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"tar-fs": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^7.4.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"exif": "^0.6.0",
|
||||
"express": "^4.17.1",
|
||||
|
|
@ -12,6 +13,7 @@
|
|||
"ws": "^7.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^5.4.1",
|
||||
"@types/exif": "^0.6.2",
|
||||
"@types/express": "^4.17.11",
|
||||
"@types/multer": "^1.4.5",
|
||||
|
|
@ -29,6 +31,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"rollup": "rollup",
|
||||
"vite": "vite"
|
||||
"vite": "vite",
|
||||
"ts-node": "node --experimental-specifier-resolution=node --loader ts-node/esm"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,5 @@ export default {
|
|||
"url",
|
||||
"path",
|
||||
],
|
||||
plugins: [typescript({
|
||||
"tsconfig": "tsconfig.server.json"
|
||||
})],
|
||||
plugins: [typescript()],
|
||||
};
|
||||
|
|
|
|||
23
scripts/import_images.ts
Normal file
23
scripts/import_images.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { DB_FILE, DB_PATCHES_DIR } from '../src/server/Dirs'
|
||||
import Db from '../src/server/Db'
|
||||
import Images from '../src/server/Images'
|
||||
|
||||
const db = new Db(DB_FILE, DB_PATCHES_DIR)
|
||||
db.patch(true)
|
||||
|
||||
const cat = ''
|
||||
const sort = 'date_desc'
|
||||
let images = Images.allImagesFromDisk(cat, sort)
|
||||
images.forEach((image: any) => {
|
||||
db.upsert('images', {
|
||||
filename: image.filename,
|
||||
filename_original: image.filename,
|
||||
title: image.title,
|
||||
created: image.created / 1000,
|
||||
}, {
|
||||
filename: image.filename
|
||||
})
|
||||
})
|
||||
|
||||
images = Images.allImagesFromDb(db, cat, sort)
|
||||
console.log(images)
|
||||
|
|
@ -1,5 +1,11 @@
|
|||
#!/bin/sh
|
||||
|
||||
# TODO: add switch via param
|
||||
|
||||
# server for built files
|
||||
cd "$RUN_DIR/build/server"
|
||||
|
||||
nodemon --max-old-space-size=64 main.js -c ../../config.json
|
||||
|
||||
# dev server
|
||||
# npm run ts-node src/server/main.ts -- -c config.json
|
||||
|
|
|
|||
|
|
@ -8,8 +8,11 @@ export type EncodedPlayer = Array<any>
|
|||
export type EncodedPiece = Array<any>
|
||||
export type EncodedPieceShape = number
|
||||
|
||||
// TODO: maybe something other than string in the future
|
||||
export type Category = string
|
||||
export interface Category {
|
||||
id: number
|
||||
slug: string
|
||||
title: string
|
||||
}
|
||||
|
||||
interface GameRng {
|
||||
obj: Rng
|
||||
|
|
@ -26,10 +29,13 @@ interface Game {
|
|||
}
|
||||
|
||||
export interface Image {
|
||||
id: number
|
||||
filename: string
|
||||
file: string
|
||||
url: string
|
||||
category: Category
|
||||
title: string
|
||||
categories: Array<Category>
|
||||
created: number
|
||||
}
|
||||
|
||||
export interface GameSettings {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@ import { EncodedPiece, EncodedPieceShape, EncodedPlayer, Piece, PieceShape, Play
|
|||
import { Point } from './Geometry'
|
||||
import { Rng } from './Rng'
|
||||
|
||||
const slug = (str: string) => {
|
||||
let tmp = str.toLowerCase()
|
||||
tmp = tmp.replace(/[^a-z0-9]+/g, '-')
|
||||
tmp = tmp.replace(/^-|-$/, '')
|
||||
return tmp
|
||||
}
|
||||
|
||||
const pad = (x: any, pad: string) => {
|
||||
const str = `${x}`
|
||||
|
|
@ -162,6 +168,7 @@ function asQueryArgs(data: any) {
|
|||
|
||||
export default {
|
||||
hash,
|
||||
slug,
|
||||
uniqId,
|
||||
|
||||
encodeShape,
|
||||
|
|
|
|||
24
src/dbpatches/01_initial.sqlite
Normal file
24
src/dbpatches/01_initial.sqlite
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
CREATE TABLE categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
slug TEXT UNIQUE,
|
||||
title TEXT UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE images (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
created TIMESTAMP NOT NULL,
|
||||
|
||||
filename TEXT NOT NULL UNIQUE,
|
||||
filename_original TEXT NOT NULL,
|
||||
title TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE image_x_category (
|
||||
image_id INTEGER NOT NULL,
|
||||
category_id INTEGER NOT NULL,
|
||||
|
||||
FOREIGN KEY(image_id) REFERENCES images(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(category_id) REFERENCES categories(id) ON DELETE CASCADE
|
||||
);
|
||||
|
|
@ -106,6 +106,7 @@ export default defineComponent({
|
|||
}
|
||||
.new-game-dialog .area-image {
|
||||
grid-area: image;
|
||||
margin: 20px;
|
||||
}
|
||||
.new-game-dialog .area-settings {
|
||||
grid-area: settings;
|
||||
|
|
|
|||
|
|
@ -7,15 +7,21 @@ gallery", if possible!
|
|||
<div class="overlay new-image-dialog" @click="$emit('bgclick')">
|
||||
<div class="overlay-content" @click.stop="">
|
||||
|
||||
<div class="area-image" :class="{'has-image': !!image.url, 'no-image': !image.url}">
|
||||
<div class="area-image" :class="{'has-image': !!previewUrl, 'no-image': !previewUrl}">
|
||||
<!-- TODO: ... -->
|
||||
<div v-if="image.url" class="has-image">
|
||||
<span class="remove btn" @click="image.url=''">X</span>
|
||||
<responsive-image :src="image.url" />
|
||||
<div v-if="previewUrl" class="has-image">
|
||||
<span class="remove btn" @click="previewUrl=''">X</span>
|
||||
<responsive-image :src="previewUrl" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<label class="upload">
|
||||
<input type="file" style="display: none" @change="preview" accept="image/*" />
|
||||
<span class="btn">{{label || 'Upload File'}}</span>
|
||||
</label>
|
||||
|
||||
|
||||
<!-- TODO: drop area for image -->
|
||||
<upload class="upload" @uploaded="mediaImgUploaded($event)" accept="image/*" label="Upload an image" />
|
||||
<!-- <upload class="upload" @uploaded="mediaImgUploaded($event)" accept="image/*" label="Upload an image" /> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -23,7 +29,7 @@ gallery", if possible!
|
|||
<table>
|
||||
<tr>
|
||||
<td><label>Title</label></td>
|
||||
<td><input type="text" v-model="image.title" placeholder="Flower by @artist" /></td>
|
||||
<td><input type="text" v-model="title" placeholder="Flower by @artist" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
|
|
@ -33,13 +39,13 @@ gallery", if possible!
|
|||
<tr>
|
||||
<!-- TODO: autocomplete category -->
|
||||
<td><label>Category</label></td>
|
||||
<td><input type="text" v-model="image.category" placeholder="Plants" /></td>
|
||||
<td><input type="text" v-model="category" placeholder="Plants" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="area-buttons">
|
||||
<!-- <button class="btn" :disabled="!canPostToGallery" @click="postToGallery">🖼️ Post to gallery</button> -->
|
||||
<button class="btn" :disabled="!canPostToGallery" @click="postToGallery">🖼️ Post to gallery</button>
|
||||
<button class="btn" :disabled="!canSetupGameClick" @click="setupGameClick">🧩 Post to gallery <br /> + set up game</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -51,7 +57,6 @@ import { defineComponent } from 'vue'
|
|||
|
||||
import Upload from './Upload.vue'
|
||||
import ResponsiveImage from './ResponsiveImage.vue'
|
||||
import { Image } from '../../common/GameCommon'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'new-image-dialog',
|
||||
|
|
@ -62,35 +67,51 @@ export default defineComponent({
|
|||
emits: {
|
||||
bgclick: null,
|
||||
setupGameClick: null,
|
||||
postToGalleryClick: null,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
image: {
|
||||
file: '',
|
||||
url: '',
|
||||
title: '',
|
||||
category: '',
|
||||
} as Image,
|
||||
previewUrl: '',
|
||||
file: null as File|null,
|
||||
title: '',
|
||||
category: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canPostToGallery () {
|
||||
return !!this.image.url
|
||||
canPostToGallery (): boolean {
|
||||
return !!(this.previewUrl && this.file)
|
||||
},
|
||||
canSetupGameClick () {
|
||||
return !!this.image.url
|
||||
canSetupGameClick (): boolean {
|
||||
return !!(this.previewUrl && this.file)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
mediaImgUploaded(data: any) {
|
||||
this.image.file = data.image.file
|
||||
this.image.url = data.image.url
|
||||
preview (evt: Event) {
|
||||
const target = (evt.target as HTMLInputElement)
|
||||
if (!target.files) return;
|
||||
const file = target.files[0]
|
||||
if (!file) return;
|
||||
|
||||
const r = new FileReader()
|
||||
r.readAsDataURL(file)
|
||||
r.onload = (ev: any) => {
|
||||
this.previewUrl = ev.target.result
|
||||
this.file = file
|
||||
}
|
||||
},
|
||||
postToGallery () {
|
||||
this.$emit('postToGallery', this.image)
|
||||
this.$emit('postToGalleryClick', {
|
||||
file: this.file,
|
||||
title: this.title,
|
||||
category: this.category,
|
||||
})
|
||||
},
|
||||
setupGameClick () {
|
||||
this.$emit('setupGameClick', this.image)
|
||||
this.$emit('setupGameClick', {
|
||||
file: this.file,
|
||||
title: this.title,
|
||||
category: this.category,
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -111,12 +132,12 @@ export default defineComponent({
|
|||
|
||||
.new-image-dialog .area-image {
|
||||
grid-area: image;
|
||||
margin: 20px;
|
||||
}
|
||||
.new-image-dialog .area-image.no-image {
|
||||
align-content: center;
|
||||
display: grid;
|
||||
text-align: center;
|
||||
margin: 20px;
|
||||
border: dashed 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
|
@ -146,6 +167,7 @@ export default defineComponent({
|
|||
}
|
||||
.new-image-dialog .area-buttons button {
|
||||
width: 100%;
|
||||
margin-top: .5em;
|
||||
}
|
||||
.new-image-dialog .upload {
|
||||
position: absolute;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ in jigsawpuzzles.io
|
|||
Category:
|
||||
<select v-model="filters.category" @change="filtersChanged">
|
||||
<option value="">All</option>
|
||||
<option v-for="(c, idx) in categories" :key="idx" :value="c">{{c}}</option>
|
||||
<option v-for="(c, idx) in categories" :key="idx" :value="c.slug">{{c.title}}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
|
|
@ -30,8 +30,8 @@ in jigsawpuzzles.io
|
|||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<image-library :images="images" :categories="categories" @imageClicked="imageClicked" />
|
||||
<new-image-dialog v-if="dialog==='new-image'" @bgclick="dialog=''" @setupGameClick="setupGameClick" />
|
||||
<image-library :images="images" @imageClicked="imageClicked" />
|
||||
<new-image-dialog v-if="dialog==='new-image'" @bgclick="dialog=''" @postToGalleryClick="postToGalleryClick" @setupGameClick="setupGameClick" />
|
||||
<new-game-dialog v-if="image && dialog==='new-game'" @bgclick="dialog=''" @newGame="onNewGame" :image="image" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -61,10 +61,13 @@ export default defineComponent({
|
|||
categories: [],
|
||||
|
||||
image: {
|
||||
url: '',
|
||||
id: 0,
|
||||
filename: '',
|
||||
file: '',
|
||||
url: '',
|
||||
title: '',
|
||||
category: '',
|
||||
categories: [],
|
||||
created: 0,
|
||||
} as Image,
|
||||
|
||||
dialog: '',
|
||||
|
|
@ -87,7 +90,26 @@ export default defineComponent({
|
|||
this.image = image
|
||||
this.dialog = 'new-game'
|
||||
},
|
||||
setupGameClick (image: Image) {
|
||||
async uploadImage (data: any) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', data.file, data.file.name);
|
||||
formData.append('title', data.title)
|
||||
formData.append('category', data.category)
|
||||
|
||||
const res = await fetch('/upload', {
|
||||
method: 'post',
|
||||
body: formData,
|
||||
})
|
||||
return await res.json()
|
||||
},
|
||||
async postToGalleryClick(data: any) {
|
||||
await this.uploadImage(data)
|
||||
this.dialog = ''
|
||||
await this.loadImages()
|
||||
},
|
||||
async setupGameClick (data: any) {
|
||||
const image = await this.uploadImage(data)
|
||||
this.loadImages() // load images in background
|
||||
this.image = image
|
||||
this.dialog = 'new-game'
|
||||
},
|
||||
|
|
|
|||
212
src/server/Db.ts
Normal file
212
src/server/Db.ts
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
import fs from 'fs'
|
||||
import bsqlite from 'better-sqlite3'
|
||||
import Integer from 'integer'
|
||||
import { logger } from '../common/Util'
|
||||
|
||||
const log = logger('Db.ts')
|
||||
|
||||
/**
|
||||
* TODO: create a more specific type for OrderBy.
|
||||
* It looks like this (example):
|
||||
* [
|
||||
* {id: -1}, // by id descending
|
||||
* {name: 1}, // then by name ascending
|
||||
* ]
|
||||
*/
|
||||
type OrderBy = Array<any>
|
||||
type Data = Record<string, any>
|
||||
type WhereRaw = Record<string, any>
|
||||
type Params = Array<any>
|
||||
|
||||
interface Where {
|
||||
sql: string
|
||||
values: Array<any>
|
||||
}
|
||||
|
||||
class Db {
|
||||
file: string
|
||||
patchesDir: string
|
||||
dbh: bsqlite.Database
|
||||
|
||||
constructor(file: string, patchesDir: string) {
|
||||
this.file = file
|
||||
this.patchesDir = patchesDir
|
||||
this.dbh = bsqlite(this.file)
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.dbh.close()
|
||||
}
|
||||
|
||||
patch (verbose: boolean =true): void {
|
||||
if (!this.get('sqlite_master', {type: 'table', name: 'db_patches'})) {
|
||||
this.run('CREATE TABLE db_patches ( id TEXT PRIMARY KEY);', [])
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(this.patchesDir)
|
||||
const patches = (this.getMany('db_patches')).map(row => row.id)
|
||||
|
||||
for (const f of files) {
|
||||
if (patches.includes(f)) {
|
||||
if (verbose) {
|
||||
log.info(`➡ skipping already applied db patch: ${f}`)
|
||||
}
|
||||
continue
|
||||
}
|
||||
const contents = fs.readFileSync(`${this.patchesDir}/${f}`, 'utf-8')
|
||||
|
||||
const all = contents.split(';').map(s => s.trim()).filter(s => !!s)
|
||||
try {
|
||||
this.dbh.transaction((all) => {
|
||||
for (const q of all) {
|
||||
if (verbose) {
|
||||
log.info(`Running: ${q}`)
|
||||
}
|
||||
this.run(q)
|
||||
}
|
||||
this.insert('db_patches', {id: f})
|
||||
})(all)
|
||||
|
||||
log.info(`✓ applied db patch: ${f}`)
|
||||
} catch (e) {
|
||||
log.error(`✖ unable to apply patch: ${f} ${e}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_buildWhere (where: WhereRaw): Where {
|
||||
const wheres = []
|
||||
const values = []
|
||||
for (const k of Object.keys(where)) {
|
||||
if (where[k] === null) {
|
||||
wheres.push(k + ' IS NULL')
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof where[k] === 'object') {
|
||||
let prop = '$nin'
|
||||
if (where[k][prop]) {
|
||||
if (where[k][prop].length > 0) {
|
||||
wheres.push(k + ' NOT IN (' + where[k][prop].map((_: any) => '?') + ')')
|
||||
values.push(...where[k][prop])
|
||||
}
|
||||
continue
|
||||
}
|
||||
prop = '$in'
|
||||
if (where[k][prop]) {
|
||||
if (where[k][prop].length > 0) {
|
||||
wheres.push(k + ' IN (' + where[k][prop].map((_: any) => '?') + ')')
|
||||
values.push(...where[k][prop])
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: implement rest of mongo like query args ($eq, $lte, $in...)
|
||||
throw new Error('not implemented: ' + JSON.stringify(where[k]))
|
||||
}
|
||||
|
||||
wheres.push(k + ' = ?')
|
||||
values.push(where[k])
|
||||
}
|
||||
|
||||
return {
|
||||
sql: wheres.length > 0 ? ' WHERE ' + wheres.join(' AND ') : '',
|
||||
values,
|
||||
}
|
||||
}
|
||||
|
||||
_buildOrderBy (orderBy: OrderBy): string {
|
||||
const sorts = []
|
||||
for (const s of orderBy) {
|
||||
const k = Object.keys(s)[0]
|
||||
sorts.push(k + ' COLLATE NOCASE ' + (s[k] > 0 ? 'ASC' : 'DESC'))
|
||||
}
|
||||
return sorts.length > 0 ? ' ORDER BY ' + sorts.join(', ') : ''
|
||||
}
|
||||
|
||||
_get (query: string, params: Params = []): any {
|
||||
return this.dbh.prepare(query).get(...params)
|
||||
}
|
||||
|
||||
run (query: string, params: Params = []): bsqlite.RunResult {
|
||||
return this.dbh.prepare(query).run(...params)
|
||||
}
|
||||
|
||||
_getMany (query: string, params: Params = []): Array<any> {
|
||||
return this.dbh.prepare(query).all(...params)
|
||||
}
|
||||
|
||||
get (
|
||||
table: string,
|
||||
whereRaw: WhereRaw = {},
|
||||
orderBy: OrderBy = []
|
||||
): any {
|
||||
const where = this._buildWhere(whereRaw)
|
||||
const orderBySql = this._buildOrderBy(orderBy)
|
||||
const sql = 'SELECT * FROM ' + table + where.sql + orderBySql
|
||||
return this._get(sql, where.values)
|
||||
}
|
||||
|
||||
getMany (
|
||||
table: string,
|
||||
whereRaw: WhereRaw = {},
|
||||
orderBy: OrderBy = []
|
||||
): Array<any> {
|
||||
const where = this._buildWhere(whereRaw)
|
||||
const orderBySql = this._buildOrderBy(orderBy)
|
||||
const sql = 'SELECT * FROM ' + table + where.sql + orderBySql
|
||||
return this._getMany(sql, where.values)
|
||||
}
|
||||
|
||||
delete (table: string, whereRaw: WhereRaw = {}): bsqlite.RunResult {
|
||||
const where = this._buildWhere(whereRaw)
|
||||
const sql = 'DELETE FROM ' + table + where.sql
|
||||
return this.run(sql, where.values)
|
||||
}
|
||||
|
||||
exists (table: string, whereRaw: WhereRaw): boolean {
|
||||
return !!this.get(table, whereRaw)
|
||||
}
|
||||
|
||||
upsert (
|
||||
table: string,
|
||||
data: Data,
|
||||
check: WhereRaw,
|
||||
idcol: string|null = null
|
||||
): any {
|
||||
if (!this.exists(table, check)) {
|
||||
return this.insert(table, data)
|
||||
}
|
||||
this.update(table, data, check)
|
||||
if (idcol === null) {
|
||||
return 0 // dont care about id
|
||||
}
|
||||
|
||||
return this.get(table, check)[idcol] // get id manually
|
||||
}
|
||||
|
||||
insert (table: string, data: Data): Integer.IntLike {
|
||||
const keys = Object.keys(data)
|
||||
const values = keys.map(k => data[k])
|
||||
const sql = 'INSERT INTO '+ table
|
||||
+ ' (' + keys.join(',') + ')'
|
||||
+ ' VALUES (' + keys.map(k => '?').join(',') + ')'
|
||||
return this.run(sql, values).lastInsertRowid
|
||||
}
|
||||
|
||||
update (table: string, data: Data, whereRaw: WhereRaw = {}): void {
|
||||
const keys = Object.keys(data)
|
||||
if (keys.length === 0) {
|
||||
return
|
||||
}
|
||||
const values = keys.map(k => data[k])
|
||||
const setSql = ' SET ' + keys.join(' = ?,') + ' = ?'
|
||||
const where = this._buildWhere(whereRaw)
|
||||
|
||||
const sql = 'UPDATE ' + table + setSql + where.sql
|
||||
this.run(sql, [...values, ...where.values])
|
||||
}
|
||||
}
|
||||
|
||||
export default Db
|
||||
|
|
@ -10,3 +10,6 @@ export const DATA_DIR = `${BASE_DIR}/data`
|
|||
export const UPLOAD_DIR = `${BASE_DIR}/data/uploads`
|
||||
export const UPLOAD_URL = `/uploads`
|
||||
export const PUBLIC_DIR = `${BASE_DIR}/build/public/`
|
||||
|
||||
export const DB_PATCHES_DIR = `${BASE_DIR}/src/dbpatches`
|
||||
export const DB_FILE = `${BASE_DIR}/data/db.sqlite`
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import exif from 'exif'
|
|||
import sharp from 'sharp'
|
||||
|
||||
import {UPLOAD_DIR, UPLOAD_URL} from './Dirs'
|
||||
import Db from './Db'
|
||||
|
||||
const resizeImage = async (filename: string) => {
|
||||
if (!filename.toLowerCase().match(/\.(jpe?g|webp|png)$/)) {
|
||||
|
|
@ -46,16 +47,76 @@ async function getExifOrientation(imagePath: string) {
|
|||
})
|
||||
}
|
||||
|
||||
const allImages = (sort: string) => {
|
||||
const getCategories = (db: Db, imageId: number) => {
|
||||
const query = `
|
||||
select * from categories c
|
||||
inner join image_x_category ixc on c.id = ixc.category_id
|
||||
where ixc.image_id = ?`
|
||||
return db._getMany(query, [imageId])
|
||||
}
|
||||
|
||||
const imageFromDb = (db: Db, imageId: number) => {
|
||||
const i = db.get('images', { id: imageId })
|
||||
return {
|
||||
id: i.id,
|
||||
filename: i.filename,
|
||||
file: `${UPLOAD_DIR}/${i.filename}`,
|
||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||
title: i.title,
|
||||
categories: getCategories(db, i.id) as any[],
|
||||
created: i.created * 1000,
|
||||
}
|
||||
}
|
||||
|
||||
const allImagesFromDb = (db: Db, categorySlug: string, sort: string) => {
|
||||
const sortMap = {
|
||||
alpha_asc: [{filename: 1}],
|
||||
alpha_desc: [{filename: -1}],
|
||||
date_asc: [{created: 1}],
|
||||
date_desc: [{created: -1}],
|
||||
} as Record<string, any>
|
||||
|
||||
// TODO: .... clean up
|
||||
const wheresRaw: Record<string, any> = {}
|
||||
if (categorySlug !== '') {
|
||||
const c = db.get('categories', {slug: categorySlug})
|
||||
if (!c) {
|
||||
return []
|
||||
}
|
||||
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)
|
||||
if (ids.length === 0) {
|
||||
return []
|
||||
}
|
||||
wheresRaw['id'] = {'$in': ids}
|
||||
}
|
||||
const images = db.getMany('images', wheresRaw, sortMap[sort])
|
||||
|
||||
return images.map(i => ({
|
||||
id: i.id as number,
|
||||
filename: i.filename,
|
||||
file: `${UPLOAD_DIR}/${i.filename}`,
|
||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||
title: i.title,
|
||||
categories: getCategories(db, i.id) as any[],
|
||||
created: i.created * 1000,
|
||||
}))
|
||||
}
|
||||
|
||||
const allImagesFromDisk = (category: string, sort: string) => {
|
||||
let images = fs.readdirSync(UPLOAD_DIR)
|
||||
.filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/))
|
||||
.map(f => ({
|
||||
id: 0,
|
||||
filename: f,
|
||||
file: `${UPLOAD_DIR}/${f}`,
|
||||
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
|
||||
title: '',
|
||||
category: '',
|
||||
ts: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(),
|
||||
title: f.replace(/\.[a-z]+$/, ''),
|
||||
categories: [] as any[],
|
||||
created: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(),
|
||||
}))
|
||||
|
||||
switch (sort) {
|
||||
|
|
@ -73,14 +134,14 @@ const allImages = (sort: string) => {
|
|||
|
||||
case 'date_asc':
|
||||
images = images.sort((a, b) => {
|
||||
return a.ts > b.ts ? 1 : -1
|
||||
return a.created > b.created ? 1 : -1
|
||||
})
|
||||
break;
|
||||
|
||||
case 'date_desc':
|
||||
default:
|
||||
images = images.sort((a, b) => {
|
||||
return a.ts < b.ts ? 1 : -1
|
||||
return a.created < b.created ? 1 : -1
|
||||
})
|
||||
break;
|
||||
}
|
||||
|
|
@ -102,7 +163,9 @@ async function getDimensions(imagePath: string) {
|
|||
}
|
||||
|
||||
export default {
|
||||
allImages,
|
||||
allImagesFromDisk,
|
||||
imageFromDb,
|
||||
allImagesFromDb,
|
||||
resizeImage,
|
||||
getDimensions,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,19 @@ import GameLog from './GameLog'
|
|||
import GameSockets from './GameSockets'
|
||||
import Time from './../common/Time'
|
||||
import Images from './Images'
|
||||
import { UPLOAD_DIR, UPLOAD_URL, PUBLIC_DIR } from './Dirs'
|
||||
import {
|
||||
DB_FILE,
|
||||
DB_PATCHES_DIR,
|
||||
PUBLIC_DIR,
|
||||
UPLOAD_DIR,
|
||||
UPLOAD_URL
|
||||
} from './Dirs'
|
||||
import { GameSettings, ScoreMode } from '../common/GameCommon'
|
||||
import GameStorage from './GameStorage'
|
||||
import Db from './Db'
|
||||
|
||||
const db = new Db(DB_FILE, DB_PATCHES_DIR)
|
||||
db.patch()
|
||||
|
||||
let configFile = ''
|
||||
let last = ''
|
||||
|
|
@ -54,8 +64,8 @@ app.get('/api/conf', (req, res) => {
|
|||
app.get('/api/newgame-data', (req, res) => {
|
||||
const q = req.query as any
|
||||
res.send({
|
||||
images: Images.allImages(q.sort),
|
||||
categories: [],
|
||||
images: Images.allImagesFromDb(db, q.category, q.sort),
|
||||
categories: db.getMany('categories', {}, [{ title: 1 }]),
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -94,12 +104,26 @@ app.post('/upload', (req, res) => {
|
|||
res.status(400).send("Something went wrong!");
|
||||
}
|
||||
|
||||
res.send({
|
||||
image: {
|
||||
file: `${UPLOAD_DIR}/${req.file.filename}`,
|
||||
url: `${UPLOAD_URL}/${req.file.filename}`,
|
||||
},
|
||||
const imageId = db.insert('images', {
|
||||
filename: req.file.filename,
|
||||
filename_original: req.file.originalname,
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
res.send(Images.imageFromDb(db, imageId as number))
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ export default vite.defineConfig({
|
|||
target: 'http://localhost:1337',
|
||||
secure: false,
|
||||
},
|
||||
'^/upload': {
|
||||
target: 'http://localhost:1337',
|
||||
secure: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue