diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a14702c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,34 @@
+# dependencies (bun install)
+node_modules
+
+# output
+out
+dist
+*.tgz
+
+# code coverage
+coverage
+*.lcov
+
+# logs
+logs
+_.log
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# caches
+.eslintcache
+.cache
+*.tsbuildinfo
+
+# IntelliJ based IDEs
+.idea
+
+# Finder (MacOS) folder config
+.DS_Store
diff --git a/bun-env.d.ts b/bun-env.d.ts
new file mode 100644
index 0000000..72f1c26
--- /dev/null
+++ b/bun-env.d.ts
@@ -0,0 +1,17 @@
+// Generated by `bun init`
+
+declare module "*.svg" {
+ /**
+ * A path to the SVG file
+ */
+ const path: `${string}.svg`;
+ export = path;
+}
+
+declare module "*.module.css" {
+ /**
+ * A record of class names to their corresponding CSS module classes
+ */
+ const classes: { readonly [key: string]: string };
+ export = classes;
+}
diff --git a/bun.lock b/bun.lock
new file mode 100644
index 0000000..90a619b
--- /dev/null
+++ b/bun.lock
@@ -0,0 +1,59 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "bun-react-template",
+ "dependencies": {
+ "react": "^19",
+ "react-dom": "^19",
+ "react-rnd": "^10.5.2",
+ },
+ "devDependencies": {
+ "@types/bun": "latest",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ },
+ },
+ },
+ "packages": {
+ "@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="],
+
+ "@types/node": ["@types/node@22.15.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg=="],
+
+ "@types/react": ["@types/react@19.1.4", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g=="],
+
+ "@types/react-dom": ["@types/react-dom@19.1.5", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg=="],
+
+ "bun-types": ["bun-types@1.2.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="],
+
+ "clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="],
+
+ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
+
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
+
+ "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
+
+ "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
+
+ "re-resizable": ["re-resizable@6.11.2", "", { "peerDependencies": { "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A=="],
+
+ "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
+
+ "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
+
+ "react-draggable": ["react-draggable@4.4.6", "", { "dependencies": { "clsx": "^1.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.3.0", "react-dom": ">= 16.3.0" } }, "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw=="],
+
+ "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
+
+ "react-rnd": ["react-rnd@10.5.2", "", { "dependencies": { "re-resizable": "6.11.2", "react-draggable": "4.4.6", "tslib": "2.6.2" }, "peerDependencies": { "react": ">=16.3.0", "react-dom": ">=16.3.0" } }, "sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw=="],
+
+ "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
+
+ "tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
+
+ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
+ }
+}
diff --git a/bunfig.toml b/bunfig.toml
new file mode 100644
index 0000000..9819bf6
--- /dev/null
+++ b/bunfig.toml
@@ -0,0 +1,2 @@
+[serve.static]
+env = "BUN_PUBLIC_*"
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..52219b5
--- /dev/null
+++ b/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "bun-react-template",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "main": "src/index.tsx",
+ "module": "src/index.tsx",
+ "scripts": {
+ "dev": "bun --hot src/index.tsx",
+ "build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'",
+ "start": "NODE_ENV=production bun src/index.tsx"
+ },
+ "dependencies": {
+ "react": "^19",
+ "react-dom": "^19",
+ "react-rnd": "^10.5.2"
+ },
+ "devDependencies": {
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "@types/bun": "latest"
+ }
+}
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..e9c8ae7
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,12 @@
+import BudgetTreemap from "./budget-component";
+import "./index.css";
+
+export function App() {
+ return (
+
+
+
+ );
+}
+
+export default App;
diff --git a/src/budget-component.tsx b/src/budget-component.tsx
new file mode 100644
index 0000000..74e4ff6
--- /dev/null
+++ b/src/budget-component.tsx
@@ -0,0 +1,168 @@
+import React, { useState } from "react";
+import { Rnd } from "react-rnd";
+
+interface BudgetItem {
+ name: string;
+ width: number;
+ height: number;
+ position: { x: number; y: number };
+ bgColor: string;
+ color: string;
+}
+
+const initialBudgets: BudgetItem[] = [
+ { name: "Santé", width: 472, height: 430, position: { x: 0, y: 0 }, bgColor: "#2E4991", color: "white" },
+ {
+ name: "Reste de la protection sociale",
+ width: 251,
+ height: 430,
+ position: { x: 472, y: 0 },
+ bgColor: "#0A2F6E",
+ color: "white",
+ },
+ { name: "Retraites", width: 723, height: 353, position: { x: 0, y: 430 }, bgColor: "#02B5E3", color: "white" },
+ { name: "Défense", width: 200, height: 154, position: { x: 723, y: 0 }, bgColor: "#02B5E4", color: "black" },
+ { name: "Sécurité", width: 163, height: 154, position: { x: 923, y: 0 }, bgColor: "#F7EB7D", color: "black" },
+ {
+ name: "Infrastructures",
+ width: 144,
+ height: 75,
+ position: { x: 1086, y: 0 },
+ bgColor: "#6856CD",
+ color: "white",
+ },
+ { name: "Justice", width: 63, height: 75, position: { x: 1230, y: 0 }, bgColor: "#7AF3B4", color: "black" },
+ { name: "Environnement", width: 204, height: 85, position: { x: 1086, y: 69 }, bgColor: "#F3A5E1", color: "white" },
+ {
+ name: "Charge de la dette",
+ width: 200,
+ height: 154,
+ position: { x: 723, y: 154 },
+ bgColor: "#FD745D",
+ color: "white",
+ },
+ { name: "Recherche", width: 195, height: 154, position: { x: 923, y: 154 }, bgColor: "#75F5B2", color: "black" },
+ {
+ name: "Culture et loisirs",
+ width: 172,
+ height: 154,
+ position: { x: 1118, y: 154 },
+ bgColor: "#F6EB7D",
+ color: "black",
+ },
+ {
+ name: "Soutien aux activités économiques",
+ width: 308,
+ height: 192,
+ position: { x: 723, y: 308 },
+ bgColor: "#6758CD",
+ color: "white",
+ },
+ {
+ name: "Transports et équipements collectifs",
+ width: 261,
+ height: 192,
+ position: { x: 1030, y: 308 },
+ bgColor: "#0AB5E4",
+ color: "black",
+ },
+ { name: "Éducation", width: 323, height: 276, position: { x: 723, y: 500 }, bgColor: "#F5A4E1", color: "black" },
+ {
+ name: "Fonctionnement des administrations publiques",
+ width: 241,
+ height: 276,
+ position: { x: 1048, y: 500 },
+ bgColor: "#FE755D",
+ color: "white",
+ },
+];
+
+export default function BudgetTreemap() {
+ const [budgets, setBudgets] = useState(initialBudgets);
+ const [totalBudget, setTotalBudget] = useState(1000);
+
+ const containerWidth = 1290;
+ const containerHeight = 782;
+ const totalArea = containerHeight * containerWidth;
+
+ const handleResize = (
+ index: number,
+ newWidth: number,
+ newHeight: number,
+ newPosition: { x: number; y: number }
+ ): void => {
+ console.log("Resizing", index, newWidth, newHeight, newPosition);
+ const updatedBudgets = [...budgets];
+ updatedBudgets[index].width = newWidth;
+ updatedBudgets[index].height = newHeight;
+ updatedBudgets[index].position = newPosition;
+ setBudgets(updatedBudgets);
+ const newTotalBudget = updatedBudgets.reduce(
+ (acc, item) => acc + +(((item.width * item.height) / totalArea) * 1000).toFixed(0),
+ 0
+ );
+ setTotalBudget(newTotalBudget);
+ };
+
+ const containerStyle: React.CSSProperties = {
+ width: `${containerWidth}px`,
+ height: `${containerHeight}px`,
+ border: "1px solid #ccc",
+ backgroundColor: "#1a1a1a",
+ color: "white",
+ position: "relative",
+ overflow: "hidden",
+ };
+
+ const boxStyle: React.CSSProperties = {
+ color: "white",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ textAlign: "center",
+ border: "1px solid white",
+ boxSizing: "border-box",
+ position: "absolute",
+ };
+
+ const headerStyle: React.CSSProperties = {
+ fontSize: "24px",
+ fontWeight: "bold",
+ marginBottom: "20px",
+ };
+
+ return (
+
+
Répartition de {totalBudget.toFixed(0)}€ de dépenses publiques
+
+ {budgets.map((item, index) => {
+ const area = item.width * item.height;
+ const value = (area / totalArea) * 1000;
+ return (
+
{
+ handleResize(index, ref.offsetWidth, ref.offsetHeight, position);
+ }}
+ onDragStop={(e, d) => {
+ const updatedBudgets = [...budgets];
+ updatedBudgets[index].position = { x: d.x, y: d.y };
+ setBudgets(updatedBudgets);
+ }}
+ style={{ ...boxStyle, backgroundColor: item.bgColor, color: item.color }}
+ >
+
+ {item.name}
+
+ {value.toFixed(0)}€
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/frontend.tsx b/src/frontend.tsx
new file mode 100644
index 0000000..e5cc9b8
--- /dev/null
+++ b/src/frontend.tsx
@@ -0,0 +1,26 @@
+/**
+ * This file is the entry point for the React app, it sets up the root
+ * element and renders the App component to the DOM.
+ *
+ * It is included in `src/index.html`.
+ */
+
+import { createRoot } from "react-dom/client";
+import { StrictMode } from "react";
+import { App } from "./App";
+
+const elem = document.getElementById("root")!;
+const app = (
+
+
+
+);
+
+if (import.meta.hot) {
+ // With hot module reloading, `import.meta.hot.data` is persisted.
+ const root = (import.meta.hot.data.root ??= createRoot(elem));
+ root.render(app);
+} else {
+ // The hot module reloading API is not available in production.
+ createRoot(elem).render(app);
+}
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..3e5feb8
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,15 @@
+:root {
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+}
+body {
+ margin: 0;
+ display: grid;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+ position: relative;
+}
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 0000000..7ae8757
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ Fais ton budget.
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/index.tsx b/src/index.tsx
new file mode 100644
index 0000000..b2a30bb
--- /dev/null
+++ b/src/index.tsx
@@ -0,0 +1,14 @@
+import { serve } from "bun";
+import index from "./index.html";
+
+const server = serve({
+ port: 0,
+ routes: {
+ // Serve index.html for all unmatched routes.
+ "/*": index,
+ },
+
+ development: process.env.NODE_ENV !== "production",
+});
+
+console.log(`🚀 Server running at ${server.url}`);
diff --git a/src/logo.svg b/src/logo.svg
new file mode 100644
index 0000000..6148437
--- /dev/null
+++ b/src/logo.svg
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..83b2227
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "allowJs": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "exclude": ["dist", "node_modules"]
+}