mirror of
https://github.com/jcreek/SvelteKitSaasBoilerplate.git
synced 2026-07-12 18:43:50 +00:00
feat(#54): Improve display of credit usage and accessibility
This commit is contained in:
@@ -52,6 +52,7 @@
|
||||
"@supabase/ssr": "^0.4.0",
|
||||
"@supabase/supabase-js": "^2.44.2",
|
||||
"chart.js": "^4.4.6",
|
||||
"simple-datatables": "^9.2.1",
|
||||
"stripe": "^15.4.0",
|
||||
"ts-debounce": "^4.0.0",
|
||||
"winston": "^3.15.0"
|
||||
|
||||
Generated
+21
@@ -23,6 +23,9 @@ importers:
|
||||
chart.js:
|
||||
specifier: ^4.4.6
|
||||
version: 4.4.6
|
||||
simple-datatables:
|
||||
specifier: ^9.2.1
|
||||
version: 9.2.1
|
||||
stripe:
|
||||
specifier: ^15.4.0
|
||||
version: 15.12.0
|
||||
@@ -1680,6 +1683,9 @@ packages:
|
||||
resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
dayjs@1.11.13:
|
||||
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
||||
|
||||
debug@4.3.7:
|
||||
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
@@ -1741,6 +1747,9 @@ packages:
|
||||
didyoumean@1.2.2:
|
||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||
|
||||
diff-dom@5.1.4:
|
||||
resolution: {integrity: sha512-TSEaVdVGictY1KHg7VpVw2nuM02YKC9C8/qBkGiCnkiAybVbu1zQTMj2/dnVLRO7Z62UsqzHGpXweiOj5/jaZg==}
|
||||
|
||||
dir-glob@3.0.1:
|
||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3003,6 +3012,9 @@ packages:
|
||||
simple-concat@1.0.1:
|
||||
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
|
||||
|
||||
simple-datatables@9.2.1:
|
||||
resolution: {integrity: sha512-R+KRyZ4GNQdqj5IoW08OYi9FHqCbndbw+MfdOJDnwOGs6me9+BhbNuj2QHZR/GXQ04TLra43Uj2/Lk1KcLiJRw==}
|
||||
|
||||
simple-get@4.0.1:
|
||||
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
||||
|
||||
@@ -5353,6 +5365,8 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
is-data-view: 1.0.1
|
||||
|
||||
dayjs@1.11.13: {}
|
||||
|
||||
debug@4.3.7:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@@ -5402,6 +5416,8 @@ snapshots:
|
||||
|
||||
didyoumean@1.2.2: {}
|
||||
|
||||
diff-dom@5.1.4: {}
|
||||
|
||||
dir-glob@3.0.1:
|
||||
dependencies:
|
||||
path-type: 4.0.0
|
||||
@@ -6715,6 +6731,11 @@ snapshots:
|
||||
|
||||
simple-concat@1.0.1: {}
|
||||
|
||||
simple-datatables@9.2.1:
|
||||
dependencies:
|
||||
dayjs: 1.11.13
|
||||
diff-dom: 5.1.4
|
||||
|
||||
simple-get@4.0.1:
|
||||
dependencies:
|
||||
decompress-response: 6.0.0
|
||||
|
||||
@@ -17,9 +17,25 @@ export async function load({ locals: { safeGetSession } }) {
|
||||
const creditsRemaining = await getUserCredits(userId);
|
||||
const transactions = await getUserCreditTransactions(userId);
|
||||
|
||||
const monthlyAggregates = transactions.reduce((acc, transaction) => {
|
||||
const date = new Date(transaction.created_at);
|
||||
const month = `${date.getFullYear()}-${date.getMonth() + 1}`;
|
||||
|
||||
if (!acc[month]) acc[month] = { credits_added: 0, credits_deducted: 0 };
|
||||
|
||||
if (transaction.credits_change > 0) {
|
||||
acc[month].credits_added += transaction.credits_change;
|
||||
} else {
|
||||
acc[month].credits_deducted += transaction.credits_change;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
creditsRemaining,
|
||||
transactions
|
||||
transactions,
|
||||
monthlyAggregates
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
|
||||
@@ -1,59 +1,120 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import Chart, { type ChartData } from 'chart.js/auto';
|
||||
import { DataTable } from 'simple-datatables';
|
||||
|
||||
export let data;
|
||||
const { creditsRemaining, transactions } = data;
|
||||
const { creditsRemaining, transactions, monthlyAggregates } = data;
|
||||
|
||||
let chartCanvas: HTMLCanvasElement | null = null;
|
||||
let creditsTrendChart: Chart | null = null;
|
||||
let noTransactions = transactions ? transactions.length === 0 : true;
|
||||
let creditsChart: Chart | null = null;
|
||||
let chartType: 'bar' | 'line' = 'bar';
|
||||
let chartData: ChartData | null = null;
|
||||
|
||||
if (!noTransactions) {
|
||||
chartData = {
|
||||
labels: transactions.map((t) => new Date(t.created_at).toLocaleDateString()),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Credit Changes',
|
||||
data: transactions.map((t) => t.credits_change),
|
||||
backgroundColor: transactions.map((t) => (t.credits_change > 0 ? 'green' : 'red'))
|
||||
}
|
||||
]
|
||||
};
|
||||
const monthlyLabels = Object.keys(monthlyAggregates).reverse();
|
||||
const creditsAdded = monthlyLabels.map((month) => monthlyAggregates[month].credits_added);
|
||||
const creditsDeducted = monthlyLabels.map((month) =>
|
||||
Math.abs(monthlyAggregates[month].credits_deducted)
|
||||
);
|
||||
|
||||
const groupedTransactions = transactions.reduce((acc, transaction) => {
|
||||
const date = new Date(transaction.created_at);
|
||||
const month = `${date.getFullYear()}-${date.getMonth() + 1}`;
|
||||
if (!acc[month]) acc[month] = [];
|
||||
acc[month].push(transaction);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
let cumulativeBalance = 0;
|
||||
const cumulativeBalanceData = monthlyLabels.map((month) => {
|
||||
const monthTransactions = groupedTransactions[month] || [];
|
||||
const monthCreditsChange = monthTransactions.reduce((sum, transaction) => {
|
||||
return sum + transaction.credits_change;
|
||||
}, 0);
|
||||
|
||||
cumulativeBalance += monthCreditsChange;
|
||||
|
||||
return cumulativeBalance;
|
||||
});
|
||||
function initialiseChartData() {
|
||||
if (chartType === 'bar') {
|
||||
chartData = {
|
||||
labels: monthlyLabels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Credits Added',
|
||||
data: creditsAdded,
|
||||
backgroundColor: 'green'
|
||||
},
|
||||
{
|
||||
label: 'Credits Deducted',
|
||||
data: creditsDeducted,
|
||||
backgroundColor: 'red'
|
||||
}
|
||||
]
|
||||
};
|
||||
} else if (chartType === 'line') {
|
||||
chartData = {
|
||||
labels: monthlyLabels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Cumulative Balance',
|
||||
data: cumulativeBalanceData,
|
||||
borderColor: 'blue',
|
||||
fill: false
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function updateChart(type: 'bar' | 'line') {
|
||||
chartType = type;
|
||||
initialiseChartData();
|
||||
if (creditsChart) {
|
||||
creditsChart.config.type = chartType;
|
||||
creditsChart.data = chartData;
|
||||
creditsChart.update();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
initialiseChartData();
|
||||
if (chartCanvas && chartData) {
|
||||
creditsTrendChart = new Chart(chartCanvas, {
|
||||
type: 'bar',
|
||||
creditsChart = new Chart(chartCanvas, {
|
||||
type: chartType,
|
||||
data: chartData,
|
||||
options: { responsive: true }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$: if (transactions && chartData) {
|
||||
chartData.labels = transactions.map((t) => new Date(t.created_at).toLocaleDateString());
|
||||
chartData.datasets[0].data = transactions.map((t) => t.credits_change);
|
||||
creditsTrendChart?.update();
|
||||
}
|
||||
const dataTable = new DataTable('#transaction-history');
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="flex flex-col items-center p-4">
|
||||
<h1 class="text-3xl font-bold mb-4">Credit Dashboard</h1>
|
||||
<p class="text-lg">Credits Remaining: {creditsRemaining}</p>
|
||||
|
||||
<div class="flex justify-center my-4">
|
||||
<button on:click={() => updateChart('bar')} class:font-bold={chartType === 'bar'}
|
||||
>Monthly Usage</button
|
||||
>
|
||||
<button on:click={() => updateChart('line')} class:font-bold={chartType === 'line'}
|
||||
>Cumulative Balance</button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="w-full md:w-3/4 mt-6">
|
||||
<canvas bind:this={chartCanvas} class="my-8"></canvas>
|
||||
</div>
|
||||
|
||||
<h2 class="text-2xl font-semibold mb-2">Transaction History</h2>
|
||||
<div class="overflow-x-auto w-full md:w-3/4">
|
||||
{#if noTransactions}
|
||||
{#if transactions.length === 0}
|
||||
<p class="text-lg">No transactions found.</p>
|
||||
{:else}
|
||||
<table class="table-auto w-full text-left border-collapse">
|
||||
<table id="transaction-history" class="table-auto w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b">
|
||||
<th class="px-4 py-2">Date</th>
|
||||
@@ -88,4 +149,15 @@
|
||||
table {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
button {
|
||||
margin: 0 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
button.font-bold {
|
||||
font-weight: bold;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user