Add a short summary and two charts
This commit is contained in:
Generated
+30
@@ -8,8 +8,10 @@
|
|||||||
"name": "fancyscribe",
|
"name": "fancyscribe",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"chart.js": "^4.4.8",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -742,6 +744,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@kurkle/color": {
|
||||||
|
"version": "0.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||||
|
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -1058,6 +1066,18 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/chart.js": {
|
||||||
|
"version": "4.4.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz",
|
||||||
|
"integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@kurkle/color": "^0.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"pnpm": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
@@ -2063,6 +2083,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-chartjs-2": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"chart.js": "^4.1.1",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.0.0",
|
"version": "19.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
|
||||||
|
|||||||
@@ -9,8 +9,10 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"chart.js": "^4.4.8",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
+15
-5
@@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState, useEffect, Fragment } from "react";
|
import React, { useRef, useState, useEffect, Fragment } from "react";
|
||||||
import factionBackground from "../assets/factionBackground.png";
|
import factionBackground from "../assets/factionBackground.png";
|
||||||
import keywordsBackground from "../assets/keywordsBackground.png";
|
import keywordsBackground from "../assets/keywordsBackground.png";
|
||||||
import cardBackground from "../assets/cardBackground.png";
|
import cardBackground from "../assets/cardBackground.png";
|
||||||
@@ -13,8 +13,14 @@ import { Weapons, hasDifferentProfiles } from "./Weapons";
|
|||||||
import { useIndexedDB } from "../helpers/useIndexedDB"; // New hook for IndexedDB
|
import { useIndexedDB } from "../helpers/useIndexedDB"; // New hook for IndexedDB
|
||||||
import { ImgEditor } from "./ImgEditor";
|
import { ImgEditor } from "./ImgEditor";
|
||||||
import { trySettingLocalStorage } from "../helpers/useLocalStorage";
|
import { trySettingLocalStorage } from "../helpers/useLocalStorage";
|
||||||
|
import { ShortSummaryTable } from "./ShortSummaryTable";
|
||||||
|
|
||||||
export const Roster = ({ roster, onePerPage, colorUserChoice }) => {
|
export const Roster = ({
|
||||||
|
roster,
|
||||||
|
onePerPage,
|
||||||
|
colorUserChoice,
|
||||||
|
primaryColor,
|
||||||
|
}) => {
|
||||||
if (!roster) {
|
if (!roster) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -26,7 +32,7 @@ export const Roster = ({ roster, onePerPage, colorUserChoice }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="print-display-none"
|
className="print-display-none flex justify-between"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "var(--primary-color)",
|
backgroundColor: "var(--primary-color)",
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
@@ -37,15 +43,19 @@ export const Roster = ({ roster, onePerPage, colorUserChoice }) => {
|
|||||||
marginBottom: "12px",
|
marginBottom: "12px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{name} [{cost.commandPoints} CP, {cost.points} pts]
|
<span>{name}</span>
|
||||||
|
<span>{cost.points} pts</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{forces.map((force, index) => (
|
{forces.map((force, index) => (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
<ShortSummaryTable force={force} primaryColor={primaryColor} />
|
||||||
<Force
|
<Force
|
||||||
key={index}
|
|
||||||
force={force}
|
force={force}
|
||||||
onePerPage={onePerPage}
|
onePerPage={onePerPage}
|
||||||
colorUserChoice={colorUserChoice}
|
colorUserChoice={colorUserChoice}
|
||||||
/>
|
/>
|
||||||
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Bar } from "react-chartjs-2";
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
BarElement,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
Title,
|
||||||
|
} from "chart.js";
|
||||||
|
import cardBackground from "../assets/cardBackground.png";
|
||||||
|
|
||||||
|
ChartJS.register(
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
BarElement,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
Title,
|
||||||
|
);
|
||||||
|
export const ShortSummaryTable = ({ force, primaryColor }) => {
|
||||||
|
const { units, factionRules, rules, catalog } = force;
|
||||||
|
|
||||||
|
const sortedUnits = units.sort((a, b) => {
|
||||||
|
// sort by points cost desc first, then by name
|
||||||
|
if (a.cost.points < b.cost.points) return 1;
|
||||||
|
if (a.cost.points > b.cost.points) return -1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prepare data for the bar chart, group by toughness and sum points cost
|
||||||
|
const groupedByToughness = units.reduce((acc, unit) => {
|
||||||
|
const toughness = unit.modelStats?.[0]?.toughness || 0;
|
||||||
|
if (!acc[toughness]) {
|
||||||
|
acc[toughness] = { totalPoints: 0, unitNames: [] };
|
||||||
|
}
|
||||||
|
acc[toughness].totalPoints += unit.cost.points;
|
||||||
|
if (!acc[toughness].unitNames.includes(unit.name)) {
|
||||||
|
acc[toughness].unitNames.push(unit.name);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const groupedChartDataToughness = Object.entries(groupedByToughness)
|
||||||
|
.map(([toughness, { totalPoints, unitNames }]) => ({
|
||||||
|
toughness: parseInt(toughness, 10),
|
||||||
|
totalPoints,
|
||||||
|
unitNames,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.toughness - b.toughness);
|
||||||
|
|
||||||
|
// Prepare data for the bar chart, group by movement and sum points cost
|
||||||
|
const groupedByMovement = units.reduce((acc, unit) => {
|
||||||
|
const movement = unit.modelStats?.[0]?.move || 0;
|
||||||
|
if (!acc[movement]) {
|
||||||
|
acc[movement] = { totalPoints: 0, unitNames: [] };
|
||||||
|
}
|
||||||
|
acc[movement].totalPoints += unit.cost.points;
|
||||||
|
if (!acc[movement].unitNames.includes(unit.name)) {
|
||||||
|
acc[movement].unitNames.push(unit.name);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const groupedChartDataMovement = Object.entries(groupedByMovement)
|
||||||
|
.map(([movement, { totalPoints, unitNames }]) => ({
|
||||||
|
movement: parseInt(movement, 10),
|
||||||
|
totalPoints,
|
||||||
|
unitNames,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.movement - b.movement);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="print-display-none my-4 -mt-4 flex flex-wrap border-2 border-[var(--primary-color)]"
|
||||||
|
style={{
|
||||||
|
background: `url(${cardBackground})`,
|
||||||
|
backgroundSize: "cover",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="mt-4 table w-full border-collapse overflow-hidden md:w-[50%] md:flex-1">
|
||||||
|
<div className="table-header-group bg-[var(--primary-color)] font-bold uppercase text-white">
|
||||||
|
<div className="table-row">
|
||||||
|
<div
|
||||||
|
className="table-cell border border-[var(--primary-color)] px-4 py-1"
|
||||||
|
style={{
|
||||||
|
fontSize: "1.1em",
|
||||||
|
color: "#fff",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Unit Name
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="table-cell border border-[var(--primary-color)] px-4 py-1 text-right"
|
||||||
|
style={{
|
||||||
|
fontSize: "1.1em",
|
||||||
|
color: "#fff",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cost (pts)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="table-row-group" style={{ lineHeight: 1.05 }}>
|
||||||
|
{sortedUnits.map((unit, index) => {
|
||||||
|
let { name, cost, models } = unit;
|
||||||
|
const count = models?.[0]?.count || 1;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={name + index}
|
||||||
|
className={`table-row ${index % 2 === 0 ? "" : "bg-[#c4c4c480]"}`}
|
||||||
|
>
|
||||||
|
<div className="table-cell border border-dotted border-[#9e9fa1] px-4 py-1">
|
||||||
|
{count > 1 ? count + "x " : ""}
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
<div className="table-cell border border-dotted border-[#9e9fa1] px-4 py-1 text-right">
|
||||||
|
{cost.points} pts
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex w-full flex-col gap-2.5 border-[var(--primary-color)] p-4 md:w-[50%] md:flex-1 md:border-l-2">
|
||||||
|
<div className="relative w-full" style={{ height: "150px" }}>
|
||||||
|
<Bar
|
||||||
|
data={{
|
||||||
|
labels: groupedChartDataMovement.map(
|
||||||
|
(data) => data.movement + '"',
|
||||||
|
),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "Total Cost (pts)",
|
||||||
|
data: groupedChartDataMovement.map(
|
||||||
|
(data) => data.totalPoints,
|
||||||
|
),
|
||||||
|
backgroundColor: primaryColor,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: "MOVEMENT OF YOUR UNITS",
|
||||||
|
font: {
|
||||||
|
size: 14,
|
||||||
|
weight: "bold",
|
||||||
|
family: "Noto Sans",
|
||||||
|
},
|
||||||
|
color: "#000",
|
||||||
|
align: "start",
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
displayColors: false,
|
||||||
|
callbacks: {
|
||||||
|
title: function (context) {
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
label: function (context) {
|
||||||
|
const movement = context.label;
|
||||||
|
const unitNames = groupedChartDataMovement.find(
|
||||||
|
(data) => data.movement === parseInt(movement, 10),
|
||||||
|
)?.unitNames;
|
||||||
|
return unitNames;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative w-full" style={{ height: "150px" }}>
|
||||||
|
<Bar
|
||||||
|
data={{
|
||||||
|
labels: groupedChartDataToughness.map((data) => data.toughness),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "Total Cost (pts)",
|
||||||
|
data: groupedChartDataToughness.map(
|
||||||
|
(data) => data.totalPoints,
|
||||||
|
),
|
||||||
|
backgroundColor: primaryColor,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: "TOUGHNESS OF YOUR UNITS",
|
||||||
|
font: {
|
||||||
|
size: 14,
|
||||||
|
weight: "bold",
|
||||||
|
family: "Noto Sans",
|
||||||
|
},
|
||||||
|
color: "#000",
|
||||||
|
align: "start",
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
displayColors: false,
|
||||||
|
callbacks: {
|
||||||
|
title: function (context) {
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
label: function (context) {
|
||||||
|
const toughness = context.label;
|
||||||
|
const unitNames = groupedChartDataToughness.find(
|
||||||
|
(data) => data.toughness === parseInt(toughness, 10),
|
||||||
|
)?.unitNames;
|
||||||
|
return unitNames;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -214,6 +214,7 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
id="js_app"
|
||||||
className="App"
|
className="App"
|
||||||
style={{
|
style={{
|
||||||
"--primary-color": primaryColor,
|
"--primary-color": primaryColor,
|
||||||
@@ -437,6 +438,7 @@ function App() {
|
|||||||
roster={roster}
|
roster={roster}
|
||||||
onePerPage={onePerPage}
|
onePerPage={onePerPage}
|
||||||
colorUserChoice={colorUserChoice}
|
colorUserChoice={colorUserChoice}
|
||||||
|
primaryColor={primaryColor}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user