Skip to content

Commit f6f97ab

Browse files
mgreichclaude
andcommitted
Add AI practices explorer application
Interactive explorer for AI engineering practices with journey map and SDLC views, detail panel, Google Sheets integration, and progress tracking via localStorage. Includes types, constants, hooks, utility functions, and all React components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ecedbe0 commit f6f97ab

15 files changed

Lines changed: 1030 additions & 0 deletions

src/App.tsx

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { useState } from "react";
2+
import type { ViewType } from "./types.ts";
3+
import { usePractices } from "./hooks/usePractices.ts";
4+
import { useCompletedPractices } from "./hooks/useCompletedPractices.ts";
5+
import { Header } from "./components/Header.tsx";
6+
import { SetupPanel } from "./components/SetupPanel.tsx";
7+
import { JourneyView } from "./components/JourneyView.tsx";
8+
import { SDLCView } from "./components/SDLCView.tsx";
9+
import { DetailPanel } from "./components/DetailPanel.tsx";
10+
11+
export default function App() {
12+
const { practices, loading, error, dataSource, showSetup, setShowSetup, fetchSheet, sheetUrl, refreshSheet, disconnect } =
13+
usePractices();
14+
const { completed, toggleComplete, isUnlocked, progress } = useCompletedPractices(practices);
15+
16+
const [view, setView] = useState<ViewType>("journey");
17+
const [selected, setSelected] = useState<string | null>(null);
18+
const [hoveredPhase, setHoveredPhase] = useState<string | null>(null);
19+
const [hoveredNode, setHoveredNode] = useState<string | null>(null);
20+
21+
const selectedPractice = practices.find((p) => p.id === selected);
22+
23+
return (
24+
<div className="min-h-screen">
25+
<div className="sticky top-0 z-50">
26+
<Header
27+
dataSource={dataSource}
28+
practiceCount={practices.length}
29+
showSetup={showSetup}
30+
setShowSetup={setShowSetup}
31+
view={view}
32+
setView={setView}
33+
setSelected={setSelected}
34+
completedCount={completed.size}
35+
progress={progress}
36+
loading={loading}
37+
refreshSheet={refreshSheet}
38+
/>
39+
40+
{showSetup && <SetupPanel loading={loading} error={error} fetchSheet={fetchSheet} sheetUrl={sheetUrl} disconnect={disconnect} isConnected={dataSource === "sheet"} />}
41+
</div>
42+
43+
<div className="flex min-h-[calc(100vh-73px)]">
44+
<div className="flex-1 p-7 overflow-auto">
45+
{view === "journey" && (
46+
<JourneyView
47+
practices={practices}
48+
completed={completed}
49+
selected={selected}
50+
setSelected={setSelected}
51+
isUnlocked={isUnlocked}
52+
hoveredNode={hoveredNode}
53+
setHoveredNode={setHoveredNode}
54+
/>
55+
)}
56+
{view === "sdlc" && (
57+
<SDLCView
58+
practices={practices}
59+
completed={completed}
60+
selected={selected}
61+
setSelected={setSelected}
62+
hoveredPhase={hoveredPhase}
63+
setHoveredPhase={setHoveredPhase}
64+
/>
65+
)}
66+
</div>
67+
68+
<div
69+
className="overflow-hidden shrink-0 bg-white/2 transition-[width] duration-300 ease-[cubic-bezier(0.4,0,0.2,1)]"
70+
style={{
71+
width: selected ? 380 : 0,
72+
borderLeft: selected ? "1px solid rgba(255,255,255,0.06)" : "none",
73+
}}
74+
>
75+
{selectedPractice && (
76+
<DetailPanel
77+
practice={selectedPractice}
78+
completed={completed}
79+
toggleComplete={toggleComplete}
80+
isUnlocked={isUnlocked(selectedPractice)}
81+
onClose={() => setSelected(null)}
82+
allPractices={practices}
83+
/>
84+
)}
85+
</div>
86+
</div>
87+
</div>
88+
);
89+
}

