feat(#76): Ensure pagination is only possible for valid pages

This commit is contained in:
Josh Creek
2024-11-12 20:09:01 +00:00
parent c77dc6ac47
commit 5de84d2ec1
3 changed files with 81 additions and 56 deletions
+3 -1
View File
@@ -625,6 +625,8 @@ const getUserTransactions = async (
const charges = await stripeClient.charges.list(params); const charges = await stripeClient.charges.list(params);
const hasNextPage = charges.has_more;
// Step 3: Format transaction data // Step 3: Format transaction data
const transactions = charges.data.map((charge) => ({ const transactions = charges.data.map((charge) => ({
id: charge.id, id: charge.id,
@@ -640,7 +642,7 @@ const getUserTransactions = async (
receipt_url: charge.receipt_url receipt_url: charge.receipt_url
})); }));
return transactions; return { transactions, hasNextPage };
} catch (error) { } catch (error) {
console.error('An error occurred while retrieving transactions:', error); console.error('An error occurred while retrieving transactions:', error);
throw new Error('Could not retrieve transactions'); throw new Error('Could not retrieve transactions');
+30 -9
View File
@@ -5,6 +5,8 @@ import { getUserSubscriptions, getUserTransactions } from '$lib/utils/supabase/a
import { requestAccountDeletion } from '$lib/utils/supabase/admin'; import { requestAccountDeletion } from '$lib/utils/supabase/admin';
const transactionsPerPage = 5; const transactionsPerPage = 5;
let firstTransactionId: string;
let hasMoreThanOnePage = false;
export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession } }) => { export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession } }) => {
const { session } = await 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 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 = { export const actions: Actions = {
@@ -84,25 +96,34 @@ export const actions: Actions = {
paginate: async ({ request, locals: { safeGetSession } }) => { paginate: async ({ request, locals: { safeGetSession } }) => {
const formData = await request.formData(); const formData = await request.formData();
const pageSize = transactionsPerPage; 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(); const { session } = await safeGetSession();
if (!session) return fail(401, { error: 'Not authenticated' }); if (!session) return fail(401, { error: 'Not authenticated' });
try { try {
const transactions = await getUserTransactions( const { transactions, hasNextPage } = await getUserTransactions(
session.user.id, session.user.id,
pageSize, pageSize,
startingAfter, startingAfter,
endingBefore endingBefore
); );
const serializableTransactions = transactions.map((transaction) => ({ ...transaction })); const isFirstPage = transactions[0].id === firstTransactionId;
const stringifiedTransactions: string = JSON.stringify(serializableTransactions); return {
transactions: JSON.stringify(transactions),
return { stringifiedTransactions }; hasNextPage: hasNextPage || (isFirstPage && hasMoreThanOnePage),
isFirstPage
};
} catch (error) { } catch (error) {
logger.error('Failed to fetch transactions', { userId: session.user.id, error }); logger.error('Failed to fetch transactions', { userId: session.user.id, error });
return fail(500, { error: 'Failed to load transactions' }); return fail(500, { error: 'Failed to load transactions' });
+35 -33
View File
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte';
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import type { SubmitFunction } from '@sveltejs/kit'; import type { SubmitFunction } from '@sveltejs/kit';
@@ -41,39 +40,21 @@
}; };
}; };
// Track pagination IDs for Stripe-based pagination
let startingAfter: string | undefined = undefined; let startingAfter: string | undefined = undefined;
let endingBefore: string | undefined = undefined; let endingBefore: string | undefined = undefined;
let hasNextPage = true;
let hasPreviousPage = false;
// Function to trigger pagination const handlePaginationSubmit: SubmitFunction = () => {
async function goToPage(direction: 'next' | 'previous') {
loading = true; loading = true;
// Set up formData with appropriate IDs for pagination return async ({ result }) => {
const formData = new FormData(); transactions = JSON.parse(result.data.transactions);
if (direction === 'next' && transactions.length) { hasNextPage = result.data.hasNextPage;
startingAfter = transactions[transactions.length - 1].id; hasPreviousPage = !result.data.isFirstPage;
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; loading = false;
} };
};
</script> </script>
<div <div
@@ -187,24 +168,45 @@
<p class="text-gray-600">No transactions found.</p> <p class="text-gray-600">No transactions found.</p>
{/if} {/if}
<!-- Pagination Controls --> <!-- Pagination Form -->
<form method="post" action="?/paginate" use:enhance={handlePaginationSubmit}>
<div class="flex justify-center items-center space-x-2 mt-4"> <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 <button
type="submit"
name="direction"
value="previous"
class="btn btn-outline" class="btn btn-outline"
disabled={!transactions.length || loading} disabled={!hasPreviousPage || loading}
on:click={() => goToPage('previous')} on:click={() => {
if (transactions.length) {
endingBefore = transactions[0].id;
startingAfter = undefined;
}
}}
> >
Previous Previous
</button> </button>
<button <button
type="submit"
name="direction"
value="next"
class="btn btn-outline" class="btn btn-outline"
disabled={!transactions.length || loading} disabled={!hasNextPage || loading}
on:click={() => goToPage('next')} on:click={() => {
if (transactions.length) {
startingAfter = transactions[transactions.length - 1].id;
endingBefore = undefined;
}
}}
> >
Next Next
</button> </button>
</div> </div>
</form>
</section> </section>
</div> </div>