Merge agenda author change #4
131
docs/superpowers/plans/2026-06-04-agenda-author-display.md
Normal file
131
docs/superpowers/plans/2026-06-04-agenda-author-display.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Agenda Author Display Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Show the presenter's linked name beneath each item in the published agenda schedule on the event detail page.
|
||||
|
||||
**Architecture:** The backend's `GET /agenda/schedule` now returns `ServiceAgendaAgendaListItem[]` (includes `user_profile`). We fix the stale `DataAgendaDoc` type references and add a conditional author link in the `AgendaSchedule` component.
|
||||
|
||||
**Tech Stack:** SvelteKit, Svelte 5 runes, TypeScript, DaisyUI 5, `$app/paths` resolve
|
||||
|
||||
---
|
||||
|
||||
## Files
|
||||
|
||||
- Modify: `src/routes/(app)/events/[eventId]/+page.server.ts` — fix import + type annotation
|
||||
- Modify: `src/lib/components/AgendaSchedule.svelte` — fix import/type, add author link
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Fix type references in `+page.server.ts`
|
||||
|
||||
`DataAgendaDoc` was removed from the SDK. The load function still references it in the import and the `agendaSchedule` variable type.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/routes/(app)/events/[eventId]/+page.server.ts`
|
||||
|
||||
- [ ] **Step 1: Update the import**
|
||||
|
||||
On line 14, change:
|
||||
```ts
|
||||
import type { DataAgenda, DataAgendaDoc } from '$lib/api';
|
||||
```
|
||||
to:
|
||||
```ts
|
||||
import type { DataAgenda, ServiceAgendaAgendaListItem } from '$lib/api';
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update the variable type annotation**
|
||||
|
||||
On line 73, change:
|
||||
```ts
|
||||
let agendaSchedule: Array<DataAgendaDoc & { descriptionHtml: string | null }> = [];
|
||||
```
|
||||
to:
|
||||
```ts
|
||||
let agendaSchedule: Array<ServiceAgendaAgendaListItem & { descriptionHtml: string | null }> = [];
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify types compile**
|
||||
|
||||
```bash
|
||||
pnpm check
|
||||
```
|
||||
Expected: no errors mentioning `DataAgendaDoc`.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Update `AgendaSchedule.svelte` — fix type and add author link
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/lib/components/AgendaSchedule.svelte`
|
||||
|
||||
- [ ] **Step 1: Fix the import and type alias**
|
||||
|
||||
Replace the current `<script>` block top (lines 1–7):
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import dayjs from '$lib/dayjs';
|
||||
import type { DataAgendaDoc } from '$lib/api';
|
||||
|
||||
type Item = DataAgendaDoc & { descriptionHtml: string | null };
|
||||
```
|
||||
with:
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import dayjs from '$lib/dayjs';
|
||||
import { resolve } from '$app/paths';
|
||||
import type { ServiceAgendaAgendaListItem } from '$lib/api';
|
||||
|
||||
type Item = ServiceAgendaAgendaListItem & { descriptionHtml: string | null };
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the author link beneath the item name**
|
||||
|
||||
In the content column (currently around line 45), after:
|
||||
```svelte
|
||||
<div class="font-sans text-sm font-medium">{item.name}</div>
|
||||
```
|
||||
add:
|
||||
```svelte
|
||||
{#if item.user_profile}
|
||||
<p class="mt-0.5 text-xs text-base-content/50">
|
||||
<a href={resolve(`/profile/${item.user_profile.user_id}` as '/')}
|
||||
class="link link-hover">
|
||||
{item.user_profile.nickname ?? item.user_profile.username}
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
```
|
||||
|
||||
`resolve` builds a base-path-aware URL (`/app/profile/…`). The block is conditional — items where the backend returns no `user_profile` render cleanly. `nickname ?? username` matches the fallback pattern used on the admin agenda page.
|
||||
|
||||
- [ ] **Step 3: Run full type check**
|
||||
|
||||
```bash
|
||||
pnpm check
|
||||
```
|
||||
Expected: exits 0, no errors.
|
||||
|
||||
- [ ] **Step 4: Lint and auto-format**
|
||||
|
||||
```bash
|
||||
pnpm lint:fix
|
||||
```
|
||||
Expected: exits 0 (or only prints files it reformatted — no unfixable lint errors).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/lib/components/AgendaSchedule.svelte src/routes/\(app\)/events/\[eventId\]/+page.server.ts
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat: display agenda author on event detail page
|
||||
|
||||
Show the presenter's linked name beneath each item in the
|
||||
published schedule. Fixes stale DataAgendaDoc refs now that
|
||||
getAgendaSchedule returns ServiceAgendaAgendaListItem[].
|
||||
|
||||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
# Agenda Author Display — Design Spec
|
||||
|
||||
**Date:** 2026-06-04
|
||||
**Status:** Approved
|
||||
|
||||
## Context
|
||||
|
||||
The public event detail page shows a published agenda schedule (`AgendaSchedule.svelte`) but currently does not display who is presenting each item. The backend's `GET /agenda/schedule` endpoint was updated to return `ServiceAgendaAgendaListItem[]` — the same type as the admin endpoint — which includes `user_profile?: { user_id, username, nickname }`. The old `DataAgendaDoc` type was removed from the generated SDK entirely. This spec covers wiring up the now-available author data so attendees can see the presenter name on the schedule.
|
||||
|
||||
## Scope
|
||||
|
||||
Show a linked presenter name beneath each agenda item title in the published schedule only. No changes to `AgendaMyList` or any admin views.
|
||||
|
||||
## Design
|
||||
|
||||
### Type fix (required regardless of feature)
|
||||
|
||||
`DataAgendaDoc` no longer exists. Two files reference it and must be updated to use `ServiceAgendaAgendaListItem`:
|
||||
|
||||
- `src/routes/(app)/events/[eventId]/+page.server.ts` — import and `agendaSchedule` variable type annotation
|
||||
- `src/lib/components/AgendaSchedule.svelte` — import and local `Item` type alias
|
||||
|
||||
### Author display
|
||||
|
||||
In `AgendaSchedule.svelte`, directly below the `name` div inside each item, add:
|
||||
|
||||
```svelte
|
||||
{#if item.user_profile}
|
||||
<p class="mt-0.5 text-xs text-base-content/50">
|
||||
<a href={resolve(`/profile/${item.user_profile.user_id}` as '/')}
|
||||
class="link link-hover">
|
||||
{item.user_profile.nickname ?? item.user_profile.username}
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
```
|
||||
|
||||
- `resolve` from `$app/paths` for correct base-path-aware links (same pattern used throughout the app)
|
||||
- Conditional on `user_profile` — items without author data render cleanly with no gap
|
||||
- `nickname ?? username` fallback matches the existing admin page pattern
|
||||
- Styling: `text-xs text-base-content/50` — subdued, consistent with the timestamp style in the same component; no label prefix
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No avatar display (not available in `user_profile`)
|
||||
- No "提交者:" label — the position (under the title) makes the relationship implicit for a public schedule view
|
||||
- No changes to the admin agenda page (already shows author correctly)
|
||||
13
openapi-ts-error-1780580740622.log
Normal file
13
openapi-ts-error-1780580740622.log
Normal file
@@ -0,0 +1,13 @@
|
||||
[2026-06-04T13:45:40.622Z] Error: Request failed with status 500: fetch failed
|
||||
Stack:
|
||||
Error: Request failed with status 500: fetch failed
|
||||
at getSpecData (file:///var/home/nvirellia/Projects/cms-client/node_modules/.pnpm/@hey-api+openapi-ts@0.96.0_typescript@6.0.2/node_modules/@hey-api/openapi-ts/dist/src-D2PCex5z.mjs:73:10)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Promise.all (index 0)
|
||||
at async createClient$1 (file:///var/home/nvirellia/Projects/cms-client/node_modules/.pnpm/@hey-api+openapi-ts@0.96.0_typescript@6.0.2/node_modules/@hey-api/openapi-ts/dist/src-D2PCex5z.mjs:80:20)
|
||||
at async file:///var/home/nvirellia/Projects/cms-client/node_modules/.pnpm/@hey-api+openapi-ts@0.96.0_typescript@6.0.2/node_modules/@hey-api/openapi-ts/dist/src-D2PCex5z.mjs:203:12
|
||||
at async Promise.all (index 0)
|
||||
at async createClient (file:///var/home/nvirellia/Projects/cms-client/node_modules/.pnpm/@hey-api+openapi-ts@0.96.0_typescript@6.0.2/node_modules/@hey-api/openapi-ts/dist/src-D2PCex5z.mjs:201:21)
|
||||
at async Command.<anonymous> (file:///var/home/nvirellia/Projects/cms-client/node_modules/.pnpm/@hey-api+openapi-ts@0.96.0_typescript@6.0.2/node_modules/@hey-api/openapi-ts/dist/run.mjs:35:8)
|
||||
at async Command.parseAsync (/var/home/nvirellia/Projects/cms-client/node_modules/.pnpm/commander@14.0.3/node_modules/commander/lib/command.js:1122:5)
|
||||
at async runCli (file:///var/home/nvirellia/Projects/cms-client/node_modules/.pnpm/@hey-api+openapi-ts@0.96.0_typescript@6.0.2/node_modules/@hey-api/openapi-ts/dist/run.mjs:39:3)
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineConfig } from '@hey-api/openapi-ts';
|
||||
|
||||
export default defineConfig({
|
||||
input: 'http://10.0.0.10:8000/swagger/doc.json',
|
||||
input: 'http://100.79.132.76:8000/swagger/doc.json',
|
||||
output: 'src/lib/api',
|
||||
plugins: [
|
||||
'@hey-api/client-fetch',
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
$effect(() => {
|
||||
if (open && mode === 'edit' && item) {
|
||||
$form.name = item.name ?? '';
|
||||
$form.title = item.name ?? '';
|
||||
$form.description = item.description ?? '';
|
||||
} else if (!open) {
|
||||
reset();
|
||||
@@ -75,18 +75,18 @@
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="agenda-name" class="text-sm font-medium">名称</label>
|
||||
<label class="input w-full {$errors.name ? 'input-error' : ''}">
|
||||
<label class="input w-full {$errors.title ? 'input-error' : ''}">
|
||||
<input
|
||||
id="agenda-name"
|
||||
name="name"
|
||||
name="title"
|
||||
type="text"
|
||||
placeholder="议程标题"
|
||||
maxlength="255"
|
||||
bind:value={$form.name}
|
||||
bind:value={$form.title}
|
||||
/>
|
||||
</label>
|
||||
{#if $errors.name}
|
||||
<p class="text-xs text-error">{$errors.name}</p>
|
||||
{#if $errors.title}
|
||||
<p class="text-xs text-error">{$errors.title}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import dayjs from '$lib/dayjs';
|
||||
import type { DataAgendaDoc } from '$lib/api';
|
||||
import { resolve } from '$app/paths';
|
||||
import type { ServiceAgendaAgendaListItem } from '$lib/api';
|
||||
|
||||
type Item = DataAgendaDoc & { descriptionHtml: string | null };
|
||||
type Item = ServiceAgendaAgendaListItem & { descriptionHtml: string | null };
|
||||
|
||||
const { items }: { items: Item[] } = $props();
|
||||
|
||||
@@ -43,6 +44,16 @@
|
||||
<!-- Content column -->
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="font-sans text-sm font-medium">{item.name}</div>
|
||||
{#if item.user_profile?.user_id}
|
||||
<p class="mt-0.5 text-xs text-base-content/50">
|
||||
<a
|
||||
href={resolve(`/profile/${item.user_profile.user_id}` as '/')}
|
||||
class="link link-hover"
|
||||
>
|
||||
{item.user_profile.nickname ?? item.user_profile.username}
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
{#if item.descriptionHtml}
|
||||
<!-- eslint-disable svelte/no-at-html-tags -->
|
||||
<div class="prose prose-sm mt-1 max-w-none dark:prose-invert">
|
||||
|
||||
@@ -7,28 +7,28 @@ import {
|
||||
} from './agenda';
|
||||
|
||||
describe('agendaItemSchema', () => {
|
||||
it('accepts name and description', () => {
|
||||
expect(agendaItemSchema.safeParse({ name: '开幕式', description: '活动开场' }).success).toBe(
|
||||
it('accepts title and description', () => {
|
||||
expect(agendaItemSchema.safeParse({ title: '开幕式', description: '活动开场' }).success).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects empty name', () => {
|
||||
expect(agendaItemSchema.safeParse({ name: '', description: '描述' }).success).toBe(false);
|
||||
it('rejects empty title', () => {
|
||||
expect(agendaItemSchema.safeParse({ title: '', description: '描述' }).success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects name over 255 chars', () => {
|
||||
expect(agendaItemSchema.safeParse({ name: 'a'.repeat(256), description: '描述' }).success).toBe(
|
||||
it('rejects title over 255 chars', () => {
|
||||
expect(agendaItemSchema.safeParse({ title: 'a'.repeat(256), description: '描述' }).success).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects missing description', () => {
|
||||
expect(agendaItemSchema.safeParse({ name: '开幕式' }).success).toBe(false);
|
||||
expect(agendaItemSchema.safeParse({ title: '开幕式' }).success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects empty description', () => {
|
||||
expect(agendaItemSchema.safeParse({ name: '开幕式', description: '' }).success).toBe(false);
|
||||
expect(agendaItemSchema.safeParse({ title: '开幕式', description: '' }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Submit/Update form: name + description
|
||||
// Submit/Update form: title + description
|
||||
// description is required — backend stores it as base64 markdown for both
|
||||
// postAgendaSubmit and patchAgendaUpdate.
|
||||
export const agendaItemSchema = z.object({
|
||||
name: z.string().min(1, '请填写名称').max(255, '名称最多 255 字'),
|
||||
title: z.string().min(1, '请填写名称').max(255, '名称最多 255 字'),
|
||||
description: z.string().min(1, '请填写描述')
|
||||
});
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ export const actions: Actions = {
|
||||
client: api,
|
||||
body: {
|
||||
agenda_id,
|
||||
name: fd.name,
|
||||
name: fd.title,
|
||||
// Backend stores description as base64; encode before sending.
|
||||
description: fd.description ? Buffer.from(fd.description).toString('base64') : undefined
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import dayjs from '$lib/dayjs';
|
||||
import type { PageData } from './$types';
|
||||
@@ -97,14 +96,14 @@
|
||||
<div class="join">
|
||||
{#if data.page > 1}
|
||||
<a
|
||||
href={resolve(`${page.url.pathname}?page=${data.page - 1}` as '/')}
|
||||
href={`${page.url.pathname}?page=${data.page - 1}`}
|
||||
class="btn join-item btn-sm"
|
||||
aria-label="上一页">«</a
|
||||
>
|
||||
{/if}
|
||||
{#if hasNextPage}
|
||||
<a
|
||||
href={resolve(`${page.url.pathname}?page=${data.page + 1}` as '/')}
|
||||
href={`${page.url.pathname}?page=${data.page + 1}`}
|
||||
class="btn join-item btn-sm"
|
||||
aria-label="下一页">»</a
|
||||
>
|
||||
|
||||
@@ -184,7 +184,7 @@ export const actions: Actions = {
|
||||
client: api,
|
||||
body: {
|
||||
event_id: eventId,
|
||||
name: fd.name,
|
||||
name: fd.title,
|
||||
description: Buffer.from(fullDescription).toString('base64')
|
||||
}
|
||||
})
|
||||
@@ -209,7 +209,7 @@ export const actions: Actions = {
|
||||
client: api,
|
||||
body: {
|
||||
agenda_id,
|
||||
name: fd.name,
|
||||
name: fd.title,
|
||||
description: Buffer.from(fd.description).toString('base64')
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user