diff --git a/backend/src/routes/campaign.js b/backend/src/routes/campaign.js
index b435eb7..686a9b2 100644
--- a/backend/src/routes/campaign.js
+++ b/backend/src/routes/campaign.js
@@ -30,4 +30,17 @@ router.get('/list', async (req, res) => {
}
});
-module.exports = router;
\ No newline at end of file
+router.get('/retrieve/:id', async (req, res) => {
+ try {
+ if (!req.user?.id) return res.status(401).json({ status: "error", msg: "errors.unauthorized" });
+
+ const campaign = await Campaign.findOne({ _id: req.params.id, createdBy: req.user.id });
+ if (!campaign) return res.json({ status: "error", msg: "errors.not-found" });
+
+ res.json({ status: "ok", campaign });
+ } catch (err) {
+ res.json({ status: "error", msg: "errors.internal", err });
+ }
+});
+
+module.exports = router;
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..bb74a5c
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,19 @@
+# Frontend
+
+This folder contains the React frontend for Dragonroll, an open-source role-playing game helper.
+
+## Features
+
+- Campaign management with character tracking
+- Player note sharing (markdown)
+- Audio for in-person campaigns
+- Encounter planning
+- Item and spell management
+
+## Files
+
+- `app/` - Application components and services
+- `app/services/` - Frontend services (Campaign, User, Window, etc.)
+- Styling and React components
+
+See the main README for complete information.
diff --git a/frontend/app/app.vue b/frontend/app/app.vue
index 19755c8..157b560 100644
--- a/frontend/app/app.vue
+++ b/frontend/app/app.vue
@@ -7,10 +7,16 @@ import { CreateWindow } from '@/services/Windows'
import { GetUser, HasAdmin } from './services/User';
import TooltipManager from './components/managers/TooltipManager.vue';
import ContextMenuManager from './components/managers/ContextMenuManager.vue';
+import { useCampaignService } from './services/Campaign';
+
+const { RestoreCampaign } = useCampaignService();
async function start(){
if(GetUser()){
- CreateWindow('main_menu');
+ const restoredCampaign = await RestoreCampaign();
+ if (!restoredCampaign) {
+ CreateWindow('main_menu');
+ }
return;
}
@@ -56,4 +62,4 @@ onMounted(() => {
width: 100%;
height: 100vh;
}
-
\ No newline at end of file
+
diff --git a/frontend/app/components/partials/ColorPicker.vue b/frontend/app/components/layouts/ColorPicker.vue
similarity index 100%
rename from frontend/app/components/partials/ColorPicker.vue
rename to frontend/app/components/layouts/ColorPicker.vue
diff --git a/frontend/app/components/partials/IconButton.vue b/frontend/app/components/layouts/IconButton.vue
similarity index 100%
rename from frontend/app/components/partials/IconButton.vue
rename to frontend/app/components/layouts/IconButton.vue
diff --git a/frontend/app/components/managers/ContentManager.vue b/frontend/app/components/managers/ContentManager.vue
index e2d3067..4959c0d 100644
--- a/frontend/app/components/managers/ContentManager.vue
+++ b/frontend/app/components/managers/ContentManager.vue
@@ -1,115 +1,17 @@
@@ -117,55 +19,7 @@ watch(Campaign, () => {
-
-
+
@@ -184,117 +38,4 @@ watch(Campaign, () => {
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/ContentSidebar.vue b/frontend/app/components/partials/ContentSidebar.vue
new file mode 100644
index 0000000..243f32c
--- /dev/null
+++ b/frontend/app/components/partials/ContentSidebar.vue
@@ -0,0 +1,354 @@
+
+
+
+
+
+
+
diff --git a/frontend/app/components/viewer/TopBar.vue b/frontend/app/components/viewer/TopBar.vue
index 544cbf7..a67d97b 100644
--- a/frontend/app/components/viewer/TopBar.vue
+++ b/frontend/app/components/viewer/TopBar.vue
@@ -2,8 +2,9 @@
import TopSearchBar from './topbar/TopSearchBar.vue';
import { useCampaignService } from '~/services/Campaign';
import { CreateWindow } from '~/services/Windows';
+import { SetShowContent } from '~/services/Content';
-const { Campaign } = useCampaignService();
+const { Campaign, SetCampaign } = useCampaignService();
const campaignName = computed(() => {
return Campaign.value?.name ?? 'Campaign';
@@ -17,6 +18,12 @@ function openCreateNoteWindow() {
CreateWindow('create_note');
}
+function exitToMainMenu() {
+ SetCampaign(null);
+ SetShowContent(false);
+ CreateWindow('main_menu');
+}
+
@@ -31,6 +38,10 @@ function openCreateNoteWindow() {