Skip to content

Commit 0eb6d4e

Browse files
fix(onboarding): source topic list from DB to stop selections being silently dropped (#599)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d6bbdfe commit 0eb6d4e

5 files changed

Lines changed: 370 additions & 89 deletions

File tree

src/lib/components/OnboardingView/OnboardingView.svelte

Lines changed: 59 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@
88
import { Modal } from '$lib/components/Modal/index.js';
99
import { IsWithinViewport } from '$lib/helpers/index.js';
1010
11+
export interface Topic {
12+
id: string;
13+
title: string;
14+
description: string;
15+
/**
16+
* The associated tag code (e.g. `AI`, `BOB`), used to select the topic
17+
* icon. Null when the collection has no tag.
18+
*/
19+
code: string | null;
20+
}
21+
1122
export interface Props {
1223
/**
1324
* Indicates whether the view is open.
@@ -17,9 +28,37 @@
1728
* A callback invoked when the view is closed.
1829
*/
1930
onclose: () => void;
31+
/**
32+
* The selectable topic collections, sourced from the database.
33+
*/
34+
topics: Topic[];
2035
}
2136
22-
const { isopen, onclose }: Props = $props();
37+
const { isopen, onclose, topics }: Props = $props();
38+
39+
/**
40+
* Tag codes that have a topic icon. Keep in sync with the `enhanced:img`
41+
* mapping in the topic-selection markup below; a topic whose code is not
42+
* here has no icon and is not shown.
43+
*/
44+
const TOPIC_ICON_CODES = new Set([
45+
'AI',
46+
'CAREER',
47+
'EDU_VOICES',
48+
'EMP_ENGAGEMENT',
49+
'NEWS',
50+
'INFRA',
51+
'INNOV',
52+
'BOB',
53+
'PROD',
54+
'STU_DEV',
55+
'STU_WELL',
56+
'WELLBEING',
57+
]);
58+
59+
const displayedTopics = $derived(
60+
topics.filter((topic) => topic.code !== null && TOPIC_ICON_CODES.has(topic.code)),
61+
);
2362
2463
let target = $state<HTMLElement | null>(null);
2564
let isStartPage = $state(true);
@@ -64,7 +103,7 @@
64103
65104
const handleConfirm = async () => {
66105
const payload = {
67-
topics: selectedTopics,
106+
collectionIds: selectedTopics,
68107
frequency: selectedFrequency,
69108
csrfToken: page.data.csrfToken,
70109
};
@@ -127,68 +166,13 @@
127166
await handleClose();
128167
};
129168
130-
const handleTopicChange = (title: string, checked: boolean) => {
169+
const handleTopicChange = (id: string, checked: boolean) => {
131170
if (checked) {
132-
selectedTopics = [...selectedTopics, title];
171+
selectedTopics = [...selectedTopics, id];
133172
} else {
134-
selectedTopics = selectedTopics.filter((topic) => topic !== title);
173+
selectedTopics = selectedTopics.filter((topic) => topic !== id);
135174
}
136175
};
137-
138-
const collectionsList = {
139-
'artificial-intelligence': {
140-
title: 'Artificial Intelligence',
141-
description: 'AI news, use cases and tools to master the changing landscape.',
142-
},
143-
'career-growth': {
144-
title: 'Career Growth',
145-
description: 'Tips to help you set career goals and transform potential into performance.',
146-
},
147-
'educator-voices': {
148-
title: 'Educator Voices',
149-
description: 'Inspiring stories from fellow educators.',
150-
},
151-
'employee-engagement': {
152-
title: 'Employee Engagement',
153-
description:
154-
'Analyze EES results and get practical tips to maintain workplace staff engagement.',
155-
},
156-
'in-the-news': {
157-
title: 'In the News',
158-
description: 'Education news and survey highlights for ground insights.',
159-
},
160-
infrastructure: {
161-
title: 'Infrastructure',
162-
description: 'Learn about device setups, connectivity, and digital tools for efficient work.',
163-
},
164-
innovation: {
165-
title: 'Innovation',
166-
description: 'Strategies to empower new ideas, innovation, and creative thinking.',
167-
},
168-
'learn-with-bob': {
169-
title: 'Learn with Bob',
170-
description: 'SPACES framework for purposeful work and a sustainable pace.',
171-
},
172-
productivity: {
173-
title: 'Productivity',
174-
description:
175-
'Master time management and workflows to minimize distractions and reach goals faster.',
176-
},
177-
'student-development': {
178-
title: 'Student Development',
179-
description:
180-
"Learn ways to develop students' cognitive, affective, physical and aesthetic abilities.",
181-
},
182-
'student-wellbeing': {
183-
title: 'Student Wellbeing',
184-
description: 'Learn ways to support students emotional and physical health.',
185-
},
186-
wellbeing: {
187-
title: 'Wellbeing',
188-
description:
189-
'Identify workplace stress, monitor peer distress, conduct check-ins, and provide support resources.',
190-
},
191-
};
192176
</script>
193177

194178
<!-- Modal -->
@@ -277,7 +261,7 @@
277261

278262
<div class="overflow-y-auto pt-6">
279263
<div class="flex w-full flex-col gap-y-2" role="group" aria-labelledby="topic">
280-
{#each Object.entries(collectionsList) as [key, { title, description }] (key)}
264+
{#each displayedTopics as { id, title, description, code } (id)}
281265
<label
282266
class={[
283267
'group flex cursor-pointer items-center rounded-3xl border border-slate-200 bg-white',
@@ -291,79 +275,79 @@
291275
<input
292276
type="checkbox"
293277
name="topics"
294-
value={title}
278+
value={id}
295279
class="sr-only"
296-
onchange={(e) => handleTopicChange(title, e.currentTarget.checked)}
280+
onchange={(e) => handleTopicChange(id, e.currentTarget.checked)}
297281
/>
298282

299283
<div class="flex h-24 w-20 shrink-0 items-center justify-center">
300-
{#if key === 'artificial-intelligence'}
284+
{#if code === 'AI'}
301285
<enhanced:img
302286
src="$lib/assets/collections/artificial-intelligence.png"
303287
alt={title}
304288
class="max-h-full max-w-full object-contain"
305289
/>
306-
{:else if key === 'career-growth'}
290+
{:else if code === 'CAREER'}
307291
<enhanced:img
308292
src="$lib/assets/collections/career-growth.png"
309293
alt={title}
310294
class="max-h-full max-w-full object-contain"
311295
/>
312-
{:else if key === 'educator-voices'}
296+
{:else if code === 'EDU_VOICES'}
313297
<enhanced:img
314298
src="$lib/assets/collections/educator-voices.png"
315299
alt={title}
316300
class="max-h-full max-w-full object-contain"
317301
/>
318-
{:else if key === 'employee-engagement'}
302+
{:else if code === 'EMP_ENGAGEMENT'}
319303
<enhanced:img
320304
src="$lib/assets/collections/employee-engagement.png"
321305
alt={title}
322306
class="max-h-full max-w-full object-contain"
323307
/>
324-
{:else if key === 'in-the-news'}
308+
{:else if code === 'NEWS'}
325309
<enhanced:img
326310
src="$lib/assets/collections/in-the-news.png"
327311
alt={title}
328312
class="max-h-full max-w-full object-contain"
329313
/>
330-
{:else if key === 'infrastructure'}
314+
{:else if code === 'INFRA'}
331315
<enhanced:img
332316
src="$lib/assets/collections/infrastructure.png"
333317
alt={title}
334318
class="max-h-full max-w-full object-contain"
335319
/>
336-
{:else if key === 'innovation'}
320+
{:else if code === 'INNOV'}
337321
<enhanced:img
338322
src="$lib/assets/collections/innovation.png"
339323
alt={title}
340324
class="max-h-full max-w-full object-contain"
341325
/>
342-
{:else if key === 'learn-with-bob'}
326+
{:else if code === 'BOB'}
343327
<enhanced:img
344328
src="$lib/assets/collections/learn-with-bob.png"
345329
alt={title}
346330
class="max-h-full max-w-full object-contain"
347331
/>
348-
{:else if key === 'productivity'}
332+
{:else if code === 'PROD'}
349333
<enhanced:img
350334
src="$lib/assets/collections/productivity.png"
351335
alt={title}
352336
class="max-h-full max-w-full object-contain"
353337
/>
354-
{:else if key === 'student-development'}
338+
{:else if code === 'STU_DEV'}
355339
<enhanced:img
356340
src="$lib/assets/collections/student-development.png"
357341
alt={title}
358342
class="max-h-full max-w-full object-contain"
359343
/>
360-
{:else if key === 'student-wellbeing'}
344+
{:else if code === 'STU_WELL'}
361345
<enhanced:img
362346
src="$lib/assets/collections/student-wellbeing.png"
363347
alt={title}
364348
class="max-h-full max-w-full object-contain"
365349
/>
366-
{:else if key === 'wellbeing'}
350+
{:else if code === 'WELLBEING'}
367351
<enhanced:img
368352
src="$lib/assets/collections/wellbeing.png"
369353
alt={title}

src/routes/(main)/(protected)/+layout.server.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { error, redirect } from '@sveltejs/kit';
22

3-
import { db, type UserFindUniqueArgs, type UserGetPayload } from '$lib/server/db';
3+
import {
4+
type CollectionFindManyArgs,
5+
type CollectionGetPayload,
6+
db,
7+
type UserFindUniqueArgs,
8+
type UserGetPayload,
9+
} from '$lib/server/db';
410

511
import type { LayoutServerLoad } from './$types';
612

@@ -35,9 +41,45 @@ export const load: LayoutServerLoad = async (event) => {
3541

3642
const onboarded = userData?.userProfile;
3743

44+
const topicArgs = {
45+
select: {
46+
id: true,
47+
title: true,
48+
description: true,
49+
tag: {
50+
select: {
51+
code: true,
52+
},
53+
},
54+
},
55+
where: {
56+
isTopic: true,
57+
},
58+
orderBy: {
59+
title: 'asc',
60+
},
61+
} satisfies CollectionFindManyArgs;
62+
63+
let topics: CollectionGetPayload<typeof topicArgs>[];
64+
try {
65+
topics = await db.collection.findMany(topicArgs);
66+
} catch (err) {
67+
logger.error({ err }, 'Failed to retrieve topic collections');
68+
throw error(500);
69+
}
70+
3871
return {
3972
username: user.name,
4073
csrfToken: event.locals.session.csrfToken(),
4174
onboarded,
75+
// Keyed distinctly from the home page's own `topics` (different shape):
76+
// page data overrides layout data on key collision, so a shared `topics`
77+
// key would let the home page shadow these and break the onboarding picker.
78+
onboardingTopics: topics.map((topic) => ({
79+
id: topic.id,
80+
title: topic.title,
81+
description: topic.description,
82+
code: topic.tag?.code ?? null,
83+
})),
4284
};
4385
};

src/routes/(main)/(protected)/+layout.svelte

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,4 +329,8 @@
329329

330330
<ChatView isopen={isChatViewOpen} onclose={handleChatViewClose} />
331331

332-
<OnboardingView isopen={isOnboardingViewOpen} onclose={handleOnboardingViewClose} />
332+
<OnboardingView
333+
isopen={isOnboardingViewOpen}
334+
onclose={handleOnboardingViewClose}
335+
topics={page.data.onboardingTopics ?? []}
336+
/>

src/routes/(main)/api/onboarding/+server.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { json } from '@sveltejs/kit';
22

3-
import { db, type UserProfileCreateArgs } from '$lib/server/db';
3+
import {
4+
type CollectionFindManyArgs,
5+
type CollectionGetPayload,
6+
db,
7+
type UserProfileCreateArgs,
8+
} from '$lib/server/db';
49

510
import type { RequestHandler } from './$types';
611

@@ -25,9 +30,10 @@ export const POST: RequestHandler = async (event) => {
2530
if (
2631
!params ||
2732
typeof params !== 'object' ||
28-
!('topics' in params) ||
29-
!Array.isArray(params['topics']) ||
30-
params['topics'].length < 3 ||
33+
!('collectionIds' in params) ||
34+
!Array.isArray(params['collectionIds']) ||
35+
params['collectionIds'].length < 3 ||
36+
!params['collectionIds'].every((id: unknown) => typeof id === 'string') ||
3137
!('frequency' in params) ||
3238
typeof params['frequency'] !== 'string' ||
3339
!('csrfToken' in params) ||
@@ -40,24 +46,34 @@ export const POST: RequestHandler = async (event) => {
4046
return json(null, { status: 400 });
4147
}
4248

43-
const { topics, frequency } = params;
49+
const { collectionIds, frequency } = params;
50+
51+
const uniqueCollectionIds = [...new Set<string>(collectionIds)];
4452

4553
try {
46-
const collections = await db.collection.findMany({
54+
const collectionArgs = {
4755
select: {
4856
id: true,
4957
},
5058
where: {
51-
title: {
52-
in: topics,
59+
id: {
60+
in: uniqueCollectionIds,
5361
},
5462
isTopic: true,
5563
},
56-
});
64+
} satisfies CollectionFindManyArgs;
65+
66+
const collections: CollectionGetPayload<typeof collectionArgs>[] =
67+
await db.collection.findMany(collectionArgs);
5768

58-
if (collections.length === 0) {
59-
logger.warn({ topics }, 'No valid collections found');
60-
return json(null, { status: 400 });
69+
if (collections.length !== uniqueCollectionIds.length) {
70+
const resolvedIds = new Set(collections.map((collection) => collection.id));
71+
const unresolvedIds = uniqueCollectionIds.filter((id) => !resolvedIds.has(id));
72+
logger.warn(
73+
{ userId: user.id, submittedIds: uniqueCollectionIds, unresolvedIds },
74+
'One or more collectionIds did not resolve to a topic collection',
75+
);
76+
return json(null, { status: 422 });
6177
}
6278

6379
const userProfileArgs = {

0 commit comments

Comments
 (0)