UPDATE: split the webapp in a widget and the app itself

This splits the webapp in:

* IronCalc (the widget to be published on npmjs)
* The frontend for our "service"
* Adds "dummy code" for the backend using sqlite
This commit is contained in:
Nicolás Hatcher
2025-01-07 18:17:06 +01:00
committed by Nicolás Hatcher Andrés
parent 378f8351d3
commit 8215cfc9fb
121 changed files with 7997 additions and 1347 deletions

2
webapp/IronCalc/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/*
dist/*

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,15 @@
{
"name": "frontend",
"name": "ironcalc",
"private": true,
"version": "0.2.0",
"version": "0.3.0",
"type": "module",
"main": "./dist/ironcalc.js",
"module": "./dist/ironcalc.js",
"source": "./src/index.ts",
"types": "./dist/index.d.ts",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"build": "vite build && tsc",
"check": "biome check ./src",
"check-write": "biome check --write ./src",
"test": "vitest run"
@@ -14,24 +17,30 @@
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@ironcalc/wasm": "file:../bindings/wasm/pkg",
"@ironcalc/wasm": "file:../../bindings/wasm/pkg",
"@mui/material": "^5.15.15",
"i18next": "^23.11.1",
"lucide-react": "^0.427.0",
"react": "^18.2.0",
"react-colorful": "^5.6.1",
"react-dom": "^18.2.0",
"react-i18next": "^13.5.0"
},
"devDependencies": {
"@biomejs/biome": "1.8.3",
"@types/react": "^18.2.75",
"@types/react-dom": "^18.2.24",
"@vitejs/plugin-react": "^4.2.1",
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
"vite": "^5.2.8",
"typescript": "~5.6.2",
"vite": "^6.0.5",
"vite-plugin-svgr": "^4.2.0",
"vitest": "^2.0.5"
"vitest": "^2.0.5",
"react": "^18.0.0"
},
"peerDependencies": {
"@types/react": "^18.0.0",
"react": "^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
}

View File

@@ -0,0 +1,21 @@
import "./index.css";
import type { Model } from "@ironcalc/wasm";
import ThemeProvider from "@mui/material/styles/ThemeProvider";
import Workbook from "./components/workbook.tsx";
import { WorkbookState } from "./components/workbookState.ts";
import { theme } from "./theme.ts";
import "./i18n";
interface IronCalcProperties {
model: Model;
}
function IronCalc(properties: IronCalcProperties) {
return (
<ThemeProvider theme={theme}>
<Workbook model={properties.model} workbookState={new WorkbookState()} />
</ThemeProvider>
);
}
export default IronCalc;

View File

@@ -5,9 +5,7 @@ import type {
WorksheetProperties,
} from "@ironcalc/wasm";
import { styled } from "@mui/material/styles";
import { PaintRoller } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import ReactDOMServer from "react-dom/server";
import SheetTabBar from "./SheetTabBar/SheetTabBar";
import {
COLUMN_WIDTH_SCALE,
@@ -143,16 +141,10 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
// FIXME: This is so that the cursor indicates there are styles to be pasted
const el = rootRef.current?.getElementsByClassName("sheet-container")[0];
if (el) {
// Taken from lucide icons: <PaintRoller /> and rotated.
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-paint-roller" style="transform:rotate(-8deg)"><rect width="16" height="6" x="2" y="2" rx="2"></rect><path d="M10 16v-2a2 2 0 0 1 2-2h8a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"></path><rect width="4" height="6" x="8" y="16" rx="1"></rect></svg>`;
(el as HTMLElement).style.cursor =
`url('data:image/svg+xml;utf8,${encodeURIComponent(
ReactDOMServer.renderToString(
<PaintRoller
width={24}
height={24}
style={{ transform: "rotate(-8deg)" }}
/>,
),
)}'), auto`;
`url('data:image/svg+xml;utf8,${encodeURIComponent(svg)}'), auto`;
}
};

View File

Before

Width:  |  Height:  |  Size: 869 B

After

Width:  |  Height:  |  Size: 869 B

View File

Before

Width:  |  Height:  |  Size: 538 B

After

Width:  |  Height:  |  Size: 538 B

View File

Before

Width:  |  Height:  |  Size: 498 B

After

Width:  |  Height:  |  Size: 498 B

View File

Before

Width:  |  Height:  |  Size: 498 B

After

Width:  |  Height:  |  Size: 498 B

View File

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 586 B

View File

Before

Width:  |  Height:  |  Size: 539 B

After

Width:  |  Height:  |  Size: 539 B

View File

Before

Width:  |  Height:  |  Size: 513 B

After

Width:  |  Height:  |  Size: 513 B

View File

Before

Width:  |  Height:  |  Size: 542 B

After

Width:  |  Height:  |  Size: 542 B

View File

Before

Width:  |  Height:  |  Size: 538 B

After

Width:  |  Height:  |  Size: 538 B

View File

Before

Width:  |  Height:  |  Size: 290 B

After

Width:  |  Height:  |  Size: 290 B

View File

Before

Width:  |  Height:  |  Size: 539 B

After

Width:  |  Height:  |  Size: 539 B

View File

Before

Width:  |  Height:  |  Size: 659 B

After

Width:  |  Height:  |  Size: 659 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1004 B

After

Width:  |  Height:  |  Size: 1004 B

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 929 B

After

Width:  |  Height:  |  Size: 929 B

View File

Before

Width:  |  Height:  |  Size: 726 B

After

Width:  |  Height:  |  Size: 726 B

View File

Before

Width:  |  Height:  |  Size: 725 B

After

Width:  |  Height:  |  Size: 725 B

View File

Before

Width:  |  Height:  |  Size: 706 B

After

Width:  |  Height:  |  Size: 706 B

View File

Before

Width:  |  Height:  |  Size: 706 B

After

Width:  |  Height:  |  Size: 706 B

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -0,0 +1,5 @@
import init, { Model } from "@ironcalc/wasm";
import IronCalc from "./IronCalc";
import { IronCalcIcon, IronCalcLogo } from "./icons";
export { init, Model, IronCalc, IronCalcIcon, IronCalcLogo };

View File

@@ -1,5 +1,4 @@
import { createTheme } from "@mui/material/styles";
import "./fonts.css";
export const theme = createTheme({
typography: {

View File

@@ -5,13 +5,15 @@
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"declaration": true,
/* Bundler mode */
"moduleResolution": "bundler",
"moduleResolution": "node",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"emitDeclarationOnly": true,
// "noEmit": true,
"jsx": "react-jsx",
/* Linting */
@@ -19,7 +21,8 @@
"noUnusedLocals": false,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true
"esModuleInterop": true,
"outDir": "dist",
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]

