mirror of
https://github.com/jcreek/SvelteKitSaasBoilerplate.git
synced 2026-07-13 02:53:50 +00:00
Merge pull request #41 from jcreek/6-enable-proper-pagination-for-the-products-page-ensuring-that-products-come-from-the-database-not-stripe
6 enable proper pagination for the products page ensuring that products come from the database not stripe
This commit is contained in:
+2
-1
@@ -49,7 +49,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@supabase/ssr": "^0.4.0",
|
"@supabase/ssr": "^0.4.0",
|
||||||
"@supabase/supabase-js": "^2.44.2",
|
"@supabase/supabase-js": "^2.44.2",
|
||||||
"stripe": "^15.4.0"
|
"stripe": "^15.4.0",
|
||||||
|
"ts-debounce": "^4.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.13.0"
|
"node": ">=18.13.0"
|
||||||
|
|||||||
Generated
+8
@@ -17,6 +17,9 @@ importers:
|
|||||||
stripe:
|
stripe:
|
||||||
specifier: ^15.4.0
|
specifier: ^15.4.0
|
||||||
version: 15.12.0
|
version: 15.12.0
|
||||||
|
ts-debounce:
|
||||||
|
specifier: ^4.0.0
|
||||||
|
version: 4.0.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@sveltejs/adapter-auto':
|
'@sveltejs/adapter-auto':
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
@@ -3051,6 +3054,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.2.0'
|
typescript: '>=4.2.0'
|
||||||
|
|
||||||
|
ts-debounce@4.0.0:
|
||||||
|
resolution: {integrity: sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==}
|
||||||
|
|
||||||
ts-interface-checker@0.1.13:
|
ts-interface-checker@0.1.13:
|
||||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||||
|
|
||||||
@@ -6580,6 +6586,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.6.2
|
typescript: 5.6.2
|
||||||
|
|
||||||
|
ts-debounce@4.0.0: {}
|
||||||
|
|
||||||
ts-interface-checker@0.1.13: {}
|
ts-interface-checker@0.1.13: {}
|
||||||
|
|
||||||
tslib@2.7.0: {}
|
tslib@2.7.0: {}
|
||||||
|
|||||||
@@ -409,6 +409,38 @@ const getUserCredits = async (userId: string) => {
|
|||||||
return creditData.credits_remaining;
|
return creditData.credits_remaining;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ProductWithPrices = Product & {
|
||||||
|
prices: Price[];
|
||||||
|
actualPrice?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActiveProductsWithPrices = async (limit = 10, offset = 0) => {
|
||||||
|
const {
|
||||||
|
data: products,
|
||||||
|
error,
|
||||||
|
count
|
||||||
|
} = await supabaseAdmin
|
||||||
|
.from('products')
|
||||||
|
.select('*, prices(*)', { count: 'exact' })
|
||||||
|
.eq('active', true)
|
||||||
|
.order('name', { ascending: true })
|
||||||
|
.range(offset, offset + limit - 1);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(`Error fetching products: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const product of products as ProductWithPrices[]) {
|
||||||
|
if (product.prices && product.prices.length > 0) {
|
||||||
|
// Assuming the default price is the first one
|
||||||
|
const defaultPrice = product.prices[0];
|
||||||
|
product.actualPrice = defaultPrice.unit_amount! / 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { products: products as ProductWithPrices[], count };
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
upsertProductRecord,
|
upsertProductRecord,
|
||||||
upsertPriceRecord,
|
upsertPriceRecord,
|
||||||
@@ -420,5 +452,6 @@ export {
|
|||||||
hasProductAccess,
|
hasProductAccess,
|
||||||
addCredits,
|
addCredits,
|
||||||
deductCredits,
|
deductCredits,
|
||||||
getUserCredits
|
getUserCredits,
|
||||||
|
getActiveProductsWithPrices
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,108 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ProductsListItem from '$lib/components/products/ProductsListItem.svelte';
|
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
let products = [] as any;
|
import { debounce } from 'ts-debounce';
|
||||||
|
import ProductsListItem from '$lib/components/products/ProductsListItem.svelte';
|
||||||
|
|
||||||
|
let products = [] as any[];
|
||||||
let enablePagination = false;
|
let enablePagination = false;
|
||||||
|
let totalPages = 1;
|
||||||
|
let currentPage = 1;
|
||||||
|
const limit = 20;
|
||||||
|
let isLoading = false;
|
||||||
|
let errorMessage = '';
|
||||||
|
|
||||||
onMount(async () => {
|
// Debounced fetchProducts function to prevent rapid requests
|
||||||
const response = await fetch('/products');
|
const fetchProducts = debounce(async (page: number) => {
|
||||||
const responseJson = await response.json();
|
isLoading = true;
|
||||||
products = responseJson.data;
|
errorMessage = '';
|
||||||
if (responseJson.has_more) {
|
|
||||||
enablePagination = true;
|
// Ensure page number is within valid range
|
||||||
|
if (page < 1) {
|
||||||
|
page = 1;
|
||||||
|
} else if (page > totalPages) {
|
||||||
|
page = totalPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/products?limit=${limit}&offset=${offset}`);
|
||||||
|
const responseJson = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
products = responseJson.products;
|
||||||
|
const count = responseJson.count;
|
||||||
|
|
||||||
|
totalPages = Math.ceil(count / limit);
|
||||||
|
enablePagination = totalPages > 1;
|
||||||
|
currentPage = page;
|
||||||
|
} else {
|
||||||
|
throw new Error(responseJson.error || 'Failed to load products.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
errorMessage = error.message || 'Failed to load products.';
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
}, 300); // 300ms debounce delay
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
fetchProducts(currentPage);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Generate pagination buttons with ellipsis for large numbers of pages
|
||||||
|
function getPageNumbers() {
|
||||||
|
const maxButtons = 5;
|
||||||
|
const pageNumbers = [];
|
||||||
|
|
||||||
|
if (totalPages <= maxButtons) {
|
||||||
|
// Show all pages as there aren't many
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
pageNumbers.push(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let startPage = Math.max(currentPage - 2, 1);
|
||||||
|
let endPage = Math.min(currentPage + 2, totalPages);
|
||||||
|
|
||||||
|
if (currentPage <= 3) {
|
||||||
|
startPage = 1;
|
||||||
|
endPage = maxButtons;
|
||||||
|
} else if (currentPage >= totalPages - 2) {
|
||||||
|
startPage = totalPages - (maxButtons - 1);
|
||||||
|
endPage = totalPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add first page and ellipsis if needed
|
||||||
|
if (startPage > 1) {
|
||||||
|
pageNumbers.push(1);
|
||||||
|
if (startPage > 2) {
|
||||||
|
pageNumbers.push('...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add page numbers
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
pageNumbers.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add ellipsis and last page if needed
|
||||||
|
if (endPage < totalPages) {
|
||||||
|
if (endPage < totalPages - 1) {
|
||||||
|
pageNumbers.push('...');
|
||||||
|
}
|
||||||
|
pageNumbers.push(totalPages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageNumbers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageClick(pageNumber: number | string) {
|
||||||
|
if (typeof pageNumber === 'number' && pageNumber !== currentPage) {
|
||||||
|
fetchProducts(pageNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="py-24">
|
<section class="py-24">
|
||||||
@@ -22,20 +112,67 @@
|
|||||||
>
|
>
|
||||||
Available Products
|
Available Products
|
||||||
</h2>
|
</h2>
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
||||||
{#each products as product (product.id)}
|
{#if isLoading}
|
||||||
<ProductsListItem {product} />
|
<p class="loading">Loading products...</p>
|
||||||
{/each}
|
{:else if errorMessage}
|
||||||
</div>
|
<p class="error">{errorMessage}</p>
|
||||||
{#if enablePagination}
|
{:else}
|
||||||
<div class="flex justify-center mt-12">
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
<div class="join">
|
{#each products as product (product.id)}
|
||||||
<button class="join-item btn">1</button>
|
<ProductsListItem {product} />
|
||||||
<button class="join-item btn btn-active">2</button>
|
{/each}
|
||||||
<button class="join-item btn">3</button>
|
|
||||||
<button class="join-item btn">4</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if enablePagination}
|
||||||
|
<div class="flex justify-center mt-12">
|
||||||
|
<div class="join">
|
||||||
|
<!-- Previous Button -->
|
||||||
|
<button
|
||||||
|
class="join-item btn"
|
||||||
|
on:click={() => fetchProducts(currentPage - 1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Page Number Buttons -->
|
||||||
|
{#each getPageNumbers() as pageNumber}
|
||||||
|
{#if pageNumber === '...'}
|
||||||
|
<span class="join-item btn">...</span>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="join-item btn {currentPage === pageNumber ? 'btn-active' : ''}"
|
||||||
|
on:click={() => handlePageClick(pageNumber)}
|
||||||
|
>
|
||||||
|
{pageNumber}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<!-- Next Button -->
|
||||||
|
<button
|
||||||
|
class="join-item btn"
|
||||||
|
on:click={() => fetchProducts(currentPage + 1)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import { stripe as stripeClient } from '$lib/utils/stripe';
|
import { getActiveProductsWithPrices } from '$lib/utils/supabase/admin';
|
||||||
|
|
||||||
export const GET = async () => {
|
|
||||||
const products = await stripeClient.products.list({
|
|
||||||
limit: 100
|
|
||||||
});
|
|
||||||
|
|
||||||
|
export const GET = async ({ url }) => {
|
||||||
try {
|
try {
|
||||||
// Get the price for each product
|
const limitParam = url.searchParams.get('limit');
|
||||||
for (const product of products.data) {
|
const offsetParam = url.searchParams.get('offset');
|
||||||
const price = await stripeClient.prices.retrieve(product.default_price as string);
|
|
||||||
if (price) {
|
const limit = limitParam ? parseInt(limitParam, 10) : 10;
|
||||||
product.actualPrice = price.unit_amount / 100;
|
const offset = offsetParam ? parseInt(offsetParam, 10) : 0;
|
||||||
}
|
|
||||||
}
|
const { products, count } = await getActiveProductsWithPrices(limit, offset);
|
||||||
|
|
||||||
|
return json({ products, count });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
return json({ error: error.message }, { status: 500 });
|
||||||
}
|
}
|
||||||
return json(products);
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user