From e6d66529e3f7d0dbed8ce08e76253d98bde6d21d Mon Sep 17 00:00:00 2001 From: BinarySandia04 Date: Wed, 29 Apr 2026 01:32:09 +0200 Subject: [PATCH] ye --- .codex | 0 backend/src/index.js | 1 + backend/src/models/Note.js | 15 + backend/src/routes/note.js | 86 ++++++ frontend/app/assets/css/colors.scss | 4 + .../components/managers/ContentManager.vue | 290 +++++++++++++++++- .../app/components/partials/CampaignEntry.vue | 7 +- frontend/app/components/viewer/TopBar.vue | 66 +++- .../app/components/viewer/content/Content.vue | 12 +- .../app/components/viewer/content/Note.vue | 156 ++++++---- .../viewer/content/NoteContainer.vue | 29 +- .../components/windows/CreateNoteWindow.vue | 150 +++++++++ frontend/app/services/Campaign.js | 12 + frontend/app/services/WindowDefinitions.js | 8 +- frontend/package-lock.json | 13 + frontend/package.json | 1 + 16 files changed, 767 insertions(+), 83 deletions(-) create mode 100644 .codex create mode 100644 backend/src/models/Note.js create mode 100644 backend/src/routes/note.js create mode 100644 frontend/app/components/windows/CreateNoteWindow.vue create mode 100644 frontend/app/services/Campaign.js diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/index.js b/backend/src/index.js index c6447bc..a14e5d2 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -53,6 +53,7 @@ app.use(checkAuth); // ROUTES WITH AUTH app.use('/campaign', require('./routes/campaign')); +app.use('/note', require('./routes/note')); /* app.use('/campaign', require('./routes/campaign')); app.use('/maps', require('./routes/map')); diff --git a/backend/src/models/Note.js b/backend/src/models/Note.js new file mode 100644 index 0000000..2ca6be1 --- /dev/null +++ b/backend/src/models/Note.js @@ -0,0 +1,15 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const NoteSchema = new Schema({ + title: { type: String }, + content: { type: String }, + campaign: { + type: Schema.Types.ObjectId, + ref: 'Campaign', + required: true + }, + date: { type: Date, default: Date.now } +}); + +module.exports = mongoose.model('Note', NoteSchema); \ No newline at end of file diff --git a/backend/src/routes/note.js b/backend/src/routes/note.js new file mode 100644 index 0000000..37daeab --- /dev/null +++ b/backend/src/routes/note.js @@ -0,0 +1,86 @@ +const express = require('express'); +const router = express.Router(); + +const Campaign = require("../models/Campaign"); +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 notes = await Note.find({ campaign }) + .select('_id title content date campaign') + .sort({ date: -1 }) + .lean(); + + res.json({ status: "ok", 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 newNote = new Note({ + title, + content, + campaign + }); + await newNote.save(); + res.json({ status: "ok", note: newNote }); + } catch (err) { + console.error(err); + 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" }); + + if(title) note.title = title; + note.content = content; + note.date = Date.now(); + await note.save(); + res.json({ status: "ok", note }); + } catch (err) { + console.error(err); + 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" }); + + await note.remove(); + res.json({ status: "ok" }); + } catch (err) { + console.error(err); + res.json({ status: "error", msg: "errors.internal" }); + } +}); + +module.exports = router; diff --git a/frontend/app/assets/css/colors.scss b/frontend/app/assets/css/colors.scss index 4a09265..86a79c3 100644 --- a/frontend/app/assets/css/colors.scss +++ b/frontend/app/assets/css/colors.scss @@ -19,6 +19,8 @@ $themes: ( toast-background: #202020, + note-border: #202324, + hover: #21262d, selected: #4a4a4b, border-color: #819796, @@ -49,6 +51,8 @@ $themes: ( toast-background: #f0f0f0, + note-border: #e0e0e0, + border-color: #e0e0e0, border: #f0f0f0, hover: #e9e9e9, diff --git a/frontend/app/components/managers/ContentManager.vue b/frontend/app/components/managers/ContentManager.vue index 2e87ed6..1622982 100644 --- a/frontend/app/components/managers/ContentManager.vue +++ b/frontend/app/components/managers/ContentManager.vue @@ -1,18 +1,302 @@ \ No newline at end of file +.content-manager { + height: 100%; + display: flex; + flex-direction: column; +} + +.content-layout { + min-height: 0; + flex: 1; + display: flex; +} + +.notes-sidebar { + width: 280px; + min-width: 280px; + border-right: 1px solid var(--color-border); + background-color: var(--color-background-light); + display: flex; + flex-direction: column; + transition: width 0.2s ease, min-width 0.2s ease; +} + +.notes-sidebar.collapsed { + width: 54px; + min-width: 54px; +} + +.sidebar-header { + padding: 12px; + display: flex; + align-items: flex-start; + gap: 10px; + border-bottom: 1px solid var(--color-border); +} + +.sidebar-toggle { + width: 30px; + height: 30px; + flex-shrink: 0; + border: 1px solid var(--color-border); + border-radius: 8px; + background: var(--color-background-soft); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.sidebar-toggle-icon { + width: 18px; + height: 18px; + filter: invert(var(--color-icon-invert)); +} + +.sidebar-copy { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.sidebar-eyebrow, +.sidebar-meta { + font-size: 12px; + opacity: 0.7; +} + +.sidebar-title { + line-height: 1.2; + word-break: break-word; +} + +.sidebar-list { + padding: 10px; + display: flex; + flex-direction: column; + overflow-y: auto; +} + +.sidebar-state { + padding: 12px; + border-radius: 10px; + background: var(--color-background-soft); + font-size: 14px; +} + +.sidebar-state.error { + color: #9e2a2b; +} + +.note-link { + padding: 6px; + margin: 0; + box-shadow: none; + border: none; + display: flex; + flex-direction: column; + align-items: flex-start; + text-align: left; + cursor: pointer; + transition: transform 0.15s ease, background-color 0.15s ease; +} + +.note-link:hover { + transform: translateX(2px); + background: var(--color-background-light); +} + +.note-link-title { + font-weight: 600; + word-break: break-word; +} + +.note-link-date { + font-size: 12px; + opacity: 0.7; +} + +@media (max-width: 900px) { + .notes-sidebar { + width: 220px; + min-width: 220px; + } +} + diff --git a/frontend/app/components/partials/CampaignEntry.vue b/frontend/app/components/partials/CampaignEntry.vue index 70fb701..d330217 100644 --- a/frontend/app/components/partials/CampaignEntry.vue +++ b/frontend/app/components/partials/CampaignEntry.vue @@ -3,6 +3,9 @@ import { onMounted, ref } from 'vue'; import { AddSound } from '../../services/Sound'; +import { useCampaignService } from '~/services/Campaign'; +import { ClearWindow } from '~/services/Windows'; +const { SetCampaign } = useCampaignService(); const props = defineProps(['data']); const data = props.data; @@ -21,8 +24,8 @@ onMounted(() => { }); function ViewCampaign(){ - // ConnectToCampaign(data); - // DisplayCampaign(data); + SetCampaign(data); + ClearWindow({type: "main_menu"}); } diff --git a/frontend/app/components/viewer/TopBar.vue b/frontend/app/components/viewer/TopBar.vue index 94b0431..544cbf7 100644 --- a/frontend/app/components/viewer/TopBar.vue +++ b/frontend/app/components/viewer/TopBar.vue @@ -1,17 +1,41 @@ @@ -25,17 +49,51 @@ import TopSearchBar from './topbar/TopSearchBar.vue'; display: flex; } +.logo { + height: 36px; + width: 36px; + position: absolute; + top: 0px; + left: 8px; +} + .left, .right { flex: 1; } .right { - text-align: right; + display: flex; + justify-content: flex-end; + align-items: center; + padding-right: 10px; } .top-bar-title { padding: 10px; display: flex; + margin-left: 48px; font-weight: bold; } - \ No newline at end of file + +.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; +} + +.note-button-icon { + width: 16px; + height: 16px; +} + diff --git a/frontend/app/components/viewer/content/Content.vue b/frontend/app/components/viewer/content/Content.vue index 17e99a3..1de52c9 100644 --- a/frontend/app/components/viewer/content/Content.vue +++ b/frontend/app/components/viewer/content/Content.vue @@ -1,16 +1,13 @@ @@ -18,6 +15,7 @@ function hideSearch(){ - - \ No newline at end of file diff --git a/frontend/app/components/viewer/content/NoteContainer.vue b/frontend/app/components/viewer/content/NoteContainer.vue index f80da38..2b32d0f 100644 --- a/frontend/app/components/viewer/content/NoteContainer.vue +++ b/frontend/app/components/viewer/content/NoteContainer.vue @@ -1,5 +1,5 @@ @@ -59,10 +77,9 @@ emitter.on("delete-note", (key) => { display: flex; height: 100%; margin: 0; - height: 100%; background-color: var(--color-background); } \ No newline at end of file + diff --git a/frontend/app/components/windows/CreateNoteWindow.vue b/frontend/app/components/windows/CreateNoteWindow.vue new file mode 100644 index 0000000..3df888f --- /dev/null +++ b/frontend/app/components/windows/CreateNoteWindow.vue @@ -0,0 +1,150 @@ + + + + + diff --git a/frontend/app/services/Campaign.js b/frontend/app/services/Campaign.js new file mode 100644 index 0000000..bd1a8d1 --- /dev/null +++ b/frontend/app/services/Campaign.js @@ -0,0 +1,12 @@ +export const useCampaignService = () => { + const Campaign = useState('campaign', () => null) + + const SetCampaign = (data) => { + Campaign.value = data; + } + + return { + Campaign, + SetCampaign + } +} \ No newline at end of file diff --git a/frontend/app/services/WindowDefinitions.js b/frontend/app/services/WindowDefinitions.js index 88a474b..c5c8006 100644 --- a/frontend/app/services/WindowDefinitions.js +++ b/frontend/app/services/WindowDefinitions.js @@ -34,9 +34,15 @@ const defWindows = { component: () => import('~/components/windows/CreateCampaignWindow.vue'), close: () => ClearWindow({type: 'create_campaign'}), movable: true + }, + create_note: { + title: "Create note", + component: () => import('~/components/windows/CreateNoteWindow.vue'), + close: () => ClearWindow({type: 'create_note'}), + movable: true } } export { defWindows -} \ No newline at end of file +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a457772..21425ee 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "dependencies": { "@nuxtjs/i18n": "^10.3.0", "axios": "^1.15.2", + "marked": "^18.0.2", "mitt": "^3.0.1", "motion": "^12.38.0", "nuxt": "^4.4.2", @@ -8886,6 +8887,18 @@ "source-map-js": "^1.2.1" } }, + "node_modules/marked": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.2.tgz", + "integrity": "sha512-NsmlUYBS/Zg57rgDWMYdnre6OTj4e+qq/JS2ot3KrYLSoHLw+sDu0Nm1ZGpRgYAq6c+b1ekaY5NzVchMCQnzcg==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 05d359f..6512e8d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "dependencies": { "@nuxtjs/i18n": "^10.3.0", "axios": "^1.15.2", + "marked": "^18.0.2", "mitt": "^3.0.1", "motion": "^12.38.0", "nuxt": "^4.4.2",