mirror of
https://github.com/jcreek/SvelteKitSaasBoilerplate.git
synced 2026-07-12 18:43:50 +00:00
feat(#54): Add credit usage dashboard
This commit is contained in:
@@ -51,6 +51,7 @@
|
||||
"@getbrevo/brevo": "^2.2.0",
|
||||
"@supabase/ssr": "^0.4.0",
|
||||
"@supabase/supabase-js": "^2.44.2",
|
||||
"chart.js": "^4.4.6",
|
||||
"stripe": "^15.4.0",
|
||||
"ts-debounce": "^4.0.0",
|
||||
"winston": "^3.15.0"
|
||||
|
||||
Generated
+16
@@ -20,6 +20,9 @@ importers:
|
||||
'@supabase/supabase-js':
|
||||
specifier: ^2.44.2
|
||||
version: 2.45.4
|
||||
chart.js:
|
||||
specifier: ^4.4.6
|
||||
version: 4.4.6
|
||||
stripe:
|
||||
specifier: ^15.4.0
|
||||
version: 15.12.0
|
||||
@@ -937,6 +940,9 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.25':
|
||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||
|
||||
'@kurkle/color@0.3.2':
|
||||
resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==}
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -1533,6 +1539,10 @@ packages:
|
||||
resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
|
||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||
|
||||
chart.js@4.4.6:
|
||||
resolution: {integrity: sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==}
|
||||
engines: {pnpm: '>=8'}
|
||||
|
||||
chokidar@3.6.0:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
@@ -4530,6 +4540,8 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
'@kurkle/color@0.3.2': {}
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
@@ -5194,6 +5206,10 @@ snapshots:
|
||||
|
||||
chalk@5.3.0: {}
|
||||
|
||||
chart.js@4.4.6:
|
||||
dependencies:
|
||||
'@kurkle/color': 0.3.2
|
||||
|
||||
chokidar@3.6.0:
|
||||
dependencies:
|
||||
anymatch: 3.1.3
|
||||
|
||||
@@ -451,6 +451,28 @@ const getUserCredits = async (userId: string) => {
|
||||
return creditData.credits_remaining;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the credit transaction history for a specific user.
|
||||
*
|
||||
* @param userId - The ID of the user whose transactions are to be fetched.
|
||||
* @returns Array of transaction records containing credits_change, description, and created_at.
|
||||
*/
|
||||
export const getUserCreditTransactions = async (userId: string) => {
|
||||
if (!userId) throw new Error('User ID is required');
|
||||
|
||||
const { data, error } = await supabaseAdmin
|
||||
.from('credit_transactions')
|
||||
.select('credits_change, description, created_at')
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false }); // Orders transactions with the latest ones first
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Error fetching transactions: ${error.message}`);
|
||||
}
|
||||
|
||||
return data || [];
|
||||
};
|
||||
|
||||
type ProductWithPrices = Product & {
|
||||
prices: Price[];
|
||||
actualPrice?: number;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { getUserCredits, getUserCreditTransactions } from '$lib/utils/supabase/admin';
|
||||
|
||||
export async function load({ locals: { safeGetSession } }) {
|
||||
const { session } = await safeGetSession();
|
||||
|
||||
if (!session) {
|
||||
redirect(303, '/');
|
||||
}
|
||||
|
||||
const userId = session.user?.id;
|
||||
if (!userId) {
|
||||
return { error: 'User not authenticated' };
|
||||
}
|
||||
|
||||
try {
|
||||
const creditsRemaining = await getUserCredits(userId);
|
||||
const transactions = await getUserCreditTransactions(userId);
|
||||
|
||||
return {
|
||||
creditsRemaining,
|
||||
transactions
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import Chart, { type ChartData } from 'chart.js/auto';
|
||||
|
||||
export let data;
|
||||
const { creditsRemaining, transactions } = data;
|
||||
|
||||
let chartCanvas: HTMLCanvasElement | null = null;
|
||||
let creditsTrendChart: Chart | null = null;
|
||||
let noTransactions = transactions ? transactions.length === 0 : true;
|
||||
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'))
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (chartCanvas && chartData) {
|
||||
creditsTrendChart = new Chart(chartCanvas, {
|
||||
type: 'bar',
|
||||
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();
|
||||
}
|
||||
</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="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}
|
||||
<p class="text-lg">No transactions found.</p>
|
||||
{:else}
|
||||
<table class="table-auto w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b">
|
||||
<th class="px-4 py-2">Date</th>
|
||||
<th class="px-4 py-2">Description</th>
|
||||
<th class="px-4 py-2">Change</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each transactions as transaction}
|
||||
<tr class="border-b hover:bg-gray-100">
|
||||
<td class="px-4 py-2">{new Date(transaction.created_at).toLocaleDateString()}</td>
|
||||
<td class="px-4 py-2">{transaction.description}</td>
|
||||
<td
|
||||
class="px-4 py-2"
|
||||
class:text-green-600={transaction.credits_change > 0}
|
||||
class:text-red-600={transaction.credits_change < 0}
|
||||
>
|
||||
{transaction.credits_change > 0 ? '+' : ''}{transaction.credits_change}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
text-align: center;
|
||||
}
|
||||
table {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user