From f9b19c464317e9f0b2d61e1706f65b0ba96595b4 Mon Sep 17 00:00:00 2001 From: Logan Ford <110533855+Logannford@users.noreply.github.com> Date: Sun, 27 Apr 2025 11:23:37 +0100 Subject: [PATCH 01/14] fix: email sending on onboarding & start of new flow --- src/actions/user/account/create-coupon.ts | 3 - .../onboarding-first-question-selection.tsx | 75 ++++--------------- src/components/shared/scrolling-animation.tsx | 24 ++++++ src/hooks/use-onboarding-steps.ts | 21 +++++- 4 files changed, 58 insertions(+), 65 deletions(-) create mode 100644 src/components/shared/scrolling-animation.tsx diff --git a/src/actions/user/account/create-coupon.ts b/src/actions/user/account/create-coupon.ts index 1277fe08d..cc530c425 100644 --- a/src/actions/user/account/create-coupon.ts +++ b/src/actions/user/account/create-coupon.ts @@ -7,9 +7,6 @@ import { getUser } from '../authed/get-user'; * Upon user signup, we create a 60% off their first 3 three * months of TechBlitz premium. * Eligible for 72 hours after signup. - * - * We are only hitting this action from within other actions, - * so we can pass the userUid as a parameter. */ export const createCouponOnSignup = async () => { const user = await getUser(); diff --git a/src/components/app/onboarding/onboarding-first-question-selection.tsx b/src/components/app/onboarding/onboarding-first-question-selection.tsx index 59ff25f58..a4df40956 100644 --- a/src/components/app/onboarding/onboarding-first-question-selection.tsx +++ b/src/components/app/onboarding/onboarding-first-question-selection.tsx @@ -1,77 +1,32 @@ import { CardHeader } from '@/components/ui/card'; -import BookBookmark from '@/components/ui/icons/book-bookmark'; -import Star3 from '@/components/ui/icons/star'; import { useOnboardingContext } from '@/contexts/onboarding-context'; import { cn } from '@/lib/utils'; import { motion } from 'framer-motion'; export default function OnboardingFirstQuestionSelection() { - const { itemVariants, firstQuestionSelection, setFirstQuestionSelection } = - useOnboardingContext(); + const { itemVariants } = useOnboardingContext(); return ( -
+
- Where would you like to begin? + Let's begin! - { - setFirstQuestionSelection('startFromScratch'); - }} - > -
- - Recommended - -
-
- -
-

Start from scratch

-

- We'll start you off with a basic tutorial to get you started. -

-
-
-
- setFirstQuestionSelection('personalizeLearning')} - > -
- -
-

Personalize your learning

-

- Select topics that interest you and get a set of recommended questions to get you - started. -

-
-
-
+ + + We're so excited to have you here! Your future in tech is bright, and we're here to help + you get there. + + + {/** 'infinite' scrolling animation of the study path buttons. */} + {/* Scrolling content */} +
); diff --git a/src/components/shared/scrolling-animation.tsx b/src/components/shared/scrolling-animation.tsx new file mode 100644 index 000000000..61ea28acf --- /dev/null +++ b/src/components/shared/scrolling-animation.tsx @@ -0,0 +1,24 @@ +import { cn } from '@/lib/utils'; + +export default function ScrollingAnimation({ + children, + className, + count, +}: { + children: React.ReactNode; + className?: string; + count: number; +}) { + return ( + <> +
+
+ {children} +
+
+ + ); +} diff --git a/src/hooks/use-onboarding-steps.ts b/src/hooks/use-onboarding-steps.ts index 2b76ead67..53a550bc4 100644 --- a/src/hooks/use-onboarding-steps.ts +++ b/src/hooks/use-onboarding-steps.ts @@ -6,6 +6,7 @@ import { useOnboardingContext } from '@/contexts/onboarding-context'; import { updateUser } from '@/actions/user/authed/update-user'; import { createCouponOnSignup } from '@/actions/user/account/create-coupon'; import { sendWelcomeEmail } from '@/actions/misc/send-welcome-email'; +import { useLocalStorage } from './use-local-storage'; export const STEPS = { USER_DETAILS: 'USER_DETAILS', // get the users info @@ -144,8 +145,24 @@ export function useOnboardingSteps() { // if this is false, we need to create a coupon and send the welcome email if (!user.hasCreatedCustomSignupCoupon) { const coupon = await createCouponOnSignup(); - // send the welcome email - await sendWelcomeEmail(serverUser, coupon?.name ?? ''); + + // Check if welcome email has already been sent using localStorage + // Use a generic key if serverUser doesn't have email + const storageKey = serverUser?.email + ? `welcome-email-sent_${serverUser.email}` + : 'welcome-email-sent'; + const { value: welcomeEmailSent, setValue: setWelcomeEmailSent } = useLocalStorage({ + defaultValue: false, + key: storageKey, + }); + + // Only send welcome email if it hasn't been sent before + if (!welcomeEmailSent) { + // send the welcome email + await sendWelcomeEmail(serverUser, coupon?.name ?? ''); + // Mark that welcome email has been sent for this user + setWelcomeEmailSent(true); + } } setCurrentStepState(stepConfig[STEPS.USER_DETAILS].next as StepKey); From a6dcaa9fdc39f05a1f5dc566136ab2883727c28b Mon Sep 17 00:00:00 2001 From: Logan Ford <110533855+Logannford@users.noreply.github.com> Date: Sun, 27 Apr 2025 11:29:24 +0100 Subject: [PATCH 02/14] feat: allows admin users to skip initial question on onboarding via a constant --- src/hooks/use-onboarding-steps.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/hooks/use-onboarding-steps.ts b/src/hooks/use-onboarding-steps.ts index 53a550bc4..a360996b0 100644 --- a/src/hooks/use-onboarding-steps.ts +++ b/src/hooks/use-onboarding-steps.ts @@ -8,6 +8,9 @@ import { createCouponOnSignup } from '@/actions/user/account/create-coupon'; import { sendWelcomeEmail } from '@/actions/misc/send-welcome-email'; import { useLocalStorage } from './use-local-storage'; +// Toggle for admin to skip initial questions +const ADMIN_SKIP_INITIAL_QUESTIONS = false; + export const STEPS = { USER_DETAILS: 'USER_DETAILS', // get the users info INITIAL_QUESTIONS: 'INITIAL_QUESTIONS', // give the user 3 very simple multiple choice questions to gauge skill level and give them quick wins! @@ -165,7 +168,15 @@ export function useOnboardingSteps() { } } - setCurrentStepState(stepConfig[STEPS.USER_DETAILS].next as StepKey); + // If user is an admin and the feature toggle is enabled, skip the INITIAL_QUESTIONS step + if (ADMIN_SKIP_INITIAL_QUESTIONS && serverUser?.userLevel === 'ADMIN') { + console.log('Admin user detected - skipping initial questions'); + // Skip to the TIME_COMMITMENT step (which is after INITIAL_QUESTIONS) + setCurrentStepState(STEPS.TIME_COMMITMENT); + } else { + // Regular user follows normal flow + setCurrentStepState(stepConfig[STEPS.USER_DETAILS].next as StepKey); + } } else if (currentStep === STEPS.INITIAL_QUESTIONS) { // Fix the userXp field by ensuring it's a numeric value // Make sure to use the existing user object and only update the userXp field From 3361f56f9265b9fbc57d49b7b0686a7e013299dc Mon Sep 17 00:00:00 2001 From: Logan Ford <110533855+Logannford@users.noreply.github.com> Date: Sun, 27 Apr 2025 11:45:42 +0100 Subject: [PATCH 03/14] fix: cleaning up code in onboarding flow --- .../app/onboarding/onboarding-footer.tsx | 50 +++++++++---------- .../app/onboarding/onboarding-form.tsx | 14 ++++-- src/hooks/use-onboarding-steps.ts | 25 ++++++++-- 3 files changed, 55 insertions(+), 34 deletions(-) diff --git a/src/components/app/onboarding/onboarding-footer.tsx b/src/components/app/onboarding/onboarding-footer.tsx index 31f3da481..d16501185 100644 --- a/src/components/app/onboarding/onboarding-footer.tsx +++ b/src/components/app/onboarding/onboarding-footer.tsx @@ -11,6 +11,7 @@ interface OnboardingFooterProps { currentStep: keyof typeof STEPS; isLoading: boolean; showSkipButton: boolean; + showBackButton: boolean; onSkip: () => void; onContinue: () => void; onBack: () => void; @@ -20,12 +21,24 @@ export default function OnboardingFooter({ currentStep, isLoading, showSkipButton, + showBackButton, onSkip, onContinue, onBack, }: OnboardingFooterProps) { const { user, canContinue } = useOnboardingContext(); + // Steps where skip button should be hidden + const hideSkipButton = ['FIRST_QUESTION_SELECTION', 'INITIAL_QUESTIONS', 'USER_DETAILS'].includes( + currentStep + ); + + // Conditions to disable continue button + const disableContinue = + isLoading || + !canContinue || + (currentStep === STEPS.USER_DETAILS && (!user?.username || !user?.howDidYouHearAboutTechBlitz)); + return ( - {(currentStep === STEPS.PRICING || - currentStep === STEPS.INITIAL_QUESTIONS || - currentStep === STEPS.QUESTIONS || - currentStep === STEPS.TIME_COMMITMENT || - currentStep === STEPS.TAGS || - currentStep === STEPS.NOTIFICATIONS || - currentStep === STEPS.FIRST_QUESTION_SELECTION) && ( + {showBackButton && (
- {showSkipButton && - // need to answer these two - currentStep !== STEPS.FIRST_QUESTION_SELECTION && - currentStep !== STEPS.INITIAL_QUESTIONS && ( - - )} - {/** disable if no username or how did you hear about us */} - + )} + + {process.env.NODE_ENV === 'development' && ( +
DEBUG: {currentStep}
+ )}
); diff --git a/src/components/app/onboarding/onboarding-form.tsx b/src/components/app/onboarding/onboarding-form.tsx index acfb621be..0ed95712a 100644 --- a/src/components/app/onboarding/onboarding-form.tsx +++ b/src/components/app/onboarding/onboarding-form.tsx @@ -49,13 +49,20 @@ const stepComponents = { export default function OnboardingForm() { const { itemVariants } = useOnboardingContext(); - const { currentStep, isLoading, handleSkip, handleContinue, handleBack, showSkipButton } = - useOnboardingSteps(); + const { + currentStep, + isLoading, + handleSkip, + handleContinue, + handleBack, + showSkipButton, + showBackButton, + } = useOnboardingSteps(); const StepComponent = stepComponents[currentStep]; return ( -
+
{ + // if we're on the first step, we shouldn't go back + if (currentStep === STEPS.USER_DETAILS) { + return; + } + // if the user is on the pricing page, they need to go back to the tags page only if they did not // start from scratch if (currentStep === STEPS.PRICING && firstQuestionSelection !== 'startFromScratch') { - setCurrentStepState(STEPS.TAGS); - return; + return setCurrentStepState(STEPS.TAGS); } const previousStep = Object.entries(stepConfig).find( - ([, config]) => config.next === currentStep + ([_, config]) => config.next === currentStep )?.[0]; if (previousStep) { setCurrentStepState(previousStep as StepKey); @@ -237,7 +241,19 @@ export function useOnboardingSteps() { const isStepOne = currentStep === STEPS.USER_DETAILS; const hasUsername = (user?.username?.length ?? 0) > 0; - return refInUrl || (!isStepOne && hasUsername) || (isStepOne && refInUrl && hasUsername); + // Never show skip button on the first page (USER_DETAILS) regardless of other conditions + if (isStepOne) { + return false; + } + + // For other steps, show the skip button if there's a ref in the URL or the user has a username + return refInUrl || hasUsername; + }; + + // Function to determine if back button should be shown + const showBackButton = () => { + // Don't show back button on the first step + return currentStep !== STEPS.USER_DETAILS; }; return { @@ -247,6 +263,7 @@ export function useOnboardingSteps() { handleContinue, handleBack, showSkipButton, + showBackButton, stepConfig, }; } From 3a0758a64751507f26c3d618376a5f5255c13521 Mon Sep 17 00:00:00 2001 From: Logan Ford <110533855+Logannford@users.noreply.github.com> Date: Sun, 27 Apr 2025 12:02:31 +0100 Subject: [PATCH 04/14] code cleanup --- .env.example | 3 - .../app/onboarding/onboarding-footer.tsx | 10 +- .../global/blocks/call-to-action-block.tsx | 36 +----- src/hooks/use-onboarding-steps.ts | 107 ++++++++++-------- 4 files changed, 74 insertions(+), 82 deletions(-) diff --git a/.env.example b/.env.example index 07a74e21e..57cf22a7a 100644 --- a/.env.example +++ b/.env.example @@ -52,9 +52,6 @@ NEXT_PRIVATE_OPEN_AI_ORG= # Keys used to connect to Claude (only needed for ai development) ANTHROPIC_API_KEY= -# already preset. -NEXT_PUBLIC_ENV=development - # secret used to authenticate cron jobs, not needed for dev CRON_SECRET= diff --git a/src/components/app/onboarding/onboarding-footer.tsx b/src/components/app/onboarding/onboarding-footer.tsx index d16501185..d54d8d4f2 100644 --- a/src/components/app/onboarding/onboarding-footer.tsx +++ b/src/components/app/onboarding/onboarding-footer.tsx @@ -53,9 +53,15 @@ export default function OnboardingFooter({ animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} > - )} diff --git a/src/components/marketing/global/blocks/call-to-action-block.tsx b/src/components/marketing/global/blocks/call-to-action-block.tsx index 2d74fa271..6d526d785 100644 --- a/src/components/marketing/global/blocks/call-to-action-block.tsx +++ b/src/components/marketing/global/blocks/call-to-action-block.tsx @@ -38,36 +38,12 @@ export default function CallToActionBlock(opts: {
- {process.env.NEXT_PUBLIC_ENV === 'development' ? ( - <> - - {rightCta && ( - - )} - - ) : ( - <> - - - - )} + <> + + +
diff --git a/src/hooks/use-onboarding-steps.ts b/src/hooks/use-onboarding-steps.ts index 12e2ca87f..4ffba1046 100644 --- a/src/hooks/use-onboarding-steps.ts +++ b/src/hooks/use-onboarding-steps.ts @@ -33,6 +33,7 @@ export function useOnboardingSteps() { user, handleGetOnboardingQuestions, canContinue, + setCanContinue, timeSpendingPerDay, firstQuestionSelection, FIRST_QUESTION_TUTORIAL_SLUG, @@ -111,7 +112,7 @@ export function useOnboardingSteps() { setIsLoading(true); try { - // if the user wants to start from scratch, the next page needs to be the pricing page, not the tags page + // Handle first question selection routing if ( currentStep === STEPS.FIRST_QUESTION_SELECTION && firstQuestionSelection === 'startFromScratch' @@ -120,37 +121,36 @@ export function useOnboardingSteps() { return; } - // if we are on the last step and the user chose to start from scratch, - // redirect them to the first question - if (currentStep === STEPS.PRICING && firstQuestionSelection === 'startFromScratch') { - // remove onboarding data - localStorage.removeItem('onboarding'); - // then redirect - router.push(`/question/${FIRST_QUESTION_TUTORIAL_SLUG}?tutorial=true`); - return; - } else if ( - currentStep === STEPS.PRICING && - firstQuestionSelection === 'personalizeLearning' - ) { - await handleGetOnboardingQuestions(); - // remove the onboarding data from local storage - localStorage.removeItem('onboarding'); - setCurrentStepState(STEPS.QUESTIONS); - return; + // Handle pricing step based on user's first question selection + if (currentStep === STEPS.PRICING) { + if (firstQuestionSelection === 'startFromScratch') { + localStorage.removeItem('onboarding'); + router.push(`/question/${FIRST_QUESTION_TUTORIAL_SLUG}?tutorial=true`); + return; + } else if (firstQuestionSelection === 'personalizeLearning') { + await handleGetOnboardingQuestions(); + localStorage.removeItem('onboarding'); + setCurrentStepState(STEPS.QUESTIONS); + return; + } } + // Handle time commitment step if (currentStep === STEPS.TIME_COMMITMENT) { await updateUser({ userDetails: { ...user, timeSpendingPerDay } }); setCurrentStepState(stepConfig[STEPS.TIME_COMMITMENT].next as StepKey); - } else if (currentStep === STEPS.USER_DETAILS) { + return; + } + + // Handle user details step + if (currentStep === STEPS.USER_DETAILS) { await updateUser({ userDetails: user }); - // if this is false, we need to create a coupon and send the welcome email + // Create coupon and send welcome email if needed if (!user.hasCreatedCustomSignupCoupon) { const coupon = await createCouponOnSignup(); - // Check if welcome email has already been sent using localStorage - // Use a generic key if serverUser doesn't have email + // Check if welcome email has already been sent const storageKey = serverUser?.email ? `welcome-email-sent_${serverUser.email}` : 'welcome-email-sent'; @@ -159,27 +159,24 @@ export function useOnboardingSteps() { key: storageKey, }); - // Only send welcome email if it hasn't been sent before if (!welcomeEmailSent) { - // send the welcome email await sendWelcomeEmail(serverUser, coupon?.name ?? ''); - // Mark that welcome email has been sent for this user setWelcomeEmailSent(true); } } - // If user is an admin and the feature toggle is enabled, skip the INITIAL_QUESTIONS step + // Admin users can skip initial questions if feature flag is enabled if (ADMIN_SKIP_INITIAL_QUESTIONS && serverUser?.userLevel === 'ADMIN') { console.log('Admin user detected - skipping initial questions'); - // Skip to the TIME_COMMITMENT step (which is after INITIAL_QUESTIONS) setCurrentStepState(STEPS.TIME_COMMITMENT); } else { - // Regular user follows normal flow setCurrentStepState(stepConfig[STEPS.USER_DETAILS].next as StepKey); } - } else if (currentStep === STEPS.INITIAL_QUESTIONS) { - // Fix the userXp field by ensuring it's a numeric value - // Make sure to use the existing user object and only update the userXp field + return; + } + + // Handle initial questions step + if (currentStep === STEPS.INITIAL_QUESTIONS) { const userXpValue = typeof totalXp === 'number' && !isNaN(totalXp) ? totalXp : 0; try { @@ -190,24 +187,23 @@ export function useOnboardingSteps() { }, }); console.log(`Successfully updated user with XP: ${userXpValue}`); - setCurrentStepState(stepConfig[STEPS.INITIAL_QUESTIONS].next as StepKey); } catch (error) { console.error('Error updating user XP:', error); - // We'll still proceed to the next step even if the XP update fails - setCurrentStepState(stepConfig[STEPS.INITIAL_QUESTIONS].next as StepKey); } - } else { - await updateUser({ userDetails: user }); - const nextStep = stepConfig[currentStep].next; - if (nextStep === 'DASHBOARD') { - localStorage.removeItem('onboarding'); - router.push('/dashboard?onboarding=true'); - } else if (nextStep === STEPS.PRICING) { - setCurrentStepState(STEPS.PRICING); - } else { - setCurrentStepState(nextStep as StepKey); - } + setCurrentStepState(stepConfig[STEPS.INITIAL_QUESTIONS].next as StepKey); + return; + } + + // Handle all other steps + await updateUser({ userDetails: user }); + const nextStep = stepConfig[currentStep].next; + + if (nextStep === 'DASHBOARD') { + localStorage.removeItem('onboarding'); + router.push('/dashboard?onboarding=true'); + } else { + setCurrentStepState(nextStep as StepKey); } } catch (error) { console.error('Error during continue:', error); @@ -228,10 +224,17 @@ export function useOnboardingSteps() { return setCurrentStepState(STEPS.TAGS); } + // Get the previous step const previousStep = Object.entries(stepConfig).find( ([_, config]) => config.next === currentStep )?.[0]; + if (previousStep) { + // If navigating back to USER_DETAILS, ensure the continue button is enabled + if (previousStep === STEPS.USER_DETAILS) { + setCanContinue(true); + } + setCurrentStepState(previousStep as StepKey); } }; @@ -252,8 +255,18 @@ export function useOnboardingSteps() { // Function to determine if back button should be shown const showBackButton = () => { - // Don't show back button on the first step - return currentStep !== STEPS.USER_DETAILS; + // Don't show back button on the first step (USER_DETAILS) + if (currentStep === STEPS.USER_DETAILS) { + return false; + } + + // Don't show back button on final steps + if (currentStep === STEPS.QUESTIONS || currentStep === STEPS.PRICING) { + return false; + } + + // Show back button on intermediate steps + return true; }; return { From b3617a2e845306b2c0373683884834ba0ecf788a Mon Sep 17 00:00:00 2001 From: Logan Ford <110533855+Logannford@users.noreply.github.com> Date: Sun, 27 Apr 2025 12:05:22 +0100 Subject: [PATCH 05/14] remove debug code --- src/components/app/onboarding/onboarding-footer.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/app/onboarding/onboarding-footer.tsx b/src/components/app/onboarding/onboarding-footer.tsx index d54d8d4f2..52e39e8b5 100644 --- a/src/components/app/onboarding/onboarding-footer.tsx +++ b/src/components/app/onboarding/onboarding-footer.tsx @@ -76,9 +76,6 @@ export default function OnboardingFooter({ Continue {isLoading && } - {process.env.NODE_ENV === 'development' && ( -
DEBUG: {currentStep}
- )}
); From 7426df8ff037845a8f1d25732f1ee735b2657dee Mon Sep 17 00:00:00 2001 From: Logan Ford <110533855+Logannford@users.noreply.github.com> Date: Tue, 29 Apr 2025 11:37:56 +0100 Subject: [PATCH 06/14] fix: redirects non authed users in onboarding & progress with new screen --- src/app/(no_nav)/onboarding/page.tsx | 14 +- .../onboarding-first-question-selection.tsx | 314 +++++++++++++++++- src/components/app/study-paths/list.tsx | 14 - 3 files changed, 321 insertions(+), 21 deletions(-) diff --git a/src/app/(no_nav)/onboarding/page.tsx b/src/app/(no_nav)/onboarding/page.tsx index c27a9644e..acbdca9a6 100644 --- a/src/app/(no_nav)/onboarding/page.tsx +++ b/src/app/(no_nav)/onboarding/page.tsx @@ -1,9 +1,12 @@ +import Link from 'next/link'; + +import { redirect } from 'next/navigation'; + import Logo from '@/components/ui/logo'; import StarsBackground from '@/components/ui/stars-background'; +import OnboardingForm from '@/components/app/onboarding/onboarding-form'; -import Link from 'next/link'; import { UserOnboardingContextProvider } from '@/contexts/onboarding-context'; -import OnboardingForm from '@/components/app/onboarding/onboarding-form'; import { useUserServer } from '@/hooks/use-user-server'; export const metadata = { @@ -11,7 +14,12 @@ export const metadata = { }; export default async function OnboardingPage() { - const [user] = await Promise.all([useUserServer()]); + const user = await useUserServer(); + + // throw the user back to login + if (!user) { + return redirect('/login'); + } return (
diff --git a/src/components/app/onboarding/onboarding-first-question-selection.tsx b/src/components/app/onboarding/onboarding-first-question-selection.tsx index a4df40956..93a3ffc75 100644 --- a/src/components/app/onboarding/onboarding-first-question-selection.tsx +++ b/src/components/app/onboarding/onboarding-first-question-selection.tsx @@ -2,6 +2,284 @@ import { CardHeader } from '@/components/ui/card'; import { useOnboardingContext } from '@/contexts/onboarding-context'; import { cn } from '@/lib/utils'; import { motion } from 'framer-motion'; +import StudyPathsList from '../study-paths/list'; +import type { Question } from '@/types'; +import { StudyPath } from '@/utils/constants/study-paths'; + +// Mock questions data similar to the storybook examples +const questions: Partial[] = [ + { + uid: '1', + title: 'JavaScript Variables', + question: 'What is the correct way to declare a variable in JavaScript?', + description: 'Understanding variable declarations in JavaScript', + answers: [ + { + uid: 'a1', + questionUid: '1', + answer: 'var x = 5;', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + { + uid: 'a2', + questionUid: '1', + answer: 'variable x = 5;', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + ], + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-01', + correctAnswer: 'a1', + slug: 'javascript-variables', + questionType: 'MULTIPLE_CHOICE', + difficulty: 'BEGINNER', + userAnswers: [ + { + uid: 'ua1', + userUid: 'user123', + questionUid: '1', + userAnswerUid: 'a1', + correctAnswer: true, + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-01', + timeTaken: 15, + difficulty: 'EASY', + }, + ], + }, + { + uid: '2', + title: 'JavaScript Arrays', + question: 'Which method is used to add an element to the end of an array?', + description: 'Understanding array manipulation in JavaScript', + answers: [ + { + uid: 'b1', + questionUid: '2', + answer: 'push()', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + { + uid: 'b2', + questionUid: '2', + answer: 'append()', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + ], + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-02', + correctAnswer: 'b1', + slug: 'javascript-arrays', + questionType: 'MULTIPLE_CHOICE', + difficulty: 'EASY', + userAnswers: [ + { + uid: 'ua2', + userUid: 'user123', + questionUid: '2', + userAnswerUid: 'b1', + correctAnswer: true, + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-02', + timeTaken: 20, + difficulty: 'EASY', + }, + ], + }, + { + uid: '3', + title: 'JavaScript Functions', + question: 'What defines an arrow function in JavaScript?', + description: 'Understanding arrow functions in JavaScript', + answers: [ + { + uid: 'c1', + questionUid: '3', + answer: '() => {}', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + { + uid: 'c2', + questionUid: '3', + answer: 'function() {}', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + ], + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-03', + correctAnswer: 'c1', + slug: 'javascript-functions', + questionType: 'MULTIPLE_CHOICE', + difficulty: 'MEDIUM', + userAnswers: [ + { + uid: 'ua3', + userUid: 'user123', + questionUid: '3', + userAnswerUid: 'c2', + correctAnswer: false, + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-03', + timeTaken: 30, + difficulty: 'MEDIUM', + }, + ], + }, + { + uid: '4', + title: 'JavaScript Variables', + question: 'What is the correct way to declare a variable in JavaScript?', + description: 'Understanding variable declarations in JavaScript', + answers: [ + { + uid: 'a1', + questionUid: '1', + answer: 'var x = 5;', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + { + uid: 'a2', + questionUid: '1', + answer: 'variable x = 5;', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + ], + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-01', + correctAnswer: 'a1', + slug: 'javascript-variables', + questionType: 'MULTIPLE_CHOICE', + difficulty: 'BEGINNER', + userAnswers: [ + { + uid: 'ua1', + userUid: 'user123', + questionUid: '1', + userAnswerUid: 'a1', + correctAnswer: true, + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-01', + timeTaken: 15, + difficulty: 'EASY', + }, + ], + }, + { + uid: '5', + title: 'JavaScript Arrays', + question: 'Which method is used to add an element to the end of an array?', + description: 'Understanding array manipulation in JavaScript', + answers: [ + { + uid: 'b1', + questionUid: '2', + answer: 'push()', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + { + uid: 'b2', + questionUid: '2', + answer: 'append()', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + ], + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-02', + correctAnswer: 'b1', + slug: 'javascript-arrays', + questionType: 'MULTIPLE_CHOICE', + difficulty: 'EASY', + userAnswers: [ + { + uid: 'ua2', + userUid: 'user123', + questionUid: '2', + userAnswerUid: 'b1', + correctAnswer: true, + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-02', + timeTaken: 20, + difficulty: 'EASY', + }, + ], + }, + { + uid: '6', + title: 'JavaScript Functions', + question: 'What defines an arrow function in JavaScript?', + description: 'Understanding arrow functions in JavaScript', + answers: [ + { + uid: 'c1', + questionUid: '3', + answer: '() => {}', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + { + uid: 'c2', + questionUid: '3', + answer: 'function() {}', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + ], + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-03', + correctAnswer: 'c1', + slug: 'javascript-functions', + questionType: 'MULTIPLE_CHOICE', + difficulty: 'MEDIUM', + userAnswers: [ + { + uid: 'ua3', + userUid: 'user123', + questionUid: '3', + userAnswerUid: 'c2', + correctAnswer: false, + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-03', + timeTaken: 30, + difficulty: 'MEDIUM', + }, + ], + }, +]; + +// Triple the questions to ensure smooth infinite scroll +const allQuestions = [...questions, ...questions, ...questions]; + +// Mock study path +const studyPath: Partial = { + slug: 'javascript-fundamentals', + title: 'JavaScript Fundamentals', + description: 'Learn the fundamentals of JavaScript programming', + heroChip: 'Master JavaScript from scratch', + questionSlugs: ['javascript-variables', 'javascript-arrays', 'javascript-functions'], + educationLevel: 'beginner', +}; export default function OnboardingFirstQuestionSelection() { const { itemVariants } = useOnboardingContext(); @@ -23,10 +301,38 @@ export default function OnboardingFirstQuestionSelection() { {/** 'infinite' scrolling animation of the study path buttons. */} {/* Scrolling content */} -
+
+ {/** top fade effect */} +
+ +
+ {/* First set of items */} + + + {/* Clone of the first set to create seamless loop */} +
+ +
+
+ + {/* Bottom fade effect */} +
+
); diff --git a/src/components/app/study-paths/list.tsx b/src/components/app/study-paths/list.tsx index 4e7c515b6..ba06928bf 100644 --- a/src/components/app/study-paths/list.tsx +++ b/src/components/app/study-paths/list.tsx @@ -165,13 +165,6 @@ export function StudyPathsSubSectionList({ return getOffset(index, offsetType, offsetMultiplier); }; - console.log('StudyPathsSubSectionList props:', { - studyPathSlug: studyPath.slug, - sectionNextQuestionIndex: nextQuestionIndex, - subSectionsCount: subSections.length, - subSectionIndices: subSections.map((sub) => sub.nextQuestionIndex), - }); - return (
{subSections.map((subSection, index) => { @@ -183,13 +176,6 @@ export function StudyPathsSubSectionList({ ? subSection.nextQuestionIndex : nextQuestionIndex; - console.log(`SubSection [${index}] ${subSection.sectionName}:`, { - hasNextIndex: subSection.nextQuestionIndex !== undefined, - subSectionNextIdx: subSection.nextQuestionIndex, - fallbackNextIdx: nextQuestionIndex, - finalNextIdx: subsectionNextQuestionIndex, - }); - return (
Date: Wed, 30 Apr 2025 00:25:20 +0100 Subject: [PATCH 07/14] chore: page cleanup and componentization --- .../javascript-array-cheat-sheet/page.tsx | 4 - .../regular-expression-cheat-sheet/page.tsx | 4 - .../javascript-coding-test/page.tsx | 4 - .../page.tsx | 4 - .../how-does-javascript-work/page.tsx | 4 - .../page.tsx | 4 - .../how-to-write-javascript/page.tsx | 4 - .../html-conditional-statement/page.tsx | 4 - .../javascript-conditionals/page.tsx | 4 - .../page.tsx | 4 - .../javascript-naming-conventions/page.tsx | 4 - .../javascript-nested-conditionals/page.tsx | 4 - .../page.tsx | 4 - .../primitive-types-in-javascript/page.tsx | 4 - .../page.tsx | 4 - .../(landing-pages)/coding-roadmap/page.tsx | 4 - .../javascript-coding-questions/page.tsx | 4 - .../javascript-for-beginners/page.tsx | 4 - .../javascript-roadmap/page.tsx | 4 - .../(landing-pages)/react-roadmap/page.tsx | 4 - src/app/(marketing)/blog/[slug]/page.tsx | 4 - .../(marketing)/features/leaderboard/page.tsx | 4 - src/app/(marketing)/features/roadmap/page.tsx | 4 - .../(marketing)/features/statistics/page.tsx | 8 +- src/app/(no_nav)/onboarding/page.tsx | 4 +- .../onboarding-first-question-selection.tsx | 313 +---------------- .../app/onboarding/onboarding-form.tsx | 18 +- .../global/blocks/call-to-action-block.tsx | 16 +- .../global/blocks/three-block-showcase.tsx | 12 +- src/components/mdx/blog-post-layout.tsx | 4 - .../infinite-scrolling-roadmap-cards.tsx | 320 ++++++++++++++++++ src/hooks/use-onboarding-steps.ts | 69 +--- 32 files changed, 345 insertions(+), 511 deletions(-) create mode 100644 src/components/shared/infinite-scrolling-roadmap-cards.tsx diff --git a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-cheat-sheet/javascript-array-cheat-sheet/page.tsx b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-cheat-sheet/javascript-array-cheat-sheet/page.tsx index 4eb963827..8b4156b32 100644 --- a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-cheat-sheet/javascript-array-cheat-sheet/page.tsx +++ b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-cheat-sheet/javascript-array-cheat-sheet/page.tsx @@ -121,10 +121,6 @@ export default async function JavascriptArrayCheatSheet({ params }: BlogPostPara
diff --git a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-cheat-sheet/regular-expression-cheat-sheet/page.tsx b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-cheat-sheet/regular-expression-cheat-sheet/page.tsx index 0686610d4..7a509e14d 100644 --- a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-cheat-sheet/regular-expression-cheat-sheet/page.tsx +++ b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-cheat-sheet/regular-expression-cheat-sheet/page.tsx @@ -121,10 +121,6 @@ export default async function JavascriptRegularExpressionCheatSheet({ params }:
diff --git a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-coding-interview-questions-and-answers/javascript-coding-test/page.tsx b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-coding-interview-questions-and-answers/javascript-coding-test/page.tsx index f100d0268..167ff1c8f 100644 --- a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-coding-interview-questions-and-answers/javascript-coding-test/page.tsx +++ b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-coding-interview-questions-and-answers/javascript-coding-test/page.tsx @@ -121,10 +121,6 @@ export default async function JavascriptCodingTest({ params }: BlogPostParams) {
diff --git a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-coding-interview-questions-and-answers/javascript-interview-questions-for-senior-developers/page.tsx b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-coding-interview-questions-and-answers/javascript-interview-questions-for-senior-developers/page.tsx index de8171e1e..7e94146b1 100644 --- a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-coding-interview-questions-and-answers/javascript-interview-questions-for-senior-developers/page.tsx +++ b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-coding-interview-questions-and-answers/javascript-interview-questions-for-senior-developers/page.tsx @@ -127,10 +127,6 @@ export default async function JavascriptInterviewQuestionsForSeniorDevelopers({
diff --git a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/how-does-javascript-work/page.tsx b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/how-does-javascript-work/page.tsx index 8e42bb1bc..bafe23a35 100644 --- a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/how-does-javascript-work/page.tsx +++ b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/how-does-javascript-work/page.tsx @@ -121,10 +121,6 @@ export default async function HowDoesJavascriptWork({ params }: BlogPostParams)
diff --git a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/how-to-write-a-function-in-javascript/page.tsx b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/how-to-write-a-function-in-javascript/page.tsx index ba044ec4c..026947de3 100644 --- a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/how-to-write-a-function-in-javascript/page.tsx +++ b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/how-to-write-a-function-in-javascript/page.tsx @@ -121,10 +121,6 @@ export default async function HowToWriteAFunctionInJavascript({ params }: BlogPo
diff --git a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/how-to-write-javascript/page.tsx b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/how-to-write-javascript/page.tsx index 5bca3df1d..b0c646afa 100644 --- a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/how-to-write-javascript/page.tsx +++ b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/how-to-write-javascript/page.tsx @@ -121,10 +121,6 @@ export default async function HowToWriteJavascript({ params }: BlogPostParams) {
diff --git a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/html-conditional-statement/page.tsx b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/html-conditional-statement/page.tsx index 868ba7e63..b1f20a5d1 100644 --- a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/html-conditional-statement/page.tsx +++ b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/html-conditional-statement/page.tsx @@ -121,10 +121,6 @@ export default async function HtmlConditionalStatement({ params }: BlogPostParam diff --git a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/javascript-conditionals/page.tsx b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/javascript-conditionals/page.tsx index dc667d8cf..ec3edc1da 100644 --- a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/javascript-conditionals/page.tsx +++ b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/javascript-conditionals/page.tsx @@ -121,10 +121,6 @@ export default async function JavaScriptConditionals({ params }: BlogPostParams) diff --git a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/javascript-format-strings-with-variables/page.tsx b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/javascript-format-strings-with-variables/page.tsx index be04a2343..7dcd73586 100644 --- a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/javascript-format-strings-with-variables/page.tsx +++ b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/javascript-format-strings-with-variables/page.tsx @@ -121,10 +121,6 @@ export default async function JavaScriptFormatStringsWithVariables({ params }: B diff --git a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/javascript-naming-conventions/page.tsx b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/javascript-naming-conventions/page.tsx index 558bb29d7..74b7b020f 100644 --- a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/javascript-naming-conventions/page.tsx +++ b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/javascript-naming-conventions/page.tsx @@ -121,10 +121,6 @@ export default async function JavaScriptNamingConventions({ params }: BlogPostPa diff --git a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/javascript-nested-conditionals/page.tsx b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/javascript-nested-conditionals/page.tsx index 14e72826a..521e9b53c 100644 --- a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/javascript-nested-conditionals/page.tsx +++ b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/javascript-nested-conditionals/page.tsx @@ -121,10 +121,6 @@ export default async function JavaScriptNestedConditionals({ params }: BlogPostP diff --git a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/loose-vs-strict-equality-in-javascript/page.tsx b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/loose-vs-strict-equality-in-javascript/page.tsx index c1645eac0..b0071e61f 100644 --- a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/loose-vs-strict-equality-in-javascript/page.tsx +++ b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/loose-vs-strict-equality-in-javascript/page.tsx @@ -121,10 +121,6 @@ export default async function LooseVsStrictEqualityInJavascript({ params }: Blog diff --git a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/primitive-types-in-javascript/page.tsx b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/primitive-types-in-javascript/page.tsx index 6d5b31902..94725cddf 100644 --- a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/primitive-types-in-javascript/page.tsx +++ b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-fundamentals/primitive-types-in-javascript/page.tsx @@ -121,10 +121,6 @@ export default async function PrimitiveTypesInJavascript({ params }: BlogPostPar diff --git a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-projects-for-beginners/programming-challenges-for-beginners/page.tsx b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-projects-for-beginners/programming-challenges-for-beginners/page.tsx index 2a3ac2560..0ecd8f245 100644 --- a/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-projects-for-beginners/programming-challenges-for-beginners/page.tsx +++ b/src/app/(marketing)/(landing-pages)/(pillar-pages)/javascript-projects-for-beginners/programming-challenges-for-beginners/page.tsx @@ -123,10 +123,6 @@ export default async function JavascriptProgrammingChallengesForBeginners({ diff --git a/src/app/(marketing)/(landing-pages)/coding-roadmap/page.tsx b/src/app/(marketing)/(landing-pages)/coding-roadmap/page.tsx index 6d464414c..81f963182 100644 --- a/src/app/(marketing)/(landing-pages)/coding-roadmap/page.tsx +++ b/src/app/(marketing)/(landing-pages)/coding-roadmap/page.tsx @@ -274,10 +274,6 @@ export default function CodingRoadmapPage() { diff --git a/src/app/(marketing)/(landing-pages)/javascript-coding-questions/page.tsx b/src/app/(marketing)/(landing-pages)/javascript-coding-questions/page.tsx index 2892f6cde..e5ccfc75a 100644 --- a/src/app/(marketing)/(landing-pages)/javascript-coding-questions/page.tsx +++ b/src/app/(marketing)/(landing-pages)/javascript-coding-questions/page.tsx @@ -272,10 +272,6 @@ export default function JavascriptCodingQuestionsPage() { diff --git a/src/app/(marketing)/(landing-pages)/javascript-for-beginners/page.tsx b/src/app/(marketing)/(landing-pages)/javascript-for-beginners/page.tsx index c95fde14b..4da3ca2b5 100644 --- a/src/app/(marketing)/(landing-pages)/javascript-for-beginners/page.tsx +++ b/src/app/(marketing)/(landing-pages)/javascript-for-beginners/page.tsx @@ -255,10 +255,6 @@ export default function JavascriptForBeginnersPage() { diff --git a/src/app/(marketing)/(landing-pages)/javascript-roadmap/page.tsx b/src/app/(marketing)/(landing-pages)/javascript-roadmap/page.tsx index d53a8534a..313f0b01e 100644 --- a/src/app/(marketing)/(landing-pages)/javascript-roadmap/page.tsx +++ b/src/app/(marketing)/(landing-pages)/javascript-roadmap/page.tsx @@ -274,10 +274,6 @@ export default function JavascriptRoadmapPage() { diff --git a/src/app/(marketing)/(landing-pages)/react-roadmap/page.tsx b/src/app/(marketing)/(landing-pages)/react-roadmap/page.tsx index afd4376f7..81936bf48 100644 --- a/src/app/(marketing)/(landing-pages)/react-roadmap/page.tsx +++ b/src/app/(marketing)/(landing-pages)/react-roadmap/page.tsx @@ -285,10 +285,6 @@ export default function ReactRoadmapPage() { diff --git a/src/app/(marketing)/blog/[slug]/page.tsx b/src/app/(marketing)/blog/[slug]/page.tsx index c1c885d17..00cc2eba8 100644 --- a/src/app/(marketing)/blog/[slug]/page.tsx +++ b/src/app/(marketing)/blog/[slug]/page.tsx @@ -143,10 +143,6 @@ export default async function BlogPost({ params }: BlogPostParams) { diff --git a/src/app/(marketing)/features/leaderboard/page.tsx b/src/app/(marketing)/features/leaderboard/page.tsx index 57c716819..6f6c7234b 100644 --- a/src/app/(marketing)/features/leaderboard/page.tsx +++ b/src/app/(marketing)/features/leaderboard/page.tsx @@ -252,10 +252,6 @@ export default function LeaderboardPage() { diff --git a/src/app/(marketing)/features/roadmap/page.tsx b/src/app/(marketing)/features/roadmap/page.tsx index bf038a006..dc76d3fb2 100644 --- a/src/app/(marketing)/features/roadmap/page.tsx +++ b/src/app/(marketing)/features/roadmap/page.tsx @@ -246,10 +246,6 @@ export default function FeatureDailyQuestionPage() { diff --git a/src/app/(marketing)/features/statistics/page.tsx b/src/app/(marketing)/features/statistics/page.tsx index 3af799d5f..573922d83 100644 --- a/src/app/(marketing)/features/statistics/page.tsx +++ b/src/app/(marketing)/features/statistics/page.tsx @@ -139,13 +139,7 @@ export default function StatisticsPage() { title="Tracking coding progress made simple." items={featureShowcaseItems} /> - + ); diff --git a/src/app/(no_nav)/onboarding/page.tsx b/src/app/(no_nav)/onboarding/page.tsx index acbdca9a6..ee34ff9a2 100644 --- a/src/app/(no_nav)/onboarding/page.tsx +++ b/src/app/(no_nav)/onboarding/page.tsx @@ -29,9 +29,7 @@ export default async function OnboardingPage() { -
- -
+
diff --git a/src/components/app/onboarding/onboarding-first-question-selection.tsx b/src/components/app/onboarding/onboarding-first-question-selection.tsx index 93a3ffc75..674508478 100644 --- a/src/components/app/onboarding/onboarding-first-question-selection.tsx +++ b/src/components/app/onboarding/onboarding-first-question-selection.tsx @@ -1,285 +1,9 @@ import { CardHeader } from '@/components/ui/card'; import { useOnboardingContext } from '@/contexts/onboarding-context'; -import { cn } from '@/lib/utils'; import { motion } from 'framer-motion'; -import StudyPathsList from '../study-paths/list'; import type { Question } from '@/types'; import { StudyPath } from '@/utils/constants/study-paths'; - -// Mock questions data similar to the storybook examples -const questions: Partial[] = [ - { - uid: '1', - title: 'JavaScript Variables', - question: 'What is the correct way to declare a variable in JavaScript?', - description: 'Understanding variable declarations in JavaScript', - answers: [ - { - uid: 'a1', - questionUid: '1', - answer: 'var x = 5;', - answerType: 'STANDARD', - isCodeSnippet: false, - }, - { - uid: 'a2', - questionUid: '1', - answer: 'variable x = 5;', - answerType: 'STANDARD', - isCodeSnippet: false, - }, - ], - createdAt: new Date(), - updatedAt: new Date(), - questionDate: '2023-01-01', - correctAnswer: 'a1', - slug: 'javascript-variables', - questionType: 'MULTIPLE_CHOICE', - difficulty: 'BEGINNER', - userAnswers: [ - { - uid: 'ua1', - userUid: 'user123', - questionUid: '1', - userAnswerUid: 'a1', - correctAnswer: true, - createdAt: new Date(), - updatedAt: new Date(), - questionDate: '2023-01-01', - timeTaken: 15, - difficulty: 'EASY', - }, - ], - }, - { - uid: '2', - title: 'JavaScript Arrays', - question: 'Which method is used to add an element to the end of an array?', - description: 'Understanding array manipulation in JavaScript', - answers: [ - { - uid: 'b1', - questionUid: '2', - answer: 'push()', - answerType: 'STANDARD', - isCodeSnippet: false, - }, - { - uid: 'b2', - questionUid: '2', - answer: 'append()', - answerType: 'STANDARD', - isCodeSnippet: false, - }, - ], - createdAt: new Date(), - updatedAt: new Date(), - questionDate: '2023-01-02', - correctAnswer: 'b1', - slug: 'javascript-arrays', - questionType: 'MULTIPLE_CHOICE', - difficulty: 'EASY', - userAnswers: [ - { - uid: 'ua2', - userUid: 'user123', - questionUid: '2', - userAnswerUid: 'b1', - correctAnswer: true, - createdAt: new Date(), - updatedAt: new Date(), - questionDate: '2023-01-02', - timeTaken: 20, - difficulty: 'EASY', - }, - ], - }, - { - uid: '3', - title: 'JavaScript Functions', - question: 'What defines an arrow function in JavaScript?', - description: 'Understanding arrow functions in JavaScript', - answers: [ - { - uid: 'c1', - questionUid: '3', - answer: '() => {}', - answerType: 'STANDARD', - isCodeSnippet: false, - }, - { - uid: 'c2', - questionUid: '3', - answer: 'function() {}', - answerType: 'STANDARD', - isCodeSnippet: false, - }, - ], - createdAt: new Date(), - updatedAt: new Date(), - questionDate: '2023-01-03', - correctAnswer: 'c1', - slug: 'javascript-functions', - questionType: 'MULTIPLE_CHOICE', - difficulty: 'MEDIUM', - userAnswers: [ - { - uid: 'ua3', - userUid: 'user123', - questionUid: '3', - userAnswerUid: 'c2', - correctAnswer: false, - createdAt: new Date(), - updatedAt: new Date(), - questionDate: '2023-01-03', - timeTaken: 30, - difficulty: 'MEDIUM', - }, - ], - }, - { - uid: '4', - title: 'JavaScript Variables', - question: 'What is the correct way to declare a variable in JavaScript?', - description: 'Understanding variable declarations in JavaScript', - answers: [ - { - uid: 'a1', - questionUid: '1', - answer: 'var x = 5;', - answerType: 'STANDARD', - isCodeSnippet: false, - }, - { - uid: 'a2', - questionUid: '1', - answer: 'variable x = 5;', - answerType: 'STANDARD', - isCodeSnippet: false, - }, - ], - createdAt: new Date(), - updatedAt: new Date(), - questionDate: '2023-01-01', - correctAnswer: 'a1', - slug: 'javascript-variables', - questionType: 'MULTIPLE_CHOICE', - difficulty: 'BEGINNER', - userAnswers: [ - { - uid: 'ua1', - userUid: 'user123', - questionUid: '1', - userAnswerUid: 'a1', - correctAnswer: true, - createdAt: new Date(), - updatedAt: new Date(), - questionDate: '2023-01-01', - timeTaken: 15, - difficulty: 'EASY', - }, - ], - }, - { - uid: '5', - title: 'JavaScript Arrays', - question: 'Which method is used to add an element to the end of an array?', - description: 'Understanding array manipulation in JavaScript', - answers: [ - { - uid: 'b1', - questionUid: '2', - answer: 'push()', - answerType: 'STANDARD', - isCodeSnippet: false, - }, - { - uid: 'b2', - questionUid: '2', - answer: 'append()', - answerType: 'STANDARD', - isCodeSnippet: false, - }, - ], - createdAt: new Date(), - updatedAt: new Date(), - questionDate: '2023-01-02', - correctAnswer: 'b1', - slug: 'javascript-arrays', - questionType: 'MULTIPLE_CHOICE', - difficulty: 'EASY', - userAnswers: [ - { - uid: 'ua2', - userUid: 'user123', - questionUid: '2', - userAnswerUid: 'b1', - correctAnswer: true, - createdAt: new Date(), - updatedAt: new Date(), - questionDate: '2023-01-02', - timeTaken: 20, - difficulty: 'EASY', - }, - ], - }, - { - uid: '6', - title: 'JavaScript Functions', - question: 'What defines an arrow function in JavaScript?', - description: 'Understanding arrow functions in JavaScript', - answers: [ - { - uid: 'c1', - questionUid: '3', - answer: '() => {}', - answerType: 'STANDARD', - isCodeSnippet: false, - }, - { - uid: 'c2', - questionUid: '3', - answer: 'function() {}', - answerType: 'STANDARD', - isCodeSnippet: false, - }, - ], - createdAt: new Date(), - updatedAt: new Date(), - questionDate: '2023-01-03', - correctAnswer: 'c1', - slug: 'javascript-functions', - questionType: 'MULTIPLE_CHOICE', - difficulty: 'MEDIUM', - userAnswers: [ - { - uid: 'ua3', - userUid: 'user123', - questionUid: '3', - userAnswerUid: 'c2', - correctAnswer: false, - createdAt: new Date(), - updatedAt: new Date(), - questionDate: '2023-01-03', - timeTaken: 30, - difficulty: 'MEDIUM', - }, - ], - }, -]; - -// Triple the questions to ensure smooth infinite scroll -const allQuestions = [...questions, ...questions, ...questions]; - -// Mock study path -const studyPath: Partial = { - slug: 'javascript-fundamentals', - title: 'JavaScript Fundamentals', - description: 'Learn the fundamentals of JavaScript programming', - heroChip: 'Master JavaScript from scratch', - questionSlugs: ['javascript-variables', 'javascript-arrays', 'javascript-functions'], - educationLevel: 'beginner', -}; +import InfiniteScrollingRoadmapCards from '@/components/shared/infinite-scrolling-roadmap-cards'; export default function OnboardingFirstQuestionSelection() { const { itemVariants } = useOnboardingContext(); @@ -299,40 +23,7 @@ export default function OnboardingFirstQuestionSelection() { you get there. - {/** 'infinite' scrolling animation of the study path buttons. */} - {/* Scrolling content */} -
- {/** top fade effect */} -
- -
- {/* First set of items */} - - - {/* Clone of the first set to create seamless loop */} -
- -
-
- - {/* Bottom fade effect */} -
-
+
); diff --git a/src/components/app/onboarding/onboarding-form.tsx b/src/components/app/onboarding/onboarding-form.tsx index 0ed95712a..7cf52aff0 100644 --- a/src/components/app/onboarding/onboarding-form.tsx +++ b/src/components/app/onboarding/onboarding-form.tsx @@ -6,9 +6,6 @@ import { motion } from 'framer-motion'; import { Card } from '@/components/ui/card'; import OnboardingUserDetails from './onboarding-user-details'; import OnboardingTimeCommitment from './onboarding-time-commitment'; -import OnboardingTags from './onboarding-tags'; -import OnboardingQuestions from './onboarding-questions'; -import OnboardingPricing from './onboarding-pricing'; import OnboardingFooter from './onboarding-footer'; import OnboardingNotifications from './onboarding-notifications'; import OnboardingInitialQuestions from './onboarding-initial-questions'; @@ -42,9 +39,6 @@ const stepComponents = { [STEPS.TIME_COMMITMENT]: OnboardingTimeCommitment, [STEPS.NOTIFICATIONS]: OnboardingNotifications, [STEPS.FIRST_QUESTION_SELECTION]: OnboardingFirstQuestionSelection, - [STEPS.TAGS]: OnboardingTags, - [STEPS.QUESTIONS]: OnboardingQuestions, - [STEPS.PRICING]: OnboardingPricing, }; export default function OnboardingForm() { @@ -62,20 +56,18 @@ export default function OnboardingForm() { const StepComponent = stepComponents[currentStep]; return ( -
+
- {' '} - {studyPath && ( - - )} +
diff --git a/src/components/mdx/blog-post-layout.tsx b/src/components/mdx/blog-post-layout.tsx index 4554d0794..e4b2bde19 100644 --- a/src/components/mdx/blog-post-layout.tsx +++ b/src/components/mdx/blog-post-layout.tsx @@ -82,10 +82,6 @@ export default function BlogPostLayout({
diff --git a/src/components/shared/infinite-scrolling-roadmap-cards.tsx b/src/components/shared/infinite-scrolling-roadmap-cards.tsx new file mode 100644 index 000000000..603f1954e --- /dev/null +++ b/src/components/shared/infinite-scrolling-roadmap-cards.tsx @@ -0,0 +1,320 @@ +import { Question } from '@/types'; +import StudyPathsList from '../app/study-paths/list'; +import { StudyPath } from '@prisma/client'; +import { cn } from '@/lib/utils'; + +// Mock questions data similar to the storybook examples +const questions: Partial[] = [ + { + uid: '1', + title: 'JavaScript Variables', + question: 'What is the correct way to declare a variable in JavaScript?', + description: 'Understanding variable declarations in JavaScript', + answers: [ + { + uid: 'a1', + questionUid: '1', + answer: 'var x = 5;', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + { + uid: 'a2', + questionUid: '1', + answer: 'variable x = 5;', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + ], + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-01', + correctAnswer: 'a1', + slug: 'javascript-variables', + questionType: 'MULTIPLE_CHOICE', + difficulty: 'BEGINNER', + userAnswers: [ + { + uid: 'ua1', + userUid: 'user123', + questionUid: '1', + userAnswerUid: 'a1', + correctAnswer: true, + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-01', + timeTaken: 15, + difficulty: 'EASY', + }, + ], + }, + { + uid: '2', + title: 'JavaScript Arrays', + question: 'Which method is used to add an element to the end of an array?', + description: 'Understanding array manipulation in JavaScript', + answers: [ + { + uid: 'b1', + questionUid: '2', + answer: 'push()', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + { + uid: 'b2', + questionUid: '2', + answer: 'append()', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + ], + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-02', + correctAnswer: 'b1', + slug: 'javascript-arrays', + questionType: 'MULTIPLE_CHOICE', + difficulty: 'EASY', + userAnswers: [ + { + uid: 'ua2', + userUid: 'user123', + questionUid: '2', + userAnswerUid: 'b1', + correctAnswer: true, + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-02', + timeTaken: 20, + difficulty: 'EASY', + }, + ], + }, + { + uid: '3', + title: 'JavaScript Functions', + question: 'What defines an arrow function in JavaScript?', + description: 'Understanding arrow functions in JavaScript', + answers: [ + { + uid: 'c1', + questionUid: '3', + answer: '() => {}', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + { + uid: 'c2', + questionUid: '3', + answer: 'function() {}', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + ], + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-03', + correctAnswer: 'c1', + slug: 'javascript-functions', + questionType: 'MULTIPLE_CHOICE', + difficulty: 'MEDIUM', + userAnswers: [ + { + uid: 'ua3', + userUid: 'user123', + questionUid: '3', + userAnswerUid: 'c2', + correctAnswer: false, + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-03', + timeTaken: 30, + difficulty: 'MEDIUM', + }, + ], + }, + { + uid: '4', + title: 'JavaScript Variables', + question: 'What is the correct way to declare a variable in JavaScript?', + description: 'Understanding variable declarations in JavaScript', + answers: [ + { + uid: 'a1', + questionUid: '1', + answer: 'var x = 5;', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + { + uid: 'a2', + questionUid: '1', + answer: 'variable x = 5;', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + ], + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-01', + correctAnswer: 'a1', + slug: 'javascript-variables', + questionType: 'MULTIPLE_CHOICE', + difficulty: 'BEGINNER', + userAnswers: [ + { + uid: 'ua1', + userUid: 'user123', + questionUid: '1', + userAnswerUid: 'a1', + correctAnswer: true, + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-01', + timeTaken: 15, + difficulty: 'EASY', + }, + ], + }, + { + uid: '5', + title: 'JavaScript Arrays', + question: 'Which method is used to add an element to the end of an array?', + description: 'Understanding array manipulation in JavaScript', + answers: [ + { + uid: 'b1', + questionUid: '2', + answer: 'push()', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + { + uid: 'b2', + questionUid: '2', + answer: 'append()', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + ], + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-02', + correctAnswer: 'b1', + slug: 'javascript-arrays', + questionType: 'MULTIPLE_CHOICE', + difficulty: 'EASY', + userAnswers: [ + { + uid: 'ua2', + userUid: 'user123', + questionUid: '2', + userAnswerUid: 'b1', + correctAnswer: true, + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-02', + timeTaken: 20, + difficulty: 'EASY', + }, + ], + }, + { + uid: '6', + title: 'JavaScript Functions', + question: 'What defines an arrow function in JavaScript?', + description: 'Understanding arrow functions in JavaScript', + answers: [ + { + uid: 'c1', + questionUid: '3', + answer: '() => {}', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + { + uid: 'c2', + questionUid: '3', + answer: 'function() {}', + answerType: 'STANDARD', + isCodeSnippet: false, + }, + ], + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-03', + correctAnswer: 'c1', + slug: 'javascript-functions', + questionType: 'MULTIPLE_CHOICE', + difficulty: 'MEDIUM', + userAnswers: [ + { + uid: 'ua3', + userUid: 'user123', + questionUid: '3', + userAnswerUid: 'c2', + correctAnswer: false, + createdAt: new Date(), + updatedAt: new Date(), + questionDate: '2023-01-03', + timeTaken: 30, + difficulty: 'MEDIUM', + }, + ], + }, +]; + +// Triple the questions to ensure smooth infinite scroll +const allQuestions = [...questions, ...questions, ...questions]; + +// Mock study path +const studyPath: Partial = { + slug: 'javascript-fundamentals', + title: 'JavaScript Fundamentals', + description: 'Learn the fundamentals of JavaScript programming', + heroChip: 'Master JavaScript from scratch', + questionSlugs: ['javascript-variables', 'javascript-arrays', 'javascript-functions'], + educationLevel: 'beginner', +}; + +export default function InfiniteScrollingRoadmapCards({ className }: { className?: string }) { + return ( + <> + {/** 'infinite' scrolling animation of the study path buttons. */} + {/* Scrolling content */} +
+ {/** top fade effect */} +
+ +
+ {/* First set of items */} + + + {/* Clone of the first set to create seamless loop */} +
+ +
+
+ + {/* Bottom fade effect */} +
+
+ + ); +} diff --git a/src/hooks/use-onboarding-steps.ts b/src/hooks/use-onboarding-steps.ts index 4ffba1046..842931a5c 100644 --- a/src/hooks/use-onboarding-steps.ts +++ b/src/hooks/use-onboarding-steps.ts @@ -16,9 +16,6 @@ export const STEPS = { INITIAL_QUESTIONS: 'INITIAL_QUESTIONS', // give the user 3 very simple multiple choice questions to gauge skill level and give them quick wins! TIME_COMMITMENT: 'TIME_COMMITMENT', // get the users daily coding goal NOTIFICATIONS: 'NOTIFICATIONS', // offer push notifications - TAGS: 'TAGS', // get the users interests - PRICING: 'PRICING', // get the users pricing plan - QUESTIONS: 'QUESTIONS', // get the users questions FIRST_QUESTION_SELECTION: 'FIRST_QUESTION_SELECTION', // get the users first question (either send them to the first question or the tag selection) } as const; @@ -31,12 +28,10 @@ export function useOnboardingSteps() { const { serverUser, user, - handleGetOnboardingQuestions, canContinue, setCanContinue, timeSpendingPerDay, firstQuestionSelection, - FIRST_QUESTION_TUTORIAL_SLUG, totalXp, } = useOnboardingContext(); const [isLoading, setIsLoading] = useState(false); @@ -74,34 +69,17 @@ export function useOnboardingSteps() { component: 'OnboardingNotifications', }, [STEPS.FIRST_QUESTION_SELECTION]: { - next: STEPS.TAGS, + next: 'ROADMAP_PAGE', component: 'OnboardingFirstQuestionSelection', }, - [STEPS.TAGS]: { - next: STEPS.PRICING, - component: 'OnboardingTags', - }, - [STEPS.PRICING]: { - next: STEPS.QUESTIONS, - component: 'OnboardingPricing', - }, - [STEPS.QUESTIONS]: { - next: 'DASHBOARD', - component: 'OnboardingQuestions', - }, } as const; const handleSkip = () => { - const nextStep = stepConfig[currentStep].next; - if (nextStep === 'DASHBOARD') { + const nextStep = stepConfig[currentStep]?.next; + if (nextStep === 'ROADMAP_PAGE') { localStorage.removeItem('onboarding'); window.location.hash = ''; - router.push('/dashboard?onboarding=true'); - } - // if the user is skipping past the tags, redirect to the dashboard - else if (nextStep === STEPS.TAGS) { - localStorage.removeItem('onboarding'); - router.push('/dashboard?onboarding=true'); + router.push('/roadmaps?onboarding=true'); } else { setCurrentStepState(nextStep as StepKey); } @@ -117,24 +95,12 @@ export function useOnboardingSteps() { currentStep === STEPS.FIRST_QUESTION_SELECTION && firstQuestionSelection === 'startFromScratch' ) { - setCurrentStepState(STEPS.PRICING); + // Navigate to roadmap instead of QUESTIONS step + localStorage.removeItem('onboarding'); + router.push('/roadmaps?onboarding=true'); return; } - // Handle pricing step based on user's first question selection - if (currentStep === STEPS.PRICING) { - if (firstQuestionSelection === 'startFromScratch') { - localStorage.removeItem('onboarding'); - router.push(`/question/${FIRST_QUESTION_TUTORIAL_SLUG}?tutorial=true`); - return; - } else if (firstQuestionSelection === 'personalizeLearning') { - await handleGetOnboardingQuestions(); - localStorage.removeItem('onboarding'); - setCurrentStepState(STEPS.QUESTIONS); - return; - } - } - // Handle time commitment step if (currentStep === STEPS.TIME_COMMITMENT) { await updateUser({ userDetails: { ...user, timeSpendingPerDay } }); @@ -197,11 +163,11 @@ export function useOnboardingSteps() { // Handle all other steps await updateUser({ userDetails: user }); - const nextStep = stepConfig[currentStep].next; + const nextStep = stepConfig[currentStep]?.next; - if (nextStep === 'DASHBOARD') { + if (nextStep === 'ROADMAP_PAGE') { localStorage.removeItem('onboarding'); - router.push('/dashboard?onboarding=true'); + router.push('/roadmaps?onboarding=true'); } else { setCurrentStepState(nextStep as StepKey); } @@ -218,15 +184,9 @@ export function useOnboardingSteps() { return; } - // if the user is on the pricing page, they need to go back to the tags page only if they did not - // start from scratch - if (currentStep === STEPS.PRICING && firstQuestionSelection !== 'startFromScratch') { - return setCurrentStepState(STEPS.TAGS); - } - // Get the previous step const previousStep = Object.entries(stepConfig).find( - ([_, config]) => config.next === currentStep + ([, config]) => config.next === currentStep )?.[0]; if (previousStep) { @@ -260,12 +220,7 @@ export function useOnboardingSteps() { return false; } - // Don't show back button on final steps - if (currentStep === STEPS.QUESTIONS || currentStep === STEPS.PRICING) { - return false; - } - - // Show back button on intermediate steps + // Show back button on all other steps return true; }; From d2dde459fbd5ded6a847139264d7a2217f7717c0 Mon Sep 17 00:00:00 2001 From: Logan Ford <110533855+Logannford@users.noreply.github.com> Date: Wed, 30 Apr 2025 00:26:48 +0100 Subject: [PATCH 08/14] chore: updates question count --- src/utils/constants/misc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/constants/misc.ts b/src/utils/constants/misc.ts index 6a1195bfc..25474c98e 100644 --- a/src/utils/constants/misc.ts +++ b/src/utils/constants/misc.ts @@ -1,3 +1,3 @@ -export const QUESTIONS_COUNT = 330; +export const QUESTIONS_COUNT = 350; export const ROADMAP_QUESTION_COUNT = 6; From b8a849909f47952b6976c9d505dc4794868f4ab8 Mon Sep 17 00:00:00 2001 From: Logan Ford <110533855+Logannford@users.noreply.github.com> Date: Sat, 3 May 2025 20:27:52 +0100 Subject: [PATCH 09/14] feat: adds new component to different areas --- src/actions/user/authed/get-user.ts | 2 +- .../onboarding/onboarding-first-question-selection.tsx | 2 -- .../marketing/global/blocks/three-block-showcase.tsx | 9 --------- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/actions/user/authed/get-user.ts b/src/actions/user/authed/get-user.ts index 85207b727..b5d3b940f 100644 --- a/src/actions/user/authed/get-user.ts +++ b/src/actions/user/authed/get-user.ts @@ -5,7 +5,7 @@ import type { UserRecord } from '@/types'; import { revalidateTag } from 'next/cache'; /** - * Get the user from the server - used in api routes, server componets & server actions + * Get the user from the server - used in api routes, server components & server actions * * @returns User | null */ diff --git a/src/components/app/onboarding/onboarding-first-question-selection.tsx b/src/components/app/onboarding/onboarding-first-question-selection.tsx index 674508478..894cd3452 100644 --- a/src/components/app/onboarding/onboarding-first-question-selection.tsx +++ b/src/components/app/onboarding/onboarding-first-question-selection.tsx @@ -1,8 +1,6 @@ import { CardHeader } from '@/components/ui/card'; import { useOnboardingContext } from '@/contexts/onboarding-context'; import { motion } from 'framer-motion'; -import type { Question } from '@/types'; -import { StudyPath } from '@/utils/constants/study-paths'; import InfiniteScrollingRoadmapCards from '@/components/shared/infinite-scrolling-roadmap-cards'; export default function OnboardingFirstQuestionSelection() { diff --git a/src/components/marketing/global/blocks/three-block-showcase.tsx b/src/components/marketing/global/blocks/three-block-showcase.tsx index 87476e248..f57c9a528 100644 --- a/src/components/marketing/global/blocks/three-block-showcase.tsx +++ b/src/components/marketing/global/blocks/three-block-showcase.tsx @@ -1,9 +1,6 @@ import ArcheryTarget from '@/components/ui/icons/target'; import { cn } from '@/lib/utils'; import AnimatedAIQuestionHelpCard from '../../homepage/personalized/ai-help-demo'; -import StudyPathsList from '@/components/app/study-paths/list'; -import { getQuestions } from '@/actions/questions/admin/list'; -import { getStudyPath } from '@/utils/data/study-paths/get'; import InfiniteScrollingRoadmapCards from '@/components/shared/infinite-scrolling-roadmap-cards'; /** @@ -46,12 +43,6 @@ export default async function ThreeBlockShowcase({ const cardDescriptionClasses = 'absolute bottom-0 left-6 translate-y-full transition-transform duration-300 md:group-hover:translate-y-[-1rem] md:group-hover:translate-y-[-1rem] text-sm text-gray-400'; - // hardcoded study path and questions - const studyPath = await getStudyPath('javascript-fundamentals'); - const questions = await getQuestions({ - questionSlugs: studyPath?.questionSlugs.slice(0, 3) ?? [], - }); - return (
From 8e9dabd065800b29853daee723614287a5ea0ad4 Mon Sep 17 00:00:00 2001 From: Logan Ford <110533855+Logannford@users.noreply.github.com> Date: Sat, 3 May 2025 21:17:02 +0100 Subject: [PATCH 10/14] feat: start of onboarding tour on roadmaps page --- .../(roadmaps)/roadmaps/page.tsx | 77 ++++++++++++------- .../(questions)/question/[slug]/layout.tsx | 63 ++------------- src/hooks/use-onboarding-steps.ts | 2 +- src/lib/onborda.tsx | 4 +- 4 files changed, 58 insertions(+), 88 deletions(-) diff --git a/src/app/(app)/(default_layout)/(roadmaps)/roadmaps/page.tsx b/src/app/(app)/(default_layout)/(roadmaps)/roadmaps/page.tsx index afd94b7e1..8b04f43c2 100644 --- a/src/app/(app)/(default_layout)/(roadmaps)/roadmaps/page.tsx +++ b/src/app/(app)/(default_layout)/(roadmaps)/roadmaps/page.tsx @@ -12,9 +12,12 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip import { createMetadata } from '@/utils/seo'; import { getStudyPathsAndGroupByCategory } from '@/utils/data/study-paths/get'; import { getBaseUrl } from '@/utils'; +import { roadmapPageSteps } from '@/lib/onborda'; // types import { WebPageJsonLd } from '@/types'; +import { Onborda, OnbordaProvider } from 'onborda'; +import { TourCard } from '@/components/app/shared/question/tour-card'; export async function generateMetadata() { return createMetadata({ @@ -58,7 +61,14 @@ const heroDescription = (
); -export default async function ExploreQuestionsPage() { +export default async function ExploreQuestionsPage({ + searchParams, +}: { + searchParams: { [key: string]: string | string[] | undefined }; +}) { + // used to determine if the onboarding tour guide needs to be shown. + const { onboarding } = searchParams; + // create json ld const jsonLd: WebPageJsonLd = { '@context': 'https://schema.org', @@ -109,35 +119,46 @@ export default async function ExploreQuestionsPage() { type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> -
- -
-
- {categories.map((category) => ( -
-
-

{category}

- {studyPathsByCategory[category][0].categoryToolTip && ( - - - - - - {studyPathsByCategory[category][0].categoryToolTip} - - - )} -
-
- {studyPathsByCategory[category].map((studyPath) => ( - - ))} -
+ + +
+ +
+
+ {categories.map((category) => ( +
+
+

{category}

+ {studyPathsByCategory[category][0].categoryToolTip && ( + + + + + + {studyPathsByCategory[category][0].categoryToolTip} + + + )} +
+
+ {studyPathsByCategory[category].map((studyPath) => ( + + ))} +
+
+ ))}
- ))} +
-
-
+ + ); } diff --git a/src/app/(questions)/question/[slug]/layout.tsx b/src/app/(questions)/question/[slug]/layout.tsx index a75de462a..148ca035e 100644 --- a/src/app/(questions)/question/[slug]/layout.tsx +++ b/src/app/(questions)/question/[slug]/layout.tsx @@ -7,23 +7,23 @@ import { redirect } from 'next/navigation'; import { QuestionSingleContextProvider } from '@/contexts/question-single-context'; // Actions & Utils -import { createMetadata, getQuestionEducationLevel } from '@/utils/seo'; -import { capitalise, getBaseUrl } from '@/utils'; +import { createMetadata } from '@/utils/seo'; +import { capitalise } from '@/utils'; import { getQuestion } from '@/utils/data/questions/get'; import { getUser } from '@/actions/user/authed/get-user'; import { getRelatedQuestions } from '@/utils/data/questions/get-related'; import { getUserAnswer } from '@/utils/data/answers/get-user-answer'; import { getNextAndPreviousQuestion } from '@/utils/data/questions/question-navigation'; -import type { QuizJsonLd } from '@/types'; import { userHasAnsweredAnyQuestion } from '@/utils/data/questions/user-has-answered-any-question'; // Components import { Onborda, OnbordaProvider } from 'onborda'; -import { steps } from '@/lib/onborda'; import { TourCard } from '@/components/app/shared/question/tour-card'; import { getSuggestions } from '@/utils/data/questions/get-suggestions'; import QuestionPageHeader from '@/components/app/layout/question-single/page-header'; +import { questionPageSteps } from '@/lib/onborda'; + // Lazy Components const PremiumQuestionDeniedAccess = lazy( () => import('@/components/app/questions/premium-question-denied-access') @@ -73,55 +73,6 @@ export default async function QuestionUidLayout({ return redirect('/coding-challenges'); } - const defaultQuestionDescription = `Practice ${capitalise( - question?.title || question?.slug?.replace(/-/g, ' ') || 'Coding Question' - )} and improve your coding skills with interactive challenges on TechBlitz`; - - const description = - question?.questionType === 'SIMPLE_MULTIPLE_CHOICE' - ? question?.afterQuestionInfo || defaultQuestionDescription - : defaultQuestionDescription; - - // create json ld - const jsonLd: QuizJsonLd = { - '@context': 'https://schema.org', - '@type': 'Quiz', - name: capitalise(question?.slug?.replace(/-/g, ' ') || ''), - description, - url: `${getBaseUrl()}/question/${slug}`, - educationLevel: getQuestionEducationLevel(question?.difficulty || 'EASY'), - educationalUse: 'practice', - learningResourceType: ['quiz', 'learning activity'], - creator: { '@type': 'Organization', name: 'TechBlitz', url: getBaseUrl() }, - assesses: ['coding'], - dateCreated: question?.createdAt.toISOString() || '', - dateModified: question?.updatedAt.toISOString() || '', - datePublished: question?.questionDate || question?.createdAt.toISOString() || '', - headline: question?.question || '', - interactivityType: 'mixed', - isAccessibleForFree: true, - isFamilyFriendly: true, - teaches: 'coding', - potentialAction: { - '@type': 'SearchAction', - target: `${getBaseUrl()}/search?q={search_term_string}`, - 'query-input': 'required name=search_term_string', - }, - breadcrumb: { - '@type': 'BreadcrumbList', - itemListElement: [ - { '@type': 'ListItem', position: 1, name: 'Home', item: `${getBaseUrl()}` }, - { '@type': 'ListItem', position: 2, name: 'Questions', item: `${getBaseUrl()}/questions` }, - { - '@type': 'ListItem', - position: 3, - name: question.slug, - item: `${getBaseUrl()}/question/${slug}`, - }, - ], - }, - }; - // not resolving the promises here - passing the promises and // using 'use' to resolve them in their own components const nextAndPreviousQuestion = getNextAndPreviousQuestion(question.uid); // cached @@ -138,13 +89,9 @@ export default async function QuestionUidLayout({ return ( <> -