This commit is contained in:
Feror 2025-05-14 17:29:58 +02:00
parent 6ce7afe6fd
commit 193c0f6392
13 changed files with 416 additions and 0 deletions

34
.gitignore vendored Normal file
View file

@ -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

17
bun-env.d.ts vendored Normal file
View file

@ -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;
}

59
bun.lock Normal file
View file

@ -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=="],
}
}

2
bunfig.toml Normal file
View file

@ -0,0 +1,2 @@
[serve.static]
env = "BUN_PUBLIC_*"

23
package.json Normal file
View file

@ -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"
}
}

12
src/App.tsx Normal file
View file

@ -0,0 +1,12 @@
import BudgetTreemap from "./budget-component";
import "./index.css";
export function App() {
return (
<div className="app">
<BudgetTreemap />
</div>
);
}
export default App;

168
src/budget-component.tsx Normal file
View file

@ -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<BudgetItem[]>(initialBudgets);
const [totalBudget, setTotalBudget] = useState<number>(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 (
<div>
<div style={headerStyle}>Répartition de {totalBudget.toFixed(0)} de dépenses publiques</div>
<div style={containerStyle}>
{budgets.map((item, index) => {
const area = item.width * item.height;
const value = (area / totalArea) * 1000;
return (
<Rnd
key={item.name}
size={{ width: item.width, height: item.height }}
position={item.position}
bounds="parent"
onResize={(e, direction, ref, delta, position) => {
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 }}
>
<div>
<strong>{item.name}</strong>
<br />
{value.toFixed(0)}
</div>
</Rnd>
);
})}
</div>
</div>
);
}

26
src/frontend.tsx Normal file
View file

@ -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 = (
<StrictMode>
<App />
</StrictMode>
);
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);
}

15
src/index.css Normal file
View file

@ -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;
}

16
src/index.html Normal file
View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="./logo.svg" />
<title>Fais ton budget.</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./frontend.tsx"></script>
</body>
</html>

14
src/index.tsx Normal file
View file

@ -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}`);

13
src/logo.svg Normal file
View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 310.75 310.75" xml:space="preserve">
<path d="M183.815,265.726c-32.444,0-60.868-21.837-76.306-54.325h102.101v-45.023H95.643c-0.284-3.642-0.437-7.29-0.437-11.016
c0-3.691,0.152-7.384,0.437-10.977h113.969V99.353H107.51c15.438-32.485,43.861-54.315,76.306-54.315
c31.01,0,60.21,20.759,76.2,54.152l40.626-19.418C277.091,30.554,232.329,0,183.815,0c-36.47,0-70.51,16.665-95.851,46.966
C75.219,62.209,65.481,79.995,59.079,99.353H10.108v45.031h40.39c-0.217,3.617-0.329,7.311-0.329,10.977
c0,3.704,0.112,7.351,0.329,11.016h-40.39V211.4h48.971c6.402,19.356,16.14,37.122,28.886,52.351
c25.341,30.303,59.381,46.999,95.851,46.999c48.515,0,93.275-30.55,116.826-79.767l-40.626-19.454
C244.025,244.965,214.825,265.726,183.815,265.726z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

17
tsconfig.json Normal file
View file

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