From 0fb4f01892d34b2558cca5be3518929a42b03ba6 Mon Sep 17 00:00:00 2001 From: BinarySandia04 Date: Mon, 8 Jun 2026 00:28:29 +0200 Subject: [PATCH] AI slop --- backend/src/index.js | 1 + backend/src/models/Folder.js | 18 + backend/src/models/Note.js | 4 + backend/src/routes/folder.js | 106 +++++ backend/src/routes/note.js | 112 ++++-- frontend/app/assets/css/colors.scss | 14 +- .../managers/ContextMenuManager.vue | 23 +- .../app/components/partials/CampaignEntry.vue | 1 - .../components/partials/ContentSidebar.vue | 307 ++++++++------- .../components/partials/EditUserPartial.vue | 1 - .../components/partials/NestedNoteList.vue | 298 +++++++++++++++ frontend/app/components/viewer/TopBar.vue | 72 +--- .../app/components/viewer/content/Note.vue | 86 ++++- .../viewer/content/NoteContainer.vue | 9 + .../components/viewer/topbar/SearchResult.vue | 75 ++++ .../components/viewer/topbar/TopSearchBar.vue | 361 ++++++++++++++---- .../components/windows/NewFolderWindow.vue | 136 +++++++ .../components/windows/RenameNoteWindow.vue | 127 ++++++ frontend/app/services/ActionRegistry.js | 54 +++ frontend/app/services/Marker.js | 2 +- frontend/app/services/WindowDefinitions.js | 12 + frontend/app/services/Windows.js | 1 + 22 files changed, 1474 insertions(+), 346 deletions(-) create mode 100644 backend/src/models/Folder.js create mode 100644 backend/src/routes/folder.js create mode 100644 frontend/app/components/partials/NestedNoteList.vue create mode 100644 frontend/app/components/viewer/topbar/SearchResult.vue create mode 100644 frontend/app/components/windows/NewFolderWindow.vue create mode 100644 frontend/app/components/windows/RenameNoteWindow.vue create mode 100644 frontend/app/services/ActionRegistry.js diff --git a/backend/src/index.js b/backend/src/index.js index ab506a5..4c88adc 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -55,6 +55,7 @@ app.use(checkAuth); // ROUTES WITH AUTH app.use('/api/campaign', require('./routes/campaign')); app.use('/api/note', require('./routes/note')); +app.use('/api/folder', require('./routes/folder')); /* app.use('/campaign', require('./routes/campaign')); app.use('/maps', require('./routes/map')); diff --git a/backend/src/models/Folder.js b/backend/src/models/Folder.js new file mode 100644 index 0000000..d25810e --- /dev/null +++ b/backend/src/models/Folder.js @@ -0,0 +1,18 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const FolderSchema = new Schema({ + name: { type: String, required: true }, + campaign: { + type: Schema.Types.ObjectId, + ref: 'Campaign', + required: true + }, + parentFolder: { + type: Schema.Types.ObjectId, + ref: 'Folder' + }, + date: { type: Date, default: Date.now } +}); + +module.exports = mongoose.model('Folder', FolderSchema); diff --git a/backend/src/models/Note.js b/backend/src/models/Note.js index 2ca6be1..df82478 100644 --- a/backend/src/models/Note.js +++ b/backend/src/models/Note.js @@ -9,6 +9,10 @@ const NoteSchema = new Schema({ ref: 'Campaign', required: true }, + folder: { + type: Schema.Types.ObjectId, + ref: 'Folder' + }, date: { type: Date, default: Date.now } }); diff --git a/backend/src/routes/folder.js b/backend/src/routes/folder.js new file mode 100644 index 0000000..f1790cb --- /dev/null +++ b/backend/src/routes/folder.js @@ -0,0 +1,106 @@ +const express = require('express'); +const router = express.Router(); + +const Campaign = require('../models/Campaign'); +const Folder = require('../models/Folder'); +const Note = require('../models/Note'); + +async function userOwnsCampaign(campaignId, userId) { + const campaign = await Campaign.findOne({ _id: campaignId, createdBy: userId }).lean(); + return Boolean(campaign); +} + +router.get('/list', async (req, res) => { + try { + const { campaign } = req.query; + if (!campaign) return res.json({ status: 'error', msg: 'errors.missing-data' }); + + const hasAccess = await userOwnsCampaign(campaign, req.user.id); + if (!hasAccess) return res.json({ status: 'error', msg: 'unauthorized' }); + + const folders = await Folder.find({ campaign }) + .select('_id name date') + .sort({ date: -1 }) + .lean(); + + res.json({ status: 'ok', folders }); + } catch (err) { + console.error(err); + res.json({ status: 'error', msg: 'errors.internal' }); + } +}); + +router.post('/create', async (req, res) => { + try { + const { name, campaign } = req.body; + if (!name || !campaign) return res.json({ status: 'error', msg: 'errors.missing-data' }); + + const hasAccess = await userOwnsCampaign(campaign, req.user.id); + if (!hasAccess) return res.json({ status: 'error', msg: 'unauthorized' }); + + const newFolder = new Folder({ + name: name.trim(), + campaign + }); + await newFolder.save(); + res.json({ status: 'ok', folder: newFolder }); + } catch (err) { + console.error(err); + res.json({ status: 'error', msg: 'errors.internal' }); + } +}); + +router.post('/delete', async (req, res) => { + try { + const { id } = req.body; + if (!id) return res.json({ status: 'error', msg: 'errors.missing-data' }); + + const folder = await Folder.findById(id); + if (!folder) return res.json({ status: 'error', msg: 'errors.notfound' }); + + const hasAccess = await userOwnsCampaign(folder.campaign, req.user.id); + if (!hasAccess) return res.json({ status: 'error', msg: 'unauthorized' }); + + async function moveRecursive(folderId) { + const subfolders = await Folder.find({ parentFolder: folderId }).select('_id').lean(); + await Note.updateMany( + { folder: folderId }, + { $set: { folder: null, date: Date.now() } } + ); + await Folder.deleteOne({ _id: folderId }); + for (const sub of subfolders) { + await moveRecursive(sub._id); + } + } + + await moveRecursive(id); + + res.json({ status: 'ok' }); + } catch (err) { + console.error(err); + res.json({ status: 'error', msg: 'errors.internal' }); + } +}); + +router.post('/rename', async (req, res) => { + try { + const { id, name } = req.body; + if (!id || !name) return res.json({ status: 'error', msg: 'errors.missing-data' }); + + const folder = await Folder.findById(id); + if (!folder) return res.json({ status: 'error', msg: 'errors.notfound' }); + + const hasAccess = await userOwnsCampaign(folder.campaign, req.user.id); + if (!hasAccess) return res.json({ status: 'error', msg: 'unauthorized' }); + + folder.name = name.trim(); + await folder.save(); + + res.json({ status: 'ok', folder }); + } catch (err) { + console.error(err); + res.json({ status: 'error', msg: 'errors.internal' }); + } +}); + +module.exports = router; diff --git a/backend/src/routes/note.js b/backend/src/routes/note.js index 37daeab..a078e54 100644 --- a/backend/src/routes/note.js +++ b/backend/src/routes/note.js @@ -1,8 +1,9 @@ const express = require('express'); const router = express.Router(); -const Campaign = require("../models/Campaign"); -const Note = require("../models/Note"); +const Campaign = require('../models/Campaign'); +const Note = require('../models/Note'); +const Folder = require('../models/Folder'); async function userOwnsCampaign(campaignId, userId) { const campaign = await Campaign.findOne({ _id: campaignId, createdBy: userId }).lean(); @@ -12,74 +13,115 @@ async function userOwnsCampaign(campaignId, userId) { router.get('/list', async (req, res) => { try { const { campaign } = req.query; - if (!campaign) return res.json({ status: "error", msg: "errors.missing-data" }); + if (!campaign) return res.json({ status: 'error', msg: 'errors.missing-data' }); const hasAccess = await userOwnsCampaign(campaign, req.user.id); - if (!hasAccess) return res.json({ status: "error", msg: "unauthorized" }); + if (!hasAccess) return res.json({ status: 'error', msg: 'unauthorized' }); - const notes = await Note.find({ campaign }) - .select('_id title content date campaign') + const folders = await Folder.find({ campaign }) + .select('_id name date') .sort({ date: -1 }) .lean(); - res.json({ status: "ok", notes }); + const rootNotes = await Note.find({ campaign, folder: null }) + .select('_id title content date') + .sort({ date: -1 }) + .lean(); + + res.json({ status: 'ok', folders, notes: rootNotes }); } catch (err) { console.error(err); - res.json({ status: "error", msg: "errors.internal" }); + res.json({ status: 'error', msg: 'errors.internal' }); + } +}); + +router.get('/subfolder/list', async (req, res) => { + try { + const { campaign, folder } = req.query; + if (!campaign || !folder) return res.json({ status: 'error', msg: 'errors.missing-data' }); + + const hasAccess = await userOwnsCampaign(campaign, req.user.id); + if (!hasAccess) return res.json({ status: 'error', msg: 'unauthorized' }); + + // Verify folder belongs to campaign + const folderDoc = await Folder.findOne({ _id: folder, campaign }).lean(); + if (!folderDoc) return res.json({ status: 'error', msg: 'errors.notfound' }); + + const subfolders = await Folder.find({ campaign, parentFolder: folder }) + .select('_id name date') + .sort({ date: -1 }) + .lean(); + + const notes = await Note.find({ campaign, folder: folder }) + .select('_id title content date') + .sort({ date: -1 }) + .lean(); + + res.json({ status: 'ok', subfolders, notes }); + } catch (err) { + console.error(err); + res.json({ status: 'error', msg: 'errors.internal' }); } }); router.post('/create', async (req, res) => { try { - const { title, content, campaign } = req.body; - const hasAccess = await userOwnsCampaign(campaign, req.user.id); - if (!hasAccess) return res.json({ status: "error", msg: "unauthorized" }); + const { title, content, campaign, folder } = req.body; + if (!title || !campaign) return res.json({ status: 'error', msg: 'errors.missing-data' }); - const newNote = new Note({ - title, - content, - campaign - }); + const hasAccess = await userOwnsCampaign(campaign, req.user.id); + if (!hasAccess) return res.json({ status: 'error', msg: 'unauthorized' }); + + let effectiveFolder = null; + if (folder) { + const folderDoc = await Folder.findOne({ _id: folder, campaign }).lean(); + if (!folderDoc) return res.json({ status: 'error', msg: 'errors.notfound' }); + effectiveFolder = folder; + } + + const newNote = new Note({ title, content, campaign, folder: effectiveFolder }); await newNote.save(); - res.json({ status: "ok", note: newNote }); + res.json({ status: 'ok', note: newNote }); } catch (err) { console.error(err); - res.json({ status: "error", msg: "errors.internal" }); + res.json({ status: 'error', msg: 'errors.internal' }); } }); router.post('/update', async (req, res) => { try { - const { id, title, content } = req.body; - const note = await Note.findById(id); - if (!note) return res.json({ status: "error", msg: "errors.notfound" }); - const hasAccess = await userOwnsCampaign(note.campaign, req.user.id); - if (!hasAccess) return res.json({ status: "error", msg: "unauthorized" }); + const { id, title, content, folder } = req.body; + if (!id) return res.json({ status: 'error', msg: 'errors.missing-data' }); - if(title) note.title = title; - note.content = content; - note.date = Date.now(); + const note = await Note.findById(id); + if (!note) return res.json({ status: 'error', msg: 'errors.notfound' }); + + const hasAccess = await userOwnsCampaign(note.campaign, req.user.id); + if (!hasAccess) return res.json({ status: 'error', msg: 'unauthorized' }); + + if (title !== undefined) note.title = title; + if (content !== undefined) note.content = content; + if (folder !== undefined) note.folder = folder; await note.save(); - res.json({ status: "ok", note }); + res.json({ status: 'ok', note }); } catch (err) { console.error(err); - res.json({ status: "error", msg: "errors.internal" }); + res.json({ status: 'error', msg: 'errors.internal' }); } }); router.post('/delete', async (req, res) => { try { const { id } = req.body; - const note = await Note.findById(id); - if (!note) return res.json({ status: "error", msg: "errors.notfound" }); - const hasAccess = await userOwnsCampaign(note.campaign, req.user.id); - if (!hasAccess) return res.json({ status: "error", msg: "unauthorized" }); + if (!id) return res.json({ status: 'error', msg: 'errors.missing-data' }); - await note.remove(); - res.json({ status: "ok" }); + const result = await Note.deleteOne({ _id: id }); + if (result.deletedCount === 0) return res.json({ status: 'error', msg: 'errors.notfound' }); + + res.json({ status: 'ok' }); } catch (err) { console.error(err); - res.json({ status: "error", msg: "errors.internal" }); + res.json({ status: 'error', msg: 'errors.internal' }); } }); diff --git a/frontend/app/assets/css/colors.scss b/frontend/app/assets/css/colors.scss index 180d11a..f64dcab 100644 --- a/frontend/app/assets/css/colors.scss +++ b/frontend/app/assets/css/colors.scss @@ -18,6 +18,7 @@ $themes: ( button-active: #202020cc, toast-background: #202020, + tooltip-background: #2a2a2a, note-border: #202324, @@ -33,7 +34,11 @@ $themes: ( green: #98c379, gray: #cccccc, - icon-invert: 100% + icon-invert: 100%, + + search-background: #20202077, + search-background-container: #202020ee, + search-hover: #2a2a2a, ), light: ( background: #ffffff, @@ -51,6 +56,7 @@ $themes: ( button-active: #d4d4d4, toast-background: #f0f0f0, + tooltip-background: #f5f5f5, note-border: #e0e0e0, @@ -66,7 +72,11 @@ $themes: ( green: #98c379, gray: #cccccc, - icon-invert: 0% + icon-invert: 0%, + + search-background: #f0f0f0aa, + search-background-container: #ffffffee, + search-hover: #e8e8e8, ) ); diff --git a/frontend/app/components/managers/ContextMenuManager.vue b/frontend/app/components/managers/ContextMenuManager.vue index 627d44b..3847320 100644 --- a/frontend/app/components/managers/ContextMenuManager.vue +++ b/frontend/app/components/managers/ContextMenuManager.vue @@ -34,19 +34,20 @@ onMounted(() => { z-index: 214748363; flex-direction: column; + background-color: var(--color-tooltip-background); + border: 1px solid var(--color-border); + overflow: visible; - .context-menu-element { - &:last-child { border-width: 1px 1px 1px 1px; } border: solid 1px var(--color-border); border-width: 1px 1px 0px 1px; - padding: 3px 5px 3px 5px; + padding: 6px 10px 6px 8px; cursor: default; user-select: none; - background-color: var(--tooltip-background); + background-color: var(--color-tooltip-background); transition: background-color 100ms; display: flex; align-items: center; @@ -54,19 +55,27 @@ onMounted(() => { span { flex-grow: 1; - padding-right: 20px; + padding-right: 24px; white-space: nowrap; } img { filter: invert(1); - width: 18px; - height: 18px; + width: 16px; + height: 16px; } &:hover { background-color: var(--color-background-softest); } + + > .context-menu { + display: none !important; + } + + &:hover > .context-menu { + display: flex !important; + } } } \ No newline at end of file diff --git a/frontend/app/components/partials/CampaignEntry.vue b/frontend/app/components/partials/CampaignEntry.vue index d330217..0fed89a 100644 --- a/frontend/app/components/partials/CampaignEntry.vue +++ b/frontend/app/components/partials/CampaignEntry.vue @@ -47,7 +47,6 @@ function ViewCampaign(){ diff --git a/frontend/app/components/viewer/TopBar.vue b/frontend/app/components/viewer/TopBar.vue index cc3f51a..93dbc0e 100644 --- a/frontend/app/components/viewer/TopBar.vue +++ b/frontend/app/components/viewer/TopBar.vue @@ -1,47 +1,10 @@ @@ -105,27 +59,5 @@ function exitToMainMenu() { font-weight: bold; } -.top-bar-button, -.note-button { - height: 30px; - padding: 0 12px; - border: 1px solid var(--color-border); - border-radius: 8px; - background: var(--color-background-soft); - display: inline-flex; - align-items: center; - gap: 8px; - cursor: pointer; -} -.note-button:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.top-bar-button-icon, -.note-button-icon { - width: 16px; - height: 16px; -} diff --git a/frontend/app/components/viewer/content/Note.vue b/frontend/app/components/viewer/content/Note.vue index b470748..348e418 100644 --- a/frontend/app/components/viewer/content/Note.vue +++ b/frontend/app/components/viewer/content/Note.vue @@ -15,9 +15,15 @@ const sourceText = ref(''); // Original markdown source, used for editing const displayText = ref(''); // Compiled HTML from markdown const editingMode = ref(false); -const editableTitle = ref(null); const title = ref(props.title); -const displayTitle = ref(''); +const isDirty = ref(false); +const showClose = ref(false); + +let savedState = { text: '', title: '' }; + +function markDirty() { + isDirty.value = sourceText.value !== savedState.text || title.value !== savedState.title; +} function closeNote(){ emitter.emit('delete-note', props.noteKey); @@ -48,20 +54,29 @@ function update(){ }, 0); } -watch(sourceText, (newText) => { - // update(); -}); +watch(sourceText, markDirty); + +watch(title, markDirty); onMounted(() => { sourceText.value = props.text; title.value = props.title; - displayTitle.value = props.title; + + savedState = { text: props.text, title: props.title }; + emitter.on('title-updated', handleTitleUpdate); // window.addEventListener('keydown', handleKeydown); setTimeout(() => setupCallout(), 0); update(); }); +function handleTitleUpdate(data) { + if (data.key !== props.noteKey) return; + title.value = data.title; + savedState.title = data.title; +} + onUnmounted(() => { + emitter.off('title-updated', handleTitleUpdate); // window.removeEventListener('keydown', handleKeydown); }); @@ -92,12 +107,11 @@ function SaveNote(){ title: title.value, }).then((response) => { if(response.data.status !== 'ok'){ - // Handle error (e.g., show a notification) return; } - // DisplayToast('green', "Note saved successfully.", 500); + savedState = { text: sourceText.value, title: title.value }; + isDirty.value = false; }).catch((error) => { - // Handle error (e.g., show a notification) }); } @@ -147,16 +161,18 @@ function setupCallout() { } -const editTitle = (e) => { - title.value = e.target.innerText; -} -