Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
708488d
Update welcome mission with new checkpoints and content
claude Mar 26, 2026
ac48957
Update cp1 instruction to mention YouTube guides section
claude Mar 26, 2026
bdcbdcd
Replace mission-the-dock with updated checkpoints and content
claude Mar 27, 2026
2d49471
Add mission-know-your-wheel (WHEEL CHECK) to missions and ground-zero…
claude Mar 27, 2026
935c23b
Move mission-ask-the-ai from operator-readiness to ground-zero circuit
claude Mar 27, 2026
82e3eec
Update Driver Circuits rows to show difficulty tier and skill level s…
claude Mar 27, 2026
10d3681
Extract ChangeDriverModal component and add driver helmet to Missions…
claude Mar 27, 2026
c16c6fc
Remove difficulty filter UI from sidebar
claude Mar 27, 2026
9b25856
Update circuit selection when driver is picked via ChangeDriverModal
claude Mar 27, 2026
0321c70
Move mission-first-alert from operator-readiness to ground-zero circuit
claude Mar 27, 2026
dba56e2
Implement driver-based mission unlock system by circuit tier
claude Mar 27, 2026
287cf79
Remove circuit picker from onboarding and reassign missions by tier
claude Mar 27, 2026
0d9f1c6
feat: update driver helmet assets
self9dmin Mar 27, 2026
7a4caf5
Merge remote-tracking branch 'origin/claude/update-welcome-mission-0n…
self9dmin Mar 27, 2026
956e43f
feat: update helmet and ft-car assets
self9dmin Mar 27, 2026
b1b4a3e
Add 3 operator-tier missions to operator-readiness circuit
claude Mar 27, 2026
eaba97e
Merge remote-tracking branch 'origin/claude/update-welcome-mission-0n…
self9dmin Mar 27, 2026
64ed355
feat: update ft-car hero image
self9dmin Mar 27, 2026
d3e6b85
Replace onboarding welcome screen with full-bleed hero layout
claude Mar 27, 2026
5e51486
Merge remote-tracking branch 'origin/claude/update-welcome-mission-0n…
self9dmin Mar 27, 2026
4f0bd50
Fix welcome hero to fill full viewport height
claude Mar 27, 2026
36516a9
Merge remote-tracking branch 'origin/claude/update-welcome-mission-0n…
self9dmin Mar 27, 2026
70d4a60
Fix welcome hero spacing and show full user name
claude Mar 27, 2026
83a487e
Merge remote-tracking branch 'origin/claude/update-welcome-mission-0n…
self9dmin Mar 27, 2026
3ccf941
Add dark navy background to welcome hero to eliminate gap seam
claude Mar 27, 2026
871c5e2
Merge remote-tracking branch 'origin/claude/update-welcome-mission-0n…
self9dmin Mar 27, 2026
9926907
feat: update ft-car hero image v3
self9dmin Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ui/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const ShellLayout = () => {
{activeTab === "missions" && (
<MissionsTab
filters={filters}
onFilterChange={setFilters}
onSwitchTab={() => handleTabChange(TAB_ORDER.indexOf("progress"))}
/>
)}
Expand Down
109 changes: 1 addition & 108 deletions ui/app/components/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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 (
<div
style={{
Expand Down Expand Up @@ -236,97 +220,6 @@ export const AppSidebar = ({ activeTab, onFilterChange, onSwitchToMissions }: Ap
}}
/>

{/* Difficulty section */}
<div style={{ marginBottom: "16px" }}>
<div
onClick={() => 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
<span
style={{
display: "inline-block",
transition: "none",
transform: difficultyOpen ? "rotate(0deg)" : "rotate(-90deg)",
fontSize: "10px",
lineHeight: 1,
}}
>
</span>
</div>
{difficultyOpen && difficultyOptions.map((opt) => {
const isActive = filters.difficulty === opt.value;
return (
<div
key={opt.value ?? "__all__"}
onClick={() => 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}
<span
style={{
background: "rgba(255, 255, 255, 0.08)",
borderRadius: "10px",
padding: "1px 7px",
fontSize: "11px",
fontWeight: 500,
color: "inherit",
}}
>
{opt.count}
</span>
</div>
);
})}
</div>

{/* Divider */}
<div
style={{
height: "1px",
background: "var(--dt-colors-border-neutral-default)",
margin: "8px 0 16px 0",
}}
/>

{/* Topic Track section */}
<div>
<div
Expand Down
131 changes: 131 additions & 0 deletions ui/app/components/ChangeDriverModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import React from "react";
import { Heading } from "@dynatrace/strato-components/typography";
import type { Discipline, ExperienceLevel } from "../types/UserState";

interface DriverPickerOption {
discipline: Discipline;
experienceLevel: ExperienceLevel;
name: string;
tier: string;
description: string;
helmet: string;
}

const DRIVER_PICKER_OPTIONS: DriverPickerOption[] = [
{ discipline: "incident-commander", experienceLevel: "new", name: "Arvid Lindblad", tier: "Rookie", description: "Just arrived. Learn the platform and find your feet.", helmet: "/ui/assets/helmets/lindblad.png" },
{ discipline: "developer", experienceLevel: "learning", name: "Liam Lawson", tier: "Intermediate", description: "You know the basics. Now push harder.", helmet: "/ui/assets/helmets/lawson.png" },
{ discipline: "platform-engineer", experienceLevel: "experienced", name: "Isack Hadjar", tier: "Advanced", description: "Comfortable under pressure. Build on solid foundations.", helmet: "/ui/assets/helmets/hadjar.png" },
{ discipline: "sre", experienceLevel: "experienced", name: "Max Verstappen", tier: "Elite", description: "No hand-holding. Full stack, full pressure.", helmet: "/ui/assets/helmets/verstappen.png" },
];

interface ChangeDriverModalProps {
isOpen: boolean;
onClose: () => void;
currentDiscipline: Discipline;
onSelect: (discipline: Discipline, experienceLevel: ExperienceLevel) => void;
}

export const ChangeDriverModal = ({
isOpen,
onClose,
currentDiscipline,
onSelect,
}: ChangeDriverModalProps) => {
if (!isOpen) return null;

return (
<div
style={{
position: "fixed",
inset: 0,
zIndex: 1000,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "rgba(0, 0, 0, 0.6)",
}}
onClick={onClose}
>
<div
style={{
background: "var(--dt-colors-background-container-neutral-default, #1a1a2e)",
border: "1px solid var(--dt-colors-border-neutral-default)",
borderRadius: "12px",
padding: "24px",
maxWidth: "560px",
width: "90%",
}}
onClick={(e) => e.stopPropagation()}
>
<div style={{ textAlign: "center", marginBottom: "8px" }}>
<Heading level={4}>Pick your driver.</Heading>
</div>
<div style={{ textAlign: "center", opacity: 0.6, fontSize: "13px", marginBottom: "20px" }}>
This changes your starting discipline and experience level.
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "12px",
}}
>
{DRIVER_PICKER_OPTIONS.map((driver) => {
const isSelected = currentDiscipline === driver.discipline;
return (
<div
key={driver.discipline}
onClick={() => 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";
}}
>
<div>
<div style={{ fontSize: "14px", fontWeight: isSelected ? 600 : 500 }}>
{driver.name}
</div>
<div style={{ fontSize: "11px", opacity: 0.5, marginTop: "2px" }}>
{driver.tier}
</div>
<div style={{ fontSize: "12px", opacity: 0.6, marginTop: "6px" }}>
{driver.description}
</div>
</div>
<img
src={driver.helmet}
alt=""
style={{
width: "64px",
height: "64px",
objectFit: "contain",
flexShrink: 0,
opacity: isSelected ? 0.9 : 0.5,
pointerEvents: "none",
}}
/>
</div>
);
})}
</div>
</div>
</div>
);
};
22 changes: 22 additions & 0 deletions ui/app/components/MissionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,15 @@ interface MissionCardProps {
isUnlocked: boolean;
isCompleted: boolean;
prerequisiteNames?: string[];
tierLocked?: string | null;
}

export const MissionCard = ({
mission,
isUnlocked,
isCompleted,
prerequisiteNames,
tierLocked,
}: MissionCardProps) => {
const navigate = useNavigate();
const hasUnmetPrereqs = !isUnlocked && prerequisiteNames && prerequisiteNames.length > 0;
Expand Down Expand Up @@ -169,6 +171,26 @@ export const MissionCard = ({
Replay
</Button>
</Flex>
) : tierLocked ? (
<Tooltip text={tierLocked}>
<span
style={{
display: "inline-flex",
alignItems: "center",
gap: "6px",
padding: "4px 12px",
fontSize: "12px",
fontWeight: 500,
borderRadius: "4px",
border: "1px solid var(--dt-colors-border-neutral-disabled)",
background: "transparent",
color: "var(--dt-colors-text-neutral-disabled)",
cursor: "not-allowed",
}}
>
{"\uD83D\uDD12"} Locked
</span>
</Tooltip>
) : (
<Button
variant="emphasized"
Expand Down
Loading
Loading