Skip to content

Commit 47e6049

Browse files
authored
Merge pull request #42 from IbrahimAhmed8/fix/onboard-and-global-a11y
dynamic imports 48px touch targets n WCAG AA contrast
2 parents dc2d5eb + 3a5cb21 commit 47e6049

4 files changed

Lines changed: 337 additions & 248 deletions

File tree

next-env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3-
import "./.next/types/routes.d.ts";
3+
import "./.next/dev/types/routes.d.ts";
44

55
// NOTE: This file should not be edited
66
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export default function OnboardingAtmosphere() {
2+
return (
3+
<div className="pointer-events-none fixed inset-0 z-[-1] overflow-hidden bg-[#090909]">
4+
<div className="absolute inset-0 bg-[radial-gradient(circle_at_16%_22%,rgba(255,255,255,0.05),transparent_20%),radial-gradient(circle_at_82%_18%,rgba(255,255,255,0.04),transparent_18%),linear-gradient(180deg,#141414_0%,#0c0c0c_55%,#090909_100%)]" />
5+
<div
6+
className="absolute inset-0 opacity-[0.045]"
7+
style={{
8+
backgroundImage:
9+
'linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px)',
10+
backgroundSize: '140px 140px',
11+
maskImage: 'radial-gradient(circle at center, black 30%, transparent 82%)',
12+
}}
13+
/>
14+
<div className="absolute left-[-12%] top-[8%] h-[24rem] w-[24rem] rounded-full bg-white/[0.05] blur-[120px]" />
15+
<div className="absolute right-[-12%] top-[16%] h-[28rem] w-[28rem] rounded-full bg-white/[0.04] blur-[150px]" />
16+
<div className="absolute bottom-[-22%] left-[32%] h-[30rem] w-[38rem] rounded-full bg-white/[0.035] blur-[180px]" />
17+
</div>
18+
);
19+
}
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
'use client';
2+
3+
import {
4+
BadgeCheck,
5+
BriefcaseBusiness,
6+
ChartColumn,
7+
Check,
8+
Cloud,
9+
Code2,
10+
GraduationCap,
11+
Megaphone,
12+
Palette,
13+
Rocket,
14+
School,
15+
Shield,
16+
Smartphone,
17+
Sparkles,
18+
Zap,
19+
type LucideIcon,
20+
} from 'lucide-react';
21+
import {
22+
onboardingAgeRangeOptions,
23+
onboardingLearningGoalOptions,
24+
onboardingLearningPaceOptions,
25+
onboardingRoleOptions,
26+
type AgeRange,
27+
type LearningGoal,
28+
type LearningPace,
29+
} from '@/src/features/dashboard/student-profile-model';
30+
31+
type OnboardingRole = (typeof onboardingRoleOptions)[number]['value'];
32+
33+
const roleMeta: Record<OnboardingRole, { icon: LucideIcon; label: string; description: string }> = {
34+
'School Student (Class 8-12)': { icon: School, label: 'School Student', description: 'Class 8-12' },
35+
'College Student (Undergraduate)': { icon: GraduationCap, label: 'College Student', description: 'Undergraduate' },
36+
'Graduate / Postgraduate (I have a degree)': {
37+
icon: BadgeCheck,
38+
label: 'Graduate / Postgraduate',
39+
description: 'I have a degree',
40+
},
41+
'Working Professional': { icon: BriefcaseBusiness, label: 'Working Professional', description: 'Career track' },
42+
};
43+
44+
const goalMeta: Record<LearningGoal, { icon: LucideIcon; label: string; description: string }> = {
45+
'Artificial Intelligence & ML': { icon: Sparkles, label: 'AI & ML', description: 'Neural networks & automation' },
46+
'Web Development': { icon: Code2, label: 'Web Dev', description: 'Modern frameworks & logic' },
47+
'App Development': { icon: Smartphone, label: 'App Dev', description: 'iOS, Android & Flutter' },
48+
'Data Science & Analytics': { icon: ChartColumn, label: 'Data Science', description: 'Visualization & analysis' },
49+
'Cloud & DevOps': { icon: Cloud, label: 'Cloud & DevOps', description: 'Infrastructure & scale' },
50+
Cybersecurity: { icon: Shield, label: 'Cybersecurity', description: 'Defense & encryption' },
51+
'UI/UX Design': { icon: Palette, label: 'UI/UX', description: 'Interface & experience' },
52+
'Digital Marketing': { icon: Megaphone, label: 'Digital Marketing', description: 'Growth strategy' },
53+
'Entrepreneurship & Startups': { icon: Rocket, label: 'Entrepreneurship', description: 'Startups & business' },
54+
};
55+
56+
const paceMeta: Record<LearningPace, { eyebrow: string; duration: string; icon: LucideIcon; recommended?: boolean }> = {
57+
Light: { eyebrow: 'Casual progress', duration: '1-2 hrs/week', icon: Sparkles },
58+
Focused: { eyebrow: 'Balanced growth', duration: '3-5 hrs/week', icon: Zap, recommended: true },
59+
Intensive: { eyebrow: 'Rapid mastery', duration: '6+ hrs/week', icon: Rocket },
60+
};
61+
62+
export type OnboardingStepsContentProps = {
63+
currentStep: number;
64+
ageRange: AgeRange | null;
65+
selectedRole: OnboardingRole | null;
66+
selectedGoals: LearningGoal[];
67+
learningPace: LearningPace | null;
68+
stepSubtitle: string;
69+
onClearStatus: () => void;
70+
onAgeRange: (value: AgeRange) => void;
71+
onRole: (value: OnboardingRole) => void;
72+
onGoalToggle: (goal: LearningGoal) => void;
73+
onPace: (value: LearningPace) => void;
74+
};
75+
76+
export default function OnboardingStepsContent({
77+
currentStep,
78+
ageRange,
79+
selectedRole,
80+
selectedGoals,
81+
learningPace,
82+
stepSubtitle,
83+
onClearStatus,
84+
onAgeRange,
85+
onRole,
86+
onGoalToggle,
87+
onPace,
88+
}: OnboardingStepsContentProps) {
89+
if (currentStep === 0) {
90+
return (
91+
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
92+
{onboardingAgeRangeOptions.map((option) => {
93+
const selected = ageRange === option;
94+
return (
95+
<button
96+
key={option}
97+
type="button"
98+
onClick={() => {
99+
onClearStatus();
100+
onAgeRange(option);
101+
}}
102+
className={`min-h-[48px] rounded-[1.9rem] border p-5 text-left transition-all ${option === '29+' ? 'xl:col-span-2' : ''
103+
} ${selected ? 'border-white bg-white/[0.12]' : 'border-white/8 bg-white/[0.03] hover:border-white/16 hover:bg-white/[0.05]'}`}
104+
>
105+
<div className="font-mono text-[10px] uppercase tracking-[0.22em] text-white/50">
106+
Category_{String(onboardingAgeRangeOptions.indexOf(option) + 1).padStart(2, '0')}
107+
</div>
108+
<div className="mt-8 flex items-end justify-between gap-4">
109+
<div className="font-display text-3xl font-semibold leading-[0.95] text-white sm:text-4xl">{option}</div>
110+
<div
111+
className={`flex h-12 w-12 shrink-0 items-center justify-center rounded-full border ${selected ? 'border-white bg-white text-black' : 'border-white/12 text-transparent'}`}
112+
aria-hidden="true"
113+
>
114+
<Check size={14} aria-hidden="true" />
115+
</div>
116+
</div>
117+
</button>
118+
);
119+
})}
120+
</div>
121+
);
122+
}
123+
124+
if (currentStep === 1) {
125+
return (
126+
<div className="grid gap-4 xl:grid-cols-2">
127+
{onboardingRoleOptions.map((option) => {
128+
const selected = selectedRole === option.value;
129+
const meta = roleMeta[option.value];
130+
const Icon = meta.icon;
131+
return (
132+
<button
133+
key={option.value}
134+
type="button"
135+
onClick={() => {
136+
onClearStatus();
137+
onRole(option.value);
138+
}}
139+
className={`min-h-[48px] rounded-[1.9rem] border p-5 text-left transition-all ${selected ? 'border-white bg-white/[0.12]' : 'border-white/8 bg-white/[0.03] hover:border-white/16 hover:bg-white/[0.05]'}`}
140+
>
141+
<div className="flex items-start justify-between gap-4">
142+
<div className="flex h-12 w-12 items-center justify-center rounded-full border border-white/10 bg-white/[0.06] text-white/82">
143+
<Icon size={18} aria-hidden="true" />
144+
</div>
145+
<div
146+
className={`flex h-12 w-12 shrink-0 items-center justify-center rounded-full border ${selected ? 'border-white bg-white text-black' : 'border-white/12 text-transparent'}`}
147+
aria-hidden="true"
148+
>
149+
<Check size={14} aria-hidden="true" />
150+
</div>
151+
</div>
152+
<div className="mt-8">
153+
<div className="font-display text-[1.8rem] font-semibold leading-[0.98] text-white sm:text-[2rem]">{meta.label}</div>
154+
<div className="mt-3 text-sm text-white/72">{meta.description}</div>
155+
</div>
156+
</button>
157+
);
158+
})}
159+
</div>
160+
);
161+
}
162+
163+
if (currentStep === 2) {
164+
return (
165+
<div className="space-y-6">
166+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
167+
<div className="text-sm text-white/72">{stepSubtitle}</div>
168+
<div className="inline-flex min-h-12 items-center rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 font-mono text-[10px] uppercase tracking-[0.18em] text-white/70">
169+
Select up to 3 topics
170+
</div>
171+
</div>
172+
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
173+
{onboardingLearningGoalOptions.map((goal) => {
174+
const selected = selectedGoals.includes(goal);
175+
const meta = goalMeta[goal];
176+
const Icon = meta.icon;
177+
return (
178+
<button
179+
key={goal}
180+
type="button"
181+
onClick={() => onGoalToggle(goal)}
182+
className={`min-h-[48px] rounded-[1.7rem] border p-4 text-left transition-all ${selected ? 'border-white bg-white/[0.12]' : 'border-white/8 bg-white/[0.03] hover:border-white/16 hover:bg-white/[0.05]'}`}
183+
>
184+
<div className="flex items-start justify-between gap-3">
185+
<div className="flex h-12 w-12 items-center justify-center rounded-full border border-white/10 bg-white/[0.05] text-white/78">
186+
<Icon size={15} aria-hidden="true" />
187+
</div>
188+
<div
189+
className={`mt-1 h-4 w-4 shrink-0 rounded-full border ${selected ? 'border-white bg-white' : 'border-white/15 bg-transparent'}`}
190+
aria-hidden="true"
191+
/>
192+
</div>
193+
<div className="mt-8">
194+
<div className="font-display text-[1.5rem] font-semibold leading-[1] text-white">{meta.label}</div>
195+
<div className="mt-2 text-sm leading-relaxed text-white/70">{meta.description}</div>
196+
</div>
197+
</button>
198+
);
199+
})}
200+
</div>
201+
</div>
202+
);
203+
}
204+
205+
return (
206+
<div className="space-y-4">
207+
{onboardingLearningPaceOptions.map((pace) => {
208+
const selected = learningPace === pace;
209+
const meta = paceMeta[pace];
210+
const Icon = meta.icon;
211+
return (
212+
<button
213+
key={pace}
214+
type="button"
215+
onClick={() => {
216+
onClearStatus();
217+
onPace(pace);
218+
}}
219+
className={`relative min-h-[48px] w-full rounded-[1.9rem] border px-5 py-5 text-left transition-all ${selected ? 'border-white bg-white/[0.12]' : 'border-white/8 bg-white/[0.03] hover:border-white/16 hover:bg-white/[0.05]'}`}
220+
>
221+
{meta.recommended ? (
222+
<div className="absolute right-4 top-3 rounded-full border border-white/14 bg-white px-2 py-1 font-mono text-[9px] uppercase tracking-[0.14em] text-black">
223+
Recommended
224+
</div>
225+
) : null}
226+
<div className="flex items-center justify-between gap-4">
227+
<div>
228+
<div className="font-mono text-[10px] uppercase tracking-[0.18em] text-white/55">{meta.eyebrow}</div>
229+
<div className="mt-3 font-display text-[2rem] font-semibold leading-none text-white">{pace}</div>
230+
<div className="mt-2 text-base text-white/72">{meta.duration}</div>
231+
</div>
232+
<div className="flex flex-col items-end gap-3">
233+
<div
234+
className={`flex h-12 w-12 items-center justify-center rounded-full border ${selected ? 'border-white bg-white text-black' : 'border-white/12 bg-white/[0.04] text-white/72'}`}
235+
>
236+
<Icon size={18} aria-hidden="true" />
237+
</div>
238+
<div className={`h-4 w-4 rounded-full border-2 ${selected ? 'border-white bg-white' : 'border-white/16'}`} aria-hidden="true" />
239+
</div>
240+
</div>
241+
</button>
242+
);
243+
})}
244+
</div>
245+
);
246+
}

0 commit comments

Comments
 (0)