Skip to content

Commit 9eb59e6

Browse files
rdmuellerclaude
andcommitted
refactor: move RiskRadar to components with CSS module, delete old file
Move main RiskRadar component to src/components/RiskRadar.jsx with RiskRadar.module.css. Update main.jsx import path. Delete old src/RiskRadar.jsx. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6c15123 commit 9eb59e6

4 files changed

Lines changed: 357 additions & 131 deletions

File tree

src/RiskRadar.jsx

Lines changed: 0 additions & 130 deletions
This file was deleted.

src/components/RiskRadar.jsx

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { useState } from "react";
2+
import T from "../i18n.js";
3+
import { useTheme } from "../theme.js";
4+
import { VERSION, TIER_BG, TYPE_COLORS } from "../constants.js";
5+
import { getTierIndex, detectBrowserLanguage } from "../utils.js";
6+
import RadarChart from "./RadarChart.jsx";
7+
import MitigationCard from "./MitigationCard.jsx";
8+
import DocSidebar from "./DocSidebar.jsx";
9+
import styles from "./RiskRadar.module.css";
10+
11+
export default function RiskRadar() {
12+
const [lang, setLang] = useState(() => {
13+
const saved = localStorage.getItem("lang");
14+
if (saved === "de" || saved === "en") return saved;
15+
return detectBrowserLanguage();
16+
});
17+
const { theme, setTheme, isDark } = useTheme();
18+
const [docsOpen, setDocsOpen] = useState(false);
19+
const [values, setValues] = useState({ codeType: 0, language: 1, deployment: 0, data: 0, blastRadius: 0 });
20+
const t = T[lang];
21+
const ti = getTierIndex(values);
22+
const tier = t.tiers[ti];
23+
const tc = TIER_BG[ti];
24+
const set = (k, v) => setValues((p) => ({ ...p, [k]: v }));
25+
const activeCount = t.mitigations.filter((g) => g.tier <= ti + 1).reduce((s, g) => s + g.measures.length, 0);
26+
27+
const toggleLang = () => {
28+
const next = lang === "de" ? "en" : "de";
29+
setLang(next);
30+
localStorage.setItem("lang", next);
31+
};
32+
33+
return (
34+
<div className={styles.app} style={{ marginRight: docsOpen ? "min(480px, 85vw)" : 0 }}>
35+
{/* Top bar */}
36+
<div className={styles.topBar}>
37+
<button onClick={() => setTheme(isDark ? "light" : "dark")} className={styles.btn} aria-label="Toggle theme">
38+
{isDark ? "\u2600\uFE0F" : "\uD83C\uDF19"} {isDark ? "Light" : "Dark"}
39+
</button>
40+
<button onClick={toggleLang} className={styles.btn}>
41+
{t.langSwitch}
42+
</button>
43+
<button
44+
onClick={() => setDocsOpen(!docsOpen)}
45+
className={styles.btn}
46+
style={{ background: docsOpen ? `${tc}22` : undefined, border: `1px solid ${docsOpen ? tc : "var(--border)"}`, color: docsOpen ? "var(--text-heading)" : undefined }}
47+
>
48+
{docsOpen ? t.closeButton : t.docsButton}
49+
</button>
50+
</div>
51+
52+
<h1 className={styles.title}>{t.title}</h1>
53+
<p className={styles.subtitle}>{t.subtitle}</p>
54+
55+
{/* Presets */}
56+
<div className={styles.presets}>
57+
{t.presets.map((p) => {
58+
const active = JSON.stringify(values) === JSON.stringify(p.values);
59+
return (
60+
<button
61+
key={p.name}
62+
onClick={() => setValues(p.values)}
63+
className={styles.presetBtn}
64+
style={{
65+
border: active ? `2px solid ${tc}` : "1px solid var(--border)",
66+
background: active ? `${tc}22` : "var(--bg-card)",
67+
color: active ? "var(--text-heading)" : "var(--text-muted)",
68+
fontWeight: active ? 600 : 400,
69+
}}
70+
>
71+
{p.name}
72+
</button>
73+
);
74+
})}
75+
</div>
76+
77+
<div className={styles.mainContent}>
78+
<div className={styles.chartWrapper}><RadarChart values={values} dimensions={t.dimensions} /></div>
79+
80+
{/* Tier badge */}
81+
<div className={styles.tierBadge} style={{ background: `${tc}18`, border: `2px solid ${tc}` }}>
82+
<span className={styles.tierNumber} style={{ color: tc }}>{ti + 1}</span>
83+
<div>
84+
<div className={styles.tierLabel} style={{ color: tc }}>{tier.label}</div>
85+
<div className={styles.tierDesc}>{tier.desc}</div>
86+
</div>
87+
</div>
88+
89+
{/* Sliders */}
90+
<div className={styles.sliders}>
91+
{t.dimensions.map((dim) => {
92+
const v = values[dim.key];
93+
const sc = TIER_BG[v <= 1 ? 0 : v <= 2 ? 1 : v <= 3 ? 2 : 3];
94+
return (
95+
<div key={dim.key} className={styles.sliderGroup}>
96+
<div className={styles.sliderHeader}>
97+
<span className={styles.sliderLabel}>{dim.label}</span>
98+
<span className={styles.sliderLevel} style={{ color: sc }}>{dim.levels[v]}</span>
99+
</div>
100+
<input type="range" min={0} max={4} step={1} value={v} onChange={(e) => set(dim.key, parseInt(e.target.value))} className={styles.slider} style={{ accentColor: sc }} />
101+
<div className={styles.sliderRange}>
102+
<span>{t.low}</span><span>{t.high}</span>
103+
</div>
104+
</div>
105+
);
106+
})}
107+
</div>
108+
109+
{/* Mitigations */}
110+
<div className={styles.mitigations}>
111+
<div className={styles.mitigationHeader}>
112+
<h2 className={styles.mitigationTitle}>{t.mitigationHeading}</h2>
113+
<span className={styles.mitigationCount}>{activeCount} {t.active}</span>
114+
</div>
115+
<div className={styles.legend}>
116+
{Object.entries(TYPE_COLORS).map(([key, c]) => (
117+
<div key={key} className={styles.legendItem}>
118+
<div className={styles.legendDot} style={{ background: c.color }} />
119+
<span className={styles.legendLabel}>{t.typeBadges[key]}</span>
120+
</div>
121+
))}
122+
</div>
123+
<div className={styles.cumulativeNote} style={{ borderLeft: `3px solid ${tc}` }}>
124+
<strong className={styles.cumulativeStrong}>{t.cumulative}:</strong> {t.cumulativeNote(ti, t.mitigations[ti].title)}
125+
</div>
126+
<div className={styles.cardList}>
127+
{t.mitigations.map((g) => <MitigationCard key={g.tier} group={g} active={g.tier <= ti + 1} accent={TIER_BG[g.tier - 1]} t={t} />)}
128+
</div>
129+
</div>
130+
131+
<div className={styles.footer}>
132+
<div>v{VERSION} · <a href="https://github.com/LLM-Coding/vibe-coding-risk-radar" target="_blank" rel="noopener" className={styles.footerLink}>{t.footer.github}</a> · <a href={`docs/risk-radar${lang === "en" ? "-en" : ""}.html`} target="_blank" rel="noopener" className={styles.footerLink}>{t.footer.fullDocs}</a></div>
133+
<div>{t.footer.madeBy} <a href="https://www.linkedin.com/in/rdmueller" target="_blank" rel="noopener" className={styles.footerLink}>Ralf D. Müller</a></div>
134+
</div>
135+
</div>
136+
137+
<DocSidebar docs={t.docs} open={docsOpen} onClose={() => setDocsOpen(false)} />
138+
</div>
139+
);
140+
}

0 commit comments

Comments
 (0)