Add subject options and redesign TV dashboard layout

Subject options:
- New subject_options table (auto-created on startup)
- SubjectOut now includes options list; all eager-loading chains updated
- Admin: Options panel per subject with add, inline edit, and delete
- WS broadcast and dashboard API include options in block subject data

TV dashboard:
- Three equal columns: Timer | Activities | Schedule
- Activities column shows current subject's options in large readable text
- Activities area has subject-colored border and tinted background
- Subject name and label displayed correctly using embedded subject data

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 11:18:55 -08:00
parent c12f07daa3
commit c9441a9c9a
11 changed files with 375 additions and 51 deletions

View File

@@ -25,12 +25,9 @@
<!-- Active session -->
<div v-else class="tv-main">
<!-- Current block (big display) -->
<div class="tv-current" v-if="scheduleStore.currentBlock">
<div
class="tv-subject-badge"
:style="{ background: currentSubjectColor }"
>
<!-- Left: timer -->
<div class="tv-timer-col" v-if="scheduleStore.currentBlock">
<div class="tv-subject-badge" :style="{ background: currentSubjectColor }">
{{ currentSubjectIcon }} {{ currentSubjectName }}
</div>
<TimerDisplay
@@ -45,8 +42,26 @@
</div>
</div>
<!-- Center: subject options -->
<div class="tv-options-col" :style="{ background: currentSubjectColor + '22', borderColor: currentSubjectColor }">
<div class="tv-options-title">Activities</div>
<div
v-if="currentSubjectOptions.length"
class="tv-options-list"
>
<div
v-for="opt in currentSubjectOptions"
:key="opt.id"
class="tv-option-item"
>
{{ opt.text }}
</div>
</div>
<div v-else class="tv-options-empty">No activities listed for this subject.</div>
</div>
<!-- Right: schedule list -->
<div class="tv-sidebar">
<!-- Schedule list -->
<div class="tv-schedule-list">
<ScheduleBlock
v-for="block in scheduleStore.blocks"
@@ -123,6 +138,9 @@ const currentSubjectIcon = computed(() => scheduleStore.currentBlock?.subject?.i
const currentSubjectName = computed(() =>
scheduleStore.currentBlock?.label || scheduleStore.currentBlock?.subject?.name || 'Current Block'
)
const currentSubjectOptions = computed(() =>
scheduleStore.currentBlock?.subject?.options || []
)
// WebSocket
const { connected: wsConnected } = useWebSocket(childId, (msg) => {
@@ -188,11 +206,12 @@ onMounted(async () => {
.tv-main {
flex: 1;
display: grid;
grid-template-columns: 1fr 380px;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
min-height: 0;
}
.tv-current {
.tv-timer-col {
display: flex;
flex-direction: column;
gap: 1.5rem;
@@ -201,18 +220,56 @@ onMounted(async () => {
}
.tv-subject-badge {
font-size: 1.75rem;
font-size: 1.4rem;
font-weight: 600;
padding: 0.75rem 2rem;
padding: 0.6rem 1.5rem;
border-radius: 999px;
color: #fff;
text-align: center;
}
.tv-block-notes {
font-size: 1.25rem;
font-size: 1rem;
color: #94a3b8;
text-align: center;
max-width: 600px;
}
.tv-options-col {
display: flex;
flex-direction: column;
gap: 1rem;
justify-content: center;
border: 2px solid;
border-radius: 1rem;
padding: 1.5rem 2rem;
}
.tv-options-title {
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: #475569;
}
.tv-options-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.tv-option-item {
font-size: 1.6rem;
font-weight: 500;
color: #e2e8f0;
padding: 0.6rem 0;
border-bottom: 1px solid #1e293b;
line-height: 1.3;
}
.tv-options-empty {
font-size: 1.1rem;
color: #334155;
font-style: italic;
}
.tv-day-progress {