mirror of
https://github.com/jcreek/SvelteKitSaasBoilerplate.git
synced 2026-07-12 18:43:50 +00:00
feat(#76): Ensure pagination is only possible for valid pages
This commit is contained in:
@@ -625,6 +625,8 @@ const getUserTransactions = async (
|
||||
|
||||
const charges = await stripeClient.charges.list(params);
|
||||
|
||||
const hasNextPage = charges.has_more;
|
||||
|
||||
// Step 3: Format transaction data
|
||||
const transactions = charges.data.map((charge) => ({
|
||||
id: charge.id,
|
||||
@@ -640,7 +642,7 @@ const getUserTransactions = async (
|
||||
receipt_url: charge.receipt_url
|
||||
}));
|
||||
|
||||
return transactions;
|
||||
return { transactions, hasNextPage };
|
||||
} catch (error) {
|
||||
console.error('An error occurred while retrieving transactions:', error);
|
||||
throw new Error('Could not retrieve transactions');
|
||||
|
||||
@@ -5,6 +5,8 @@ import { getUserSubscriptions, getUserTransactions } from '$lib/utils/supabase/a
|
||||
import { requestAccountDeletion } from '$lib/utils/supabase/admin';
|
||||
|
||||
const transactionsPerPage = 5;
|
||||
let firstTransactionId: string;
|
||||
let hasMoreThanOnePage = false;
|
||||
|
||||
export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession } }) => {
|
||||
const { session } = await safeGetSession();
|
||||
@@ -21,9 +23,19 @@ export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession
|
||||
|
||||
const subscriptions = await getUserSubscriptions(session.user.id);
|
||||
|
||||
const transactions = await getUserTransactions(session.user.id, transactionsPerPage);
|
||||
const transactionsData = await getUserTransactions(session.user.id, transactionsPerPage);
|
||||
|
||||
return { session, user, subscriptions, transactions, pageSize: transactionsPerPage };
|
||||
const transactions = transactionsData.transactions;
|
||||
firstTransactionId = transactions[0]?.id;
|
||||
hasMoreThanOnePage = transactionsData.hasNextPage;
|
||||
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
subscriptions,
|
||||
transactions,
|
||||
pageSize: transactionsPerPage
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
@@ -84,25 +96,34 @@ export const actions: Actions = {
|
||||
paginate: async ({ request, locals: { safeGetSession } }) => {
|
||||
const formData = await request.formData();
|
||||
const pageSize = transactionsPerPage;
|
||||
const startingAfter = formData.get('startingAfter')?.toString() || undefined;
|
||||
const endingBefore = formData.get('endingBefore')?.toString() || undefined;
|
||||
|
||||
let startingAfter = formData.get('startingAfter')?.toString() || undefined;
|
||||
if (startingAfter === 'undefined') {
|
||||
startingAfter = undefined;
|
||||
}
|
||||
let endingBefore = formData.get('endingBefore')?.toString() || undefined;
|
||||
if (endingBefore === 'undefined') {
|
||||
endingBefore = undefined;
|
||||
}
|
||||
|
||||
const { session } = await safeGetSession();
|
||||
if (!session) return fail(401, { error: 'Not authenticated' });
|
||||
|
||||
try {
|
||||
const transactions = await getUserTransactions(
|
||||
const { transactions, hasNextPage } = await getUserTransactions(
|
||||
session.user.id,
|
||||
pageSize,
|
||||
startingAfter,
|
||||
endingBefore
|
||||
);
|
||||
|
||||
const serializableTransactions = transactions.map((transaction) => ({ ...transaction }));
|
||||
const isFirstPage = transactions[0].id === firstTransactionId;
|
||||
|
||||
const stringifiedTransactions: string = JSON.stringify(serializableTransactions);
|
||||
|
||||
return { stringifiedTransactions };
|
||||
return {
|
||||
transactions: JSON.stringify(transactions),
|
||||
hasNextPage: hasNextPage || (isFirstPage && hasMoreThanOnePage),
|
||||
isFirstPage
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch transactions', { userId: session.user.id, error });
|
||||
return fail(500, { error: 'Failed to load transactions' });
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
import type { SubmitFunction } from '@sveltejs/kit';
|
||||
|
||||
@@ -41,39 +40,21 @@
|
||||
};
|
||||
};
|
||||
|
||||
// Track pagination IDs for Stripe-based pagination
|
||||
let startingAfter: string | undefined = undefined;
|
||||
let endingBefore: string | undefined = undefined;
|
||||
let hasNextPage = true;
|
||||
let hasPreviousPage = false;
|
||||
|
||||
// Function to trigger pagination
|
||||
async function goToPage(direction: 'next' | 'previous') {
|
||||
const handlePaginationSubmit: SubmitFunction = () => {
|
||||
loading = true;
|
||||
|
||||
// Set up formData with appropriate IDs for pagination
|
||||
const formData = new FormData();
|
||||
if (direction === 'next' && transactions.length) {
|
||||
startingAfter = transactions[transactions.length - 1].id;
|
||||
formData.append('startingAfter', startingAfter);
|
||||
endingBefore = undefined; // Reset `endingBefore` for forward navigation
|
||||
} else if (direction === 'previous' && transactions.length) {
|
||||
endingBefore = transactions[0].id;
|
||||
formData.append('endingBefore', endingBefore);
|
||||
startingAfter = undefined; // Reset `startingAfter` for backward navigation
|
||||
}
|
||||
|
||||
// Fetch paginated transactions
|
||||
const response = await fetch('/account?/paginate', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// TODO - work out why the data comes back in such a weird format here
|
||||
transactions = JSON.parse(JSON.parse(result.data)[1]);
|
||||
|
||||
loading = false;
|
||||
}
|
||||
return async ({ result }) => {
|
||||
transactions = JSON.parse(result.data.transactions);
|
||||
hasNextPage = result.data.hasNextPage;
|
||||
hasPreviousPage = !result.data.isFirstPage;
|
||||
loading = false;
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -187,24 +168,45 @@
|
||||
<p class="text-gray-600">No transactions found.</p>
|
||||
{/if}
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
<div class="flex justify-center items-center space-x-2 mt-4">
|
||||
<button
|
||||
class="btn btn-outline"
|
||||
disabled={!transactions.length || loading}
|
||||
on:click={() => goToPage('previous')}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<!-- Pagination Form -->
|
||||
<form method="post" action="?/paginate" use:enhance={handlePaginationSubmit}>
|
||||
<div class="flex justify-center items-center space-x-2 mt-4">
|
||||
<input type="hidden" name="startingAfter" value={startingAfter} />
|
||||
<input type="hidden" name="endingBefore" value={endingBefore} />
|
||||
|
||||
<button
|
||||
class="btn btn-outline"
|
||||
disabled={!transactions.length || loading}
|
||||
on:click={() => goToPage('next')}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
name="direction"
|
||||
value="previous"
|
||||
class="btn btn-outline"
|
||||
disabled={!hasPreviousPage || loading}
|
||||
on:click={() => {
|
||||
if (transactions.length) {
|
||||
endingBefore = transactions[0].id;
|
||||
startingAfter = undefined;
|
||||
}
|
||||
}}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
name="direction"
|
||||
value="next"
|
||||
class="btn btn-outline"
|
||||
disabled={!hasNextPage || loading}
|
||||
on:click={() => {
|
||||
if (transactions.length) {
|
||||
startingAfter = transactions[transactions.length - 1].id;
|
||||
endingBefore = undefined;
|
||||
}
|
||||
}}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user