View File

@@ -1,6 +1,7 @@
{
"compilerOptions": {
"composite": true,
"declaration": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",

View File

@@ -0,0 +1,37 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';
import { resolve } from 'node:path';
// https://vitejs.dev/config/
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'IronCalc',
// the proper extensions will be added
fileName: 'ironcalc',
},
rollupOptions: {
// make sure to externalize deps that shouldn't be bundled
// into your library
external: ['react', 'react-dom', '@ironcalc/wasm'],
output: {
// Provide global variables to use in the UMD build
// for externalized deps
globals: {
react: 'React',
'react-dom': 'ReactDOM',
'@ironcalc/wasm': 'IronCalc',
},
},
},
},
plugins: [react(), svgr()],
server: {
fs: {
// Allow serving files from one level up to the project root
allow: ['..'],
},
},
});

View File

@@ -0,0 +1,7 @@
:2080
# rocket API
reverse_proxy /api/* 127.0.0.1:8000
# everything else is the frontend
reverse_proxy :5173

View File

@@ -0,0 +1,44 @@
# IronCalc service application
This directory contains the code (frontend and backend) to run the code deployed at https://app.ironcalc.com
## Development build:
1. Run in this folder `caddy run` (that just just a proxy for the front end and backend).
You will need to leave it running all the time.
2. In the server folder run `cargo run`
3. In the frontend folder `npm install` and `npm run dev`
That's it if you point your browser to localhost:2080 you should see the app.
Note that step three involves alo building thw wasm bindings and the widget
## Deployment
The development environment is very close to a deployment environment.
### Build the server binary:
In the server directory run:
```
cargo build --release
```
You will find a single binary in target/release/ironcalc_server
### Build the frontend files
In the frontend folder:
```
npm install
npm run build
```
That will create a bunch of files that you should copy to your server
## TODO
Deployment details, brotli, logs, stats, Postgres, systemctl files, ...

View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist/*
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,17 @@
# IronCalc application
This is the front end deployed at https:://app.ironcalc.com
To build for production:
```
npm install
npm run build
```
A development build:
```
npm run dev
```

View File

@@ -0,0 +1,19 @@
{
"organizeImports": { "enabled": true },
"linter": {
"enabled": true,
"rules": {
"correctness": { "noUnusedImports": "error" }
}
},
"formatter": {
"indentStyle": "space",
"indentWidth": 2
},
"css": {
"formatter": {
"enabled": true
}
}
}

View File

@@ -12,6 +12,5 @@
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script defer data-domain="app.ironcalc.com" src="https://plausible.io/js/script.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
{
"name": "frontend",
"private": true,
"version": "0.3.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"check": "biome check ./src",
"check-write": "biome check --write ./src"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@ironcalc/ironcalc": "file:../../IronCalc",
"@mui/material": "^6.3.1",
"lucide": "^0.469.0",
"lucide-react": "^0.469.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@biomejs/biome": "1.8.3",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "~5.6.2",
"vite": "^6.0.5",
"vite-plugin-svgr": "^4.2.0"
}
}

View File

@@ -1,15 +1,12 @@
import "./App.css";
import Workbook from "./components/workbook";
import "./i18n";
import styled from "@emotion/styled";
import init, { Model } from "@ironcalc/wasm";
import { useEffect, useState } from "react";
import { FileBar } from "./AppComponents/FileBar";
import { FileBar } from "./components/FileBar";
import {
get_documentation_model,
get_model,
uploadFile,
} from "./AppComponents/rpc";
} from "./components/rpc";
import {
createNewModel,
deleteSelectedModel,
@@ -17,15 +14,13 @@ import {
saveModelToStorage,
saveSelectedModelInStorage,
selectModelFromStorage,
} from "./AppComponents/storage";
import { WorkbookState } from "./components/workbookState";
import { IronCalcIcon } from "./icons";
} from "./components/storage";
// From IronCalc
import { IronCalc, IronCalcIcon, Model, init } from "@ironcalc/ironcalc";
function App() {
const [model, setModel] = useState<Model | null>(null);
const [workbookState, setWorkbookState] = useState<WorkbookState | null>(
null,
);
useEffect(() => {
async function start() {
@@ -60,12 +55,11 @@ function App() {
const newModel = loadModelFromStorageOrCreate();
setModel(newModel);
}
setWorkbookState(new WorkbookState());
}
start();
}, []);
if (!model || !workbookState) {
if (!model) {
return (
<Loading>
<IronCalcIcon style={{ width: 24, height: 24, marginBottom: 16 }} />
@@ -114,7 +108,7 @@ function App() {
}
}}
/>
<Workbook model={model} workbookState={workbookState} />
<IronCalc model={model} />
</Wrapper>
);
}

View File

@@ -1,8 +1,9 @@
import styled from "@emotion/styled";
import type { Model } from "@ironcalc/wasm";
import type { Model } from "@ironcalc/ironcalc";
import { IronCalcIcon, IronCalcLogo } from "@ironcalc/ironcalc";
import { CircleCheck } from "lucide-react";
import { useRef, useState } from "react";
import { IronCalcIcon, IronCalcLogo } from "./../icons";
// import { IronCalcIcon, IronCalcLogo } from "./../icons";
import { FileMenu } from "./FileMenu";
import { ShareButton } from "./ShareButton";
import { WorkbookTitle } from "./WorkbookTitle";

View File

@@ -14,7 +14,6 @@ function UploadFileDialog(properties: {
const { onModelUpload } = properties;
useEffect(() => {
const root = document.getElementById("root");
if (crossRef.current) {
crossRef.current.focus();
}

View File

@@ -1,4 +1,4 @@
import { Model } from "@ironcalc/wasm";
import { Model } from "@ironcalc/ironcalc";
import { base64ToBytes, bytesToBase64 } from "./util";
const MAX_WORKBOOKS = 50;

View File

@@ -0,0 +1,5 @@
body {
inset: 0px;
margin: 0;
padding: 0;
}

View File

@@ -0,0 +1,12 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
import "./fonts.css";
// biome-ignore lint: we know the 'root' element exists.
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);

View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

Some files were not shown because too many files have changed in this diff Show More