Skip to content

feat: onboarding flow#484

Draft
nicholasjjlim wants to merge 16 commits intomainfrom
feat/onboarding-flow
Draft

feat: onboarding flow#484
nicholasjjlim wants to merge 16 commits intomainfrom
feat/onboarding-flow

Conversation

@nicholasjjlim
Copy link
Collaborator

@nicholasjjlim nicholasjjlim commented Jan 26, 2026

Closes GLOW-89

🚀 Summary

This PR adds a user onboarding flow to find out topics that users are interested to learn about, as well as allowing user to subscribe to latest updates to receive up-to-date modules that are newly uploaded to Glow.

✏️ Changes

  • added onboarding flow modal
  • updated DB schema to fit the new onboarding flow

Currently pending copy from PM to add to the topics description, and UX for latest frame on the Get Started view

@nicholasjjlim nicholasjjlim self-assigned this Jan 26, 2026
Comment on lines +30 to +43
model UserProfile {
userId String @id @map("user_id") @db.Uuid
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(3)
learningFrequency LearningFrequency? @map("learning_frequency")
isSubscribed Boolean @default(false) @map("is_subscribed")

// Relations.
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
interests UserInterest[]

@@map("user_profiles")
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First time encountering using the PK of the parent table as the PK of the dependent table for one-to-one relationship, I did some research on the web and it's a good strategy to implement strict one-to-one relationships with the storage efficiency and performance benefits due to taking advantage of PK default unique constraint instead of additional FK and unique index. 🚀

};
</script>

<Portal>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to wrap the Modal in Portal component here? Inside the Modal component it is already wrapped in a Portal.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will remove this part

Comment on lines 227 to 233
<span>Welcome to Glow!</span>
</div>

<div class="flex flex-col text-center">
<span>You are already ahead in personal</span>
<span>growth as MOE teacher / staff.</span>
<span>Let's get started</span>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we gonna use this new message?

Image

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will do all content-related updates together

Comment on lines 18 to 47
@@ -44,7 +44,7 @@ const routeProtectionHandle: Handle = async ({ event, resolve }) => {
event.url.pathname === '/terms' ||
event.url.pathname === '/privacy'
) {
return await resolve(event);
return resolve(event);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the await for resolve is removed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await is redundant in this case since resolve itself returns a promise, so await is necessary here.

It's only needed when we need to do error handling, post-processing of response or sequential operations. Unless you feel we should do some error handling here

Comment on lines +7 to +81
export const POST: RequestHandler = async (event) => {
const logger = event.locals.logger.child({
handler: 'api_update_onboarding',
});

const { user } = event.locals.session;
if (!user) {
logger.warn('User not authenticated');
return json(null, { status: 401 });
}

if (event.request.headers.get('content-type')?.split(';')[0] !== 'application/json') {
return json(null, { status: 415 });
}

let params;
try {
params = await event.request.json();
if (
!params ||
typeof params !== 'object' ||
!('topics' in params) ||
!Array.isArray(params['topics']) ||
params['topics'].length < 3 ||
!('frequency' in params) ||
typeof params['frequency'] !== 'string' ||
!('csrfToken' in params) ||
typeof params['csrfToken'] !== 'string'
) {
return json(null, { status: 422 });
}
} catch (err) {
logger.error({ err, userId: user.id }, 'Failed to parse request body');
return json(null, { status: 400 });
}

const { topics, frequency } = params;

try {
const collections = await db.collection.findMany({
where: {
type: {
in: topics,
},
},
select: {
id: true,
},
});

if (collections.length === 0) {
logger.warn({ topics }, 'No valid collections found');
return json(null, { status: 400 });
}

const userProfileArgs = {
data: {
userId: user.id,
learningFrequency: frequency,
interests: {
create: collections.map((collection) => ({
collectionId: collection.id,
})),
},
},
} satisfies UserProfileCreateArgs;

await db.userProfile.create(userProfileArgs);
} catch (err) {
logger.error({ err }, 'Failed to complete onboarding');
return json(null, { status: 500 });
}

return json(null, { status: 200 });
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any tradeoffs why we use JSON API instead of form actions here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Form actions are better if page routing is used for onboarding. But in this case since onboarding exist as a modal component and used on a protected layout setting it's better to use API routing which has a much cleaner approach

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants