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"] +}