Conversation
c4dfc4b to
a0be062
Compare
| 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") | ||
| } | ||
|
|
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
Do we need to wrap the Modal in Portal component here? Inside the Modal component it is already wrapped in a Portal.
There was a problem hiding this comment.
will remove this part
| <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> |
There was a problem hiding this comment.
I will do all content-related updates together
| @@ -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); | |||
There was a problem hiding this comment.
Why the await for resolve is removed?
There was a problem hiding this comment.
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
| 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 }); | ||
| }; |
There was a problem hiding this comment.
Any tradeoffs why we use JSON API instead of form actions here?
There was a problem hiding this comment.
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

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
Currently pending copy from PM to add to the topics description, and UX for latest frame on the Get Started view