OverviewFilesCommitsIssuesDocsReleasesPeers

electrical-review / design / app.jsx

120 lines · jsx ref:
/* ============================================================================
   PRSM SOLAR — APP SHELL
   ========================================================================== */
const { useState: useStateA, useEffect: useEffectA } = React;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "theme": "field",
  "texture": true,
  "typeScale": 1.0,
  "leadAccent": "pine"
}/*EDITMODE-END*/;

const TABS = [
  { id: "overview", n: "01", label: "Overview", Comp: () => <Overview /> },
  { id: "bom", n: "02", label: "Bill of Materials", Comp: () => <BOM /> },
  { id: "costs", n: "03", label: "Procurement & Costs", Comp: () => <Costs /> },
  { id: "schematics", n: "04", label: "Schematics", Comp: () => <Schematics /> },
  { id: "racking", n: "05", label: "Racking", Comp: () => <Racking /> },
  { id: "flyover", n: "06", label: "3D Flyover", Comp: () => <Flyover /> },
];

function Masthead() {
  const s = window.PRSM.site;
  return (
    <header className="masthead">
      <div className="mast-top">
        <div className="mast-id">
          <div className="mast-mark" />
          <div>
            <div className="mast-title">PRSM SOLAR</div>
            <div className="mast-sub">{s.subtitle}</div>
          </div>
        </div>
        <div className="mast-meta">
          <div><b>{s.place}</b> · {s.zip}</div>
          <div>{s.latlon}</div>
          <div>{s.region} · captured {s.captured}</div>
        </div>
      </div>
      <div className="mast-facts">
        <span className="fact"><b>Land</b> {s.land}</span>
        <span className="fact"><b>Mount</b> {s.mount}</span>
        <span className="fact"><b>Tilt</b> {s.tilt}</span>
        <span className="fact"><b>Frost</b> {s.frost}</span>
        <span className="fact"><b>Snow</b> {s.snowLoad}</span>
        <span className="fact"><b>Wind</b> {s.windDesign}</span>
      </div>
    </header>
  );
}

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [tab, setTab] = useStateA("overview");

  useEffectA(() => {
    const r = document.documentElement;
    r.setAttribute("data-theme", t.theme);
    r.style.fontSize = (16 * (t.typeScale || 1)) + "px";
  }, [t.theme, t.typeScale]);

  // lead-accent swaps the nav underline + mark dot emphasis
  useEffectA(() => {
    document.documentElement.style.setProperty("--lead", t.leadAccent === "amber" ? "var(--amber)" : "var(--pine)");
  }, [t.leadAccent]);

  const Active = TABS.find((x) => x.id === tab).Comp;

  return (
    <div className="app">
      <Masthead />
      <nav className="nav">
        <div className="nav-in">
          {TABS.map((x) => (
            <button key={x.id} className={"nav-btn" + (tab === x.id ? " on" : "")} onClick={() => setTab(x.id)}>
              <span className="n">{x.n}</span>{x.label}
            </button>
          ))}
        </div>
      </nav>

      <Active />

      <TweaksPanel>
        <TweakSection label="Overall look" />
        <TweakRadio label="Theme" value={t.theme}
          options={["field", "topo", "birch"]}
          onChange={(v) => setTweak("theme", v)} />
        <TweakToggle label="Contour texture" value={t.texture}
          onChange={(v) => setTweak("texture", v)} />
        <TweakSection label="Accent & type" />
        <TweakRadio label="Lead accent" value={t.leadAccent}
          options={["pine", "amber"]}
          onChange={(v) => setTweak("leadAccent", v)} />
        <TweakSlider label="Type scale" value={t.typeScale} min={0.9} max={1.15} step={0.05}
          onChange={(v) => setTweak("typeScale", v)} />
      </TweaksPanel>

      <TextureToggle on={t.texture} />
    </div>
  );
}

// mounts/unmounts the contour background based on the tweak
function TextureToggle({ on }) {
  useEffectA(() => {
    let el = document.querySelector(".topo-bg");
    if (on && !el) {
      el = document.createElement("div");
      el.className = "topo-bg";
      document.body.insertBefore(el, document.body.firstChild);
    } else if (!on && el) {
      el.remove();
    }
  }, [on]);
  return null;
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);