|
| 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