src/components/DetailPanel.tsx

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import type { Practice } from "../types.ts";
2+
import { PHASES, TIER_CONFIG } from "../constants.ts";
3+
import { Label } from "./Label.tsx";
4+
5+
interface DetailPanelProps {
6+
practice: Practice;
7+
completed: Set<string>;
8+
toggleComplete: (id: string) => void;
9+
isUnlocked: boolean;
10+
onClose: () => void;
11+
allPractices: Practice[];
12+
}
13+
14+
export function DetailPanel({
15+
practice,
16+
completed,
17+
toggleComplete,
18+
isUnlocked,
19+
onClose,
20+
allPractices,
21+
}: DetailPanelProps) {
22+
const cfg = TIER_CONFIG[practice.tier] || TIER_CONFIG.low;
23+
const done = completed.has(practice.id);
24+
const prereqPractices = practice.prereqs
25+
.map((id) => allPractices.find((p) => p.id === id))
26+
.filter((p): p is Practice => p !== undefined);
27+
const dependents = allPractices.filter((p) => p.prereqs.includes(practice.id));
28+
29+
return (
30+
<div className="p-5 w-[344px]">
31+
<div className="flex justify-end mb-2.5">
32+
<button
33+
onClick={onClose}
34+
className="bg-white/6 border-none text-zinc-500 cursor-pointer px-2 py-0.5 rounded text-[13px] hover:bg-white/10 transition-colors"
35+
>
36+
{"\u2715"}
37+
</button>
38+
</div>
39+
40+
<div
41+
className="inline-block px-2 py-0.5 text-[10px] font-bold font-mono uppercase tracking-wider rounded mb-2.5"
42+
style={{
43+
color: cfg.color,
44+
background: cfg.bg + "33",
45+
border: `1px solid ${cfg.border}44`,
46+
}}
47+
>
48+
{cfg.label} &middot; {practice.effort} Effort
49+
</div>
50+
51+
<h3 className="text-xl font-bold font-mono tracking-tight mb-2.5 text-zinc-100">
52+
{practice.title}
53+
</h3>
54+
<p className="text-[13px] text-zinc-400 leading-relaxed mb-4.5">
55+
{practice.description}
56+
</p>
57+
58+
<Label>SDLC Phases</Label>
59+
<div className="flex gap-1.5 flex-wrap mb-4.5">
60+
{PHASES.map((ph) => {
61+
const active = practice.phases.includes(ph.id);
62+
return (
63+
<span
64+
key={ph.id}
65+
className="text-[11px] px-2 py-0.5 rounded-[5px] font-mono"
66+
style={{
67+
background: active ? "rgba(96,165,250,0.1)" : "rgba(255,255,255,0.03)",
68+
color: active ? "#60a5fa" : "#3f3f46",
69+
border: active ? "1px solid rgba(96,165,250,0.2)" : "1px solid transparent",
70+
}}
71+
>
72+
{ph.icon} {ph.label}
73+
</span>
74+
);
75+
})}
76+
</div>
77+
78+
{practice.actions.length > 0 && (
79+
<>
80+
<Label>Key Actions</Label>
81+
<div className="flex flex-col gap-1.5 mb-4.5">
82+
{practice.actions.map((a, i) => (
83+
<div
84+
key={i}
85+
className="text-[12.5px] text-zinc-400 px-2 py-1 bg-white/[0.025] rounded-[5px]"
86+
style={{ borderLeft: `2px solid ${cfg.color}44` }}
87+
>
88+
{a}
89+
</div>
90+
))}
91+
</div>
92+
</>
93+
)}
94+
95+
{prereqPractices.length > 0 && (
96+
<>
97+
<Label>Prerequisites</Label>
98+
<div className="mb-4.5">
99+
{prereqPractices.map((p) => {
100+
const pCfg = TIER_CONFIG[p.tier] || TIER_CONFIG.low;
101+
return (
102+
<div
103+
key={p.id}
104+
className="flex items-center gap-2 text-[12.5px] py-0.5"
105+
style={{ color: completed.has(p.id) ? pCfg.color : "#71717a" }}
106+
>
107+
<span className="text-[11px]">{completed.has(p.id) ? "\u2705" : "\u2B1C"}</span>
108+
{p.title}
109+
</div>
110+
);
111+
})}
112+
</div>
113+
</>
114+
)}
115+
116+
{dependents.length > 0 && (
117+
<>
118+
<Label>Unlocks</Label>
119+
<div className="mb-5">
120+
{dependents.map((p) => (
121+
<div key={p.id} className="text-[12.5px] text-zinc-500 py-0.5">
122+
{"\u2192"} {p.title}
123+
</div>
124+
))}
125+
</div>
126+
</>
127+
)}
128+
129+
<button
130+
onClick={() => toggleComplete(practice.id)}
131+
disabled={!isUnlocked && !done}
132+
className="w-full py-2.5 rounded-lg text-[13px] font-semibold font-sans border-none transition-all"
133+
style={{
134+
cursor: isUnlocked || done ? "pointer" : "not-allowed",
135+
background: done
136+
? "rgba(255,255,255,0.06)"
137+
: isUnlocked
138+
? `linear-gradient(135deg, ${cfg.color}, ${cfg.border})`
139+
: "rgba(255,255,255,0.04)",
140+
color: done ? "#a1a1aa" : isUnlocked ? "#fff" : "#3f3f46",
141+
}}
142+
>
143+
{done
144+
? "\u21A9 Mark Incomplete"
145+
: isUnlocked
146+
? "\u2713 Mark Complete"
147+
: "\uD83D\uDD12 Complete Prerequisites First"}
148+
</button>
149+
</div>
150+
);
151+
}

