/* help.jsx — modernized HEMS help page.
Loads content from help-content.json and renders all sections with
collapsible cards, sticky pill nav, and consistent dashboard chrome. */
const HELP_GRADIENTS = {
'from-energy-charge to-energy-solar': 'linear-gradient(90deg, #5BA8FF, #F4C84B)',
'from-energy-grid to-energy-consumption': 'linear-gradient(90deg, #5BA8FF, #FF8A47)',
'from-energy-solar to-energy-discharge': 'linear-gradient(90deg, #F4C84B, #3FCC8B)',
'from-energy-charge to-energy-hold': 'linear-gradient(90deg, #5BA8FF, #9D7BFF)',
'from-destructive to-energy-solar': 'linear-gradient(90deg, #FF5A65, #F4C84B)',
'from-energy-discharge to-destructive': 'linear-gradient(90deg, #3FCC8B, #FF5A65)',
'from-primary to-energy-solar': 'linear-gradient(90deg, #5BA8FF, #F4C84B)',
'from-energy-hold to-primary': 'linear-gradient(90deg, #9D7BFF, #5BA8FF)',
};
// Section icon-circle bg + color
const SECTION_ICON = {
quickstart: { bg: 'rgba(91,168,255,.14)', color: 'var(--blue)', icon: 'gear' },
commands: { bg: 'rgba(255,138,71,.14)', color: 'var(--c-cover)', icon: 'flash' },
'flow-examples': { bg: 'rgba(244,200,75,.14)', color: 'var(--c-charge-sol)', icon: 'sun' },
'multi-battery': { bg: 'rgba(157,123,255,.14)', color: 'var(--violet)', icon: 'battery' },
'peak-shaving': { bg: 'rgba(255,90,101,.14)', color: 'var(--c-peak)', icon: 'lightning' },
troubleshooting: { bg: 'rgba(63,204,139,.14)', color: 'var(--c-sell)', icon: 'gear' },
faq: { bg: 'rgba(91,168,255,.14)', color: 'var(--blue)', icon: 'eye' },
more: { bg: 'rgba(157,123,255,.14)', color: 'var(--violet)', icon: 'chart' },
};
// ─── Section wrapper ────────────────────────────────────────────────
const Section = ({ id, data, open, onToggle, children }) => {
const meta = SECTION_ICON[id] || {};
return (
{open && {children}
}
);
};
// ─── Q&A row ────────────────────────────────────────────────────────
const QA = ({ q, a }) => {
const [open, setOpen] = React.useState(false);
return (
{open &&
{a}
}
);
};
// ─── Quickstart section ────────────────────────────────────────────
const QuickstartBody = ({ data }) => (
<>
{/* ⚠️ Lyft det enda som förstör allt — näteffekt-sensorn — högst upp */}
{data.criticalCallout && (
{data.criticalCallout.title}
{data.criticalCallout.lead}
✓ Rätt: {data.criticalCallout.good}
✕ Fel: {data.criticalCallout.bad}
{data.criticalCallout.check}
)}
{/* Grundflöde — 4 korta steg */}
{data.steps.map((s, i) => (
{s.n}
{s.title}
{s.body}
{s.image && (
)}
))}
{data.stepNote && (
{data.stepNote}
)}
{/* Fält-tabell med tydlig teckenkonvention */}
{data.sensorTable.heading}
| Fält | Vad det är | Tecken | Exempel |
{data.sensorTable.rows.map((r,i)=>{
const sc = r.signKind==='grid' ? '#2ED47A' : r.signKind==='batt' ? '#F5B544' : 'var(--ink-2)';
return (
| {r.field} |
{r.what} |
{r.sign} |
{r.example} |
);
})}
{data.sensorTable.signLegend && (
{data.sensorTable.signLegend.map((l,i)=>(
{l.t}
))}
)}
{/* Avancerat — hopfällt */}
{(data.advanced||[]).map((a,i)=>(
))}
>
);
// ─── Commands section ──────────────────────────────────────────────
const CommandsBody = ({ data }) => (
<>
{data.commands.map((c, i) => (
{c.emoji}
{c.cmd}
NÄR-kort: "{c.narCard}"
Växelriktaren: {c.inverterAction}
Effekt-tagg:{' '}
{c.hasPower ? 'Ja' : 'Nej'}
{c.powerNote && {c.powerNote}}
))}
{data.tagInfo.heading}
{data.tagInfo.body}
{data.tagInfo.tags.map((t,i)=>(
- {t.name} — {t.desc}
))}
{data.expandables.map((e,i)=>)}
>
);
// ─── Flow examples section ─────────────────────────────────────────
const FlowExamplesBody = ({ data, onLightbox }) => (
<>
{data.flows.map((f, i) => (
{f.title}
{f.description}
{/* Textkort (image:null, t.ex. egenförbrukningsrådet) hoppar över
bildslotten — img med src=null renderar en trasig-bild-ikon. */}
{f.image &&
}
{f.imageHint && {f.imageHint}
}
))}
{data.comingSoon && (
{data.comingSoon}
)}
>
);
const FlowImage = ({ src, alt, onLightbox }) => {
const [error, setError] = React.useState(false);
if (error){
return (
Bilden kunde inte laddas.
({src})
);
}
return (
onLightbox(src, alt)}
onError={()=>setError(true)}/>
);
};
// ─── Multi-battery section ─────────────────────────────────────────
const MultiBatteryBody = ({ data }) => (
<>
{data.howItWorks.heading}
{data.howItWorks.bullets.map((b,i)=>{
// Inline-render badges for B1 = charge / B2 = pause hints
if (b.includes('charge') && b.includes('pause')){
return (
-
Exempel: B1 = charge (SOC 30%),
{' '}B2 = pause (SOC 100% = fullt)
);
}
return - {b}
;
})}
{data.buildFlows.heading}
{data.buildFlows.body}
{data.buildFlows.flowExample.map((row, i)=>(
{row.type}: {row.text}
))}
Antal flöden: Ett per batteri per kommando.
Exempel: 2 batterier × 3 kommandon (charge, pause, selfconsumption) = 6 flöden.
Utan OCH-kort körs SEDAN-kortet för ALLA batterier — båda får samma kommando, vilket kan bli fel om HEMS vill ladda B1 men pausa B2.
{data.socExample.heading}
| Situation | B1 (SOC 30%) | B2 (SOC 100%) |
{data.socExample.table.map((r,i)=>(
| {r.situation} |
{r.b1} |
{r.b2} |
))}
{data.socExample.note}
{data.aiBatteries.heading}
{data.aiBatteries.bullets.map((b,i)=>- {b}
)}
{data.config.heading}
{data.config.bullets.map((b,i)=>(
-
{b.includes('Inställningar') ? (
<>Gå till Inställningar → "Flera batterier">
) : b}
))}
>
);
// ─── Peak shaving section ──────────────────────────────────────────
const PeakShavingBody = ({ data }) => (
<>
{data.whatIs.heading}
{data.whatIs.body}
{data.howHelps.heading}
{data.howHelps.bullets.map((b,i)=>(
- $1')}}/>
))}
{data.reserve.heading}
{data.reserve.body}
{data.reserve.phases.map((p,i)=>(
{p.phase}
{p.value}
{p.desc}
))}
{data.reactiveProtection.heading}
{data.reactiveProtection.body}
{data.reactiveProtection.steps.map((s,i)=>(
))}
{data.modes.heading}
{data.modes.items.map((m,i)=>(
))}
>
);
// ─── Troubleshooting section ───────────────────────────────────────
const TroubleshootingBody = ({ data }) => (
<>
{data.problems.map((p,i)=>(
{p.n}
{p.title}
{p.symptom && (
)}
{p.explanation && (
Förklaring
{p.explanation}
)}
{p.example && (
Exempel
{p.example.map((line, j) => (
(borde vara $1)')
.replace(/← FEL/g, '
← FEL')
}}/>
))}
)}
{p.info && (
)}
{p.action && (
)}
{p.solution && (
Lösning
$1')
}}/>
{p.solutionBullets && (
{p.solutionBullets.map((b,j)=>(
-
•
$1')}}/>
))}
)}
)}
{p.solutionSteps && (
Lösning
{p.solutionSteps.map((s,j)=>- {s}
)}
)}
{p.test && (
)}
))}
{/* Quick troubleshooting */}
{data.quickTroubleshooting.heading}
{data.quickTroubleshooting.items.map((it,i)=>(
{it.problem}
{it.solution}
))}
>
);
// ─── FAQ section ───────────────────────────────────────────────────
const FaqBody = ({ data }) => (
{data.items.map((q,i)=>)}
);
// ─── More info section ─────────────────────────────────────────────
const MoreBody = ({ data }) => (
{data.expandables.map((e,i)=>(
))}
);
const Expandable = ({ title, content }) => {
const [open, setOpen] = React.useState(false);
return (
{open && (
{content.warning && (
)}
{content.body &&
{content.body}
}
{content.bullets && (
{content.bullets.map((b,i)=>- {b}
)}
)}
{content.rule && (
)}
{content.example && (
{content.example.heading}
{content.example.rows.map((r,i)=>(
{r.label}
{r.value}
))}
)}
{content.codeBlock && (
{content.codeBlock}
)}
)}
);
};
// ─── Lightbox ──────────────────────────────────────────────────────
const Lightbox = ({ src, alt, onClose }) => {
React.useEffect(() => {
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, []);
return (

e.stopPropagation()}/>
);
};
// ─── Main app ──────────────────────────────────────────────────────
const SECTION_RENDERERS = {
quickstart: QuickstartBody,
commands: CommandsBody,
'flow-examples': FlowExamplesBody,
'multi-battery': MultiBatteryBody,
'peak-shaving': PeakShavingBody,
troubleshooting: TroubleshootingBody,
faq: FaqBody,
more: MoreBody,
};
function HelpApp(){
const [content, setContent] = React.useState(null);
const [open, setOpen] = React.useState({});
const [lightbox, setLightbox] = React.useState(null);
const [activeId, setActiveId] = React.useState('quickstart');
React.useEffect(() => {
fetch('help-content.json')
.then(r => r.json())
.then(d => {
setContent(d);
// Initialise open state from defaultOpen
const o = {};
for (const k of Object.keys(d.sections)){
o[k] = !!d.sections[k].defaultOpen;
}
setOpen(o);
})
.catch(e => console.error('Failed to load help content', e));
}, []);
// Scroll-spy
React.useEffect(() => {
if (!content) return;
const onScroll = () => {
const fromTop = 130;
let cur = activeId;
for (const id of Object.keys(content.sections)){
const el = document.getElementById(id);
if (!el) continue;
const r = el.getBoundingClientRect();
if (r.top < fromTop) cur = id;
}
if (cur !== activeId) setActiveId(cur);
};
window.addEventListener('scroll', onScroll, {passive:true});
return () => window.removeEventListener('scroll', onScroll);
}, [content, activeId]);
if (!content){
return (
);
}
const toggle = (id) => setOpen(o => ({...o, [id]: !o[id]}));
const openOnly = (id) => {
setOpen(o => ({...o, [id]: true}));
setTimeout(() => {
const el = document.getElementById(id);
if (el) el.scrollIntoView({behavior:'smooth', block:'start'});
}, 30);
};
return (
{content.meta.title}
{content.meta.subtitle}
{content.meta.intro.heading}
{content.meta.intro.body}
{content.sectionNav.map(navItem => {
const data = content.sections[navItem.id];
if (!data) return null;
const Body = SECTION_RENDERERS[navItem.id];
return (
toggle(navItem.id)}>
{Body && setLightbox({src,alt})}/>}
);
})}
Snabblänkar
{content.quickLinks.map((ql, i) => (
{ql.label}
))}
{lightbox &&
setLightbox(null)}/>}
);
}
ReactDOM.createRoot(document.getElementById('root')).render(
);