diff --git a/Dockerfile b/Dockerfile index 5f63182..da79714 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,12 +22,16 @@ FROM alpine:3.17.2 as final ARG PUID=0 ARG PGID=0 ARG TITLE="" -ARG BASE_PATH="" +ARG BASE_PATH="/" ARG LOCAL_IMAGES_CLEANUP_INTERVAL=1440 ENV VITE_TITLE $TITLE ENV BASE_PATH $BASE_PATH ENV LOCAL_IMAGES_CLEANUP_INTERVAL=$LOCAL_IMAGES_CLEANUP_INTERVAL -ENV NODE_ENV prod +ENV CONFIG_DIR="/config" +ENV TASKS_DIR="/tasks" +ENV PUID $PUID +ENV PGID $PGID +ENV PORT 8080 USER root RUN set -eux \ diff --git a/backend/package.json b/backend/package.json index 07ff1cb..79b6136 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,6 +11,6 @@ "uuid": "^9.0.0" }, "scripts": { - "start": "node server.js" + "start": "PORT='8080' CONFIG_DIR='config' TASKS_DIR='tasks' BASE_PATH='/' PUID='1000' PGID='1000' LOCAL_IMAGES_CLEANUP_INTERVAL='1440' node server.js" } } diff --git a/backend/server.js b/backend/server.js index 7dba682..436ef21 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,315 +1,437 @@ -const fs = require('fs'); -const uuid = require('uuid'); -const Koa = require('koa'); +const fs = require("fs"); +const uuid = require("uuid"); +const Koa = require("koa"); const app = new Koa(); -const router = require('@koa/router')(); -const bodyParser = require('koa-bodyparser'); -const cors = require('@koa/cors'); -const multer = require('@koa/multer'); -const send = require('koa-send') -const mount = require('koa-mount'); -const serve = require('koa-static'); -const PUID = Number(process.env.PUID !== undefined ? process.env.PUID : '1000'); -const PGID = Number(process.env.PGID !== undefined ? process.env.PGID : '1000'); -const BASE_PATH = process.env.BASE_PATH ? `${process.env.BASE_PATH}/` : '/'; -const TASKS_DIR = process.env.NODE_ENV === 'prod' ? '/tasks' : 'tasks'; -const CONFIG_DIR = process.env.NODE_ENV === 'prod' ? '/config' : 'config'; -const LOCAL_IMAGES_CLEANUP_INTERVAL = process.env.LOCAL_IMAGES_CLEANUP_INTERVAL === undefined - ? 1440 - : Number(process.env.LOCAL_IMAGES_CLEANUP_INTERVAL); +const router = require("@koa/router")(); +const bodyParser = require("koa-bodyparser"); +const cors = require("@koa/cors"); +const multer = require("@koa/multer"); +const send = require("koa-send"); +const mount = require("koa-mount"); +const serve = require("koa-static"); + +const PUID = Number(process.env.PUID); +const PGID = Number(process.env.PGID); +const BASE_PATH = + process.env.BASE_PATH.at(-1) === "/" + ? process.env.BASE_PATH + : `${process.env.BASE_PATH}/`; const multerInstance = multer(); function getContent(path) { - return fs.promises.readFile(path).then(res => res.toString()); + return fs.promises.readFile(path).then((res) => res.toString()); } async function getLanesNames() { - await fs.promises.mkdir(TASKS_DIR, { recursive: true }); - return fs.promises.readdir(TASKS_DIR); + await fs.promises.mkdir(process.env.TASKS_DIR, { recursive: true }); + return fs.promises.readdir(process.env.TASKS_DIR); } async function getTags(ctx) { - const lanes = await getLanesNames(); - const lanesFiles = await Promise.all(lanes.map(lane => fs.promises.readdir(`${TASKS_DIR}/${lane}`) - .then(files => files.map(file => ({ lane, name: file })))) - ); - const files = lanesFiles.flat(); - const filesContents = await Promise.all( - files.map(file => getContent(`${TASKS_DIR}/${file.lane}/${file.name}`)) - ); - const usedTagsTexts = filesContents - .map(content => getTagsTextsFromCardContent(content)) - .flat() - .sort((a, b) => a.localeCompare(b)); - const usedTagsTextsWithoutDuplicates = Array.from(new Set(usedTagsTexts.map(tagText => tagText.toLowerCase()))); - const allTags = await fs.promises.readFile(`${CONFIG_DIR}/tags.json`) - .then(res => JSON.parse(res.toString())) - .catch(err => []); - const usedTags = usedTagsTextsWithoutDuplicates - .map(tag => allTags.find(tagToFind => tagToFind.name.toLowerCase() === tag) || { name: tag, backgroundColor: 'var(--tag-color-1)' }) - await fs.promises.writeFile(`${CONFIG_DIR}/tags.json`, JSON.stringify(usedTags)); - ctx.status = 200; - ctx.body = usedTags; + const lanes = await getLanesNames(); + const lanesFiles = await Promise.all( + lanes.map((lane) => + fs.promises + .readdir(`${process.env.TASKS_DIR}/${lane}`) + .then((files) => files.map((file) => ({ lane, name: file }))) + ) + ); + const files = lanesFiles.flat(); + const filesContents = await Promise.all( + files.map((file) => + getContent(`${process.env.TASKS_DIR}/${file.lane}/${file.name}`) + ) + ); + const usedTagsTexts = filesContents + .map((content) => getTagsTextsFromCardContent(content)) + .flat() + .sort((a, b) => a.localeCompare(b)); + const usedTagsTextsWithoutDuplicates = Array.from( + new Set(usedTagsTexts.map((tagText) => tagText.toLowerCase())) + ); + const allTags = await fs.promises + .readFile(`${process.env.CONFIG_DIR}/tags.json`) + .then((res) => JSON.parse(res.toString())) + .catch((err) => []); + const usedTags = usedTagsTextsWithoutDuplicates.map( + (tag) => + allTags.find((tagToFind) => tagToFind.name.toLowerCase() === tag) || { + name: tag, + backgroundColor: "var(--tag-color-1)", + } + ); + await fs.promises.writeFile( + `${process.env.CONFIG_DIR}/tags.json`, + JSON.stringify(usedTags) + ); + ctx.status = 200; + ctx.body = usedTags; } -router.get('/tags', getTags); +router.get("/tags", getTags); async function updateTagBackgroundColor(ctx) { - const name = ctx.params.tagName; - const backgroundColor = ctx.request.body.backgroundColor; - const tags = await fs.promises.readFile(`${CONFIG_DIR}/tags.json`) - .then(res => JSON.parse(res.toString())) - .catch(err => []); - const tagIndex = tags.findIndex(tag => tag.name.toLowerCase() === name.toLowerCase()); - if (tagIndex === -1) { - ctx.status = 404; - ctx.body = `Tag ${name} not found`; - return; - } - tags[tagIndex].backgroundColor = backgroundColor; - await fs.promises.writeFile(`${CONFIG_DIR}/tags.json`, JSON.stringify(tags)); - ctx.status = 204; + const name = ctx.params.tagName; + const backgroundColor = ctx.request.body.backgroundColor; + const tags = await fs.promises + .readFile(`${process.env.CONFIG_DIR}/tags.json`) + .then((res) => JSON.parse(res.toString())) + .catch((err) => []); + const tagIndex = tags.findIndex( + (tag) => tag.name.toLowerCase() === name.toLowerCase() + ); + if (tagIndex === -1) { + ctx.status = 404; + ctx.body = `Tag ${name} not found`; + return; + } + tags[tagIndex].backgroundColor = backgroundColor; + await fs.promises.writeFile( + `${process.env.CONFIG_DIR}/tags.json`, + JSON.stringify(tags) + ); + ctx.status = 204; } -router.patch('/tags/:tagName', updateTagBackgroundColor); +router.patch("/tags/:tagName", updateTagBackgroundColor); function getTagsTextsFromCardContent(cardContent) { - const indexOfTagsKeyword = cardContent.toLowerCase().indexOf('tags: '); - if (indexOfTagsKeyword === -1) { - return []; - } - let startOfTags = cardContent.substring(indexOfTagsKeyword + 'tags: '.length); - const lineBreak = cardContent.indexOf('\n'); - if (lineBreak > 0) { - startOfTags = startOfTags.split('\n')[0]; - } - const tags = startOfTags - .split(',') - .map(tag => tag.trim()) - .filter(tag => tag !== '') - - return tags; + const indexOfTagsKeyword = cardContent.toLowerCase().indexOf("tags: "); + if (indexOfTagsKeyword === -1) { + return []; + } + let startOfTags = cardContent.substring(indexOfTagsKeyword + "tags: ".length); + const lineBreak = cardContent.indexOf("\n"); + if (lineBreak > 0) { + startOfTags = startOfTags.split("\n")[0]; + } + const tags = startOfTags + .split(",") + .map((tag) => tag.trim()) + .filter((tag) => tag !== ""); + + return tags; } async function getLaneByCardName(cardName) { - const lanes = await getLanesNames(); - const lanesFiles = await Promise.all(lanes.map(lane => fs.promises.readdir(`${TASKS_DIR}/${lane}`) - .then(files => files.map(file => ({ lane, name: file })))) - ); - const files = lanesFiles.flat(); - return files.find(file => file.name === `${cardName}.md`).lane; + const lanes = await getLanesNames(); + const lanesFiles = await Promise.all( + lanes.map((lane) => + fs.promises + .readdir(`${process.env.TASKS_DIR}/${lane}`) + .then((files) => files.map((file) => ({ lane, name: file }))) + ) + ); + const files = lanesFiles.flat(); + return files.find((file) => file.name === `${cardName}.md`).lane; } async function getLanes(ctx) { - const lanes = await fs.promises.readdir(TASKS_DIR); - ctx.body = lanes; -}; + const lanes = await fs.promises.readdir(process.env.TASKS_DIR); + ctx.body = lanes; +} -router.get('/lanes', getLanes); +router.get("/lanes", getLanes); async function getCards(ctx) { - const lanes = await getLanesNames(); - const lanesFiles = await Promise.all(lanes.map(lane => fs.promises.readdir(`${TASKS_DIR}/${lane}`) - .then(files => files.map(file => ({ lane, name: file })))) - ); - const files = lanesFiles.flat(); - const filesContents = await Promise.all( - files.map(async file => { - const content = await getContent(`${TASKS_DIR}/${file.lane}/${file.name}`); - const newName = file.name.substring(0, file.name.length - 3); - return { ...file, content, name: newName } - }) - ); - ctx.body = filesContents; -}; - -router.get('/cards', getCards); + const lanes = await getLanesNames(); + const lanesFiles = await Promise.all( + lanes.map((lane) => + fs.promises + .readdir(`${process.env.TASKS_DIR}/${lane}`) + .then((files) => files.map((file) => ({ lane, name: file }))) + ) + ); + const files = lanesFiles.flat(); + const filesContents = await Promise.all( + files.map(async (file) => { + const content = await getContent( + `${process.env.TASKS_DIR}/${file.lane}/${file.name}` + ); + const newName = file.name.substring(0, file.name.length - 3); + return { ...file, content, name: newName }; + }) + ); + ctx.body = filesContents; +} + +router.get("/cards", getCards); async function createCard(ctx) { - const lane = ctx.request.body.lane; - const name = uuid.v4(); - await fs.promises.writeFile(`${TASKS_DIR}/${lane}/${name}.md`, ''); - await fs.promises.chown(`${TASKS_DIR}/${lane}/${name}.md`, PUID, PGID); - ctx.body = name; - ctx.status = 201; + const lane = ctx.request.body.lane; + const name = uuid.v4(); + await fs.promises.writeFile( + `${process.env.TASKS_DIR}/${lane}/${name}.md`, + "" + ); + await fs.promises.chown( + `${process.env.TASKS_DIR}/${lane}/${name}.md`, + PUID, + PGID + ); + ctx.body = name; + ctx.status = 201; } -router.post('/cards', createCard); +router.post("/cards", createCard); async function updateCard(ctx) { - const oldLane = await getLaneByCardName(ctx.params.card); - const name = ctx.params.card; - const newLane = ctx.request.body.lane || oldLane; - const newName = ctx.request.body.name || name; - const newContent = ctx.request.body.content; - if (newLane !== oldLane || name !== newName) { - await fs.promises.rename(`${TASKS_DIR}/${oldLane}/${name}.md`, `${TASKS_DIR}/${newLane}/${newName}.md`); - } - if (newContent) { - await fs.promises.writeFile(`${TASKS_DIR}/${newLane}/${newName}.md`, newContent); - } - await fs.promises.chown(`${TASKS_DIR}/${newLane}/${newName}.md`, PUID, PGID); - ctx.status = 204; + const oldLane = await getLaneByCardName(ctx.params.card); + const name = ctx.params.card; + const newLane = ctx.request.body.lane || oldLane; + const newName = ctx.request.body.name || name; + const newContent = ctx.request.body.content; + if (newLane !== oldLane || name !== newName) { + await fs.promises.rename( + `${process.env.TASKS_DIR}/${oldLane}/${name}.md`, + `${process.env.TASKS_DIR}/${newLane}/${newName}.md` + ); + } + if (newContent) { + await fs.promises.writeFile( + `${process.env.TASKS_DIR}/${newLane}/${newName}.md`, + newContent + ); + } + await fs.promises.chown( + `${process.env.TASKS_DIR}/${newLane}/${newName}.md`, + PUID, + PGID + ); + ctx.status = 204; } -router.patch('/cards/:card', updateCard); +router.patch("/cards/:card", updateCard); async function deleteCard(ctx) { - const lane = await getLaneByCardName(ctx.params.card); - const name = ctx.params.card; - await fs.promises.rm(`${TASKS_DIR}/${lane}/${name}.md`); - ctx.status = 204; + const lane = await getLaneByCardName(ctx.params.card); + const name = ctx.params.card; + await fs.promises.rm(`${process.env.TASKS_DIR}/${lane}/${name}.md`); + ctx.status = 204; } -router.delete('/cards/:card', deleteCard); +router.delete("/cards/:card", deleteCard); async function createCard(ctx) { - const lane = ctx.request.body.lane; - const name = uuid.v4(); - await fs.promises.writeFile(`${TASKS_DIR}/${lane}/${name}.md`, ''); - await fs.promises.chown(`${TASKS_DIR}/${lane}/${name}.md`, PUID, PGID); - ctx.body = name; - ctx.status = 201; + const lane = ctx.request.body.lane; + const name = uuid.v4(); + await fs.promises.writeFile( + `${process.env.TASKS_DIR}/${lane}/${name}.md`, + "" + ); + await fs.promises.chown( + `${process.env.TASKS_DIR}/${lane}/${name}.md`, + PUID, + PGID + ); + ctx.body = name; + ctx.status = 201; } async function createLane(ctx) { - const lane = uuid.v4(); - await fs.promises.mkdir(`${TASKS_DIR}/${lane}`); - await fs.promises.chown(`${TASKS_DIR}/${lane}`, PUID, PGID); - ctx.body = lane; - ctx.status = 201; + const lane = uuid.v4(); + await fs.promises.mkdir(`${process.env.TASKS_DIR}/${lane}`); + await fs.promises.chown(`${process.env.TASKS_DIR}/${lane}`, PUID, PGID); + ctx.body = lane; + ctx.status = 201; } -router.post('/lanes', createLane); +router.post("/lanes", createLane); async function updateLane(ctx) { - const name = ctx.params.lane; - const newName = ctx.request.body.name; - await fs.promises.rename(`${TASKS_DIR}/${name}`, `${TASKS_DIR}/${newName}`); - await fs.promises.chown(`${TASKS_DIR}/${newName}`, PUID, PGID); - ctx.status = 204; + const name = ctx.params.lane; + const newName = ctx.request.body.name; + await fs.promises.rename( + `${process.env.TASKS_DIR}/${name}`, + `${process.env.TASKS_DIR}/${newName}` + ); + await fs.promises.chown(`${process.env.TASKS_DIR}/${newName}`, PUID, PGID); + ctx.status = 204; } -router.patch('/lanes/:lane', updateLane); +router.patch("/lanes/:lane", updateLane); async function deleteLane(ctx) { - const lane = ctx.params.lane; - await fs.promises.rm(`${TASKS_DIR}/${lane}`, { force: true, recursive: true }); - ctx.status = 204; + const lane = ctx.params.lane; + await fs.promises.rm(`${process.env.TASKS_DIR}/${lane}`, { + force: true, + recursive: true, + }); + ctx.status = 204; } -router.delete('/lanes/:lane', deleteLane); +router.delete("/lanes/:lane", deleteLane); async function getTitle(ctx) { - ctx.body = process.env.TITLE; + ctx.body = process.env.TITLE; } -router.get('/title', getTitle); +router.get("/title", getTitle); async function getLanesSort(ctx) { - const lanes = await fs.promises.readFile(`${CONFIG_DIR}/sort/lanes.json`) - .then(res => JSON.parse(res.toString())) - .catch(err => []) - ctx.status = 200; - ctx.body = lanes; + const lanes = await fs.promises + .readFile(`${process.env.CONFIG_DIR}/sort/lanes.json`) + .then((res) => JSON.parse(res.toString())) + .catch((err) => []); + ctx.status = 200; + ctx.body = lanes; } -router.get('/sort/lanes', getLanesSort); +router.get("/sort/lanes", getLanesSort); async function saveLanesSort(ctx) { - const newSort = JSON.stringify(ctx.request.body || []); - await fs.promises.mkdir(`${CONFIG_DIR}/sort`, { recursive: true }); - await fs.promises.writeFile(`${CONFIG_DIR}/sort/lanes.json`, newSort); - await fs.promises.chown(`${CONFIG_DIR}/sort/lanes.json`, PUID, PGID); - ctx.status = 200; + const newSort = JSON.stringify(ctx.request.body || []); + await fs.promises.mkdir(`${process.env.CONFIG_DIR}/sort`, { + recursive: true, + }); + await fs.promises.writeFile( + `${process.env.CONFIG_DIR}/sort/lanes.json`, + newSort + ); + await fs.promises.chown( + `${process.env.CONFIG_DIR}/sort/lanes.json`, + PUID, + PGID + ); + ctx.status = 200; } -router.post('/sort/lanes', saveLanesSort); +router.post("/sort/lanes", saveLanesSort); async function getCardsSort(ctx) { - const cards = await fs.promises.readFile(`${CONFIG_DIR}/sort/cards.json`) - .then(res => JSON.parse(res.toString())) - .catch(err => []) - ctx.status = 200; - ctx.body = cards; + const cards = await fs.promises + .readFile(`${process.env.CONFIG_DIR}/sort/cards.json`) + .then((res) => JSON.parse(res.toString())) + .catch((err) => []); + ctx.status = 200; + ctx.body = cards; } -router.get('/sort/cards', getCardsSort); +router.get("/sort/cards", getCardsSort); async function saveCardsSort(ctx) { - const newSort = JSON.stringify(ctx.request.body || []); - await fs.promises.mkdir(`${CONFIG_DIR}/sort`, { recursive: true }); - await fs.promises.writeFile(`${CONFIG_DIR}/sort/cards.json`, newSort); - await fs.promises.chown(`${CONFIG_DIR}/sort/cards.json`, PUID, PGID); - ctx.status = 200; + const newSort = JSON.stringify(ctx.request.body || []); + await fs.promises.mkdir(`${process.env.CONFIG_DIR}/sort`, { + recursive: true, + }); + await fs.promises.writeFile( + `${process.env.CONFIG_DIR}/sort/cards.json`, + newSort + ); + await fs.promises.chown( + `${process.env.CONFIG_DIR}/sort/cards.json`, + PUID, + PGID + ); + ctx.status = 200; } -router.post('/sort/cards', saveCardsSort); +router.post("/sort/cards", saveCardsSort); async function getImage(ctx) { - await send(ctx, `${CONFIG_DIR}/images/${ctx.params.image}`, { root: process.env.NODE_ENV === 'prod' ? '/' : __dirname }); + await send(ctx, `${process.env.CONFIG_DIR}/images/${ctx.params.image}`, { + root: process.env.NODE_ENV === "prod" ? "/" : __dirname, + }); } -router.get('/images/:image', getImage); +router.get("/images/:image", getImage); async function saveImage(ctx) { - const imageName = ctx.request.file.originalname; - await fs.promises.mkdir(`${CONFIG_DIR}/images`, { recursive: true }); - await fs.promises.writeFile(`${CONFIG_DIR}/images/${imageName}`, ctx.request.file.buffer); - await fs.promises.chown(`${CONFIG_DIR}/images/${imageName}`, PUID, PGID); - ctx.status = 204; + const imageName = ctx.request.file.originalname; + await fs.promises.mkdir(`${process.env.CONFIG_DIR}/images`, { + recursive: true, + }); + await fs.promises.writeFile( + `${process.env.CONFIG_DIR}/images/${imageName}`, + ctx.request.file.buffer + ); + await fs.promises.chown( + `${process.env.CONFIG_DIR}/images/${imageName}`, + PUID, + PGID + ); + ctx.status = 204; } -router.post('/images', multerInstance.single('file'), saveImage); +router.post("/images", multerInstance.single("file"), saveImage); app.use(cors()); -app.use(bodyParser()) +app.use(bodyParser()); app.use(async (ctx, next) => { try { await next(); } catch (err) { - console.error(err) + console.error(err); err.status = err.statusCode || err.status || 500; throw err; } }); + app.use(async (ctx, next) => { - if (BASE_PATH === '/') { - return next(); - } - if (ctx.URL.href === `${ctx.URL.origin}${BASE_PATH.substring(0, BASE_PATH.length - 1)}`) { - ctx.status = 301; - return ctx.redirect(`${ctx.URL.origin}${BASE_PATH}`); - } - await next(); + if (BASE_PATH === "/") { + return next(); + } + if ( + ctx.URL.href === + `${ctx.URL.origin}${BASE_PATH.substring(0, BASE_PATH.length - 1)}` + ) { + ctx.status = 301; + return ctx.redirect(`${ctx.URL.origin}${BASE_PATH}`); + } + await next(); }); app.use(mount(`${BASE_PATH}api`, router.routes())); -app.use(mount(BASE_PATH, serve('/static'))); -app.use(mount(`${BASE_PATH}stylesheets/`, serve(`${CONFIG_DIR}/stylesheets`))); +app.use(mount(BASE_PATH, serve("/static"))); +app.use( + mount( + `${BASE_PATH}stylesheets/`, + serve(`${process.env.CONFIG_DIR}/stylesheets`) + ) +); async function removeUnusedImages() { - const lanes = await getLanesNames(); - const lanesFiles = await Promise.all(lanes.map(lane => fs.promises.readdir(`${TASKS_DIR}/${lane}`) - .then(files => files.map(file => ({ lane, name: file })))) - ); - const files = lanesFiles.flat(); - const filesContents = await Promise.all( - files.map(async file => getContent(`${TASKS_DIR}/${file.lane}/${file.name}`)) - ); - const imagesBeingUsed = filesContents - .map(content => content.match(/!\[[^\]]*\]\(([^\s]+[.]*)\)/g)) - .flat() - .filter(image => !!image && image.includes('/api/images/')) - .map(image => image.split('/api/images/')[1].slice(0, -1)); - const allImages = await fs.promises.readdir(`${CONFIG_DIR}/images`); - const unusedImages = allImages.filter(image => !imagesBeingUsed.includes(image)); - await Promise.all(unusedImages.map(image => fs.promises.rm(`${CONFIG_DIR}/images/${image}`))); -}; - -if (LOCAL_IMAGES_CLEANUP_INTERVAL > 0) { - const intervalInMs = LOCAL_IMAGES_CLEANUP_INTERVAL * 60000; - setInterval(removeUnusedImages, intervalInMs); + const lanes = await getLanesNames(); + const lanesFiles = await Promise.all( + lanes.map((lane) => + fs.promises + .readdir(`${process.env.TASKS_DIR}/${lane}`) + .then((files) => files.map((file) => ({ lane, name: file }))) + ) + ); + const files = lanesFiles.flat(); + const filesContents = await Promise.all( + files.map(async (file) => + getContent(`${process.env.TASKS_DIR}/${file.lane}/${file.name}`) + ) + ); + const imagesBeingUsed = filesContents + .map((content) => content.match(/!\[[^\]]*\]\(([^\s]+[.]*)\)/g)) + .flat() + .filter((image) => !!image && image.includes("/api/images/")) + .map((image) => image.split("/api/images/")[1].slice(0, -1)); + const allImages = await fs.promises.readdir( + `${process.env.CONFIG_DIR}/images` + ); + const unusedImages = allImages.filter( + (image) => !imagesBeingUsed.includes(image) + ); + await Promise.all( + unusedImages.map((image) => + fs.promises.rm(`${process.env.CONFIG_DIR}/images/${image}`) + ) + ); +} + +if (process.env.LOCAL_IMAGES_CLEANUP_INTERVAL) { + const intervalInMs = process.env.LOCAL_IMAGES_CLEANUP_INTERVAL * 60000; + try { + if (intervalInMs > 0) { + setInterval(removeUnusedImages, intervalInMs); + } + } catch (error) { + console.error(error); + } } -app.listen(8080); \ No newline at end of file +app.listen(process.env.PORT); \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 37ca0fa..f55846c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "description": "", "scripts": { - "start": "vite", + "start": "VITE_PORT=3000 VITE_API_PORT=8080 vite", "dev": "vite", "build": "vite build", "serve": "vite preview" diff --git a/frontend/src/api.js b/frontend/src/api.js index fb97bc5..1f815ff 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -1 +1,2 @@ -export const api = import.meta.env.DEV ? 'http://localhost:8080/api' : `${window.location.href}api`; +const basePath = import.meta.env.DEV ? `http://localhost:${import.meta.env.VITE_API_PORT}/` : window.location.href; +export const api = `${basePath.at(-1) === '/' ? basePath : `${basePath}/`}api`; diff --git a/frontend/src/components/menu.jsx b/frontend/src/components/menu.jsx index aa839c0..5e92501 100644 --- a/frontend/src/components/menu.jsx +++ b/frontend/src/components/menu.jsx @@ -35,7 +35,8 @@ export function Menu(props) { props.onClose(); } - function handleOptionConfirmation() { + function handleOptionConfirmation(e) { + e.stopImmediatePropagation(); confirmationPromptCb()(); setConfirmationPromptCb(null); props.onClose(); @@ -80,7 +81,7 @@ export function Menu(props) { onClick={handleOptionConfirmation} id="confirm-btn" onKeyDown={(e) => - handleKeyDown(e, () => handleOptionConfirmation(), handleCancel) + handleKeyDown(e, () => handleOptionConfirmation(e), handleCancel) } > Are you sure? diff --git a/frontend/vite.config.js b/frontend/vite.config.js index fdd2249..bcc2fdf 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -53,7 +53,7 @@ export default defineConfig({ }), ], server: { - port: 3000, + port: Number(process.env.VITE_PORT) }, build: { target: "esnext",