src/components/Header.tsx

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import type { ViewType, DataSource } from "../types.ts";
2+
3+
interface HeaderProps {
4+
dataSource: DataSource;
5+
practiceCount: number;
6+
showSetup: boolean;
7+
setShowSetup: (fn: (prev: boolean) => boolean) => void;
8+
view: ViewType;
9+
setView: (v: ViewType) => void;
10+
setSelected: (id: string | null) => void;
11+
completedCount: number;
12+
progress: number;
13+
loading: boolean;
14+
refreshSheet: () => void;
15+
}
16+
17+
export function Header({
18+
dataSource,
19+
practiceCount,
20+
showSetup,
21+
setShowSetup,
22+
view,
23+
setView,
24+
setSelected,
25+
completedCount,
26+
progress,
27+
loading,
28+
refreshSheet,
29+
}: HeaderProps) {
30+
const isConnected = dataSource === "sheet";
31+
32+
return (
33+
<header className="flex flex-wrap items-center justify-between gap-3 border-b border-white/6 px-7 py-4 backdrop-blur-md bg-[rgba(10,10,15,0.85)]">
34+
<div className="flex items-center gap-4">
35+
<div>
36+
<h1 className="text-lg font-mono font-bold tracking-tight m-0 bg-gradient-to-br from-blue-400 to-purple-400 bg-clip-text text-transparent">
37+
AI Engineering Practices
38+
</h1>
39+
<p className="text-[11px] text-zinc-500 mt-0.5 font-mono">TXI Digital</p>
40+
</div>
41+
<button
42+
onClick={() => setShowSetup((s) => !s)}
43+
className="flex items-center gap-1.5 px-3 py-1 rounded-md text-[11px] font-mono cursor-pointer transition-colors"
44+
style={{
45+
background: isConnected ? "rgba(22,163,74,0.12)" : "rgba(255,255,255,0.05)",
46+
border: isConnected ? "1px solid rgba(22,163,74,0.3)" : "1px solid rgba(255,255,255,0.08)",
47+
color: isConnected ? "#4ade80" : "#71717a",
48+
}}
49+
aria-expanded={showSetup}
50+
>
51+
<span
52+
className="w-1.5 h-1.5 rounded-full"
53+
style={{ background: isConnected ? "#4ade80" : "#71717a" }}
54+
/>
55+
{isConnected ? `Sheet connected (${practiceCount} practices)` : "Demo data"}
56+
</button>
57+
{isConnected && (
58+
<button
59+
onClick={refreshSheet}
60+
disabled={loading}
61+
className="flex items-center gap-1.5 px-2.5 py-1 rounded-md text-[11px] font-mono cursor-pointer transition-colors bg-white/5 border border-white/8 text-zinc-400 hover:text-zinc-200 hover:bg-white/8 disabled:opacity-50 disabled:cursor-wait"
62+
title="Refresh data from Google Sheet"
63+
>
64+
<svg
65+
className="w-3 h-3"
66+
style={loading ? { animation: "spin 1s linear infinite" } : undefined}
67+
fill="none"
68+
viewBox="0 0 24 24"
69+
stroke="currentColor"
70+
strokeWidth={2}
71+
>
72+
<path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h5M20 20v-5h-5M20.49 9A9 9 0 0 0 5.64 5.64L4 9m16 6l-1.64 3.36A9 9 0 0 1 3.51 15" />
73+
</svg>
74+
{loading ? "Syncing\u2026" : "Refresh"}
75+
</button>
76+
)}
77+
</div>
78+
79+
<div className="flex items-center gap-2">
80+
<div className="flex rounded-lg p-0.5 bg-white/5 border border-white/8">
81+
{(["journey", "sdlc"] as const).map((v) => (
82+
<button
83+
key={v}
84+
onClick={() => {
85+
setView(v);
86+
setSelected(null);
87+
}}
88+
className="px-3.5 py-1.5 text-xs font-medium border-none rounded-md cursor-pointer font-sans transition-all"
89+
style={{
90+
background: view === v ? "rgba(255,255,255,0.1)" : "transparent",
91+
color: view === v ? "#e4e4e7" : "#71717a",
92+
}}
93+
>
94+
{v === "journey" ? "Journey" : "SDLC"}
95+
</button>
96+
))}
97+
</div>
98+
<div className="flex items-center gap-2 px-3 py-1 bg-white/4 rounded-full border border-white/8">
99+
<div className="w-12 h-1 bg-white/8 rounded-sm overflow-hidden">
100+
<div
101+
className="h-full bg-gradient-to-r from-blue-400 to-purple-400 rounded-sm transition-[width] duration-400"
102+
style={{ width: `${progress}%` }}
103+
/>
104+
</div>
105+
<span className="text-[11px] font-mono text-zinc-400">
106+
{completedCount}/{practiceCount}
107+
</span>
108+
</div>
109+
</div>
110+
</header>
111+
);
112+
}

0 commit comments

Comments
 (0)