import "./App.css"; import styled from "@emotion/styled"; // From IronCalc import { IronCalc, IronCalcIcon, init, Model } from "@ironcalc/workbook"; import { Modal } from "@mui/material"; import { useEffect, useState } from "react"; import { FileBar } from "./components/FileBar"; import LeftDrawer from "./components/LeftDrawer/LeftDrawer"; import { get_documentation_model, get_model, uploadFile, } from "./components/rpc"; import { createModelWithSafeTimezone, createNewModel, deleteModelByUuid, deleteSelectedModel, isStorageEmpty, loadSelectedModelFromStorage, saveModelToStorage, saveSelectedModelInStorage, selectModelFromStorage, } from "./components/storage"; import TemplatesDialog from "./components/WelcomeDialog/TemplatesDialog"; import WelcomeDialog from "./components/WelcomeDialog/WelcomeDialog"; function App() { const [model, setModel] = useState(null); const [showWelcomeDialog, setShowWelcomeDialog] = useState(false); const [isTemplatesDialogOpen, setTemplatesDialogOpen] = useState(false); const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [localStorageId, setLocalStorageId] = useState(1); useEffect(() => { async function start() { await init(); const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); const modelHash = urlParams.get("model"); const exampleFilename = urlParams.get("example"); // If there is a model name ?model=modelHash we try to load it // if there is not, or the loading failed we load an empty model if (modelHash) { // Get a remote model try { const model_bytes = await get_model(modelHash); const importedModel = Model.from_bytes(model_bytes); localStorage.removeItem("selected"); setModel(importedModel); } catch (e) { alert("Model not found, or failed to load"); } } else if (exampleFilename) { try { const model_bytes = await get_documentation_model(exampleFilename); const importedModel = Model.from_bytes(model_bytes); localStorage.removeItem("selected"); setModel(importedModel); } catch (e) { alert("Example file not found, or failed to load"); } } else { // try to load from local storage const newModel = loadSelectedModelFromStorage(); if (!newModel) { setShowWelcomeDialog(true); const createdModel = createModelWithSafeTimezone("template"); setModel(createdModel); } else { setModel(newModel); } } } start(); }, []); // biome-ignore lint/correctness/useExhaustiveDependencies: localStorageId needed to detect name changes (model mutates internally) useEffect(() => { if (model) { const workbookName = model.getName(); document.title = workbookName ? `${workbookName} - IronCalc` : "IronCalc"; } else { document.title = "IronCalc"; } }, [model, localStorageId]); if (!model) { return (
Loading IronCalc
); } // We try to save the model every second setInterval(() => { const queue = model.flushSendQueue(); if (queue.length !== 1) { saveSelectedModelInStorage(model); } }, 1000); // Handlers for model changes that also update our models state const handleNewModel = () => { const newModel = createNewModel(); setModel(newModel); }; const handleSetModel = (uuid: string) => { const newModel = selectModelFromStorage(uuid); if (newModel) { setModel(newModel); } }; const handleDeleteModel = () => { const newModel = deleteSelectedModel(); if (newModel) { setModel(newModel); } }; const handleDeleteModelByUuid = (uuid: string) => { const newModel = deleteModelByUuid(uuid); if (newModel) { setModel(newModel); } }; // We could use context for model, but the problem is that it should initialized to null. // Passing the property down makes sure it is always defined. return ( setIsDrawerOpen(false)} newModel={handleNewModel} setModel={handleSetModel} onDelete={handleDeleteModelByUuid} localStorageId={localStorageId} /> {isDrawerOpen && ( setIsDrawerOpen(false)} /> )} { const blob = await uploadFile(arrayBuffer, fileName); const bytes = new Uint8Array(await blob.arrayBuffer()); const newModel = Model.from_bytes(bytes); saveModelToStorage(newModel); setModel(newModel); }} newModel={handleNewModel} newModelFromTemplate={() => { setTemplatesDialogOpen(true); }} setModel={handleSetModel} onDelete={handleDeleteModel} isDrawerOpen={isDrawerOpen} setIsDrawerOpen={setIsDrawerOpen} setLocalStorageId={setLocalStorageId} /> {showWelcomeDialog && ( { if (isStorageEmpty()) { const createdModel = createNewModel(); setModel(createdModel); } setShowWelcomeDialog(false); }} onSelectTemplate={async (templateId) => { switch (templateId) { case "blank": { const createdModel = createNewModel(); setModel(createdModel); break; } default: { const model_bytes = await get_documentation_model(templateId); const importedModel = Model.from_bytes(model_bytes); saveModelToStorage(importedModel); setModel(importedModel); break; } } setShowWelcomeDialog(false); }} /> )} setTemplatesDialogOpen(false)} aria-labelledby="templates-dialog-title" aria-describedby="templates-dialog-description" > setTemplatesDialogOpen(false)} onSelectTemplate={async (fileName) => { const model_bytes = await get_documentation_model(fileName); const importedModel = Model.from_bytes(model_bytes); saveModelToStorage(importedModel); setModel(importedModel); setTemplatesDialogOpen(false); }} /> ); } const Wrapper = styled("div")` display: flex; width: 100%; height: 100%; position: relative; overflow: hidden; `; const DRAWER_WIDTH = 264; const MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE = 440; const MainContent = styled("div")<{ isDrawerOpen: boolean }>` margin-left: ${({ isDrawerOpen }) => (isDrawerOpen ? "0px" : `-${DRAWER_WIDTH}px`)}; width: ${({ isDrawerOpen }) => (isDrawerOpen ? `calc(100% - ${DRAWER_WIDTH}px)` : "100%")}; display: flex; flex-direction: column; position: relative; @media (max-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px) { ${({ isDrawerOpen }) => isDrawerOpen && `min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE}px;`} } `; const MobileOverlay = styled("div")` position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(255, 255, 255, 0.8); z-index: 1; cursor: pointer; @media (min-width: ${MIN_MAIN_CONTENT_WIDTH_FOR_MOBILE + 1}px) { display: none; } `; const Loading = styled("div")` height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; font-family: "Inter"; font-size: 14px; `; export default App;