Merge agenda author change #4

Merged
nvirellia merged 5 commits from develop into main 2026-06-04 14:12:24 +00:00
11 changed files with 226 additions and 25 deletions

View 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 17):
```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
)"
```

View File

@@ -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)

View 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)

View File

@@ -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',

View File

@@ -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>

View File

@@ -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">

View File

@@ -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);
});
});

View File

@@ -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, '请填写描述')
});

View File

@@ -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
}

View File

@@ -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
>

View File

@@ -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')
}
})