events linked to projects; truths; rules audit
This commit is contained in:
@@ -626,6 +626,30 @@
|
|||||||
}
|
}
|
||||||
.tl-chooser-btn:hover { background: rgba(184,138,67,.12); color: var(--paper); }
|
.tl-chooser-btn:hover { background: rgba(184,138,67,.12); color: var(--paper); }
|
||||||
|
|
||||||
|
/* Truths */
|
||||||
|
.truth-list { display:flex; flex-direction:column; gap:.6rem; margin-top:.8rem; }
|
||||||
|
.truth-item {
|
||||||
|
display:flex; align-items:flex-start; gap:1rem;
|
||||||
|
padding:.8rem 1rem; border-left:3px solid var(--brass);
|
||||||
|
background:rgba(184,138,67,.05);
|
||||||
|
font:italic 1rem/1.6 var(--serif); color:var(--paper);
|
||||||
|
}
|
||||||
|
.truth-item-text { flex:1; }
|
||||||
|
.project-events {
|
||||||
|
margin-top:.8rem; padding-top:.8rem; border-top:1px solid var(--line);
|
||||||
|
}
|
||||||
|
.project-events-label {
|
||||||
|
font:700 .62rem/1 var(--mono); letter-spacing:.12em; text-transform:uppercase;
|
||||||
|
color:var(--muted); margin-bottom:.5rem;
|
||||||
|
}
|
||||||
|
.project-events-list { display:flex; flex-direction:column; gap:.3rem; }
|
||||||
|
.project-event-link {
|
||||||
|
font:.85rem/1.4 var(--sans); color:var(--muted); text-decoration:none;
|
||||||
|
display:flex; align-items:baseline; gap:.5rem;
|
||||||
|
}
|
||||||
|
.project-event-link:hover { color:var(--paper); }
|
||||||
|
.project-event-date { font:.62rem/1 var(--mono); color:var(--muted); flex-shrink:0; }
|
||||||
|
|
||||||
/* Autocomplete dropdown */
|
/* Autocomplete dropdown */
|
||||||
.ac-wrap { position: relative; }
|
.ac-wrap { position: relative; }
|
||||||
.ac-dropdown {
|
.ac-dropdown {
|
||||||
@@ -747,6 +771,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Truths -->
|
||||||
|
<div class="section" x-show="(game && game.truths && game.truths.length) || editMode" style="margin-bottom:3rem">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-label">Truths</div>
|
||||||
|
<button class="btn sm" x-show="editMode" @click="openEditGame()">Edit</button>
|
||||||
|
</div>
|
||||||
|
<div class="truth-list" x-show="game && game.truths && game.truths.length">
|
||||||
|
<template x-for="(t, i) in (game && game.truths || [])" :key="i">
|
||||||
|
<div class="truth-item">
|
||||||
|
<span class="truth-item-text" x-text="t"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div x-show="editMode && (!game || !game.truths || !game.truths.length)" class="empty-section">
|
||||||
|
No truths established. Add the absolute rules of your setting in Edit Session Details.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Timeline -->
|
<!-- Timeline -->
|
||||||
<div class="timeline-section">
|
<div class="timeline-section">
|
||||||
<div class="chronicle-eyebrow" style="margin-bottom:.8rem">Colony Chronicle // Timeline</div>
|
<div class="chronicle-eyebrow" style="margin-bottom:.8rem">Colony Chronicle // Timeline</div>
|
||||||
@@ -846,6 +888,7 @@
|
|||||||
<div class="entry-eyebrow">
|
<div class="entry-eyebrow">
|
||||||
<span x-text="item.date"></span>
|
<span x-text="item.date"></span>
|
||||||
<span x-show="item.isAnchor"><span class="sep"> // </span><span class="e-anchor" x-text="item.anchorType === 'start' ? 'Opening' : 'Closing'"></span></span>
|
<span x-show="item.isAnchor"><span class="sep"> // </span><span class="e-anchor" x-text="item.anchorType === 'start' ? 'Opening' : 'Closing'"></span></span>
|
||||||
|
<span x-show="projectForEvent(item)"><span class="sep"> // </span><a :href="'#entry-' + item.project_id" style="color:var(--oxygen);text-decoration:none" x-text="projectForEvent(item)?.title"></a></span>
|
||||||
<span x-show="item.discipline"><span class="sep"> // </span><span class="disc-emoji" x-text="disciplineIcon(item.discipline)"></span> <span x-text="item.discipline"></span></span>
|
<span x-show="item.discipline"><span class="sep"> // </span><span class="disc-emoji" x-text="disciplineIcon(item.discipline)"></span> <span x-text="item.discipline"></span></span>
|
||||||
<span x-show="item.mood"><span class="sep"> // </span><span :class="item.mood === 'dark' ? 'e-dark' : ''" x-text="item.mood === 'dark' ? '◉' : '○'"></span></span>
|
<span x-show="item.mood"><span class="sep"> // </span><span :class="item.mood === 'dark' ? 'e-dark' : ''" x-text="item.mood === 'dark' ? '◉' : '○'"></span></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -895,6 +938,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="entry-title" x-text="item.title"></div>
|
<div class="entry-title" x-text="item.title"></div>
|
||||||
<div class="entry-desc" x-show="item.description" x-html="renderDesc(item.description)"></div>
|
<div class="entry-desc" x-show="item.description" x-html="renderDesc(item.description)"></div>
|
||||||
|
<div class="project-events" x-show="eventsForProject(item.id).length > 0">
|
||||||
|
<div class="project-events-label">Events in this project</div>
|
||||||
|
<div class="project-events-list">
|
||||||
|
<template x-for="ev in eventsForProject(item.id)" :key="ev.id">
|
||||||
|
<a class="project-event-link" :href="'#entry-' + ev.id">
|
||||||
|
<span class="project-event-date" x-text="ev.date"></span>
|
||||||
|
<span x-text="ev.title"></span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="entry-env" x-show="item.env || deckCache[item.id]">
|
<div class="entry-env" x-show="item.env || deckCache[item.id]">
|
||||||
<img
|
<img
|
||||||
:src="item.env ? 'assets/' + item.env + '.jpg' : 'assets/deck-images/' + deckCache[item.id]"
|
:src="item.env ? 'assets/' + item.env + '.jpg' : 'assets/deck-images/' + deckCache[item.id]"
|
||||||
@@ -1156,6 +1210,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="form-section-title">Truths</div>
|
||||||
|
<p style="font:.82rem/1.5 var(--sans);color:var(--muted);margin:0 0 .8rem">Absolute rules of the setting. Nothing created during play can contradict them.</p>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:.4rem;margin-bottom:.8rem">
|
||||||
|
<template x-for="(t, i) in (activeForm.truths || [])" :key="i">
|
||||||
|
<div style="display:flex;align-items:center;gap:.6rem;padding:.5rem .8rem;border-left:2px solid var(--brass);background:rgba(184,138,67,.05)">
|
||||||
|
<span style="flex:1;font:italic .9rem/1.5 var(--serif);color:var(--paper)" x-text="t"></span>
|
||||||
|
<button class="btn sm danger" @click="activeForm.truths.splice(i,1)">✕</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:.6rem">
|
||||||
|
<input type="text" x-model="newTruth" placeholder="e.g. There is no faster-than-light travel."
|
||||||
|
@keydown.enter.prevent="if(newTruth.trim()){(activeForm.truths=activeForm.truths||[]).push(newTruth.trim());newTruth=''}"
|
||||||
|
style="flex:1" />
|
||||||
|
<button class="btn sm" @click="if(newTruth.trim()){(activeForm.truths=activeForm.truths||[]).push(newTruth.trim());newTruth=''}">Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-error" x-show="formError" x-text="formError"></div>
|
<div class="form-error" x-show="formError" x-text="formError"></div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="btn ghost" @click="closeModal()">Cancel</button>
|
<button class="btn ghost" @click="closeModal()">Cancel</button>
|
||||||
@@ -1173,10 +1246,21 @@
|
|||||||
:placeholder="activeForm.type === 'event' ? 'What happened?' : activeForm.type === 'project' ? 'Project name' : 'Character name'" />
|
:placeholder="activeForm.type === 'event' ? 'What happened?' : activeForm.type === 'project' ? 'Project name' : 'Character name'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Event: single date -->
|
<!-- Event: single date + optional project -->
|
||||||
<div class="form-group" x-show="activeForm.type === 'event'">
|
<div x-show="activeForm.type === 'event'">
|
||||||
<label class="form-label">Date <span>*</span></label>
|
<div class="form-group">
|
||||||
<input type="date" x-model="activeForm.date" :min="game && game.timeline_start" :max="game && game.timeline_end" />
|
<label class="form-label">Date <span>*</span></label>
|
||||||
|
<input type="date" x-model="activeForm.date" :min="game && game.timeline_start" :max="game && game.timeline_end" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group" x-show="projects().length">
|
||||||
|
<label class="form-label">Project (optional)</label>
|
||||||
|
<select x-model="activeForm.project_id">
|
||||||
|
<option value="">— Standalone event —</option>
|
||||||
|
<template x-for="p in projects()" :key="p.id">
|
||||||
|
<option :value="p.id" x-text="p.title"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Project: start + end + status -->
|
<!-- Project: start + end + status -->
|
||||||
@@ -1427,6 +1511,8 @@
|
|||||||
showCard: false,
|
showCard: false,
|
||||||
// Timeline click chooser
|
// Timeline click chooser
|
||||||
tlChooser: { show: false, x: 0, y: 0, date: '' },
|
tlChooser: { show: false, x: 0, y: 0, date: '' },
|
||||||
|
// Truths
|
||||||
|
newTruth: '',
|
||||||
// Autocomplete
|
// Autocomplete
|
||||||
acMode: null,
|
acMode: null,
|
||||||
acQuery: '',
|
acQuery: '',
|
||||||
@@ -1989,6 +2075,15 @@
|
|||||||
return result.sort((a, b) => a.term.localeCompare(b.term))
|
return result.sort((a, b) => a.term.localeCompare(b.term))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
eventsForProject(projectId) {
|
||||||
|
return this.events().filter(e => e.project_id === projectId)
|
||||||
|
},
|
||||||
|
|
||||||
|
projectForEvent(item) {
|
||||||
|
if (!item.project_id) return null
|
||||||
|
return this.projects().find(p => p.id === item.project_id) || null
|
||||||
|
},
|
||||||
|
|
||||||
castUsages(name) {
|
castUsages(name) {
|
||||||
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
const re = new RegExp(`@"${escaped}"`, 'i')
|
const re = new RegExp(`@"${escaped}"`, 'i')
|
||||||
@@ -2113,7 +2208,7 @@
|
|||||||
|
|
||||||
openAddItem(type) {
|
openAddItem(type) {
|
||||||
const env = this.randomEnv(null)
|
const env = this.randomEnv(null)
|
||||||
this.activeForm = { type, title: '', description: '', mood: '', date: '', end_date: '', status: 'active', discipline: '', env, env_layout: env ? this.randomEnvLayout() : 'after' }
|
this.activeForm = { type, title: '', description: '', mood: '', date: '', end_date: '', status: 'active', discipline: '', env, env_layout: env ? this.randomEnvLayout() : 'after', project_id: '' }
|
||||||
this.editingId = null
|
this.editingId = null
|
||||||
this.formError = null
|
this.formError = null
|
||||||
this.modalType = 'item'
|
this.modalType = 'item'
|
||||||
@@ -2121,7 +2216,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
openEditItem(item) {
|
openEditItem(item) {
|
||||||
this.activeForm = Object.assign({ env: '', env_layout: 'after' }, item)
|
this.activeForm = Object.assign({ env: '', env_layout: 'after', project_id: '' }, item)
|
||||||
this.editingId = item.id
|
this.editingId = item.id
|
||||||
this.formError = null
|
this.formError = null
|
||||||
this.modalType = 'item'
|
this.modalType = 'item'
|
||||||
@@ -2138,7 +2233,8 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
openEditGame() {
|
openEditGame() {
|
||||||
this.activeForm = Object.assign({ deck_id: '' }, this.game)
|
this.activeForm = Object.assign({ deck_id: '', truths: [] }, this.game)
|
||||||
|
this.activeForm.truths = [...(this.game.truths || [])]
|
||||||
this.formError = null
|
this.formError = null
|
||||||
this.modalType = 'editGame'
|
this.modalType = 'editGame'
|
||||||
this.showModal = true
|
this.showModal = true
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ function publicGame(g) {
|
|||||||
start_title: g.start_title || '', start_description: g.start_description || '', start_mood: g.start_mood || '',
|
start_title: g.start_title || '', start_description: g.start_description || '', start_mood: g.start_mood || '',
|
||||||
end_title: g.end_title || '', end_description: g.end_description || '', end_mood: g.end_mood || '',
|
end_title: g.end_title || '', end_description: g.end_description || '', end_mood: g.end_mood || '',
|
||||||
deck_id: g.deck_id || '',
|
deck_id: g.deck_id || '',
|
||||||
|
truths: g.truths || [],
|
||||||
created_at: g.created_at
|
created_at: g.created_at
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,6 +184,7 @@ app.put('/api/games/:slug', requirePassword, (req, res) => {
|
|||||||
game.end_description = end_description || ''
|
game.end_description = end_description || ''
|
||||||
game.end_mood = end_mood || ''
|
game.end_mood = end_mood || ''
|
||||||
game.deck_id = req.body.deck_id || ''
|
game.deck_id = req.body.deck_id || ''
|
||||||
|
game.truths = req.body.truths || []
|
||||||
saveDB(req.db); broadcast(req.params.slug)
|
saveDB(req.db); broadcast(req.params.slug)
|
||||||
res.json(publicGame(game))
|
res.json(publicGame(game))
|
||||||
})
|
})
|
||||||
@@ -209,7 +211,7 @@ app.get('/api/games/:slug/items', (req, res) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.post('/api/games/:slug/items', requirePassword, (req, res) => {
|
app.post('/api/games/:slug/items', requirePassword, (req, res) => {
|
||||||
const { type, title, description, mood, date, end_date, status, discipline, env } = req.body
|
const { type, title, description, mood, date, end_date, status, discipline, env, project_id } = req.body
|
||||||
if (!type || !title || !date) return res.status(400).json({ error: 'type, title, date required' })
|
if (!type || !title || !date) return res.status(400).json({ error: 'type, title, date required' })
|
||||||
if (!['event', 'project', 'person'].includes(type)) {
|
if (!['event', 'project', 'person'].includes(type)) {
|
||||||
return res.status(400).json({ error: 'type must be event, project, or person' })
|
return res.status(400).json({ error: 'type must be event, project, or person' })
|
||||||
@@ -218,7 +220,8 @@ app.post('/api/games/:slug/items', requirePassword, (req, res) => {
|
|||||||
id: newId(), type, title, description: description || '',
|
id: newId(), type, title, description: description || '',
|
||||||
mood: mood || null, date, end_date: end_date || null,
|
mood: mood || null, date, end_date: end_date || null,
|
||||||
status: status || null, discipline: discipline || null,
|
status: status || null, discipline: discipline || null,
|
||||||
env: env || null, env_layout: req.body.env_layout || null, created_at: now()
|
env: env || null, env_layout: req.body.env_layout || null,
|
||||||
|
project_id: project_id || null, created_at: now()
|
||||||
}
|
}
|
||||||
req.game.items = req.game.items || []
|
req.game.items = req.game.items || []
|
||||||
req.game.items.push(item)
|
req.game.items.push(item)
|
||||||
@@ -233,7 +236,7 @@ app.put('/api/games/:slug/items/:id', requirePassword, (req, res) => {
|
|||||||
const { title, description, mood, date, end_date, status, discipline } = req.body
|
const { title, description, mood, date, end_date, status, discipline } = req.body
|
||||||
if (!title || !date) return res.status(400).json({ error: 'title, date required' })
|
if (!title || !date) return res.status(400).json({ error: 'title, date required' })
|
||||||
const { env, env_layout } = req.body
|
const { env, env_layout } = req.body
|
||||||
items[idx] = { ...items[idx], title, description: description || '', mood: mood || null, date, end_date: end_date || null, status: status || null, discipline: discipline || null, env: env || null, env_layout: env_layout || null }
|
items[idx] = { ...items[idx], title, description: description || '', mood: mood || null, date, end_date: end_date || null, status: status || null, discipline: discipline || null, env: env || null, env_layout: env_layout || null, project_id: req.body.project_id || null }
|
||||||
saveDB(req.db); broadcast(req.params.slug)
|
saveDB(req.db); broadcast(req.params.slug)
|
||||||
res.json(items[idx])
|
res.json(items[idx])
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user