Merge pull request #69 from jcreek/59-style-account-page

59 style account page
This commit is contained in:
Olly
2024-11-05 20:57:49 +00:00
committed by GitHub
3 changed files with 248 additions and 40 deletions
+91 -1
View File
@@ -480,6 +480,94 @@ const getProductById = async (productId: string) => {
return product as Product; return product as Product;
}; };
const getUserSubscriptions = async (userId: string) => {
try {
// Step 1: Retrieve subscription data
const { data: subscriptions, error } = await supabaseAdmin
.from('subscriptions')
.select('*')
.eq('user_id', userId);
if (error) {
console.error('Error retrieving subscriptions:', error.message);
throw new Error('Failed to fetch subscriptions');
}
// Step 2: Map through each subscription and fetch product and price details
const subscriptionDetails = await Promise.all(
subscriptions.map(async (sub) => {
// Fetch product and price details from Stripe
const product = await stripeClient.products.retrieve(sub.product_id);
const price = await stripeClient.prices.retrieve(sub.price_id);
const expiryDate = new Date(sub.current_period_end);
// Format output data
return {
productName: product.name,
amount: (price.unit_amount / 100).toFixed(2),
currency: price.currency.toUpperCase(),
interval: price.recurring?.interval || 'one-time',
expiryDate: expiryDate.toLocaleDateString('en-GB', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
}),
status: sub.status
};
})
);
return subscriptionDetails;
} catch (error) {
console.error('An error occurred while retrieving subscription details:', error);
throw new Error('Could not retrieve subscription details');
}
};
const getUserTransactions = async (userId: string) => {
try {
// Step 1: Retrieve the customer ID associated with the user
const { data, error } = await supabaseAdmin
.from('customers')
.select('stripe_customer_id')
.eq('id', userId)
.single();
if (error) {
console.error('Error retrieving user data:', error.message);
throw new Error('Failed to fetch user data');
}
const customerId = data?.stripe_customer_id;
if (!customerId) {
throw new Error('No Stripe customer ID found for this user');
}
// Step 2: Fetch charge transactions from Stripe
const charges = await stripeClient.charges.list({ customer: customerId });
// Step 3: Format transaction data
const transactions = charges.data.map((charge) => ({
amount: (charge.amount / 100).toFixed(2),
currency: charge.currency.toUpperCase(),
description: charge.description ?? 'No description provided',
status: charge.status,
created: new Date(charge.created * 1000).toLocaleDateString('en-GB', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
}),
receipt_url: charge.receipt_url
}));
return transactions;
} catch (error) {
console.error('An error occurred while retrieving transactions:', error);
throw new Error('Could not retrieve transactions');
}
};
export { export {
upsertProductRecord, upsertProductRecord,
upsertPriceRecord, upsertPriceRecord,
@@ -495,5 +583,7 @@ export {
getActiveProductsWithPrices, getActiveProductsWithPrices,
getProductById, getProductById,
upsertCustomerToSupabase, upsertCustomerToSupabase,
getStripeCustomerId getStripeCustomerId,
getUserSubscriptions,
getUserTransactions
}; };
+16 -6
View File
@@ -1,5 +1,6 @@
import { fail, redirect } from '@sveltejs/kit'; import { fail, redirect } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types'; import type { Actions, PageServerLoad } from './$types';
import { getUserSubscriptions, getUserTransactions } from '$lib/utils/supabase/admin';
export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession } }) => { export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession } }) => {
const { session } = await safeGetSession(); const { session } = await safeGetSession();
@@ -14,7 +15,11 @@ export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession
.eq('id', session.user.id) .eq('id', session.user.id)
.single(); .single();
return { session, user }; const subscriptions = await getUserSubscriptions(session.user.id);
const transactions = await getUserTransactions(session.user.id);
return { session, user, subscriptions, transactions };
}; };
export const actions: Actions = { export const actions: Actions = {
@@ -27,20 +32,25 @@ export const actions: Actions = {
if (!session) { if (!session) {
return fail(401, { name }); return fail(401, { name });
} }
const { error } = await supabase.from('users').update({ const { error } = await supabase
name: name, .from('users')
}).eq('id', session?.user.id); .update({
name: name
})
.eq('id', session?.user.id);
console.error(error); console.error('Failed to update user:', error);
if (error) { if (error) {
return fail(500, { return fail(500, {
name, name,
message: 'Failed to update profile',
error: error.message
}); });
} }
return { return {
name, name
}; };
}, },
signout: async ({ locals: { supabase, safeGetSession } }) => { signout: async ({ locals: { supabase, safeGetSession } }) => {
+141 -33
View File
@@ -5,7 +5,7 @@
export let data; export let data;
export let form; export let form;
let { session, supabase, user } = data; let { session, user, subscriptions, transactions } = data;
$: ({ session, supabase, user } = data); $: ({ session, supabase, user } = data);
let profileForm: HTMLFormElement; let profileForm: HTMLFormElement;
@@ -18,42 +18,150 @@
loading = false; loading = false;
}; };
}; };
const handleSignOut: SubmitFunction = () => {
loading = true;
return async ({ update }) => {
loading = false;
update();
};
};
</script> </script>
<div class="form-widget"> <div
<form class="account-page max-w-4xl mx-auto p-6 bg-base-100 rounded-lg shadow-md space-y-6 overflow-y-auto"
class="form-widget" >
method="post" <!-- Flex container for two columns -->
action="?/update" <div class="flex flex-col lg:flex-row gap-6">
use:enhance={handleSubmit} <!-- Left Column: Account Overview, Subscriptions, and Billing History -->
bind:this={profileForm} <div class="flex-1 space-y-6">
> <!-- Account Overview -->
<div> <section class="space-y-4">
<label for="name">Name</label> <h2 class="text-xl font-semibold">Account Overview</h2>
<input id="name" name="name" type="text" value={form?.name ?? name} /> <div class="form-control">
<label for="email" class="label font-medium">Email</label>
<input
id="email"
type="email"
class="input input-bordered"
value={session.user.email}
disabled
/>
</div>
<div class="text-sm text-gray-500">Signed in with magic link.</div>
<form
class="form-widget space-y-4"
method="post"
action="?/update"
use:enhance={handleSubmit}
bind:this={profileForm}
>
<div class="form-control">
<label for="name" class="label font-medium">Name</label>
<input
id="name"
name="name"
type="text"
value={form?.name ?? name}
class="input input-bordered w-full"
placeholder="Enter your name"
/>
</div>
<div>
<input
type="submit"
class="btn btn-primary w-full"
value={loading ? 'Loading...' : 'Update Name'}
disabled={loading}
/>
</div>
</form>
</section>
<!-- Active Subscriptions -->
<section class="space-y-4">
<h2 class="text-xl font-semibold">Subscriptions</h2>
{#if subscriptions.length === 0}
<p class="text-gray-600">You do not have any subscriptions.</p>
{:else}
<ul class="space-y-3">
{#each subscriptions as subscription}
<li class="p-4 border border-gray-200 rounded-md">
<div class="flex items-center justify-between">
<div>
<p class="font-semibold">{subscription.productName}</p>
<p class="text-sm text-gray-500">
Price: {subscription.amount}
{subscription.currency} / {subscription.interval}
</p>
<p class="text-sm text-gray-500">
Expiry Date: {subscription.expiryDate}
</p>
<p class="text-sm text-gray-500">
Status: {subscription.status}
</p>
</div>
<!-- <button class="btn btn-secondary btn-sm">Manage</button> -->
</div>
</li>
{/each}
</ul>
{/if}
</section>
<!-- Billing History -->
<section class="space-y-4">
<h2 class="text-xl font-semibold">Billing History</h2>
<ul class="space-y-3">
{#if transactions}
{#each transactions as transaction}
<li class="flex justify-between p-4 border border-gray-200 rounded-md">
<div>
<p class="text-sm text-gray-500">Date: {transaction.created}</p>
<p class="text-sm text-gray-500">
Amount: {transaction.amount}
{transaction.currency}
</p>
</div>
{#if transaction.receipt_url}
<a href={transaction.receipt_url} class="btn btn-outline btn-sm" target="_blank">
Download Receipt
</a>
{:else}
<span class="text-gray-500">Receipt unavailable</span>
{/if}
</li>
{/each}
{/if}
</ul>
</section>
</div> </div>
<div> <!-- Right Column: Notification Preferences and Feedback -->
<input <div class="flex-none w-full lg:w-64 space-y-6">
type="submit" <!-- Notification Preferences -->
class="button block primary" <section class="space-y-4">
value={loading ? 'Loading...' : 'Update'} <h2 class="text-xl font-semibold">Notification Preferences</h2>
disabled={loading} <form method="post" action="?/update_notifications" class="space-y-2">
/> <label class="flex items-center space-x-3">
</div> <input type="checkbox" class="checkbox checkbox-primary" />
</form> <!-- checked={user.notifications.productUpdates} -->
<span>Product Updates</span>
</label>
<label class="flex items-center space-x-3">
<input type="checkbox" class="checkbox checkbox-primary" />
<!-- checked={user.notifications.billingReminders} -->
<span>Billing Reminders</span>
</label>
<label class="flex items-center space-x-3">
<input type="checkbox" class="checkbox checkbox-primary" />
<!-- checked={user.notifications.promotions} -->
<span>Promotional Emails</span>
</label>
</form>
</section>
<form method="post" action="?/signout" use:enhance={handleSignOut}> <!-- Feedback Section -->
<div> <section class="space-y-4">
<button class="button block" disabled={loading}>Sign Out</button> <h2 class="text-xl font-semibold">Feedback</h2>
<p class="text-gray-600">
We'd love to hear your thoughts! Please feel free to provide any feedback.
</p>
<a href="/contact" class="btn btn-outline w-full">Provide Feedback</a>
</section>
</div> </div>
</form> </div>
</div> </div>