diff --git a/ui/app/App.tsx b/ui/app/App.tsx index 2342ee7..e1f4c49 100644 --- a/ui/app/App.tsx +++ b/ui/app/App.tsx @@ -81,6 +81,7 @@ const ShellLayout = () => { {activeTab === "missions" && ( handleTabChange(TAB_ORDER.indexOf("progress"))} /> )} diff --git a/ui/app/components/AppSidebar.tsx b/ui/app/components/AppSidebar.tsx index d2e1042..72d2321 100644 --- a/ui/app/components/AppSidebar.tsx +++ b/ui/app/components/AppSidebar.tsx @@ -22,7 +22,7 @@ import { GroupIcon, type SvgIconProps, } from "@dynatrace/strato-icons"; -import { TOPIC_META_ORDERED, TOPIC_META } from "../types/UserState"; +import { TOPIC_META } from "../types/UserState"; import type { TopicId } from "../types/UserState"; import { MISSIONS } from "../data/missions"; import { useUserStateContext } from "../context/UserStateContext"; @@ -80,7 +80,6 @@ export const AppSidebar = ({ activeTab, onFilterChange, onSwitchToMissions }: Ap }, []); // eslint-disable-line react-hooks/exhaustive-deps const [statusOpen, setStatusOpen] = useState(true); - const [difficultyOpen, setDifficultyOpen] = useState(true); const [topicOpen, setTopicOpen] = useState(true); @@ -114,21 +113,6 @@ export const AppSidebar = ({ activeTab, onFilterChange, onSwitchToMissions }: Ap { value: "completed", label: "Completed", count: statusCounts.completed }, ]; - const difficultyCounts = useMemo(() => { - const total = MISSIONS.length; - const rookie = MISSIONS.filter((m) => m.difficulty === "rookie").length; - const operator = MISSIONS.filter((m) => m.difficulty === "operator").length; - const elite = MISSIONS.filter((m) => m.difficulty === "elite").length; - return { total, rookie, operator, elite }; - }, []); - - const difficultyOptions: { value: "rookie" | "operator" | "elite" | null; label: string; count: number }[] = [ - { value: null, label: "All", count: difficultyCounts.total }, - { value: "rookie", label: "Rookie", count: difficultyCounts.rookie }, - { value: "operator", label: "Operator", count: difficultyCounts.operator }, - { value: "elite", label: "Elite", count: difficultyCounts.elite }, - ]; - return (
- {/* Difficulty section */} -
-
setDifficultyOpen((v) => !v)} - style={{ - display: "flex", - justifyContent: "space-between", - alignItems: "center", - cursor: "pointer", - fontSize: "12px", - fontWeight: 600, - textTransform: "uppercase", - letterSpacing: "0.5px", - color: "var(--dt-colors-text-neutral-subdued)", - marginBottom: "6px", - }} - > - Difficulty - - ▾ - -
- {difficultyOpen && difficultyOptions.map((opt) => { - const isActive = filters.difficulty === opt.value; - return ( -
update("difficulty", opt.value)} - style={{ - display: "flex", - justifyContent: "space-between", - alignItems: "center", - padding: "4px 8px", - fontSize: "12px", - cursor: "pointer", - borderRadius: "4px", - background: isActive - ? "var(--dt-colors-background-container-neutral-default)" - : "transparent", - color: isActive - ? "var(--dt-colors-text-primary-default, #fff)" - : "var(--dt-colors-text-neutral-subdued)", - fontWeight: isActive ? 600 : 400, - transition: "background 0.15s", - }} - onMouseEnter={(e) => { - if (!isActive) { - e.currentTarget.style.background = "var(--dt-colors-background-container-neutral-subdued)"; - } - }} - onMouseLeave={(e) => { - if (!isActive) { - e.currentTarget.style.background = "transparent"; - } - }} - > - {opt.label} - - {opt.count} - -
- ); - })} -
- - {/* Divider */} -
- {/* Topic Track section */}
void; + currentDiscipline: Discipline; + onSelect: (discipline: Discipline, experienceLevel: ExperienceLevel) => void; +} + +export const ChangeDriverModal = ({ + isOpen, + onClose, + currentDiscipline, + onSelect, +}: ChangeDriverModalProps) => { + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()} + > +
+ Pick your driver. +
+
+ This changes your starting discipline and experience level. +
+
+ {DRIVER_PICKER_OPTIONS.map((driver) => { + const isSelected = currentDiscipline === driver.discipline; + return ( +
onSelect(driver.discipline, driver.experienceLevel)} + style={{ + padding: "16px 20px", + borderRadius: "8px", + cursor: "pointer", + border: isSelected + ? "2px solid var(--dt-colors-charts-categorical-default-12, #1496ff)" + : "1px solid var(--dt-colors-border-neutral-default)", + background: isSelected + ? "var(--dt-colors-background-container-neutral-default)" + : "transparent", + transition: "all 0.15s", + display: "flex", + alignItems: "center", + justifyContent: "space-between", + }} + onMouseEnter={(e) => { + if (!isSelected) e.currentTarget.style.background = "var(--dt-colors-background-container-neutral-subdued)"; + }} + onMouseLeave={(e) => { + if (!isSelected) e.currentTarget.style.background = "transparent"; + }} + > +
+
+ {driver.name} +
+
+ {driver.tier} +
+
+ {driver.description} +
+
+ +
+ ); + })} +
+
+
+ ); +}; diff --git a/ui/app/components/MissionCard.tsx b/ui/app/components/MissionCard.tsx index 975489b..ee2cbb8 100644 --- a/ui/app/components/MissionCard.tsx +++ b/ui/app/components/MissionCard.tsx @@ -117,6 +117,7 @@ interface MissionCardProps { isUnlocked: boolean; isCompleted: boolean; prerequisiteNames?: string[]; + tierLocked?: string | null; } export const MissionCard = ({ @@ -124,6 +125,7 @@ export const MissionCard = ({ isUnlocked, isCompleted, prerequisiteNames, + tierLocked, }: MissionCardProps) => { const navigate = useNavigate(); const hasUnmetPrereqs = !isUnlocked && prerequisiteNames && prerequisiteNames.length > 0; @@ -169,6 +171,26 @@ export const MissionCard = ({ Replay + ) : tierLocked ? ( + + + {"\uD83D\uDD12"} Locked + + ) : ( -
-
- - )} + color: "#fff", + marginTop: "4px", + }} + > + Your seat on the grid is ready. + + +
+
+ ) : ( + + {step >= 1 && step < 4 && } - {/* Screen 2 — Experience level selector (step 1 of 3) */} + {/* Step 1 — Experience level */} {step === 1 && ( @@ -472,7 +464,7 @@ export const OnboardingWizard = ({ onComplete }: OnboardingWizardProps) => { )} - {/* Screen 3 — Driver picker (step 2 of 5) */} + {/* Step 2 — Driver picker */} {step === 2 && ( @@ -534,7 +526,7 @@ export const OnboardingWizard = ({ onComplete }: OnboardingWizardProps) => { @@ -542,76 +534,8 @@ export const OnboardingWizard = ({ onComplete }: OnboardingWizardProps) => { )} - {/* Screen 4 — Pre-Season circuit picker (step 3 of 5) */} + {/* Step 3 — Country picker */} {step === 3 && ( - - - Pick your starting circuit. - - You can switch circuits at any time. - - - - - {preSeasonCircuits.map((circuit) => { - const isSelected = selectedCircuit === circuit.id; - const missionCount = circuit.missionIds.length; - return ( -
setSelectedCircuit(circuit.id)} - style={{ - ...cardStyle(isSelected), - display: "flex", - alignItems: "center", - justifyContent: "space-between", - }} - onMouseEnter={(e) => handleHover(e, isSelected, true)} - onMouseLeave={(e) => handleHover(e, isSelected, false)} - > -
-
- {circuit.name} -
-
- {circuit.description} -
-
- {missionCount} {missionCount === 1 ? "mission" : "missions"} -
-
- -
- ); - })} -
- - - - - -
- )} - - {/* Screen 5 — Country picker (step 4 of 5) */} - {step === 4 && ( Which country are you representing? @@ -667,11 +591,11 @@ export const OnboardingWizard = ({ onComplete }: OnboardingWizardProps) => { - + @@ -679,8 +603,8 @@ export const OnboardingWizard = ({ onComplete }: OnboardingWizardProps) => { )} - {/* Screen 6 — Starting path (step 5 of 5) */} - {step === 5 && startingCircuit && ( + {/* Step 4 — Confirm */} + {step === 4 && ( {/* Formation lights replace step indicator */}
@@ -701,32 +625,19 @@ export const OnboardingWizard = ({ onComplete }: OnboardingWizardProps) => { Your Grid Position, {firstName} - - -
- {startingCircuit.name} - - {getCircuitTier(startingCircuit.id)} - -
- + {startingCircuit && ( +
+ {startingCircuit.name} + {startingCircuit.description} -
+
{startingCircuit.missionIds.map((mId) => { const mission = MISSIONS.find((m) => m.id === mId); if (!mission) return null; @@ -740,7 +651,7 @@ export const OnboardingWizard = ({ onComplete }: OnboardingWizardProps) => { alignItems: "center", padding: "8px 12px", borderRadius: "6px", - background: "var(--dt-colors-background-container-neutral-subdued)", + background: "var(--dt-colors-background-container-neutral-default)", opacity: isLocked ? 0.45 : 1, }} > @@ -754,22 +665,29 @@ export const OnboardingWizard = ({ onComplete }: OnboardingWizardProps) => { ); })}
- - +
+ )} + + {!startingCircuit && ( +
+ + All circuits unlocked. Choose your first mission from the grid. + +
+ )} - You can change your path at any time from the Pace tab. + You can change your driver and circuit at any time. - {operatorReadiness && ( - - All drivers complete {operatorReadiness.name} first — 3 short missions - that build your foundation. - - )} - - +
- {/* Driver picker modal */} - {driverPickerOpen && ( -
setDriverPickerOpen(false)} + currentDiscipline={userState?.startingDiscipline ?? "incident-commander"} + onSelect={(discipline, experienceLevel) => { + void updateUserState({ startingDiscipline: discipline, experienceLevel }); + setDriverPickerOpen(false); }} - onClick={() => setDriverPickerOpen(false)} - > -
e.stopPropagation()} - > -
- Pick your driver. -
-
- This changes your starting discipline and experience level. -
-
- {DRIVER_PICKER_OPTIONS.map((driver) => { - const isSelected = userState?.startingDiscipline === driver.discipline; - return ( -
{ - void updateUserState({ - startingDiscipline: driver.discipline, - experienceLevel: driver.experienceLevel, - }); - setDriverPickerOpen(false); - }} - style={{ - padding: "16px 20px", - borderRadius: "8px", - cursor: "pointer", - border: isSelected - ? "2px solid var(--dt-colors-charts-categorical-default-12, #1496ff)" - : "1px solid var(--dt-colors-border-neutral-default)", - background: isSelected - ? "var(--dt-colors-background-container-neutral-default)" - : "transparent", - transition: "all 0.15s", - display: "flex", - alignItems: "center", - justifyContent: "space-between", - }} - onMouseEnter={(e) => { - if (!isSelected) e.currentTarget.style.background = "var(--dt-colors-background-container-neutral-subdued)"; - }} - onMouseLeave={(e) => { - if (!isSelected) e.currentTarget.style.background = "transparent"; - }} - > -
-
- {driver.name} -
-
- {driver.tier} -
-
- {driver.description} -
-
- -
- ); - })} -
-
-
- )} + />
{(["incident-commander", "developer", "platform-engineer", "sre"] as Discipline[]).map((disc) => { @@ -463,7 +356,14 @@ export const ProgressTab = ({ onSwitchTab }: ProgressTabProps) => { "incident-commander": { name: "The Strategist", driverName: "Arvid Lindblad", helmet: "/ui/assets/helmets/lindblad.png" }, "platform-engineer": { name: "The Builder", driverName: "Isack Hadjar", helmet: "/ui/assets/helmets/hadjar.png" }, }; + const difficultyTierMap: Record = { + "incident-commander": "Rookie", + developer: "Intermediate", + "platform-engineer": "Advanced", + sre: "Elite", + }; const info = driverInfo[disc]; + const difficultyTier = difficultyTierMap[disc]; const isStarting = userState.startingDiscipline === disc; const currentThresholdXP = XP_THRESHOLDS.slice().reverse().find((t) => progress.xp >= t.xp)?.xp ?? 0; @@ -502,12 +402,13 @@ export const ProgressTab = ({ onSwitchTab }: ProgressTabProps) => { />
{info.name} - {meta.label} - {info.driverName} + + {info.driverName} · {difficultyTier} + + + {progress.levelName} · {progress.xp} / {isMax ? "MAX" : `${next.xp} XP`} +
- - {progress.levelName} -
= TIER_XP_THRESHOLDS.elite) return "elite"; + if (totalXP >= TIER_XP_THRESHOLDS.advanced) return "advanced"; + if (totalXP >= TIER_XP_THRESHOLDS.intermediate) return "intermediate"; + return "rookie"; +} + +const TIER_ORDER: DriverTier[] = ["rookie", "intermediate", "advanced", "elite"]; + +export function isTierUnlocked(tier: DriverTier, unlockedTier: DriverTier): boolean { + return TIER_ORDER.indexOf(tier) <= TIER_ORDER.indexOf(unlockedTier); +} + +export function getNextTierInfo( + unlockedTier: DriverTier +): { tier: DriverTier; xpRequired: number; label: string } | null { + const idx = TIER_ORDER.indexOf(unlockedTier); + if (idx >= TIER_ORDER.length - 1) return null; + const nextTier = TIER_ORDER[idx + 1]; + const labelMap: Record = { + rookie: "Recruit", + intermediate: "Analyst", + advanced: "Specialist", + elite: "Expert", + }; + return { + tier: nextTier, + xpRequired: TIER_XP_THRESHOLDS[nextTier], + label: labelMap[nextTier], + }; +} diff --git a/ui/assets/ft-car.png b/ui/assets/ft-car.png index 2a1f810..aab3a23 100644 Binary files a/ui/assets/ft-car.png and b/ui/assets/ft-car.png differ diff --git a/ui/assets/helmets/hadjar.png b/ui/assets/helmets/hadjar.png index d6f1b63..a7c2ff8 100644 Binary files a/ui/assets/helmets/hadjar.png and b/ui/assets/helmets/hadjar.png differ diff --git a/ui/assets/helmets/lawson.png b/ui/assets/helmets/lawson.png index f2c4859..3c7008c 100644 Binary files a/ui/assets/helmets/lawson.png and b/ui/assets/helmets/lawson.png differ diff --git a/ui/assets/helmets/lindblad.png b/ui/assets/helmets/lindblad.png index 887cec2..fd1c3b6 100644 Binary files a/ui/assets/helmets/lindblad.png and b/ui/assets/helmets/lindblad.png differ diff --git a/ui/assets/helmets/verstappen.png b/ui/assets/helmets/verstappen.png index 4ebbde8..4727039 100644 Binary files a/ui/assets/helmets/verstappen.png and b/ui/assets/helmets/verstappen.png differ