Compare commits

...

5 Commits

Author SHA1 Message Date
3ca967591e Leaderboard: Implement simple points cumsum chart
All checks were successful
Build Formula11 Docker Image / pocketbase-docker (push) Successful in 1m3s
2025-06-07 19:59:39 +02:00
6e6ce020a3 Env: Add carbon-charts dep 2025-06-07 19:59:22 +02:00
d54ee01227 Lib: Update fetchers and schema after database changes 2025-06-07 19:58:34 +02:00
3339ffaa5f Seasonpicks: Invert table background colors 2025-06-06 16:26:53 +02:00
78ee291795 Racepicks: Invert table background colors 2025-06-06 16:26:46 +02:00
9 changed files with 1205 additions and 32 deletions

1016
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
}, },
"devDependencies": { "devDependencies": {
"@carbon/charts-svelte": "^1.22.18",
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@fsouza/prettierd": "^0.26.1", "@fsouza/prettierd": "^0.26.1",
"@skeletonlabs/skeleton": "^2.10.4", "@skeletonlabs/skeleton": "^2.10.4",

View File

@ -9,6 +9,7 @@ import type {
RacePick, RacePick,
RacePickPoints, RacePickPoints,
RacePickPointsAcc, RacePickPointsAcc,
RacePickPointsTotal,
RaceResult, RaceResult,
ScrapedDriverStanding, ScrapedDriverStanding,
ScrapedRaceResult, ScrapedRaceResult,
@ -281,7 +282,7 @@ export const fetch_racepickpoints = async (
}; };
/** /**
* Fetch all [RacePickPointsAcc] from the database, ordered descendingly by total points. * Fetch all [RacePickPointsAcc] from the database, ordered ascendingly by step.
*/ */
export const fetch_racepickpointsacc = async ( export const fetch_racepickpointsacc = async (
fetch: (_: any) => Promise<Response>, fetch: (_: any) => Promise<Response>,
@ -293,6 +294,19 @@ export const fetch_racepickpointsacc = async (
return racepickpointsacc; return racepickpointsacc;
}; };
/**
* Fetch all [RacePickPointsTotal] from the database, ordered descendingly by total points.
*/
export const fetch_racepickpointstotal = async (
fetch: (_: any) => Promise<Response>,
): Promise<RacePickPointsTotal[]> => {
const racepickpointstotal: RacePickPointsTotal[] = await pb
.collection("racepickpointstotal")
.getFullList({ fetch: fetch });
return racepickpointstotal;
};
/** /**
* Fetch all [ScrapedDriverStandings] from the database, ordered ascendingly by position. * Fetch all [ScrapedDriverStandings] from the database, ordered ascendingly by position.
*/ */

View File

@ -140,11 +140,21 @@ export interface RacePickPoints {
} }
export interface RacePickPointsAcc { export interface RacePickPointsAcc {
id: string;
user: string;
step: number;
acc_pxx_points: number;
acc_dnf_points: number;
acc_points: number;
}
export interface RacePickPointsTotal {
id: string; id: string;
user: string; user: string;
total_pxx_points: number; total_pxx_points: number;
total_dnf_points: number; total_dnf_points: number;
total_points: number; total_points: number;
total_points_per_pick: number;
} }
// Scraped Data // Scraped Data

View File

@ -1,10 +1,28 @@
<script lang="ts"> <script lang="ts">
import { Table, type TableColumn } from "$lib/components"; import { Table, type TableColumn } from "$lib/components";
import { get_by_value } from "$lib/database"; import { get_by_value } from "$lib/database";
import type { RacePickPoints, RacePickPointsAcc, User } from "$lib/schema";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import {
LineChart,
ScaleTypes,
type ChartTabularData,
type LineChartOptions,
} from "@carbon/charts-svelte";
import "@carbon/charts-svelte/styles.css";
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
// Await promises
let users: User[] | undefined = $state(undefined);
data.users.then((u: User[]) => (users = u));
let racepickpoints: RacePickPoints[] | undefined = $state(undefined);
data.racepickpoints.then((r: RacePickPoints[]) => (racepickpoints = r));
let racepickpointsacc: RacePickPointsAcc[] | undefined = $state(undefined);
data.racepickpointsacc.then((r: RacePickPointsAcc[]) => (racepickpointsacc = r));
const leaderboard_columns: TableColumn[] = $derived([ const leaderboard_columns: TableColumn[] = $derived([
{ {
data_value_name: "user", data_value_name: "user",
@ -14,15 +32,124 @@
}, },
{ {
data_value_name: "total_points", data_value_name: "total_points",
label: "Points", label: "Total",
},
{
data_value_name: "total_pxx_points",
label: "PXX",
},
{
data_value_name: "total_dnf_points",
label: "DNF",
},
{
data_value_name: "total_points_per_pick",
label: "Per Pick",
valuefun: async (value: string): Promise<string> => Number.parseFloat(value).toFixed(2),
}, },
]); ]);
const points_chart_data: ChartTabularData = $derived.by(() => {
if (!users || !racepickpointsacc) return [];
return users
.map((user: User) => {
return {
group: user.firstname,
step: "0",
points: 0,
};
})
.concat(
racepickpointsacc.map((points: RacePickPointsAcc) => {
return {
group: get_by_value(users ?? [], "id", points.user)?.firstname || "INVALID",
step: points.step.toString(),
points: points.acc_points,
};
}),
);
});
const points_chart_options: LineChartOptions = {
title: "I ❤️ CumSum",
axes: {
bottom: {
mapsTo: "step",
scaleType: ScaleTypes.LABELS,
},
left: {
mapsTo: "points",
scaleType: ScaleTypes.LINEAR,
},
},
curve: "curveMonotoneX",
// toolbar: {
// enabled: false,
// },
animations: true,
// canvasZoom: {
// enabled: false,
// },
grid: {
x: {
enabled: true,
alignWithAxisTicks: true,
},
y: {
enabled: true,
alignWithAxisTicks: true,
},
},
legend: {
enabled: true,
clickable: true,
position: "top",
},
points: {
enabled: true,
radius: 5,
},
tooltip: {
showTotal: false,
},
resizable: true,
width: "100%",
height: "400px",
};
</script> </script>
<svelte:head> <svelte:head>
<title>Formula 11 - Leaderboard</title> <title>Formula 11 - Leaderboard</title>
</svelte:head> </svelte:head>
{#await Promise.all( [data.users, data.racepickpoints, data.racepickpointsacc], ) then [users, racepickpoints, racepickpointsacc]} {#await Promise.all( [data.users, data.racepickpoints, data.racepickpointsacc, data.racepickpointstotal], ) then [users, racepickpoints, racepickpointsacc, racepickpointstotal]}
<Table data={racepickpointsacc} columns={leaderboard_columns} /> <div class="flex gap-2">
<!-- Podium -->
<!-- <div class="card w-60 bg-surface-100 p-2 shadow"> -->
<!-- <div class="flex h-20 w-full gap-1"></div> -->
<!-- <div class="flex h-20 w-full gap-1"> -->
<!-- <div class="w-20"> -->
<!-- <div class="h-[30%] w-full"></div> -->
<!-- <div class="h-[70%] w-full bg-surface-500"></div> -->
<!-- </div> -->
<!-- <div class="w-20"> -->
<!-- <div class="h-[100%] w-full bg-surface-500"></div> -->
<!-- </div> -->
<!-- <div class="w-20"> -->
<!-- <div class="h-[60%] w-full"></div> -->
<!-- <div class="h-[40%] w-full bg-surface-600"></div> -->
<!-- </div> -->
<!-- </div> -->
<!-- </div> -->
<!-- Points chart -->
<div class="card w-full bg-surface-100 p-2 shadow">
<LineChart data={points_chart_data} options={points_chart_options} />
</div>
</div>
<div class="mt-2">
<Table data={racepickpointstotal} columns={leaderboard_columns} />
</div>
{/await} {/await}

View File

@ -1,4 +1,9 @@
import { fetch_users, fetch_racepickpoints, fetch_racepickpointsacc } from "$lib/fetch"; import {
fetch_users,
fetch_racepickpoints,
fetch_racepickpointsacc,
fetch_racepickpointstotal,
} from "$lib/fetch";
import type { PageLoad } from "../$types"; import type { PageLoad } from "../$types";
export const load: PageLoad = async ({ fetch, depends }) => { export const load: PageLoad = async ({ fetch, depends }) => {
@ -8,5 +13,6 @@ export const load: PageLoad = async ({ fetch, depends }) => {
users: fetch_users(fetch), users: fetch_users(fetch),
racepickpoints: fetch_racepickpoints(fetch), racepickpoints: fetch_racepickpoints(fetch),
racepickpointsacc: fetch_racepickpointsacc(fetch), racepickpointsacc: fetch_racepickpointsacc(fetch),
racepickpointstotal: fetch_racepickpointstotal(fetch),
}; };
}; };

View File

@ -256,8 +256,8 @@
{#await data.currentpickedusers then currentpicked} {#await data.currentpickedusers then currentpicked}
{#each currentpicked as user} {#each currentpicked as user}
<div <div
class="card ml-1 mt-2 w-full min-w-14 rounded-b-none py-2 class="card ml-1 mt-2 w-full min-w-14 rounded-b-none bg-surface-400 py-2
{$pbUser && $pbUser.username === user.username ? 'bg-primary-300' : ''}" {$pbUser && $pbUser.username === user.username ? '!bg-primary-400' : ''}"
> >
<!-- Avatar + name display at the top --> <!-- Avatar + name display at the top -->
<div class="m-auto flex h-10 w-fit"> <div class="m-auto flex h-10 w-fit">
@ -293,7 +293,7 @@
<div <div
use:popup={race_popupsettings(race?.id ?? "Invalid")} use:popup={race_popupsettings(race?.id ?? "Invalid")}
class="card mt-1 flex h-20 w-7 cursor-pointer flex-col !rounded-r-none lg:w-36 lg:p-2" class="card mt-1 flex h-16 w-7 cursor-pointer flex-col !rounded-r-none bg-surface-400 lg:h-20 lg:w-36 lg:p-2"
> >
<!-- For large screens --> <!-- For large screens -->
<span class="hidden text-sm font-bold lg:block"> <span class="hidden text-sm font-bold lg:block">
@ -306,13 +306,13 @@
<!-- For small screens --> <!-- For small screens -->
<!-- TODO: This requires the race name to end with an emoji, but this is never enforced --> <!-- TODO: This requires the race name to end with an emoji, but this is never enforced -->
<div class="mx-[2px] my-[25px] block text-lg font-bold lg:hidden"> <div class="mx-[2px] my-[18px] block text-lg font-bold lg:hidden">
{runes(race?.name ?? "Invalid").at(-1)} {runes(race?.name ?? "Invalid").at(-1)}
</div> </div>
</div> </div>
<!-- The race result popup is triggered on click on the race --> <!-- The race result popup is triggered on click on the race -->
<div data-popup={race?.id ?? "Invalid"} class="card z-50 p-2 shadow"> <div data-popup={race?.id ?? "Invalid"} class="card z-50 bg-surface-400 p-2 shadow">
<span class="font-bold">Result:</span> <span class="font-bold">Result:</span>
<div class="mt-2 flex flex-col gap-1"> <div class="mt-2 flex flex-col gap-1">
{#each result.pxxs as pxx, index} {#each result.pxxs as pxx, index}
@ -360,7 +360,7 @@
result.dnfs.indexOf(pick?.dnf ?? "Invalid") >= 0 ? PXX_COLORS[3] : PXX_COLORS[-1]} result.dnfs.indexOf(pick?.dnf ?? "Invalid") >= 0 ? PXX_COLORS[3] : PXX_COLORS[-1]}
{#if pick} {#if pick}
<div class="mt-1 h-20 w-full border bg-surface-300 px-1 py-2 lg:px-2"> <div class="mt-1 h-16 w-full border bg-surface-200 px-1 py-2 lg:h-20 lg:px-2">
<div class="mx-auto flex h-full w-fit flex-col justify-evenly"> <div class="mx-auto flex h-full w-fit flex-col justify-evenly">
<span <span
class="w-10 p-1 text-center text-xs rounded-container-token lg:text-sm" class="w-10 p-1 text-center text-xs rounded-container-token lg:text-sm"
@ -377,7 +377,7 @@
</div> </div>
</div> </div>
{:else} {:else}
<div class="mt-1 h-20 w-full px-1 py-2 lg:px-2"></div> <div class="mt-1 h-16 w-full px-1 py-2 lg:h-20 lg:px-2"></div>
{/if} {/if}
{/each} {/each}
</div> </div>

View File

@ -264,8 +264,8 @@
{#await data.seasonpickedusers then seasonpicked} {#await data.seasonpickedusers then seasonpicked}
{#each seasonpicked as user} {#each seasonpicked as user}
<div <div
class="card ml-1 mt-2 w-full min-w-36 rounded-b-none py-2 class="card ml-1 mt-2 w-full min-w-36 rounded-b-none bg-surface-400 py-2
{$pbUser && $pbUser.username === user.username ? 'bg-primary-300' : ''}" {$pbUser && $pbUser.username === user.username ? '!bg-primary-400' : ''}"
> >
<!-- Avatar + name display at the top --> <!-- Avatar + name display at the top -->
<div class="m-auto flex h-10 w-fit"> <div class="m-auto flex h-10 w-fit">
@ -296,7 +296,7 @@
class="sticky left-0 z-10 w-7 min-w-7 max-w-7 bg-surface-50 lg:w-36 lg:min-w-36 lg:max-w-36" class="sticky left-0 z-10 w-7 min-w-7 max-w-7 bg-surface-50 lg:w-36 lg:min-w-36 lg:max-w-36"
> >
<!-- Hottake --> <!-- Hottake -->
<div class="card mt-1 flex h-32 w-7 flex-col !rounded-r-none p-2 lg:w-36"> <div class="card mt-1 flex h-32 w-7 flex-col !rounded-r-none bg-surface-400 p-2 lg:w-36">
<span class="hidden text-nowrap text-sm font-bold lg:block">Hottake</span> <span class="hidden text-nowrap text-sm font-bold lg:block">Hottake</span>
<span class="block rotate-90 text-nowrap text-xs font-bold lg:hidden">Hottake</span> <span class="block rotate-90 text-nowrap text-xs font-bold lg:hidden">Hottake</span>
</div> </div>
@ -304,13 +304,13 @@
{#await data.seasonpicks then seasonpicks} {#await data.seasonpicks then seasonpicks}
{#if seasonpicks.length > 0} {#if seasonpicks.length > 0}
<!-- Drivers Champion --> <!-- Drivers Champion -->
<div class="card mt-1 flex h-20 w-7 flex-col !rounded-r-none p-2 lg:w-36"> <div class="card mt-1 flex h-20 w-7 flex-col !rounded-r-none bg-surface-400 p-2 lg:w-36">
<span class="hidden text-nowrap text-sm font-bold lg:block">Drivers<br />Champion</span> <span class="hidden text-nowrap text-sm font-bold lg:block">Drivers<br />Champion</span>
<span class="block rotate-90 text-nowrap text-xs font-bold lg:hidden">WDC</span> <span class="block rotate-90 text-nowrap text-xs font-bold lg:hidden">WDC</span>
</div> </div>
<!-- Constructors Champion --> <!-- Constructors Champion -->
<div class="card mt-1 flex h-20 w-7 flex-col !rounded-r-none p-2 lg:w-36"> <div class="card mt-1 flex h-20 w-7 flex-col !rounded-r-none bg-surface-400 p-2 lg:w-36">
<span class="hidden text-nowrap text-sm font-bold lg:block"> <span class="hidden text-nowrap text-sm font-bold lg:block">
Constructors<br />Champion Constructors<br />Champion
</span> </span>
@ -318,25 +318,25 @@
</div> </div>
<!-- Overtakes --> <!-- Overtakes -->
<div class="card mt-1 flex h-20 w-7 flex-col !rounded-r-none p-2 lg:w-36"> <div class="card mt-1 flex h-20 w-7 flex-col !rounded-r-none bg-surface-400 p-2 lg:w-36">
<span class="hidden text-nowrap text-sm font-bold lg:block">Most Overtakes</span> <span class="hidden text-nowrap text-sm font-bold lg:block">Most Overtakes</span>
<span class="block rotate-90 text-nowrap text-xs font-bold lg:hidden">Overtakes</span> <span class="block rotate-90 text-nowrap text-xs font-bold lg:hidden">Overtakes</span>
</div> </div>
<!-- DNFs --> <!-- DNFs -->
<div class="card mt-1 flex h-20 w-7 flex-col !rounded-r-none p-2 lg:w-36"> <div class="card mt-1 flex h-20 w-7 flex-col !rounded-r-none bg-surface-400 p-2 lg:w-36">
<span class="hidden text-nowrap text-sm font-bold lg:block">Most DNFs</span> <span class="hidden text-nowrap text-sm font-bold lg:block">Most DNFs</span>
<span class="block rotate-90 text-nowrap text-xs font-bold lg:hidden">DNFs</span> <span class="block rotate-90 text-nowrap text-xs font-bold lg:hidden">DNFs</span>
</div> </div>
<!-- Doohan Starts --> <!-- Doohan Starts -->
<div class="card mt-1 flex h-20 w-7 flex-col !rounded-r-none p-2 lg:w-36"> <div class="card mt-1 flex h-20 w-7 flex-col !rounded-r-none bg-surface-400 p-2 lg:w-36">
<span class="hidden text-nowrap text-sm font-bold lg:block">Doohan Starts</span> <span class="hidden text-nowrap text-sm font-bold lg:block">Doohan Starts</span>
<span class="block rotate-90 text-nowrap text-xs font-bold lg:hidden">Doohan</span> <span class="block rotate-90 text-nowrap text-xs font-bold lg:hidden">Doohan</span>
</div> </div>
<!-- Teamwinners --> <!-- Teamwinners -->
<div class="card mt-1 flex h-64 w-7 flex-col !rounded-r-none p-2 lg:w-36"> <div class="card mt-1 flex h-64 w-7 flex-col !rounded-r-none bg-surface-400 p-2 lg:w-36">
<span class="hidden text-nowrap text-sm font-bold lg:block" <span class="hidden text-nowrap text-sm font-bold lg:block"
>Team-Battle<br />Winners</span >Team-Battle<br />Winners</span
> >
@ -344,7 +344,7 @@
</div> </div>
<!-- Podiums --> <!-- Podiums -->
<div class="card mt-1 flex h-64 w-7 flex-col !rounded-r-none p-2 lg:w-36"> <div class="card mt-1 flex h-64 w-7 flex-col !rounded-r-none bg-surface-400 p-2 lg:w-36">
<span class="hidden text-nowrap text-sm font-bold lg:block">Podiums</span> <span class="hidden text-nowrap text-sm font-bold lg:block">Podiums</span>
<span class="block rotate-90 text-nowrap text-xs font-bold lg:hidden">Podiums</span> <span class="block rotate-90 text-nowrap text-xs font-bold lg:hidden">Podiums</span>
</div> </div>
@ -374,7 +374,7 @@
<div class="ml-1 w-full min-w-36"> <div class="ml-1 w-full min-w-36">
<!-- Hottake --> <!-- Hottake -->
<div <div
class="mt-1 h-32 w-full overflow-y-scroll border bg-surface-300 px-1 py-2 leading-3 lg:px-2" class="mt-1 h-32 w-full overflow-y-scroll border bg-surface-200 px-1 py-2 leading-3 lg:px-2"
> >
<div class="mx-auto w-fit text-xs font-bold lg:text-sm"> <div class="mx-auto w-fit text-xs font-bold lg:text-sm">
{hottake?.hottake ?? "?"} {hottake?.hottake ?? "?"}
@ -383,7 +383,7 @@
{#if seasonpicks.length > 0} {#if seasonpicks.length > 0}
<!-- Drivers Champion --> <!-- Drivers Champion -->
<div class="mt-1 h-20 w-full border bg-surface-300 px-1 py-2 leading-3 lg:px-2"> <div class="mt-1 h-20 w-full border bg-surface-200 px-1 py-2 leading-3 lg:px-2">
<div class="mx-auto w-fit"> <div class="mx-auto w-fit">
<!-- NOTE: The containerstyle should be 64x64, don't know why that doesn't fit... (also below) --> <!-- NOTE: The containerstyle should be 64x64, don't know why that doesn't fit... (also below) -->
<LazyImage <LazyImage
@ -398,7 +398,7 @@
</div> </div>
<!-- Constructors Champion --> <!-- Constructors Champion -->
<div class="mt-1 h-20 w-full border bg-surface-300 p-1 px-1 py-2 leading-3 lg:px-2"> <div class="mt-1 h-20 w-full border bg-surface-200 p-1 px-1 py-2 leading-3 lg:px-2">
<div class="mx-auto w-fit"> <div class="mx-auto w-fit">
<LazyImage <LazyImage
src={wccwinner?.banner_url ?? get_team_banner_template(data.graphics)} src={wccwinner?.banner_url ?? get_team_banner_template(data.graphics)}
@ -412,7 +412,7 @@
</div> </div>
<!-- Most Overtakes --> <!-- Most Overtakes -->
<div class="mt-1 h-20 w-full border bg-surface-300 px-1 py-2 leading-3 lg:px-2"> <div class="mt-1 h-20 w-full border bg-surface-200 px-1 py-2 leading-3 lg:px-2">
<div class="mx-auto w-fit"> <div class="mx-auto w-fit">
<LazyImage <LazyImage
src={mostovertakes?.headshot_url ?? get_driver_headshot_template(data.graphics)} src={mostovertakes?.headshot_url ?? get_driver_headshot_template(data.graphics)}
@ -428,7 +428,7 @@
</div> </div>
<!-- Most DNFs --> <!-- Most DNFs -->
<div class="mt-1 h-20 w-full border bg-surface-300 px-1 py-2 leading-3 lg:px-2"> <div class="mt-1 h-20 w-full border bg-surface-200 px-1 py-2 leading-3 lg:px-2">
<div class="mx-auto w-fit"> <div class="mx-auto w-fit">
<LazyImage <LazyImage
src={mostdnfs?.headshot_url ?? get_driver_headshot_template(data.graphics)} src={mostdnfs?.headshot_url ?? get_driver_headshot_template(data.graphics)}
@ -442,7 +442,7 @@
</div> </div>
<!-- Doohan Starts --> <!-- Doohan Starts -->
<div class="mt-1 h-20 w-full border bg-surface-300 p-1 px-1 py-2 leading-3 lg:px-2"> <div class="mt-1 h-20 w-full border bg-surface-200 p-1 px-1 py-2 leading-3 lg:px-2">
<div class="mx-auto w-fit text-xs lg:text-sm"> <div class="mx-auto w-fit text-xs lg:text-sm">
Jack Doohan startet <span class="font-bold">{pick?.doohanstarts ?? "?"}</span> mal. Jack Doohan startet <span class="font-bold">{pick?.doohanstarts ?? "?"}</span> mal.
</div> </div>
@ -450,7 +450,7 @@
<!-- Teamwinners --> <!-- Teamwinners -->
<div <div
class="mt-1 h-64 w-full overflow-y-scroll border bg-surface-300 p-1 px-1 py-2 leading-3 lg:px-2" class="mt-1 h-64 w-full overflow-y-scroll border bg-surface-200 p-1 px-1 py-2 leading-3 lg:px-2"
> >
{#if pick && pick.teamwinners} {#if pick && pick.teamwinners}
<div class="grid grid-cols-2 gap-1"> <div class="grid grid-cols-2 gap-1">
@ -479,7 +479,7 @@
<!-- Podiums --> <!-- Podiums -->
<!-- TODO: Replace all style tags throughout the page with custom classes like height here --> <!-- TODO: Replace all style tags throughout the page with custom classes like height here -->
<div <div
class="mt-1 h-64 w-full overflow-y-scroll border bg-surface-300 p-1 px-1 py-2 leading-3 shadow lg:px-2" class="mt-1 h-64 w-full overflow-y-scroll border bg-surface-200 p-1 px-1 py-2 leading-3 shadow lg:px-2"
> >
{#if pick && pick.podiums} {#if pick && pick.podiums}
<div class="grid grid-cols-2 gap-1"> <div class="grid grid-cols-2 gap-1">

View File

@ -3,6 +3,9 @@ import { defineConfig } from "vite";
export default defineConfig({ export default defineConfig({
plugins: [sveltekit()], plugins: [sveltekit()],
ssr: {
noExternal: process.env.NODE_ENV === "production" ? ["@carbon/charts"] : [],
},
build: { build: {
rollupOptions: { rollupOptions: {
external: ["sharp"], external: ["sharp"],